From a73a9664c9f99e21b797b75db0a62e28b5475a3e Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Tue, 7 Feb 2023 10:17:17 +0100 Subject: [PATCH 01/91] Initial commit --- .gitignore | 10 +++ LICENSE | 201 +++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 + 3 files changed, 213 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..088ba6b --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e8a5f34 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# blade-ink-rs +Inkle Ink runtime implementation in Rust From 56763254f16d06d876bdcb6056e6a52597c6ab27 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Tue, 7 Feb 2023 10:30:17 +0100 Subject: [PATCH 02/91] Update README.md --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index e8a5f34..0e46bb2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,25 @@ # blade-ink-rs Inkle Ink runtime implementation in Rust + +Currently under development. This is the implementation status: + +- [ ] Loading .json file +- [ ] Show plain lines (no logic nor choices) +- [ ] Choices +- [ ] Knots and Stitches +- [ ] Diverts +- [ ] Variable Text +- [ ] Conditional Text +- [ ] Game Queries and Functions +- [ ] Diverts +- [ ] Nested flows +- [ ] Variables and Logic +- [ ] Conditional blocks (if/else) +- [ ] Variables and Logic +- [ ] Temporary Variables +- [ ] Functions +- [ ] Tunnels +- [ ] Threads +- [ ] Lists + + From 06f187d31ffcea6ef6e70f95fb46d7d1bb644ba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Tue, 7 Feb 2023 10:43:54 +0000 Subject: [PATCH 03/91] Initial project structure --- .devcontainer/devcontainer.json | 22 ++++++++++++++++ .gitignore | 2 ++ .vscode/launch.json | 45 +++++++++++++++++++++++++++++++++ Cargo.toml | 16 ++++++++++++ src/bin/console-player.rs | 5 ++++ src/lib.rs | 14 ++++++++++ 6 files changed, 104 insertions(+) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .vscode/launch.json create mode 100644 Cargo.toml create mode 100644 src/bin/console-player.rs create mode 100644 src/lib.rs diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..b5b68f1 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,22 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/rust +{ + "name": "Rust", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/rust:0-1-bullseye" + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "rustc --version", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.gitignore b/.gitignore index 088ba6b..5dd6ff5 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk + +.DS_Store diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..065edd6 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,45 @@ +{ + // Use IntelliSense para saber los atributos posibles. + // Mantenga el puntero para ver las descripciones de los existentes atributos. + // Para más información, visite: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'console-player'", + "cargo": { + "args": [ + "build", + "--bin=console-player", + "--package=blade-ink" + ], + "filter": { + "name": "console-player", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'console-player'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=console-player", + "--package=blade-ink" + ], + "filter": { + "name": "console-player", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d353bbd --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "blade-ink" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "bladeink" +path = "src/lib.rs" + +[[bin]] +name = "console-player" +path = "src/bin/console-player.rs" + +[dependencies] diff --git a/src/bin/console-player.rs b/src/bin/console-player.rs new file mode 100644 index 0000000..85aad77 --- /dev/null +++ b/src/bin/console-player.rs @@ -0,0 +1,5 @@ +fn main() { + println!("Hello, world!"); +} + + diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..7d12d9a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} From 67e0b7d83c71c5607b1c4107bee1c08451200527 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Fri, 10 Feb 2023 20:15:57 +0000 Subject: [PATCH 04/91] Story reads the version from json --- Cargo.toml | 3 + examples/inkfiles/basictext/oneline.ink | 1 + examples/inkfiles/basictext/oneline.ink.json | 1 + examples/inkfiles/basictext/twolines.ink | 2 + examples/inkfiles/basictext/twolines.ink.json | 1 + .../inkfiles/choices/conditional-choice.ink | 8 +++ .../choices/conditional-choice.ink.json | 1 + examples/inkfiles/choices/divert-choice.ink | 9 +++ .../inkfiles/choices/divert-choice.ink.json | 1 + examples/inkfiles/choices/fallback-choice.ink | 9 +++ .../inkfiles/choices/fallback-choice.ink.json | 1 + examples/inkfiles/choices/label-flow.ink | 15 ++++ examples/inkfiles/choices/label-flow.ink.json | 1 + .../inkfiles/choices/label-scope-error.ink | 9 +++ .../choices/label-scope-error.ink.json | 1 + examples/inkfiles/choices/label-scope.ink | 10 +++ .../inkfiles/choices/label-scope.ink.json | 1 + examples/inkfiles/choices/mixed-choice.ink | 5 ++ .../inkfiles/choices/mixed-choice.ink.json | 1 + examples/inkfiles/choices/multi-choice.ink | 7 ++ .../inkfiles/choices/multi-choice.ink.json | 1 + examples/inkfiles/choices/no-choice-text.ink | 3 + .../inkfiles/choices/no-choice-text.ink.json | 1 + examples/inkfiles/choices/one.ink | 4 ++ examples/inkfiles/choices/one.ink.json | 1 + examples/inkfiles/choices/single-choice.ink | 4 ++ .../inkfiles/choices/single-choice.ink.json | 1 + examples/inkfiles/choices/sticky-choice.ink | 8 +++ .../inkfiles/choices/sticky-choice.ink.json | 1 + examples/inkfiles/choices/suppress-choice.ink | 4 ++ .../inkfiles/choices/suppress-choice.ink.json | 1 + examples/inkfiles/choices/varying-choice.ink | 9 +++ .../inkfiles/choices/varying-choice.ink.json | 1 + examples/inkfiles/conditional/condopt.ink | 18 +++++ .../inkfiles/conditional/condopt.ink.json | 1 + examples/inkfiles/conditional/condtext.ink | 17 +++++ .../inkfiles/conditional/condtext.ink.json | 1 + examples/inkfiles/conditional/cycle.ink | 9 +++ examples/inkfiles/conditional/cycle.ink.json | 1 + .../inkfiles/conditional/ifelse-ext-text1.ink | 13 ++++ .../conditional/ifelse-ext-text1.ink.json | 1 + .../inkfiles/conditional/ifelse-ext-text2.ink | 13 ++++ .../conditional/ifelse-ext-text2.ink.json | 1 + .../inkfiles/conditional/ifelse-ext-text3.ink | 13 ++++ .../conditional/ifelse-ext-text3.ink.json | 1 + examples/inkfiles/conditional/ifelse-ext.ink | 12 ++++ .../inkfiles/conditional/ifelse-ext.ink.json | 1 + examples/inkfiles/conditional/ifelse.ink | 9 +++ examples/inkfiles/conditional/ifelse.ink.json | 1 + examples/inkfiles/conditional/iffalse.ink | 7 ++ .../inkfiles/conditional/iffalse.ink.json | 1 + examples/inkfiles/conditional/iftrue.ink | 7 ++ examples/inkfiles/conditional/iftrue.ink.json | 1 + .../inkfiles/conditional/multiline-choice.ink | 12 ++++ .../conditional/multiline-choice.ink.json | 1 + .../inkfiles/conditional/multiline-divert.ink | 16 +++++ .../conditional/multiline-divert.ink.json | 1 + examples/inkfiles/conditional/multiline.ink | 11 +++ .../inkfiles/conditional/multiline.ink.json | 1 + examples/inkfiles/conditional/once.ink | 8 +++ examples/inkfiles/conditional/once.ink.json | 1 + examples/inkfiles/conditional/shuffle.ink | 9 +++ .../inkfiles/conditional/shuffle.ink.json | 1 + .../inkfiles/conditional/shuffle_once.ink | 8 +++ .../conditional/shuffle_once.ink.json | 1 + .../inkfiles/conditional/shuffle_stopping.ink | 9 +++ .../conditional/shuffle_stopping.ink.json | 1 + examples/inkfiles/conditional/stopping.ink | 9 +++ .../inkfiles/conditional/stopping.ink.json | 1 + .../inkfiles/divert/complex-branching.ink | 25 +++++++ .../divert/complex-branching.ink.json | 1 + examples/inkfiles/divert/divert-on-choice.ink | 7 ++ .../inkfiles/divert/divert-on-choice.ink.json | 1 + examples/inkfiles/divert/invisible-divert.ink | 5 ++ .../inkfiles/divert/invisible-divert.ink.json | 1 + examples/inkfiles/divert/simple-divert.ink | 6 ++ .../inkfiles/divert/simple-divert.ink.json | 1 + examples/inkfiles/function/complex-func1.ink | 12 ++++ .../inkfiles/function/complex-func1.ink.json | 1 + examples/inkfiles/function/complex-func2.ink | 17 +++++ .../inkfiles/function/complex-func2.ink.json | 1 + examples/inkfiles/function/complex-func3.ink | 27 +++++++ .../inkfiles/function/complex-func3.ink.json | 1 + .../evaluating-function-variablestate-bug.ink | 22 ++++++ ...uating-function-variablestate-bug.ink.json | 1 + examples/inkfiles/function/func-basic.ink | 7 ++ .../inkfiles/function/func-basic.ink.json | 1 + examples/inkfiles/function/func-inline.ink | 5 ++ .../inkfiles/function/func-inline.ink.json | 1 + examples/inkfiles/function/func-none.ink | 7 ++ examples/inkfiles/function/func-none.ink.json | 1 + examples/inkfiles/function/rnd-func.ink | 6 ++ examples/inkfiles/function/rnd-func.ink.json | 1 + examples/inkfiles/function/setvar-func.ink | 7 ++ .../inkfiles/function/setvar-func.ink.json | 1 + examples/inkfiles/function/test-error.ink | 9 +++ .../inkfiles/function/test-error.ink.json | 1 + examples/inkfiles/gather/complex-flow.ink | 19 +++++ .../inkfiles/gather/complex-flow.ink.json | 1 + examples/inkfiles/gather/deep-nesting.ink | 9 +++ .../inkfiles/gather/deep-nesting.ink.json | 1 + examples/inkfiles/gather/gather-basic.ink | 9 +++ .../inkfiles/gather/gather-basic.ink.json | 1 + examples/inkfiles/gather/gather-chain.ink | 10 +++ .../inkfiles/gather/gather-chain.ink.json | 1 + examples/inkfiles/gather/nested-flow.ink | 9 +++ examples/inkfiles/gather/nested-flow.ink.json | 1 + examples/inkfiles/gather/nested-gather.ink | 14 ++++ .../inkfiles/gather/nested-gather.ink.json | 1 + examples/inkfiles/glue/glue-with-divert.ink | 11 +++ .../inkfiles/glue/glue-with-divert.ink.json | 1 + .../glue/left-right-glue-matching.ink | 8 +++ .../glue/left-right-glue-matching.ink.json | 1 + examples/inkfiles/glue/simple-glue.ink | 3 + examples/inkfiles/glue/simple-glue.ink.json | 1 + examples/inkfiles/glue/testbugfix1.ink | 8 +++ examples/inkfiles/glue/testbugfix1.ink.json | 1 + examples/inkfiles/glue/testbugfix2.ink | 7 ++ examples/inkfiles/glue/testbugfix2.ink.json | 1 + examples/inkfiles/knot/multi-line.ink | 3 + examples/inkfiles/knot/multi-line.ink.json | 1 + examples/inkfiles/knot/param-floats.ink | 8 +++ examples/inkfiles/knot/param-floats.ink.json | 1 + examples/inkfiles/knot/param-ints.ink | 8 +++ examples/inkfiles/knot/param-ints.ink.json | 1 + examples/inkfiles/knot/param-multi.ink | 8 +++ examples/inkfiles/knot/param-multi.ink.json | 1 + examples/inkfiles/knot/param-recurse.ink | 14 ++++ examples/inkfiles/knot/param-recurse.ink.json | 1 + examples/inkfiles/knot/param-strings.ink | 8 +++ examples/inkfiles/knot/param-strings.ink.json | 1 + examples/inkfiles/knot/param-vars.ink | 11 +++ examples/inkfiles/knot/param-vars.ink.json | 1 + examples/inkfiles/knot/single-line.ink | 1 + examples/inkfiles/knot/single-line.ink.json | 1 + examples/inkfiles/knot/strip-empty-lines.ink | 4 ++ .../inkfiles/knot/strip-empty-lines.ink.json | 1 + examples/inkfiles/lists/basic-operations.ink | 8 +++ .../inkfiles/lists/basic-operations.ink.json | 1 + .../inkfiles/lists/bug-adding-element.ink | 14 ++++ .../lists/bug-adding-element.ink.json | 1 + .../empty-list-origin-after-assignment.ink | 3 + ...mpty-list-origin-after-assignment.ink.json | 1 + examples/inkfiles/lists/empty-list-origin.ink | 2 + .../inkfiles/lists/empty-list-origin.ink.json | 1 + examples/inkfiles/lists/list-mixed-items.ink | 4 ++ .../inkfiles/lists/list-mixed-items.ink.json | 1 + examples/inkfiles/lists/list-range.ink | 10 +++ examples/inkfiles/lists/list-range.ink.json | 1 + examples/inkfiles/lists/list-save-load.ink | 12 ++++ .../inkfiles/lists/list-save-load.ink.json | 1 + .../inkfiles/lists/more-list-operations.ink | 13 ++++ .../lists/more-list-operations.ink.json | 1 + examples/inkfiles/misc/issue15.ink | 24 +++++++ examples/inkfiles/misc/issue15.ink.json | 1 + .../runtime/external-function-0-arg.ink | 9 +++ .../runtime/external-function-0-arg.ink.json | 1 + .../runtime/external-function-1-arg.ink | 9 +++ .../runtime/external-function-1-arg.ink.json | 1 + .../runtime/external-function-2-arg.ink | 7 ++ .../runtime/external-function-2-arg.ink.json | 1 + .../runtime/external-function-3-arg.ink | 9 +++ .../runtime/external-function-3-arg.ink.json | 1 + examples/inkfiles/runtime/jump-knot.ink | 12 ++++ examples/inkfiles/runtime/jump-knot.ink.json | 1 + examples/inkfiles/runtime/jump-stitch.ink | 20 ++++++ .../inkfiles/runtime/jump-stitch.ink.json | 1 + examples/inkfiles/runtime/load-save.ink | 28 ++++++++ examples/inkfiles/runtime/load-save.ink.json | 1 + .../inkfiles/runtime/multiflow-basics.ink | 9 +++ .../runtime/multiflow-basics.ink.json | 1 + .../runtime/multiflow-saveloadthreads.ink | 30 ++++++++ .../multiflow-saveloadthreads.ink.json | 1 + .../inkfiles/runtime/read-visit-counts.ink | 17 +++++ .../runtime/read-visit-counts.ink.json | 1 + examples/inkfiles/runtime/saving-loading.ink | 11 +++ .../inkfiles/runtime/saving-loading.ink.json | 1 + .../inkfiles/runtime/set-get-variables.ink | 17 +++++ .../runtime/set-get-variables.ink.json | 1 + .../inkfiles/runtime/variable-observers.ink | 9 +++ .../runtime/variable-observers.ink.json | 1 + examples/inkfiles/stitch/auto-stitch.ink | 13 ++++ examples/inkfiles/stitch/auto-stitch.ink.json | 1 + examples/inkfiles/stitch/manual-stitch.ink | 15 ++++ .../inkfiles/stitch/manual-stitch.ink.json | 1 + examples/inkfiles/tags/tags.ink | 16 +++++ examples/inkfiles/tags/tags.ink.json | 1 + examples/inkfiles/tags/tagsDynamicContent.ink | 1 + .../inkfiles/tags/tagsDynamicContent.ink.json | 1 + examples/inkfiles/tags/tagsInChoice.ink | 1 + examples/inkfiles/tags/tagsInChoice.ink.json | 1 + examples/inkfiles/tags/tagsInSeq.ink | 4 ++ examples/inkfiles/tags/tagsInSeq.ink.json | 1 + examples/inkfiles/threads/thread-bug.ink | 14 ++++ examples/inkfiles/threads/thread-bug.ink.json | 1 + .../tunnel-onwards-divert-override.ink | 10 +++ .../tunnel-onwards-divert-override.ink.json | 1 + examples/inkfiles/variable/var-divert.ink | 12 ++++ .../inkfiles/variable/var-divert.ink.json | 1 + examples/inkfiles/variable/varcalc.ink | 14 ++++ examples/inkfiles/variable/varcalc.ink.json | 1 + .../variable/variable-declaration.ink | 4 ++ .../variable/variable-declaration.ink.json | 1 + examples/inkfiles/variable/varstringinc.ink | 6 ++ .../inkfiles/variable/varstringinc.ink.json | 1 + examples/inkfiles/variabletext/cycle.ink | 4 ++ examples/inkfiles/variabletext/cycle.ink.json | 1 + .../inkfiles/variabletext/empty-elements.ink | 4 ++ .../variabletext/empty-elements.ink.json | 1 + .../inkfiles/variabletext/list-in-choice.ink | 5 ++ .../variabletext/list-in-choice.ink.json | 1 + examples/inkfiles/variabletext/once.ink | 4 ++ examples/inkfiles/variabletext/once.ink.json | 1 + examples/inkfiles/variabletext/sequence.ink | 5 ++ .../inkfiles/variabletext/sequence.ink.json | 1 + src/json_serialization.rs | 15 ++++ src/lib.rs | 16 +---- src/story.rs | 72 +++++++++++++++++++ 218 files changed, 1249 insertions(+), 14 deletions(-) create mode 100644 examples/inkfiles/basictext/oneline.ink create mode 100644 examples/inkfiles/basictext/oneline.ink.json create mode 100644 examples/inkfiles/basictext/twolines.ink create mode 100644 examples/inkfiles/basictext/twolines.ink.json create mode 100644 examples/inkfiles/choices/conditional-choice.ink create mode 100644 examples/inkfiles/choices/conditional-choice.ink.json create mode 100644 examples/inkfiles/choices/divert-choice.ink create mode 100644 examples/inkfiles/choices/divert-choice.ink.json create mode 100644 examples/inkfiles/choices/fallback-choice.ink create mode 100644 examples/inkfiles/choices/fallback-choice.ink.json create mode 100644 examples/inkfiles/choices/label-flow.ink create mode 100644 examples/inkfiles/choices/label-flow.ink.json create mode 100644 examples/inkfiles/choices/label-scope-error.ink create mode 100644 examples/inkfiles/choices/label-scope-error.ink.json create mode 100644 examples/inkfiles/choices/label-scope.ink create mode 100644 examples/inkfiles/choices/label-scope.ink.json create mode 100644 examples/inkfiles/choices/mixed-choice.ink create mode 100644 examples/inkfiles/choices/mixed-choice.ink.json create mode 100644 examples/inkfiles/choices/multi-choice.ink create mode 100644 examples/inkfiles/choices/multi-choice.ink.json create mode 100644 examples/inkfiles/choices/no-choice-text.ink create mode 100644 examples/inkfiles/choices/no-choice-text.ink.json create mode 100644 examples/inkfiles/choices/one.ink create mode 100644 examples/inkfiles/choices/one.ink.json create mode 100644 examples/inkfiles/choices/single-choice.ink create mode 100644 examples/inkfiles/choices/single-choice.ink.json create mode 100644 examples/inkfiles/choices/sticky-choice.ink create mode 100644 examples/inkfiles/choices/sticky-choice.ink.json create mode 100644 examples/inkfiles/choices/suppress-choice.ink create mode 100644 examples/inkfiles/choices/suppress-choice.ink.json create mode 100644 examples/inkfiles/choices/varying-choice.ink create mode 100644 examples/inkfiles/choices/varying-choice.ink.json create mode 100644 examples/inkfiles/conditional/condopt.ink create mode 100644 examples/inkfiles/conditional/condopt.ink.json create mode 100644 examples/inkfiles/conditional/condtext.ink create mode 100644 examples/inkfiles/conditional/condtext.ink.json create mode 100644 examples/inkfiles/conditional/cycle.ink create mode 100644 examples/inkfiles/conditional/cycle.ink.json create mode 100644 examples/inkfiles/conditional/ifelse-ext-text1.ink create mode 100644 examples/inkfiles/conditional/ifelse-ext-text1.ink.json create mode 100644 examples/inkfiles/conditional/ifelse-ext-text2.ink create mode 100644 examples/inkfiles/conditional/ifelse-ext-text2.ink.json create mode 100644 examples/inkfiles/conditional/ifelse-ext-text3.ink create mode 100644 examples/inkfiles/conditional/ifelse-ext-text3.ink.json create mode 100644 examples/inkfiles/conditional/ifelse-ext.ink create mode 100644 examples/inkfiles/conditional/ifelse-ext.ink.json create mode 100644 examples/inkfiles/conditional/ifelse.ink create mode 100644 examples/inkfiles/conditional/ifelse.ink.json create mode 100644 examples/inkfiles/conditional/iffalse.ink create mode 100644 examples/inkfiles/conditional/iffalse.ink.json create mode 100644 examples/inkfiles/conditional/iftrue.ink create mode 100644 examples/inkfiles/conditional/iftrue.ink.json create mode 100644 examples/inkfiles/conditional/multiline-choice.ink create mode 100644 examples/inkfiles/conditional/multiline-choice.ink.json create mode 100644 examples/inkfiles/conditional/multiline-divert.ink create mode 100644 examples/inkfiles/conditional/multiline-divert.ink.json create mode 100644 examples/inkfiles/conditional/multiline.ink create mode 100644 examples/inkfiles/conditional/multiline.ink.json create mode 100644 examples/inkfiles/conditional/once.ink create mode 100644 examples/inkfiles/conditional/once.ink.json create mode 100644 examples/inkfiles/conditional/shuffle.ink create mode 100644 examples/inkfiles/conditional/shuffle.ink.json create mode 100644 examples/inkfiles/conditional/shuffle_once.ink create mode 100644 examples/inkfiles/conditional/shuffle_once.ink.json create mode 100644 examples/inkfiles/conditional/shuffle_stopping.ink create mode 100644 examples/inkfiles/conditional/shuffle_stopping.ink.json create mode 100644 examples/inkfiles/conditional/stopping.ink create mode 100644 examples/inkfiles/conditional/stopping.ink.json create mode 100644 examples/inkfiles/divert/complex-branching.ink create mode 100644 examples/inkfiles/divert/complex-branching.ink.json create mode 100644 examples/inkfiles/divert/divert-on-choice.ink create mode 100644 examples/inkfiles/divert/divert-on-choice.ink.json create mode 100644 examples/inkfiles/divert/invisible-divert.ink create mode 100644 examples/inkfiles/divert/invisible-divert.ink.json create mode 100644 examples/inkfiles/divert/simple-divert.ink create mode 100644 examples/inkfiles/divert/simple-divert.ink.json create mode 100644 examples/inkfiles/function/complex-func1.ink create mode 100644 examples/inkfiles/function/complex-func1.ink.json create mode 100644 examples/inkfiles/function/complex-func2.ink create mode 100644 examples/inkfiles/function/complex-func2.ink.json create mode 100644 examples/inkfiles/function/complex-func3.ink create mode 100644 examples/inkfiles/function/complex-func3.ink.json create mode 100644 examples/inkfiles/function/evaluating-function-variablestate-bug.ink create mode 100644 examples/inkfiles/function/evaluating-function-variablestate-bug.ink.json create mode 100644 examples/inkfiles/function/func-basic.ink create mode 100644 examples/inkfiles/function/func-basic.ink.json create mode 100644 examples/inkfiles/function/func-inline.ink create mode 100644 examples/inkfiles/function/func-inline.ink.json create mode 100644 examples/inkfiles/function/func-none.ink create mode 100644 examples/inkfiles/function/func-none.ink.json create mode 100644 examples/inkfiles/function/rnd-func.ink create mode 100644 examples/inkfiles/function/rnd-func.ink.json create mode 100644 examples/inkfiles/function/setvar-func.ink create mode 100644 examples/inkfiles/function/setvar-func.ink.json create mode 100644 examples/inkfiles/function/test-error.ink create mode 100644 examples/inkfiles/function/test-error.ink.json create mode 100644 examples/inkfiles/gather/complex-flow.ink create mode 100644 examples/inkfiles/gather/complex-flow.ink.json create mode 100644 examples/inkfiles/gather/deep-nesting.ink create mode 100644 examples/inkfiles/gather/deep-nesting.ink.json create mode 100644 examples/inkfiles/gather/gather-basic.ink create mode 100644 examples/inkfiles/gather/gather-basic.ink.json create mode 100644 examples/inkfiles/gather/gather-chain.ink create mode 100644 examples/inkfiles/gather/gather-chain.ink.json create mode 100644 examples/inkfiles/gather/nested-flow.ink create mode 100644 examples/inkfiles/gather/nested-flow.ink.json create mode 100644 examples/inkfiles/gather/nested-gather.ink create mode 100644 examples/inkfiles/gather/nested-gather.ink.json create mode 100644 examples/inkfiles/glue/glue-with-divert.ink create mode 100644 examples/inkfiles/glue/glue-with-divert.ink.json create mode 100644 examples/inkfiles/glue/left-right-glue-matching.ink create mode 100644 examples/inkfiles/glue/left-right-glue-matching.ink.json create mode 100644 examples/inkfiles/glue/simple-glue.ink create mode 100644 examples/inkfiles/glue/simple-glue.ink.json create mode 100644 examples/inkfiles/glue/testbugfix1.ink create mode 100644 examples/inkfiles/glue/testbugfix1.ink.json create mode 100644 examples/inkfiles/glue/testbugfix2.ink create mode 100644 examples/inkfiles/glue/testbugfix2.ink.json create mode 100644 examples/inkfiles/knot/multi-line.ink create mode 100644 examples/inkfiles/knot/multi-line.ink.json create mode 100644 examples/inkfiles/knot/param-floats.ink create mode 100644 examples/inkfiles/knot/param-floats.ink.json create mode 100644 examples/inkfiles/knot/param-ints.ink create mode 100644 examples/inkfiles/knot/param-ints.ink.json create mode 100644 examples/inkfiles/knot/param-multi.ink create mode 100644 examples/inkfiles/knot/param-multi.ink.json create mode 100644 examples/inkfiles/knot/param-recurse.ink create mode 100644 examples/inkfiles/knot/param-recurse.ink.json create mode 100644 examples/inkfiles/knot/param-strings.ink create mode 100644 examples/inkfiles/knot/param-strings.ink.json create mode 100644 examples/inkfiles/knot/param-vars.ink create mode 100644 examples/inkfiles/knot/param-vars.ink.json create mode 100644 examples/inkfiles/knot/single-line.ink create mode 100644 examples/inkfiles/knot/single-line.ink.json create mode 100644 examples/inkfiles/knot/strip-empty-lines.ink create mode 100644 examples/inkfiles/knot/strip-empty-lines.ink.json create mode 100644 examples/inkfiles/lists/basic-operations.ink create mode 100644 examples/inkfiles/lists/basic-operations.ink.json create mode 100644 examples/inkfiles/lists/bug-adding-element.ink create mode 100644 examples/inkfiles/lists/bug-adding-element.ink.json create mode 100644 examples/inkfiles/lists/empty-list-origin-after-assignment.ink create mode 100644 examples/inkfiles/lists/empty-list-origin-after-assignment.ink.json create mode 100644 examples/inkfiles/lists/empty-list-origin.ink create mode 100644 examples/inkfiles/lists/empty-list-origin.ink.json create mode 100644 examples/inkfiles/lists/list-mixed-items.ink create mode 100644 examples/inkfiles/lists/list-mixed-items.ink.json create mode 100644 examples/inkfiles/lists/list-range.ink create mode 100644 examples/inkfiles/lists/list-range.ink.json create mode 100644 examples/inkfiles/lists/list-save-load.ink create mode 100644 examples/inkfiles/lists/list-save-load.ink.json create mode 100644 examples/inkfiles/lists/more-list-operations.ink create mode 100644 examples/inkfiles/lists/more-list-operations.ink.json create mode 100644 examples/inkfiles/misc/issue15.ink create mode 100644 examples/inkfiles/misc/issue15.ink.json create mode 100644 examples/inkfiles/runtime/external-function-0-arg.ink create mode 100644 examples/inkfiles/runtime/external-function-0-arg.ink.json create mode 100644 examples/inkfiles/runtime/external-function-1-arg.ink create mode 100644 examples/inkfiles/runtime/external-function-1-arg.ink.json create mode 100644 examples/inkfiles/runtime/external-function-2-arg.ink create mode 100644 examples/inkfiles/runtime/external-function-2-arg.ink.json create mode 100644 examples/inkfiles/runtime/external-function-3-arg.ink create mode 100644 examples/inkfiles/runtime/external-function-3-arg.ink.json create mode 100644 examples/inkfiles/runtime/jump-knot.ink create mode 100644 examples/inkfiles/runtime/jump-knot.ink.json create mode 100644 examples/inkfiles/runtime/jump-stitch.ink create mode 100644 examples/inkfiles/runtime/jump-stitch.ink.json create mode 100644 examples/inkfiles/runtime/load-save.ink create mode 100644 examples/inkfiles/runtime/load-save.ink.json create mode 100644 examples/inkfiles/runtime/multiflow-basics.ink create mode 100644 examples/inkfiles/runtime/multiflow-basics.ink.json create mode 100644 examples/inkfiles/runtime/multiflow-saveloadthreads.ink create mode 100644 examples/inkfiles/runtime/multiflow-saveloadthreads.ink.json create mode 100644 examples/inkfiles/runtime/read-visit-counts.ink create mode 100644 examples/inkfiles/runtime/read-visit-counts.ink.json create mode 100644 examples/inkfiles/runtime/saving-loading.ink create mode 100644 examples/inkfiles/runtime/saving-loading.ink.json create mode 100644 examples/inkfiles/runtime/set-get-variables.ink create mode 100644 examples/inkfiles/runtime/set-get-variables.ink.json create mode 100644 examples/inkfiles/runtime/variable-observers.ink create mode 100644 examples/inkfiles/runtime/variable-observers.ink.json create mode 100644 examples/inkfiles/stitch/auto-stitch.ink create mode 100644 examples/inkfiles/stitch/auto-stitch.ink.json create mode 100644 examples/inkfiles/stitch/manual-stitch.ink create mode 100644 examples/inkfiles/stitch/manual-stitch.ink.json create mode 100644 examples/inkfiles/tags/tags.ink create mode 100644 examples/inkfiles/tags/tags.ink.json create mode 100644 examples/inkfiles/tags/tagsDynamicContent.ink create mode 100644 examples/inkfiles/tags/tagsDynamicContent.ink.json create mode 100644 examples/inkfiles/tags/tagsInChoice.ink create mode 100644 examples/inkfiles/tags/tagsInChoice.ink.json create mode 100644 examples/inkfiles/tags/tagsInSeq.ink create mode 100644 examples/inkfiles/tags/tagsInSeq.ink.json create mode 100644 examples/inkfiles/threads/thread-bug.ink create mode 100644 examples/inkfiles/threads/thread-bug.ink.json create mode 100644 examples/inkfiles/tunnels/tunnel-onwards-divert-override.ink create mode 100644 examples/inkfiles/tunnels/tunnel-onwards-divert-override.ink.json create mode 100644 examples/inkfiles/variable/var-divert.ink create mode 100644 examples/inkfiles/variable/var-divert.ink.json create mode 100644 examples/inkfiles/variable/varcalc.ink create mode 100644 examples/inkfiles/variable/varcalc.ink.json create mode 100644 examples/inkfiles/variable/variable-declaration.ink create mode 100644 examples/inkfiles/variable/variable-declaration.ink.json create mode 100644 examples/inkfiles/variable/varstringinc.ink create mode 100644 examples/inkfiles/variable/varstringinc.ink.json create mode 100644 examples/inkfiles/variabletext/cycle.ink create mode 100644 examples/inkfiles/variabletext/cycle.ink.json create mode 100644 examples/inkfiles/variabletext/empty-elements.ink create mode 100644 examples/inkfiles/variabletext/empty-elements.ink.json create mode 100644 examples/inkfiles/variabletext/list-in-choice.ink create mode 100644 examples/inkfiles/variabletext/list-in-choice.ink.json create mode 100644 examples/inkfiles/variabletext/once.ink create mode 100644 examples/inkfiles/variabletext/once.ink.json create mode 100644 examples/inkfiles/variabletext/sequence.ink create mode 100644 examples/inkfiles/variabletext/sequence.ink.json create mode 100644 src/json_serialization.rs create mode 100644 src/story.rs diff --git a/Cargo.toml b/Cargo.toml index d353bbd..005a6a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,6 @@ name = "console-player" path = "src/bin/console-player.rs" [dependencies] +serde = { version = "1.0.152", features = ["derive"] } +serde_json = "1.0.93" +log = "0.4.17" diff --git a/examples/inkfiles/basictext/oneline.ink b/examples/inkfiles/basictext/oneline.ink new file mode 100644 index 0000000..435077a --- /dev/null +++ b/examples/inkfiles/basictext/oneline.ink @@ -0,0 +1 @@ +Line. diff --git a/examples/inkfiles/basictext/oneline.ink.json b/examples/inkfiles/basictext/oneline.ink.json new file mode 100644 index 0000000..8a6354a --- /dev/null +++ b/examples/inkfiles/basictext/oneline.ink.json @@ -0,0 +1 @@ +{"inkVersion":21,"root":[["^Line.","\n",["done",{"#n":"g-0"}],null],"done",null],"listDefs":{}} \ No newline at end of file diff --git a/examples/inkfiles/basictext/twolines.ink b/examples/inkfiles/basictext/twolines.ink new file mode 100644 index 0000000..1a032d2 --- /dev/null +++ b/examples/inkfiles/basictext/twolines.ink @@ -0,0 +1,2 @@ +Line. +Other line. diff --git a/examples/inkfiles/basictext/twolines.ink.json b/examples/inkfiles/basictext/twolines.ink.json new file mode 100644 index 0000000..f1301db --- /dev/null +++ b/examples/inkfiles/basictext/twolines.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/choices/conditional-choice.ink b/examples/inkfiles/choices/conditional-choice.ink new file mode 100644 index 0000000..b336ac7 --- /dev/null +++ b/examples/inkfiles/choices/conditional-choice.ink @@ -0,0 +1,8 @@ + + Test conditional choices + * { true } { false } not displayed + * { true } { true } { true and true } one + * { false } not displayed + * { true } two + * { true } { true } three + * { true } four diff --git a/examples/inkfiles/choices/conditional-choice.ink.json b/examples/inkfiles/choices/conditional-choice.ink.json new file mode 100644 index 0000000..c7055d6 --- /dev/null +++ b/examples/inkfiles/choices/conditional-choice.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/choices/divert-choice.ink b/examples/inkfiles/choices/divert-choice.ink new file mode 100644 index 0000000..5c6a067 --- /dev/null +++ b/examples/inkfiles/choices/divert-choice.ink @@ -0,0 +1,9 @@ +-> knot +=== knot + You see a soldier. + * [Pull a face] + You pull a face, and the soldier comes at you! -> shove + * (shove) [Shove the guard aside] You shove the guard to one side, but he comes back swinging. + * {shove} [Grapple and fight] + - -> knot + -> END \ No newline at end of file diff --git a/examples/inkfiles/choices/divert-choice.ink.json b/examples/inkfiles/choices/divert-choice.ink.json new file mode 100644 index 0000000..c0992f8 --- /dev/null +++ b/examples/inkfiles/choices/divert-choice.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/choices/fallback-choice.ink b/examples/inkfiles/choices/fallback-choice.ink new file mode 100644 index 0000000..a4e1662 --- /dev/null +++ b/examples/inkfiles/choices/fallback-choice.ink @@ -0,0 +1,9 @@ +-> find_help +=== find_help === + + You search desperately for a friendly face in the crowd. + * The woman in the hat[?] pushes you roughly aside. -> find_help + * The man with the briefcase[?] looks disgusted as you stumble past him. -> find_help + * -> + - But it is too late: you collapse onto the station platform. This is the end. + -> END \ No newline at end of file diff --git a/examples/inkfiles/choices/fallback-choice.ink.json b/examples/inkfiles/choices/fallback-choice.ink.json new file mode 100644 index 0000000..1c748a0 --- /dev/null +++ b/examples/inkfiles/choices/fallback-choice.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/choices/label-flow.ink b/examples/inkfiles/choices/label-flow.ink new file mode 100644 index 0000000..8869936 --- /dev/null +++ b/examples/inkfiles/choices/label-flow.ink @@ -0,0 +1,15 @@ +-> meet_guard + +=== meet_guard === + The guard frowns at you. + * (greet) [Greet him] + 'Greetings.' + * (get_out) 'Get out of my way[.'],' you tell the guard. + - 'Hmm,' replies the guard. + * {greet} 'Having a nice day?' + * 'Hmm?'[] you reply. + * {get_out} [Shove him aside] + You shove him sharply. He stares in reply, and draws his sword! + -> END + - 'Mff,' the guard replies, and then offers you a paper bag. 'Toffee?' + -> END \ No newline at end of file diff --git a/examples/inkfiles/choices/label-flow.ink.json b/examples/inkfiles/choices/label-flow.ink.json new file mode 100644 index 0000000..a468c3b --- /dev/null +++ b/examples/inkfiles/choices/label-flow.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/choices/label-scope-error.ink b/examples/inkfiles/choices/label-scope-error.ink new file mode 100644 index 0000000..46dca5f --- /dev/null +++ b/examples/inkfiles/choices/label-scope-error.ink @@ -0,0 +1,9 @@ +-> knot + +=== knot === + = stitch_one + * an option + - (gatherpoint) Some content. + -> knot.stitch_two + = stitch_two + * {gatherpoint} Found gatherpoint \ No newline at end of file diff --git a/examples/inkfiles/choices/label-scope-error.ink.json b/examples/inkfiles/choices/label-scope-error.ink.json new file mode 100644 index 0000000..f65ba89 --- /dev/null +++ b/examples/inkfiles/choices/label-scope-error.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/choices/label-scope.ink b/examples/inkfiles/choices/label-scope.ink new file mode 100644 index 0000000..21ae169 --- /dev/null +++ b/examples/inkfiles/choices/label-scope.ink @@ -0,0 +1,10 @@ +-> knot + +=== knot === + = stitch_one + * an option + - (gatherpoint) Some content. + -> knot.stitch_two + = stitch_two + * {knot.stitch_one.gatherpoint} Found gatherpoint + -> END \ No newline at end of file diff --git a/examples/inkfiles/choices/label-scope.ink.json b/examples/inkfiles/choices/label-scope.ink.json new file mode 100644 index 0000000..eb00f09 --- /dev/null +++ b/examples/inkfiles/choices/label-scope.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/choices/mixed-choice.ink b/examples/inkfiles/choices/mixed-choice.ink new file mode 100644 index 0000000..5c35e2d --- /dev/null +++ b/examples/inkfiles/choices/mixed-choice.ink @@ -0,0 +1,5 @@ +Hello world! + * Hello [back!] right back to you! + Nice to hear from you. + +->DONE \ No newline at end of file diff --git a/examples/inkfiles/choices/mixed-choice.ink.json b/examples/inkfiles/choices/mixed-choice.ink.json new file mode 100644 index 0000000..d51fd82 --- /dev/null +++ b/examples/inkfiles/choices/mixed-choice.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/choices/multi-choice.ink b/examples/inkfiles/choices/multi-choice.ink new file mode 100644 index 0000000..7afe173 --- /dev/null +++ b/examples/inkfiles/choices/multi-choice.ink @@ -0,0 +1,7 @@ +Hello, world! + * Hello back! + Nice to hear from you + * Goodbye + See you later +- +-> END \ No newline at end of file diff --git a/examples/inkfiles/choices/multi-choice.ink.json b/examples/inkfiles/choices/multi-choice.ink.json new file mode 100644 index 0000000..8b0ec6e --- /dev/null +++ b/examples/inkfiles/choices/multi-choice.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/choices/no-choice-text.ink b/examples/inkfiles/choices/no-choice-text.ink new file mode 100644 index 0000000..3982bd9 --- /dev/null +++ b/examples/inkfiles/choices/no-choice-text.ink @@ -0,0 +1,3 @@ +Hello world! +* [Hello back!] +-> DONE \ No newline at end of file diff --git a/examples/inkfiles/choices/no-choice-text.ink.json b/examples/inkfiles/choices/no-choice-text.ink.json new file mode 100644 index 0000000..20f6628 --- /dev/null +++ b/examples/inkfiles/choices/no-choice-text.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/choices/one.ink b/examples/inkfiles/choices/one.ink new file mode 100644 index 0000000..b3bca7f --- /dev/null +++ b/examples/inkfiles/choices/one.ink @@ -0,0 +1,4 @@ +Hello world! +* Hello back! + +->DONE \ No newline at end of file diff --git a/examples/inkfiles/choices/one.ink.json b/examples/inkfiles/choices/one.ink.json new file mode 100644 index 0000000..7ab756e --- /dev/null +++ b/examples/inkfiles/choices/one.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/choices/single-choice.ink b/examples/inkfiles/choices/single-choice.ink new file mode 100644 index 0000000..d09c8af --- /dev/null +++ b/examples/inkfiles/choices/single-choice.ink @@ -0,0 +1,4 @@ +Hello, world! + * Hello back! + Nice to hear from you +-> END \ No newline at end of file diff --git a/examples/inkfiles/choices/single-choice.ink.json b/examples/inkfiles/choices/single-choice.ink.json new file mode 100644 index 0000000..1a50964 --- /dev/null +++ b/examples/inkfiles/choices/single-choice.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/choices/sticky-choice.ink b/examples/inkfiles/choices/sticky-choice.ink new file mode 100644 index 0000000..f9abf87 --- /dev/null +++ b/examples/inkfiles/choices/sticky-choice.ink @@ -0,0 +1,8 @@ +-> homers_couch + +=== homers_couch === + + [Eat another donut] + You eat another donut. -> homers_couch + * [Get off the couch] + You struggle up off the couch to go and compose epic poetry. + -> END \ No newline at end of file diff --git a/examples/inkfiles/choices/sticky-choice.ink.json b/examples/inkfiles/choices/sticky-choice.ink.json new file mode 100644 index 0000000..191e7fc --- /dev/null +++ b/examples/inkfiles/choices/sticky-choice.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/choices/suppress-choice.ink b/examples/inkfiles/choices/suppress-choice.ink new file mode 100644 index 0000000..aa8cc3b --- /dev/null +++ b/examples/inkfiles/choices/suppress-choice.ink @@ -0,0 +1,4 @@ +Hello world! + * [Hello back!] + Nice to hear from you. + -> END \ No newline at end of file diff --git a/examples/inkfiles/choices/suppress-choice.ink.json b/examples/inkfiles/choices/suppress-choice.ink.json new file mode 100644 index 0000000..308bcbd --- /dev/null +++ b/examples/inkfiles/choices/suppress-choice.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/choices/varying-choice.ink b/examples/inkfiles/choices/varying-choice.ink new file mode 100644 index 0000000..8d2da29 --- /dev/null +++ b/examples/inkfiles/choices/varying-choice.ink @@ -0,0 +1,9 @@ +-> find_help + +=== find_help === + + You search desperately for a friendly face in the crowd. + * The woman in the hat[?] pushes you roughly aside. -> find_help + * The man with the briefcase[?] looks disgusted as you stumble past him. -> find_help + +->DONE \ No newline at end of file diff --git a/examples/inkfiles/choices/varying-choice.ink.json b/examples/inkfiles/choices/varying-choice.ink.json new file mode 100644 index 0000000..8644e3e --- /dev/null +++ b/examples/inkfiles/choices/varying-choice.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/conditional/condopt.ink b/examples/inkfiles/conditional/condopt.ink new file mode 100644 index 0000000..4086f44 --- /dev/null +++ b/examples/inkfiles/conditional/condopt.ink @@ -0,0 +1,18 @@ +I looked... + * [at the door] + -> door_open + * [outside] + -> leave + + === door_open + at the door. It was open. + -> leave + + === leave + I stood up and... + { door_open: + * I strode out of the compartment[] and I fancied I heard my master quietly tutting to himself. -> END + - else: + * I asked permission to leave[] and Monsieur Fogg looked surprised. -> END + * I stood and went to open the door[]. Monsieur Fogg seemed untroubled by this small rebellion. -> END + } \ No newline at end of file diff --git a/examples/inkfiles/conditional/condopt.ink.json b/examples/inkfiles/conditional/condopt.ink.json new file mode 100644 index 0000000..c3ce23b --- /dev/null +++ b/examples/inkfiles/conditional/condopt.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/conditional/condtext.ink b/examples/inkfiles/conditional/condtext.ink new file mode 100644 index 0000000..b5e17a2 --- /dev/null +++ b/examples/inkfiles/conditional/condtext.ink @@ -0,0 +1,17 @@ +"We are going on a trip," said Monsieur Fogg. + * [The wager.] -> know_about_wager + * [I was surprised.] -> i_stared + + === know_about_wager + I had heard about the wager. + -> i_stared + + === i_stared + I stared at Monsieur Fogg. + { know_about_wager: + <> "But surely you are not serious?" I demanded. + - else: + <> "But there must be a reason for this trip," I observed. + } + He said nothing in reply, merely considering his newspaper with as much thoroughness as entomologist considering his latest pinned addition. + -> END \ No newline at end of file diff --git a/examples/inkfiles/conditional/condtext.ink.json b/examples/inkfiles/conditional/condtext.ink.json new file mode 100644 index 0000000..f527164 --- /dev/null +++ b/examples/inkfiles/conditional/condtext.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/conditional/cycle.ink b/examples/inkfiles/conditional/cycle.ink new file mode 100644 index 0000000..bdfa3de --- /dev/null +++ b/examples/inkfiles/conditional/cycle.ink @@ -0,0 +1,9 @@ +-> test + +=== test + { cycle: + - I held my breath. + - I waited impatiently. + - I paused. + } + + [Try again] -> test \ No newline at end of file diff --git a/examples/inkfiles/conditional/cycle.ink.json b/examples/inkfiles/conditional/cycle.ink.json new file mode 100644 index 0000000..5ff8ec9 --- /dev/null +++ b/examples/inkfiles/conditional/cycle.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/conditional/ifelse-ext-text1.ink b/examples/inkfiles/conditional/ifelse-ext-text1.ink new file mode 100644 index 0000000..15fc9b0 --- /dev/null +++ b/examples/inkfiles/conditional/ifelse-ext-text1.ink @@ -0,0 +1,13 @@ + + VAR x = 0 + { + - x == 0: + This is text 1. + - x > 0: + This is text 2. + - else: + This is text 3. + } + + [The Choice.] -> to_end + === to_end + This is the end. -> END \ No newline at end of file diff --git a/examples/inkfiles/conditional/ifelse-ext-text1.ink.json b/examples/inkfiles/conditional/ifelse-ext-text1.ink.json new file mode 100644 index 0000000..ec72908 --- /dev/null +++ b/examples/inkfiles/conditional/ifelse-ext-text1.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/conditional/ifelse-ext-text2.ink b/examples/inkfiles/conditional/ifelse-ext-text2.ink new file mode 100644 index 0000000..576c00f --- /dev/null +++ b/examples/inkfiles/conditional/ifelse-ext-text2.ink @@ -0,0 +1,13 @@ + + VAR x = 2 + { + - x == 0: + This is text 1. + - x > 0: + This is text 2. + - else: + This is text 3. + } + + [The Choice.] -> to_end + === to_end + This is the end. -> END \ No newline at end of file diff --git a/examples/inkfiles/conditional/ifelse-ext-text2.ink.json b/examples/inkfiles/conditional/ifelse-ext-text2.ink.json new file mode 100644 index 0000000..431cd02 --- /dev/null +++ b/examples/inkfiles/conditional/ifelse-ext-text2.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/conditional/ifelse-ext-text3.ink b/examples/inkfiles/conditional/ifelse-ext-text3.ink new file mode 100644 index 0000000..dc35c76 --- /dev/null +++ b/examples/inkfiles/conditional/ifelse-ext-text3.ink @@ -0,0 +1,13 @@ + + VAR x = -2 + { + - x == 0: + This is text 1. + - x > 0: + This is text 2. + - else: + This is text 3. + } + + [The Choice.] -> to_end + === to_end + This is the end. -> END \ No newline at end of file diff --git a/examples/inkfiles/conditional/ifelse-ext-text3.ink.json b/examples/inkfiles/conditional/ifelse-ext-text3.ink.json new file mode 100644 index 0000000..aeb8611 --- /dev/null +++ b/examples/inkfiles/conditional/ifelse-ext-text3.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/conditional/ifelse-ext.ink b/examples/inkfiles/conditional/ifelse-ext.ink new file mode 100644 index 0000000..22f59c4 --- /dev/null +++ b/examples/inkfiles/conditional/ifelse-ext.ink @@ -0,0 +1,12 @@ + + VAR x = -2 + VAR y = 3 + { + - x == 0: + ~ y = 0 + - x > 0: + ~ y = x - 1 + - else: + ~ y = x + 1 + } + The value is {y}. -> END \ No newline at end of file diff --git a/examples/inkfiles/conditional/ifelse-ext.ink.json b/examples/inkfiles/conditional/ifelse-ext.ink.json new file mode 100644 index 0000000..03e8041 --- /dev/null +++ b/examples/inkfiles/conditional/ifelse-ext.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/conditional/ifelse.ink b/examples/inkfiles/conditional/ifelse.ink new file mode 100644 index 0000000..dd81388 --- /dev/null +++ b/examples/inkfiles/conditional/ifelse.ink @@ -0,0 +1,9 @@ + + VAR x = 0 + VAR y = 3 + { x > 0: + ~ y = x - 1 + - else: + ~ y = x + 1 + } + The value is {y}. -> END \ No newline at end of file diff --git a/examples/inkfiles/conditional/ifelse.ink.json b/examples/inkfiles/conditional/ifelse.ink.json new file mode 100644 index 0000000..0084640 --- /dev/null +++ b/examples/inkfiles/conditional/ifelse.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/conditional/iffalse.ink b/examples/inkfiles/conditional/iffalse.ink new file mode 100644 index 0000000..6f08d2d --- /dev/null +++ b/examples/inkfiles/conditional/iffalse.ink @@ -0,0 +1,7 @@ + + VAR x = 0 + VAR y = 3 + { x > 0: + ~ y = x - 1 + } + The value is {y}. -> END \ No newline at end of file diff --git a/examples/inkfiles/conditional/iffalse.ink.json b/examples/inkfiles/conditional/iffalse.ink.json new file mode 100644 index 0000000..e38e29c --- /dev/null +++ b/examples/inkfiles/conditional/iffalse.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/conditional/iftrue.ink b/examples/inkfiles/conditional/iftrue.ink new file mode 100644 index 0000000..da3ba6e --- /dev/null +++ b/examples/inkfiles/conditional/iftrue.ink @@ -0,0 +1,7 @@ + + VAR x = 2 + VAR y = 0 + { x > 0: + ~ y = x - 1 + } + The value is {y}. -> END diff --git a/examples/inkfiles/conditional/iftrue.ink.json b/examples/inkfiles/conditional/iftrue.ink.json new file mode 100644 index 0000000..31e02cd --- /dev/null +++ b/examples/inkfiles/conditional/iftrue.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/conditional/multiline-choice.ink b/examples/inkfiles/conditional/multiline-choice.ink new file mode 100644 index 0000000..f22e5cb --- /dev/null +++ b/examples/inkfiles/conditional/multiline-choice.ink @@ -0,0 +1,12 @@ +-> test +=== test + { stopping: + - At the table, I drew a card. Ace of Hearts. + - 2 of Diamonds. + "Should I hit you again," the croupier asks. + * [No.] I left the table. -> END + - King of Spades. + "You lose," he crowed. + -> END + } + + [Draw a card] I drew a card. -> test \ No newline at end of file diff --git a/examples/inkfiles/conditional/multiline-choice.ink.json b/examples/inkfiles/conditional/multiline-choice.ink.json new file mode 100644 index 0000000..2d47d41 --- /dev/null +++ b/examples/inkfiles/conditional/multiline-choice.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/conditional/multiline-divert.ink b/examples/inkfiles/conditional/multiline-divert.ink new file mode 100644 index 0000000..c6a83fc --- /dev/null +++ b/examples/inkfiles/conditional/multiline-divert.ink @@ -0,0 +1,16 @@ +->test + +=== test + { stopping: + - At the table, I drew a card. Ace of Hearts. + - <> 2 of Diamonds. + "Should I hit you again," the croupier asks. + - <> King of Spades. + -> he_crowed + } + + [Draw a card] I drew a card. -> test + + == he_crowed + "You lose," he crowed. + + -> END \ No newline at end of file diff --git a/examples/inkfiles/conditional/multiline-divert.ink.json b/examples/inkfiles/conditional/multiline-divert.ink.json new file mode 100644 index 0000000..805a979 --- /dev/null +++ b/examples/inkfiles/conditional/multiline-divert.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/conditional/multiline.ink b/examples/inkfiles/conditional/multiline.ink new file mode 100644 index 0000000..9e8756e --- /dev/null +++ b/examples/inkfiles/conditional/multiline.ink @@ -0,0 +1,11 @@ +-> test + +=== test + { stopping: + - At the table, I drew a card. Ace of Hearts. + - <> 2 of Diamonds. + "Should I hit you again," the croupier asks. + - <> King of Spades. + "You lose," he crowed. + } + + [Draw a card] I drew a card. -> test \ No newline at end of file diff --git a/examples/inkfiles/conditional/multiline.ink.json b/examples/inkfiles/conditional/multiline.ink.json new file mode 100644 index 0000000..99132bc --- /dev/null +++ b/examples/inkfiles/conditional/multiline.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/conditional/once.ink b/examples/inkfiles/conditional/once.ink new file mode 100644 index 0000000..5a1d229 --- /dev/null +++ b/examples/inkfiles/conditional/once.ink @@ -0,0 +1,8 @@ +-> test + +=== test + { once: + - Would my luck hold? + - Could I win the hand? + } + + [Try again] -> test \ No newline at end of file diff --git a/examples/inkfiles/conditional/once.ink.json b/examples/inkfiles/conditional/once.ink.json new file mode 100644 index 0000000..39bead8 --- /dev/null +++ b/examples/inkfiles/conditional/once.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/conditional/shuffle.ink b/examples/inkfiles/conditional/shuffle.ink new file mode 100644 index 0000000..738dd22 --- /dev/null +++ b/examples/inkfiles/conditional/shuffle.ink @@ -0,0 +1,9 @@ +-> test + +=== test + { shuffle: + - Ace of Hearts. + - King of Spades. + - 2 of Diamonds. + } + + [Try again] -> test \ No newline at end of file diff --git a/examples/inkfiles/conditional/shuffle.ink.json b/examples/inkfiles/conditional/shuffle.ink.json new file mode 100644 index 0000000..7a3493d --- /dev/null +++ b/examples/inkfiles/conditional/shuffle.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/conditional/shuffle_once.ink b/examples/inkfiles/conditional/shuffle_once.ink new file mode 100644 index 0000000..63b8062 --- /dev/null +++ b/examples/inkfiles/conditional/shuffle_once.ink @@ -0,0 +1,8 @@ +-> test + +=== test + { shuffle once: + - one + - two + } + + [Try again] -> test \ No newline at end of file diff --git a/examples/inkfiles/conditional/shuffle_once.ink.json b/examples/inkfiles/conditional/shuffle_once.ink.json new file mode 100644 index 0000000..38075a4 --- /dev/null +++ b/examples/inkfiles/conditional/shuffle_once.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/conditional/shuffle_stopping.ink b/examples/inkfiles/conditional/shuffle_stopping.ink new file mode 100644 index 0000000..cd77da0 --- /dev/null +++ b/examples/inkfiles/conditional/shuffle_stopping.ink @@ -0,0 +1,9 @@ +-> test + +=== test + { stopping shuffle: + - one + - two + - final + } + + [Try again] -> test \ No newline at end of file diff --git a/examples/inkfiles/conditional/shuffle_stopping.ink.json b/examples/inkfiles/conditional/shuffle_stopping.ink.json new file mode 100644 index 0000000..0eb4bd8 --- /dev/null +++ b/examples/inkfiles/conditional/shuffle_stopping.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/conditional/stopping.ink b/examples/inkfiles/conditional/stopping.ink new file mode 100644 index 0000000..2275ca9 --- /dev/null +++ b/examples/inkfiles/conditional/stopping.ink @@ -0,0 +1,9 @@ +-> test + +=== test + { stopping: + - I entered the casino. + - I entered the casino again. + - Once more, I went inside. + } + + [Try again] -> test \ No newline at end of file diff --git a/examples/inkfiles/conditional/stopping.ink.json b/examples/inkfiles/conditional/stopping.ink.json new file mode 100644 index 0000000..9b5cd6c --- /dev/null +++ b/examples/inkfiles/conditional/stopping.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/divert/complex-branching.ink b/examples/inkfiles/divert/complex-branching.ink new file mode 100644 index 0000000..cda88a1 --- /dev/null +++ b/examples/inkfiles/divert/complex-branching.ink @@ -0,0 +1,25 @@ +-> back_in_london +=== back_in_london === + + We arrived into London at 9.45pm exactly. + + * "There is not a moment to lose!"[] I declared. + -> hurry_outside + + * "Monsieur, let us savour this moment!"[] I declared. + My master clouted me firmly around the head and dragged me out of the door. + -> dragged_outside + + * [We hurried home] -> hurry_outside + + + === hurry_outside === + We hurried home to Savile Row -> as_fast_as_we_could + + + === dragged_outside === + He insisted that we hurried home to Savile Row -> as_fast_as_we_could + + + === as_fast_as_we_could === + <>as fast as we could. -> END \ No newline at end of file diff --git a/examples/inkfiles/divert/complex-branching.ink.json b/examples/inkfiles/divert/complex-branching.ink.json new file mode 100644 index 0000000..4f76827 --- /dev/null +++ b/examples/inkfiles/divert/complex-branching.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/divert/divert-on-choice.ink b/examples/inkfiles/divert/divert-on-choice.ink new file mode 100644 index 0000000..9c8a877 --- /dev/null +++ b/examples/inkfiles/divert/divert-on-choice.ink @@ -0,0 +1,7 @@ +->paragraph_1 +== paragraph_1 === + You stand by the wall of Analand, sword in hand. + * [Open the gate] -> paragraph_2 + + === paragraph_2 === + You open the gate, and step out onto the path. -> END \ No newline at end of file diff --git a/examples/inkfiles/divert/divert-on-choice.ink.json b/examples/inkfiles/divert/divert-on-choice.ink.json new file mode 100644 index 0000000..b0386c1 --- /dev/null +++ b/examples/inkfiles/divert/divert-on-choice.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/divert/invisible-divert.ink b/examples/inkfiles/divert/invisible-divert.ink new file mode 100644 index 0000000..9925c63 --- /dev/null +++ b/examples/inkfiles/divert/invisible-divert.ink @@ -0,0 +1,5 @@ + + We hurried home to Savile Row -> as_fast_as_we_could + + === as_fast_as_we_could === + as fast as we could. -> END \ No newline at end of file diff --git a/examples/inkfiles/divert/invisible-divert.ink.json b/examples/inkfiles/divert/invisible-divert.ink.json new file mode 100644 index 0000000..2164d63 --- /dev/null +++ b/examples/inkfiles/divert/invisible-divert.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/divert/simple-divert.ink b/examples/inkfiles/divert/simple-divert.ink new file mode 100644 index 0000000..7df3aad --- /dev/null +++ b/examples/inkfiles/divert/simple-divert.ink @@ -0,0 +1,6 @@ + + We arrived into London at 9.45pm exactly. + -> hurry_home + + === hurry_home === + We hurried home to Savile Row as fast as we could. -> END diff --git a/examples/inkfiles/divert/simple-divert.ink.json b/examples/inkfiles/divert/simple-divert.ink.json new file mode 100644 index 0000000..137863e --- /dev/null +++ b/examples/inkfiles/divert/simple-divert.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/function/complex-func1.ink b/examples/inkfiles/function/complex-func1.ink new file mode 100644 index 0000000..fdab4c9 --- /dev/null +++ b/examples/inkfiles/function/complex-func1.ink @@ -0,0 +1,12 @@ +~ derp(2, 3, 4) + The values are {x} and {y}. + -> END + + === function derp(a, b, c) === + VAR x = 0 + ~ x = a + b + VAR y = 3 + { x == 5: + ~ x = 6 + } + ~ y = x + c \ No newline at end of file diff --git a/examples/inkfiles/function/complex-func1.ink.json b/examples/inkfiles/function/complex-func1.ink.json new file mode 100644 index 0000000..30de178 --- /dev/null +++ b/examples/inkfiles/function/complex-func1.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/function/complex-func2.ink b/examples/inkfiles/function/complex-func2.ink new file mode 100644 index 0000000..769f438 --- /dev/null +++ b/examples/inkfiles/function/complex-func2.ink @@ -0,0 +1,17 @@ +~ derp(2, 3) + The values are {x} and {y} and {z}. + -> END + + === function derp(a, b) === + VAR x = 0 + ~ x = a - b + VAR y = 3 + { + - x == 0: + ~ y = 0 + - x > 0: + ~ y = x - 1 + - else: + ~ y = x + 1 + } + VAR z = 1 \ No newline at end of file diff --git a/examples/inkfiles/function/complex-func2.ink.json b/examples/inkfiles/function/complex-func2.ink.json new file mode 100644 index 0000000..9a39f21 --- /dev/null +++ b/examples/inkfiles/function/complex-func2.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/function/complex-func3.ink b/examples/inkfiles/function/complex-func3.ink new file mode 100644 index 0000000..7451530 --- /dev/null +++ b/examples/inkfiles/function/complex-func3.ink @@ -0,0 +1,27 @@ + + ~ merchant_init() + "I will pay you {fee} reales if you get the goods to their destination. The goods will take up {weight} cargo spaces." + -> END + + === function merchant_init() + VAR weight = 20 + VAR roll = 0 + VAR mult = 1 + + { roll == 0: + ~ mult = 2 + } + + { mult == 2: + ~ roll = 1 + } + + { roll == 0: + ~ mult = 3 + } + + VAR dst = 5 + VAR deadline = 0 + ~ deadline = (dst * (100)) / 100 + VAR fee = 0 + ~ fee = (1 + dst) * 10 * mult diff --git a/examples/inkfiles/function/complex-func3.ink.json b/examples/inkfiles/function/complex-func3.ink.json new file mode 100644 index 0000000..85ded53 --- /dev/null +++ b/examples/inkfiles/function/complex-func3.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/function/evaluating-function-variablestate-bug.ink b/examples/inkfiles/function/evaluating-function-variablestate-bug.ink new file mode 100644 index 0000000..b64649e --- /dev/null +++ b/examples/inkfiles/function/evaluating-function-variablestate-bug.ink @@ -0,0 +1,22 @@ +Start + -> tunnel -> + End + -> END + + == tunnel == + In tunnel. + ->-> + + === function function_to_evaluate() === + { zero_equals_(1): + ~ return "WRONG" + - else: + ~ return "RIGHT" + } + + === function zero_equals_(k) === + ~ do_nothing(0) + ~ return (0 == k) + + === function do_nothing(k) + ~ return 0 diff --git a/examples/inkfiles/function/evaluating-function-variablestate-bug.ink.json b/examples/inkfiles/function/evaluating-function-variablestate-bug.ink.json new file mode 100644 index 0000000..97debc1 --- /dev/null +++ b/examples/inkfiles/function/evaluating-function-variablestate-bug.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/function/func-basic.ink b/examples/inkfiles/function/func-basic.ink new file mode 100644 index 0000000..c185eb3 --- /dev/null +++ b/examples/inkfiles/function/func-basic.ink @@ -0,0 +1,7 @@ +VAR x = 0.0 +~ x = lerp(2, 8, 0.4) + The value of x is {x}. + -> END + + === function lerp(a, b, k) === + ~ return ((b - a) * k) + a \ No newline at end of file diff --git a/examples/inkfiles/function/func-basic.ink.json b/examples/inkfiles/function/func-basic.ink.json new file mode 100644 index 0000000..e0f6ed4 --- /dev/null +++ b/examples/inkfiles/function/func-basic.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/function/func-inline.ink b/examples/inkfiles/function/func-inline.ink new file mode 100644 index 0000000..a6e6c43 --- /dev/null +++ b/examples/inkfiles/function/func-inline.ink @@ -0,0 +1,5 @@ +The value of x is {lerp(2, 8, 0.4)}. + -> END + + === function lerp(a, b, k) === + ~ return ((b - a) * k) + a \ No newline at end of file diff --git a/examples/inkfiles/function/func-inline.ink.json b/examples/inkfiles/function/func-inline.ink.json new file mode 100644 index 0000000..1b64dd4 --- /dev/null +++ b/examples/inkfiles/function/func-inline.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/function/func-none.ink b/examples/inkfiles/function/func-none.ink new file mode 100644 index 0000000..33cfae1 --- /dev/null +++ b/examples/inkfiles/function/func-none.ink @@ -0,0 +1,7 @@ +VAR x = 0.0 +~ x = f() + The value of x is {x}. + -> END + + === function f() === + ~ return 3.8 \ No newline at end of file diff --git a/examples/inkfiles/function/func-none.ink.json b/examples/inkfiles/function/func-none.ink.json new file mode 100644 index 0000000..99d4160 --- /dev/null +++ b/examples/inkfiles/function/func-none.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/function/rnd-func.ink b/examples/inkfiles/function/rnd-func.ink new file mode 100644 index 0000000..67aa98f --- /dev/null +++ b/examples/inkfiles/function/rnd-func.ink @@ -0,0 +1,6 @@ +~ SEED_RANDOM(10) + +Rolling dice 1: {RANDOM(1,6)}. +Rolling dice 2: {RANDOM(1,6)}. +Rolling dice 3: {RANDOM(1,6)}. +Rolling dice 4: {RANDOM(1,6)}. \ No newline at end of file diff --git a/examples/inkfiles/function/rnd-func.ink.json b/examples/inkfiles/function/rnd-func.ink.json new file mode 100644 index 0000000..8b444c5 --- /dev/null +++ b/examples/inkfiles/function/rnd-func.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/function/setvar-func.ink b/examples/inkfiles/function/setvar-func.ink new file mode 100644 index 0000000..d655942 --- /dev/null +++ b/examples/inkfiles/function/setvar-func.ink @@ -0,0 +1,7 @@ +~ herp(2, 3) + The value is {x}. + -> END + + === function herp(a, b) === + VAR x = 0.0 + ~x = a * b \ No newline at end of file diff --git a/examples/inkfiles/function/setvar-func.ink.json b/examples/inkfiles/function/setvar-func.ink.json new file mode 100644 index 0000000..f8426a4 --- /dev/null +++ b/examples/inkfiles/function/setvar-func.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/function/test-error.ink b/examples/inkfiles/function/test-error.ink new file mode 100644 index 0000000..4c3a391 --- /dev/null +++ b/examples/inkfiles/function/test-error.ink @@ -0,0 +1,9 @@ +VAR roll = 0 + + { roll == 0: + ~ roll = 2 + } + + { roll == 0: + ~ roll = 1 + } diff --git a/examples/inkfiles/function/test-error.ink.json b/examples/inkfiles/function/test-error.ink.json new file mode 100644 index 0000000..d271f64 --- /dev/null +++ b/examples/inkfiles/function/test-error.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/gather/complex-flow.ink b/examples/inkfiles/gather/complex-flow.ink new file mode 100644 index 0000000..f62f989 --- /dev/null +++ b/examples/inkfiles/gather/complex-flow.ink @@ -0,0 +1,19 @@ +I looked at Monsieur Fogg + * ... and I could contain myself no longer. + 'What is the purpose of our journey, Monsieur?' + 'A wager,' he replied. + * * 'A wager!'[] I returned. + He nodded. + * * * 'But surely that is foolishness!' + * * * 'A most serious matter then!' + - - - He nodded again. + * * * 'But can we win?' + 'That is what we will endeavour to find out,' he answered. + * * * 'A modest wager, I trust?' + 'Twenty thousand pounds,' he replied, quite flatly. + * * * I asked nothing further of him then[.], and after a final, polite cough, he offered nothing more to me. <> + * * 'Ah[.'],' I replied, uncertain what I thought. + - - After that, <> + * ... but I said nothing[] and <> + - we passed the day in silence. + - -> END \ No newline at end of file diff --git a/examples/inkfiles/gather/complex-flow.ink.json b/examples/inkfiles/gather/complex-flow.ink.json new file mode 100644 index 0000000..f90c262 --- /dev/null +++ b/examples/inkfiles/gather/complex-flow.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/gather/deep-nesting.ink b/examples/inkfiles/gather/deep-nesting.ink new file mode 100644 index 0000000..21662d5 --- /dev/null +++ b/examples/inkfiles/gather/deep-nesting.ink @@ -0,0 +1,9 @@ +Tell us a tale, Captain!" + * "Very well, you sea-dogs. Here's a tale..." + * * "It was a dark and stormy night..." + * * * "...and the crew were restless..." + * * * * "... and they said to their Captain..." + * * * * * "...Tell us a tale Captain!" + * "No, it's past your bed-time." + - To a man, the crew began to yawn. + -> END \ No newline at end of file diff --git a/examples/inkfiles/gather/deep-nesting.ink.json b/examples/inkfiles/gather/deep-nesting.ink.json new file mode 100644 index 0000000..9b63ecd --- /dev/null +++ b/examples/inkfiles/gather/deep-nesting.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/gather/gather-basic.ink b/examples/inkfiles/gather/gather-basic.ink new file mode 100644 index 0000000..d837b15 --- /dev/null +++ b/examples/inkfiles/gather/gather-basic.ink @@ -0,0 +1,9 @@ +What's that?" my master asked. + * "I am somewhat tired[."]," I repeated. + "Really," he responded. "How deleterious." + * "Nothing, Monsieur!"[] I replied. + "Very good, then." + * "I said, this journey is appalling[."] and I want no more of it." + "Ah," he replied, not unkindly. "I see you are feeling frustrated. Tomorrow, things will improve." + - With that Monsieur Fogg left the room. + -> END \ No newline at end of file diff --git a/examples/inkfiles/gather/gather-basic.ink.json b/examples/inkfiles/gather/gather-basic.ink.json new file mode 100644 index 0000000..d4401c6 --- /dev/null +++ b/examples/inkfiles/gather/gather-basic.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/gather/gather-chain.ink b/examples/inkfiles/gather/gather-chain.ink new file mode 100644 index 0000000..b8ba906 --- /dev/null +++ b/examples/inkfiles/gather/gather-chain.ink @@ -0,0 +1,10 @@ + + I ran through the forest, the dogs snapping at my heels. + * I checked the jewels[] were still in my pocket, and the feel of them brought a spring to my step. <> + * I did not pause for breath[] but kept on running. <> + * I cheered with joy. <> + - The road could not be much further! Mackie would have the engine running, and then I'd be safe. + * I reached the road and looked about[]. And would you believe it? + * I should interrupt to say Mackie is normally very reliable[]. He's never once let me down. Or rather, never once, previously to that night. + - The road was empty. Mackie was nowhere to be seen. + -> END \ No newline at end of file diff --git a/examples/inkfiles/gather/gather-chain.ink.json b/examples/inkfiles/gather/gather-chain.ink.json new file mode 100644 index 0000000..51a5184 --- /dev/null +++ b/examples/inkfiles/gather/gather-chain.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/gather/nested-flow.ink b/examples/inkfiles/gather/nested-flow.ink new file mode 100644 index 0000000..20f47de --- /dev/null +++ b/examples/inkfiles/gather/nested-flow.ink @@ -0,0 +1,9 @@ +Well, Poirot? Murder or suicide?" + * "Murder!" + "And who did it?" + * * "Detective-Inspector Japp!" + * * "Captain Hastings!" + * * "Myself!" + * "Suicide!" + - Mrs. Christie lowered her manuscript a moment. The rest of the writing group sat, open-mouthed. + -> END \ No newline at end of file diff --git a/examples/inkfiles/gather/nested-flow.ink.json b/examples/inkfiles/gather/nested-flow.ink.json new file mode 100644 index 0000000..0851a1d --- /dev/null +++ b/examples/inkfiles/gather/nested-flow.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/gather/nested-gather.ink b/examples/inkfiles/gather/nested-gather.ink new file mode 100644 index 0000000..fe9258c --- /dev/null +++ b/examples/inkfiles/gather/nested-gather.ink @@ -0,0 +1,14 @@ +Well, Poirot? Murder or suicide?" + * "Murder!" + "And who did it?" + * * "Detective-Inspector Japp!" + * * "Captain Hastings!" + * * "Myself!" + - - "You must be joking!" + * * "Mon ami, I am deadly serious." + * * "If only..." + * "Suicide!" + "Really, Poirot? Are you quite sure?" + * * "Quite sure." + * * "It is perfectly obvious." + - Mrs. Christie lowered her manuscript a moment. The rest of the writing group sat, open-mouthed. \ No newline at end of file diff --git a/examples/inkfiles/gather/nested-gather.ink.json b/examples/inkfiles/gather/nested-gather.ink.json new file mode 100644 index 0000000..dc7d8d0 --- /dev/null +++ b/examples/inkfiles/gather/nested-gather.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/glue/glue-with-divert.ink b/examples/inkfiles/glue/glue-with-divert.ink new file mode 100644 index 0000000..52b5e5f --- /dev/null +++ b/examples/inkfiles/glue/glue-with-divert.ink @@ -0,0 +1,11 @@ + +We hurried home <> +-> to_savile_row + +=== to_savile_row === +to Savile Row +-> as_fast_as_we_could + +=== as_fast_as_we_could === +<> as fast as we could. +-> END \ No newline at end of file diff --git a/examples/inkfiles/glue/glue-with-divert.ink.json b/examples/inkfiles/glue/glue-with-divert.ink.json new file mode 100644 index 0000000..dc39220 --- /dev/null +++ b/examples/inkfiles/glue/glue-with-divert.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/glue/left-right-glue-matching.ink b/examples/inkfiles/glue/left-right-glue-matching.ink new file mode 100644 index 0000000..c209be5 --- /dev/null +++ b/examples/inkfiles/glue/left-right-glue-matching.ink @@ -0,0 +1,8 @@ +A line. +{ f(): + Another line. +} + +== function f == +{false:nothing} +~ return true diff --git a/examples/inkfiles/glue/left-right-glue-matching.ink.json b/examples/inkfiles/glue/left-right-glue-matching.ink.json new file mode 100644 index 0000000..1c3f8dc --- /dev/null +++ b/examples/inkfiles/glue/left-right-glue-matching.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/glue/simple-glue.ink b/examples/inkfiles/glue/simple-glue.ink new file mode 100644 index 0000000..8c2ba60 --- /dev/null +++ b/examples/inkfiles/glue/simple-glue.ink @@ -0,0 +1,3 @@ +Some <> +content <> +with glue. diff --git a/examples/inkfiles/glue/simple-glue.ink.json b/examples/inkfiles/glue/simple-glue.ink.json new file mode 100644 index 0000000..cf307d4 --- /dev/null +++ b/examples/inkfiles/glue/simple-glue.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/glue/testbugfix1.ink b/examples/inkfiles/glue/testbugfix1.ink new file mode 100644 index 0000000..e4c9ad1 --- /dev/null +++ b/examples/inkfiles/glue/testbugfix1.ink @@ -0,0 +1,8 @@ +A +{f():X} +C + +=== function f() +{ true: + ~ return false +} \ No newline at end of file diff --git a/examples/inkfiles/glue/testbugfix1.ink.json b/examples/inkfiles/glue/testbugfix1.ink.json new file mode 100644 index 0000000..3f49454 --- /dev/null +++ b/examples/inkfiles/glue/testbugfix1.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/glue/testbugfix2.ink b/examples/inkfiles/glue/testbugfix2.ink new file mode 100644 index 0000000..ce27ec7 --- /dev/null +++ b/examples/inkfiles/glue/testbugfix2.ink @@ -0,0 +1,7 @@ +A {f():B} +X + +=== function f() === +{true: + ~ return false +} \ No newline at end of file diff --git a/examples/inkfiles/glue/testbugfix2.ink.json b/examples/inkfiles/glue/testbugfix2.ink.json new file mode 100644 index 0000000..a81b31d --- /dev/null +++ b/examples/inkfiles/glue/testbugfix2.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/knot/multi-line.ink b/examples/inkfiles/knot/multi-line.ink new file mode 100644 index 0000000..c52d935 --- /dev/null +++ b/examples/inkfiles/knot/multi-line.ink @@ -0,0 +1,3 @@ +Hello, world! +Hello? +Hello, are you there? \ No newline at end of file diff --git a/examples/inkfiles/knot/multi-line.ink.json b/examples/inkfiles/knot/multi-line.ink.json new file mode 100644 index 0000000..3dbc4e8 --- /dev/null +++ b/examples/inkfiles/knot/multi-line.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/knot/param-floats.ink b/examples/inkfiles/knot/param-floats.ink new file mode 100644 index 0000000..c819b1b --- /dev/null +++ b/examples/inkfiles/knot/param-floats.ink @@ -0,0 +1,8 @@ +How much do you give? + * [$1] -> give(1.2) + * [$2] -> give(2.5) + * [Nothing] -> give(0) + + === give(amount) === + You give {amount} dollars. + -> END \ No newline at end of file diff --git a/examples/inkfiles/knot/param-floats.ink.json b/examples/inkfiles/knot/param-floats.ink.json new file mode 100644 index 0000000..5da4669 --- /dev/null +++ b/examples/inkfiles/knot/param-floats.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/knot/param-ints.ink b/examples/inkfiles/knot/param-ints.ink new file mode 100644 index 0000000..2b9596b --- /dev/null +++ b/examples/inkfiles/knot/param-ints.ink @@ -0,0 +1,8 @@ +How much do you give? + * [$1] -> give(1) + * [$2] -> give(2) + * [Nothing] -> give(0) + + === give(amount) === + You give {amount} dollars. + -> END \ No newline at end of file diff --git a/examples/inkfiles/knot/param-ints.ink.json b/examples/inkfiles/knot/param-ints.ink.json new file mode 100644 index 0000000..dc550d0 --- /dev/null +++ b/examples/inkfiles/knot/param-ints.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/knot/param-multi.ink b/examples/inkfiles/knot/param-multi.ink new file mode 100644 index 0000000..bf6ccdd --- /dev/null +++ b/examples/inkfiles/knot/param-multi.ink @@ -0,0 +1,8 @@ +VAR x = 1 + VAR y = "Hmm." + How much do you give? + * [I don't know] -> give(x, 2, y) + + === give(a, b, c) === + You give {a} or {b} dollars. {y} + -> END \ No newline at end of file diff --git a/examples/inkfiles/knot/param-multi.ink.json b/examples/inkfiles/knot/param-multi.ink.json new file mode 100644 index 0000000..7e4cec8 --- /dev/null +++ b/examples/inkfiles/knot/param-multi.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/knot/param-recurse.ink b/examples/inkfiles/knot/param-recurse.ink new file mode 100644 index 0000000..e3a8886 --- /dev/null +++ b/examples/inkfiles/knot/param-recurse.ink @@ -0,0 +1,14 @@ +-> add_one_to_one_hundred(0, 1) + + === add_one_to_one_hundred(total, x) === + ~ total = total + x + { x == 15: + -> finished(total) + - else: + -> add_one_to_one_hundred(total, x + 1) + } + + === finished(total) === + "The result is {total}!" you announce. + Gauss stares at you in horror. + -> END \ No newline at end of file diff --git a/examples/inkfiles/knot/param-recurse.ink.json b/examples/inkfiles/knot/param-recurse.ink.json new file mode 100644 index 0000000..faff9e9 --- /dev/null +++ b/examples/inkfiles/knot/param-recurse.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/knot/param-strings.ink b/examples/inkfiles/knot/param-strings.ink new file mode 100644 index 0000000..c7183b9 --- /dev/null +++ b/examples/inkfiles/knot/param-strings.ink @@ -0,0 +1,8 @@ +Who do you accuse? + * [Accuse Hasting] -> accuse("Hastings") + * [Accuse Mrs Black] -> accuse("Claudia") + * [Accuse myself] -> accuse("myself") + + === accuse(who) === + "I accuse {who}!" Poirot declared. + -> END \ No newline at end of file diff --git a/examples/inkfiles/knot/param-strings.ink.json b/examples/inkfiles/knot/param-strings.ink.json new file mode 100644 index 0000000..9c73ca1 --- /dev/null +++ b/examples/inkfiles/knot/param-strings.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/knot/param-vars.ink b/examples/inkfiles/knot/param-vars.ink new file mode 100644 index 0000000..3ad4eea --- /dev/null +++ b/examples/inkfiles/knot/param-vars.ink @@ -0,0 +1,11 @@ +VAR x = 1 + VAR y = 2 + VAR z = 0 + How much do you give? + * [$1] -> give(x) + * [$2] -> give(y) + * [Nothing] -> give(z) + + === give(amount) === + You give {amount} dollars. + -> END \ No newline at end of file diff --git a/examples/inkfiles/knot/param-vars.ink.json b/examples/inkfiles/knot/param-vars.ink.json new file mode 100644 index 0000000..709477a --- /dev/null +++ b/examples/inkfiles/knot/param-vars.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/knot/single-line.ink b/examples/inkfiles/knot/single-line.ink new file mode 100644 index 0000000..af5626b --- /dev/null +++ b/examples/inkfiles/knot/single-line.ink @@ -0,0 +1 @@ +Hello, world! diff --git a/examples/inkfiles/knot/single-line.ink.json b/examples/inkfiles/knot/single-line.ink.json new file mode 100644 index 0000000..9965165 --- /dev/null +++ b/examples/inkfiles/knot/single-line.ink.json @@ -0,0 +1 @@ +{"inkVersion":21,"root":[["^Hello, world!","\n",["done",{"#n":"g-0"}],null],"done",null],"listDefs":{}} \ No newline at end of file diff --git a/examples/inkfiles/knot/strip-empty-lines.ink b/examples/inkfiles/knot/strip-empty-lines.ink new file mode 100644 index 0000000..066727a --- /dev/null +++ b/examples/inkfiles/knot/strip-empty-lines.ink @@ -0,0 +1,4 @@ +Hello, world! + +Hello? +Hello, are you there? \ No newline at end of file diff --git a/examples/inkfiles/knot/strip-empty-lines.ink.json b/examples/inkfiles/knot/strip-empty-lines.ink.json new file mode 100644 index 0000000..3dbc4e8 --- /dev/null +++ b/examples/inkfiles/knot/strip-empty-lines.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/lists/basic-operations.ink b/examples/inkfiles/lists/basic-operations.ink new file mode 100644 index 0000000..911242a --- /dev/null +++ b/examples/inkfiles/lists/basic-operations.ink @@ -0,0 +1,8 @@ +LIST list = a, (b), c, (d), e +{list} +{(a, c) + (b, e)} +{(a, b, c) ^ (c, b, e)} +{list ? (b, d, e)} +{list ? (d, b)} +{list !? (c)} + \ No newline at end of file diff --git a/examples/inkfiles/lists/basic-operations.ink.json b/examples/inkfiles/lists/basic-operations.ink.json new file mode 100644 index 0000000..6977758 --- /dev/null +++ b/examples/inkfiles/lists/basic-operations.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/lists/bug-adding-element.ink b/examples/inkfiles/lists/bug-adding-element.ink new file mode 100644 index 0000000..87b1a54 --- /dev/null +++ b/examples/inkfiles/lists/bug-adding-element.ink @@ -0,0 +1,14 @@ +LIST gameState = KNOW_ALIEN_REPORT + +- (init) + ++ a + ~ gameState += KNOW_ALIEN_REPORT + -> init + ++ {gameState ? KNOW_ALIEN_REPORT} OK + -> init + ++ FAIL + -> END + diff --git a/examples/inkfiles/lists/bug-adding-element.ink.json b/examples/inkfiles/lists/bug-adding-element.ink.json new file mode 100644 index 0000000..21b68f8 --- /dev/null +++ b/examples/inkfiles/lists/bug-adding-element.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/lists/empty-list-origin-after-assignment.ink b/examples/inkfiles/lists/empty-list-origin-after-assignment.ink new file mode 100644 index 0000000..f3adc65 --- /dev/null +++ b/examples/inkfiles/lists/empty-list-origin-after-assignment.ink @@ -0,0 +1,3 @@ +LIST x = a, b, c +~ x = () +{LIST_ALL(x)} diff --git a/examples/inkfiles/lists/empty-list-origin-after-assignment.ink.json b/examples/inkfiles/lists/empty-list-origin-after-assignment.ink.json new file mode 100644 index 0000000..ddcf32a --- /dev/null +++ b/examples/inkfiles/lists/empty-list-origin-after-assignment.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/lists/empty-list-origin.ink b/examples/inkfiles/lists/empty-list-origin.ink new file mode 100644 index 0000000..f6dd129 --- /dev/null +++ b/examples/inkfiles/lists/empty-list-origin.ink @@ -0,0 +1,2 @@ +LIST list = a, b +{LIST_ALL(list)} diff --git a/examples/inkfiles/lists/empty-list-origin.ink.json b/examples/inkfiles/lists/empty-list-origin.ink.json new file mode 100644 index 0000000..d054578 --- /dev/null +++ b/examples/inkfiles/lists/empty-list-origin.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/lists/list-mixed-items.ink b/examples/inkfiles/lists/list-mixed-items.ink new file mode 100644 index 0000000..58b4257 --- /dev/null +++ b/examples/inkfiles/lists/list-mixed-items.ink @@ -0,0 +1,4 @@ +LIST list = (a), b, (c), d, e +LIST list2 = x, (y), z +{list + list2} + \ No newline at end of file diff --git a/examples/inkfiles/lists/list-mixed-items.ink.json b/examples/inkfiles/lists/list-mixed-items.ink.json new file mode 100644 index 0000000..9359975 --- /dev/null +++ b/examples/inkfiles/lists/list-mixed-items.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/lists/list-range.ink b/examples/inkfiles/lists/list-range.ink new file mode 100644 index 0000000..d5acc65 --- /dev/null +++ b/examples/inkfiles/lists/list-range.ink @@ -0,0 +1,10 @@ +LIST Food = Pizza, Pasta, Curry, Paella +LIST Currency = Pound, Euro, Dollar +LIST Numbers = One, Two, Three, Four, Five, Six, Seven +VAR all = () +~ all = LIST_ALL(Food) + LIST_ALL(Currency) +{all} +{LIST_RANGE(all, 2, 3)} +{LIST_RANGE(LIST_ALL(Numbers), Two, Six)} +{LIST_RANGE((Pizza, Pasta), -1, 100)} // allow out of range + \ No newline at end of file diff --git a/examples/inkfiles/lists/list-range.ink.json b/examples/inkfiles/lists/list-range.ink.json new file mode 100644 index 0000000..0f17c72 --- /dev/null +++ b/examples/inkfiles/lists/list-range.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/lists/list-save-load.ink b/examples/inkfiles/lists/list-save-load.ink new file mode 100644 index 0000000..61015cd --- /dev/null +++ b/examples/inkfiles/lists/list-save-load.ink @@ -0,0 +1,12 @@ +LIST l1 = (a), b, (c) +LIST l2 = (x), y, z + +VAR t = () +~ t = l1 + l2 +{t} + +== elsewhere == +~ t= z +{t} +-> END + diff --git a/examples/inkfiles/lists/list-save-load.ink.json b/examples/inkfiles/lists/list-save-load.ink.json new file mode 100644 index 0000000..b1168b7 --- /dev/null +++ b/examples/inkfiles/lists/list-save-load.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/lists/more-list-operations.ink b/examples/inkfiles/lists/more-list-operations.ink new file mode 100644 index 0000000..0256c10 --- /dev/null +++ b/examples/inkfiles/lists/more-list-operations.ink @@ -0,0 +1,13 @@ +LIST list = l, m = 5, n +{LIST_VALUE(l)} + +{list(1)} + +~ temp t = list() +~ t= n +{t} +~ t = LIST_ALL(t) +~ t -= n +{t} +~ t = LIST_INVERT(t) +{t} diff --git a/examples/inkfiles/lists/more-list-operations.ink.json b/examples/inkfiles/lists/more-list-operations.ink.json new file mode 100644 index 0000000..fd38f89 --- /dev/null +++ b/examples/inkfiles/lists/more-list-operations.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/misc/issue15.ink b/examples/inkfiles/misc/issue15.ink new file mode 100644 index 0000000..d5024f9 --- /dev/null +++ b/examples/inkfiles/misc/issue15.ink @@ -0,0 +1,24 @@ +// Issue reported here: https://github.com/bladecoder/blade-ink/issues/15 +// The correct output has to be: +// This is a test +// X is set + +VAR x = "" + +This is a test +SET_X: +{ + - x == "": + -> x_not_set + - else: + -> x_is_set +} +-> END + += x_not_set +X is not set! +-> END + += x_is_set +X is set +-> END \ No newline at end of file diff --git a/examples/inkfiles/misc/issue15.ink.json b/examples/inkfiles/misc/issue15.ink.json new file mode 100644 index 0000000..d37a4f3 --- /dev/null +++ b/examples/inkfiles/misc/issue15.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/runtime/external-function-0-arg.ink b/examples/inkfiles/runtime/external-function-0-arg.ink new file mode 100644 index 0000000..c188499 --- /dev/null +++ b/examples/inkfiles/runtime/external-function-0-arg.ink @@ -0,0 +1,9 @@ +EXTERNAL externalFunction() + +The value is {externalFunction()}. +-> END + +=== function externalFunction() === +// Usually external functions can only return placeholder +// results, otherwise they'd be defined in ink! +~ return "" \ No newline at end of file diff --git a/examples/inkfiles/runtime/external-function-0-arg.ink.json b/examples/inkfiles/runtime/external-function-0-arg.ink.json new file mode 100644 index 0000000..5bdfb98 --- /dev/null +++ b/examples/inkfiles/runtime/external-function-0-arg.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/runtime/external-function-1-arg.ink b/examples/inkfiles/runtime/external-function-1-arg.ink new file mode 100644 index 0000000..ee4b689 --- /dev/null +++ b/examples/inkfiles/runtime/external-function-1-arg.ink @@ -0,0 +1,9 @@ +EXTERNAL externalFunction(integer) + +The value is {externalFunction(1)}. +-> END + +=== 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/examples/inkfiles/runtime/external-function-1-arg.ink.json b/examples/inkfiles/runtime/external-function-1-arg.ink.json new file mode 100644 index 0000000..7779381 --- /dev/null +++ b/examples/inkfiles/runtime/external-function-1-arg.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/runtime/external-function-2-arg.ink b/examples/inkfiles/runtime/external-function-2-arg.ink new file mode 100644 index 0000000..0867bfd --- /dev/null +++ b/examples/inkfiles/runtime/external-function-2-arg.ink @@ -0,0 +1,7 @@ +EXTERNAL externalFunction(x,y) + +The value is {externalFunction(3, 4.0)}. +-> END + +=== function externalFunction(x,y) === +~ return x + y \ No newline at end of file diff --git a/examples/inkfiles/runtime/external-function-2-arg.ink.json b/examples/inkfiles/runtime/external-function-2-arg.ink.json new file mode 100644 index 0000000..a402fe5 --- /dev/null +++ b/examples/inkfiles/runtime/external-function-2-arg.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/runtime/external-function-3-arg.ink b/examples/inkfiles/runtime/external-function-3-arg.ink new file mode 100644 index 0000000..d5b14bc --- /dev/null +++ b/examples/inkfiles/runtime/external-function-3-arg.ink @@ -0,0 +1,9 @@ +EXTERNAL externalFunction(x,y,z) + +The value is {externalFunction(1, 2, 3)}. +-> END + +=== function externalFunction(x,y,z) === +// Usually external functions can only return placeholder +// results, otherwise they'd be defined in ink! +~ return 0 \ No newline at end of file diff --git a/examples/inkfiles/runtime/external-function-3-arg.ink.json b/examples/inkfiles/runtime/external-function-3-arg.ink.json new file mode 100644 index 0000000..61780c9 --- /dev/null +++ b/examples/inkfiles/runtime/external-function-3-arg.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/runtime/jump-knot.ink b/examples/inkfiles/runtime/jump-knot.ink new file mode 100644 index 0000000..17073f4 --- /dev/null +++ b/examples/inkfiles/runtime/jump-knot.ink @@ -0,0 +1,12 @@ +=== one === +One -> end + +=== two === +Two -> end + +=== three === +Three -> end + + +=== end === +-> END \ No newline at end of file diff --git a/examples/inkfiles/runtime/jump-knot.ink.json b/examples/inkfiles/runtime/jump-knot.ink.json new file mode 100644 index 0000000..ae882fa --- /dev/null +++ b/examples/inkfiles/runtime/jump-knot.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/runtime/jump-stitch.ink b/examples/inkfiles/runtime/jump-stitch.ink new file mode 100644 index 0000000..379c825 --- /dev/null +++ b/examples/inkfiles/runtime/jump-stitch.ink @@ -0,0 +1,20 @@ +=== one === += sone +One.1 -> end + += stwo +One.2 -> end + +=== two === + += sone +Two.1 -> end + += stwo +Two.2 -> end + += sthree +Two.3 -> end + +=== end === +-> END \ No newline at end of file diff --git a/examples/inkfiles/runtime/jump-stitch.ink.json b/examples/inkfiles/runtime/jump-stitch.ink.json new file mode 100644 index 0000000..44d0378 --- /dev/null +++ b/examples/inkfiles/runtime/jump-stitch.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/runtime/load-save.ink b/examples/inkfiles/runtime/load-save.ink new file mode 100644 index 0000000..ccd6921 --- /dev/null +++ b/examples/inkfiles/runtime/load-save.ink @@ -0,0 +1,28 @@ +-> back_in_london + +=== back_in_london === + +We arrived into London at 9.45pm exactly. + +* "There is not a moment to lose!"[] I declared. + -> hurry_outside + +* "Monsieur, let us savour this moment!"[] I declared. + My master clouted me firmly around the head and dragged me out of the door. + -> dragged_outside + +* [We hurried home] -> hurry_outside + + +=== hurry_outside === +We hurried home to Savile Row -> as_fast_as_we_could + + +=== dragged_outside === +He insisted that we hurried home to Savile Row +-> as_fast_as_we_could + + +=== as_fast_as_we_could === +<> as fast as we could. +-> END diff --git a/examples/inkfiles/runtime/load-save.ink.json b/examples/inkfiles/runtime/load-save.ink.json new file mode 100644 index 0000000..a5b5767 --- /dev/null +++ b/examples/inkfiles/runtime/load-save.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/runtime/multiflow-basics.ink b/examples/inkfiles/runtime/multiflow-basics.ink new file mode 100644 index 0000000..4c71e11 --- /dev/null +++ b/examples/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/examples/inkfiles/runtime/multiflow-basics.ink.json b/examples/inkfiles/runtime/multiflow-basics.ink.json new file mode 100644 index 0000000..09d69db --- /dev/null +++ b/examples/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/examples/inkfiles/runtime/multiflow-saveloadthreads.ink b/examples/inkfiles/runtime/multiflow-saveloadthreads.ink new file mode 100644 index 0000000..27dc6cf --- /dev/null +++ b/examples/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/examples/inkfiles/runtime/multiflow-saveloadthreads.ink.json b/examples/inkfiles/runtime/multiflow-saveloadthreads.ink.json new file mode 100644 index 0000000..1a11845 --- /dev/null +++ b/examples/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/examples/inkfiles/runtime/read-visit-counts.ink b/examples/inkfiles/runtime/read-visit-counts.ink new file mode 100644 index 0000000..ba15c1b --- /dev/null +++ b/examples/inkfiles/runtime/read-visit-counts.ink @@ -0,0 +1,17 @@ +VAR x = 0 + +-> one + +=== one === +{ x < 4: + -> two.s2 +- else: + -> two +} + +=== two === +-> END + += s2 +~ x = x + 1 +-> one diff --git a/examples/inkfiles/runtime/read-visit-counts.ink.json b/examples/inkfiles/runtime/read-visit-counts.ink.json new file mode 100644 index 0000000..af3b9e3 --- /dev/null +++ b/examples/inkfiles/runtime/read-visit-counts.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/runtime/saving-loading.ink b/examples/inkfiles/runtime/saving-loading.ink new file mode 100644 index 0000000..d2d7734 --- /dev/null +++ b/examples/inkfiles/runtime/saving-loading.ink @@ -0,0 +1,11 @@ +=== hurry_home === +We hurried home <> +-> to_savile_row + +=== to_savile_row === +to Savile Row +-> as_fast_as_we_could + +=== as_fast_as_we_could === +<> as fast as we could. +-> END \ No newline at end of file diff --git a/examples/inkfiles/runtime/saving-loading.ink.json b/examples/inkfiles/runtime/saving-loading.ink.json new file mode 100644 index 0000000..f42774d --- /dev/null +++ b/examples/inkfiles/runtime/saving-loading.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/runtime/set-get-variables.ink b/examples/inkfiles/runtime/set-get-variables.ink new file mode 100644 index 0000000..98ce5d0 --- /dev/null +++ b/examples/inkfiles/runtime/set-get-variables.ink @@ -0,0 +1,17 @@ +VAR x = 0 + +~ x = 10 + +* [Set variable from code to 15] + +{ x == 15: + +OK + +- else: + +KO + +} + +-> END \ No newline at end of file diff --git a/examples/inkfiles/runtime/set-get-variables.ink.json b/examples/inkfiles/runtime/set-get-variables.ink.json new file mode 100644 index 0000000..4fcf0ce --- /dev/null +++ b/examples/inkfiles/runtime/set-get-variables.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/runtime/variable-observers.ink b/examples/inkfiles/runtime/variable-observers.ink new file mode 100644 index 0000000..f9e7f01 --- /dev/null +++ b/examples/inkfiles/runtime/variable-observers.ink @@ -0,0 +1,9 @@ +VAR x = 0 + +~ x = 5 + +* Sets x = 10 + +~ x = 10 + +-> END \ No newline at end of file diff --git a/examples/inkfiles/runtime/variable-observers.ink.json b/examples/inkfiles/runtime/variable-observers.ink.json new file mode 100644 index 0000000..5316003 --- /dev/null +++ b/examples/inkfiles/runtime/variable-observers.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/stitch/auto-stitch.ink b/examples/inkfiles/stitch/auto-stitch.ink new file mode 100644 index 0000000..1e8ac9f --- /dev/null +++ b/examples/inkfiles/stitch/auto-stitch.ink @@ -0,0 +1,13 @@ +-> the_orient_express + +=== the_orient_express === + + = in_first_class + I settled my master. + * [Move to third class] + -> in_third_class + * [Are you sure] -> the_orient_express + + = in_third_class + I put myself in third. + -> END \ No newline at end of file diff --git a/examples/inkfiles/stitch/auto-stitch.ink.json b/examples/inkfiles/stitch/auto-stitch.ink.json new file mode 100644 index 0000000..c6312af --- /dev/null +++ b/examples/inkfiles/stitch/auto-stitch.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/stitch/manual-stitch.ink b/examples/inkfiles/stitch/manual-stitch.ink new file mode 100644 index 0000000..984d184 --- /dev/null +++ b/examples/inkfiles/stitch/manual-stitch.ink @@ -0,0 +1,15 @@ +-> the_orient_express + +=== the_orient_express === + How shall we travel? + * [In first class] -> in_first_class + * [I'll go cheap] -> the_orient_express.in_third_class + + = in_first_class + I settled my master. + * [Move to third class] + -> in_third_class + + = in_third_class + I put myself in third. + -> END diff --git a/examples/inkfiles/stitch/manual-stitch.ink.json b/examples/inkfiles/stitch/manual-stitch.ink.json new file mode 100644 index 0000000..ea2ed8f --- /dev/null +++ b/examples/inkfiles/stitch/manual-stitch.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/tags/tags.ink b/examples/inkfiles/tags/tags.ink new file mode 100644 index 0000000..834e45f --- /dev/null +++ b/examples/inkfiles/tags/tags.ink @@ -0,0 +1,16 @@ +VAR x = 2 +# author: Joe +# title: My Great Story +This is the content + +== knot == +# knot tag +Knot content +# end of knot tag +-> END + += stitch +# stitch tag +Stitch content +# this tag is below some content so isn't included in the static tags for the stitch +-> END \ No newline at end of file diff --git a/examples/inkfiles/tags/tags.ink.json b/examples/inkfiles/tags/tags.ink.json new file mode 100644 index 0000000..5b5fabf --- /dev/null +++ b/examples/inkfiles/tags/tags.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/tags/tagsDynamicContent.ink b/examples/inkfiles/tags/tagsDynamicContent.ink new file mode 100644 index 0000000..63576b9 --- /dev/null +++ b/examples/inkfiles/tags/tagsDynamicContent.ink @@ -0,0 +1 @@ +tag # pic{5+3}{red|blue}.jpg \ No newline at end of file diff --git a/examples/inkfiles/tags/tagsDynamicContent.ink.json b/examples/inkfiles/tags/tagsDynamicContent.ink.json new file mode 100644 index 0000000..b2ed459 --- /dev/null +++ b/examples/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/examples/inkfiles/tags/tagsInChoice.ink b/examples/inkfiles/tags/tagsInChoice.ink new file mode 100644 index 0000000..77be22b --- /dev/null +++ b/examples/inkfiles/tags/tagsInChoice.ink @@ -0,0 +1 @@ ++ one #one [two #two] three #three -> END \ No newline at end of file diff --git a/examples/inkfiles/tags/tagsInChoice.ink.json b/examples/inkfiles/tags/tagsInChoice.ink.json new file mode 100644 index 0000000..5184748 --- /dev/null +++ b/examples/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/examples/inkfiles/tags/tagsInSeq.ink b/examples/inkfiles/tags/tagsInSeq.ink new file mode 100644 index 0000000..b39f74d --- /dev/null +++ b/examples/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/examples/inkfiles/tags/tagsInSeq.ink.json b/examples/inkfiles/tags/tagsInSeq.ink.json new file mode 100644 index 0000000..5c2dfa2 --- /dev/null +++ b/examples/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/examples/inkfiles/threads/thread-bug.ink b/examples/inkfiles/threads/thread-bug.ink new file mode 100644 index 0000000..4fa40b0 --- /dev/null +++ b/examples/inkfiles/threads/thread-bug.ink @@ -0,0 +1,14 @@ +-> start + +=== start === +Here is some gold. Do you want it? +- (top) + <- choices(-> top) + + Yes + You win! + -> END + +=== choices(-> goback) === ++ No + Try again! + -> goback \ No newline at end of file diff --git a/examples/inkfiles/threads/thread-bug.ink.json b/examples/inkfiles/threads/thread-bug.ink.json new file mode 100644 index 0000000..a8bc091 --- /dev/null +++ b/examples/inkfiles/threads/thread-bug.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/tunnels/tunnel-onwards-divert-override.ink b/examples/inkfiles/tunnels/tunnel-onwards-divert-override.ink new file mode 100644 index 0000000..68c194c --- /dev/null +++ b/examples/inkfiles/tunnels/tunnel-onwards-divert-override.ink @@ -0,0 +1,10 @@ +-> A -> +We will never return to here! + +== A == +This is A +->-> B + +== B == +Now in B. +-> END \ No newline at end of file diff --git a/examples/inkfiles/tunnels/tunnel-onwards-divert-override.ink.json b/examples/inkfiles/tunnels/tunnel-onwards-divert-override.ink.json new file mode 100644 index 0000000..621c7da --- /dev/null +++ b/examples/inkfiles/tunnels/tunnel-onwards-divert-override.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/variable/var-divert.ink b/examples/inkfiles/variable/var-divert.ink new file mode 100644 index 0000000..ac0f28e --- /dev/null +++ b/examples/inkfiles/variable/var-divert.ink @@ -0,0 +1,12 @@ +VAR current_epilogue = -> everybody_dies + Divert as variable example + -> continue_or_quit + + === continue_or_quit + Give up now, or keep trying to save your Kingdom? + * [Keep trying!] -> continue_or_quit + * [Give up] -> current_epilogue + + === everybody_dies + Everybody dies. + -> END \ No newline at end of file diff --git a/examples/inkfiles/variable/var-divert.ink.json b/examples/inkfiles/variable/var-divert.ink.json new file mode 100644 index 0000000..f688692 --- /dev/null +++ b/examples/inkfiles/variable/var-divert.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/variable/varcalc.ink b/examples/inkfiles/variable/varcalc.ink new file mode 100644 index 0000000..c896038 --- /dev/null +++ b/examples/inkfiles/variable/varcalc.ink @@ -0,0 +1,14 @@ + + VAR knows = false + VAR x = 2 + VAR y = 3 + VAR c = 4 + VAR str = "" + ~ knows = true + ~ x = (x * x) - (y * y) + c + ~ y = 2 * x * y + ~ str = "a" + ~ str += "a" + + The values are {knows} and {x} and {y} and {str}. + -> END \ No newline at end of file diff --git a/examples/inkfiles/variable/varcalc.ink.json b/examples/inkfiles/variable/varcalc.ink.json new file mode 100644 index 0000000..29ed74c --- /dev/null +++ b/examples/inkfiles/variable/varcalc.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/variable/variable-declaration.ink b/examples/inkfiles/variable/variable-declaration.ink new file mode 100644 index 0000000..56dd489 --- /dev/null +++ b/examples/inkfiles/variable/variable-declaration.ink @@ -0,0 +1,4 @@ +VAR friendly_name_of_player = "Jackie" + VAR age = 23 + + "My name is Jean Passepartout, but my friend's call me {friendly_name_of_player}. I'm {age} years old." \ No newline at end of file diff --git a/examples/inkfiles/variable/variable-declaration.ink.json b/examples/inkfiles/variable/variable-declaration.ink.json new file mode 100644 index 0000000..e9a1b2c --- /dev/null +++ b/examples/inkfiles/variable/variable-declaration.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/variable/varstringinc.ink b/examples/inkfiles/variable/varstringinc.ink new file mode 100644 index 0000000..6e0e438 --- /dev/null +++ b/examples/inkfiles/variable/varstringinc.ink @@ -0,0 +1,6 @@ +VAR v = "" +~ v = "a" +* inc + ~ v = v + "b" + {v}. + -> END \ No newline at end of file diff --git a/examples/inkfiles/variable/varstringinc.ink.json b/examples/inkfiles/variable/varstringinc.ink.json new file mode 100644 index 0000000..e8c9c52 --- /dev/null +++ b/examples/inkfiles/variable/varstringinc.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/variabletext/cycle.ink b/examples/inkfiles/variabletext/cycle.ink new file mode 100644 index 0000000..3e1c873 --- /dev/null +++ b/examples/inkfiles/variabletext/cycle.ink @@ -0,0 +1,4 @@ +->test +=== test + The radio hissed into life. {&"Three!"|"Two!"|"One!"} + + [Again] -> test \ No newline at end of file diff --git a/examples/inkfiles/variabletext/cycle.ink.json b/examples/inkfiles/variabletext/cycle.ink.json new file mode 100644 index 0000000..5890a29 --- /dev/null +++ b/examples/inkfiles/variabletext/cycle.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/variabletext/empty-elements.ink b/examples/inkfiles/variabletext/empty-elements.ink new file mode 100644 index 0000000..2e58826 --- /dev/null +++ b/examples/inkfiles/variabletext/empty-elements.ink @@ -0,0 +1,4 @@ +-> test +=== test + The radio hissed into life. {||"One!"} + + [Again] -> test \ No newline at end of file diff --git a/examples/inkfiles/variabletext/empty-elements.ink.json b/examples/inkfiles/variabletext/empty-elements.ink.json new file mode 100644 index 0000000..2482a03 --- /dev/null +++ b/examples/inkfiles/variabletext/empty-elements.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/variabletext/list-in-choice.ink b/examples/inkfiles/variabletext/list-in-choice.ink new file mode 100644 index 0000000..7f36b96 --- /dev/null +++ b/examples/inkfiles/variabletext/list-in-choice.ink @@ -0,0 +1,5 @@ +->test + +=== test + He looked at me oddly. + + ["Hello, {&Master|Monsieur|you}!"] -> test \ No newline at end of file diff --git a/examples/inkfiles/variabletext/list-in-choice.ink.json b/examples/inkfiles/variabletext/list-in-choice.ink.json new file mode 100644 index 0000000..e9afe37 --- /dev/null +++ b/examples/inkfiles/variabletext/list-in-choice.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/variabletext/once.ink b/examples/inkfiles/variabletext/once.ink new file mode 100644 index 0000000..f86094d --- /dev/null +++ b/examples/inkfiles/variabletext/once.ink @@ -0,0 +1,4 @@ +->test +=== test + The radio hissed into life. {!"Three!"|"Two!"|"One!"} + + [Again] -> test \ No newline at end of file diff --git a/examples/inkfiles/variabletext/once.ink.json b/examples/inkfiles/variabletext/once.ink.json new file mode 100644 index 0000000..ae9a77e --- /dev/null +++ b/examples/inkfiles/variabletext/once.ink.json @@ -0,0 +1 @@ +{"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/examples/inkfiles/variabletext/sequence.ink b/examples/inkfiles/variabletext/sequence.ink new file mode 100644 index 0000000..56e382d --- /dev/null +++ b/examples/inkfiles/variabletext/sequence.ink @@ -0,0 +1,5 @@ +-> test + +=== test + The radio hissed into life. {"Three!"|"Two!"|"One!"|There was the white noise racket of an explosion.} + + [Again] -> test \ No newline at end of file diff --git a/examples/inkfiles/variabletext/sequence.ink.json b/examples/inkfiles/variabletext/sequence.ink.json new file mode 100644 index 0000000..be4e896 --- /dev/null +++ b/examples/inkfiles/variabletext/sequence.ink.json @@ -0,0 +1 @@ +{"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 diff --git a/src/json_serialization.rs b/src/json_serialization.rs new file mode 100644 index 0000000..792e65b --- /dev/null +++ b/src/json_serialization.rs @@ -0,0 +1,15 @@ +use serde_json::Value; + +pub fn jtoken_to_runtime_object(token: &Value) { + +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn basic_test() { + + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 7d12d9a..89cca4c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,2 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +mod story; +mod json_serialization; diff --git a/src/story.rs b/src/story.rs new file mode 100644 index 0000000..a3311d1 --- /dev/null +++ b/src/story.rs @@ -0,0 +1,72 @@ +#![allow(unused_variables, dead_code)] + +use crate::json_serialization; + +const INK_VERSION_CURRENT: i32 = 21; +const INK_VERSION_MINIMUM_COMPATIBLE: i32 = 18; + +pub struct Story {} + +impl Story { + pub fn new(json_string: &str) -> Result { + let json: serde_json::Value = match serde_json::from_str(json_string) { + Ok(value) => value, + Err(_) => return Err("Story not in JSON format.".to_string()), + }; + + let version_opt = json.get("inkVersion"); + + if version_opt.is_none() || !version_opt.unwrap().is_number() { + return Err( + "ink version number not found. Are you sure it's a valid .ink.json file?" + .to_string(), + ); + } + + let version: i32 = version_opt.unwrap().as_i64().unwrap().try_into().unwrap(); + + if version > INK_VERSION_CURRENT { + return Err("Version of ink used to build story was newer than the current version of the engine".to_string()); + } else if version < INK_VERSION_MINIMUM_COMPATIBLE { + return Err("Version of ink used to build story is too old to be loaded by this version of the engine".to_string()); + } else if version != INK_VERSION_CURRENT { + log::debug!("WARNING: Version of ink used to build story doesn't match current version of engine. Non-critical, but recommend synchronising."); + } + + let rootToken = match json.get("root") { + Some(value) => value, + None => { + return Err( + "Root node for ink not found. Are you sure it's a valid .ink.json file?" + .to_string(), + ) + } + }; + + //object listDefsObj; + //if (rootObject.TryGetValue ("listDefs", out listDefsObj)) { + // _listDefinitions = Json.JTokenToListDefinitions (listDefsObj); + //} + + //_mainContentContainer = Json.JTokenToRuntimeObject (rootToken) as Container; + json_serialization::jtoken_to_runtime_object(rootToken); + + //ResetState (); + + Ok(Story {}) + } +} + +#[cfg(test)] +mod tests { + use std::fs; + + use super::*; + + #[test] + fn create_test() { + let json_string = + fs::read_to_string("examples/inkfiles/basictext/oneline.ink.json").unwrap(); + Story::new(&json_string).unwrap(); + } +} From b16a70a8aa45f918657f18d7a42a3680def5e72b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Mon, 13 Feb 2023 01:05:46 +0000 Subject: [PATCH 05/91] Deserializing values and container --- src/container.rs | 22 ++++++++ src/ink_value.rs | 106 ++++++++++++++++++++++++++++++++++++++ src/json_serialization.rs | 89 +++++++++++++++++++++++++++++--- src/lib.rs | 4 ++ src/rt_object.rs | 25 +++++++++ src/story.rs | 12 +++-- 6 files changed, 247 insertions(+), 11 deletions(-) create mode 100644 src/container.rs create mode 100644 src/ink_value.rs create mode 100644 src/rt_object.rs diff --git a/src/container.rs b/src/container.rs new file mode 100644 index 0000000..e28446f --- /dev/null +++ b/src/container.rs @@ -0,0 +1,22 @@ +use std::{collections::HashMap, any::Any}; + +use crate::rt_object::RTObject; + +pub struct Container { + pub content: Vec>, + pub name: Option, + pub count_flags: i32, + //named_content: HashMap +} + +impl Container { + pub fn new(content: Vec>, name: Option, count_flags: i32) -> Box { + Box::new(Container{content, name, count_flags}) + } +} + +impl RTObject for Container { + fn as_any(&self) -> &dyn Any { + self + } +} diff --git a/src/ink_value.rs b/src/ink_value.rs new file mode 100644 index 0000000..ac29ded --- /dev/null +++ b/src/ink_value.rs @@ -0,0 +1,106 @@ +// downcast using Any: https://bennett.dev/rust/downcast-trait-object/ +// enum with integers: https://enodev.fr/posts/rusticity-convert-an-integer-to-an-enum.html + +use std::any::Any; + +use crate::rt_object::RTObject; + +enum ValueType { + Bool = -1, + Int, + Float, + List, + String, + + // Not used for coersion described above + DivertTarget, + VariablePointer, +} + +trait InkValue: RTObject { + fn value_type() -> ValueType; + fn is_truthy(&self) -> bool; + //fn cast() -> dyn InkValue; + //fn value_object() -> object; +} + +// Bool Value +pub struct BoolValue { + value: bool, +} + +impl RTObject for BoolValue { + fn as_any(&self) -> &dyn Any { + self + } +} + +impl InkValue for BoolValue { + fn value_type() -> ValueType { + return ValueType::Bool; + } + + fn is_truthy(&self) -> bool { + return self.value; + } +} + +impl BoolValue { + pub fn new(value: bool) -> Box { + Box::new(BoolValue { value }) + } +} + +// Int Value +pub struct IntValue { + value: i32, +} + +impl RTObject for IntValue { + fn as_any(&self) -> &dyn Any { + self + } +} + +impl InkValue for IntValue { + fn value_type() -> ValueType { + ValueType::Int + } + + fn is_truthy(&self) -> bool { + self.value != 0 + } +} + +impl IntValue { + pub fn new(value: i32) -> Box { + Box::new(IntValue { value }) + } +} + +// Float Value +pub struct FloatValue { + value: f32, +} + +impl RTObject for FloatValue { + fn as_any(&self) -> &dyn Any { + self + } +} + +impl InkValue for FloatValue { + fn value_type() -> ValueType { + ValueType::Float + } + + fn is_truthy(&self) -> bool { + self.value != 0.0 + } +} + +impl FloatValue { + pub fn new(value: f32) -> Box { + Box::new(FloatValue { value }) + } +} diff --git a/src/json_serialization.rs b/src/json_serialization.rs index 792e65b..2e80a32 100644 --- a/src/json_serialization.rs +++ b/src/json_serialization.rs @@ -1,15 +1,92 @@ +use std::collections::HashMap; + use serde_json::Value; -pub fn jtoken_to_runtime_object(token: &Value) { +use crate::{ + container::Container, + ink_value::{BoolValue, FloatValue, IntValue}, + rt_object::{self, RTObject}, +}; + +pub fn jtoken_to_runtime_object(token: &Value) -> Box { + match token { + Value::Null => Box::new(rt_object::Null), + Value::Bool(value) => BoolValue::new(value.clone()), + Value::Number(_) => { + if token.is_i64() { + IntValue::new(token.as_i64().unwrap().try_into().unwrap()) + } else { + let val: f32 = token.as_f64().unwrap() as f32; + FloatValue::new(val) + } + } + Value::String(_) => todo!(), + Value::Array(value) => jarray_to_container(value), + Value::Object(_) => todo!(), + } +} + +fn jarray_to_container(jarray: &Vec) -> Box { + let container_content = jarray_to_runtime_obj_list(jarray, true); + + // Final object in the array is always a combination of + // - named content + // - a "#f" key with the countFlags + // (if either exists at all, otherwise null) + let terminatingObj = jarray[jarray.len() - 1].as_object(); + let mut name: Option = None; + let mut flags = 0; + + if terminatingObj.is_some() { + let terminatingObj = terminatingObj.unwrap(); + let namedOnlyContent: HashMap> = + HashMap::with_capacity(terminatingObj.len()); + + for (k, v) in terminatingObj { + match k.as_str() { + "#f" => flags = v.as_i64().unwrap().try_into().unwrap(), + "#n" => name = Some(v.as_str().unwrap().to_string()), + _ => { + let namedContentItem = jtoken_to_runtime_object(v); + /* TODO + let namedSubContainer = namedContentItem as Container; + if namedSubContainer { + namedSubContainer.name = k; + } + + namedOnlyContent[k] = namedContentItem; + */ + } + } + } + + // TODO container.namedOnlyContent = namedOnlyContent; + } + + Container::new(container_content, name, flags) +} + +fn jarray_to_runtime_obj_list(jarray: &Vec, skip_last: bool) -> Vec> { + let mut count = jarray.len(); + + if skip_last { + count -= 1; + } + + let mut list: Vec> = Vec::with_capacity(jarray.len()); + + for i in 0..count { + let jtok = &jarray[i]; + let runtime_obj = jtoken_to_runtime_object(&jtok); + list.push(runtime_obj); + } + list } #[cfg(test)] mod tests { - use super::*; #[test] - fn basic_test() { - - } -} \ No newline at end of file + fn simple_test() {} +} diff --git a/src/lib.rs b/src/lib.rs index 89cca4c..6f1c43a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,6 @@ mod story; mod json_serialization; +mod rt_object; +mod ink_value; +mod container; + diff --git a/src/rt_object.rs b/src/rt_object.rs new file mode 100644 index 0000000..2517b6d --- /dev/null +++ b/src/rt_object.rs @@ -0,0 +1,25 @@ +/* +// location feature +struct RTObject { + //parent: &RTObject, + //path: Path, + //debug_metadata: DebugMetadata, +} + +impl RTObject { + +} +*/ + +use std::any::Any; + +pub trait RTObject { + fn as_any(&self) -> &dyn Any; +} + +pub struct Null; +impl RTObject for Null { + fn as_any(&self) -> &dyn Any { + self + } +} diff --git a/src/story.rs b/src/story.rs index a3311d1..211c136 100644 --- a/src/story.rs +++ b/src/story.rs @@ -1,11 +1,13 @@ #![allow(unused_variables, dead_code)] -use crate::json_serialization; +use crate::{json_serialization, container::Container, rt_object::RTObject}; const INK_VERSION_CURRENT: i32 = 21; const INK_VERSION_MINIMUM_COMPATIBLE: i32 = 18; -pub struct Story {} +pub struct Story{ + main_content_container: Box, +} impl Story { pub fn new(json_string: &str) -> Result { @@ -48,12 +50,12 @@ impl Story { // _listDefinitions = Json.JTokenToListDefinitions (listDefsObj); //} - //_mainContentContainer = Json.JTokenToRuntimeObject (rootToken) as Container; - json_serialization::jtoken_to_runtime_object(rootToken); + + let main_content_container = json_serialization::jtoken_to_runtime_object(rootToken); //ResetState (); - Ok(Story {}) + Ok(Story {main_content_container}) } } From f57812967a5511645a89bf8abc7f6998a1daf7c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Mon, 13 Feb 2023 02:26:26 +0000 Subject: [PATCH 06/91] Parses oneline test --- src/control_command.rs | 44 ++++++++++++++++++ src/ink_value.rs | 27 +++++++++++ src/json_serialization.rs | 95 +++++++++++++++++++++++++++++++++------ src/lib.rs | 1 + src/story.rs | 2 +- 5 files changed, 154 insertions(+), 15 deletions(-) create mode 100644 src/control_command.rs diff --git a/src/control_command.rs b/src/control_command.rs new file mode 100644 index 0000000..3ec7116 --- /dev/null +++ b/src/control_command.rs @@ -0,0 +1,44 @@ +use std::any::Any; + +use crate::rt_object::RTObject; + +pub enum ControlCommand { + 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 +} + +impl ControlCommand { + +} + +impl RTObject for ControlCommand { + fn as_any(&self) -> &dyn Any { + self + } +} + diff --git a/src/ink_value.rs b/src/ink_value.rs index ac29ded..13bb863 100644 --- a/src/ink_value.rs +++ b/src/ink_value.rs @@ -104,3 +104,30 @@ impl FloatValue { Box::new(FloatValue { value }) } } + +// String Value +pub struct StringValue { + value: String, +} + +impl RTObject for StringValue { + fn as_any(&self) -> &dyn Any { + self + } +} + +impl InkValue for StringValue { + fn value_type() -> ValueType { + ValueType::String + } + + fn is_truthy(&self) -> bool { + self.value.len() > 0 + } +} + +impl StringValue { + pub fn new(value: String) -> Box { + Box::new(StringValue { value }) + } +} diff --git a/src/json_serialization.rs b/src/json_serialization.rs index 2e80a32..450454a 100644 --- a/src/json_serialization.rs +++ b/src/json_serialization.rs @@ -4,29 +4,62 @@ use serde_json::Value; use crate::{ container::Container, - ink_value::{BoolValue, FloatValue, IntValue}, - rt_object::{self, RTObject}, + ink_value::{BoolValue, FloatValue, IntValue, StringValue}, + rt_object::{self, RTObject}, control_command::{ControlCommand, self}, }; -pub fn jtoken_to_runtime_object(token: &Value) -> Box { +pub fn jtoken_to_runtime_object(token: &Value) -> Result, String> { match token { - Value::Null => Box::new(rt_object::Null), - Value::Bool(value) => BoolValue::new(value.clone()), + Value::Null => Ok(Box::new(rt_object::Null)), + Value::Bool(value) => Ok(BoolValue::new(value.clone())), Value::Number(_) => { if token.is_i64() { - IntValue::new(token.as_i64().unwrap().try_into().unwrap()) + Ok(IntValue::new(token.as_i64().unwrap().try_into().unwrap())) } else { let val: f32 = token.as_f64().unwrap() as f32; - FloatValue::new(val) + Ok(FloatValue::new(val)) } } - Value::String(_) => todo!(), - Value::Array(value) => jarray_to_container(value), + + Value::String(value) => { + let str = value; + // String value + let firstChar = str.chars().next().unwrap(); + if firstChar == '^' {return Ok(StringValue::new(str[1..].to_string()));} + else if firstChar == '\n' && str.len() == 1 {return Ok(StringValue::new("\n".to_string()));} + + // Glue + // TODO if "<>".eq(str) {return new Glue();} + + if let Some(controlCommand) = create_control_command(str) { + return Ok(Box::new(controlCommand)); + } + + /* TODO + + // 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^".eq(str)) {str = "^";} + if NativeFunctionCall.callExistsWithName(str) {return NativeFunctionCall.callWithName(str);} + + // Pop + if ("->->".eq(str)) {return ControlCommand.popTunnel();} + else if ("~ret".eq(str)) {return ControlCommand.popFunction();} + + // Void + if ("void".eq(str)) {return new Void();} + */ + + Err("Failed to convert token to runtime RTObject: ".to_string() + &token.to_string()) + }, + Value::Array(value) => Ok(jarray_to_container(value)?), Value::Object(_) => todo!(), } } -fn jarray_to_container(jarray: &Vec) -> Box { +fn jarray_to_container(jarray: &Vec) -> Result, String> { let container_content = jarray_to_runtime_obj_list(jarray, true); // Final object in the array is always a combination of @@ -63,10 +96,10 @@ fn jarray_to_container(jarray: &Vec) -> Box { // TODO container.namedOnlyContent = namedOnlyContent; } - Container::new(container_content, name, flags) + Ok(Container::new(container_content?, name, flags)) } -fn jarray_to_runtime_obj_list(jarray: &Vec, skip_last: bool) -> Vec> { +fn jarray_to_runtime_obj_list(jarray: &Vec, skip_last: bool) -> Result>, String> { let mut count = jarray.len(); if skip_last { @@ -78,10 +111,44 @@ fn jarray_to_runtime_obj_list(jarray: &Vec, skip_last: bool) -> Vec Option { + let result = match name { + "ev" => Some(ControlCommand::EvalStart), + "out" => Some(ControlCommand::EvalOutput), + "/ev" => Some(ControlCommand::EvalEnd), + "du" => Some(ControlCommand::Duplicate), + "pop" => Some(ControlCommand::PopEvaluatedValue), + "~ret" => Some(ControlCommand::PopFunction), + "->->" => Some(ControlCommand::PopTunnel), + "str" => Some(ControlCommand::BeginString), + "/str" => Some(ControlCommand::EndString), + "nop" => Some(ControlCommand::NoOp), + "choiceCnt" => Some(ControlCommand::ChoiceCount), + "turn" => Some(ControlCommand::Turns), + "turns" => Some(ControlCommand::TurnsSince), + "readc" => Some(ControlCommand::ReadCount), + "rnd" => Some(ControlCommand::Random), + "srnd" => Some(ControlCommand::SeedRandom), + "visit" => Some(ControlCommand::VisitIndex), + "seq" => Some(ControlCommand::SequenceShuffleIndex), + "thread" => Some(ControlCommand::StartThread), + "done" => Some(ControlCommand::Done), + "end" => Some(ControlCommand::End), + "listInt" => Some(ControlCommand::ListFromInt), + "range" => Some(ControlCommand::ListRange), + "lrnd" => Some(ControlCommand::ListRandom), + "#" => Some(ControlCommand::BeginTag), + "/#" => Some(ControlCommand::EndTag), + _ => None, + }; + + result } #[cfg(test)] diff --git a/src/lib.rs b/src/lib.rs index 6f1c43a..a3d16af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,4 +3,5 @@ mod json_serialization; mod rt_object; mod ink_value; mod container; +mod control_command; diff --git a/src/story.rs b/src/story.rs index 211c136..f308ea5 100644 --- a/src/story.rs +++ b/src/story.rs @@ -51,7 +51,7 @@ impl Story { //} - let main_content_container = json_serialization::jtoken_to_runtime_object(rootToken); + let main_content_container = json_serialization::jtoken_to_runtime_object(rootToken)?; //ResetState (); From 1625fb28884a9a08258b657d3f41209e0da7d47e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Wed, 17 May 2023 17:31:14 +0000 Subject: [PATCH 07/91] second try --- .devcontainer/devcontainer.json | 2 +- Cargo.toml | 4 + src/callstack.rs | 27 +++ src/container.rs | 165 ++++++++++++++++++- src/control_command.rs | 29 +++- src/ink_value.rs | 133 --------------- src/json_serialization.rs | 131 ++++++++------- src/lib.rs | 11 +- src/object.rs | 127 ++++++++++++++ src/object_enum.rs | 32 ++++ src/path.rs | 283 ++++++++++++++++++++++++++++++++ src/pointer.rs | 69 ++++++++ src/rt_object.rs | 25 --- src/search_result.rs | 3 + src/story.rs | 102 +++++++++++- src/story_state.rs | 29 ++++ src/value.rs | 67 ++++++++ 17 files changed, 992 insertions(+), 247 deletions(-) create mode 100644 src/callstack.rs delete mode 100644 src/ink_value.rs create mode 100644 src/object.rs create mode 100644 src/object_enum.rs create mode 100644 src/path.rs create mode 100644 src/pointer.rs delete mode 100644 src/rt_object.rs create mode 100644 src/search_result.rs create mode 100644 src/story_state.rs create mode 100644 src/value.rs diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b5b68f1..093bbb1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,7 +3,7 @@ { "name": "Rust", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/rust:0-1-bullseye" + "image": "mcr.microsoft.com/devcontainers/rust:bullseye" // Features to add to the dev container. More info: https://containers.dev/features. // "features": {}, diff --git a/Cargo.toml b/Cargo.toml index 005a6a5..4402d2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,7 @@ path = "src/bin/console-player.rs" serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.93" log = "0.4.17" +strum_macros = "0.24" +strum = { version = "0.24", features = ["derive"] } +as-any = "0.3.0" + diff --git a/src/callstack.rs b/src/callstack.rs new file mode 100644 index 0000000..f999334 --- /dev/null +++ b/src/callstack.rs @@ -0,0 +1,27 @@ +use crate::pointer::Pointer; + +pub(crate) struct CallStack { + +} + +impl CallStack { + pub(crate) fn get_current_element(&self) -> &Element { + todo!() + } +} + +pub(crate) struct Element { + pub(crate) current_pointer: Pointer, +} + +impl Element { + +} + +pub(crate) struct Thread { + +} + +impl Thread { + +} \ No newline at end of file diff --git a/src/container.rs b/src/container.rs index e28446f..0476e3d 100644 --- a/src/container.rs +++ b/src/container.rs @@ -1,22 +1,173 @@ -use std::{collections::HashMap, any::Any}; +use std::{ + cell::{RefCell, Ref}, + fmt, + rc::Rc, +}; -use crate::rt_object::RTObject; +use crate::{ + object::{Object, RTObject}, + object_enum::ObjectEnum, + value::ValueType, +}; pub struct Container { - pub content: Vec>, + obj: Object, + pub content: Vec, pub name: Option, pub count_flags: i32, //named_content: HashMap } impl Container { - pub fn new(content: Vec>, name: Option, count_flags: i32) -> Box { - Box::new(Container{content, name, count_flags}) + pub fn new(name: Option, count_flags: i32) -> Container { + Container { + obj: Object::new(), + content: Vec::new(), + name, + count_flags, + } + } + + pub fn add_contents(container: &Rc>, objs: &Vec) { + objs.iter() + .for_each(|o| Container::add_content(container, o)); + } + + pub fn add_content(container: &Rc>, obj: &ObjectEnum) { + container.as_ref().borrow_mut().content.push(obj.clone()); + obj.get_obj_mut().parent = Some(container.clone()); + } + + pub fn has_valid_name(&self) -> bool { + self.name.is_some() && !self.name.as_ref().unwrap().is_empty() + } + + pub(crate) fn get_name(&self) -> &str { + todo!() + } + + pub fn build_string_of_hierarchy( + &self, + sb: &mut String, + indentation: usize, + pointed_obj: Option, + ) { + Container::append_indentation(sb, indentation); + + sb.push('['); + + if self.has_valid_name() { + sb.push_str(" ({"); + sb.push_str(self.name.as_ref().unwrap()); + sb.push_str("})"); + } + + if let Some(pointed_obj) = pointed_obj { + if let ObjectEnum::Container(obj) = pointed_obj { + if std::ptr::eq(obj.as_ptr(), self) { + sb.push_str(" <---"); + } + } + } + + sb.push('\n'); + let indentation = indentation + 1; + + for (i, obj) in self.content.iter().enumerate() { + match obj { + ObjectEnum::Container(c) => { + c.as_ref() + .borrow() + .build_string_of_hierarchy(sb, indentation, pointed_obj); + } + + ObjectEnum::Value(v) => { + Container::append_indentation(sb, indentation); + if let ValueType::String(s) = v.as_ref().borrow().value { + sb.push('\"'); + sb.push_str(&s.clone().replace('\n', "\\n")); + sb.push('\"'); + } else { + sb.push_str(&v.as_ref().borrow().to_string()); + } + } + + ObjectEnum::ControlCommand(o) => { + sb.push_str(&o.as_ref().borrow().to_string()); + } + + ObjectEnum::Null(o) => { + sb.push_str(&o.as_ref().borrow().to_string()); + } + } + + if i != self.content.len() - 1 { + sb.push(','); + } + + if let Some(pointed_obj) = pointed_obj { + if let ObjectEnum::Container(pointed_obj) = pointed_obj { + if let ObjectEnum::Container(obj) = obj { + if &obj.as_ref().borrow() as *const _ + == &pointed_obj.as_ref().borrow() as *const _ + { + sb.push_str(" <---"); + } + } + } + } + + sb.push('\n'); + } + + /* TODO + 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"); + } + } + */ + + let indentation = indentation - 1; + Container::append_indentation(sb, indentation); + sb.push(']'); + } + + fn append_indentation(sb: &mut String, indentation: usize) { + const SPACES_PER_INDENT: usize = 4; + + for _ in 0..(SPACES_PER_INDENT * indentation) { + sb.push(' '); + } } } impl RTObject for Container { - fn as_any(&self) -> &dyn Any { - self + fn get_object(&self) -> &Object { + &self.obj + } +} + +impl fmt::Display for Container { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "**Container**") } } diff --git a/src/control_command.rs b/src/control_command.rs index 3ec7116..9daf389 100644 --- a/src/control_command.rs +++ b/src/control_command.rs @@ -1,8 +1,11 @@ -use std::any::Any; +use std::{any::Any, fmt, rc::Rc}; -use crate::rt_object::RTObject; +use strum::Display; -pub enum ControlCommand { +use crate::{object::{RTObject, Object}, container::Container}; + +#[derive(Display)] +pub enum CommandType { NotSet, EvalStart, EvalOutput, @@ -32,13 +35,27 @@ pub enum ControlCommand { EndTag } -impl ControlCommand { +pub(crate) struct ControlCommand { + obj: Object, + command_type: CommandType +} +impl ControlCommand { + pub(crate) fn new(command_type: CommandType) -> Self { + ControlCommand {obj: Object::new(), command_type} + } } impl RTObject for ControlCommand { - fn as_any(&self) -> &dyn Any { - self + fn get_object(&self) -> &Object { + &self.obj + } +} + +impl fmt::Display for ControlCommand { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.command_type.to_string()) } } + diff --git a/src/ink_value.rs b/src/ink_value.rs deleted file mode 100644 index 13bb863..0000000 --- a/src/ink_value.rs +++ /dev/null @@ -1,133 +0,0 @@ -// downcast using Any: https://bennett.dev/rust/downcast-trait-object/ -// enum with integers: https://enodev.fr/posts/rusticity-convert-an-integer-to-an-enum.html - -use std::any::Any; - -use crate::rt_object::RTObject; - -enum ValueType { - Bool = -1, - Int, - Float, - List, - String, - - // Not used for coersion described above - DivertTarget, - VariablePointer, -} - -trait InkValue: RTObject { - fn value_type() -> ValueType; - fn is_truthy(&self) -> bool; - //fn cast() -> dyn InkValue; - //fn value_object() -> object; -} - -// Bool Value -pub struct BoolValue { - value: bool, -} - -impl RTObject for BoolValue { - fn as_any(&self) -> &dyn Any { - self - } -} - -impl InkValue for BoolValue { - fn value_type() -> ValueType { - return ValueType::Bool; - } - - fn is_truthy(&self) -> bool { - return self.value; - } -} - -impl BoolValue { - pub fn new(value: bool) -> Box { - Box::new(BoolValue { value }) - } -} - -// Int Value -pub struct IntValue { - value: i32, -} - -impl RTObject for IntValue { - fn as_any(&self) -> &dyn Any { - self - } -} - -impl InkValue for IntValue { - fn value_type() -> ValueType { - ValueType::Int - } - - fn is_truthy(&self) -> bool { - self.value != 0 - } -} - -impl IntValue { - pub fn new(value: i32) -> Box { - Box::new(IntValue { value }) - } -} - -// Float Value -pub struct FloatValue { - value: f32, -} - -impl RTObject for FloatValue { - fn as_any(&self) -> &dyn Any { - self - } -} - -impl InkValue for FloatValue { - fn value_type() -> ValueType { - ValueType::Float - } - - fn is_truthy(&self) -> bool { - self.value != 0.0 - } -} - -impl FloatValue { - pub fn new(value: f32) -> Box { - Box::new(FloatValue { value }) - } -} - -// String Value -pub struct StringValue { - value: String, -} - -impl RTObject for StringValue { - fn as_any(&self) -> &dyn Any { - self - } -} - -impl InkValue for StringValue { - fn value_type() -> ValueType { - ValueType::String - } - - fn is_truthy(&self) -> bool { - self.value.len() > 0 - } -} - -impl StringValue { - pub fn new(value: String) -> Box { - Box::new(StringValue { value }) - } -} diff --git a/src/json_serialization.rs b/src/json_serialization.rs index 450454a..0cbc7dd 100644 --- a/src/json_serialization.rs +++ b/src/json_serialization.rs @@ -1,38 +1,36 @@ -use std::collections::HashMap; - -use serde_json::Value; +use std::{collections::HashMap, rc::Rc, cell::RefCell}; use crate::{ container::Container, - ink_value::{BoolValue, FloatValue, IntValue, StringValue}, - rt_object::{self, RTObject}, control_command::{ControlCommand, self}, + object::{self, RTObject}, control_command::{CommandType, ControlCommand}, value::Value, object_enum::ObjectEnum, }; -pub fn jtoken_to_runtime_object(token: &Value) -> Result, String> { +pub fn jtoken_to_runtime_object(token: &serde_json::Value) -> Result { match token { - Value::Null => Ok(Box::new(rt_object::Null)), - Value::Bool(value) => Ok(BoolValue::new(value.clone())), - Value::Number(_) => { + serde_json::Value::Null => Ok(ObjectEnum::Null(Rc::new(RefCell::new(object::Null::new())))), + serde_json::Value::Bool(value) => Ok(ObjectEnum::Value(Rc::new(RefCell::new(Value::new_bool(value.to_owned()))))), + serde_json::Value::Number(_) => { if token.is_i64() { - Ok(IntValue::new(token.as_i64().unwrap().try_into().unwrap())) + let val:i32 = token.as_i64().unwrap().try_into().unwrap(); + Ok(ObjectEnum::Value(Rc::new(RefCell::new(Value::new_int(val))))) } else { let val: f32 = token.as_f64().unwrap() as f32; - Ok(FloatValue::new(val)) + Ok(ObjectEnum::Value(Rc::new(RefCell::new(Value::new_float(val))))) } } - Value::String(value) => { + serde_json::Value::String(value) => { let str = value; // String value - let firstChar = str.chars().next().unwrap(); - if firstChar == '^' {return Ok(StringValue::new(str[1..].to_string()));} - else if firstChar == '\n' && str.len() == 1 {return Ok(StringValue::new("\n".to_string()));} + let first_char = str.chars().next().unwrap(); + if first_char == '^' {return Ok(ObjectEnum::Value(Rc::new(RefCell::new(Value::new_string(&str[1..])))));} + else if first_char == '\n' && str.len() == 1 {return Ok(ObjectEnum::Value(Rc::new(RefCell::new(Value::new_string("\n")))));} // Glue // TODO if "<>".eq(str) {return new Glue();} - if let Some(controlCommand) = create_control_command(str) { - return Ok(Box::new(controlCommand)); + if let Some(control_command) = create_control_command(str) { + return Ok(ObjectEnum::ControlCommand(Rc::new(RefCell::new(control_command)))); } /* TODO @@ -45,8 +43,8 @@ pub fn jtoken_to_runtime_object(token: &Value) -> Result, Stri if NativeFunctionCall.callExistsWithName(str) {return NativeFunctionCall.callWithName(str);} // Pop - if ("->->".eq(str)) {return ControlCommand.popTunnel();} - else if ("~ret".eq(str)) {return ControlCommand.popFunction();} + if ("->->".eq(str)) {return CommandType.popTunnel();} + else if ("~ret".eq(str)) {return CommandType.popFunction();} // Void if ("void".eq(str)) {return new Void();} @@ -54,35 +52,35 @@ pub fn jtoken_to_runtime_object(token: &Value) -> Result, Stri Err("Failed to convert token to runtime RTObject: ".to_string() + &token.to_string()) }, - Value::Array(value) => Ok(jarray_to_container(value)?), - Value::Object(_) => todo!(), + serde_json::Value::Array(value) => Ok(ObjectEnum::Container(jarray_to_container(value)?)), + serde_json::Value::Object(_) => todo!(), } } -fn jarray_to_container(jarray: &Vec) -> Result, String> { - let container_content = jarray_to_runtime_obj_list(jarray, true); +fn jarray_to_container(jarray: &Vec) -> Result>, String> { + let container = Rc::new(RefCell::new(Container::new(None, 0))); + Container::add_contents(&container, &jarray_to_runtime_obj_list(jarray, true)?); // Final object in the array is always a combination of // - named content // - a "#f" key with the countFlags // (if either exists at all, otherwise null) - let terminatingObj = jarray[jarray.len() - 1].as_object(); + let terminating_obj = jarray[jarray.len() - 1].as_object(); let mut name: Option = None; let mut flags = 0; - if terminatingObj.is_some() { - let terminatingObj = terminatingObj.unwrap(); - let namedOnlyContent: HashMap> = - HashMap::with_capacity(terminatingObj.len()); + if let Some(terminating_obj) = terminating_obj { + let named_only_content: HashMap> = + HashMap::with_capacity(terminating_obj.len()); - for (k, v) in terminatingObj { + for (k, v) in terminating_obj { match k.as_str() { - "#f" => flags = v.as_i64().unwrap().try_into().unwrap(), - "#n" => name = Some(v.as_str().unwrap().to_string()), + "#f" => container.borrow_mut().count_flags = v.as_i64().unwrap().try_into().unwrap(), + "#n" => container.borrow_mut().name = Some(v.as_str().unwrap().to_string()), _ => { - let namedContentItem = jtoken_to_runtime_object(v); + let named_content_item = jtoken_to_runtime_object(v); /* TODO - let namedSubContainer = namedContentItem as Container; + let namedSubContainer = named_content_item as Container; if namedSubContainer { namedSubContainer.name = k; } @@ -96,59 +94,58 @@ fn jarray_to_container(jarray: &Vec) -> Result, String> { // TODO container.namedOnlyContent = namedOnlyContent; } - Ok(Container::new(container_content?, name, flags)) + Ok(container) } -fn jarray_to_runtime_obj_list(jarray: &Vec, skip_last: bool) -> Result>, String> { +fn jarray_to_runtime_obj_list(jarray: &Vec, skip_last: bool) -> Result, String> { let mut count = jarray.len(); if skip_last { count -= 1; } - let mut list: Vec> = Vec::with_capacity(jarray.len()); + let mut list: Vec = Vec::with_capacity(jarray.len()); for i in 0..count { let jtok = &jarray[i]; - let runtime_obj = jtoken_to_runtime_object(&jtok); + let runtime_obj = jtoken_to_runtime_object(jtok); list.push(runtime_obj?); } Ok(list) } -fn create_control_command(name: &str ) -> Option { - let result = match name { - "ev" => Some(ControlCommand::EvalStart), - "out" => Some(ControlCommand::EvalOutput), - "/ev" => Some(ControlCommand::EvalEnd), - "du" => Some(ControlCommand::Duplicate), - "pop" => Some(ControlCommand::PopEvaluatedValue), - "~ret" => Some(ControlCommand::PopFunction), - "->->" => Some(ControlCommand::PopTunnel), - "str" => Some(ControlCommand::BeginString), - "/str" => Some(ControlCommand::EndString), - "nop" => Some(ControlCommand::NoOp), - "choiceCnt" => Some(ControlCommand::ChoiceCount), - "turn" => Some(ControlCommand::Turns), - "turns" => Some(ControlCommand::TurnsSince), - "readc" => Some(ControlCommand::ReadCount), - "rnd" => Some(ControlCommand::Random), - "srnd" => Some(ControlCommand::SeedRandom), - "visit" => Some(ControlCommand::VisitIndex), - "seq" => Some(ControlCommand::SequenceShuffleIndex), - "thread" => Some(ControlCommand::StartThread), - "done" => Some(ControlCommand::Done), - "end" => Some(ControlCommand::End), - "listInt" => Some(ControlCommand::ListFromInt), - "range" => Some(ControlCommand::ListRange), - "lrnd" => Some(ControlCommand::ListRandom), - "#" => Some(ControlCommand::BeginTag), - "/#" => Some(ControlCommand::EndTag), +fn create_control_command(name: &str) -> Option { + match name { + "ev" => Some(ControlCommand::new(CommandType::EvalStart)), + "out" => Some(ControlCommand::new(CommandType::EvalOutput)), + "/ev" => Some(ControlCommand::new(CommandType::EvalEnd)), + "du" => Some(ControlCommand::new(CommandType::Duplicate)), + "pop" => Some(ControlCommand::new(CommandType::PopEvaluatedValue)), + "~ret" => Some(ControlCommand::new(CommandType::PopFunction)), + "->->" => Some(ControlCommand::new(CommandType::PopTunnel)), + "str" => Some(ControlCommand::new(CommandType::BeginString)), + "/str" => Some(ControlCommand::new(CommandType::EndString)), + "nop" => Some(ControlCommand::new(CommandType::NoOp)), + "choiceCnt" => Some(ControlCommand::new(CommandType::ChoiceCount)), + "turn" => Some(ControlCommand::new(CommandType::Turns)), + "turns" => Some(ControlCommand::new(CommandType::TurnsSince)), + "readc" => Some(ControlCommand::new(CommandType::ReadCount)), + "rnd" => Some(ControlCommand::new(CommandType::Random)), + "srnd" => Some(ControlCommand::new(CommandType::SeedRandom)), + "visit" => Some(ControlCommand::new(CommandType::VisitIndex)), + "seq" => Some(ControlCommand::new(CommandType::SequenceShuffleIndex)), + "thread" => Some(ControlCommand::new(CommandType::StartThread)), + "done" => Some(ControlCommand::new(CommandType::Done)), + "end" => Some(ControlCommand::new(CommandType::End)), + "listInt" => Some(ControlCommand::new(CommandType::ListFromInt)), + "range" => Some(ControlCommand::new(CommandType::ListRange)), + "lrnd" => Some(ControlCommand::new(CommandType::ListRandom,)), + "#" => Some(ControlCommand::new(CommandType::BeginTag)), + "/#" => Some(ControlCommand::new(CommandType::EndTag)), _ => None, - }; + } - result } #[cfg(test)] diff --git a/src/lib.rs b/src/lib.rs index a3d16af..693730e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,14 @@ mod story; mod json_serialization; -mod rt_object; -mod ink_value; +mod object; +mod value; mod container; mod control_command; +mod story_state; +mod pointer; +mod path; +mod search_result; +mod callstack; +mod object_enum; + diff --git a/src/object.rs b/src/object.rs new file mode 100644 index 0000000..c02e7fd --- /dev/null +++ b/src/object.rs @@ -0,0 +1,127 @@ +use core::fmt; +use std::{fmt::Display, rc::Rc, cell::RefCell}; + +use as_any::AsAny; + +use crate::{ + container::Container, + path::{Component, Path}, + search_result::SearchResult, object_enum::ObjectEnum, +}; + +pub struct Object { + pub parent: Option>>, + path: Option, + //debug_metadata: DebugMetadata, +} + +impl Object { + pub fn new() -> Object { + Object { + parent: None, + path: None + } + } + + pub fn is_root(&self) -> bool { + self.parent.is_none() + } + + pub fn get_path(oe: ObjectEnum) -> &'static Path { + match oe.get_obj().parent { + Some(_) => { + let mut comps: Vec = Vec::new(); + let mut child = oe; + + let mut container = child.get_obj().parent; + + while let Some(c) = container { + let mut child_valid_name = false; + + if let ObjectEnum::Container(cc) = child { + if cc.borrow().has_valid_name() { + child_valid_name = true; + comps.push(Component::new(cc.borrow().get_name())); + } + } + + if !child_valid_name { + comps.push(Component::new_i( + c.borrow().content + .iter() + .position(|r| r as *const _ == &child ) + .unwrap(), + )); + } + + + child = ObjectEnum::Container(c); + container = c.borrow().get_object().parent; + } + + // Reverse list because components are searched in reverse order. + comps.reverse(); + + oe.get_obj().path = Some(Path::new(&comps, Path::default().is_relative())) + }, + None => oe.get_obj().path = Some(Path::new_with_defaults()), + } + + oe.get_obj().path.as_ref().unwrap() + } + + + pub fn resolve_path(&self) -> Result { + todo!() + } + + pub fn convert_path_to_relative(&self, global_path: Path) -> Path { + todo!() + } + + pub fn compact_path_string(&self, other_path: Path) -> Path { + todo!() + } + + pub fn get_root_container(oe: ObjectEnum) -> Rc> { + let mut ancestor = oe; + + while let Some(p) = ancestor.get_obj().parent { + ancestor = ObjectEnum::Container(p); + } + + match ancestor { + ObjectEnum::Container(c) => c, + _ => panic!("Impossible") + } + } +} + +pub trait RTObject: Display + AsAny { + fn get_object(&self) -> &Object; +} + +// TODO Temporal RTObject. Maybe we sould return Optional::None in null json. +pub struct Null { + obj: Object, +} + +impl Null { + pub(crate) fn new() -> Null { + Null { + obj: Object::new(), + } + } +} + +impl RTObject for Null { + fn get_object(&self) -> &Object { + &self.obj + } +} + +impl fmt::Display for Null { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "**Null**") + } +} diff --git a/src/object_enum.rs b/src/object_enum.rs new file mode 100644 index 0000000..b90d7d5 --- /dev/null +++ b/src/object_enum.rs @@ -0,0 +1,32 @@ +use std::{rc::Rc, cell::RefCell}; + +use crate::{ object::{Null, Object, RTObject}, value::Value, control_command::ControlCommand, container::Container}; + +#[derive(Clone)] +pub enum ObjectEnum { + Value(Rc>), + Container(Rc>), + ControlCommand(Rc>), + Null(Rc>) +} + +impl ObjectEnum { + pub fn get_obj(&self) -> &Object { + match self { + ObjectEnum::Value(o) => o.borrow().get_object(), + ObjectEnum::Container(o) => o.borrow().get_object(), + ObjectEnum::ControlCommand(o) => o.borrow().get_object(), + ObjectEnum::Null(o) => o.borrow().get_object(), + } + } + + pub fn get_obj_mut(&self) -> &Object { + match self { + ObjectEnum::Value(o) => o.borrow_mut().get_object(), + ObjectEnum::Container(o) => o.borrow_mut().get_object(), + ObjectEnum::ControlCommand(o) => o.borrow_mut().get_object(), + ObjectEnum::Null(o) => o.borrow_mut().get_object(), + } + } +} + diff --git a/src/path.rs b/src/path.rs new file mode 100644 index 0000000..ec1ae60 --- /dev/null +++ b/src/path.rs @@ -0,0 +1,283 @@ +use std::{ + fmt, + hash::{Hash, Hasher}, +}; + +const PARENT_ID: &str = "^"; + + +/// The componentsString field from the C# impl. has been removed and it is always generated dinamically from the components field. +#[derive(Eq, Clone)] +pub struct Path { + components: Vec, + is_relative: bool, +} + +impl Path { + pub fn new(components: &[Component], relative: bool) -> Path { + let mut comp: Vec = Vec::new(); + comp.extend_from_slice(components); + Path { + components: comp, + is_relative: relative, + ..Default::default() + } + } + + pub fn new_with_defaults() -> Path { + Path { + ..Default::default() + } + } + + pub fn new_with_components_string(components_string: Option) -> Path { + let cs = components_string; + let mut is_relative = false; + + // Empty path, empty components + // (path is to root, like "/" in file system) + if cs.is_none() || cs.as_ref().unwrap().is_empty() { + return Path { + ..Default::default() + }; + } + + let mut cs = cs.unwrap(); + + // When components start with ".", it indicates a relative path, e.g. + // .^.^.hello.5 + // is equivalent to file system style path: + // ../../hello/5 + + if cs.chars().next().unwrap() == '.' { + is_relative = true; + cs = cs[1..].to_string(); + } else { + is_relative = false; + } + + let component_string = cs.split('.'); + let mut components = Vec::new(); + + for str in component_string { + let index = str.parse::(); + + match index { + Ok(index) => components.push(Component::new_i(index)), + Err(_) => components.push(Component::new(str)), + } + } + + Path { + components, + is_relative, + } + } + + pub fn get_component(&self, index: usize) -> Option<&Component> { + self.components.get(index) + } + + pub fn is_relative(&self) -> bool { + self.is_relative + } + + fn get_tail(&self) -> Path { + if self.components.len() >= 2 { + let tail_comps = &self.components[1..]; + + return Path::new(tail_comps, false); + } else { + return Path::get_self(); + } + } + + pub fn len(&self) -> usize { + self.components.len() + } + + pub fn get_self() -> Path { + Path { + is_relative: true, + ..Default::default() + } + } + + pub fn get_last_component(&self) -> Option<&Component> { + if self.components.len() > 0 { + return self.components.get(self.components.len() - 1); + } + + None + } + + pub fn path_by_appending_path(&self, path_to_append: Path) -> Path { + let mut upward_moves = 0; + + for component in path_to_append.components { + if component.is_parent() { + upward_moves += 1; + } else { + break; + } + } + + let mut components = Vec::new(); + + // TODO check that this is correct + for i in 0..self.components.len() - upward_moves { + components.push(self.components.get(i).unwrap().clone()); + } + + for i in upward_moves..self.components.len() { + components.push(self.components.get(i).unwrap().clone()); + } + + Path { + components, + ..Default::default() + } + } + + fn get_components_string(&self) -> String { + let mut sb = String::new(); + + if self.components.len() > 0 { + sb.push_str(&self.components.get(0).unwrap().to_string()); + + for i in 1..self.components.len() { + sb.push('.'); + sb.push_str(&self.components.get(i).unwrap().to_string()); + } + } + + if self.is_relative { + return ".".to_string() + &sb; + } + + sb + } + + pub(crate) fn path_by_appending_component( &self, c: Component) -> Path { + let mut p = Path::new(self.components.as_ref(), false); + p.components.push(c); + + p + } +} + +impl fmt::Display for Path { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.get_components_string()) + } +} + +impl Default for Path { + fn default() -> Self { + Self { + components: Default::default(), + is_relative: Default::default(), + } + } +} + +impl Hash for Path { + fn hash(&self, state: &mut H) { + self.to_string().hash(state) + } +} + +impl PartialEq for Path { + fn eq(&self, other: &Self) -> bool { + if other.components.len() != self.components.len() { + return false; + } + + if other.is_relative() != self.is_relative() { + return false; + } + + for i in 0..other.components.len() { + if !other + .components + .get(i) + .unwrap() + .eq(self.components.get(i).unwrap()) + { + return false; + } + } + + return true; + } +} + +#[derive(Eq, Clone)] +pub struct Component { + index: Option, + name: Option, +} + +impl Component { + pub(crate) fn new(name: &str) -> Component { + Component { + name: Some(name.to_string()), + index: None, + } + } + + pub(crate) fn new_i(index: usize) -> Component { + Component { + name: None, + index: Some(index), + } + } + + fn to_parent() -> Component { + Component::new(PARENT_ID) + } + + fn is_index(&self) -> bool { + self.index.is_some() + } + + fn is_parent(&self) -> bool { + match &self.name { + Some(name) => name.eq(PARENT_ID), + None => false, + } + } +} + +impl fmt::Display for Component { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self.index { + Some(index) => index.to_string(), + None => self.name.as_ref().unwrap().to_string(), + }; + + write!(f, "{s}") + } +} + +impl PartialEq for Component { + fn eq(&self, other: &Self) -> bool { + if other.is_index() == self.is_index() { + match self.index { + Some(index) => return index == other.index.unwrap(), + None => return self.name.as_ref().unwrap().eq(other.name.as_ref().unwrap()), + } + } + + false + } +} + +impl Hash for Component { + fn hash(&self, state: &mut H) { + match self.index { + Some(index) => return index.hash(state), + None => return self.name.as_ref().unwrap().hash(state), + } + } +} diff --git a/src/pointer.rs b/src/pointer.rs new file mode 100644 index 0000000..d003bba --- /dev/null +++ b/src/pointer.rs @@ -0,0 +1,69 @@ +use std::{rc::Rc, fmt, cell::RefCell}; + +use crate::{container::Container, object::{RTObject, Object}, path::{Path, Component}, object_enum::ObjectEnum}; + +pub const NULL: Pointer = Pointer::new(None, -1); + + +#[derive(Clone)] +pub struct Pointer { + container: Option>>, + index: i32, +} + +impl Pointer { + pub const fn new(container: Option>>, index: i32) -> Pointer { + Pointer { container, index } + } + + pub fn resolve(&self) -> Option { + match &self.container { + Some(container) => { + if self.index < 0 || container.borrow().content.len() == 0 { + return Some(ObjectEnum::Container(container.clone())); + } + + return match container.borrow().content.get(self.index as usize) { + Some(o) => Some(o.clone()), + None => None, + }; + } + None => None, + } + } + + pub fn is_null(&self) -> bool { + self.container.is_none() + } + + pub fn get_path(&self) -> Option { + if self.is_null() { + return None; + } + + let container = ObjectEnum::Container(self.container.as_ref().unwrap().clone()); + + if self.index >= 0 { + let components: Vec = Vec::new(); + let c = Component::new_i(self.index as usize); + + return Some(Object::get_path(container) + .path_by_appending_component(c)); + } + + Some(Object::get_path(container).clone()) + } + + pub(crate) fn start_of(container:Option>>) -> Pointer { + return Pointer{container, index:0}; + } +} + +impl fmt::Display for Pointer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.container { + Some(container) => write!(f, "Ink Pointer -> {} -- index {}", Object::get_path(ObjectEnum::Container(self.container.as_ref().unwrap().clone())).to_string(), self.index), + None => write!(f, "Ink Pointer (null)"), + } + } +} diff --git a/src/rt_object.rs b/src/rt_object.rs deleted file mode 100644 index 2517b6d..0000000 --- a/src/rt_object.rs +++ /dev/null @@ -1,25 +0,0 @@ -/* -// location feature -struct RTObject { - //parent: &RTObject, - //path: Path, - //debug_metadata: DebugMetadata, -} - -impl RTObject { - -} -*/ - -use std::any::Any; - -pub trait RTObject { - fn as_any(&self) -> &dyn Any; -} - -pub struct Null; -impl RTObject for Null { - fn as_any(&self) -> &dyn Any { - self - } -} diff --git a/src/search_result.rs b/src/search_result.rs new file mode 100644 index 0000000..64e960d --- /dev/null +++ b/src/search_result.rs @@ -0,0 +1,3 @@ +pub(crate) struct SearchResult { + +} \ No newline at end of file diff --git a/src/story.rs b/src/story.rs index f308ea5..159d103 100644 --- a/src/story.rs +++ b/src/story.rs @@ -1,12 +1,15 @@ #![allow(unused_variables, dead_code)] -use crate::{json_serialization, container::Container, rt_object::RTObject}; +use std::{rc::Rc, cell::RefCell}; + +use crate::{json_serialization, container::Container, story_state::StoryState, object_enum::ObjectEnum}; const INK_VERSION_CURRENT: i32 = 21; const INK_VERSION_MINIMUM_COMPATIBLE: i32 = 18; pub struct Story{ - main_content_container: Box, + pub main_content_container: Rc>, + state: StoryState, } impl Story { @@ -53,9 +56,70 @@ impl Story { let main_content_container = json_serialization::jtoken_to_runtime_object(rootToken)?; - //ResetState (); + let main_content_container = match main_content_container { + ObjectEnum::Container(container) => container, + _ => return Err("Root node for ink is not a container?".to_string()), + }; + + let mut story = Story {main_content_container, state: StoryState::new()}; + + story.reset_state(); + + Ok(story) + } + + fn reset_state(&mut self) { + //TODO ifAsyncWeCant("ResetState"); + + self.state = StoryState::new(); + + // TODO state.getVariablesState().setVariableChangedEvent(this); + + self.reset_globals(); + } + + fn reset_globals(&self) { + /* TODO + 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(); + */ + } + + pub fn build_string_of_hierarchy(&self) -> String { + let mut sb = String::new(); + + self.main_content_container.borrow() + .build_string_of_hierarchy(&mut sb, 0, None);// TODO state.getCurrentPointer().resolve()); - Ok(Story {main_content_container}) + sb + } + + pub fn can_continue(&self) -> bool { + self.state.can_continue() + } + + pub fn cont(&self) -> String { + self.continue_async(0.0); + self.get_current_text() + } + + pub fn continue_async(&self, millisecs_limit_async: f32) { + todo!() + } + + pub fn get_current_text(&self) -> String { + todo!() } } @@ -66,9 +130,35 @@ mod tests { use super::*; #[test] - fn create_test() { + fn oneline_test() { let json_string = fs::read_to_string("examples/inkfiles/basictext/oneline.ink.json").unwrap(); - Story::new(&json_string).unwrap(); + let story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + } + + #[test] + fn twolines_test() { + let json_string = + fs::read_to_string("examples/inkfiles/basictext/twolines.ink.json").unwrap(); + let story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + } + + fn next_all(story: &Story, text: &mut Vec) { + while story.can_continue() { + let line = story.cont(); + print!("{line}"); + + if !line.trim().is_empty() { + text.push(line.trim().to_string()); + } + } + + /* TODO + if story.has_error() { + fail(TestUtils.joinText(story.getCurrentErrors())); + } + */ } } diff --git a/src/story_state.rs b/src/story_state.rs new file mode 100644 index 0000000..cf50111 --- /dev/null +++ b/src/story_state.rs @@ -0,0 +1,29 @@ +use crate::{pointer::Pointer, callstack::CallStack}; + +pub struct StoryState { + +} + +impl StoryState { + pub fn new() -> StoryState{ + StoryState {} + } + + pub fn can_continue(&self) -> bool { + return !self.get_current_pointer().is_null() && !self.has_error(); + } + + pub fn has_error(&self) -> bool { + // TODO return currentErrors != null && currentErrors.size() > 0; + false + } + + fn get_current_pointer(&self) -> Pointer { + return self.get_callstack().get_current_element().current_pointer.clone(); + } + + fn get_callstack(&self) -> CallStack { + todo!() + } + +} \ No newline at end of file diff --git a/src/value.rs b/src/value.rs new file mode 100644 index 0000000..5aa60db --- /dev/null +++ b/src/value.rs @@ -0,0 +1,67 @@ +// enum with integers: https://enodev.fr/posts/rusticity-convert-an-integer-to-an-enum.html + +use std::{fmt}; + +use crate::{object::{RTObject, Object}}; + +#[repr(i8)] +pub enum ValueType { + Bool(bool) = -1, + Int(i32), + Float(f32), + //List(List), + String(String), + + // Not used for coersion described above + //DivertTarget, + //VariablePointer, +} + +pub struct Value { + obj: Object, + pub value: ValueType, +} + +impl RTObject for Value { + fn get_object(&self) -> &Object { + &self.obj + } +} + +impl fmt::Display for Value { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.value { + ValueType::Bool(v) => write!(f, "{}", v), + ValueType::Int(v) => write!(f, "{}", v), + ValueType::Float(v) => write!(f, "{}", v), + ValueType::String(v) => write!(f, "{}", v), + } + } +} + +impl Value { + pub fn new_bool(v:bool) -> Value { + Value { obj: Object::new(), value: ValueType::Bool(v) } + } + + pub fn new_int(v:i32) -> Value { + Value { obj: Object::new(), value: ValueType::Int(v) } + } + + pub fn new_float(v:f32) -> Value { + Value { obj: Object::new(), value: ValueType::Float(v) } + } + + pub fn new_string(v:&str) -> Value { + Value { obj: Object::new(), value: ValueType::String(v.to_string()) } + } + + pub fn is_truthy(&self) -> bool { + match &self.value { + ValueType::Bool(v) => *v, + ValueType::Int(v) => *v != 0, + ValueType::Float(v) => *v != 0.0, + ValueType::String(v) => v.len() > 0, + } + } +} \ No newline at end of file From 7a7c832d7ba850cdf8824bf84b7a0971766a788f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Mon, 11 Sep 2023 08:41:44 +0000 Subject: [PATCH 08/91] Minimize RefCell use. Compiles. --- Cargo.toml | 4 +- src/container.rs | 80 +++++++++++++++++---------------------- src/json_serialization.rs | 32 ++++++++-------- src/object.rs | 73 +++++++++++++++++++++-------------- src/object_enum.rs | 31 +++------------ src/pointer.rs | 27 +++++++------ src/search_result.rs | 2 +- src/story.rs | 15 ++++---- 8 files changed, 123 insertions(+), 141 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4402d2c..7a98ffa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ path = "src/bin/console-player.rs" serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.93" log = "0.4.17" -strum_macros = "0.24" -strum = { version = "0.24", features = ["derive"] } +strum_macros = "0.25.2" +strum = { version = "0.25.0", features = ["derive"] } as-any = "0.3.0" diff --git a/src/container.rs b/src/container.rs index 0476e3d..bc24a6e 100644 --- a/src/container.rs +++ b/src/container.rs @@ -1,41 +1,37 @@ use std::{ - cell::{RefCell, Ref}, + cell::{Ref, RefCell}, fmt, rc::Rc, }; +use as_any::{AsAny, Downcast}; + use crate::{ - object::{Object, RTObject}, + object::{Object, RTObject, Null}, object_enum::ObjectEnum, - value::ValueType, + value::{ValueType, Value}, control_command::ControlCommand, }; pub struct Container { obj: Object, - pub content: Vec, + pub content: Vec>, pub name: Option, pub count_flags: i32, //named_content: HashMap } impl Container { - pub fn new(name: Option, count_flags: i32) -> Container { - Container { + pub fn new(name: Option, count_flags: i32, content: Vec>) -> Rc { + let c = Rc::new(Container { obj: Object::new(), - content: Vec::new(), + content, name, count_flags, - } - } + }); - pub fn add_contents(container: &Rc>, objs: &Vec) { - objs.iter() - .for_each(|o| Container::add_content(container, o)); - } + c.content.iter().for_each(|o| o.get_object().set_parent(&c)); - pub fn add_content(container: &Rc>, obj: &ObjectEnum) { - container.as_ref().borrow_mut().content.push(obj.clone()); - obj.get_obj_mut().parent = Some(container.clone()); + c } pub fn has_valid_name(&self) -> bool { @@ -50,7 +46,7 @@ impl Container { &self, sb: &mut String, indentation: usize, - pointed_obj: Option, + pointed_obj: Option<&dyn RTObject>, ) { Container::append_indentation(sb, indentation); @@ -63,8 +59,8 @@ impl Container { } if let Some(pointed_obj) = pointed_obj { - if let ObjectEnum::Container(obj) = pointed_obj { - if std::ptr::eq(obj.as_ptr(), self) { + if let Some(c) = pointed_obj.downcast_ref::() { + if std::ptr::eq(c, self) { sb.push_str(" <---"); } } @@ -74,31 +70,27 @@ impl Container { let indentation = indentation + 1; for (i, obj) in self.content.iter().enumerate() { - match obj { - ObjectEnum::Container(c) => { - c.as_ref() - .borrow() - .build_string_of_hierarchy(sb, indentation, pointed_obj); - } + if let Some(c) = obj.downcast_ref::() { + c.build_string_of_hierarchy(sb, indentation, pointed_obj); + } - ObjectEnum::Value(v) => { - Container::append_indentation(sb, indentation); - if let ValueType::String(s) = v.as_ref().borrow().value { - sb.push('\"'); - sb.push_str(&s.clone().replace('\n', "\\n")); - sb.push('\"'); - } else { - sb.push_str(&v.as_ref().borrow().to_string()); - } + if let Some(v) = obj.downcast_ref::() { + Container::append_indentation(sb, indentation); + if let ValueType::String(s) = &v.value { + sb.push('\"'); + sb.push_str(&s.clone().replace('\n', "\\n")); + sb.push('\"'); + } else { + sb.push_str(&v.to_string()); } + } - ObjectEnum::ControlCommand(o) => { - sb.push_str(&o.as_ref().borrow().to_string()); - } + if let Some(v) = obj.downcast_ref::() { + sb.push_str(&v.to_string()); + } - ObjectEnum::Null(o) => { - sb.push_str(&o.as_ref().borrow().to_string()); - } + if let Some(n) = obj.downcast_ref::() { + sb.push_str(&n.to_string()); } if i != self.content.len() - 1 { @@ -106,11 +98,9 @@ impl Container { } if let Some(pointed_obj) = pointed_obj { - if let ObjectEnum::Container(pointed_obj) = pointed_obj { - if let ObjectEnum::Container(obj) = obj { - if &obj.as_ref().borrow() as *const _ - == &pointed_obj.as_ref().borrow() as *const _ - { + if let Some(pointed_obj) = pointed_obj.downcast_ref::() { + if let Some(obj) = obj.downcast_ref::() { + if std::ptr::eq(obj, pointed_obj) { sb.push_str(" <---"); } } diff --git a/src/json_serialization.rs b/src/json_serialization.rs index 0cbc7dd..449bc42 100644 --- a/src/json_serialization.rs +++ b/src/json_serialization.rs @@ -5,17 +5,17 @@ use crate::{ object::{self, RTObject}, control_command::{CommandType, ControlCommand}, value::Value, object_enum::ObjectEnum, }; -pub fn jtoken_to_runtime_object(token: &serde_json::Value) -> Result { +pub fn jtoken_to_runtime_object(token: &serde_json::Value) -> Result, String> { match token { - serde_json::Value::Null => Ok(ObjectEnum::Null(Rc::new(RefCell::new(object::Null::new())))), - serde_json::Value::Bool(value) => Ok(ObjectEnum::Value(Rc::new(RefCell::new(Value::new_bool(value.to_owned()))))), + serde_json::Value::Null => Ok(Rc::new(object::Null::new())), + serde_json::Value::Bool(value) => Ok(Rc::new(Value::new_bool(value.to_owned()))), serde_json::Value::Number(_) => { if token.is_i64() { let val:i32 = token.as_i64().unwrap().try_into().unwrap(); - Ok(ObjectEnum::Value(Rc::new(RefCell::new(Value::new_int(val))))) + Ok(Rc::new(Value::new_int(val))) } else { let val: f32 = token.as_f64().unwrap() as f32; - Ok(ObjectEnum::Value(Rc::new(RefCell::new(Value::new_float(val))))) + Ok(Rc::new(Value::new_float(val))) } } @@ -23,14 +23,14 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value) -> Result".eq(str) {return new Glue();} if let Some(control_command) = create_control_command(str) { - return Ok(ObjectEnum::ControlCommand(Rc::new(RefCell::new(control_command)))); + return Ok(Rc::new(control_command)); } /* TODO @@ -52,15 +52,12 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value) -> Result Ok(ObjectEnum::Container(jarray_to_container(value)?)), + serde_json::Value::Array(value) => Ok(jarray_to_container(value)?), serde_json::Value::Object(_) => todo!(), } } -fn jarray_to_container(jarray: &Vec) -> Result>, String> { - let container = Rc::new(RefCell::new(Container::new(None, 0))); - Container::add_contents(&container, &jarray_to_runtime_obj_list(jarray, true)?); - +fn jarray_to_container(jarray: &Vec) -> Result, String> { // Final object in the array is always a combination of // - named content // - a "#f" key with the countFlags @@ -75,8 +72,8 @@ fn jarray_to_container(jarray: &Vec) -> Result container.borrow_mut().count_flags = v.as_i64().unwrap().try_into().unwrap(), - "#n" => container.borrow_mut().name = Some(v.as_str().unwrap().to_string()), + "#f" => flags = v.as_i64().unwrap().try_into().unwrap(), + "#n" => name = Some(v.as_str().unwrap().to_string()), _ => { let named_content_item = jtoken_to_runtime_object(v); /* TODO @@ -94,17 +91,18 @@ fn jarray_to_container(jarray: &Vec) -> Result, skip_last: bool) -> Result, String> { +fn jarray_to_runtime_obj_list(jarray: &Vec, skip_last: bool) -> Result>, String> { let mut count = jarray.len(); if skip_last { count -= 1; } - let mut list: Vec = Vec::with_capacity(jarray.len()); + let mut list: Vec> = Vec::with_capacity(jarray.len()); for i in 0..count { let jtok = &jarray[i]; diff --git a/src/object.rs b/src/object.rs index c02e7fd..0679b39 100644 --- a/src/object.rs +++ b/src/object.rs @@ -1,73 +1,88 @@ use core::fmt; -use std::{fmt::Display, rc::Rc, cell::RefCell}; +use std::{fmt::Display, rc::{Weak, Rc}, cell::RefCell}; -use as_any::AsAny; +use as_any::{AsAny, Downcast}; use crate::{ container::Container, path::{Component, Path}, - search_result::SearchResult, object_enum::ObjectEnum, + search_result::SearchResult }; pub struct Object { - pub parent: Option>>, - path: Option, + parent: RefCell>, + path: RefCell>>, //debug_metadata: DebugMetadata, } impl Object { pub fn new() -> Object { Object { - parent: None, - path: None + parent: RefCell::new(Weak::new()), + path: RefCell::new(None), } } pub fn is_root(&self) -> bool { - self.parent.is_none() + self.parent.borrow().upgrade().is_none() } - pub fn get_path(oe: ObjectEnum) -> &'static Path { - match oe.get_obj().parent { + pub fn get_parent(&self) -> Option> { + self.parent.borrow().upgrade() + } + + pub(crate) fn set_parent(&self, parent: &Rc) { + self.parent.replace(Rc::downgrade(parent)); + } + + pub fn get_path(rtobject: Rc) -> Rc { + if let Some(p) = rtobject.get_object().path.borrow().as_ref() { + return p.clone(); + } + + match rtobject.get_object().get_parent() { Some(_) => { let mut comps: Vec = Vec::new(); - let mut child = oe; - - let mut container = child.get_obj().parent; + + let mut container = rtobject.get_object().get_parent(); + let mut child = rtobject.clone(); while let Some(c) = container { let mut child_valid_name = false; - if let ObjectEnum::Container(cc) = child { - if cc.borrow().has_valid_name() { + if let Some(cc) = child.downcast_ref::() { + if cc.has_valid_name() { child_valid_name = true; - comps.push(Component::new(cc.borrow().get_name())); + comps.push(Component::new(cc.get_name())); } } if !child_valid_name { comps.push(Component::new_i( - c.borrow().content + c.content .iter() - .position(|r| r as *const _ == &child ) + .position(|r| std::ptr::eq(r.as_ref(), child.as_ref())) .unwrap(), )); } - child = ObjectEnum::Container(c); - container = c.borrow().get_object().parent; + container = c.get_object().get_parent(); + child = c; + } // Reverse list because components are searched in reverse order. comps.reverse(); - oe.get_obj().path = Some(Path::new(&comps, Path::default().is_relative())) + rtobject.get_object().path.replace(Some(Rc::new(Path::new(&comps, Path::default().is_relative())))); + }, + None => { + rtobject.get_object().path.replace(Some(Rc::new(Path::new_with_defaults()))); }, - None => oe.get_obj().path = Some(Path::new_with_defaults()), } - oe.get_obj().path.as_ref().unwrap() + rtobject.get_object().path.borrow().as_ref().unwrap().clone() } @@ -83,15 +98,15 @@ impl Object { todo!() } - pub fn get_root_container(oe: ObjectEnum) -> Rc> { - let mut ancestor = oe; + pub fn get_root_container(rtobject: Rc) -> Rc { + let mut ancestor = rtobject; - while let Some(p) = ancestor.get_obj().parent { - ancestor = ObjectEnum::Container(p); + while let Some(p) = ancestor.get_object().get_parent() { + ancestor = p; } - match ancestor { - ObjectEnum::Container(c) => c, + match ancestor.downcast_ref::>() { + Some(c) => c.clone(), _ => panic!("Impossible") } } diff --git a/src/object_enum.rs b/src/object_enum.rs index b90d7d5..2e97e05 100644 --- a/src/object_enum.rs +++ b/src/object_enum.rs @@ -4,29 +4,8 @@ use crate::{ object::{Null, Object, RTObject}, value::Value, control_command::Co #[derive(Clone)] pub enum ObjectEnum { - Value(Rc>), - Container(Rc>), - ControlCommand(Rc>), - Null(Rc>) -} - -impl ObjectEnum { - pub fn get_obj(&self) -> &Object { - match self { - ObjectEnum::Value(o) => o.borrow().get_object(), - ObjectEnum::Container(o) => o.borrow().get_object(), - ObjectEnum::ControlCommand(o) => o.borrow().get_object(), - ObjectEnum::Null(o) => o.borrow().get_object(), - } - } - - pub fn get_obj_mut(&self) -> &Object { - match self { - ObjectEnum::Value(o) => o.borrow_mut().get_object(), - ObjectEnum::Container(o) => o.borrow_mut().get_object(), - ObjectEnum::ControlCommand(o) => o.borrow_mut().get_object(), - ObjectEnum::Null(o) => o.borrow_mut().get_object(), - } - } -} - + Value, + Container, + ControlCommand, + Null +} \ No newline at end of file diff --git a/src/pointer.rs b/src/pointer.rs index d003bba..aaf3404 100644 --- a/src/pointer.rs +++ b/src/pointer.rs @@ -7,23 +7,23 @@ pub const NULL: Pointer = Pointer::new(None, -1); #[derive(Clone)] pub struct Pointer { - container: Option>>, + container: Option>, index: i32, } impl Pointer { - pub const fn new(container: Option>>, index: i32) -> Pointer { + pub const fn new(container: Option>, index: i32) -> Pointer { Pointer { container, index } } - pub fn resolve(&self) -> Option { + pub fn resolve(&self) -> Option> { match &self.container { Some(container) => { - if self.index < 0 || container.borrow().content.len() == 0 { - return Some(ObjectEnum::Container(container.clone())); + if self.index < 0 || container.content.is_empty() { + return Some(container.clone()); } - return match container.borrow().content.get(self.index as usize) { + return match container.content.get(self.index as usize) { Some(o) => Some(o.clone()), None => None, }; @@ -36,25 +36,24 @@ impl Pointer { self.container.is_none() } - pub fn get_path(&self) -> Option { + pub fn get_path(&self) -> Option> { if self.is_null() { return None; } - let container = ObjectEnum::Container(self.container.as_ref().unwrap().clone()); + let container = self.container.as_ref().unwrap(); if self.index >= 0 { - let components: Vec = Vec::new(); let c = Component::new_i(self.index as usize); - return Some(Object::get_path(container) - .path_by_appending_component(c)); + return Some(Rc::new(Object::get_path(container.clone()) + .path_by_appending_component(c))); } - Some(Object::get_path(container).clone()) + Some(Object::get_path(container.clone())) } - pub(crate) fn start_of(container:Option>>) -> Pointer { + pub(crate) fn start_of(container:Option>) -> Pointer { return Pointer{container, index:0}; } } @@ -62,7 +61,7 @@ impl Pointer { impl fmt::Display for Pointer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.container { - Some(container) => write!(f, "Ink Pointer -> {} -- index {}", Object::get_path(ObjectEnum::Container(self.container.as_ref().unwrap().clone())).to_string(), self.index), + Some(container) => write!(f, "Ink Pointer -> {} -- index {}", Object::get_path(container.clone()).to_string(), self.index), None => write!(f, "Ink Pointer (null)"), } } diff --git a/src/search_result.rs b/src/search_result.rs index 64e960d..2dc9666 100644 --- a/src/search_result.rs +++ b/src/search_result.rs @@ -1,3 +1,3 @@ -pub(crate) struct SearchResult { +pub struct SearchResult { } \ No newline at end of file diff --git a/src/story.rs b/src/story.rs index 159d103..9a970bf 100644 --- a/src/story.rs +++ b/src/story.rs @@ -2,13 +2,15 @@ use std::{rc::Rc, cell::RefCell}; -use crate::{json_serialization, container::Container, story_state::StoryState, object_enum::ObjectEnum}; +use as_any::Downcast; + +use crate::{json_serialization, container::{Container, self}, story_state::StoryState, object_enum::ObjectEnum}; const INK_VERSION_CURRENT: i32 = 21; const INK_VERSION_MINIMUM_COMPATIBLE: i32 = 18; pub struct Story{ - pub main_content_container: Rc>, + pub main_content_container: Rc, state: StoryState, } @@ -56,12 +58,11 @@ impl Story { let main_content_container = json_serialization::jtoken_to_runtime_object(rootToken)?; - let main_content_container = match main_content_container { - ObjectEnum::Container(container) => container, - _ => return Err("Root node for ink is not a container?".to_string()), + if main_content_container.as_any().downcast_ref::().is_none() { + return Err("Root node for ink is not a container?".to_string()); }; - let mut story = Story {main_content_container, state: StoryState::new()}; + let mut story = Story { main_content_container: main_content_container.downcast_ref::>().unwrap().clone(), state: StoryState::new()}; story.reset_state(); @@ -99,7 +100,7 @@ impl Story { pub fn build_string_of_hierarchy(&self) -> String { let mut sb = String::new(); - self.main_content_container.borrow() + self.main_content_container .build_string_of_hierarchy(&mut sb, 0, None);// TODO state.getCurrentPointer().resolve()); sb From 3ad7f597eab193caa23a9740cf47e85d830b7090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Mon, 11 Sep 2023 18:23:37 +0000 Subject: [PATCH 09/91] tests executes ok but doesn't print the tree --- src/object.rs | 19 +++++++++++++++---- src/story.rs | 15 ++++++++------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/object.rs b/src/object.rs index 0679b39..8ef11e8 100644 --- a/src/object.rs +++ b/src/object.rs @@ -1,5 +1,5 @@ use core::fmt; -use std::{fmt::Display, rc::{Weak, Rc}, cell::RefCell}; +use std::{fmt::Display, rc::{Weak, Rc}, cell::RefCell, any::Any}; use as_any::{AsAny, Downcast}; @@ -105,14 +105,25 @@ impl Object { ancestor = p; } - match ancestor.downcast_ref::>() { - Some(c) => c.clone(), + match ancestor.into_any().downcast::() { + Ok(c) => c.clone(), _ => panic!("Impossible") } } } -pub trait RTObject: Display + AsAny { +pub trait IntoAny: AsAny { + fn into_any(self: Rc) -> Rc; +} + +impl IntoAny for T { + #[inline(always)] + fn into_any(self: Rc) -> Rc { + self + } +} + +pub trait RTObject: Display + IntoAny { fn get_object(&self) -> &Object; } diff --git a/src/story.rs b/src/story.rs index 9a970bf..4cde283 100644 --- a/src/story.rs +++ b/src/story.rs @@ -1,8 +1,7 @@ #![allow(unused_variables, dead_code)] -use std::{rc::Rc, cell::RefCell}; - -use as_any::Downcast; +use std::{rc::Rc, cell::RefCell, any::Any}; +use as_any::{AsAny, Downcast}; use crate::{json_serialization, container::{Container, self}, story_state::StoryState, object_enum::ObjectEnum}; @@ -40,7 +39,7 @@ impl Story { log::debug!("WARNING: Version of ink used to build story doesn't match current version of engine. Non-critical, but recommend synchronising."); } - let rootToken = match json.get("root") { + let root_token = match json.get("root") { Some(value) => value, None => { return Err( @@ -56,13 +55,15 @@ impl Story { //} - let main_content_container = json_serialization::jtoken_to_runtime_object(rootToken)?; + let main_content_container= json_serialization::jtoken_to_runtime_object(root_token)?; + + let main_content_container = main_content_container.into_any().downcast::(); - if main_content_container.as_any().downcast_ref::().is_none() { + if main_content_container.is_err() { return Err("Root node for ink is not a container?".to_string()); }; - let mut story = Story { main_content_container: main_content_container.downcast_ref::>().unwrap().clone(), state: StoryState::new()}; + let mut story = Story { main_content_container: main_content_container.unwrap(), state: StoryState::new()}; story.reset_state(); From ffe410feef2bc53ef81b07a8140c2557cef1455b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Tue, 12 Sep 2023 15:43:53 +0000 Subject: [PATCH 10/91] Update devcontainer to Rust 1.72.0 --- .devcontainer/Dockerfile | 30 ++++++++++++++++++++++++++++++ .devcontainer/devcontainer.json | 10 +++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 .devcontainer/Dockerfile diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..a25e0fd --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,30 @@ +FROM buildpack-deps:bookworm + +ENV RUSTUP_HOME=/usr/local/rustup \ + CARGO_HOME=/usr/local/cargo \ + PATH=/usr/local/cargo/bin:$PATH \ + RUST_VERSION=1.72.0 + +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + # Remove imagemagick due to https://security-tracker.debian.org/tracker/CVE-2019-10131 + && apt-get purge -y imagemagick imagemagick-6-common + +RUN set -eux; \ + dpkgArch="$(dpkg --print-architecture)"; \ + case "${dpkgArch##*-}" in \ + amd64) rustArch='x86_64-unknown-linux-gnu'; rustupSha256='0b2f6c8f85a3d02fde2efc0ced4657869d73fccfce59defb4e8d29233116e6db' ;; \ + armhf) rustArch='armv7-unknown-linux-gnueabihf'; rustupSha256='f21c44b01678c645d8fbba1e55e4180a01ac5af2d38bcbd14aa665e0d96ed69a' ;; \ + arm64) rustArch='aarch64-unknown-linux-gnu'; rustupSha256='673e336c81c65e6b16dcdede33f4cc9ed0f08bde1dbe7a935f113605292dc800' ;; \ + i386) rustArch='i686-unknown-linux-gnu'; rustupSha256='e7b0f47557c1afcd86939b118cbcf7fb95a5d1d917bdd355157b63ca00fc4333' ;; \ + *) echo >&2 "unsupported architecture: ${dpkgArch}"; exit 1 ;; \ + esac; \ + url="https://static.rust-lang.org/rustup/archive/1.26.0/${rustArch}/rustup-init"; \ + wget "$url"; \ + echo "${rustupSha256} *rustup-init" | sha256sum -c -; \ + chmod +x rustup-init; \ + ./rustup-init -y --no-modify-path --profile minimal --default-toolchain $RUST_VERSION --default-host ${rustArch}; \ + rm rustup-init; \ + chmod -R a+w $RUSTUP_HOME $CARGO_HOME; \ + rustup --version; \ + cargo --version; \ + rustc --version; \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 093bbb1..27c7a84 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,7 +3,12 @@ { "name": "Rust", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/rust:bullseye" + //"image": "mcr.microsoft.com/devcontainers/rust:latest", + + "build": { + // Path is relative to the devcontainer.json file. + "dockerfile": "Dockerfile" + } // Features to add to the dev container. More info: https://containers.dev/features. // "features": {}, @@ -12,8 +17,7 @@ // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "rustc --version", - + //"postCreateCommand": "rustc --version" // Configure tool-specific properties. // "customizations": {}, From 7dc9d238c3131a163a8fabe9a2bfbb71ee8b32d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Tue, 12 Sep 2023 15:49:44 +0000 Subject: [PATCH 11/91] Object::get_path test --- src/container.rs | 17 +++++++++-------- src/object.rs | 37 ++++++++++++++++++++++++++++++++----- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/container.rs b/src/container.rs index bc24a6e..1bf594c 100644 --- a/src/container.rs +++ b/src/container.rs @@ -70,26 +70,27 @@ impl Container { let indentation = indentation + 1; for (i, obj) in self.content.iter().enumerate() { - if let Some(c) = obj.downcast_ref::() { + if let Some(c) = obj.as_ref().downcast_ref::() { c.build_string_of_hierarchy(sb, indentation, pointed_obj); } - if let Some(v) = obj.downcast_ref::() { + if let Some(v) = obj.as_ref().downcast_ref::() { Container::append_indentation(sb, indentation); if let ValueType::String(s) = &v.value { sb.push('\"'); - sb.push_str(&s.clone().replace('\n', "\\n")); + sb.push_str(&&s.replace('\n', "\\n")); sb.push('\"'); } else { sb.push_str(&v.to_string()); } } - if let Some(v) = obj.downcast_ref::() { - sb.push_str(&v.to_string()); + if let Some(cc) = obj.as_ref().downcast_ref::() { + Container::append_indentation(sb, indentation); + sb.push_str(&cc.to_string()); } - if let Some(n) = obj.downcast_ref::() { + if let Some(n) = obj.as_ref().downcast_ref::() { sb.push_str(&n.to_string()); } @@ -99,7 +100,7 @@ impl Container { if let Some(pointed_obj) = pointed_obj { if let Some(pointed_obj) = pointed_obj.downcast_ref::() { - if let Some(obj) = obj.downcast_ref::() { + if let Some(obj) = obj.as_ref().downcast_ref::() { if std::ptr::eq(obj, pointed_obj) { sb.push_str(" <---"); } @@ -160,4 +161,4 @@ impl fmt::Display for Container { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "**Container**") } -} +} \ No newline at end of file diff --git a/src/object.rs b/src/object.rs index 8ef11e8..474c6cc 100644 --- a/src/object.rs +++ b/src/object.rs @@ -1,5 +1,5 @@ use core::fmt; -use std::{fmt::Display, rc::{Weak, Rc}, cell::RefCell, any::Any}; +use std::{fmt::Display, rc::{Weak, Rc}, cell::RefCell, any::Any, borrow::BorrowMut}; use as_any::{AsAny, Downcast}; @@ -50,7 +50,7 @@ impl Object { while let Some(c) = container { let mut child_valid_name = false; - if let Some(cc) = child.downcast_ref::() { + if let Some(cc) = child.as_ref().downcast_ref::() { if cc.has_valid_name() { child_valid_name = true; comps.push(Component::new(cc.get_name())); @@ -61,15 +61,13 @@ impl Object { comps.push(Component::new_i( c.content .iter() - .position(|r| std::ptr::eq(r.as_ref(), child.as_ref())) + .position(|r| Rc::ptr_eq( r, &child) ) .unwrap(), )); } - container = c.get_object().get_parent(); child = c; - } // Reverse list because components are searched in reverse order. @@ -151,3 +149,32 @@ impl fmt::Display for Null { write!(f, "**Null**") } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn get_path_test() { + let container1 = Container::new(None, 0, Vec::new()); + let container21 = Container::new(None, 0, Vec::new()); + let container2 = Container::new(None, 0, vec![container21.clone()]); + let root = Container::new(None, 0, vec![container1.clone(), container2.clone()]); + + let mut sb = String::new(); + + root.build_string_of_hierarchy(&mut sb, 0, None); + + println!("root c:{:p}", &*root); + println!("container1 p:{:p} c:{:p}", &*(container1.get_object().get_parent().unwrap()), &*container1); + println!("container2 p:{:p} c:{:p}", &*(container2.get_object().get_parent().unwrap()), &*container2); + println!("container21 p:{:p} c:{:p}", &*(container21.get_object().get_parent().unwrap()), &*container21); + + println!("root: {}", sb); + + assert_eq!(Object::get_path(container1).to_string(), "0"); + assert_eq!(Object::get_path(container2).to_string(), "1"); + assert_eq!(Object::get_path(container21).to_string(), "1.0"); + assert_eq!(Object::get_path(root).to_string(), ""); + } +} From 2230d3dcf04a121cebec275283762c8f3aaedf2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Sat, 16 Sep 2023 10:19:03 +0000 Subject: [PATCH 12/91] WIP to pass the one line test. --- .devcontainer/Dockerfile | 1 + src/callstack.rs | 118 ++++++- src/choice.rs | 29 ++ src/container.rs | 64 +++- src/control_command.rs | 3 +- src/error.rs | 12 + src/flow.rs | 21 ++ src/glue.rs | 34 ++ src/lib.rs | 7 + src/pointer.rs | 25 +- src/push_pop.rs | 6 + src/story.rs | 647 +++++++++++++++++++++++++++++++++++++-- src/story_state.rs | 343 ++++++++++++++++++++- src/value.rs | 23 +- src/variables_state.rs | 47 +++ src/void.rs | 28 ++ 16 files changed, 1350 insertions(+), 58 deletions(-) create mode 100644 src/choice.rs create mode 100644 src/error.rs create mode 100644 src/flow.rs create mode 100644 src/glue.rs create mode 100644 src/push_pop.rs create mode 100644 src/variables_state.rs create mode 100644 src/void.rs diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index a25e0fd..0629fa7 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -25,6 +25,7 @@ RUN set -eux; \ ./rustup-init -y --no-modify-path --profile minimal --default-toolchain $RUST_VERSION --default-host ${rustArch}; \ rm rustup-init; \ chmod -R a+w $RUSTUP_HOME $CARGO_HOME; \ + rustup component add rustfmt; \ rustup --version; \ cargo --version; \ rustc --version; \ No newline at end of file diff --git a/src/callstack.rs b/src/callstack.rs index f999334..3110284 100644 --- a/src/callstack.rs +++ b/src/callstack.rs @@ -1,27 +1,123 @@ -use crate::pointer::Pointer; +use std::{collections::HashMap, rc::Rc}; -pub(crate) struct CallStack { +use crate::{pointer::{Pointer, self}, object::RTObject, push_pop::PushPopType, story::Story}; +pub(crate) struct Element { + pub current_pointer: Pointer, + pub in_expression_evaluation: bool, + pub temporary_variables: HashMap>, + pub push_pop_type: PushPopType, + pub evaluation_stack_height_when_pushed: usize, + pub function_start_in_output_stream: i32, } -impl CallStack { - pub(crate) fn get_current_element(&self) -> &Element { - todo!() +impl Element { + fn new(push_pop_type: PushPopType, pointer: Pointer, in_expression_evaluation: bool) -> Element { + Element { + current_pointer: pointer, + in_expression_evaluation: in_expression_evaluation, + temporary_variables: HashMap::new(), + push_pop_type: push_pop_type, + evaluation_stack_height_when_pushed:0, + function_start_in_output_stream: 0 + } } } -pub(crate) struct Element { - pub(crate) current_pointer: Pointer, +pub(crate) struct Thread { + pub(crate) callstack: Vec, + pub(crate) previous_pointer: Pointer, + thread_index: usize } -impl Element { +impl Thread { + fn new() -> Thread { + Thread { + callstack: Vec::new(), + previous_pointer: pointer::NULL, + thread_index: 0, + } + } +} +pub(crate) struct CallStack { + thread_counter: usize, + start_of_root: Pointer, + threads: Vec } -pub(crate) struct Thread { +impl CallStack { + pub fn new(story: &Story) -> CallStack { + let mut cs = CallStack { + thread_counter: 0, + start_of_root: Pointer::start_of(story.get_main_content_container().clone()), + threads: Vec::new(), + }; -} + cs.reset(); -impl Thread { + cs + } + pub(crate) fn get_current_element(&self) -> &Element { + let thread = self.threads.last().unwrap(); + let cs = &thread.callstack; + cs.last().unwrap() + } + + pub(crate) fn get_current_element_mut(&mut self) -> &mut Element { + let thread = self.threads.last_mut().unwrap(); + let cs = &mut thread.callstack; + cs.last_mut().unwrap() + } + + fn reset(&mut self) { + self.threads.clear(); + self.threads.push(Thread::new()); + self.threads[0].callstack.push(Element::new(PushPopType::Tunnel, self.start_of_root.clone(), false)); + } + + pub(crate) fn can_pop_thread(&self) -> bool { + todo!() + } + + pub(crate) fn pop_thread(&mut self) { + todo!() + } + + pub(crate) fn can_pop(&self) -> bool { + todo!() + } + + pub(crate) fn can_pop_type(&self, t: PushPopType) -> bool { + todo!() + } + + pub(crate) fn element_is_evaluate_from_game(&self) -> bool { + todo!() + } + + pub(crate) fn get_elements(&self) -> &Vec { + self.get_callstack() + } + + pub(crate) fn get_elements_mut(&mut self) -> &mut Vec { + self.get_callstack_mut() + } + + pub(crate) fn get_callstack(&self) -> &Vec { + &self.get_current_thread().callstack + } + + pub(crate) fn get_callstack_mut(&mut self) -> &mut Vec { + &mut self.get_current_thread_mut().callstack + } + + pub(crate) fn get_current_thread(&self) -> &Thread { + self.threads.last().unwrap() + } + + pub(crate) fn get_current_thread_mut(&mut self) -> &mut Thread { + self.threads.last_mut().unwrap() + } } \ No newline at end of file diff --git a/src/choice.rs b/src/choice.rs new file mode 100644 index 0000000..f0af654 --- /dev/null +++ b/src/choice.rs @@ -0,0 +1,29 @@ +use std::rc::Rc; + +use crate::{path::Path, callstack::Thread}; + +pub struct Choice { + target_path: Path, + is_invisible_default: bool, + tags: Vec, + index: usize, + original_thread_index: usize, + text: String, + thread_at_generation: Rc, + source_path: String +} + +impl Choice { + pub fn new() -> Choice { + Choice { + target_path: todo!(), + is_invisible_default: todo!(), + tags: todo!(), + index: todo!(), + original_thread_index: todo!(), + text: todo!(), + thread_at_generation: todo!(), + source_path: todo!(), + } + } +} \ No newline at end of file diff --git a/src/container.rs b/src/container.rs index 1bf594c..c17727b 100644 --- a/src/container.rs +++ b/src/container.rs @@ -1,32 +1,41 @@ use std::{ - cell::{Ref, RefCell}, fmt, rc::Rc, }; -use as_any::{AsAny, Downcast}; +use as_any::Downcast; use crate::{ object::{Object, RTObject, Null}, - object_enum::ObjectEnum, value::{ValueType, Value}, control_command::ControlCommand, }; +const COUNTFLAGS_VISITS: i32 = 1; +const COUNTFLAGS_TURNS: i32 = 2; +const COUNTFLAGS_COUNTSTARTONLY: i32 = 4; + pub struct Container { obj: Object, - pub content: Vec>, pub name: Option, - pub count_flags: i32, + pub content: Vec>, //named_content: HashMap + pub visits_should_be_counted: bool, + pub turn_index_should_be_counted: bool, + pub counting_at_start_only: bool, } impl Container { - pub fn new(name: Option, count_flags: i32, content: Vec>) -> Rc { + pub fn new(name: Option, count_flags: i32, content: Vec>, ) -> Rc { + + let (visits_should_be_counted, turn_index_should_be_counted, counting_at_start_only) = Container::split_count_flags(count_flags); + let c = Rc::new(Container { obj: Object::new(), content, name, - count_flags, + visits_should_be_counted: visits_should_be_counted, + turn_index_should_be_counted: turn_index_should_be_counted, + counting_at_start_only: counting_at_start_only, }); c.content.iter().for_each(|o| o.get_object().set_parent(&c)); @@ -78,7 +87,7 @@ impl Container { Container::append_indentation(sb, indentation); if let ValueType::String(s) = &v.value { sb.push('\"'); - sb.push_str(&&s.replace('\n', "\\n")); + sb.push_str(&&s.string.replace('\n', "\\n")); sb.push('\"'); } else { sb.push_str(&v.to_string()); @@ -149,6 +158,45 @@ impl Container { sb.push(' '); } } + + pub(crate) fn get_count_flags(&self) -> i32 { + let mut flags: i32 = 0; + + if self.visits_should_be_counted { + flags |= COUNTFLAGS_VISITS + } + + if self.turn_index_should_be_counted { + flags |= COUNTFLAGS_TURNS; + } + + if self.counting_at_start_only { + 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; + } + + fn split_count_flags(value: i32) -> (bool, bool, bool) { + + let visits_should_be_counted = if (value & COUNTFLAGS_VISITS) > 0 { true } else { false} ; + + let turn_index_should_be_counted = if (value & COUNTFLAGS_TURNS) > 0 { true } else { false} ; + + let counting_at_start_only = if (value & COUNTFLAGS_COUNTSTARTONLY) > 0 { true } else { false} ; + + (visits_should_be_counted, turn_index_should_be_counted, counting_at_start_only) + } + } impl RTObject for Container { diff --git a/src/control_command.rs b/src/control_command.rs index 9daf389..a9a3b77 100644 --- a/src/control_command.rs +++ b/src/control_command.rs @@ -5,6 +5,7 @@ use strum::Display; use crate::{object::{RTObject, Object}, container::Container}; #[derive(Display)] +#[derive(PartialEq)] pub enum CommandType { NotSet, EvalStart, @@ -37,7 +38,7 @@ pub enum CommandType { pub(crate) struct ControlCommand { obj: Object, - command_type: CommandType + pub command_type: CommandType } impl ControlCommand { diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..4b1123d --- /dev/null +++ b/src/error.rs @@ -0,0 +1,12 @@ +pub trait ErrorHandler { + fn error(message: &str, error_type: ErrorType); +} + +pub 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 +} \ No newline at end of file diff --git a/src/flow.rs b/src/flow.rs new file mode 100644 index 0000000..5ecc7a0 --- /dev/null +++ b/src/flow.rs @@ -0,0 +1,21 @@ +use std::{rc::Rc, cell::RefCell}; + +use crate::{callstack::CallStack, story::Story, choice::Choice, object::RTObject}; + +pub(crate) struct Flow { + pub name: String, + pub callstack: Rc>, + pub output_stream: Vec>, + pub current_choices: Vec +} + +impl Flow { + pub fn new(name: &str, story: &Story) -> Flow { + Flow { + name: name.to_string(), + callstack: Rc::new(RefCell::new(CallStack::new(story))), + output_stream: Vec::new(), + current_choices: Vec::new() + } + } +} \ No newline at end of file diff --git a/src/glue.rs b/src/glue.rs new file mode 100644 index 0000000..4af54aa --- /dev/null +++ b/src/glue.rs @@ -0,0 +1,34 @@ +use std::{ + fmt, + rc::Rc, +}; + +use as_any::Downcast; + +use crate::{ + object::{Object, RTObject, Null}, + value::{ValueType, Value}, control_command::ControlCommand, +}; + + +pub struct Glue { + obj: Object, +} + +impl Glue { + pub fn new() -> Rc { + Rc::new(Glue {obj: Object::new()}) + } +} + +impl RTObject for Glue { + fn get_object(&self) -> &Object { + &self.obj + } +} + +impl fmt::Display for Glue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Glue") + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 693730e..d8aeeec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,5 +10,12 @@ mod path; mod search_result; mod callstack; mod object_enum; +mod flow; +mod push_pop; +mod variables_state; +mod choice; +mod error; +mod glue; +mod void; diff --git a/src/pointer.rs b/src/pointer.rs index aaf3404..1fd6bbd 100644 --- a/src/pointer.rs +++ b/src/pointer.rs @@ -1,14 +1,14 @@ -use std::{rc::Rc, fmt, cell::RefCell}; +use std::{rc::Rc, fmt}; -use crate::{container::Container, object::{RTObject, Object}, path::{Path, Component}, object_enum::ObjectEnum}; +use crate::{container::Container, object::{RTObject, Object}, path::{Path, Component}}; -pub const NULL: Pointer = Pointer::new(None, -1); +pub(crate) const NULL: Pointer = Pointer::new(None, -1); #[derive(Clone)] -pub struct Pointer { - container: Option>, - index: i32, +pub(crate) struct Pointer { + pub container: Option>, + pub index: i32, } impl Pointer { @@ -53,8 +53,8 @@ impl Pointer { Some(Object::get_path(container.clone())) } - pub(crate) fn start_of(container:Option>) -> Pointer { - return Pointer{container, index:0}; + pub(crate) fn start_of(container:Rc) -> Pointer { + return Pointer{container: Some(container), index:0}; } } @@ -66,3 +66,12 @@ impl fmt::Display for Pointer { } } } + +impl Default for Pointer { + fn default() -> Self { + Self { + container: Default::default(), + index: Default::default(), + } + } +} diff --git a/src/push_pop.rs b/src/push_pop.rs new file mode 100644 index 0000000..27a0dfd --- /dev/null +++ b/src/push_pop.rs @@ -0,0 +1,6 @@ +#[derive(PartialEq)] +pub(crate) enum PushPopType { + Tunnel, + Function, + FunctionEvaluationFromGa +} \ No newline at end of file diff --git a/src/story.rs b/src/story.rs index 4cde283..29e38cc 100644 --- a/src/story.rs +++ b/src/story.rs @@ -1,16 +1,35 @@ #![allow(unused_variables, dead_code)] -use std::{rc::Rc, cell::RefCell, any::Any}; -use as_any::{AsAny, Downcast}; +use std::{rc::Rc, time::Instant, borrow::BorrowMut}; -use crate::{json_serialization, container::{Container, self}, story_state::StoryState, object_enum::ObjectEnum}; +use crate::{ + container::{Container}, + error::{ErrorType}, + json_serialization, + push_pop::PushPopType, + story_state::StoryState, pointer::{Pointer, self}, object::RTObject, void::Void, +}; const INK_VERSION_CURRENT: i32 = 21; const INK_VERSION_MINIMUM_COMPATIBLE: i32 = 18; -pub struct Story{ - pub main_content_container: Rc, - state: StoryState, +#[derive(PartialEq)] +enum OutputStateChange { + NoChange, + ExtendedBeyondNewline, + NewlineRemoved +} + +pub struct Story { + main_content_container: Rc, + state: Option, + temporaty_evaluation_container: Option>, + recursive_continue_count: usize, + async_continue_active: bool, + async_saving: bool, + saw_lookahead_unsafe_function_after_new_line: bool, + state_snapshot_at_last_new_line: Option, + on_error: Option, } impl Story { @@ -54,8 +73,7 @@ impl Story { // _listDefinitions = Json.JTokenToListDefinitions (listDefsObj); //} - - let main_content_container= json_serialization::jtoken_to_runtime_object(root_token)?; + let main_content_container = json_serialization::jtoken_to_runtime_object(root_token)?; let main_content_container = main_content_container.into_any().downcast::(); @@ -63,7 +81,17 @@ impl Story { return Err("Root node for ink is not a container?".to_string()); }; - let mut story = Story { main_content_container: main_content_container.unwrap(), state: StoryState::new()}; + let mut story = Story { + main_content_container: main_content_container.unwrap(), + state: None, + temporaty_evaluation_container: None, + recursive_continue_count: 0, + async_continue_active: false, + async_saving: false, + saw_lookahead_unsafe_function_after_new_line: false, + state_snapshot_at_last_new_line: None, + on_error: None, + }; story.reset_state(); @@ -73,11 +101,11 @@ impl Story { fn reset_state(&mut self) { //TODO ifAsyncWeCant("ResetState"); - self.state = StoryState::new(); + self.state = Some(StoryState::new(self)); // TODO state.getVariablesState().setVariableChangedEvent(this); - self.reset_globals(); + self.reset_globals(); } fn reset_globals(&self) { @@ -102,29 +130,606 @@ impl Story { let mut sb = String::new(); self.main_content_container - .build_string_of_hierarchy(&mut sb, 0, None);// TODO state.getCurrentPointer().resolve()); + .build_string_of_hierarchy(&mut sb, 0, None); // TODO state.getCurrentPointer().resolve()); sb } pub fn can_continue(&self) -> bool { - self.state.can_continue() + self.state.as_ref().unwrap().can_continue() } - pub fn cont(&self) -> String { + pub fn cont(&mut self) -> String { self.continue_async(0.0); self.get_current_text() } - pub fn continue_async(&self, millisecs_limit_async: f32) { - todo!() + pub fn continue_async(&mut self, millisecs_limit_async: f32) { + // TODO: if (!hasValidatedExternals) validateExternalBindings(); + + self.continue_internal(millisecs_limit_async); + } + + fn continue_internal(&mut self, millisecs_limit_async: f32) -> Result<(), String> { + let is_async_time_limited = millisecs_limit_async > 0.0; + + self.recursive_continue_count += 1; + + // Doing either: + // - full run through non-async (so not active and don't want to be) + // - Starting async run-through + if !self.async_continue_active { + self.async_continue_active = is_async_time_limited; + if (!self.can_continue()) { + return Err( + "Can't continue - should check canContinue before calling Continue".to_string(), + ); + } + + self.state.as_mut().unwrap().set_did_safe_exit(false); + + self.state.as_mut().unwrap().reset_output(None); + + // 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 self.recursive_continue_count == 1 { + self.state + .as_mut() + .unwrap() + .get_variables_state_mut() + .set_batch_observing_variable_changes(true); + } + } + + // Start timing + let duration_stopwatch = Instant::now(); + + let mut output_stream_ends_in_newline = false; + self.saw_lookahead_unsafe_function_after_new_line = false; + + loop { + match self.continue_single_step() { + Ok(r) => output_stream_ends_in_newline = r, + Err(s) => { + //self.add_error(s, false, e.useEndLineNumber); + break; + } + } + + if output_stream_ends_in_newline { + break; + } + + // Run out of async time? + if self.async_continue_active + && duration_stopwatch.elapsed().as_millis() as f32 > millisecs_limit_async + { + break; + } + + if !self.can_continue() { + break; + } + } + + // 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 output_stream_ends_in_newline || !self.can_continue() { + // Need to rewind, due to evaluating further than we should? + if self.state_snapshot_at_last_new_line.is_some() { + self.restore_state_snapshot(); + } + + // Finished a section of content / reached a choice point? + if !self.can_continue() { + if self + .state + .as_ref() + .unwrap() + .get_callstack() + .borrow() + .can_pop_thread() + { + self.add_error("Thread available to pop, threads should always be flat by the end of evaluation?"); + } + + if self + .state + .as_ref() + .unwrap() + .get_generated_choices() + .is_empty() + && !self.state.as_ref().unwrap().is_did_safe_exit() + && self.temporaty_evaluation_container.is_none() + { + if self + .state + .as_ref() + .unwrap() + .get_callstack() + .borrow() + .can_pop_type(PushPopType::Tunnel) + { + self.add_error("unexpectedly reached end of content. Do you need a '->->' to return from a tunnel?"); + } else if self + .state + .as_ref() + .unwrap() + .get_callstack() + .borrow() + .can_pop_type(PushPopType::Function) + { + self.add_error( + "unexpectedly reached end of content. Do you need a '~ return'?", + ); + } else if !self.state.as_ref().unwrap().get_callstack().borrow().can_pop() { + self.add_error("ran out of content. Do you need a '-> DONE' or '-> END'?"); + } else { + self.add_error("unexpectedly reached end of content for unknown reason. Please debug compiler!"); + } + } + } + self.state.as_mut().unwrap().set_did_safe_exit(false); + self.saw_lookahead_unsafe_function_after_new_line = false; + + if self.recursive_continue_count == 1 { + self.state + .as_mut() + .unwrap() + .get_variables_state_mut() + .set_batch_observing_variable_changes(false); + } + + self.async_continue_active = false; + } + + self.recursive_continue_count -= 1; + + // 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 self.state.as_ref().unwrap().has_error() || self.state.as_ref().unwrap().has_warning() { + match self.on_error { + Some(on_err) => { + if self.state.as_ref().unwrap().has_error() { + for err in self.state.as_ref().unwrap().get_current_errors() { + (on_err)(&err, ErrorType::Error); + } + } + + if self.state.as_ref().unwrap().has_warning() { + for err in self.state.as_ref().unwrap().get_current_warnings() { + (on_err)(&err, ErrorType::Warning); + } + } + + self.reset_errors(); + } + // Throw an exception since there's no error handler + None => { + let mut sb = String::new(); + sb.push_str("Ink had "); + + if self.state.as_ref().unwrap().has_error() { + sb.push_str(&self.state.as_ref().unwrap().get_current_errors().len().to_string()); + + if self.state.as_ref().unwrap().get_current_errors().len() == 1 { + sb.push_str(" error"); + } else { + sb.push_str(" errors"); + } + + if self.state.as_ref().unwrap().has_warning() { + sb.push_str(" and "); + } + } + + if self.state.as_ref().unwrap().has_warning() { + sb.push_str(self.state.as_ref().unwrap().get_current_warnings().len().to_string().as_str()); + if self.state.as_ref().unwrap().get_current_errors().len() == 1 { + sb.push_str(" warning"); + } else { + sb.push_str(" warnings"); + } + } + + sb.push_str(". It is strongly suggested that you assign an error handler to story.onError. The first issue was: "); + + if self.state.as_ref().unwrap().has_error() { + sb.push_str(self.state.as_ref().unwrap().get_current_errors()[0].as_str()); + } else { + sb.push_str(self.state.as_ref().unwrap().get_current_warnings()[0].to_string().as_str()); + } + + return Err(sb); + } + } + } + + Ok(()) + } + + fn continue_single_step(&mut self) -> Result { + // Run main step function (walks through content) + self.step(); + + // Run out of content and we have a default invisible choice that we can follow? + if !self.can_continue() && !self.state.as_ref().unwrap().get_callstack().borrow().element_is_evaluate_from_game() { + self.try_follow_default_invisible_choice(); + } + + // Don't save/rewind during string evaluation, which is e.g. used for choices + if !self.state.as_ref().unwrap().in_string_evaluation(){ + + // We previously found a newline, but were we just double checking that + // it wouldn't immediately be removed by glue? + if let Some(state_snapshot_at_last_new_line) = self.state_snapshot_at_last_new_line.as_ref() { + + // 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. + let change = self.calculate_newline_output_state_change( + state_snapshot_at_last_new_line.get_current_text(), self.state.as_ref().unwrap().get_current_text(), + state_snapshot_at_last_new_line.get_current_tags().len(), + self.state.as_ref().unwrap().get_current_tags().len()); + + // 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 || self.saw_lookahead_unsafe_function_after_new_line { + self.restore_state_snapshot(); + + // Hit a newline for sure, we're done + return Ok(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 { + self.state_snapshot_at_last_new_line = None; + self.discard_snapshot(); + } + } + + // Current content ends in a newline - approaching end of our evaluation + if self.state.as_ref().unwrap().output_stream_ends_in_newline() { + + // 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 self.can_continue() { + + // 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 self.state_snapshot_at_last_new_line.is_some() { + self.state_snapshot(); + } + } + + // Can't continue, so we're about to exit - make sure we + // don't have an old state hanging around. + else { + self.discard_snapshot(); + } + } + } + + // outputStreamEndsInNewline = false + return Ok(false); } pub fn get_current_text(&self) -> String { todo!() } + + pub(crate) fn get_main_content_container(&self) -> Rc { + match self.temporaty_evaluation_container.as_ref() { + Some(c) => c.clone(), + None => self.main_content_container.clone(), + } + } + + fn restore_state_snapshot(&self) { + todo!() + } + + fn add_error(&self, arg: &str) { + todo!() + } + + fn reset_errors(&self) { + todo!() + } + + fn step(&mut self) { + let mut should_add_to_stream = true; + + // Get current content + let mut pointer = self.state.as_ref().unwrap().get_current_pointer().clone(); + + if pointer.is_null() { + return; + } + + // Step directly to the first element of content in a container (if + // necessary) + let r = pointer.resolve(); + + let mut container_to_enter = match r { + Some(o) => match o.into_any().downcast::() { + Ok(c) => Some(c), + Err(_) => None, + }, + None => None, + }; + + while let Some(cte) = container_to_enter.as_ref() { + + // Mark container as being entered + self.visit_container(cte, true); + + // No content? the most we can do is step past it + if cte.content.is_empty() { + break; + } + + pointer = Pointer::start_of(cte.clone()); + + let r = pointer.resolve(); + + container_to_enter = match r { + Some(o) => match o.into_any().downcast::() { + Ok(c) => Some(c), + Err(_) => None, + }, + None => None, + }; + } + + self.state.as_mut().unwrap().set_current_pointer(pointer.clone()); + + // 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) + let current_content_obj = pointer.resolve(); + let is_logic_or_flow_control = self.perform_logic_and_flow_control(¤t_content_obj); + + // Has flow been forced to end by flow control above? + if self.state.as_ref().unwrap().get_current_pointer().is_null() { + return; + } + + if is_logic_or_flow_control { + should_add_to_stream = false; + } + + // Choice with condition? + // TODO + // ChoicePoint choicePoint = currentContentObj instanceof ChoicePoint ? (ChoicePoint) currentContentObj : null; + + // if (choicePoint != null) { + // Choice choice = processChoice(choicePoint); + // if (choice != null) { + // state.getGeneratedChoices().add(choice); + // } + + // currentContentObj = null; + // should_add_to_stream = false; + // } + + // If the container has no content, then it will be + // the "content" itself, but we skip over it. + if current_content_obj.is_some() && current_content_obj.as_ref().unwrap().as_any().is::() { + should_add_to_stream = false; + } + + // Content to add to evaluation stack or the output stream + if should_add_to_stream { + + // 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. + + // TODO + // 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 self.state.as_ref().unwrap().get_in_expression_evaluation() { + self.state.as_mut().unwrap().push_evaluation_stack(current_content_obj); + } + // Output stream content (i.e. not expression evaluation) + else { + self.state.as_mut().unwrap().push_to_output_stream(current_content_obj); + } + } + + // Increment the content pointer, following diverts if necessary + self.next_content(); + + // 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. + + // TODO + // let controlCmd = + // currentContentObj instanceof ControlCommand ? (ControlCommand) currentContentObj : null; + // if (controlCmd != null && controlCmd.getCommandType() == ControlCommand.CommandType.StartThread) { + // state.getCallStack().pushThread(); + // } + } + + fn try_follow_default_invisible_choice(&self) { + todo!() + } + + fn calculate_newline_output_state_change(&self, get_current_text_1: String, get_current_text_2: String, len_1: usize, len_2: usize) -> OutputStateChange { + todo!() + } + + fn state_snapshot(&self) { + todo!() + } + + fn discard_snapshot(&self) { + todo!() + } + + fn visit_container(&mut self, container: &Container, at_start: bool) { + if !container.counting_at_start_only || at_start { + if container.visits_should_be_counted { + self.state.as_mut().unwrap().increment_visit_count_for_container(container); + } + + if container.turn_index_should_be_counted { + self.state.as_mut().unwrap().record_turn_index_visit_to_container(container); + } + } + } + + fn perform_logic_and_flow_control(&self, current_content_obj: &Option>) -> bool { + match current_content_obj { + Some(current_content_obj) => { + // TODO + return false; + }, + None => return false, + } + } + + fn next_content(&mut self) { + // Setting previousContentObject is critical for + // VisitChangedContainersDueToDivert + let cp = self.state.as_ref().unwrap().get_current_pointer(); + self.state.as_mut().unwrap().set_previous_pointer(cp); + + // Divert step? + + // TODO + // if !self.state.as_ref().unwrap().get_diverted_pointer().is_null() { + + // self.state.as_mut().unwrap().setCurrentPointer(state.getDivertedPointer()); + // self.state.as_mut().unwrap().setDivertedPointer(Pointer.Null); + + // // Internally uses state.previousContentObject and + // // state.currentContentObject + // self.visitChangedContainersDueToDivert(); + + // // Diverted location has valid content? + // if !self.state.as_ref().unwrap().get_current_pointer().is_null() { + // 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 + // } + + let successful_pointer_increment = self.increment_content_pointer(); + + // Ran out of content? Try to auto-exit from a function, + // or finish evaluating the content of a thread + if !successful_pointer_increment { + + let mut didPop = false; + + if self.state.as_ref().unwrap().get_callstack().as_ref().borrow().can_pop_type(PushPopType::Function) { + + // Pop from the call stack + self.state.as_mut().unwrap().pop_callstack(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 self.state.as_ref().unwrap().get_in_expression_evaluation() { + self.state.as_mut().unwrap().push_evaluation_stack(Some(Void::new())); + } + + didPop = true; + } else if (self.state.as_ref().unwrap().get_callstack().as_ref().borrow().can_pop_thread()) { + self.state.as_ref().unwrap().get_callstack().as_ref().borrow_mut().pop_thread(); + + didPop = true; + } else { + self.state.as_mut().unwrap().try_exit_function_evaluation_from_game(); + } + + // Step past the point where we last called out + if didPop && !self.state.as_ref().unwrap().get_current_pointer().is_null() { + self.next_content(); + } + } + } + + fn increment_content_pointer(&self) -> bool { + let mut successful_increment = true; + + let mut pointer = self.state.as_ref().unwrap().get_callstack().as_ref().borrow().get_current_element().current_pointer.clone(); + pointer.index += 1; + + let container= pointer.container.as_ref().unwrap().clone(); + + // 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 >= container.content.len() as i32 { + + successful_increment = false; + + let next_ancestor = container.get_object().get_parent(); + + if next_ancestor.is_none() { + break; + } + + let container: Rc = container.clone(); + let index_in_ancestor = next_ancestor.as_ref().unwrap().content.iter().position(|s| Rc::ptr_eq(s, &container)); + if index_in_ancestor.is_none() { + break; + } + + pointer = Pointer::new(next_ancestor, index_in_ancestor.unwrap() as i32); + + // Increment to next content in outer container + pointer.index += 1; + + successful_increment = true; + } + + if !successful_increment { + pointer = pointer::NULL.clone(); + } + + self.state.as_ref().unwrap().get_callstack().as_ref().borrow_mut().get_current_element_mut().current_pointer = pointer; + + return successful_increment; + } } + #[cfg(test)] mod tests { use std::fs; @@ -135,8 +740,14 @@ mod tests { fn oneline_test() { let json_string = fs::read_to_string("examples/inkfiles/basictext/oneline.ink.json").unwrap(); - let story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); + + assert!(story.can_continue()); + let line = story.cont(); + println!("{}", line); + assert_eq!("Line.", line); + assert!(!story.can_continue()); } #[test] @@ -147,7 +758,7 @@ mod tests { println!("{}", story.build_string_of_hierarchy()); } - fn next_all(story: &Story, text: &mut Vec) { + fn next_all(story: &mut Story, text: &mut Vec) { while story.can_continue() { let line = story.cont(); print!("{line}"); diff --git a/src/story_state.rs b/src/story_state.rs index cf50111..7865aab 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -1,16 +1,38 @@ -use crate::{pointer::Pointer, callstack::CallStack}; +#![allow(unused_variables, dead_code)] -pub struct StoryState { +use std::{rc::Rc, borrow::BorrowMut, cell::RefCell, collections::VecDeque}; + +use crate::{pointer::Pointer, callstack::CallStack, story::Story, flow::Flow, variables_state::VariablesState, choice::Choice, object::RTObject, value::{Value, ValueType}, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}}; + +pub const INK_SAVE_STATE_VERSION: u32 = 10; +pub const MIN_COMPATIBLE_LOAD_VERSION: u32 = 8; +static DEFAULT_FLOW_NAME: &str = "DEFAULT_FLOW"; + +pub struct StoryState { + pub(crate) current_flow: Flow, + pub(crate) did_safe_exit: bool, + output_stream_text_dirty: bool, + output_stream_tags_dirty: bool, + variables_state: VariablesState, } impl StoryState { - pub fn new() -> StoryState{ - StoryState {} + pub fn new(story: &Story) -> StoryState { + let current_flow = Flow::new(DEFAULT_FLOW_NAME, story); + let callstack = current_flow.callstack.clone(); + + StoryState { + current_flow: current_flow, + did_safe_exit: false, + output_stream_text_dirty: true, + output_stream_tags_dirty: true, + variables_state: VariablesState::new(callstack), + } } pub fn can_continue(&self) -> bool { - return !self.get_current_pointer().is_null() && !self.has_error(); + !self.get_current_pointer().is_null() && !self.has_error() } pub fn has_error(&self) -> bool { @@ -18,11 +40,316 @@ impl StoryState { false } - fn get_current_pointer(&self) -> Pointer { - return self.get_callstack().get_current_element().current_pointer.clone(); + pub(crate) fn get_current_pointer(&self) -> Pointer { + self.get_callstack().borrow().get_current_element().current_pointer.clone() + } + + pub(crate) fn get_callstack(&self) -> &Rc> { + &self.current_flow.callstack + } + + pub(crate) fn set_did_safe_exit(&mut self, did_safe_exit: bool) { + self.did_safe_exit = did_safe_exit; + } + + pub(crate) fn reset_output(&mut self, objs: Option>>) { + self.get_output_stream_mut().clear(); + if let Some(objs) = objs { + for o in objs { + self.get_output_stream_mut().push(o.clone()); + } + } + self.output_stream_dirty(); + } + + pub(crate) fn get_variables_state(&self) -> &VariablesState { + &self.variables_state + } + + pub(crate) fn get_variables_state_mut(&mut self) -> &mut VariablesState { + &mut self.variables_state + } + + pub(crate) fn get_generated_choices(&self) -> &Vec { + &self.current_flow.current_choices + } + + pub(crate) fn is_did_safe_exit(&self) -> bool { + todo!() + } + + pub(crate) fn has_warning(&self) -> bool { + todo!() + } + + pub(crate) fn get_current_errors(&self) -> &Vec { + todo!() + } + + pub(crate) fn get_current_warnings(&self) -> &Vec { + todo!() + } + + fn get_output_stream(&self) -> &Vec> { + &self.current_flow.output_stream + } + + fn get_output_stream_mut(&mut self) -> &mut Vec> { + &mut self.current_flow.output_stream + } + + fn output_stream_dirty(&mut self) { + self.output_stream_text_dirty = true; + self.output_stream_tags_dirty = true; + } + + pub(crate) fn in_string_evaluation(&self) -> bool { + todo!() + } + + pub(crate) fn get_current_text(&self) -> String { + todo!() + } + + pub(crate) fn get_current_tags(&self) -> Vec { + todo!() + } + + pub(crate) fn output_stream_ends_in_newline(&self) -> bool { + todo!() + } + + pub(crate) fn set_current_pointer(&self, pointer: Pointer) { + self.get_callstack().as_ref().borrow_mut().get_current_element_mut().current_pointer = pointer; + } + + pub(crate) fn get_in_expression_evaluation(&self) -> bool { + self.get_callstack().borrow().get_current_element().in_expression_evaluation + } + + pub(crate) fn push_evaluation_stack(&self, current_content_obj: Option>) { + todo!() + } + + pub(crate) fn push_to_output_stream(&mut self, obj: Option>) { + let text = match &obj { + Some(obj) => { + let obj = obj.clone(); + match obj.into_any().downcast::() { + Ok(v) => match &v.value { + ValueType::String(s) => Some(s.clone()), + _ => None, + }, + Err(_) => None, + } + }, + None => None, + }; + + if let Some(s) = text { + let list_text = StoryState::try_splitting_head_tail_whitespace(&s.string); + + if let Some(list_text) = list_text { + for text_obj in list_text { + self.push_to_output_stream_individual(Rc::new(text_obj)); + } + self.output_stream_dirty(); + return; + } + } + + self.push_to_output_stream_individual(obj.unwrap()); + } + + pub(crate) fn increment_visit_count_for_container(&self, container: &crate::container::Container) { + todo!() + } + + pub(crate) fn record_turn_index_visit_to_container(&self, container: &crate::container::Container) { + todo!() + } + + fn try_splitting_head_tail_whitespace(text: &str) -> Option> { + let mut head_first_newline_idx = -1; + let mut head_last_newline_idx = -1; + for (i, c) in text.chars().enumerate() { + if c == '\n' { + if head_first_newline_idx == -1 { + head_first_newline_idx = i as i32; + } + head_last_newline_idx = i as i32; + } else if c == ' ' || c == '\t' { + continue; + } else { + break; + } + } + + let mut tail_last_newline_idx = -1; + let mut tail_first_newline_idx = -1; + for (i, c) in text.chars().rev().enumerate() { + let reversed_i = text.len() as i32 - i as i32 - 1; + if c == '\n' { + if tail_last_newline_idx == -1 { + tail_last_newline_idx = reversed_i; + } + tail_first_newline_idx = reversed_i; + } else if c == ' ' || c == '\t' { + continue; + } else { + break; + } + } + + if head_first_newline_idx == -1 && tail_last_newline_idx == -1 { + return None; + } + + let mut list_texts = Vec::new(); + let mut inner_str_start = 0; + let mut inner_str_end = text.len(); + + if head_first_newline_idx != -1 { + if head_first_newline_idx > 0 { + let leading_spaces = Value::new_string(&text[0..head_first_newline_idx as usize]); + list_texts.push(leading_spaces); + } + list_texts.push(Value::new_string("\n")); + inner_str_start = head_last_newline_idx + 1; + } + + if tail_last_newline_idx != -1 { + inner_str_end = tail_first_newline_idx as usize; + } + + if inner_str_end > inner_str_start as usize { + let inner_str_text = &text[inner_str_start as usize..inner_str_end]; + list_texts.push(Value::new_string(inner_str_text)); + } + + if tail_last_newline_idx != -1 && tail_first_newline_idx > head_last_newline_idx { + list_texts.push(Value::new_string("\n")); + if tail_last_newline_idx < text.len() as i32 - 1 { + let num_spaces = (text.len() as i32 - tail_last_newline_idx) - 1; + let trailing_spaces = Value::new_string( + &text[(tail_last_newline_idx + 1) as usize..(num_spaces + tail_last_newline_idx + 1) as usize], + ); + list_texts.push(trailing_spaces); + } + } + + Some(list_texts) + } + + fn push_to_output_stream_individual(&mut self, obj: Rc) { + let glue = obj.clone().into_any().downcast::(); + let text = obj.clone().into_any().downcast::(); + let mut include_in_output = true; + + // New glue, so chomp away any whitespace from the end of the stream + if let Ok(_) = glue { + self.trim_newlines_from_output_stream(); + include_in_output = 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 let Ok(text) = text { + let mut function_trim_index = -1; + let cs = self.get_callstack().borrow(); + let curr_el = cs.get_current_element(); + if curr_el.push_pop_type == PushPopType::Function { + function_trim_index = curr_el.function_start_in_output_stream as i32; + } + + let mut glue_trim_index = -1; + for (i, o) in self.get_output_stream().iter().rev().enumerate() { + if let Some(c) = o.as_ref().as_any().downcast_ref::() { + if c.command_type == CommandType::BeginString { + if i as i32 >= function_trim_index { + function_trim_index = -1; + } + + break; + } + } else if let Some(_) = o.as_ref().as_any().downcast_ref::() { + glue_trim_index = i as i32; + break; + } + } + + let mut trim_index = -1; + if glue_trim_index != -1 && function_trim_index != -1 { + trim_index = function_trim_index.min(glue_trim_index); + } else if glue_trim_index != -1 { + trim_index = glue_trim_index; + } else { + trim_index = function_trim_index; + } + + if trim_index != -1 { + if let ValueType::String(t) = &text.value { + if t.is_newline { + include_in_output = false; + } else if t.is_non_whitespace() { + if glue_trim_index > -1 { + self.remove_existing_glue(); + } + + if function_trim_index > -1 { + let mut cs = self.get_callstack().as_ref().borrow_mut(); + let callstack_elements = cs.get_elements_mut(); + for i in (0..callstack_elements.len()).rev() { + if let Some(el) = callstack_elements.get_mut(i) { + if el.push_pop_type == PushPopType::Function { + el.function_start_in_output_stream = -1; + } else { + break; + } + } + } + } + } + } + } else if let ValueType::String(t) = &text.value { + if t.is_newline { + if self.output_stream_ends_in_newline() || !self.output_stream_contains_content() { + include_in_output = false; + } + } + } + } + + if include_in_output { + self.get_output_stream_mut().push(obj); + self.output_stream_dirty(); + } + } + + fn trim_newlines_from_output_stream(&self) { + todo!() + } + + fn remove_existing_glue(&self) { + todo!() + } + + fn output_stream_contains_content(&self) -> bool { + todo!() + } + + pub(crate) fn set_previous_pointer(&self, p: Pointer) { + self.get_callstack().as_ref().borrow_mut().get_current_thread_mut().previous_pointer = p.clone(); + } + + pub(crate) fn try_exit_function_evaluation_from_game(&self) { + todo!() } - fn get_callstack(&self) -> CallStack { + pub(crate) fn pop_callstack(&self, function: PushPopType) { todo!() } diff --git a/src/value.rs b/src/value.rs index 5aa60db..ebcb4a9 100644 --- a/src/value.rs +++ b/src/value.rs @@ -10,13 +10,28 @@ pub enum ValueType { Int(i32), Float(f32), //List(List), - String(String), + String(StringValue), // Not used for coersion described above //DivertTarget, //VariablePointer, } + +#[derive(Clone)] +pub struct StringValue { + pub string: String, + pub is_inline_whitespace: bool, + pub is_newline: bool +} + +impl StringValue { + pub(crate) fn is_non_whitespace(&self) -> bool { + return !self.is_newline && !self.is_inline_whitespace; + } + +} + pub struct Value { obj: Object, pub value: ValueType, @@ -34,7 +49,7 @@ impl fmt::Display for Value { ValueType::Bool(v) => write!(f, "{}", v), ValueType::Int(v) => write!(f, "{}", v), ValueType::Float(v) => write!(f, "{}", v), - ValueType::String(v) => write!(f, "{}", v), + ValueType::String(v) => write!(f, "{}", v.string), } } } @@ -53,7 +68,7 @@ impl Value { } pub fn new_string(v:&str) -> Value { - Value { obj: Object::new(), value: ValueType::String(v.to_string()) } + Value { obj: Object::new(), value: ValueType::String(StringValue {string: v.to_string(), is_inline_whitespace: false, is_newline: false}) } } pub fn is_truthy(&self) -> bool { @@ -61,7 +76,7 @@ impl Value { ValueType::Bool(v) => *v, ValueType::Int(v) => *v != 0, ValueType::Float(v) => *v != 0.0, - ValueType::String(v) => v.len() > 0, + ValueType::String(v) => v.string.len() > 0, } } } \ No newline at end of file diff --git a/src/variables_state.rs b/src/variables_state.rs new file mode 100644 index 0000000..7f790d8 --- /dev/null +++ b/src/variables_state.rs @@ -0,0 +1,47 @@ +use std::{collections::{HashMap, HashSet}, rc::Rc, cell::RefCell}; + +use crate::{object::RTObject, callstack::CallStack}; + +pub struct VariablesState { + global_variables: HashMap>, + default_global_variables: Option>>, + batch_observing_variable_changes: bool, + callstack: Rc>, + changed_variables_for_batch_obs: Option>, + variable_changed_event: Option, + //TODO listDefsOrigin: ListDefinitionsOrigin + //TODO patch: StatePatch +} + +impl VariablesState { + pub(crate) fn new(callstack: Rc>) -> VariablesState { + VariablesState { + global_variables: HashMap::new(), + default_global_variables: None, + batch_observing_variable_changes: false, + callstack: callstack, + changed_variables_for_batch_obs: None, + variable_changed_event: None, + } + } + + pub(crate) fn set_batch_observing_variable_changes(&mut self, value: bool) { + self.batch_observing_variable_changes = value; + + if value { + self.changed_variables_for_batch_obs = Some(HashSet::new()); + } else { + // Finished observing variables in a batch - now send + // notifications for changed variables all in one go. + if self.changed_variables_for_batch_obs.is_some() { + for variableName in self.changed_variables_for_batch_obs.as_ref().unwrap() { + let current_value = self.global_variables.get(variableName).unwrap(); + + (self.variable_changed_event.as_ref().unwrap())(variableName, current_value.as_ref()); + } + } + + self.changed_variables_for_batch_obs = None; + } + } +} diff --git a/src/void.rs b/src/void.rs new file mode 100644 index 0000000..bbdb4c2 --- /dev/null +++ b/src/void.rs @@ -0,0 +1,28 @@ +use std::{ + fmt, + rc::Rc, +}; + +use crate::object::{Object, RTObject}; + +pub struct Void { + obj: Object, +} + +impl Void { + pub fn new() -> Rc { + Rc::new(Void {obj: Object::new()}) + } +} + +impl RTObject for Void { + fn get_object(&self) -> &Object { + &self.obj + } +} + +impl fmt::Display for Void { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Void") + } +} \ No newline at end of file From c39e50e855cb2151e8e682eede24354b7c156270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Sat, 16 Sep 2023 22:29:07 +0000 Subject: [PATCH 13/91] Oneline test passed. --- src/callstack.rs | 55 +++++++++-- src/choice.rs | 54 ++++++---- src/container.rs | 8 +- src/flow.rs | 8 +- src/push_pop.rs | 4 +- src/story.rs | 220 +++++++++++++++++++++++++++++++++-------- src/story_state.rs | 163 ++++++++++++++++++++++++++---- src/variables_state.rs | 4 + 8 files changed, 420 insertions(+), 96 deletions(-) diff --git a/src/callstack.rs b/src/callstack.rs index 3110284..8eb26e9 100644 --- a/src/callstack.rs +++ b/src/callstack.rs @@ -1,6 +1,6 @@ use std::{collections::HashMap, rc::Rc}; -use crate::{pointer::{Pointer, self}, object::RTObject, push_pop::PushPopType, story::Story}; +use crate::{pointer::{Pointer, self}, object::RTObject, push_pop::PushPopType, story::Story, container::Container}; pub(crate) struct Element { pub current_pointer: Pointer, @@ -22,6 +22,15 @@ impl Element { function_start_in_output_stream: 0 } } + + fn copy(&self) -> Element { + let mut copy = Element::new(self.push_pop_type, self.current_pointer.clone(), self.in_expression_evaluation); + copy.temporary_variables = self.temporary_variables.clone(); + copy.evaluation_stack_height_when_pushed = self.evaluation_stack_height_when_pushed; + copy.function_start_in_output_stream = self.function_start_in_output_stream; + + copy + } } pub(crate) struct Thread { @@ -38,6 +47,19 @@ impl Thread { thread_index: 0, } } + + pub(crate) fn copy(&self) -> Thread { + let mut copy = Thread::new(); + copy.thread_index = self.thread_index; + + for e in self.callstack.iter() { + copy.callstack.push(e.copy()); + } + + copy.previous_pointer = self.previous_pointer.clone(); + + copy + } } pub(crate) struct CallStack { @@ -47,10 +69,10 @@ pub(crate) struct CallStack { } impl CallStack { - pub fn new(story: &Story) -> CallStack { + pub fn new(main_content_container: Rc) -> CallStack { let mut cs = CallStack { thread_counter: 0, - start_of_root: Pointer::start_of(story.get_main_content_container().clone()), + start_of_root: Pointer::start_of(main_content_container), threads: Vec::new(), }; @@ -78,11 +100,16 @@ impl CallStack { } pub(crate) fn can_pop_thread(&self) -> bool { - todo!() + return self.threads.len() > 1 && !self.element_is_evaluate_from_game(); } - pub(crate) fn pop_thread(&mut self) { - todo!() + pub(crate) fn pop_thread(&mut self) -> Result<(), String> { + if self.can_pop_thread() { + self.threads.remove(self.threads.len() - 1); + Ok(()) + } else { + Err("Can't pop thread".to_string()) + } } pub(crate) fn can_pop(&self) -> bool { @@ -94,7 +121,7 @@ impl CallStack { } pub(crate) fn element_is_evaluate_from_game(&self) -> bool { - todo!() + self.get_current_element().push_pop_type == PushPopType::FunctionEvaluationFromGame } pub(crate) fn get_elements(&self) -> &Vec { @@ -120,4 +147,18 @@ impl CallStack { pub(crate) fn get_current_thread_mut(&mut self) -> &mut Thread { self.threads.last_mut().unwrap() } + + pub(crate) fn set_current_thread(&mut self, value: Thread) { + // Debug.Assert (threads.Count == 1, "Shouldn't be directly setting the + // current thread when we have a stack of them"); + self.threads.clear(); + self.threads.push(value); + } + + pub(crate) fn fork_thread(&mut self) -> Thread { + let mut forked_thread = self.get_current_thread().copy(); + self.thread_counter += 1; + forked_thread.thread_index = self.thread_counter; + forked_thread + } } \ No newline at end of file diff --git a/src/choice.rs b/src/choice.rs index f0af654..e04d732 100644 --- a/src/choice.rs +++ b/src/choice.rs @@ -1,29 +1,43 @@ -use std::rc::Rc; +use core::fmt; -use crate::{path::Path, callstack::Thread}; +use crate::{path::Path, callstack::Thread, object::{Object, RTObject}}; -pub struct Choice { - target_path: Path, - is_invisible_default: bool, - tags: Vec, - index: usize, - original_thread_index: usize, - text: String, - thread_at_generation: Rc, - source_path: String +pub(crate) struct Choice { + obj: Object, + pub target_path: Path, + pub is_invisible_default: bool, + pub tags: Vec, + pub index: usize, + pub original_thread_index: usize, + pub text: String, + pub thread_at_generation: Thread, + pub source_path: String } impl Choice { - pub fn new() -> Choice { + pub fn new(target_path: Path, source_path: String, is_invisible_default: bool, tags: Vec, thread_at_generation: Thread, text: String, index: usize, original_thread_index: usize) -> Choice { Choice { - target_path: todo!(), - is_invisible_default: todo!(), - tags: todo!(), - index: todo!(), - original_thread_index: todo!(), - text: todo!(), - thread_at_generation: todo!(), - source_path: todo!(), + obj: Object::new(), + target_path: target_path, + is_invisible_default: is_invisible_default, + tags: tags, + index: index, + original_thread_index: original_thread_index, + text: text, + thread_at_generation: thread_at_generation, + source_path: source_path, } } +} + +impl RTObject for Choice { + fn get_object(&self) -> &Object { + &self.obj + } +} + +impl fmt::Display for Choice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "**Choice**") + } } \ No newline at end of file diff --git a/src/container.rs b/src/container.rs index c17727b..9eb5813 100644 --- a/src/container.rs +++ b/src/container.rs @@ -108,11 +108,9 @@ impl Container { } if let Some(pointed_obj) = pointed_obj { - if let Some(pointed_obj) = pointed_obj.downcast_ref::() { - if let Some(obj) = obj.as_ref().downcast_ref::() { - if std::ptr::eq(obj, pointed_obj) { - sb.push_str(" <---"); - } + if !pointed_obj.is::() { + if std::ptr::eq(obj.as_ref(), pointed_obj) { + sb.push_str(" <---"); } } } diff --git a/src/flow.rs b/src/flow.rs index 5ecc7a0..72a653c 100644 --- a/src/flow.rs +++ b/src/flow.rs @@ -1,19 +1,19 @@ use std::{rc::Rc, cell::RefCell}; -use crate::{callstack::CallStack, story::Story, choice::Choice, object::RTObject}; +use crate::{callstack::CallStack, choice::Choice, object::RTObject, container::Container}; pub(crate) struct Flow { pub name: String, pub callstack: Rc>, pub output_stream: Vec>, - pub current_choices: Vec + pub current_choices: Vec> } impl Flow { - pub fn new(name: &str, story: &Story) -> Flow { + pub fn new(name: &str, main_content_container: Rc) -> Flow { Flow { name: name.to_string(), - callstack: Rc::new(RefCell::new(CallStack::new(story))), + callstack: Rc::new(RefCell::new(CallStack::new(main_content_container))), output_stream: Vec::new(), current_choices: Vec::new() } diff --git a/src/push_pop.rs b/src/push_pop.rs index 27a0dfd..22ac56a 100644 --- a/src/push_pop.rs +++ b/src/push_pop.rs @@ -1,6 +1,6 @@ -#[derive(PartialEq)] +#[derive(PartialEq, Clone, Copy)] pub(crate) enum PushPopType { Tunnel, Function, - FunctionEvaluationFromGa + FunctionEvaluationFromGame } \ No newline at end of file diff --git a/src/story.rs b/src/story.rs index 29e38cc..ed1773e 100644 --- a/src/story.rs +++ b/src/story.rs @@ -7,7 +7,7 @@ use crate::{ error::{ErrorType}, json_serialization, push_pop::PushPopType, - story_state::StoryState, pointer::{Pointer, self}, object::RTObject, void::Void, + story_state::StoryState, pointer::{Pointer, self}, object::RTObject, void::Void, path::Path, control_command::ControlCommand, choice::Choice, }; const INK_VERSION_CURRENT: i32 = 21; @@ -101,36 +101,42 @@ impl Story { fn reset_state(&mut self) { //TODO ifAsyncWeCant("ResetState"); - self.state = Some(StoryState::new(self)); + self.state = Some(StoryState::new(self.main_content_container.clone())); // TODO state.getVariablesState().setVariableChangedEvent(this); self.reset_globals(); } - fn reset_globals(&self) { - /* TODO - if (mainContentContainer.getNamedContent().containsKey("global decl")) { - final Pointer originalPointer = new Pointer(state.getCurrentPointer()); + fn reset_globals(&mut self) { + // TODO + // if (self.main_content_container.get_named_content().containsKey("global decl")) { + // let originalPointer = self.state.as_ref().unwrap().get_current_pointer().clone(); - choosePath(new Path("global decl"), false); + // self.choose_path(Path::new_with_components_string(Some("global decl".to_string())), false); - // Continue, but without validating external bindings, - // since we may be doing this reset at initialisation time. - continueInternal(); + // // Continue, but without validating external bindings, + // // since we may be doing this reset at initialisation time. + // self.continue_internal(); - state.setCurrentPointer(originalPointer); - } + // self.state.as_ref().unwrap().set_current_pointer(originalPointer); + // } - state.getVariablesState().snapshotDefaultGlobals(); - */ + self.state.as_mut().unwrap().get_variables_state_mut().snapshot_default_globals(); } pub fn build_string_of_hierarchy(&self) -> String { let mut sb = String::new(); + let cp = self.state.as_ref().unwrap().get_current_pointer().resolve(); + + let cp = match cp { + Some(_) => Some(cp.as_ref().unwrap().as_ref()), + None => None, + }; + self.main_content_container - .build_string_of_hierarchy(&mut sb, 0, None); // TODO state.getCurrentPointer().resolve()); + .build_string_of_hierarchy(&mut sb, 0, cp); sb } @@ -139,15 +145,17 @@ impl Story { self.state.as_ref().unwrap().can_continue() } - pub fn cont(&mut self) -> String { - self.continue_async(0.0); - self.get_current_text() + pub fn cont(&mut self) -> Result { + self.continue_async(0.0)?; + Ok(self.get_current_text()) } - pub fn continue_async(&mut self, millisecs_limit_async: f32) { + pub fn continue_async(&mut self, millisecs_limit_async: f32) -> Result<(), String> { // TODO: if (!hasValidatedExternals) validateExternalBindings(); - self.continue_internal(millisecs_limit_async); + self.continue_internal(millisecs_limit_async)?; + + Ok(()) } fn continue_internal(&mut self, millisecs_limit_async: f32) -> Result<(), String> { @@ -197,6 +205,8 @@ impl Story { } } + println!("{}", self.build_string_of_hierarchy()); + if output_stream_ends_in_newline { break; } @@ -369,14 +379,15 @@ impl Story { // We previously found a newline, but were we just double checking that // it wouldn't immediately be removed by glue? - if let Some(state_snapshot_at_last_new_line) = self.state_snapshot_at_last_new_line.as_ref() { + if let Some(state_snapshot_at_last_new_line) = self.state_snapshot_at_last_new_line.as_mut() { // 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. - let change = self.calculate_newline_output_state_change( - state_snapshot_at_last_new_line.get_current_text(), self.state.as_ref().unwrap().get_current_text(), - state_snapshot_at_last_new_line.get_current_tags().len(), - self.state.as_ref().unwrap().get_current_tags().len()); + let change = Story::calculate_newline_output_state_change( + &state_snapshot_at_last_new_line.get_current_text(), + &self.state.as_mut().unwrap().get_current_text(), + state_snapshot_at_last_new_line.get_current_tags().len() as i32, + self.state.as_ref().unwrap().get_current_tags().len() as i32); // The last time we saw a newline, it was definitely the end of the line, so we // want to rewind to that point. @@ -425,8 +436,9 @@ impl Story { return Ok(false); } - pub fn get_current_text(&self) -> String { - todo!() + pub fn get_current_text(&mut self) -> String { + //TODO ifAsyncWeCant("call currentText since it's a work in progress"); + self.state.as_mut().unwrap().get_current_text() } pub(crate) fn get_main_content_container(&self) -> Rc { @@ -581,13 +593,83 @@ impl Story { } fn try_follow_default_invisible_choice(&self) { - todo!() - } + let all_choices = match self.state.as_ref().unwrap().get_current_choices() { + Some(c) => c, + None => return, + }; - fn calculate_newline_output_state_change(&self, get_current_text_1: String, get_current_text_2: String, len_1: usize, len_2: usize) -> OutputStateChange { - todo!() + // Is a default invisible choice the ONLY choice? + // var invisibleChoices = allChoices.Where (c => + // c.choicePoint.isInvisibleDefault).ToList(); + let mut invisible_choices:Vec> = Vec::new(); + for c in all_choices { + if c.is_invisible_default { + invisible_choices.push(c.clone()); + } + } + + if invisible_choices.len() == 0 || all_choices.len() > invisible_choices.len() { + return; + } + + let choice = &invisible_choices[0]; + + // Invisible choice may have been generated on a different thread, + // in which case we need to restore it before we continue + self.state.as_ref().unwrap().get_callstack().as_ref().borrow_mut().set_current_thread(choice.thread_at_generation.copy()); + + // 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 self.state_snapshot_at_last_new_line.is_some() { + let fork_thread = self.state.as_ref().unwrap().get_callstack().as_ref().borrow_mut().fork_thread(); + self.state.as_ref().unwrap().get_callstack().as_ref().borrow_mut().set_current_thread(fork_thread); + } + + self.choose_path(&choice.target_path, false); } + fn calculate_newline_output_state_change( + prev_text: &str, + curr_text: &str, + prev_tag_count: i32, + curr_tag_count: i32, + ) -> OutputStateChange { + // Simple case: nothing's changed, and we still have a newline + // at the end of the current content + let newline_still_exists = curr_text.len() >= prev_text.len() + && prev_text.len() > 0 + && curr_text.chars().nth(prev_text.len() - 1) == Some('\n'); + if prev_tag_count == curr_tag_count + && prev_text.len() == curr_text.len() + && newline_still_exists + { + return OutputStateChange::NoChange; + } + + // Old newline has been removed, it wasn't the end of the line after all + if !newline_still_exists { + return OutputStateChange::NewlineRemoved; + } + + // Tag added - definitely the start of a new line + if curr_tag_count > prev_tag_count { + return OutputStateChange::ExtendedBeyondNewline; + } + + // There must be new content - check whether it's just whitespace + for c in curr_text.chars().skip(prev_text.len()) { + 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. + OutputStateChange::NoChange + } + fn state_snapshot(&self) { todo!() } @@ -608,14 +690,62 @@ impl Story { } } - fn perform_logic_and_flow_control(&self, current_content_obj: &Option>) -> bool { - match current_content_obj { - Some(current_content_obj) => { - // TODO - return false; + fn perform_logic_and_flow_control(&mut self, content_obj: &Option>) -> bool { + let content_obj = match content_obj { + Some(content_obj) => { + content_obj.clone() }, None => return false, + }; + + if let Some(eval_command) = content_obj.as_ref().as_any().downcast_ref::() { + match eval_command.command_type { + crate::control_command::CommandType::NotSet => todo!(), + crate::control_command::CommandType::EvalStart => todo!(), + crate::control_command::CommandType::EvalOutput => todo!(), + crate::control_command::CommandType::EvalEnd => todo!(), + crate::control_command::CommandType::Duplicate => todo!(), + crate::control_command::CommandType::PopEvaluatedValue => todo!(), + crate::control_command::CommandType::PopFunction => todo!(), + crate::control_command::CommandType::PopTunnel => todo!(), + crate::control_command::CommandType::BeginString => todo!(), + crate::control_command::CommandType::EndString => todo!(), + crate::control_command::CommandType::NoOp => todo!(), + crate::control_command::CommandType::ChoiceCount => todo!(), + crate::control_command::CommandType::Turns => todo!(), + crate::control_command::CommandType::TurnsSince => todo!(), + crate::control_command::CommandType::ReadCount => todo!(), + crate::control_command::CommandType::Random => todo!(), + crate::control_command::CommandType::SeedRandom => todo!(), + crate::control_command::CommandType::VisitIndex => todo!(), + crate::control_command::CommandType::SequenceShuffleIndex => todo!(), + crate::control_command::CommandType::StartThread => todo!(), + crate::control_command::CommandType::Done => { + // We may exist in the context of the initial + // act of creating the thread, or in the context of + // evaluating the content. + if self.state.as_ref().unwrap().get_callstack().borrow().can_pop_thread() { + self.state.as_ref().unwrap().get_callstack().as_ref().borrow_mut().pop_thread(); + } + + // In normal flow - allow safe exit without warning + else { + self.state.as_mut().unwrap().set_did_safe_exit(true); + + // Stop flow in current thread + self.state.as_ref().unwrap().set_current_pointer(pointer::NULL); + } + }, + crate::control_command::CommandType::End => todo!(), + crate::control_command::CommandType::ListFromInt => todo!(), + crate::control_command::CommandType::ListRange => todo!(), + crate::control_command::CommandType::ListRandom => todo!(), + crate::control_command::CommandType::BeginTag => todo!(), + crate::control_command::CommandType::EndTag => todo!(), + } } + + false } fn next_content(&mut self) { @@ -727,6 +857,10 @@ impl Story { return successful_increment; } + + fn choose_path(&self, target_path: &Path, arg: bool) { + todo!() + } } @@ -737,17 +871,19 @@ mod tests { use super::*; #[test] - fn oneline_test() { + fn oneline_test() -> Result<(), String> { let json_string = fs::read_to_string("examples/inkfiles/basictext/oneline.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); assert!(story.can_continue()); - let line = story.cont(); + let line = story.cont()?; println!("{}", line); - assert_eq!("Line.", line); + assert_eq!("Line.", line.trim()); assert!(!story.can_continue()); + + Ok(()) } #[test] @@ -758,9 +894,9 @@ mod tests { println!("{}", story.build_string_of_hierarchy()); } - fn next_all(story: &mut Story, text: &mut Vec) { + fn next_all(story: &mut Story, text: &mut Vec) -> Result<(), String> { while story.can_continue() { - let line = story.cont(); + let line = story.cont()?; print!("{line}"); if !line.trim().is_empty() { @@ -773,5 +909,7 @@ mod tests { fail(TestUtils.joinText(story.getCurrentErrors())); } */ + + Ok(()) } } diff --git a/src/story_state.rs b/src/story_state.rs index 7865aab..6048e5a 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -2,7 +2,7 @@ use std::{rc::Rc, borrow::BorrowMut, cell::RefCell, collections::VecDeque}; -use crate::{pointer::Pointer, callstack::CallStack, story::Story, flow::Flow, variables_state::VariablesState, choice::Choice, object::RTObject, value::{Value, ValueType}, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}}; +use crate::{pointer::Pointer, callstack::CallStack, story::Story, flow::Flow, variables_state::VariablesState, choice::Choice, object::RTObject, value::{Value, ValueType}, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}, container::Container}; pub const INK_SAVE_STATE_VERSION: u32 = 10; pub const MIN_COMPATIBLE_LOAD_VERSION: u32 = 8; @@ -15,20 +15,47 @@ pub struct StoryState { output_stream_text_dirty: bool, output_stream_tags_dirty: bool, variables_state: VariablesState, + alive_flow_names_dirty: bool, + evaluation_stack: Vec>, + main_content_container: Rc, + current_errors: Vec, + current_warnings: Vec, + current_text: Option, } impl StoryState { - pub fn new(story: &Story) -> StoryState { - let current_flow = Flow::new(DEFAULT_FLOW_NAME, story); + pub fn new(main_content_container: Rc) -> StoryState { + let current_flow = Flow::new(DEFAULT_FLOW_NAME, main_content_container.clone()); let callstack = current_flow.callstack.clone(); - StoryState { + let mut state = StoryState { current_flow: current_flow, did_safe_exit: false, output_stream_text_dirty: true, output_stream_tags_dirty: true, variables_state: VariablesState::new(callstack), - } + alive_flow_names_dirty: true, + evaluation_stack: Vec::new(), + main_content_container: main_content_container, + current_errors: Vec::with_capacity(0), + current_warnings: Vec::with_capacity(0), + current_text: None, + }; + + // TODO + // 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; + + state.go_to_start(); + + state } pub fn can_continue(&self) -> bool { @@ -36,8 +63,7 @@ impl StoryState { } pub fn has_error(&self) -> bool { - // TODO return currentErrors != null && currentErrors.size() > 0; - false + !self.current_errors.is_empty() } pub(crate) fn get_current_pointer(&self) -> Pointer { @@ -70,24 +96,24 @@ impl StoryState { &mut self.variables_state } - pub(crate) fn get_generated_choices(&self) -> &Vec { + pub(crate) fn get_generated_choices(&self) -> &Vec> { &self.current_flow.current_choices } pub(crate) fn is_did_safe_exit(&self) -> bool { - todo!() + self.did_safe_exit } pub(crate) fn has_warning(&self) -> bool { - todo!() + !self.current_warnings.is_empty() } pub(crate) fn get_current_errors(&self) -> &Vec { - todo!() + &self.current_errors } pub(crate) fn get_current_warnings(&self) -> &Vec { - todo!() + &self.current_warnings } fn get_output_stream(&self) -> &Vec> { @@ -104,11 +130,49 @@ impl StoryState { } pub(crate) fn in_string_evaluation(&self) -> bool { - todo!() + for e in self.get_output_stream().iter().rev() { + if let Some(cmd) = e.as_any().downcast_ref::() { + if cmd.command_type == CommandType::BeginString { + return true; + } + } + } + false } - pub(crate) fn get_current_text(&self) -> String { - todo!() + pub fn get_current_text(&mut self) -> String { + if self.output_stream_text_dirty { + let mut sb = String::new(); + let mut in_tag = false; + + for outputObj in self.get_output_stream() { + let text_content = match outputObj.as_ref().as_any().downcast_ref::() { + Some(v) => match &v.value { + ValueType::String(s) => Some(s), + _ => None, + }, + None => None, + }; + + if !in_tag && text_content.is_some() { + sb.push_str(&text_content.unwrap().string); + } else { + if let Some(controlCommand) = outputObj.as_ref().as_any().downcast_ref::() { + if controlCommand.command_type == CommandType::BeginTag { + in_tag = true; + } else if controlCommand.command_type == CommandType::EndTag { + in_tag = false; + } + } + } + } + + self.current_text = Some(StoryState::clean_output_whitespace(&sb)); + + self.output_stream_tags_dirty = false; + } + + self.current_text.as_ref().unwrap().to_string() } pub(crate) fn get_current_tags(&self) -> Vec { @@ -116,7 +180,25 @@ impl StoryState { } pub(crate) fn output_stream_ends_in_newline(&self) -> bool { - todo!() + if !self.get_output_stream().is_empty() { + for e in self.get_output_stream().iter().rev() { + if let Some(cmd) = e.as_any().downcast_ref::() { + break; + } + + if let Some(val) = e.as_any().downcast_ref::() { + if let ValueType::String(text) = &val.value { + if text.is_newline { + return true; + } else if text.is_non_whitespace() { + break; + } + } + } + } + } + + false } pub(crate) fn set_current_pointer(&self, pointer: Pointer) { @@ -127,7 +209,7 @@ impl StoryState { self.get_callstack().borrow().get_current_element().in_expression_evaluation } - pub(crate) fn push_evaluation_stack(&self, current_content_obj: Option>) { + pub(crate) fn push_evaluation_stack(&self, content_obj: Option>) { todo!() } @@ -353,4 +435,51 @@ impl StoryState { todo!() } + fn go_to_start(&self) { + self.get_callstack().as_ref().borrow_mut().get_current_element_mut().current_pointer = Pointer::start_of(self.main_content_container.clone()) + } + + pub(crate) fn get_current_choices(&self) -> Option<&Vec>> { + // 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 self.can_continue() { + return None; + } + + Some(&self.current_flow.current_choices) + } + + fn clean_output_whitespace(input_str: &str) -> String { + let mut result = String::with_capacity(input_str.len()); + let mut current_whitespace_start = -1; + let mut start_of_line = 0; + + for (i, c) in input_str.chars().enumerate() { + let is_inline_whitespace = c == ' ' || c == '\t'; + + if is_inline_whitespace && current_whitespace_start == -1 { + current_whitespace_start = i as i32; + } + + if !is_inline_whitespace { + if c != '\n' && current_whitespace_start > 0 && current_whitespace_start != start_of_line { + result.push(' '); + } + current_whitespace_start = -1; + } + + if c == '\n' { + start_of_line = i as i32 + 1; + } + + if !is_inline_whitespace { + result.push(c); + } + } + + result + } + + } \ No newline at end of file diff --git a/src/variables_state.rs b/src/variables_state.rs index 7f790d8..b742bea 100644 --- a/src/variables_state.rs +++ b/src/variables_state.rs @@ -44,4 +44,8 @@ impl VariablesState { self.changed_variables_for_batch_obs = None; } } + + pub(crate) fn snapshot_default_globals(&mut self) { + self.default_global_variables = Some(self.global_variables.clone()); + } } From 3e2f757eba5bd6b581f144b2d0dc474471dda50c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Sun, 17 Sep 2023 12:11:41 +0000 Subject: [PATCH 14/91] Integration test folder + twolines test passes! --- Cargo.toml | 1 + src/callstack.rs | 14 +++ src/glue.rs | 7 +- src/lib.rs | 3 +- src/state_patch.rs | 32 +++++ src/story.rs | 99 ++++++--------- src/story_state.rs | 261 +++++++++++++++++++++++++++++++++------ src/value.rs | 18 ++- src/variables_state.rs | 25 ++-- tests/basic_text_test.rs | 36 ++++++ tests/test_utils.rs | 18 +++ 11 files changed, 393 insertions(+), 121 deletions(-) create mode 100644 src/state_patch.rs create mode 100644 tests/basic_text_test.rs create mode 100644 tests/test_utils.rs diff --git a/Cargo.toml b/Cargo.toml index 7a98ffa..3bd0af4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,4 +20,5 @@ log = "0.4.17" strum_macros = "0.25.2" strum = { version = "0.25.0", features = ["derive"] } as-any = "0.3.0" +rand = "0.8.5" diff --git a/src/callstack.rs b/src/callstack.rs index 8eb26e9..52a6857 100644 --- a/src/callstack.rs +++ b/src/callstack.rs @@ -81,6 +81,20 @@ impl CallStack { cs } + pub fn new_from(to_copy: &CallStack) -> CallStack { + let mut cs = CallStack { + thread_counter: to_copy.thread_counter, + start_of_root: to_copy.start_of_root.clone(), + threads: Vec::new(), + }; + + for other_thread in &to_copy.threads { + cs.threads.push(other_thread.copy()); + } + + cs + } + pub(crate) fn get_current_element(&self) -> &Element { let thread = self.threads.last().unwrap(); let cs = &thread.callstack; diff --git a/src/glue.rs b/src/glue.rs index 4af54aa..2dc0c00 100644 --- a/src/glue.rs +++ b/src/glue.rs @@ -3,12 +3,7 @@ use std::{ rc::Rc, }; -use as_any::Downcast; - -use crate::{ - object::{Object, RTObject, Null}, - value::{ValueType, Value}, control_command::ControlCommand, -}; +use crate::object::{Object, RTObject}; pub struct Glue { diff --git a/src/lib.rs b/src/lib.rs index d8aeeec..bea9dda 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -mod story; +pub mod story; mod json_serialization; mod object; mod value; @@ -17,5 +17,6 @@ mod choice; mod error; mod glue; mod void; +mod state_patch; diff --git a/src/state_patch.rs b/src/state_patch.rs new file mode 100644 index 0000000..f06afa3 --- /dev/null +++ b/src/state_patch.rs @@ -0,0 +1,32 @@ +use std::{ + rc::Rc, collections::{HashMap, HashSet}, +}; + +use crate::object::RTObject; + +#[derive(Clone)] +pub(crate) struct StatePatch { + pub globals: HashMap>, + pub changed_variables: HashSet, + pub visit_counts: HashMap, + pub turn_indices: HashMap, +} + +impl StatePatch { + pub fn new(to_copy: Option<&StatePatch>) -> StatePatch { + match to_copy { + Some(to_copy) => StatePatch { + globals: to_copy.globals.clone(), + changed_variables: to_copy.changed_variables.clone(), + visit_counts: to_copy.visit_counts.clone(), + turn_indices: to_copy.turn_indices.clone(), + }, + None => StatePatch { + globals: HashMap::new(), + changed_variables: HashSet::new(), + visit_counts: HashMap::new(), + turn_indices: HashMap::new(), + }, + } + } +} \ No newline at end of file diff --git a/src/story.rs b/src/story.rs index ed1773e..81c9eba 100644 --- a/src/story.rs +++ b/src/story.rs @@ -1,10 +1,10 @@ #![allow(unused_variables, dead_code)] -use std::{rc::Rc, time::Instant, borrow::BorrowMut}; +use std::{rc::Rc, time::Instant}; use crate::{ - container::{Container}, - error::{ErrorType}, + container::Container, + error::ErrorType, json_serialization, push_pop::PushPopType, story_state::StoryState, pointer::{Pointer, self}, object::RTObject, void::Void, path::Path, control_command::ControlCommand, choice::Choice, @@ -387,7 +387,7 @@ impl Story { &state_snapshot_at_last_new_line.get_current_text(), &self.state.as_mut().unwrap().get_current_text(), state_snapshot_at_last_new_line.get_current_tags().len() as i32, - self.state.as_ref().unwrap().get_current_tags().len() as i32); + self.state.as_mut().unwrap().get_current_tags().len() as i32); // The last time we saw a newline, it was definitely the end of the line, so we // want to rewind to that point. @@ -419,7 +419,7 @@ impl Story { // e.g.: // Hello world\n // record state at the end of here // ~ complexCalculation() // don't actually need this unless it generates text - if self.state_snapshot_at_last_new_line.is_some() { + if self.state_snapshot_at_last_new_line.is_none() { self.state_snapshot(); } } @@ -448,8 +448,22 @@ impl Story { } } - fn restore_state_snapshot(&self) { - todo!() + fn restore_state_snapshot(&mut self) { + // 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. + self.state_snapshot_at_last_new_line.as_mut().unwrap().restore_after_patch(); + + self.state = self.state_snapshot_at_last_new_line.take(); + + // 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 !self.async_saving { + self.state.as_mut().unwrap().apply_any_patch(); + } } fn add_error(&self, arg: &str) { @@ -668,14 +682,25 @@ impl Story { // potential // for glue to kill the newline. OutputStateChange::NoChange - } + } - fn state_snapshot(&self) { - todo!() + fn state_snapshot(&mut self) { + self.state_snapshot_at_last_new_line = self.state.take(); + self.state = Some(self.state_snapshot_at_last_new_line.as_ref().unwrap().copy_and_start_patching()); } - fn discard_snapshot(&self) { - todo!() + fn discard_snapshot(&mut self) { + // 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. + + // TODO + //if (!asyncSaving) state.applyAnyPatch(); + + // No longer need the snapshot. + self.state_snapshot_at_last_new_line = None; } fn visit_container(&mut self, container: &Container, at_start: bool) { @@ -863,53 +888,3 @@ impl Story { } } - -#[cfg(test)] -mod tests { - use std::fs; - - use super::*; - - #[test] - fn oneline_test() -> Result<(), String> { - let json_string = - fs::read_to_string("examples/inkfiles/basictext/oneline.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); - println!("{}", story.build_string_of_hierarchy()); - - assert!(story.can_continue()); - let line = story.cont()?; - println!("{}", line); - assert_eq!("Line.", line.trim()); - assert!(!story.can_continue()); - - Ok(()) - } - - #[test] - fn twolines_test() { - let json_string = - fs::read_to_string("examples/inkfiles/basictext/twolines.ink.json").unwrap(); - let story = Story::new(&json_string).unwrap(); - println!("{}", story.build_string_of_hierarchy()); - } - - fn next_all(story: &mut Story, text: &mut Vec) -> Result<(), String> { - while story.can_continue() { - let line = story.cont()?; - print!("{line}"); - - if !line.trim().is_empty() { - text.push(line.trim().to_string()); - } - } - - /* TODO - if story.has_error() { - fail(TestUtils.joinText(story.getCurrentErrors())); - } - */ - - Ok(()) - } -} diff --git a/src/story_state.rs b/src/story_state.rs index 6048e5a..fc9519b 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -1,17 +1,19 @@ #![allow(unused_variables, dead_code)] -use std::{rc::Rc, borrow::BorrowMut, cell::RefCell, collections::VecDeque}; +use std::{rc::Rc, cell::RefCell, collections::HashMap}; -use crate::{pointer::Pointer, callstack::CallStack, story::Story, flow::Flow, variables_state::VariablesState, choice::Choice, object::RTObject, value::{Value, ValueType}, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}, container::Container}; +use crate::{pointer::{Pointer, self}, callstack::CallStack, flow::Flow, variables_state::VariablesState, choice::Choice, object::RTObject, value::{Value, ValueType}, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}, container::Container, state_patch::StatePatch}; + +use rand::Rng; pub const INK_SAVE_STATE_VERSION: u32 = 10; pub const MIN_COMPATIBLE_LOAD_VERSION: u32 = 8; static DEFAULT_FLOW_NAME: &str = "DEFAULT_FLOW"; -pub struct StoryState { - pub(crate) current_flow: Flow, - pub(crate) did_safe_exit: bool, +pub(crate) struct StoryState { + pub current_flow: Flow, + pub did_safe_exit: bool, output_stream_text_dirty: bool, output_stream_tags_dirty: bool, variables_state: VariablesState, @@ -21,6 +23,14 @@ pub struct StoryState { current_errors: Vec, current_warnings: Vec, current_text: Option, + patch: Option, + named_flows: Option>, + diverted_pointer: Pointer, + visit_counts: HashMap, + turn_indices: HashMap, + current_turn_index: i32, + story_seed: i32, + previous_random: i32, } impl StoryState { @@ -28,6 +38,9 @@ impl StoryState { let current_flow = Flow::new(DEFAULT_FLOW_NAME, main_content_container.clone()); let callstack = current_flow.callstack.clone(); + let mut rng = rand::thread_rng(); + let story_seed = rng.gen_range(0..100); + let mut state = StoryState { current_flow: current_flow, did_safe_exit: false, @@ -40,19 +53,16 @@ impl StoryState { current_errors: Vec::with_capacity(0), current_warnings: Vec::with_capacity(0), current_text: None, + patch: None, + named_flows: None, + diverted_pointer: pointer::NULL.clone(), + visit_counts: HashMap::new(), + turn_indices: HashMap::new(), + current_turn_index: -1, + story_seed: story_seed, + previous_random: 0, }; - // TODO - // 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; - state.go_to_start(); state @@ -175,9 +185,97 @@ impl StoryState { self.current_text.as_ref().unwrap().to_string() } - pub(crate) fn get_current_tags(&self) -> Vec { - todo!() + pub(crate) fn get_current_tags(&mut self) -> Vec { + if self.output_stream_tags_dirty { + let mut current_tags = Vec::new(); + let mut in_tag = false; + let mut sb = String::new(); + + // TODO + + // for output_obj in self.get_output_stream().iter() { + // match output_obj { + // RTObject::ControlCommand(control_command) => { + // match control_command.get_command_type() { + // ControlCommandType::BeginTag => { + // if in_tag && !sb.is_empty() { + // let txt = clean_output_whitespace(&sb); + // current_tags.push(txt); + // sb.clear(); + // } + // in_tag = true; + // } + // ControlCommandType::EndTag => { + // if !sb.is_empty() { + // let txt = clean_output_whitespace(&sb); + // current_tags.push(txt); + // sb.clear(); + // } + // in_tag = false; + // } + // _ => {} + // } + // } + // RTObject::StringValue(str_val) => { + // if in_tag { + // sb.push_str(&str_val.value); + // } + // } + // RTObject::Tag(tag) => { + // if let Some(text) = &tag.get_text() { + // if !text.is_empty() { + // current_tags.push(text.clone()); // tag.text has whitespace already cleaned + // } + // } + // } + // _ => {} + // } + // } + + if !sb.is_empty() { + let txt = StoryState::clean_output_whitespace(&sb); + current_tags.push(txt); + sb.clear(); + } + + self.output_stream_tags_dirty = false; + current_tags + } else { + Vec::new() + } + } + + fn clean_output_whitespace(input_str: &str) -> String { + let mut sb = String::with_capacity(input_str.len()); + let mut current_whitespace_start = -1; + let mut start_of_line = 0; + + for (i, c) in input_str.chars().enumerate() { + let is_inline_whitespace = c == ' ' || c == '\t'; + + if is_inline_whitespace && current_whitespace_start == -1 { + current_whitespace_start = i as i32; + } + + if !is_inline_whitespace { + if c != '\n' && current_whitespace_start > 0 && current_whitespace_start != start_of_line { + sb.push(' '); + } + current_whitespace_start = -1; + } + + if c == '\n' { + start_of_line = i as i32 + 1; + } + + if !is_inline_whitespace { + sb.push(c); + } + } + + sb } + pub(crate) fn output_stream_ends_in_newline(&self) -> bool { if !self.get_output_stream().is_empty() { @@ -420,13 +518,25 @@ impl StoryState { } fn output_stream_contains_content(&self) -> bool { - todo!() + for content in self.get_output_stream() { + if let Some(v) = content.as_any().downcast_ref::() { + if let ValueType::String(_) = v.value { + return true; + } + } + } + + false } pub(crate) fn set_previous_pointer(&self, p: Pointer) { self.get_callstack().as_ref().borrow_mut().get_current_thread_mut().previous_pointer = p.clone(); } + pub(crate) fn get_previous_pointer(&self) -> Pointer { + self.get_callstack().as_ref().borrow_mut().get_current_thread_mut().previous_pointer.clone() + } + pub(crate) fn try_exit_function_evaluation_from_game(&self) { todo!() } @@ -450,35 +560,104 @@ impl StoryState { Some(&self.current_flow.current_choices) } - fn clean_output_whitespace(input_str: &str) -> String { - let mut result = String::with_capacity(input_str.len()); - let mut current_whitespace_start = -1; - let mut start_of_line = 0; - - for (i, c) in input_str.chars().enumerate() { - let is_inline_whitespace = c == ' ' || c == '\t'; - - if is_inline_whitespace && current_whitespace_start == -1 { - current_whitespace_start = i as i32; - } + pub(crate) fn copy_and_start_patching(&self) -> StoryState { + let mut copy = StoryState::new(self.main_content_container.clone()); + + copy.patch = Some(StatePatch::new(self.patch.as_ref())); + + // 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.current_flow.name = self.current_flow.name.clone(); + copy.current_flow.callstack = Rc::new(RefCell::new(CallStack::new_from(&self.current_flow.callstack.as_ref().borrow()))); + copy.current_flow.current_choices = self.current_flow.current_choices.clone(); + copy.current_flow.output_stream = self.current_flow.output_stream.clone(); + copy.output_stream_dirty(); + + // 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 let Some(named_flows) = &self.named_flows { + // TODO + // 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 self.has_error() { + copy.current_errors = self.current_errors.clone(); + } + + if self.has_warning() { + copy.current_warnings = self.current_warnings.clone(); + + } + + // 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) + + //TODO + // copy.variables_state = self.variables_state.; + // copy.variablesState.setCallStack(copy.getCallStack()); + // copy.variablesState.setPatch(copy.patch); + + copy.evaluation_stack = self.evaluation_stack.clone(); + + if !self.diverted_pointer.is_null() { + copy.diverted_pointer = self.diverted_pointer.clone(); + } + + copy.set_previous_pointer(self.get_previous_pointer().clone()); + + // visit counts and turn indicies will be read only, not modified + // while in patch mode + copy.visit_counts = self.visit_counts.clone(); + copy.turn_indices = self.turn_indices.clone(); + + copy.current_turn_index = self.current_turn_index; + copy.story_seed = self.story_seed; + copy.previous_random = self.previous_random; + + copy.set_did_safe_exit(self.did_safe_exit); + + copy + } + + pub(crate) fn restore_after_patch(&mut self) { + // 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. + self.variables_state.callstack = self.get_callstack().clone(); + self.variables_state.patch = self.patch.clone(); // usually null + } + + pub(crate) fn apply_any_patch(&mut self) { + if self.patch.is_none() { + return; + } - if !is_inline_whitespace { - if c != '\n' && current_whitespace_start > 0 && current_whitespace_start != start_of_line { - result.push(' '); - } - current_whitespace_start = -1; - } + self.variables_state.apply_patch(); - if c == '\n' { - start_of_line = i as i32 + 1; + if let Some(patch) = &self.patch { + for (container, count) in &patch.visit_counts { + self.apply_count_changes(container.clone(), *count, true); } - if !is_inline_whitespace { - result.push(c); + for (container, index) in &patch.turn_indices { + self.apply_count_changes(container.clone(), *index, false); } } - result + self.patch = None; + } + + fn apply_count_changes(&self, clone: String, count: usize, arg: bool) { + todo!() } diff --git a/src/value.rs b/src/value.rs index ebcb4a9..5c6cb47 100644 --- a/src/value.rs +++ b/src/value.rs @@ -68,7 +68,23 @@ impl Value { } pub fn new_string(v:&str) -> Value { - Value { obj: Object::new(), value: ValueType::String(StringValue {string: v.to_string(), is_inline_whitespace: false, is_newline: false}) } + + let mut inline_ws = true; + + for c in v.chars() { + if c != ' ' && c != '\t' { + inline_ws = false; + break; + } + } + + Value { + obj: Object::new(), + value: ValueType::String(StringValue { + string: v.to_string(), + is_inline_whitespace: inline_ws, + is_newline: v.eq("\n")}) + } } pub fn is_truthy(&self) -> bool { diff --git a/src/variables_state.rs b/src/variables_state.rs index b742bea..d395e39 100644 --- a/src/variables_state.rs +++ b/src/variables_state.rs @@ -1,16 +1,16 @@ use std::{collections::{HashMap, HashSet}, rc::Rc, cell::RefCell}; -use crate::{object::RTObject, callstack::CallStack}; - -pub struct VariablesState { - global_variables: HashMap>, - default_global_variables: Option>>, - batch_observing_variable_changes: bool, - callstack: Rc>, - changed_variables_for_batch_obs: Option>, - variable_changed_event: Option, +use crate::{object::RTObject, callstack::CallStack, state_patch::StatePatch}; + +pub(crate) struct VariablesState { + pub global_variables: HashMap>, + pub default_global_variables: Option>>, + pub batch_observing_variable_changes: bool, + pub callstack: Rc>, + pub changed_variables_for_batch_obs: Option>, + pub variable_changed_event: Option, //TODO listDefsOrigin: ListDefinitionsOrigin - //TODO patch: StatePatch + pub patch: Option, } impl VariablesState { @@ -22,6 +22,7 @@ impl VariablesState { callstack: callstack, changed_variables_for_batch_obs: None, variable_changed_event: None, + patch: None, } } @@ -48,4 +49,8 @@ impl VariablesState { pub(crate) fn snapshot_default_globals(&mut self) { self.default_global_variables = Some(self.global_variables.clone()); } + + pub(crate) fn apply_patch(&self) { + todo!() + } } diff --git a/tests/basic_text_test.rs b/tests/basic_text_test.rs new file mode 100644 index 0000000..6c6a841 --- /dev/null +++ b/tests/basic_text_test.rs @@ -0,0 +1,36 @@ +use std::fs; +use bladeink::story::Story; + +mod test_utils; + +#[test] +fn oneline_test() -> Result<(), String> { + let json_string = + fs::read_to_string("examples/inkfiles/basictext/oneline.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + + assert!(story.can_continue()); + let line = story.cont()?; + println!("{}", line); + assert_eq!("Line.", line.trim()); + assert!(!story.can_continue()); + + Ok(()) +} + +#[test] +fn twolines_test() -> Result<(), String> { + let json_string = + fs::read_to_string("examples/inkfiles/basictext/twolines.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + + let mut text: Vec = Vec::new(); + test_utils::next_all(&mut story, &mut text)?; + assert_eq!(2, text.len()); + assert_eq!("Line.", text[0]); + assert_eq!("Other line.", text[1]); + + Ok(()) +} diff --git a/tests/test_utils.rs b/tests/test_utils.rs new file mode 100644 index 0000000..9c170aa --- /dev/null +++ b/tests/test_utils.rs @@ -0,0 +1,18 @@ +use bladeink::story::Story; + +pub fn next_all(story: &mut Story, text: &mut Vec) -> Result<(), String> { + while story.can_continue() { + let line = story.cont()?; + print!("{line}"); + + if !line.trim().is_empty() { + text.push(line.trim().to_string()); + } + } + + // if story.has_error() { + // Err(TestUtils.joinText(story.get_current_errors())); + // } + + Ok(()) +} \ No newline at end of file From cf1aa687c22fb8d3baaac0900e9ddf1bb8a7a5ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Sun, 17 Sep 2023 17:18:21 +0000 Subject: [PATCH 15/91] Simple Glue test passes. --- src/choice.rs | 6 +- src/glue.rs | 4 +- src/json_serialization.rs | 6 +- src/story.rs | 16 +++++ src/story_state.rs | 120 +++++++++++++++++++++++++++----------- src/value.rs | 10 ++++ tests/choice_test.rs | 14 +++++ tests/glue_test.rs | 19 ++++++ tests/test_utils.rs | 87 +++++++++++++++++++++++++-- 9 files changed, 237 insertions(+), 45 deletions(-) create mode 100644 tests/choice_test.rs create mode 100644 tests/glue_test.rs diff --git a/src/choice.rs b/src/choice.rs index e04d732..e651b24 100644 --- a/src/choice.rs +++ b/src/choice.rs @@ -2,7 +2,7 @@ use core::fmt; use crate::{path::Path, callstack::Thread, object::{Object, RTObject}}; -pub(crate) struct Choice { +pub struct Choice { obj: Object, pub target_path: Path, pub is_invisible_default: bool, @@ -10,12 +10,12 @@ pub(crate) struct Choice { pub index: usize, pub original_thread_index: usize, pub text: String, - pub thread_at_generation: Thread, + pub(crate) thread_at_generation: Thread, pub source_path: String } impl Choice { - pub fn new(target_path: Path, source_path: String, is_invisible_default: bool, tags: Vec, thread_at_generation: Thread, text: String, index: usize, original_thread_index: usize) -> Choice { + pub(crate) fn new(target_path: Path, source_path: String, is_invisible_default: bool, tags: Vec, thread_at_generation: Thread, text: String, index: usize, original_thread_index: usize) -> Choice { Choice { obj: Object::new(), target_path: target_path, diff --git a/src/glue.rs b/src/glue.rs index 2dc0c00..b171606 100644 --- a/src/glue.rs +++ b/src/glue.rs @@ -11,8 +11,8 @@ pub struct Glue { } impl Glue { - pub fn new() -> Rc { - Rc::new(Glue {obj: Object::new()}) + pub fn new() -> Self { + Glue {obj: Object::new()} } } diff --git a/src/json_serialization.rs b/src/json_serialization.rs index 449bc42..2f73224 100644 --- a/src/json_serialization.rs +++ b/src/json_serialization.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, rc::Rc, cell::RefCell}; use crate::{ container::Container, - object::{self, RTObject}, control_command::{CommandType, ControlCommand}, value::Value, object_enum::ObjectEnum, + object::{self, RTObject}, control_command::{CommandType, ControlCommand}, value::Value, object_enum::ObjectEnum, glue::Glue, }; pub fn jtoken_to_runtime_object(token: &serde_json::Value) -> Result, String> { @@ -27,7 +27,9 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value) -> Result".eq(str) {return new Glue();} + if "<>".eq(str) { + return Ok(Rc::new(Glue::new())); + } if let Some(control_command) = create_control_command(str) { return Ok(Rc::new(control_command)); diff --git a/src/story.rs b/src/story.rs index 81c9eba..aa89aa8 100644 --- a/src/story.rs +++ b/src/story.rs @@ -886,5 +886,21 @@ impl Story { fn choose_path(&self, target_path: &Path, arg: bool) { todo!() } + + pub fn get_current_choices(&self) -> Vec { + todo!() + } + + pub fn has_error(&self) -> bool { + self.state.as_ref().unwrap().has_error() + } + + pub fn get_current_errors(&self) -> &Vec { + self.state.as_ref().unwrap().get_current_errors() + } + + pub fn choose_choice_index(&self, choice_list_index: usize) { + todo!() + } } diff --git a/src/story_state.rs b/src/story_state.rs index fc9519b..cedd0e9 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -423,7 +423,7 @@ impl StoryState { fn push_to_output_stream_individual(&mut self, obj: Rc) { let glue = obj.clone().into_any().downcast::(); - let text = obj.clone().into_any().downcast::(); + let text = Value::get_string_value(obj.as_ref()); let mut include_in_output = true; // New glue, so chomp away any whitespace from the end of the stream @@ -437,12 +437,15 @@ impl StoryState { // - Function start/end trimming // - User-defined glue: <> // We also need to know when to stop trimming when there's non-whitespace. - else if let Ok(text) = text { + else if let Some(text) = text { let mut function_trim_index = -1; - let cs = self.get_callstack().borrow(); - let curr_el = cs.get_current_element(); - if curr_el.push_pop_type == PushPopType::Function { - function_trim_index = curr_el.function_start_in_output_stream as i32; + + { // block to release cs borrow + let cs = self.get_callstack().borrow(); + let curr_el = cs.get_current_element(); + if curr_el.push_pop_type == PushPopType::Function { + function_trim_index = curr_el.function_start_in_output_stream as i32; + } } let mut glue_trim_index = -1; @@ -470,35 +473,31 @@ impl StoryState { trim_index = function_trim_index; } - if trim_index != -1 { - if let ValueType::String(t) = &text.value { - if t.is_newline { - include_in_output = false; - } else if t.is_non_whitespace() { - if glue_trim_index > -1 { - self.remove_existing_glue(); - } - - if function_trim_index > -1 { - let mut cs = self.get_callstack().as_ref().borrow_mut(); - let callstack_elements = cs.get_elements_mut(); - for i in (0..callstack_elements.len()).rev() { - if let Some(el) = callstack_elements.get_mut(i) { - if el.push_pop_type == PushPopType::Function { - el.function_start_in_output_stream = -1; - } else { - break; - } + if trim_index != -1 { + if text.is_newline { + include_in_output = false; + } else if text.is_non_whitespace() { + if glue_trim_index > -1 { + self.remove_existing_glue(); + } + + if function_trim_index > -1 { + let mut cs = self.get_callstack().as_ref().borrow_mut(); + let callstack_elements = cs.get_elements_mut(); + for i in (0..callstack_elements.len()).rev() { + if let Some(el) = callstack_elements.get_mut(i) { + if el.push_pop_type == PushPopType::Function { + el.function_start_in_output_stream = -1; + } else { + break; } } } } } - } else if let ValueType::String(t) = &text.value { - if t.is_newline { - if self.output_stream_ends_in_newline() || !self.output_stream_contains_content() { - include_in_output = false; - } + } else if text.is_newline { + if self.output_stream_ends_in_newline() || !self.output_stream_contains_content() { + include_in_output = false; } } } @@ -509,12 +508,65 @@ impl StoryState { } } - fn trim_newlines_from_output_stream(&self) { - todo!() + fn trim_newlines_from_output_stream(&mut self) { + let mut remove_whitespace_from = -1; + let output_stream = self.get_output_stream_mut(); + + // 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 + let mut i = output_stream.len() as i32 - 1; + while i >= 0 { + if let Some(obj) = output_stream.get(i as usize) { + if obj.as_ref().as_any().is::() { + break; + } else if let Some(sv) = Value::get_string_value(obj.as_ref()) { + + if sv.is_non_whitespace() { + break; + } else if sv.is_newline { + remove_whitespace_from = i; + } + } + } + i -= 1; + } + + // Remove the whitespace + if remove_whitespace_from >= 0 { + i = remove_whitespace_from; + while i < output_stream.len() as i32 { + if let Some(text) = Value::get_string_value(output_stream[i as usize].as_ref()) { + output_stream.remove(i as usize); + } else { + i += 1; + } + } + } + + self.output_stream_dirty(); } - fn remove_existing_glue(&self) { - todo!() + fn remove_existing_glue(&mut self) { + let output_stream = self.get_output_stream_mut(); + + let mut i = output_stream.len() as i32 - 1; + while i >= 0 { + if let Some(c) = output_stream.get(i as usize) { + if c.as_ref().as_any().is::() { + output_stream.remove(i as usize); + } else if c.as_ref().as_any().is::() { + break; + } + } + i -= 1; + } + + self.output_stream_dirty(); } fn output_stream_contains_content(&self) -> bool { diff --git a/src/value.rs b/src/value.rs index 5c6cb47..50c67f6 100644 --- a/src/value.rs +++ b/src/value.rs @@ -95,4 +95,14 @@ impl Value { ValueType::String(v) => v.string.len() > 0, } } + + pub fn get_string_value(o: &dyn RTObject) -> Option<&StringValue> { + match o.as_any().downcast_ref::() { + Some(v) => match &v.value { + ValueType::String(v) => Some(&v), + _ => None, + }, + None => None, + } + } } \ No newline at end of file diff --git a/tests/choice_test.rs b/tests/choice_test.rs new file mode 100644 index 0000000..1608a00 --- /dev/null +++ b/tests/choice_test.rs @@ -0,0 +1,14 @@ + +mod test_utils; + +#[test] +fn no_choice_test() -> Result<(), String> { + let mut errors:Vec = Vec::new(); + + let text = test_utils::run_story("examples/inkfiles/choices/no-choice-text.ink.json", None, &mut errors)?; + + assert_eq!(0, errors.len()); + assert_eq!("Hello world!\nHello back!\n", test_utils::join_text(&text)); + + Ok(()) +} diff --git a/tests/glue_test.rs b/tests/glue_test.rs new file mode 100644 index 0000000..57deb7a --- /dev/null +++ b/tests/glue_test.rs @@ -0,0 +1,19 @@ +use std::fs; +use bladeink::story::Story; + +mod test_utils; + +#[test] +fn simple_glue_test() -> Result<(), String> { + let json_string = + test_utils::get_json_string("examples/inkfiles/glue/simple-glue.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + + let mut text: Vec = Vec::new(); + test_utils::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("Some content with glue.", text[0]); + + Ok(()) +} diff --git a/tests/test_utils.rs b/tests/test_utils.rs index 9c170aa..6dca322 100644 --- a/tests/test_utils.rs +++ b/tests/test_utils.rs @@ -1,4 +1,7 @@ +use std::{error::Error, path::Path, fs}; + use bladeink::story::Story; +use rand::Rng; pub fn next_all(story: &mut Story, text: &mut Vec) -> Result<(), String> { while story.can_continue() { @@ -10,9 +13,85 @@ pub fn next_all(story: &mut Story, text: &mut Vec) -> Result<(), String> } } - // if story.has_error() { - // Err(TestUtils.joinText(story.get_current_errors())); - // } + if story.has_error() { + return Err(join_text(&story.get_current_errors())); + } Ok(()) -} \ No newline at end of file +} + +pub fn join_text(text: &Vec) -> String { + let mut sb = String::new(); + + for s in text { + sb.push_str(s); + } + + sb +} + + +pub fn run_story( + filename: &str, + choice_list: Option>, + errors: &mut Vec, +) -> Result, String> { + // 1) Load story + let json = get_json_string(filename).unwrap(); + + let mut story = Story::new(&json)?; + + let mut text = Vec::new(); + + let mut choice_list_index = 0; + + let mut rng = rand::thread_rng(); + + while story.can_continue() || !story.get_current_choices().is_empty() { + // 2) Game content, line by line + while story.can_continue() { + let line = story.cont()?; + print!("{}", line); + text.push(line); + } + + if story.has_error() { + for error_msg in story.get_current_errors() { + println!("{}", error_msg); + errors.push(error_msg.to_string()); + } + } + + // 3) Display story.current_choices list, allow player to choose one + let current_choices = story.get_current_choices(); + if !current_choices.is_empty() { + let len = current_choices.len(); + + for choice in current_choices { + println!("{}", choice.text); + text.push(format!("{}\n", choice.text)); + } + + if let Some(choice_list) = &choice_list { + if choice_list_index < choice_list.len() { + story.choose_choice_index(choice_list[choice_list_index]); + choice_list_index += 1; + } else { + let random_choice_index = rng.gen_range(0..len); + story.choose_choice_index(random_choice_index); + } + } else { + let random_choice_index = rng.gen_range(0..len); + story.choose_choice_index(random_choice_index); + } + } + } + + Ok(text) +} + +pub fn get_json_string(filename: &str) -> Result> { + let path = Path::new(filename); + let json = fs::read_to_string(path)?; + Ok(json) +} From bfb68a8bf8586b6a4fc3350db73b49fe12e0055e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Mon, 18 Sep 2023 20:55:24 +0000 Subject: [PATCH 16/91] WIP passing choice test --- src/choice.rs | 24 +++- src/choice_point.rs | 123 ++++++++++++++++++ src/container.rs | 133 ++++++++++++++----- src/divert.rs | 98 ++++++++++++++ src/json_serialization.rs | 219 ++++++++++++++++++++++++++++---- src/lib.rs | 3 + src/object.rs | 93 ++++++++++++-- src/path.rs | 30 ++--- src/pointer.rs | 10 +- src/search_result.rs | 38 ++++++ src/story.rs | 261 ++++++++++++++++++++++++++++++++++---- src/story_state.rs | 69 ++++++++-- src/tag.rs | 31 +++++ src/value.rs | 10 +- src/void.rs | 2 +- 15 files changed, 1018 insertions(+), 126 deletions(-) create mode 100644 src/choice_point.rs create mode 100644 src/divert.rs create mode 100644 src/tag.rs diff --git a/src/choice.rs b/src/choice.rs index e651b24..a58de4e 100644 --- a/src/choice.rs +++ b/src/choice.rs @@ -1,4 +1,5 @@ use core::fmt; +use std::cell::RefCell; use crate::{path::Path, callstack::Thread, object::{Object, RTObject}}; @@ -7,10 +8,10 @@ pub struct Choice { pub target_path: Path, pub is_invisible_default: bool, pub tags: Vec, - pub index: usize, + pub index: RefCell, pub original_thread_index: usize, pub text: String, - pub(crate) thread_at_generation: Thread, + pub(crate) thread_at_generation: RefCell>, pub source_path: String } @@ -21,10 +22,25 @@ impl Choice { target_path: target_path, is_invisible_default: is_invisible_default, tags: tags, - index: index, + index: RefCell::new(index), original_thread_index: original_thread_index, text: text, - thread_at_generation: thread_at_generation, + thread_at_generation: RefCell::new(Some(thread_at_generation)), + source_path: source_path, + } + } + + pub(crate) fn new_from_json(path_string_on_choice: &str, source_path: String, text: &str, index: usize, original_thread_index: usize) -> Choice { + + Choice { + obj: Object::new(), + target_path: Path::new_with_components_string(Some(path_string_on_choice)), + is_invisible_default: false, + tags: Vec::new(), + index: RefCell::new(index), + original_thread_index: original_thread_index, + text: text.to_string(), + thread_at_generation: RefCell::new(None), source_path: source_path, } } diff --git a/src/choice_point.rs b/src/choice_point.rs new file mode 100644 index 0000000..8cdcbf0 --- /dev/null +++ b/src/choice_point.rs @@ -0,0 +1,123 @@ +use core::fmt; +use std::{rc::Rc, cell::RefCell}; + +use crate::{path::Path, object::{Object, RTObject}, container::Container}; + +pub struct ChoicePoint { + obj: Object, + has_choice_only_content: bool, + has_start_content: bool, + is_invisible_default: bool, + once_only: bool, + has_condition: bool, + path_on_choice: RefCell>, +} + +impl ChoicePoint { + pub fn new(flags: i32, path_string_on_choice: &str) -> Self { + Self { + obj: Object::new(), + has_choice_only_content: (flags & 4) > 0, + has_start_content: (flags & 2) > 0, + is_invisible_default: (flags & 8) > 0, + once_only: (flags & 16) > 0, + has_condition: (flags & 1) > 0, + path_on_choice: RefCell::new(Some(Path::new_with_components_string(Some(path_string_on_choice)))), + } + } + + pub fn with_once_only(once_only: bool) -> Self { + Self { + obj: Object::new(), + has_choice_only_content: false, + has_start_content: false, + is_invisible_default: false, + once_only, + has_condition: false, + path_on_choice: RefCell::new(None), + } + } + + pub fn get_choice_target(self: &Rc) -> Option> { + Object::resolve_path(self.clone(), &self.path_on_choice.borrow().as_ref().unwrap()).get_container() + } + + pub fn get_flags(&self) -> i32 { + let mut flags = 0; + if self.has_condition() { + flags |= 1; + } + if self.has_start_content() { + flags |= 2; + } + if self.has_choice_only_content() { + flags |= 4; + } + if self.is_invisible_default() { + flags |= 8; + } + if self.once_only() { + flags |= 16; + } + flags + } + + pub fn has_choice_only_content(&self) -> bool { + self.has_choice_only_content + } + + pub fn has_condition(&self) -> bool { + self.has_condition + } + + pub fn has_start_content(&self) -> bool { + self.has_start_content + } + + pub fn is_invisible_default(&self) -> bool { + self.is_invisible_default + } + + pub fn once_only(&self) -> bool { + self.once_only + } + + pub fn get_path_on_choice(self: &Rc) -> Path { + // Resolve any relative paths to global ones as we come across them + let has_path = self.path_on_choice.borrow().is_some(); + if has_path && self.path_on_choice.borrow().as_ref().unwrap().is_relative(){ + if let Some(choice_target_obj) = self.get_choice_target() { + self.path_on_choice.replace(Some(choice_target_obj.get_path())); + } + } + + self.path_on_choice.borrow().as_ref().unwrap().clone() + } + + pub fn get_path_string_on_choice(self: &Rc) -> String { + Object::compact_path_string(self.clone(), self.get_path_on_choice()) + } +} + +impl RTObject for ChoicePoint { + fn get_object(&self) -> &Object { + &self.obj + } +} + +impl fmt::Display for ChoicePoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // let target_line_num = self.get_debug_line_number_of_path(self.get_path_on_choice()?)?; + + // let mut target_string = self.get_path_on_choice()?.to_string(); + + let target_string = self.path_on_choice.borrow().as_ref().unwrap().to_string(); + + // if let Some(line_num) = target_line_num { + // target_string = format!(" line {}({})", line_num, target_string); + // } + + + write!(f,"Choice: -> {}", target_string) + } +} \ No newline at end of file diff --git a/src/container.rs b/src/container.rs index 9eb5813..20bc6d1 100644 --- a/src/container.rs +++ b/src/container.rs @@ -1,13 +1,13 @@ use std::{ fmt, - rc::Rc, + rc::Rc, collections::HashMap, }; use as_any::Downcast; use crate::{ object::{Object, RTObject, Null}, - value::{ValueType, Value}, control_command::ControlCommand, + value::{ValueType, Value}, control_command::ControlCommand, path::{Path, Component}, search_result::SearchResult, }; const COUNTFLAGS_VISITS: i32 = 1; @@ -18,20 +18,21 @@ pub struct Container { obj: Object, pub name: Option, pub content: Vec>, - //named_content: HashMap + pub named_content: HashMap>, pub visits_should_be_counted: bool, pub turn_index_should_be_counted: bool, pub counting_at_start_only: bool, } impl Container { - pub fn new(name: Option, count_flags: i32, content: Vec>, ) -> Rc { + pub fn new(name: Option, count_flags: i32, content: Vec>, named_content: HashMap>) -> Rc { let (visits_should_be_counted, turn_index_should_be_counted, counting_at_start_only) = Container::split_count_flags(count_flags); let c = Rc::new(Container { obj: Object::new(), content, + named_content, name, visits_should_be_counted: visits_should_be_counted, turn_index_should_be_counted: turn_index_should_be_counted, @@ -81,26 +82,19 @@ impl Container { for (i, obj) in self.content.iter().enumerate() { if let Some(c) = obj.as_ref().downcast_ref::() { c.build_string_of_hierarchy(sb, indentation, pointed_obj); - } - - if let Some(v) = obj.as_ref().downcast_ref::() { + } else if let Some(v) = obj.as_ref().downcast_ref::() { Container::append_indentation(sb, indentation); if let ValueType::String(s) = &v.value { sb.push('\"'); sb.push_str(&&s.string.replace('\n', "\\n")); sb.push('\"'); } else { + Container::append_indentation(sb, indentation); sb.push_str(&v.to_string()); } - } - - if let Some(cc) = obj.as_ref().downcast_ref::() { + } else { Container::append_indentation(sb, indentation); - sb.push_str(&cc.to_string()); - } - - if let Some(n) = obj.as_ref().downcast_ref::() { - sb.push_str(&n.to_string()); + sb.push_str(&obj.to_string()); } if i != self.content.len() - 1 { @@ -118,31 +112,30 @@ impl Container { sb.push('\n'); } - /* TODO - HashMap onlyNamed = new HashMap(); + // HashMap onlyNamed = new HashMap(); - for (Entry objKV : getNamedContent().entrySet()) { - if (getContent().contains(objKV.getValue())) { - continue; - } else { - onlyNamed.put(objKV.getKey(), objKV.getValue()); - } - } + // for (Entry objKV : getNamedContent().entrySet()) { + // if (getContent().contains(objKV.getValue())) { + // continue; + // } else { + // onlyNamed.put(objKV.getKey(), objKV.getValue()); + // } + // } + + let only_named = &self.named_content; - if (onlyNamed.size() > 0) { - appendIndentation(sb, indentation); + if only_named.len() > 0 { + Container::append_indentation(sb, indentation); - sb.append("-- named: --\n"); + sb.push_str("-- named: --\n"); - for (Entry objKV : onlyNamed.entrySet()) { + for v in only_named.values() { // 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"); + v.build_string_of_hierarchy(sb, indentation, pointed_obj); + sb.push('\n'); } } - */ let indentation = indentation - 1; Container::append_indentation(sb, indentation); @@ -157,6 +150,58 @@ impl Container { } } + pub(crate) fn get_path(self: &Rc) -> Path { + Object::get_path(self.clone()) + } + + pub(crate) fn content_at_path( + self: &Rc, + path: &Path, + partial_path_start: usize, + mut partial_path_length: i32, + ) -> SearchResult { + + if partial_path_length == -1 { + partial_path_length = path.len() as i32; + } + + let mut approximate = false; + + let mut current_container = Some(self.clone()); + let mut current_obj:Rc = self.clone(); + + for i in partial_path_start..partial_path_length as usize { + let comp = path.get_component(i); + + // Path component was wrong type + if current_container.is_none() { + approximate = true; + break; + } + + let found_obj = current_container + .unwrap() + .content_with_path_component(comp.unwrap()); + + // Couldn't resolve entire path? + if found_obj.is_none() { + approximate = true; + break; + } + + current_obj = found_obj.unwrap().clone(); + current_container = if let Ok(container) = current_obj.clone().into_any().downcast::() { + Some(container) + } else { + None + }; + } + + + SearchResult::new(current_obj, approximate) + } + + pub(crate) fn get_count_flags(&self) -> i32 { let mut flags: i32 = 0; @@ -194,6 +239,30 @@ impl Container { (visits_should_be_counted, turn_index_should_be_counted, counting_at_start_only) } + + fn content_with_path_component(&self, component: &Component) -> Option> { + if component.is_index() { + if let Some(index) = component.index { + if (index >= 0) && (index < self.content.len()) { + return Some(self.content[index].clone()); + } + } + } else if component.is_parent() { + // When path is out of range, quietly return None + // (useful as we step/increment forwards through content) + return match self.get_object().get_parent() { + Some(o) => Some(o as Rc), + None => None, + }; + + //TODO + + // } else if let Some(found_content) = self.get_named_content().get(&component.get_name()) { + // return Some(found_content.clone().into()); + } + + None + } } diff --git a/src/divert.rs b/src/divert.rs new file mode 100644 index 0000000..ecbadf0 --- /dev/null +++ b/src/divert.rs @@ -0,0 +1,98 @@ +use std::{ + fmt, + rc::Rc, +}; + +use crate::{object::{Object, RTObject}, push_pop::PushPopType, pointer::{Pointer, self}, path::Path}; + + +pub(crate) struct Divert { + obj: Object, + pub external_args: i32, + pub is_conditional: bool, + pub is_external: bool, + pub pushes_to_stack: bool, + pub stack_push_type: PushPopType, + pub target_pointer: Pointer, + pub target_path: Option, + pub variable_divert_name: Option, +} + +impl Divert { + pub(crate) fn new(pushes_to_stack: bool, stack_push_type: PushPopType, is_external: bool, external_args: i32, is_conditional: bool, var_divert_name: Option, target_path: Option<&str>) -> Self { + Divert { + obj: Object::new(), + is_conditional, + pushes_to_stack, + stack_push_type, + is_external, + external_args, + target_pointer: pointer::NULL, + target_path: Self::target_path_string(target_path), + variable_divert_name: var_divert_name, + } + } + + fn target_path_string(value: Option<&str>) -> Option{ + if let Some(value) = value { + Some(Path::new_with_components_string(Some(value))) + } else { + None + } + } + + fn get_target_path_string(&self) -> Option { + if let Some(target_path) = &self.target_path { + // TODO Some(compact_path_string(target_path)) + None + } else { + None + } + } + + pub fn has_variable_target(&self) -> bool { + self.variable_divert_name.is_some() + } +} + +impl RTObject for Divert { + fn get_object(&self) -> &Object { + &self.obj + } +} + +impl fmt::Display for Divert { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut result = String::new(); + + if let Some(variable_diver_name) = &self.variable_divert_name { + result.push_str(&format!("Divert(variable: {})", variable_diver_name)); + } else if self.target_path.is_none() { + result.push_str("Divert(null)"); + } else { + let mut sb = String::new(); + let target_str = self.target_path.as_ref().unwrap().to_string(); + // if let Some(target_line_num) = debug_line_number_of_path(self.get_target_path().unwrap()) { + // sb.push_str(&format!("line {}", target_line_num)); + // } + + result.push_str("Divert"); + + if self.is_conditional { + result.push('?'); + } + + if self.pushes_to_stack { + if self.stack_push_type == PushPopType::Function { + result.push_str(" function"); + } else { + result.push_str(" tunnel"); + } + } + + result.push_str(&format!(" -> {} ({})", self.get_target_path_string().unwrap_or_default(), sb)); + } + + write!(f, "{result}") + } +} \ No newline at end of file diff --git a/src/json_serialization.rs b/src/json_serialization.rs index 2f73224..7dace89 100644 --- a/src/json_serialization.rs +++ b/src/json_serialization.rs @@ -1,11 +1,13 @@ use std::{collections::HashMap, rc::Rc, cell::RefCell}; +use serde_json::Map; + use crate::{ container::Container, - object::{self, RTObject}, control_command::{CommandType, ControlCommand}, value::Value, object_enum::ObjectEnum, glue::Glue, + object::{self, RTObject}, control_command::{CommandType, ControlCommand}, value::Value, object_enum::ObjectEnum, glue::Glue, path::Path, choice_point::ChoicePoint, choice::Choice, push_pop::PushPopType, divert::Divert, }; -pub fn jtoken_to_runtime_object(token: &serde_json::Value) -> Result, String> { +pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) -> Result, String> { match token { serde_json::Value::Null => Ok(Rc::new(object::Null::new())), serde_json::Value::Bool(value) => Ok(Rc::new(Value::new_bool(value.to_owned()))), @@ -17,7 +19,7 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value) -> Result { let str = value; @@ -52,40 +54,204 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value) -> Result Ok(jarray_to_container(value, name)?), + serde_json::Value::Object(obj) => { + // Divert target value to path + let prop_value = obj.get("^->"); + + if prop_value.is_some() { + return Ok(Rc::new(Value::new_divert_target(Path::new_with_components_string(prop_value.unwrap().as_str())))); + } + + // // VariablePointerValue + // prop_value = obj.get("^var"); + // if (prop_value.is_some()) { + // VariablePointerValue varPtr = new VariablePointerValue((String) prop_value); + + // prop_value = obj.get("ci"); + + // if (prop_value != null) varPtr.setContextIndex((Integer) prop_value); + + // return varPtr; + // } + + // // Divert + let mut isDivert = false; + let mut pushesToStack = false; + let mut divPushType = PushPopType::Function; + let mut external = false; + + let mut prop_value = obj.get("->"); + if prop_value.is_some() { + isDivert = true; + } else { + prop_value = obj.get("f()"); + if prop_value.is_some() { + isDivert = true; + pushesToStack = true; + divPushType = PushPopType::Function; + } else { + prop_value = obj.get("->t->"); + if prop_value.is_some() { + isDivert = true; + pushesToStack = true; + divPushType = PushPopType::Tunnel; + } else { + prop_value = obj.get("x()"); + if prop_value.is_some() { + isDivert = true; + external = true; + pushesToStack = false; + divPushType = PushPopType::Function; + } + } + } + } + + if isDivert { + let target = prop_value.unwrap().as_str().unwrap().to_string(); + + let mut var_divert_name: Option = None; + let mut target_path: Option = None; + + prop_value = obj.get("var"); + + if prop_value.is_some() { + var_divert_name = Some(target); + } else { + target_path = Some(target); + } + + prop_value = obj.get("c"); + let conditional = prop_value.is_some(); + let mut external_args = 0; + + if external { + prop_value = obj.get("exArgs"); + if prop_value.is_some() { + external_args = prop_value.unwrap().as_i64().unwrap() as i32; + } + } + + return Ok(Rc::new(Divert::new(pushesToStack, divPushType, external, external_args, conditional, var_divert_name, target_path.as_deref()))); + } + + // Choice + let prop_value = obj.get("*"); + if let Some(cp) = prop_value { + let mut flags = 0; + let path_string_on_choice = cp.as_str().unwrap(); + let prop_value = obj.get("flg"); + if let Some(f) = prop_value { + flags = f.as_u64().unwrap(); + } + + return Ok(Rc::new(ChoicePoint::new(flags as i32, path_string_on_choice))); + } + + // // Variable reference + // prop_value = obj.get("VAR?"); + // if (prop_value != null) { + // return new VariableReference(prop_value.toString()); + // } else { + // prop_value = obj.get("CNT?"); + // if (prop_value != null) { + // VariableReference readCountVarRef = new VariableReference(); + // readCountVarRef.setPathStringForCount(prop_value.toString()); + // return readCountVarRef; + // } + // } + // // Variable assignment + // boolean isVarAss = false; + // boolean isGlobalVar = false; + + // prop_value = obj.get("VAR="); + // if (prop_value != null) { + // isVarAss = true; + // isGlobalVar = true; + // } else { + // prop_value = obj.get("temp="); + // if (prop_value != null) { + // isVarAss = true; + // isGlobalVar = false; + // } + // } + // if (isVarAss) { + // String varName = prop_value.toString(); + // prop_value = obj.get("re"); + // boolean isNewDecl = prop_value == null; + + // VariableAssignment varAss = new VariableAssignment(varName, isNewDecl); + // varAss.setIsGlobal(isGlobalVar); + // return varAss; + // } + + // // Legacy Tag + // prop_value = obj.get("#"); + // if (prop_value != null) { + // return new Tag((String) prop_value); + // } + + // // List value + // prop_value = obj.get("list"); + + // if (prop_value != null) { + // HashMap listContent = (HashMap) prop_value; + // InkList rawList = new InkList(); + + // prop_value = obj.get("origins"); + + // if (prop_value != null) { + // List namesAsObjs = (List) prop_value; + + // 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").is_some() { + return jobject_to_choice(obj); + } + + return Err(format!("Failed to convert token to runtime RTObject: {}", &token.to_string())); }, - serde_json::Value::Array(value) => Ok(jarray_to_container(value)?), - serde_json::Value::Object(_) => todo!(), } + } -fn jarray_to_container(jarray: &Vec) -> Result, String> { +fn jarray_to_container(jarray: &Vec, name: Option) -> Result, String> { // Final object in the array is always a combination of // - named content // - a "#f" key with the countFlags // (if either exists at all, otherwise null) let terminating_obj = jarray[jarray.len() - 1].as_object(); - let mut name: Option = None; + let mut name: Option = name; let mut flags = 0; - if let Some(terminating_obj) = terminating_obj { - let named_only_content: HashMap> = - HashMap::with_capacity(terminating_obj.len()); + let mut named_only_content: HashMap> = + HashMap::new(); + if let Some(terminating_obj) = terminating_obj { for (k, v) in terminating_obj { match k.as_str() { "#f" => flags = v.as_i64().unwrap().try_into().unwrap(), "#n" => name = Some(v.as_str().unwrap().to_string()), - _ => { - let named_content_item = jtoken_to_runtime_object(v); - /* TODO - let namedSubContainer = named_content_item as Container; - if namedSubContainer { - namedSubContainer.name = k; - } + k => { + let named_content_item = jtoken_to_runtime_object(v, Some(k.to_string())).unwrap(); - namedOnlyContent[k] = namedContentItem; - */ + let named_sub_container = named_content_item.into_any().downcast::().unwrap(); + + named_only_content.insert(k.to_string(), named_sub_container); } } } @@ -93,7 +259,7 @@ fn jarray_to_container(jarray: &Vec) -> Result, // TODO container.namedOnlyContent = namedOnlyContent; } - let container = Container::new(name, flags, jarray_to_runtime_obj_list(jarray, true)?); + let container = Container::new(name, flags, jarray_to_runtime_obj_list(jarray, true)?, named_only_content); Ok(container) } @@ -108,7 +274,7 @@ fn jarray_to_runtime_obj_list(jarray: &Vec, skip_last: bool) for i in 0..count { let jtok = &jarray[i]; - let runtime_obj = jtoken_to_runtime_object(jtok); + let runtime_obj = jtoken_to_runtime_object(jtok, None); list.push(runtime_obj?); } @@ -145,7 +311,16 @@ fn create_control_command(name: &str) -> Option { "/#" => Some(ControlCommand::new(CommandType::EndTag)), _ => None, } +} + +fn jobject_to_choice(obj: &Map) -> Result, String> { + let text = obj.get("text").unwrap().as_str().unwrap(); + let index = obj.get("index").unwrap().as_u64().unwrap() as usize; + let source_path = obj.get("originalChoicePath").unwrap().as_str().unwrap(); + let original_thread_index = obj.get("originalThreadIndex").unwrap().as_i64().unwrap() as usize; + let path_string_on_choice = obj.get("targetPath").unwrap().as_str().unwrap(); + return Ok(Rc::new(Choice::new_from_json(path_string_on_choice, source_path.to_string(), text, index, original_thread_index))); } #[cfg(test)] diff --git a/src/lib.rs b/src/lib.rs index bea9dda..c8643bc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,5 +18,8 @@ mod error; mod glue; mod void; mod state_patch; +mod choice_point; +mod tag; +mod divert; diff --git a/src/object.rs b/src/object.rs index 474c6cc..44888a1 100644 --- a/src/object.rs +++ b/src/object.rs @@ -11,7 +11,7 @@ use crate::{ pub struct Object { parent: RefCell>, - path: RefCell>>, + path: RefCell>, //debug_metadata: DebugMetadata, } @@ -35,7 +35,7 @@ impl Object { self.parent.replace(Rc::downgrade(parent)); } - pub fn get_path(rtobject: Rc) -> Rc { + pub fn get_path(rtobject: Rc) -> Path { if let Some(p) = rtobject.get_object().path.borrow().as_ref() { return p.clone(); } @@ -73,10 +73,10 @@ impl Object { // Reverse list because components are searched in reverse order. comps.reverse(); - rtobject.get_object().path.replace(Some(Rc::new(Path::new(&comps, Path::default().is_relative())))); + rtobject.get_object().path.replace(Some(Path::new(&comps, Path::default().is_relative()))); }, None => { - rtobject.get_object().path.replace(Some(Rc::new(Path::new_with_defaults()))); + rtobject.get_object().path.replace(Some(Path::new_with_defaults())); }, } @@ -84,16 +84,79 @@ impl Object { } - pub fn resolve_path(&self) -> Result { - todo!() + pub fn resolve_path(rtobject: Rc, path: &Path) -> SearchResult { + if path.is_relative() { + let mut p = path.clone(); + let mut nearest_container = rtobject.clone().into_any().downcast::().ok(); + + if nearest_container.is_none() { + nearest_container = rtobject.get_object().get_parent(); + p = path.get_tail(); + }; + + return nearest_container.unwrap().content_at_path(&p, 0, -1); + + } else { + Object::get_root_container(rtobject).content_at_path(path, 0, -1) + } } - pub fn convert_path_to_relative(&self, global_path: Path) -> Path { - todo!() + pub fn convert_path_to_relative(rtobject: &Rc, global_path: &Path) -> Path { + // 1. Find last shared ancestor + // 2. Drill up using ".." style (actually represented as "^") + // 3. Re-build downward chain from common ancestor + let own_path = rtobject.get_object().path.borrow(); + let min_path_length = std::cmp::min(global_path.len(), own_path.as_ref().unwrap().len()); + let mut last_shared_path_comp_index:i32 = -1; + + for i in 0..min_path_length { + let own_comp = &own_path.as_ref().unwrap().get_component(i as usize); + let other_comp = &global_path.get_component(i); + + if own_comp == other_comp { + last_shared_path_comp_index = i as i32; + } else { + break; + } + } + + // No shared path components, so just use the global path + if last_shared_path_comp_index == -1 { + return global_path.clone(); + } + + let num_upwards_moves = (own_path.as_ref().unwrap().len() - 1) - last_shared_path_comp_index as usize; + let mut new_path_comps = Vec::new(); + + for _ in 0..num_upwards_moves { + new_path_comps.push(Component::to_parent()); + } + + for down in (last_shared_path_comp_index as usize + 1)..global_path.len() { + new_path_comps.push(global_path.get_component(down).unwrap().clone()); + } + + Path::new(&new_path_comps, true) } - pub fn compact_path_string(&self, other_path: Path) -> Path { - todo!() + pub fn compact_path_string(rtobject: Rc, other_path: Path) -> String { + let global_path_str: String; + let relative_path_str: String; + + if other_path.is_relative() { + relative_path_str = other_path.get_components_string(); + global_path_str = Object::get_path(rtobject.clone()).path_by_appending_path(&other_path).get_components_string(); + } else { + let relative_path = Object::convert_path_to_relative(&rtobject, &other_path); + relative_path_str = relative_path.get_components_string(); + global_path_str = other_path.get_components_string(); + } + + if relative_path_str.len() < global_path_str.len() { + relative_path_str + } else { + global_path_str + } } pub fn get_root_container(rtobject: Rc) -> Rc { @@ -152,14 +215,16 @@ impl fmt::Display for Null { #[cfg(test)] mod tests { + use std::collections::HashMap; + use super::*; #[test] fn get_path_test() { - let container1 = Container::new(None, 0, Vec::new()); - let container21 = Container::new(None, 0, Vec::new()); - let container2 = Container::new(None, 0, vec![container21.clone()]); - let root = Container::new(None, 0, vec![container1.clone(), container2.clone()]); + let container1 = Container::new(None, 0, Vec::new(), HashMap::new()); + let container21 = Container::new(None, 0, Vec::new(), HashMap::new()); + let container2 = Container::new(None, 0, vec![container21.clone()], HashMap::new()); + let root = Container::new(None, 0, vec![container1.clone(), container2.clone()], HashMap::new()); let mut sb = String::new(); diff --git a/src/path.rs b/src/path.rs index ec1ae60..f5a86fd 100644 --- a/src/path.rs +++ b/src/path.rs @@ -14,7 +14,7 @@ pub struct Path { } impl Path { - pub fn new(components: &[Component], relative: bool) -> Path { + pub(crate) fn new(components: &[Component], relative: bool) -> Path { let mut comp: Vec = Vec::new(); comp.extend_from_slice(components); Path { @@ -30,7 +30,7 @@ impl Path { } } - pub fn new_with_components_string(components_string: Option) -> Path { + pub fn new_with_components_string(components_string: Option<&str>) -> Path { let cs = components_string; let mut is_relative = false; @@ -42,7 +42,7 @@ impl Path { }; } - let mut cs = cs.unwrap(); + let mut cs = cs.unwrap().to_string(); // When components start with ".", it indicates a relative path, e.g. // .^.^.hello.5 @@ -74,7 +74,7 @@ impl Path { } } - pub fn get_component(&self, index: usize) -> Option<&Component> { + pub(crate) fn get_component(&self, index: usize) -> Option<&Component> { self.components.get(index) } @@ -82,7 +82,7 @@ impl Path { self.is_relative } - fn get_tail(&self) -> Path { + pub fn get_tail(&self) -> Path { if self.components.len() >= 2 { let tail_comps = &self.components[1..]; @@ -103,7 +103,7 @@ impl Path { } } - pub fn get_last_component(&self) -> Option<&Component> { + pub(crate) fn get_last_component(&self) -> Option<&Component> { if self.components.len() > 0 { return self.components.get(self.components.len() - 1); } @@ -111,10 +111,10 @@ impl Path { None } - pub fn path_by_appending_path(&self, path_to_append: Path) -> Path { + pub fn path_by_appending_path(&self, path_to_append: &Path) -> Path { let mut upward_moves = 0; - for component in path_to_append.components { + for component in path_to_append.components.iter() { if component.is_parent() { upward_moves += 1; } else { @@ -139,7 +139,7 @@ impl Path { } } - fn get_components_string(&self) -> String { + pub(crate) fn get_components_string(&self) -> String { let mut sb = String::new(); if self.components.len() > 0 { @@ -213,9 +213,9 @@ impl PartialEq for Path { } #[derive(Eq, Clone)] -pub struct Component { - index: Option, - name: Option, +pub(crate) struct Component { + pub index: Option, + pub name: Option, } impl Component { @@ -233,15 +233,15 @@ impl Component { } } - fn to_parent() -> Component { + pub(crate) fn to_parent() -> Component { Component::new(PARENT_ID) } - fn is_index(&self) -> bool { + pub(crate) fn is_index(&self) -> bool { self.index.is_some() } - fn is_parent(&self) -> bool { + pub(crate) fn is_parent(&self) -> bool { match &self.name { Some(name) => name.eq(PARENT_ID), None => false, diff --git a/src/pointer.rs b/src/pointer.rs index 1fd6bbd..079410c 100644 --- a/src/pointer.rs +++ b/src/pointer.rs @@ -36,7 +36,7 @@ impl Pointer { self.container.is_none() } - pub fn get_path(&self) -> Option> { + pub fn get_path(&self) -> Option { if self.is_null() { return None; } @@ -46,11 +46,11 @@ impl Pointer { if self.index >= 0 { let c = Component::new_i(self.index as usize); - return Some(Rc::new(Object::get_path(container.clone()) - .path_by_appending_component(c))); + return Some(container.get_path() + .path_by_appending_component(c)); } - Some(Object::get_path(container.clone())) + Some(container.get_path()) } pub(crate) fn start_of(container:Rc) -> Pointer { @@ -61,7 +61,7 @@ impl Pointer { impl fmt::Display for Pointer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.container { - Some(container) => write!(f, "Ink Pointer -> {} -- index {}", Object::get_path(container.clone()).to_string(), self.index), + Some(container) => write!(f, "Ink Pointer -> {} -- index {}", container.get_path().to_string(), self.index), None => write!(f, "Ink Pointer (null)"), } } diff --git a/src/search_result.rs b/src/search_result.rs index 2dc9666..29430eb 100644 --- a/src/search_result.rs +++ b/src/search_result.rs @@ -1,3 +1,41 @@ +use std::rc::Rc; + +use crate::{object::{RTObject, Object}, container::Container}; + + +#[derive(Clone)] pub struct SearchResult { + pub obj: Rc, + pub approximate: bool, +} + +impl SearchResult { + pub fn new(obj: Rc, approximate: bool) -> Self { + SearchResult { + obj, + approximate, + } + } + + pub fn from_search_result(sr: &SearchResult) -> Self { + SearchResult { + obj: sr.obj.clone(), + approximate: sr.approximate, + } + } + + pub fn correct_obj(&self) -> Option> { + if self.approximate { + None + } else { + Some(self.obj.clone()) + } + } + pub fn get_container(&self) -> Option> { + match self.obj.clone().into_any().downcast::() { + Ok(c) => Some(c), + Err(_) => None, + } + } } \ No newline at end of file diff --git a/src/story.rs b/src/story.rs index aa89aa8..45a7713 100644 --- a/src/story.rs +++ b/src/story.rs @@ -1,13 +1,13 @@ #![allow(unused_variables, dead_code)] -use std::{rc::Rc, time::Instant}; +use std::{rc::Rc, time::Instant, collections::VecDeque}; use crate::{ container::Container, error::ErrorType, json_serialization, push_pop::PushPopType, - story_state::StoryState, pointer::{Pointer, self}, object::RTObject, void::Void, path::Path, control_command::ControlCommand, choice::Choice, + story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, }; const INK_VERSION_CURRENT: i32 = 21; @@ -73,7 +73,7 @@ impl Story { // _listDefinitions = Json.JTokenToListDefinitions (listDefsObj); //} - let main_content_container = json_serialization::jtoken_to_runtime_object(root_token)?; + let main_content_container = json_serialization::jtoken_to_runtime_object(root_token, None)?; let main_content_container = main_content_container.into_any().downcast::(); @@ -527,7 +527,7 @@ impl Story { // 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) - let current_content_obj = pointer.resolve(); + let mut current_content_obj = pointer.resolve(); let is_logic_or_flow_control = self.perform_logic_and_flow_control(¤t_content_obj); // Has flow been forced to end by flow control above? @@ -540,18 +540,16 @@ impl Story { } // Choice with condition? - // TODO - // ChoicePoint choicePoint = currentContentObj instanceof ChoicePoint ? (ChoicePoint) currentContentObj : null; + if let Ok(choicePoint) = current_content_obj.clone().unwrap().into_any().downcast::() { - // if (choicePoint != null) { - // Choice choice = processChoice(choicePoint); - // if (choice != null) { - // state.getGeneratedChoices().add(choice); - // } + let choice = self.process_choice(&choicePoint); + if choice.is_some() { + self.state.as_mut().unwrap().get_generated_choices().push(choice.unwrap()); + } - // currentContentObj = null; - // should_add_to_stream = false; - // } + current_content_obj = None; + should_add_to_stream = false; + } // If the container has no content, then it will be // the "content" itself, but we skip over it. @@ -582,7 +580,7 @@ impl Story { // Expression evaluation content if self.state.as_ref().unwrap().get_in_expression_evaluation() { - self.state.as_mut().unwrap().push_evaluation_stack(current_content_obj); + self.state.as_mut().unwrap().push_evaluation_stack(current_content_obj.unwrap()); } // Output stream content (i.e. not expression evaluation) else { @@ -630,7 +628,7 @@ impl Story { // Invisible choice may have been generated on a different thread, // in which case we need to restore it before we continue - self.state.as_ref().unwrap().get_callstack().as_ref().borrow_mut().set_current_thread(choice.thread_at_generation.copy()); + self.state.as_ref().unwrap().get_callstack().as_ref().borrow_mut().set_current_thread(choice.thread_at_generation.borrow().as_ref().unwrap().copy()); // 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 @@ -723,18 +721,143 @@ impl Story { None => return false, }; + // Divert + if let Some(current_divert) = content_obj.as_ref().as_any().downcast_ref::() { + if current_divert.is_conditional { + let o = self.state.as_mut().unwrap().pop_evaluation_stack(); + if !self.is_truthy(o) { + return true; + } + } + + if current_divert.has_variable_target() { + // let var_name = current_divert.variable_divert_name; + // if let Some(var_contents) = self.state.as_ref().unwrap().get_variables_state().get_variable_with_name(var_name) { + // if let Some(target) = var_contents.downcast_ref::() { + // self.state.as_ref().unwrap().set_diverted_pointer(pointer_at_path(&target.get_target_path())); + // } else { + // let int_content = var_contents.downcast_ref::(); + // let error_message = format!( + // "Tried to divert to a target from a variable, but the variable ({}) didn't contain a divert target, it ", + // var_name + // ); + // let error_message = if let Some(int_content) = int_content { + // if int_content.value == 0 { + // format!("{}was empty/null (the value 0).", error_message) + // } else { + // format!("{}contained '{}'.", error_message, var_contents) + // } + // } else { + // error_message + // }; + + // error(error_message); + // } + // } else { + // error(format!( + // "Tried to divert using a target from a variable that could not be found ({})", + // var_name + // )); + // } + } else if current_divert.is_external { + //call_external_function(¤t_divert.get_target_path_string(), current_divert.get_external_args()); + return true; + } else { + self.state.as_mut().unwrap().set_diverted_pointer(current_divert.target_pointer.clone()); + } + + if current_divert.pushes_to_stack { + // self.state.as_ref().unwrap() + // .get_call_stack() + // .push(current_divert.stack_push_type, 0, state.get_output_stream().len()); + // + } + + if self.state.as_ref().unwrap().diverted_pointer.is_null() && !current_divert.is_external { + // if let Some(source_name) = ¤t_divert.get_debug_metadata().source_name { + // error(format!("Divert target doesn't exist: {}", source_name)); + // } else { + // error(format!("Divert resolution failed: {:?}", current_divert)); + // } + } + + return true; + } + if let Some(eval_command) = content_obj.as_ref().as_any().downcast_ref::() { match eval_command.command_type { crate::control_command::CommandType::NotSet => todo!(), - crate::control_command::CommandType::EvalStart => todo!(), + crate::control_command::CommandType::EvalStart => { + assert!(!self.state.as_ref().unwrap().get_in_expression_evaluation(), "Already in expression evaluation?"); + self.state.as_ref().unwrap().set_in_expression_evaluation(true); + }, crate::control_command::CommandType::EvalOutput => todo!(), - crate::control_command::CommandType::EvalEnd => todo!(), + crate::control_command::CommandType::EvalEnd => { + assert!(self.state.as_ref().unwrap().get_in_expression_evaluation(), "Not in expression evaluation mode"); + self.state.as_ref().unwrap().set_in_expression_evaluation(false); + }, crate::control_command::CommandType::Duplicate => todo!(), crate::control_command::CommandType::PopEvaluatedValue => todo!(), crate::control_command::CommandType::PopFunction => todo!(), crate::control_command::CommandType::PopTunnel => todo!(), - crate::control_command::CommandType::BeginString => todo!(), - crate::control_command::CommandType::EndString => todo!(), + crate::control_command::CommandType::BeginString => { + self.state.as_mut().unwrap().push_to_output_stream(Some(content_obj.clone())); + + assert!(self.state.as_ref().unwrap().get_in_expression_evaluation(), + "Expected to be in an expression when evaluating a string"); + self.state.as_ref().unwrap().set_in_expression_evaluation(false); + }, + crate::control_command::CommandType::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 + let mut content_stack_for_string: VecDeque> = VecDeque::new(); + let mut content_to_retain: VecDeque> = VecDeque::new(); + + let mut output_count_consumed = 0; + + for i in (0..self.state.as_ref().unwrap().get_output_stream().len()).rev() { + let obj = &self.state.as_ref().unwrap().get_output_stream()[i]; + output_count_consumed += 1; + + if let Some(command) = obj.as_ref().as_any().downcast_ref::() { + if command.command_type == CommandType::BeginString { + break; + } + } + + if let Some(tag) = obj.as_ref().as_any().downcast_ref::() { + content_to_retain.push_back(obj.clone()); + } + + if let Some(sv) = Value::get_string_value(obj.as_ref()) { + content_stack_for_string.push_back(obj.clone()); + } + } + + // Consume the content that was produced for this string + self.state.as_mut().unwrap().pop_from_output_stream(output_count_consumed); + + // 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 rescued_tag in content_to_retain.iter() { + self.state.as_mut().unwrap().push_to_output_stream(Some(rescued_tag.clone())); + } + + // Build string out of the content we collected + let mut sb = String::new(); + + while let Some(c) = content_stack_for_string.pop_back() { + sb.push_str(&c.to_string()); + } + + // Return to expression evaluation (from content mode) + self.state.as_ref().unwrap().set_in_expression_evaluation(true); + self.state.as_mut().unwrap().push_evaluation_stack(Rc::new(Value::new_string(&sb))); + }, crate::control_command::CommandType::NoOp => todo!(), crate::control_command::CommandType::ChoiceCount => todo!(), crate::control_command::CommandType::Turns => todo!(), @@ -820,7 +943,7 @@ impl Story { // so in this case, we make sure that the evaluator has // something to chomp on if it needs it if self.state.as_ref().unwrap().get_in_expression_evaluation() { - self.state.as_mut().unwrap().push_evaluation_stack(Some(Void::new())); + self.state.as_mut().unwrap().push_evaluation_stack(Void::new()); } didPop = true; @@ -887,8 +1010,20 @@ impl Story { todo!() } - pub fn get_current_choices(&self) -> Vec { - todo!() + pub fn get_current_choices(&self) -> Vec> { + // Don't include invisible choices for external usage. + let mut choices = Vec::new(); + + if let Some(current_choices) = self.state.as_ref().unwrap().get_current_choices() { + for c in current_choices { + if !c.is_invisible_default { + c.index.replace(choices.len()); + choices.push(c.clone()); + } + } + } + + choices } pub fn has_error(&self) -> bool { @@ -902,5 +1037,85 @@ impl Story { pub fn choose_choice_index(&self, choice_list_index: usize) { todo!() } + + fn is_truthy(&self, obj: Rc) -> bool { + let truthy = false; + + if let Ok(val) = obj.into_any().downcast::() { + // TODO + + // 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.is_truthy(); + } + + return truthy; + } + + fn process_choice(&mut self, choice_point: &Rc) -> Option> { + let mut showChoice = true; + + // Don't create choice if choice point doesn't pass conditional + if choice_point.has_condition() { + let condition_value = self.state.as_mut().unwrap().pop_evaluation_stack(); + if !self.is_truthy(condition_value) { + showChoice = false; + } + } + + let mut start_text = String::new(); + let mut choice_only_text = String::new(); + let mut tags: Vec = Vec::with_capacity(0); + + if choice_point.has_choice_only_content() { + choice_only_text = self.pop_choice_string_and_tags(&mut tags); + } + + if choice_point.has_start_content() { + start_text = self.pop_choice_string_and_tags(&mut tags); + } + + // Don't create choice if player has already read this content + if choice_point.once_only() { + //TODO + // let 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 None; + } + + start_text.push_str(&choice_only_text); + + let choice = Rc::new(Choice::new(choice_point.get_path_on_choice(), Object::get_path(choice_point.clone()).to_string(), choice_point.is_invisible_default(), tags, self.state.as_ref().unwrap().get_callstack().borrow_mut().fork_thread(), start_text.trim().to_string(), 0, 0)); + + Some(choice) + } + + fn pop_choice_string_and_tags(&mut self, tags: &[String]) -> String { + let obj = self.state.as_mut().unwrap().pop_evaluation_stack(); + let choiceOnlyStrVal = Value::get_string_value(obj.as_ref()).unwrap(); + + // TODO + + // while (self.state.as_ref().unwrap().evaluation_stack.len() > 0 && self.state.as_ref().unwrap().peek_evaluation_stack() instanceof Tag) { + // Tag tag = (Tag) state.popEvaluationStack(); + // tags.add(0, tag.getText()); // popped in reverse order + // } + + return choiceOnlyStrVal.string.to_string(); + } } diff --git a/src/story_state.rs b/src/story_state.rs index cedd0e9..9b4ff54 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -18,14 +18,14 @@ pub(crate) struct StoryState { output_stream_tags_dirty: bool, variables_state: VariablesState, alive_flow_names_dirty: bool, - evaluation_stack: Vec>, + pub evaluation_stack: Vec>, main_content_container: Rc, current_errors: Vec, current_warnings: Vec, current_text: Option, patch: Option, named_flows: Option>, - diverted_pointer: Pointer, + pub diverted_pointer: Pointer, visit_counts: HashMap, turn_indices: HashMap, current_turn_index: i32, @@ -106,8 +106,8 @@ impl StoryState { &mut self.variables_state } - pub(crate) fn get_generated_choices(&self) -> &Vec> { - &self.current_flow.current_choices + pub(crate) fn get_generated_choices(&self) -> Vec> { + self.current_flow.current_choices.clone() } pub(crate) fn is_did_safe_exit(&self) -> bool { @@ -126,7 +126,7 @@ impl StoryState { &self.current_warnings } - fn get_output_stream(&self) -> &Vec> { + pub(crate) fn get_output_stream(&self) -> &Vec> { &self.current_flow.output_stream } @@ -307,8 +307,40 @@ impl StoryState { self.get_callstack().borrow().get_current_element().in_expression_evaluation } - pub(crate) fn push_evaluation_stack(&self, content_obj: Option>) { - todo!() + pub(crate) fn set_in_expression_evaluation(&self, value: bool) { + self.get_callstack().borrow_mut().get_current_element_mut().in_expression_evaluation = value; + } + + pub(crate) fn push_evaluation_stack(&mut self, obj: Rc) { + + // TODO + + // let list_value = if let RTObject::ListValue(list_val) = &obj { + // Some(list_val) + // } else { + // None + // }; + + // if let Some(list_val) = list_value { + // if let InkList { + // origin_names: Some(origin_names), + // origins: Some(origins), + // .. + // } = &mut list_val.value + // { + // origins.clear(); + + // for name in origin_names.iter() { + // if let Some(def) = self.story.list_definitions.get_list_definition(name) { + // if !origins.contains(def) { + // origins.push(def.clone()); + // } + // } + // } + // } + // } + + self.evaluation_stack.push(obj); } pub(crate) fn push_to_output_stream(&mut self, obj: Option>) { @@ -711,6 +743,27 @@ impl StoryState { fn apply_count_changes(&self, clone: String, count: usize, arg: bool) { todo!() } - + + pub(crate) fn pop_from_output_stream(&mut self, count: usize) { + let len = self.get_output_stream().len(); + + if count <= len { + let start = len - count; + self.get_output_stream_mut().drain(start..len); + } + + self.output_stream_dirty(); + } + + pub(crate) fn pop_evaluation_stack(&mut self) -> Rc { + let obj = self.evaluation_stack.last().unwrap().clone(); + self.evaluation_stack.remove(self.evaluation_stack.len() - 1); + + obj + } + + pub(crate) fn set_diverted_pointer(&mut self, p: Pointer) { + self.diverted_pointer = p; + } } \ No newline at end of file diff --git a/src/tag.rs b/src/tag.rs new file mode 100644 index 0000000..60d7516 --- /dev/null +++ b/src/tag.rs @@ -0,0 +1,31 @@ +use std::fmt; + +use crate::object::{Object, RTObject}; + +pub struct Tag { + obj: Object, + text: String, +} + +impl Tag { + pub fn new(text: &str) -> Self { + Tag {obj: Object::new(), text: text.to_string()} + } + + pub(crate) fn get_text(&self) -> String { + self.text.clone() + } +} + +impl RTObject for Tag { + fn get_object(&self) -> &Object { + &self.obj + } +} + +impl fmt::Display for Tag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let t = &self.text; + write!(f, "# {t}") + } +} \ No newline at end of file diff --git a/src/value.rs b/src/value.rs index 50c67f6..967ecab 100644 --- a/src/value.rs +++ b/src/value.rs @@ -2,7 +2,7 @@ use std::{fmt}; -use crate::{object::{RTObject, Object}}; +use crate::{object::{RTObject, Object}, path::Path}; #[repr(i8)] pub enum ValueType { @@ -13,7 +13,7 @@ pub enum ValueType { String(StringValue), // Not used for coersion described above - //DivertTarget, + DivertTarget(Path), //VariablePointer, } @@ -50,6 +50,7 @@ impl fmt::Display for Value { ValueType::Int(v) => write!(f, "{}", v), ValueType::Float(v) => write!(f, "{}", v), ValueType::String(v) => write!(f, "{}", v.string), + ValueType::DivertTarget(p) => write!(f, "DivertTargetValue({})", p.to_string()), } } } @@ -87,12 +88,17 @@ impl Value { } } + pub fn new_divert_target(p:Path) -> Value { + Value { obj: Object::new(), value: ValueType::DivertTarget(p) } + } + pub fn is_truthy(&self) -> bool { match &self.value { ValueType::Bool(v) => *v, ValueType::Int(v) => *v != 0, ValueType::Float(v) => *v != 0.0, ValueType::String(v) => v.string.len() > 0, + ValueType::DivertTarget(p) => false, // exception Shouldn't be checking the truthiness of a divert target?? } } diff --git a/src/void.rs b/src/void.rs index bbdb4c2..4fde905 100644 --- a/src/void.rs +++ b/src/void.rs @@ -10,7 +10,7 @@ pub struct Void { } impl Void { - pub fn new() -> Rc { + pub fn new() -> Rc { Rc::new(Void {obj: Object::new()}) } } From 0a06b50f981eff01f0721890f78627c7568adf84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Tue, 19 Sep 2023 16:57:27 +0000 Subject: [PATCH 17/91] First choice test works! --- src/callstack.rs | 2 +- src/choice.rs | 13 ++- src/container.rs | 18 ++-- src/control_command.rs | 4 +- src/divert.rs | 2 +- src/glue.rs | 5 +- src/json_serialization.rs | 34 +++---- src/object.rs | 3 +- src/object_enum.rs | 4 - src/search_result.rs | 2 +- src/state_patch.rs | 12 ++- src/story.rs | 180 +++++++++++++++++++++++++++++++++----- src/story_state.rs | 86 +++++++++++++++--- src/value.rs | 2 +- src/variables_state.rs | 6 +- tests/glue_test.rs | 1 - 16 files changed, 295 insertions(+), 79 deletions(-) diff --git a/src/callstack.rs b/src/callstack.rs index 52a6857..f42dedd 100644 --- a/src/callstack.rs +++ b/src/callstack.rs @@ -43,7 +43,7 @@ impl Thread { fn new() -> Thread { Thread { callstack: Vec::new(), - previous_pointer: pointer::NULL, + previous_pointer: pointer::NULL.clone(), thread_index: 0, } } diff --git a/src/choice.rs b/src/choice.rs index a58de4e..6ce3b4f 100644 --- a/src/choice.rs +++ b/src/choice.rs @@ -11,7 +11,7 @@ pub struct Choice { pub index: RefCell, pub original_thread_index: usize, pub text: String, - pub(crate) thread_at_generation: RefCell>, + thread_at_generation: RefCell>, pub source_path: String } @@ -44,6 +44,17 @@ impl Choice { source_path: source_path, } } + + pub(crate) fn set_thread_at_generation(&mut self, thread: Thread) { + self.thread_at_generation.replace(Some(thread)); + } + + pub(crate) fn get_thread_at_generation(&self) -> Option { + match self.thread_at_generation.borrow().as_ref() { + Some(t) => Some(t.copy()), + None => None, + } + } } impl RTObject for Choice { diff --git a/src/container.rs b/src/container.rs index 20bc6d1..5e8ca22 100644 --- a/src/container.rs +++ b/src/container.rs @@ -6,8 +6,8 @@ use std::{ use as_any::Downcast; use crate::{ - object::{Object, RTObject, Null}, - value::{ValueType, Value}, control_command::ControlCommand, path::{Path, Component}, search_result::SearchResult, + object::{Object, RTObject}, + value::{ValueType, Value}, path::{Path, Component}, search_result::SearchResult, }; const COUNTFLAGS_VISITS: i32 = 1; @@ -191,6 +191,9 @@ impl Container { current_obj = found_obj.unwrap().clone(); current_container = if let Ok(container) = current_obj.clone().into_any().downcast::() { + let mut sb = String::new(); + container.build_string_of_hierarchy(&mut sb, 0, None); + println!("CONTAINER NAME: {}", sb); Some(container) } else { None @@ -243,7 +246,7 @@ impl Container { fn content_with_path_component(&self, component: &Component) -> Option> { if component.is_index() { if let Some(index) = component.index { - if (index >= 0) && (index < self.content.len()) { + if index < self.content.len() { return Some(self.content[index].clone()); } } @@ -253,12 +256,9 @@ impl Container { return match self.get_object().get_parent() { Some(o) => Some(o as Rc), None => None, - }; - - //TODO - - // } else if let Some(found_content) = self.get_named_content().get(&component.get_name()) { - // return Some(found_content.clone().into()); + } + } else if let Some(found_content) = self.named_content.get(component.name.as_ref().unwrap()) { + return Some(found_content.clone()); } None diff --git a/src/control_command.rs b/src/control_command.rs index a9a3b77..ccd5145 100644 --- a/src/control_command.rs +++ b/src/control_command.rs @@ -1,8 +1,8 @@ -use std::{any::Any, fmt, rc::Rc}; +use std::fmt; use strum::Display; -use crate::{object::{RTObject, Object}, container::Container}; +use crate::object::{RTObject, Object}; #[derive(Display)] #[derive(PartialEq)] diff --git a/src/divert.rs b/src/divert.rs index ecbadf0..dc5a8aa 100644 --- a/src/divert.rs +++ b/src/divert.rs @@ -27,7 +27,7 @@ impl Divert { stack_push_type, is_external, external_args, - target_pointer: pointer::NULL, + target_pointer: pointer::NULL.clone(), target_path: Self::target_path_string(target_path), variable_divert_name: var_divert_name, } diff --git a/src/glue.rs b/src/glue.rs index b171606..0d561a2 100644 --- a/src/glue.rs +++ b/src/glue.rs @@ -1,7 +1,4 @@ -use std::{ - fmt, - rc::Rc, -}; +use std::fmt; use crate::object::{Object, RTObject}; diff --git a/src/json_serialization.rs b/src/json_serialization.rs index 7dace89..ca00c5b 100644 --- a/src/json_serialization.rs +++ b/src/json_serialization.rs @@ -1,10 +1,10 @@ -use std::{collections::HashMap, rc::Rc, cell::RefCell}; +use std::{collections::HashMap, rc::Rc}; use serde_json::Map; use crate::{ container::Container, - object::{self, RTObject}, control_command::{CommandType, ControlCommand}, value::Value, object_enum::ObjectEnum, glue::Glue, path::Path, choice_point::ChoicePoint, choice::Choice, push_pop::PushPopType, divert::Divert, + object::{self, RTObject}, control_command::{CommandType, ControlCommand}, value::Value, glue::Glue, path::Path, choice_point::ChoicePoint, choice::Choice, push_pop::PushPopType, divert::Divert, }; pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) -> Result, String> { @@ -78,39 +78,39 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) // } // // Divert - let mut isDivert = false; - let mut pushesToStack = false; - let mut divPushType = PushPopType::Function; + let mut is_divert = false; + let mut pushes_to_stack = false; + let mut div_push_type = PushPopType::Function; let mut external = false; let mut prop_value = obj.get("->"); if prop_value.is_some() { - isDivert = true; + is_divert = true; } else { prop_value = obj.get("f()"); if prop_value.is_some() { - isDivert = true; - pushesToStack = true; - divPushType = PushPopType::Function; + is_divert = true; + pushes_to_stack = true; + div_push_type = PushPopType::Function; } else { prop_value = obj.get("->t->"); if prop_value.is_some() { - isDivert = true; - pushesToStack = true; - divPushType = PushPopType::Tunnel; + is_divert = true; + pushes_to_stack = true; + div_push_type = PushPopType::Tunnel; } else { prop_value = obj.get("x()"); if prop_value.is_some() { - isDivert = true; + is_divert = true; external = true; - pushesToStack = false; - divPushType = PushPopType::Function; + pushes_to_stack = false; + div_push_type = PushPopType::Function; } } } } - if isDivert { + if is_divert { let target = prop_value.unwrap().as_str().unwrap().to_string(); let mut var_divert_name: Option = None; @@ -135,7 +135,7 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) } } - return Ok(Rc::new(Divert::new(pushesToStack, divPushType, external, external_args, conditional, var_divert_name, target_path.as_deref()))); + return Ok(Rc::new(Divert::new(pushes_to_stack, div_push_type, external, external_args, conditional, var_divert_name, target_path.as_deref()))); } // Choice diff --git a/src/object.rs b/src/object.rs index 44888a1..bb99bad 100644 --- a/src/object.rs +++ b/src/object.rs @@ -1,5 +1,5 @@ use core::fmt; -use std::{fmt::Display, rc::{Weak, Rc}, cell::RefCell, any::Any, borrow::BorrowMut}; +use std::{fmt::Display, rc::{Weak, Rc}, cell::RefCell, any::Any}; use as_any::{AsAny, Downcast}; @@ -83,7 +83,6 @@ impl Object { rtobject.get_object().path.borrow().as_ref().unwrap().clone() } - pub fn resolve_path(rtobject: Rc, path: &Path) -> SearchResult { if path.is_relative() { let mut p = path.clone(); diff --git a/src/object_enum.rs b/src/object_enum.rs index 2e97e05..56eb35f 100644 --- a/src/object_enum.rs +++ b/src/object_enum.rs @@ -1,7 +1,3 @@ -use std::{rc::Rc, cell::RefCell}; - -use crate::{ object::{Null, Object, RTObject}, value::Value, control_command::ControlCommand, container::Container}; - #[derive(Clone)] pub enum ObjectEnum { Value, diff --git a/src/search_result.rs b/src/search_result.rs index 29430eb..b036144 100644 --- a/src/search_result.rs +++ b/src/search_result.rs @@ -1,6 +1,6 @@ use std::rc::Rc; -use crate::{object::{RTObject, Object}, container::Container}; +use crate::{object::RTObject, container::Container}; #[derive(Clone)] diff --git a/src/state_patch.rs b/src/state_patch.rs index f06afa3..3b671ef 100644 --- a/src/state_patch.rs +++ b/src/state_patch.rs @@ -2,7 +2,7 @@ use std::{ rc::Rc, collections::{HashMap, HashSet}, }; -use crate::object::RTObject; +use crate::{object::{RTObject, Object}, container::Container}; #[derive(Clone)] pub(crate) struct StatePatch { @@ -29,4 +29,14 @@ impl StatePatch { }, } } + + pub(crate) fn get_visit_count(&self, container: &Rc) -> Option { + let key = Object::get_path(container.clone()).to_string(); + self.visit_counts.get(&key).copied() + } + + pub(crate) fn set_visit_count(&mut self, container: &Rc, count: usize) { + let key = Object::get_path(container.clone()).to_string(); + self.visit_counts.insert(key, count); + } } \ No newline at end of file diff --git a/src/story.rs b/src/story.rs index 45a7713..4a36ffa 100644 --- a/src/story.rs +++ b/src/story.rs @@ -7,7 +7,7 @@ use crate::{ error::ErrorType, json_serialization, push_pop::PushPopType, - story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, + story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, search_result::SearchResult, }; const INK_VERSION_CURRENT: i32 = 21; @@ -30,6 +30,7 @@ pub struct Story { saw_lookahead_unsafe_function_after_new_line: bool, state_snapshot_at_last_new_line: Option, on_error: Option, + prev_containers: Vec>, } impl Story { @@ -91,6 +92,7 @@ impl Story { saw_lookahead_unsafe_function_after_new_line: false, state_snapshot_at_last_new_line: None, on_error: None, + prev_containers: Vec::new(), }; story.reset_state(); @@ -168,7 +170,7 @@ impl Story { // - Starting async run-through if !self.async_continue_active { self.async_continue_active = is_async_time_limited; - if (!self.can_continue()) { + if !self.can_continue() { return Err( "Can't continue - should check canContinue before calling Continue".to_string(), ); @@ -540,11 +542,11 @@ impl Story { } // Choice with condition? - if let Ok(choicePoint) = current_content_obj.clone().unwrap().into_any().downcast::() { + if let Ok(choice_point) = current_content_obj.clone().unwrap().into_any().downcast::() { - let choice = self.process_choice(&choicePoint); + let choice = self.process_choice(&choice_point); if choice.is_some() { - self.state.as_mut().unwrap().get_generated_choices().push(choice.unwrap()); + self.state.as_mut().unwrap().get_generated_choices_mut().push(choice.unwrap()); } current_content_obj = None; @@ -604,7 +606,7 @@ impl Story { // } } - fn try_follow_default_invisible_choice(&self) { + fn try_follow_default_invisible_choice(&mut self) { let all_choices = match self.state.as_ref().unwrap().get_current_choices() { Some(c) => c, None => return, @@ -628,7 +630,7 @@ impl Story { // Invisible choice may have been generated on a different thread, // in which case we need to restore it before we continue - self.state.as_ref().unwrap().get_callstack().as_ref().borrow_mut().set_current_thread(choice.thread_at_generation.borrow().as_ref().unwrap().copy()); + self.state.as_ref().unwrap().get_callstack().as_ref().borrow_mut().set_current_thread(choice.get_thread_at_generation().unwrap().copy()); // 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 @@ -701,7 +703,7 @@ impl Story { self.state_snapshot_at_last_new_line = None; } - fn visit_container(&mut self, container: &Container, at_start: bool) { + fn visit_container(&mut self, container: &Rc, at_start: bool) { if !container.counting_at_start_only || at_start { if container.visits_should_be_counted { self.state.as_mut().unwrap().increment_visit_count_for_container(container); @@ -881,7 +883,7 @@ impl Story { self.state.as_mut().unwrap().set_did_safe_exit(true); // Stop flow in current thread - self.state.as_ref().unwrap().set_current_pointer(pointer::NULL); + self.state.as_ref().unwrap().set_current_pointer(pointer::NULL.clone()); } }, crate::control_command::CommandType::End => todo!(), @@ -891,6 +893,7 @@ impl Story { crate::control_command::CommandType::BeginTag => todo!(), crate::control_command::CommandType::EndTag => todo!(), } + return true; } false @@ -947,7 +950,7 @@ impl Story { } didPop = true; - } else if (self.state.as_ref().unwrap().get_callstack().as_ref().borrow().can_pop_thread()) { + } else if self.state.as_ref().unwrap().get_callstack().as_ref().borrow().can_pop_thread() { self.state.as_ref().unwrap().get_callstack().as_ref().borrow_mut().pop_thread(); didPop = true; @@ -1006,10 +1009,6 @@ impl Story { return successful_increment; } - fn choose_path(&self, target_path: &Path, arg: bool) { - todo!() - } - pub fn get_current_choices(&self) -> Vec> { // Don't include invisible choices for external usage. let mut choices = Vec::new(); @@ -1034,8 +1033,26 @@ impl Story { self.state.as_ref().unwrap().get_current_errors() } - pub fn choose_choice_index(&self, choice_list_index: usize) { - todo!() + pub fn choose_choice_index(&mut self, choice_index: usize) { + let choices = self.get_current_choices(); + //assert!(choice_index < choices.len(), "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. + let choice_to_choose = choices.get(choice_index).unwrap(); + self.state.as_ref().unwrap().get_callstack().borrow_mut().set_current_thread(choice_to_choose.get_thread_at_generation().unwrap()); + + self.choose_path(&choice_to_choose.target_path, true); + } + + fn choose_path(&mut self, p: &Path, incrementing_turn_index: bool) { + self.state.as_mut().unwrap().set_chosen_path( &p, incrementing_turn_index); + + // Take a note of newly visited containers for read counts etc + self.visit_changed_containers_due_to_divert(); } fn is_truthy(&self, obj: Rc) -> bool { @@ -1059,13 +1076,13 @@ impl Story { } fn process_choice(&mut self, choice_point: &Rc) -> Option> { - let mut showChoice = true; + let mut show_choice = true; // Don't create choice if choice point doesn't pass conditional if choice_point.has_condition() { let condition_value = self.state.as_mut().unwrap().pop_evaluation_stack(); if !self.is_truthy(condition_value) { - showChoice = false; + show_choice = false; } } @@ -1093,7 +1110,7 @@ impl Story { // 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 { + if !show_choice { return None; } @@ -1106,7 +1123,7 @@ impl Story { fn pop_choice_string_and_tags(&mut self, tags: &[String]) -> String { let obj = self.state.as_mut().unwrap().pop_evaluation_stack(); - let choiceOnlyStrVal = Value::get_string_value(obj.as_ref()).unwrap(); + let choice_only_str_val = Value::get_string_value(obj.as_ref()).unwrap(); // TODO @@ -1115,7 +1132,128 @@ impl Story { // tags.add(0, tag.getText()); // popped in reverse order // } - return choiceOnlyStrVal.string.to_string(); + return choice_only_str_val.string.to_string(); + } + + pub(crate) fn pointer_at_path(main_content_container: &Rc, path: &Path) -> Pointer { + if path.len() == 0 { + return pointer::NULL.clone(); + } + + let mut p = Pointer::default(); + let mut path_length_to_use = path.len() as i32; + + + let result: SearchResult = + if path.get_last_component().unwrap().is_index() { + path_length_to_use -= 1; + let result = SearchResult::from_search_result(&main_content_container.content_at_path(path, 0, path_length_to_use)); + p.container = result.get_container(); + p.index = path.get_last_component().unwrap().index.unwrap() as i32; + + result + } else { + let result = SearchResult::from_search_result(&main_content_container.content_at_path(path, 0, -1)); + p.container = result.get_container(); + p.index = -1; + + result + }; + + let main_container: Rc = main_content_container.clone(); + + if Rc::ptr_eq(&result.obj, &main_container) && path_length_to_use > 0 { + // self.error(format!( + // "Failed to find content at path '{}', and no approximation of it was possible.", + // path + // )); + } else if result.approximate { + // warning(format!( + // "Failed to find content at path '{}', so it was approximated to: '{}'.", + // path, + // result.obj.unwrap().get_path() + // )); + } + + p } + + fn visit_changed_containers_due_to_divert(&mut self) { + let previous_pointer = self.state.as_ref().unwrap().get_previous_pointer(); + let pointer = self.state.as_ref().unwrap().get_current_pointer(); + + // 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.is_null() || pointer.index == -1 { + return; + } + + // First, find the previously open set of containers + self.prev_containers.clear(); + + if !previous_pointer.is_null() { + let mut prev_ancestor = None; + + let resolved = previous_pointer.resolve(); + if resolved.is_some() && resolved.as_ref().unwrap().as_any().is::() { + prev_ancestor = resolved.unwrap().into_any().downcast::().ok(); + } else if previous_pointer.container.is_some() { + prev_ancestor = previous_pointer.container.clone(); + } + + while let Some(prev_anc) = prev_ancestor { + self.prev_containers.push(prev_anc.clone()); + prev_ancestor = prev_anc.get_object().get_parent(); + } + } + + // 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 + let current_child_of_container = pointer.resolve(); + + if current_child_of_container.is_none() { + return; + } + + let mut current_child_of_container = current_child_of_container.unwrap(); + + let mut current_container_ancestor = current_child_of_container + .get_object().get_parent(); + + let mut all_children_entered_at_start = true; + + while let Some(current_container) = current_container_ancestor { + if !self.prev_containers.iter().any(|e| Rc::ptr_eq(e, ¤t_container)) + || current_container.counting_at_start_only + { + // Check whether this ancestor container is being entered at the start, + // by checking whether the child Object is the first. + let entering_at_start = current_container + .content + .first() + .map(|first_child| Rc::ptr_eq(first_child, ¤t_child_of_container) && all_children_entered_at_start) + .unwrap_or(false); + + // Don't count it as entering at start if we're entering randomly 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 !entering_at_start { + all_children_entered_at_start = false; + } + + // Mark a visit to this container + self.visit_container(¤t_container, entering_at_start); + + current_child_of_container = current_container.clone(); + current_container_ancestor = current_container.get_object().get_parent(); + } else { + break; + } + } + } + + } diff --git a/src/story_state.rs b/src/story_state.rs index 9b4ff54..6d7a614 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -2,7 +2,7 @@ use std::{rc::Rc, cell::RefCell, collections::HashMap}; -use crate::{pointer::{Pointer, self}, callstack::CallStack, flow::Flow, variables_state::VariablesState, choice::Choice, object::RTObject, value::{Value, ValueType}, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}, container::Container, state_patch::StatePatch}; +use crate::{pointer::{Pointer, self}, callstack::CallStack, flow::Flow, variables_state::VariablesState, choice::Choice, object::RTObject, value::{Value, ValueType}, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}, container::Container, state_patch::StatePatch, story::Story, path::Path}; use rand::Rng; @@ -106,8 +106,12 @@ impl StoryState { &mut self.variables_state } - pub(crate) fn get_generated_choices(&self) -> Vec> { - self.current_flow.current_choices.clone() + pub(crate) fn get_generated_choices_mut(&mut self) -> &mut Vec> { + &mut self.current_flow.current_choices + } + + pub(crate) fn get_generated_choices(&self) -> &Vec> { + &self.current_flow.current_choices } pub(crate) fn is_did_safe_exit(&self) -> bool { @@ -155,8 +159,8 @@ impl StoryState { let mut sb = String::new(); let mut in_tag = false; - for outputObj in self.get_output_stream() { - let text_content = match outputObj.as_ref().as_any().downcast_ref::() { + for output_obj in self.get_output_stream() { + let text_content = match output_obj.as_ref().as_any().downcast_ref::() { Some(v) => match &v.value { ValueType::String(s) => Some(s), _ => None, @@ -167,10 +171,10 @@ impl StoryState { if !in_tag && text_content.is_some() { sb.push_str(&text_content.unwrap().string); } else { - if let Some(controlCommand) = outputObj.as_ref().as_any().downcast_ref::() { - if controlCommand.command_type == CommandType::BeginTag { + if let Some(control_command) = output_obj.as_ref().as_any().downcast_ref::() { + if control_command.command_type == CommandType::BeginTag { in_tag = true; - } else if controlCommand.command_type == CommandType::EndTag { + } else if control_command.command_type == CommandType::EndTag { in_tag = false; } } @@ -340,6 +344,7 @@ impl StoryState { // } // } + println!("PUSH: {}", obj.to_string()); self.evaluation_stack.push(obj); } @@ -373,8 +378,51 @@ impl StoryState { self.push_to_output_stream_individual(obj.unwrap()); } - pub(crate) fn increment_visit_count_for_container(&self, container: &crate::container::Container) { - todo!() + pub(crate) fn increment_visit_count_for_container(&mut self, container: &Rc) { + let has_patch = self.patch.is_some(); + + if has_patch { + let curr_count = self.visit_count_for_container(container); + let new_count = curr_count + 1; + self.patch.as_mut().unwrap().set_visit_count(container, new_count); + } else { + let mut count = 0; + let container_path_str = container.get_path().to_string(); + + if let Some(&existing_count) = self.visit_counts.get(&container_path_str) { + count = existing_count; + } + + count += 1; + self.visit_counts.insert(container_path_str, count); + } + } + + fn visit_count_for_container(&mut self, container: &Rc) -> usize { + if !container.visits_should_be_counted { + // TODO + + // story.error(format!( + // "Read count for target ({:?} - on {:?}) unknown.", + // container.get_name(), + // container.get_debug_metadata() + // )); + return 0; + } + + if let Some(patch) = &self.patch { + if let Some(visit_count) = patch.get_visit_count(container) { + return visit_count; + } + } + + let container_path_str = container.get_path().to_string(); + + if let Some(&count) = self.visit_counts.get(&container_path_str) { + return count; + } + + 0 } pub(crate) fn record_turn_index_visit_to_container(&self, container: &crate::container::Container) { @@ -759,6 +807,8 @@ impl StoryState { let obj = self.evaluation_stack.last().unwrap().clone(); self.evaluation_stack.remove(self.evaluation_stack.len() - 1); + println!("POP: {}", obj.to_string()); + obj } @@ -766,4 +816,20 @@ impl StoryState { self.diverted_pointer = p; } + pub(crate) fn set_chosen_path(&mut self, path: &Path, incrementing_turn_index: bool) { + // Changing direction, assume we need to clear current set of choices + self.current_flow.current_choices.clear(); + + let mut new_pointer = Story::pointer_at_path(&self.main_content_container, &path); + if !new_pointer.is_null() && new_pointer.index == -1 { + new_pointer.index = 0; + } + + self.set_current_pointer(new_pointer); + + if incrementing_turn_index { + self.current_turn_index += 1; + } + } + } \ No newline at end of file diff --git a/src/value.rs b/src/value.rs index 967ecab..431fb06 100644 --- a/src/value.rs +++ b/src/value.rs @@ -98,7 +98,7 @@ impl Value { ValueType::Int(v) => *v != 0, ValueType::Float(v) => *v != 0.0, ValueType::String(v) => v.string.len() > 0, - ValueType::DivertTarget(p) => false, // exception Shouldn't be checking the truthiness of a divert target?? + ValueType::DivertTarget(_) => false, // exception Shouldn't be checking the truthiness of a divert target?? } } diff --git a/src/variables_state.rs b/src/variables_state.rs index d395e39..b8637bb 100644 --- a/src/variables_state.rs +++ b/src/variables_state.rs @@ -35,10 +35,10 @@ impl VariablesState { // Finished observing variables in a batch - now send // notifications for changed variables all in one go. if self.changed_variables_for_batch_obs.is_some() { - for variableName in self.changed_variables_for_batch_obs.as_ref().unwrap() { - let current_value = self.global_variables.get(variableName).unwrap(); + for variable_name in self.changed_variables_for_batch_obs.as_ref().unwrap() { + let current_value = self.global_variables.get(variable_name).unwrap(); - (self.variable_changed_event.as_ref().unwrap())(variableName, current_value.as_ref()); + (self.variable_changed_event.as_ref().unwrap())(variable_name, current_value.as_ref()); } } diff --git a/tests/glue_test.rs b/tests/glue_test.rs index 57deb7a..add55a2 100644 --- a/tests/glue_test.rs +++ b/tests/glue_test.rs @@ -1,4 +1,3 @@ -use std::fs; use bladeink::story::Story; mod test_utils; From 3ebdfb3da38e84ef2c32f7630ed4069fba7a56aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Tue, 19 Sep 2023 17:06:30 +0000 Subject: [PATCH 18/91] Unify visibility --- src/callstack.rs | 42 ++++++++++++------------ src/choice.rs | 8 ++--- src/container.rs | 8 ++--- src/control_command.rs | 4 +-- src/divert.rs | 4 +-- src/flow.rs | 2 +- src/object.rs | 4 +-- src/path.rs | 22 ++++++------- src/pointer.rs | 6 ++-- src/push_pop.rs | 2 +- src/state_patch.rs | 6 ++-- src/story.rs | 4 +-- src/story_state.rs | 72 +++++++++++++++++++++--------------------- src/tag.rs | 2 +- src/value.rs | 2 +- src/variables_state.rs | 10 +++--- 16 files changed, 99 insertions(+), 99 deletions(-) diff --git a/src/callstack.rs b/src/callstack.rs index f42dedd..9fd94dc 100644 --- a/src/callstack.rs +++ b/src/callstack.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, rc::Rc}; use crate::{pointer::{Pointer, self}, object::RTObject, push_pop::PushPopType, story::Story, container::Container}; -pub(crate) struct Element { +pub struct Element { pub current_pointer: Pointer, pub in_expression_evaluation: bool, pub temporary_variables: HashMap>, @@ -33,9 +33,9 @@ impl Element { } } -pub(crate) struct Thread { - pub(crate) callstack: Vec, - pub(crate) previous_pointer: Pointer, +pub struct Thread { + pub callstack: Vec, + pub previous_pointer: Pointer, thread_index: usize } @@ -48,7 +48,7 @@ impl Thread { } } - pub(crate) fn copy(&self) -> Thread { + pub fn copy(&self) -> Thread { let mut copy = Thread::new(); copy.thread_index = self.thread_index; @@ -62,7 +62,7 @@ impl Thread { } } -pub(crate) struct CallStack { +pub struct CallStack { thread_counter: usize, start_of_root: Pointer, threads: Vec @@ -95,13 +95,13 @@ impl CallStack { cs } - pub(crate) fn get_current_element(&self) -> &Element { + pub fn get_current_element(&self) -> &Element { let thread = self.threads.last().unwrap(); let cs = &thread.callstack; cs.last().unwrap() } - pub(crate) fn get_current_element_mut(&mut self) -> &mut Element { + pub fn get_current_element_mut(&mut self) -> &mut Element { let thread = self.threads.last_mut().unwrap(); let cs = &mut thread.callstack; cs.last_mut().unwrap() @@ -113,11 +113,11 @@ impl CallStack { self.threads[0].callstack.push(Element::new(PushPopType::Tunnel, self.start_of_root.clone(), false)); } - pub(crate) fn can_pop_thread(&self) -> bool { + pub fn can_pop_thread(&self) -> bool { return self.threads.len() > 1 && !self.element_is_evaluate_from_game(); } - pub(crate) fn pop_thread(&mut self) -> Result<(), String> { + pub fn pop_thread(&mut self) -> Result<(), String> { if self.can_pop_thread() { self.threads.remove(self.threads.len() - 1); Ok(()) @@ -126,50 +126,50 @@ impl CallStack { } } - pub(crate) fn can_pop(&self) -> bool { + pub fn can_pop(&self) -> bool { todo!() } - pub(crate) fn can_pop_type(&self, t: PushPopType) -> bool { + pub fn can_pop_type(&self, t: PushPopType) -> bool { todo!() } - pub(crate) fn element_is_evaluate_from_game(&self) -> bool { + pub fn element_is_evaluate_from_game(&self) -> bool { self.get_current_element().push_pop_type == PushPopType::FunctionEvaluationFromGame } - pub(crate) fn get_elements(&self) -> &Vec { + pub fn get_elements(&self) -> &Vec { self.get_callstack() } - pub(crate) fn get_elements_mut(&mut self) -> &mut Vec { + pub fn get_elements_mut(&mut self) -> &mut Vec { self.get_callstack_mut() } - pub(crate) fn get_callstack(&self) -> &Vec { + pub fn get_callstack(&self) -> &Vec { &self.get_current_thread().callstack } - pub(crate) fn get_callstack_mut(&mut self) -> &mut Vec { + pub fn get_callstack_mut(&mut self) -> &mut Vec { &mut self.get_current_thread_mut().callstack } - pub(crate) fn get_current_thread(&self) -> &Thread { + pub fn get_current_thread(&self) -> &Thread { self.threads.last().unwrap() } - pub(crate) fn get_current_thread_mut(&mut self) -> &mut Thread { + pub fn get_current_thread_mut(&mut self) -> &mut Thread { self.threads.last_mut().unwrap() } - pub(crate) fn set_current_thread(&mut self, value: Thread) { + pub fn set_current_thread(&mut self, value: Thread) { // Debug.Assert (threads.Count == 1, "Shouldn't be directly setting the // current thread when we have a stack of them"); self.threads.clear(); self.threads.push(value); } - pub(crate) fn fork_thread(&mut self) -> Thread { + pub fn fork_thread(&mut self) -> Thread { let mut forked_thread = self.get_current_thread().copy(); self.thread_counter += 1; forked_thread.thread_index = self.thread_counter; diff --git a/src/choice.rs b/src/choice.rs index 6ce3b4f..b529831 100644 --- a/src/choice.rs +++ b/src/choice.rs @@ -16,7 +16,7 @@ pub struct Choice { } impl Choice { - pub(crate) fn new(target_path: Path, source_path: String, is_invisible_default: bool, tags: Vec, thread_at_generation: Thread, text: String, index: usize, original_thread_index: usize) -> Choice { + pub fn new(target_path: Path, source_path: String, is_invisible_default: bool, tags: Vec, thread_at_generation: Thread, text: String, index: usize, original_thread_index: usize) -> Choice { Choice { obj: Object::new(), target_path: target_path, @@ -30,7 +30,7 @@ impl Choice { } } - pub(crate) fn new_from_json(path_string_on_choice: &str, source_path: String, text: &str, index: usize, original_thread_index: usize) -> Choice { + pub fn new_from_json(path_string_on_choice: &str, source_path: String, text: &str, index: usize, original_thread_index: usize) -> Choice { Choice { obj: Object::new(), @@ -45,11 +45,11 @@ impl Choice { } } - pub(crate) fn set_thread_at_generation(&mut self, thread: Thread) { + pub fn set_thread_at_generation(&mut self, thread: Thread) { self.thread_at_generation.replace(Some(thread)); } - pub(crate) fn get_thread_at_generation(&self) -> Option { + pub fn get_thread_at_generation(&self) -> Option { match self.thread_at_generation.borrow().as_ref() { Some(t) => Some(t.copy()), None => None, diff --git a/src/container.rs b/src/container.rs index 5e8ca22..f77f8ed 100644 --- a/src/container.rs +++ b/src/container.rs @@ -48,7 +48,7 @@ impl Container { self.name.is_some() && !self.name.as_ref().unwrap().is_empty() } - pub(crate) fn get_name(&self) -> &str { + pub fn get_name(&self) -> &str { todo!() } @@ -150,11 +150,11 @@ impl Container { } } - pub(crate) fn get_path(self: &Rc) -> Path { + pub fn get_path(self: &Rc) -> Path { Object::get_path(self.clone()) } - pub(crate) fn content_at_path( + pub fn content_at_path( self: &Rc, path: &Path, partial_path_start: usize, @@ -205,7 +205,7 @@ impl Container { } - pub(crate) fn get_count_flags(&self) -> i32 { + pub fn get_count_flags(&self) -> i32 { let mut flags: i32 = 0; if self.visits_should_be_counted { diff --git a/src/control_command.rs b/src/control_command.rs index ccd5145..9cfa4ca 100644 --- a/src/control_command.rs +++ b/src/control_command.rs @@ -36,13 +36,13 @@ pub enum CommandType { EndTag } -pub(crate) struct ControlCommand { +pub struct ControlCommand { obj: Object, pub command_type: CommandType } impl ControlCommand { - pub(crate) fn new(command_type: CommandType) -> Self { + pub fn new(command_type: CommandType) -> Self { ControlCommand {obj: Object::new(), command_type} } } diff --git a/src/divert.rs b/src/divert.rs index dc5a8aa..9c8e882 100644 --- a/src/divert.rs +++ b/src/divert.rs @@ -6,7 +6,7 @@ use std::{ use crate::{object::{Object, RTObject}, push_pop::PushPopType, pointer::{Pointer, self}, path::Path}; -pub(crate) struct Divert { +pub struct Divert { obj: Object, pub external_args: i32, pub is_conditional: bool, @@ -19,7 +19,7 @@ pub(crate) struct Divert { } impl Divert { - pub(crate) fn new(pushes_to_stack: bool, stack_push_type: PushPopType, is_external: bool, external_args: i32, is_conditional: bool, var_divert_name: Option, target_path: Option<&str>) -> Self { + pub fn new(pushes_to_stack: bool, stack_push_type: PushPopType, is_external: bool, external_args: i32, is_conditional: bool, var_divert_name: Option, target_path: Option<&str>) -> Self { Divert { obj: Object::new(), is_conditional, diff --git a/src/flow.rs b/src/flow.rs index 72a653c..b66b536 100644 --- a/src/flow.rs +++ b/src/flow.rs @@ -2,7 +2,7 @@ use std::{rc::Rc, cell::RefCell}; use crate::{callstack::CallStack, choice::Choice, object::RTObject, container::Container}; -pub(crate) struct Flow { +pub struct Flow { pub name: String, pub callstack: Rc>, pub output_stream: Vec>, diff --git a/src/object.rs b/src/object.rs index bb99bad..5b68837 100644 --- a/src/object.rs +++ b/src/object.rs @@ -31,7 +31,7 @@ impl Object { self.parent.borrow().upgrade() } - pub(crate) fn set_parent(&self, parent: &Rc) { + pub fn set_parent(&self, parent: &Rc) { self.parent.replace(Rc::downgrade(parent)); } @@ -193,7 +193,7 @@ pub struct Null { } impl Null { - pub(crate) fn new() -> Null { + pub fn new() -> Null { Null { obj: Object::new(), } diff --git a/src/path.rs b/src/path.rs index f5a86fd..db223e2 100644 --- a/src/path.rs +++ b/src/path.rs @@ -14,7 +14,7 @@ pub struct Path { } impl Path { - pub(crate) fn new(components: &[Component], relative: bool) -> Path { + pub fn new(components: &[Component], relative: bool) -> Path { let mut comp: Vec = Vec::new(); comp.extend_from_slice(components); Path { @@ -74,7 +74,7 @@ impl Path { } } - pub(crate) fn get_component(&self, index: usize) -> Option<&Component> { + pub fn get_component(&self, index: usize) -> Option<&Component> { self.components.get(index) } @@ -103,7 +103,7 @@ impl Path { } } - pub(crate) fn get_last_component(&self) -> Option<&Component> { + pub fn get_last_component(&self) -> Option<&Component> { if self.components.len() > 0 { return self.components.get(self.components.len() - 1); } @@ -139,7 +139,7 @@ impl Path { } } - pub(crate) fn get_components_string(&self) -> String { + pub fn get_components_string(&self) -> String { let mut sb = String::new(); if self.components.len() > 0 { @@ -158,7 +158,7 @@ impl Path { sb } - pub(crate) fn path_by_appending_component( &self, c: Component) -> Path { + pub fn path_by_appending_component( &self, c: Component) -> Path { let mut p = Path::new(self.components.as_ref(), false); p.components.push(c); @@ -213,35 +213,35 @@ impl PartialEq for Path { } #[derive(Eq, Clone)] -pub(crate) struct Component { +pub struct Component { pub index: Option, pub name: Option, } impl Component { - pub(crate) fn new(name: &str) -> Component { + pub fn new(name: &str) -> Component { Component { name: Some(name.to_string()), index: None, } } - pub(crate) fn new_i(index: usize) -> Component { + pub fn new_i(index: usize) -> Component { Component { name: None, index: Some(index), } } - pub(crate) fn to_parent() -> Component { + pub fn to_parent() -> Component { Component::new(PARENT_ID) } - pub(crate) fn is_index(&self) -> bool { + pub fn is_index(&self) -> bool { self.index.is_some() } - pub(crate) fn is_parent(&self) -> bool { + pub fn is_parent(&self) -> bool { match &self.name { Some(name) => name.eq(PARENT_ID), None => false, diff --git a/src/pointer.rs b/src/pointer.rs index 079410c..ea20a18 100644 --- a/src/pointer.rs +++ b/src/pointer.rs @@ -2,11 +2,11 @@ use std::{rc::Rc, fmt}; use crate::{container::Container, object::{RTObject, Object}, path::{Path, Component}}; -pub(crate) const NULL: Pointer = Pointer::new(None, -1); +pub const NULL: Pointer = Pointer::new(None, -1); #[derive(Clone)] -pub(crate) struct Pointer { +pub struct Pointer { pub container: Option>, pub index: i32, } @@ -53,7 +53,7 @@ impl Pointer { Some(container.get_path()) } - pub(crate) fn start_of(container:Rc) -> Pointer { + pub fn start_of(container:Rc) -> Pointer { return Pointer{container: Some(container), index:0}; } } diff --git a/src/push_pop.rs b/src/push_pop.rs index 22ac56a..d799446 100644 --- a/src/push_pop.rs +++ b/src/push_pop.rs @@ -1,5 +1,5 @@ #[derive(PartialEq, Clone, Copy)] -pub(crate) enum PushPopType { +pub enum PushPopType { Tunnel, Function, FunctionEvaluationFromGame diff --git a/src/state_patch.rs b/src/state_patch.rs index 3b671ef..e0428ec 100644 --- a/src/state_patch.rs +++ b/src/state_patch.rs @@ -5,7 +5,7 @@ use std::{ use crate::{object::{RTObject, Object}, container::Container}; #[derive(Clone)] -pub(crate) struct StatePatch { +pub struct StatePatch { pub globals: HashMap>, pub changed_variables: HashSet, pub visit_counts: HashMap, @@ -30,12 +30,12 @@ impl StatePatch { } } - pub(crate) fn get_visit_count(&self, container: &Rc) -> Option { + pub fn get_visit_count(&self, container: &Rc) -> Option { let key = Object::get_path(container.clone()).to_string(); self.visit_counts.get(&key).copied() } - pub(crate) fn set_visit_count(&mut self, container: &Rc, count: usize) { + pub fn set_visit_count(&mut self, container: &Rc, count: usize) { let key = Object::get_path(container.clone()).to_string(); self.visit_counts.insert(key, count); } diff --git a/src/story.rs b/src/story.rs index 4a36ffa..c01eea4 100644 --- a/src/story.rs +++ b/src/story.rs @@ -443,7 +443,7 @@ impl Story { self.state.as_mut().unwrap().get_current_text() } - pub(crate) fn get_main_content_container(&self) -> Rc { + pub fn get_main_content_container(&self) -> Rc { match self.temporaty_evaluation_container.as_ref() { Some(c) => c.clone(), None => self.main_content_container.clone(), @@ -1135,7 +1135,7 @@ impl Story { return choice_only_str_val.string.to_string(); } - pub(crate) fn pointer_at_path(main_content_container: &Rc, path: &Path) -> Pointer { + pub fn pointer_at_path(main_content_container: &Rc, path: &Path) -> Pointer { if path.len() == 0 { return pointer::NULL.clone(); } diff --git a/src/story_state.rs b/src/story_state.rs index 6d7a614..8035db3 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -11,7 +11,7 @@ pub const MIN_COMPATIBLE_LOAD_VERSION: u32 = 8; static DEFAULT_FLOW_NAME: &str = "DEFAULT_FLOW"; -pub(crate) struct StoryState { +pub struct StoryState { pub current_flow: Flow, pub did_safe_exit: bool, output_stream_text_dirty: bool, @@ -76,19 +76,19 @@ impl StoryState { !self.current_errors.is_empty() } - pub(crate) fn get_current_pointer(&self) -> Pointer { + pub fn get_current_pointer(&self) -> Pointer { self.get_callstack().borrow().get_current_element().current_pointer.clone() } - pub(crate) fn get_callstack(&self) -> &Rc> { + pub fn get_callstack(&self) -> &Rc> { &self.current_flow.callstack } - pub(crate) fn set_did_safe_exit(&mut self, did_safe_exit: bool) { + pub fn set_did_safe_exit(&mut self, did_safe_exit: bool) { self.did_safe_exit = did_safe_exit; } - pub(crate) fn reset_output(&mut self, objs: Option>>) { + pub fn reset_output(&mut self, objs: Option>>) { self.get_output_stream_mut().clear(); if let Some(objs) = objs { for o in objs { @@ -98,39 +98,39 @@ impl StoryState { self.output_stream_dirty(); } - pub(crate) fn get_variables_state(&self) -> &VariablesState { + pub fn get_variables_state(&self) -> &VariablesState { &self.variables_state } - pub(crate) fn get_variables_state_mut(&mut self) -> &mut VariablesState { + pub fn get_variables_state_mut(&mut self) -> &mut VariablesState { &mut self.variables_state } - pub(crate) fn get_generated_choices_mut(&mut self) -> &mut Vec> { + pub fn get_generated_choices_mut(&mut self) -> &mut Vec> { &mut self.current_flow.current_choices } - pub(crate) fn get_generated_choices(&self) -> &Vec> { + pub fn get_generated_choices(&self) -> &Vec> { &self.current_flow.current_choices } - pub(crate) fn is_did_safe_exit(&self) -> bool { + pub fn is_did_safe_exit(&self) -> bool { self.did_safe_exit } - pub(crate) fn has_warning(&self) -> bool { + pub fn has_warning(&self) -> bool { !self.current_warnings.is_empty() } - pub(crate) fn get_current_errors(&self) -> &Vec { + pub fn get_current_errors(&self) -> &Vec { &self.current_errors } - pub(crate) fn get_current_warnings(&self) -> &Vec { + pub fn get_current_warnings(&self) -> &Vec { &self.current_warnings } - pub(crate) fn get_output_stream(&self) -> &Vec> { + pub fn get_output_stream(&self) -> &Vec> { &self.current_flow.output_stream } @@ -143,7 +143,7 @@ impl StoryState { self.output_stream_tags_dirty = true; } - pub(crate) fn in_string_evaluation(&self) -> bool { + pub fn in_string_evaluation(&self) -> bool { for e in self.get_output_stream().iter().rev() { if let Some(cmd) = e.as_any().downcast_ref::() { if cmd.command_type == CommandType::BeginString { @@ -189,7 +189,7 @@ impl StoryState { self.current_text.as_ref().unwrap().to_string() } - pub(crate) fn get_current_tags(&mut self) -> Vec { + pub fn get_current_tags(&mut self) -> Vec { if self.output_stream_tags_dirty { let mut current_tags = Vec::new(); let mut in_tag = false; @@ -281,7 +281,7 @@ impl StoryState { } - pub(crate) fn output_stream_ends_in_newline(&self) -> bool { + pub fn output_stream_ends_in_newline(&self) -> bool { if !self.get_output_stream().is_empty() { for e in self.get_output_stream().iter().rev() { if let Some(cmd) = e.as_any().downcast_ref::() { @@ -303,19 +303,19 @@ impl StoryState { false } - pub(crate) fn set_current_pointer(&self, pointer: Pointer) { + pub fn set_current_pointer(&self, pointer: Pointer) { self.get_callstack().as_ref().borrow_mut().get_current_element_mut().current_pointer = pointer; } - pub(crate) fn get_in_expression_evaluation(&self) -> bool { + pub fn get_in_expression_evaluation(&self) -> bool { self.get_callstack().borrow().get_current_element().in_expression_evaluation } - pub(crate) fn set_in_expression_evaluation(&self, value: bool) { + pub fn set_in_expression_evaluation(&self, value: bool) { self.get_callstack().borrow_mut().get_current_element_mut().in_expression_evaluation = value; } - pub(crate) fn push_evaluation_stack(&mut self, obj: Rc) { + pub fn push_evaluation_stack(&mut self, obj: Rc) { // TODO @@ -348,7 +348,7 @@ impl StoryState { self.evaluation_stack.push(obj); } - pub(crate) fn push_to_output_stream(&mut self, obj: Option>) { + pub fn push_to_output_stream(&mut self, obj: Option>) { let text = match &obj { Some(obj) => { let obj = obj.clone(); @@ -378,7 +378,7 @@ impl StoryState { self.push_to_output_stream_individual(obj.unwrap()); } - pub(crate) fn increment_visit_count_for_container(&mut self, container: &Rc) { + pub fn increment_visit_count_for_container(&mut self, container: &Rc) { let has_patch = self.patch.is_some(); if has_patch { @@ -425,7 +425,7 @@ impl StoryState { 0 } - pub(crate) fn record_turn_index_visit_to_container(&self, container: &crate::container::Container) { + pub fn record_turn_index_visit_to_container(&self, container: &crate::container::Container) { todo!() } @@ -661,19 +661,19 @@ impl StoryState { false } - pub(crate) fn set_previous_pointer(&self, p: Pointer) { + pub fn set_previous_pointer(&self, p: Pointer) { self.get_callstack().as_ref().borrow_mut().get_current_thread_mut().previous_pointer = p.clone(); } - pub(crate) fn get_previous_pointer(&self) -> Pointer { + pub fn get_previous_pointer(&self) -> Pointer { self.get_callstack().as_ref().borrow_mut().get_current_thread_mut().previous_pointer.clone() } - pub(crate) fn try_exit_function_evaluation_from_game(&self) { + pub fn try_exit_function_evaluation_from_game(&self) { todo!() } - pub(crate) fn pop_callstack(&self, function: PushPopType) { + pub fn pop_callstack(&self, function: PushPopType) { todo!() } @@ -681,7 +681,7 @@ impl StoryState { self.get_callstack().as_ref().borrow_mut().get_current_element_mut().current_pointer = Pointer::start_of(self.main_content_container.clone()) } - pub(crate) fn get_current_choices(&self) -> Option<&Vec>> { + pub fn get_current_choices(&self) -> Option<&Vec>> { // 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. @@ -692,7 +692,7 @@ impl StoryState { Some(&self.current_flow.current_choices) } - pub(crate) fn copy_and_start_patching(&self) -> StoryState { + pub fn copy_and_start_patching(&self) -> StoryState { let mut copy = StoryState::new(self.main_content_container.clone()); copy.patch = Some(StatePatch::new(self.patch.as_ref())); @@ -759,7 +759,7 @@ impl StoryState { copy } - pub(crate) fn restore_after_patch(&mut self) { + pub fn restore_after_patch(&mut self) { // 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 @@ -768,7 +768,7 @@ impl StoryState { self.variables_state.patch = self.patch.clone(); // usually null } - pub(crate) fn apply_any_patch(&mut self) { + pub fn apply_any_patch(&mut self) { if self.patch.is_none() { return; } @@ -792,7 +792,7 @@ impl StoryState { todo!() } - pub(crate) fn pop_from_output_stream(&mut self, count: usize) { + pub fn pop_from_output_stream(&mut self, count: usize) { let len = self.get_output_stream().len(); if count <= len { @@ -803,7 +803,7 @@ impl StoryState { self.output_stream_dirty(); } - pub(crate) fn pop_evaluation_stack(&mut self) -> Rc { + pub fn pop_evaluation_stack(&mut self) -> Rc { let obj = self.evaluation_stack.last().unwrap().clone(); self.evaluation_stack.remove(self.evaluation_stack.len() - 1); @@ -812,11 +812,11 @@ impl StoryState { obj } - pub(crate) fn set_diverted_pointer(&mut self, p: Pointer) { + pub fn set_diverted_pointer(&mut self, p: Pointer) { self.diverted_pointer = p; } - pub(crate) fn set_chosen_path(&mut self, path: &Path, incrementing_turn_index: bool) { + pub fn set_chosen_path(&mut self, path: &Path, incrementing_turn_index: bool) { // Changing direction, assume we need to clear current set of choices self.current_flow.current_choices.clear(); diff --git a/src/tag.rs b/src/tag.rs index 60d7516..e588cc0 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -12,7 +12,7 @@ impl Tag { Tag {obj: Object::new(), text: text.to_string()} } - pub(crate) fn get_text(&self) -> String { + pub fn get_text(&self) -> String { self.text.clone() } } diff --git a/src/value.rs b/src/value.rs index 431fb06..1724377 100644 --- a/src/value.rs +++ b/src/value.rs @@ -26,7 +26,7 @@ pub struct StringValue { } impl StringValue { - pub(crate) fn is_non_whitespace(&self) -> bool { + pub fn is_non_whitespace(&self) -> bool { return !self.is_newline && !self.is_inline_whitespace; } diff --git a/src/variables_state.rs b/src/variables_state.rs index b8637bb..1790c0c 100644 --- a/src/variables_state.rs +++ b/src/variables_state.rs @@ -2,7 +2,7 @@ use std::{collections::{HashMap, HashSet}, rc::Rc, cell::RefCell}; use crate::{object::RTObject, callstack::CallStack, state_patch::StatePatch}; -pub(crate) struct VariablesState { +pub struct VariablesState { pub global_variables: HashMap>, pub default_global_variables: Option>>, pub batch_observing_variable_changes: bool, @@ -14,7 +14,7 @@ pub(crate) struct VariablesState { } impl VariablesState { - pub(crate) fn new(callstack: Rc>) -> VariablesState { + pub fn new(callstack: Rc>) -> VariablesState { VariablesState { global_variables: HashMap::new(), default_global_variables: None, @@ -26,7 +26,7 @@ impl VariablesState { } } - pub(crate) fn set_batch_observing_variable_changes(&mut self, value: bool) { + pub fn set_batch_observing_variable_changes(&mut self, value: bool) { self.batch_observing_variable_changes = value; if value { @@ -46,11 +46,11 @@ impl VariablesState { } } - pub(crate) fn snapshot_default_globals(&mut self) { + pub fn snapshot_default_globals(&mut self) { self.default_global_variables = Some(self.global_variables.clone()); } - pub(crate) fn apply_patch(&self) { + pub fn apply_patch(&self) { todo!() } } From 1b2e7b5003ff3de8d3dd64658bdaf79022462152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Fri, 22 Sep 2023 17:20:19 +0000 Subject: [PATCH 19/91] WIP choice tests --- src/callstack.rs | 89 +++++++- src/container.rs | 66 ++++-- src/divert.rs | 123 +++++++++-- src/json_serialization.rs | 82 +++---- src/lib.rs | 1 + src/object.rs | 27 ++- src/path.rs | 1 + src/pointer.rs | 2 +- src/state_patch.rs | 8 +- src/story.rs | 212 +++++++++--------- src/story_state.rs | 80 ++++--- src/value.rs | 45 +++- src/variable_assigment.rs | 33 +++ src/variables_state.rs | 177 +++++++++++++++- src/void.rs | 4 +- tests/basic_text_test.rs | 4 +- tests/choice_test.rs | 283 ++++++++++++++++++++++++- tests/{test_utils.rs => common/mod.rs} | 7 + tests/divert_test.rs | 19 ++ tests/glue_test.rs | 6 +- 20 files changed, 1034 insertions(+), 235 deletions(-) create mode 100644 src/variable_assigment.rs rename tests/{test_utils.rs => common/mod.rs} (93%) create mode 100644 tests/divert_test.rs diff --git a/src/callstack.rs b/src/callstack.rs index 9fd94dc..db95873 100644 --- a/src/callstack.rs +++ b/src/callstack.rs @@ -107,7 +107,11 @@ impl CallStack { cs.last_mut().unwrap() } - fn reset(&mut self) { + pub fn get_current_element_index(&self) -> i32 { + self.get_callstack().len() as i32 - 1 + } + + pub fn reset(&mut self) { self.threads.clear(); self.threads.push(Thread::new()); self.threads[0].callstack.push(Element::new(PushPopType::Tunnel, self.start_of_root.clone(), false)); @@ -127,11 +131,27 @@ impl CallStack { } pub fn can_pop(&self) -> bool { - todo!() + self.get_callstack().len() > 1 } pub fn can_pop_type(&self, t: PushPopType) -> bool { - todo!() + if !self.can_pop() { + return false; + } + + //if t.is_some() {return true;} + + self.get_current_element().push_pop_type == t + } + + pub fn pop_type(&mut self, t: PushPopType) { + if self.can_pop_type(t) { + let l = self.get_callstack().len() - 1; + self.get_callstack_mut().remove(l); + return; + } else { + panic!("Mismatched push/pop in Callstack"); + } } pub fn element_is_evaluate_from_game(&self) -> bool { @@ -175,4 +195,67 @@ impl CallStack { forked_thread.thread_index = self.thread_counter; forked_thread } + + pub fn set_temporary_variable( + &mut self, + name: String, + value: Rc, + declare_new: bool, + mut context_index: i32, + ) -> Result<(), String> { + if context_index == -1 { + context_index = self.get_current_element_index() + 1; + } + + let context_element = self.get_callstack_mut().get_mut((context_index - 1) as usize).unwrap(); + + if !declare_new && !context_element.temporary_variables.contains_key(&name) { + return Err(format!("Could not find temporary variable to set: {}", name)); + } + + let old_value = context_element.temporary_variables.get(&name).cloned(); + + if let Some(old_value) = &old_value { + // TODO + //ListValue::retain_list_origins_for_assignment(old_value, &value); + } + + context_element.temporary_variables.insert(name, value); + + Ok(()) + } + + pub fn context_for_variable_named(&self, name: &str) -> usize { + // Check if the current temporary context contains the variable. + if self.get_current_element().temporary_variables.contains_key(name) { + return (self.get_current_element_index() + 1) as usize; + } + + // Otherwise, it's a global variable. + 0 + } + + pub fn get_temporary_variable_with_name(&self, name: &str, context_index: i32) -> Option> { + let mut context_index = context_index; + if context_index == -1 { + context_index = self.get_current_element_index() + 1; + } + + let context_element = self.get_callstack().get((context_index - 1)as usize); + let var_value = context_element.unwrap().temporary_variables.get(name); + + var_value.cloned() + } + + pub fn push( &mut self, t: PushPopType, external_evaluation_stack_height: usize, output_stream_length_with_pushed: i32) { + // When pushing to callstack, maintain the current content path, but + // jump + // out of expressions by default + let mut element = Element::new(t, self.get_current_element().current_pointer.clone(), false); + + element.evaluation_stack_height_when_pushed = external_evaluation_stack_height; + element.function_start_in_output_stream = output_stream_length_with_pushed; + + self.get_callstack_mut().push(element); + } } \ No newline at end of file diff --git a/src/container.rs b/src/container.rs index f77f8ed..2ed1129 100644 --- a/src/container.rs +++ b/src/container.rs @@ -18,7 +18,7 @@ pub struct Container { obj: Object, pub name: Option, pub content: Vec>, - pub named_content: HashMap>, + named_content: HashMap>, pub visits_should_be_counted: bool, pub turn_index_should_be_counted: bool, pub counting_at_start_only: bool, @@ -27,6 +27,16 @@ pub struct Container { impl Container { pub fn new(name: Option, count_flags: i32, content: Vec>, named_content: HashMap>) -> Rc { + let mut named_content = named_content; + + content.iter().for_each(|o| { + if let Ok(c) = o.clone().into_any().downcast::() { + if c.has_valid_name() { + named_content.insert(c.name.as_ref().unwrap().to_string(), c); + } + } + }); + let (visits_should_be_counted, turn_index_should_be_counted, counting_at_start_only) = Container::split_count_flags(count_flags); let c = Rc::new(Container { @@ -40,6 +50,7 @@ impl Container { }); c.content.iter().for_each(|o| o.get_object().set_parent(&c)); + c.named_content.values().for_each(|o| o.get_object().set_parent(&c)); c } @@ -48,10 +59,6 @@ impl Container { self.name.is_some() && !self.name.as_ref().unwrap().is_empty() } - pub fn get_name(&self) -> &str { - todo!() - } - pub fn build_string_of_hierarchy( &self, sb: &mut String, @@ -89,7 +96,6 @@ impl Container { sb.push_str(&&s.string.replace('\n', "\\n")); sb.push('\"'); } else { - Container::append_indentation(sb, indentation); sb.push_str(&v.to_string()); } } else { @@ -109,20 +115,25 @@ impl Container { } } + sb.push_str(" ("); + sb.push_str(&Object::get_path(obj.as_ref()).to_string()); + sb.push(')'); + sb.push('\n'); } - // HashMap onlyNamed = new HashMap(); + let mut only_named: HashMap> = HashMap::new(); - // for (Entry objKV : getNamedContent().entrySet()) { - // if (getContent().contains(objKV.getValue())) { - // continue; - // } else { - // onlyNamed.put(objKV.getKey(), objKV.getValue()); - // } - // } + for (k, v) in self.named_content.iter() { + let o: Rc = v.clone(); + if self.content.iter().any(|e| Rc::ptr_eq(e, &o)) { + continue; + } else { + only_named.insert(k.clone(), v.clone()); + } + } - let only_named = &self.named_content; + if only_named.len() > 0 { Container::append_indentation(sb, indentation); @@ -151,7 +162,7 @@ impl Container { } pub fn get_path(self: &Rc) -> Path { - Object::get_path(self.clone()) + Object::get_path(self.as_ref()) } pub fn content_at_path( @@ -191,9 +202,8 @@ impl Container { current_obj = found_obj.unwrap().clone(); current_container = if let Ok(container) = current_obj.clone().into_any().downcast::() { - let mut sb = String::new(); - container.build_string_of_hierarchy(&mut sb, 0, None); - println!("CONTAINER NAME: {}", sb); + + Some(container) } else { None @@ -263,6 +273,24 @@ impl Container { None } + + pub fn get_named_only_content(&self) -> HashMap> { + let mut named_only_content_dict = HashMap::new(); + + for (key, value) in self.named_content.iter() { + named_only_content_dict.insert(key.clone(), value.clone()); + } + + for c in &self.content { + if let Some(named) = c.as_any().downcast_ref::() { + if named.has_valid_name() { + named_only_content_dict.remove(named.name.as_ref().unwrap()); + } + } + } + + named_only_content_dict + } } diff --git a/src/divert.rs b/src/divert.rs index 9c8e882..5ba173e 100644 --- a/src/divert.rs +++ b/src/divert.rs @@ -1,9 +1,8 @@ use std::{ - fmt, - rc::Rc, + fmt, rc::Rc, cell::RefCell, }; -use crate::{object::{Object, RTObject}, push_pop::PushPopType, pointer::{Pointer, self}, path::Path}; +use crate::{object::{Object, RTObject}, push_pop::PushPopType, pointer::{Pointer, self}, path::{Path, Component}, container::Container}; pub struct Divert { @@ -13,8 +12,8 @@ pub struct Divert { pub is_external: bool, pub pushes_to_stack: bool, pub stack_push_type: PushPopType, - pub target_pointer: Pointer, - pub target_path: Option, + target_pointer: RefCell, + target_path: RefCell>, pub variable_divert_name: Option, } @@ -27,8 +26,8 @@ impl Divert { stack_push_type, is_external, external_args, - target_pointer: pointer::NULL.clone(), - target_path: Self::target_path_string(target_path), + target_pointer: RefCell::new(pointer::NULL.clone()), + target_path: RefCell::new(Self::target_path_string(target_path)), variable_divert_name: var_divert_name, } } @@ -42,17 +41,106 @@ impl Divert { } fn get_target_path_string(&self) -> Option { - if let Some(target_path) = &self.target_path { - // TODO Some(compact_path_string(target_path)) - None - } else { - None + match self.target_path.borrow().as_ref() { + Some(p) => Some(self.compact_path_string(p)), + None => None, } } pub fn has_variable_target(&self) -> bool { self.variable_divert_name.is_some() } + + fn compact_path_string(&self, other_path: &Path) -> String { + let global_path_str; + let relative_path_str; + + if other_path.is_relative() { + relative_path_str = other_path.get_components_string(); + global_path_str = Object::get_path(self).path_by_appending_path(other_path).get_components_string(); + } else { + let relative_path = self.convert_path_to_relative(other_path); + relative_path_str = relative_path.get_components_string(); + global_path_str = other_path.get_components_string(); + } + + if relative_path_str.len() < global_path_str.len() { + relative_path_str.clone() + } else { + global_path_str.clone() + } + } + + pub fn get_target_pointer(self: &Rc) -> Pointer { + let target_pointer_null = self.target_pointer.borrow().is_null(); + if target_pointer_null { + let target_obj = Object::resolve_path(self.clone(), self.target_path.borrow().as_ref().unwrap()).obj.clone(); + + if self.target_path.borrow().as_ref().unwrap().get_last_component().unwrap().is_index() { + self.target_pointer.borrow_mut().container = target_obj.get_object().get_parent(); + self.target_pointer.borrow_mut().index = self.target_path.borrow().as_ref().unwrap().get_last_component().unwrap().index.unwrap() as i32; + } else { + let c = target_obj.into_any().downcast::(); + self.target_pointer.replace(Pointer::start_of(c.unwrap())); + } + } + + self.target_pointer.borrow().clone() + } + + pub fn get_target_path(self: &Rc) -> Option { + // Resolve any relative paths to global ones as we come across them + let target_path = self.target_path.borrow(); + + match target_path.as_ref() { + Some(target_path) => { + if target_path.is_relative() { + let target_obj = self.get_target_pointer().resolve(); + + if let Some(target_obj) = target_obj { + self.target_path.replace(Some(Object::get_path(target_obj.as_ref()))); + } + } + Some(self.target_path.borrow().as_ref().unwrap().clone()) + }, + None => None, + } + } + + + fn convert_path_to_relative(&self, global_path: &Path) -> Path { + let own_path = Object::get_path(self); + let min_path_length = std::cmp::min(global_path.len(), own_path.len()); + let mut last_shared_path_comp_index: i32 = -1; + + for i in 0..min_path_length { + let own_comp = own_path.get_component(i); + let other_comp = global_path.get_component(i); + + if own_comp.eq(&other_comp) { + last_shared_path_comp_index = i as i32; + } else { + break; + } + } + + if last_shared_path_comp_index == -1 { + return global_path.clone(); + } + + let num_upwards_moves = (own_path.len() - 1) - last_shared_path_comp_index as usize; + let mut new_path_comps = Vec::new(); + + for _ in 0..num_upwards_moves { + new_path_comps.push(Component::to_parent()); + } + + for down in (last_shared_path_comp_index as usize + 1)..global_path.len() { + new_path_comps.push(global_path.get_component(down).unwrap().clone()); + } + + Path::new(&new_path_comps, true) + } } impl RTObject for Divert { @@ -67,14 +155,10 @@ impl fmt::Display for Divert { if let Some(variable_diver_name) = &self.variable_divert_name { result.push_str(&format!("Divert(variable: {})", variable_diver_name)); - } else if self.target_path.is_none() { + } else if self.target_path.borrow().is_none() { result.push_str("Divert(null)"); } else { - let mut sb = String::new(); - let target_str = self.target_path.as_ref().unwrap().to_string(); - // if let Some(target_line_num) = debug_line_number_of_path(self.get_target_path().unwrap()) { - // sb.push_str(&format!("line {}", target_line_num)); - // } + let target_str = self.target_path.borrow().as_ref().unwrap().get_components_string(); result.push_str("Divert"); @@ -90,7 +174,8 @@ impl fmt::Display for Divert { } } - result.push_str(&format!(" -> {} ({})", self.get_target_path_string().unwrap_or_default(), sb)); + //result.push_str(&format!(" -> {} ({})", self.get_target_path_string().unwrap_or_default(), target_str)); + result.push_str(&format!(" -> {}", self.get_target_path_string().unwrap_or_default())); } write!(f, "{result}") diff --git a/src/json_serialization.rs b/src/json_serialization.rs index ca00c5b..5ce7cf3 100644 --- a/src/json_serialization.rs +++ b/src/json_serialization.rs @@ -4,7 +4,7 @@ use serde_json::Map; use crate::{ container::Container, - object::{self, RTObject}, control_command::{CommandType, ControlCommand}, value::Value, glue::Glue, path::Path, choice_point::ChoicePoint, choice::Choice, push_pop::PushPopType, divert::Divert, + object::{self, RTObject}, control_command::{CommandType, ControlCommand}, value::Value, glue::Glue, path::Path, choice_point::ChoicePoint, choice::Choice, push_pop::PushPopType, divert::Divert, variable_assigment::VariableAssignment, void::Void, }; pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) -> Result, String> { @@ -49,10 +49,11 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) // Pop if ("->->".eq(str)) {return CommandType.popTunnel();} else if ("~ret".eq(str)) {return CommandType.popFunction();} - - // Void - if ("void".eq(str)) {return new Void();} */ + + // Void + if "void".eq(str) {return Ok(Rc::new(Void::new()));} + return Err(format!("Failed to convert token to runtime RTObject: {}", &token.to_string())); }, @@ -66,16 +67,21 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) } // // VariablePointerValue - // prop_value = obj.get("^var"); - // if (prop_value.is_some()) { - // VariablePointerValue varPtr = new VariablePointerValue((String) prop_value); + let prop_value = obj.get("^var"); - // prop_value = obj.get("ci"); + if let Some(v) = prop_value { + let variable_name = v.as_str().unwrap(); + let mut contex_index = -1; + let prop_value = obj.get("ci"); - // if (prop_value != null) varPtr.setContextIndex((Integer) prop_value); + if let Some(v) = prop_value { + contex_index = v.as_i64().unwrap() as i32; + } - // return varPtr; - // } + let var_ptr = Rc::new(Value::new_variable_pointer( variable_name, contex_index)); + + return Ok(var_ptr); + } // // Divert let mut is_divert = false; @@ -164,29 +170,32 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) // } // } // // Variable assignment - // boolean isVarAss = false; - // boolean isGlobalVar = false; + let mut is_var_ass = false; + let mut is_global_var = false; + + let mut prop_value = obj.get("VAR="); + match prop_value { + Some(_) => { + is_var_ass = true; + is_global_var = true; + }, + None => { + prop_value = obj.get("temp="); + if let Some(_) = prop_value { + is_var_ass = true; + is_global_var = false; + } + } + } - // prop_value = obj.get("VAR="); - // if (prop_value != null) { - // isVarAss = true; - // isGlobalVar = true; - // } else { - // prop_value = obj.get("temp="); - // if (prop_value != null) { - // isVarAss = true; - // isGlobalVar = false; - // } - // } - // if (isVarAss) { - // String varName = prop_value.toString(); - // prop_value = obj.get("re"); - // boolean isNewDecl = prop_value == null; - - // VariableAssignment varAss = new VariableAssignment(varName, isNewDecl); - // varAss.setIsGlobal(isGlobalVar); - // return varAss; - // } + if is_var_ass { + let var_name = prop_value.unwrap().as_str().unwrap(); + let prop_value = obj.get("re"); + let is_new_decl = prop_value.is_none(); + + let var_ass = Rc::new(VariableAssignment::new(var_name, is_new_decl, is_global_var)); + return Ok(var_ass); + } // // Legacy Tag // prop_value = obj.get("#"); @@ -322,10 +331,3 @@ fn jobject_to_choice(obj: &Map) -> Result) -> Path { + pub fn get_path(rtobject: &dyn RTObject) -> Path { if let Some(p) = rtobject.get_object().path.borrow().as_ref() { return p.clone(); } @@ -46,14 +46,15 @@ impl Object { let mut container = rtobject.get_object().get_parent(); let mut child = rtobject.clone(); + let mut child_rc = None; while let Some(c) = container { let mut child_valid_name = false; - if let Some(cc) = child.as_ref().downcast_ref::() { + if let Some(cc) = child.downcast_ref::() { if cc.has_valid_name() { child_valid_name = true; - comps.push(Component::new(cc.get_name())); + comps.push(Component::new(cc.name.as_ref().unwrap())); } } @@ -61,13 +62,17 @@ impl Object { comps.push(Component::new_i( c.content .iter() - .position(|r| Rc::ptr_eq( r, &child) ) - .unwrap(), + .position(|r| { + let a = r.as_ref() as *const _ as *const (); + let b = child as *const _ as *const (); + std::ptr::eq(a, b) + }).unwrap(), )); } container = c.get_object().get_parent(); - child = c; + child_rc = Some(c); + child = child_rc.as_ref().unwrap().as_ref(); } // Reverse list because components are searched in reverse order. @@ -144,7 +149,7 @@ impl Object { if other_path.is_relative() { relative_path_str = other_path.get_components_string(); - global_path_str = Object::get_path(rtobject.clone()).path_by_appending_path(&other_path).get_components_string(); + global_path_str = Object::get_path(rtobject.as_ref()).path_by_appending_path(&other_path).get_components_string(); } else { let relative_path = Object::convert_path_to_relative(&rtobject, &other_path); relative_path_str = relative_path.get_components_string(); @@ -236,9 +241,9 @@ mod tests { println!("root: {}", sb); - assert_eq!(Object::get_path(container1).to_string(), "0"); - assert_eq!(Object::get_path(container2).to_string(), "1"); - assert_eq!(Object::get_path(container21).to_string(), "1.0"); - assert_eq!(Object::get_path(root).to_string(), ""); + assert_eq!(Object::get_path(container1.as_ref()).to_string(), "0"); + assert_eq!(Object::get_path(container2.as_ref()).to_string(), "1"); + assert_eq!(Object::get_path(container21.as_ref()).to_string(), "1.0"); + assert_eq!(Object::get_path(root.as_ref()).to_string(), ""); } } diff --git a/src/path.rs b/src/path.rs index db223e2..31522d9 100644 --- a/src/path.rs +++ b/src/path.rs @@ -11,6 +11,7 @@ const PARENT_ID: &str = "^"; pub struct Path { components: Vec, is_relative: bool, + // components_string: RefCell, // TODO } impl Path { diff --git a/src/pointer.rs b/src/pointer.rs index ea20a18..d12b87d 100644 --- a/src/pointer.rs +++ b/src/pointer.rs @@ -1,6 +1,6 @@ use std::{rc::Rc, fmt}; -use crate::{container::Container, object::{RTObject, Object}, path::{Path, Component}}; +use crate::{container::Container, object::RTObject, path::{Path, Component}}; pub const NULL: Pointer = Pointer::new(None, -1); diff --git a/src/state_patch.rs b/src/state_patch.rs index e0428ec..8b5f2b4 100644 --- a/src/state_patch.rs +++ b/src/state_patch.rs @@ -31,12 +31,16 @@ impl StatePatch { } pub fn get_visit_count(&self, container: &Rc) -> Option { - let key = Object::get_path(container.clone()).to_string(); + let key = Object::get_path(container.as_ref()).to_string(); self.visit_counts.get(&key).copied() } pub fn set_visit_count(&mut self, container: &Rc, count: usize) { - let key = Object::get_path(container.clone()).to_string(); + let key = Object::get_path(container.as_ref()).to_string(); self.visit_counts.insert(key, count); } + + pub fn get_global(&self, name: &str) -> Option>{ + self.globals.get(name).cloned() + } } \ No newline at end of file diff --git a/src/story.rs b/src/story.rs index c01eea4..2bd712b 100644 --- a/src/story.rs +++ b/src/story.rs @@ -7,7 +7,7 @@ use crate::{ error::ErrorType, json_serialization, push_pop::PushPopType, - story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, search_result::SearchResult, + story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, search_result::SearchResult, variable_assigment::VariableAssignment, }; const INK_VERSION_CURRENT: i32 = 21; @@ -207,7 +207,7 @@ impl Story { } } - println!("{}", self.build_string_of_hierarchy()); + //println!("{}", self.build_string_of_hierarchy()); if output_stream_ends_in_newline { break; @@ -530,6 +530,7 @@ impl Story { // return/done statement in knot // that was diverted to rather than called as a function) let mut current_content_obj = pointer.resolve(); + let is_logic_or_flow_control = self.perform_logic_and_flow_control(¤t_content_obj); // Has flow been forced to end by flow control above? @@ -542,15 +543,17 @@ impl Story { } // Choice with condition? - if let Ok(choice_point) = current_content_obj.clone().unwrap().into_any().downcast::() { + if current_content_obj.is_some() { + if let Ok(choice_point) = current_content_obj.clone().unwrap().into_any().downcast::() { - let choice = self.process_choice(&choice_point); - if choice.is_some() { - self.state.as_mut().unwrap().get_generated_choices_mut().push(choice.unwrap()); - } + let choice = self.process_choice(&choice_point); + if choice.is_some() { + self.state.as_mut().unwrap().get_generated_choices_mut().push(choice.unwrap()); + } - current_content_obj = None; - should_add_to_stream = false; + current_content_obj = None; + should_add_to_stream = false; + } } // If the container has no content, then it will be @@ -567,18 +570,16 @@ impl Story { // to our current (possibly temporary) context index. And make a // copy of the pointer // so that we're not editing the original runtime Object. - - // TODO - // VariablePointerValue varPointer = - // currentContentObj instanceof VariablePointerValue ? (VariablePointerValue) currentContentObj : null; + let var_pointer = + Value::get_variable_pointer_value(current_content_obj.as_ref().unwrap().as_ref()); - // if (varPointer != null && varPointer.getContextIndex() == -1) { + if var_pointer.is_some() && var_pointer.unwrap().context_index == -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); - // } + // Create new Object so we're not overwriting the story's own + // data + let context_idx = self.state.as_ref().unwrap().get_callstack().borrow().context_for_variable_named(&var_pointer.unwrap().variable_name); + current_content_obj = Some(Rc::new(Value::new_variable_pointer(&var_pointer.unwrap().variable_name, context_idx as i32))); + } // Expression evaluation content if self.state.as_ref().unwrap().get_in_expression_evaluation() { @@ -586,7 +587,7 @@ impl Story { } // Output stream content (i.e. not expression evaluation) else { - self.state.as_mut().unwrap().push_to_output_stream(current_content_obj); + self.state.as_mut().unwrap().push_to_output_stream(current_content_obj.unwrap()); } } @@ -724,7 +725,7 @@ impl Story { }; // Divert - if let Some(current_divert) = content_obj.as_ref().as_any().downcast_ref::() { + if let Ok(current_divert) = content_obj.clone().into_any().downcast::() { if current_divert.is_conditional { let o = self.state.as_mut().unwrap().pop_evaluation_stack(); if !self.is_truthy(o) { @@ -733,54 +734,54 @@ impl Story { } if current_divert.has_variable_target() { - // let var_name = current_divert.variable_divert_name; - // if let Some(var_contents) = self.state.as_ref().unwrap().get_variables_state().get_variable_with_name(var_name) { - // if let Some(target) = var_contents.downcast_ref::() { - // self.state.as_ref().unwrap().set_diverted_pointer(pointer_at_path(&target.get_target_path())); - // } else { - // let int_content = var_contents.downcast_ref::(); - // let error_message = format!( - // "Tried to divert to a target from a variable, but the variable ({}) didn't contain a divert target, it ", - // var_name - // ); - // let error_message = if let Some(int_content) = int_content { - // if int_content.value == 0 { - // format!("{}was empty/null (the value 0).", error_message) - // } else { - // format!("{}contained '{}'.", error_message, var_contents) - // } - // } else { - // error_message - // }; - - // error(error_message); - // } - // } else { - // error(format!( - // "Tried to divert using a target from a variable that could not be found ({})", - // var_name - // )); - // } + let var_name = ¤t_divert.variable_divert_name; + if let Some(var_contents) = self.state.as_ref().unwrap().get_variables_state().get_variable_with_name(var_name.as_ref().unwrap(), -1) { + if let Some(target) = Value::get_divert_target_value(var_contents.as_ref()) { + self.state.as_mut().unwrap().set_diverted_pointer(Self::pointer_at_path(&self.main_content_container, target)); + println!("SET DIVERTED POINTER: {} PATH: {}", self.state.as_mut().unwrap().diverted_pointer, target); + } else { + // TODO + // let int_content = var_contents.downcast_ref::(); + // let error_message = format!( + // "Tried to divert to a target from a variable, but the variable ({}) didn't contain a divert target, it ", + // var_name + // ); + // let error_message = if let Some(int_content) = int_content { + // if int_content.value == 0 { + // format!("{}was empty/null (the value 0).", error_message) + // } else { + // format!("{}contained '{}'.", error_message, var_contents) + // } + // } else { + // error_message + // }; + + // error(error_message); + panic!(); + } + } else { + // TODO + // error(format!( + // "Tried to divert using a target from a variable that could not be found ({})", + // var_name + // )); + panic!(); + } } else if current_divert.is_external { //call_external_function(¤t_divert.get_target_path_string(), current_divert.get_external_args()); return true; } else { - self.state.as_mut().unwrap().set_diverted_pointer(current_divert.target_pointer.clone()); + self.state.as_mut().unwrap().set_diverted_pointer(current_divert.get_target_pointer()); } if current_divert.pushes_to_stack { - // self.state.as_ref().unwrap() - // .get_call_stack() - // .push(current_divert.stack_push_type, 0, state.get_output_stream().len()); - // + self.state.as_ref().unwrap() + .get_callstack().borrow_mut() + .push(current_divert.stack_push_type, 0, self.state.as_ref().unwrap().get_output_stream().len() as i32); } if self.state.as_ref().unwrap().diverted_pointer.is_null() && !current_divert.is_external { - // if let Some(source_name) = ¤t_divert.get_debug_metadata().source_name { - // error(format!("Divert target doesn't exist: {}", source_name)); - // } else { // error(format!("Divert resolution failed: {:?}", current_divert)); - // } } return true; @@ -803,7 +804,7 @@ impl Story { crate::control_command::CommandType::PopFunction => todo!(), crate::control_command::CommandType::PopTunnel => todo!(), crate::control_command::CommandType::BeginString => { - self.state.as_mut().unwrap().push_to_output_stream(Some(content_obj.clone())); + self.state.as_mut().unwrap().push_to_output_stream(content_obj.clone()); assert!(self.state.as_ref().unwrap().get_in_expression_evaluation(), "Expected to be in an expression when evaluating a string"); @@ -846,7 +847,7 @@ impl Story { // At the time of writing, this only applies to Tag objects generated // by choices, which are pushed to the stack during string generation. for rescued_tag in content_to_retain.iter() { - self.state.as_mut().unwrap().push_to_output_stream(Some(rescued_tag.clone())); + self.state.as_mut().unwrap().push_to_output_stream(rescued_tag.clone()); } // Build string out of the content we collected @@ -886,7 +887,7 @@ impl Story { self.state.as_ref().unwrap().set_current_pointer(pointer::NULL.clone()); } }, - crate::control_command::CommandType::End => todo!(), + crate::control_command::CommandType::End => self.state.as_mut().unwrap().force_end(), crate::control_command::CommandType::ListFromInt => todo!(), crate::control_command::CommandType::ListRange => todo!(), crate::control_command::CommandType::ListRandom => todo!(), @@ -896,6 +897,22 @@ impl Story { return true; } + // Variable assignment + if let Some(var_ass) = content_obj.as_ref().as_any().downcast_ref::() { + let assigned_val = self.state.as_mut().unwrap().pop_evaluation_stack(); + + // When in temporary evaluation, don't create new variables purely + // within + // the temporary context, but attempt to create them globally + // var prioritiseHigherInCallStack = _temporaryEvaluationContainer + // != null; + + self.state.as_mut().unwrap().get_variables_state_mut().assign( var_ass, assigned_val); + + return true; + } + + false } @@ -906,27 +923,25 @@ impl Story { self.state.as_mut().unwrap().set_previous_pointer(cp); // Divert step? + if !self.state.as_ref().unwrap().diverted_pointer.is_null() { + let dp = self.state.as_ref().unwrap().diverted_pointer.clone(); + self.state.as_mut().unwrap().set_current_pointer(dp); + self.state.as_mut().unwrap().set_diverted_pointer(pointer::NULL.clone()); + + // Internally uses state.previousContentObject and + // state.currentContentObject + self.visit_changed_containers_due_to_divert(); + + // Diverted location has valid content? + if !self.state.as_ref().unwrap().get_current_pointer().is_null() { + return; + } - // TODO - // if !self.state.as_ref().unwrap().get_diverted_pointer().is_null() { - - // self.state.as_mut().unwrap().setCurrentPointer(state.getDivertedPointer()); - // self.state.as_mut().unwrap().setDivertedPointer(Pointer.Null); - - // // Internally uses state.previousContentObject and - // // state.currentContentObject - // self.visitChangedContainersDueToDivert(); - - // // Diverted location has valid content? - // if !self.state.as_ref().unwrap().get_current_pointer().is_null() { - // 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 - // } + // 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 + } let successful_pointer_increment = self.increment_content_pointer(); @@ -936,7 +951,8 @@ impl Story { let mut didPop = false; - if self.state.as_ref().unwrap().get_callstack().as_ref().borrow().can_pop_type(PushPopType::Function) { + let can_pop_type = self.state.as_ref().unwrap().get_callstack().as_ref().borrow().can_pop_type(PushPopType::Function); + if can_pop_type { // Pop from the call stack self.state.as_mut().unwrap().pop_callstack(PushPopType::Function); @@ -946,7 +962,7 @@ impl Story { // so in this case, we make sure that the evaluator has // something to chomp on if it needs it if self.state.as_ref().unwrap().get_in_expression_evaluation() { - self.state.as_mut().unwrap().push_evaluation_stack(Void::new()); + self.state.as_mut().unwrap().push_evaluation_stack(Rc::new(Void::new())); } didPop = true; @@ -962,7 +978,7 @@ impl Story { if didPop && !self.state.as_ref().unwrap().get_current_pointer().is_null() { self.next_content(); } - } + } } fn increment_content_pointer(&self) -> bool { @@ -971,7 +987,7 @@ impl Story { let mut pointer = self.state.as_ref().unwrap().get_callstack().as_ref().borrow().get_current_element().current_pointer.clone(); pointer.index += 1; - let container= pointer.container.as_ref().unwrap().clone(); + let mut container= pointer.container.as_ref().unwrap().clone(); // Each time we step off the end, we fall out to the next container, all // the @@ -986,13 +1002,14 @@ impl Story { break; } - let container: Rc = container.clone(); - let index_in_ancestor = next_ancestor.as_ref().unwrap().content.iter().position(|s| Rc::ptr_eq(s, &container)); + let rto: Rc = container; + let index_in_ancestor = next_ancestor.as_ref().unwrap().content.iter().position(|s| Rc::ptr_eq(s, &rto)); if index_in_ancestor.is_none() { break; } pointer = Pointer::new(next_ancestor, index_in_ancestor.unwrap() as i32); + container= pointer.container.as_ref().unwrap().clone(); // Increment to next content in outer container pointer.index += 1; @@ -1057,17 +1074,15 @@ impl Story { fn is_truthy(&self, obj: Rc) -> bool { let truthy = false; + + if let Some(val) = obj.as_ref().as_any().downcast_ref::() { - if let Ok(val) = obj.into_any().downcast::() { - // TODO - - // 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; - // } + if let Some(_) = Value::get_divert_target_value(obj.as_ref()) { + // self.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.is_truthy(); } @@ -1116,7 +1131,7 @@ impl Story { start_text.push_str(&choice_only_text); - let choice = Rc::new(Choice::new(choice_point.get_path_on_choice(), Object::get_path(choice_point.clone()).to_string(), choice_point.is_invisible_default(), tags, self.state.as_ref().unwrap().get_callstack().borrow_mut().fork_thread(), start_text.trim().to_string(), 0, 0)); + let choice = Rc::new(Choice::new(choice_point.get_path_on_choice(), Object::get_path(choice_point.as_ref()).to_string(), choice_point.is_invisible_default(), tags, self.state.as_ref().unwrap().get_callstack().borrow_mut().fork_thread(), start_text.trim().to_string(), 0, 0)); Some(choice) } @@ -1254,6 +1269,5 @@ impl Story { } } - } diff --git a/src/story_state.rs b/src/story_state.rs index 8035db3..c68ed5c 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -304,6 +304,11 @@ impl StoryState { } pub fn set_current_pointer(&self, pointer: Pointer) { + if !pointer.container.is_none() && pointer.index >= pointer.container.as_ref().unwrap().content.len() as i32 { + panic!() + } + + println!("POINTER: {}", pointer.to_string()); self.get_callstack().as_ref().borrow_mut().get_current_element_mut().current_pointer = pointer; } @@ -344,13 +349,12 @@ impl StoryState { // } // } - println!("PUSH: {}", obj.to_string()); + println!("PUSH: {}", obj.as_ref()); self.evaluation_stack.push(obj); } - pub fn push_to_output_stream(&mut self, obj: Option>) { - let text = match &obj { - Some(obj) => { + pub fn push_to_output_stream(&mut self, obj: Rc) { + let text = { let obj = obj.clone(); match obj.into_any().downcast::() { Ok(v) => match &v.value { @@ -359,8 +363,6 @@ impl StoryState { }, Err(_) => None, } - }, - None => None, }; if let Some(s) = text { @@ -375,7 +377,7 @@ impl StoryState { } } - self.push_to_output_stream_individual(obj.unwrap()); + self.push_to_output_stream_individual(obj); } pub fn increment_visit_count_for_container(&mut self, container: &Rc) { @@ -669,12 +671,21 @@ impl StoryState { self.get_callstack().as_ref().borrow_mut().get_current_thread_mut().previous_pointer.clone() } - pub fn try_exit_function_evaluation_from_game(&self) { - todo!() + pub fn try_exit_function_evaluation_from_game(&mut self) -> bool { + if self.get_callstack().borrow().get_current_element().push_pop_type == PushPopType::FunctionEvaluationFromGame { + self.set_current_pointer(pointer::NULL.clone()); + self.did_safe_exit = true; + return true; + } + + return false; } - pub fn pop_callstack(&self, function: PushPopType) { - todo!() + pub fn pop_callstack(&self, t: PushPopType) { + // Add the end of a function call, trim any whitespace from the end. + if self.get_callstack().borrow().get_current_element().push_pop_type == PushPopType::Function {self.trim_whitespace_from_function_end();} + + self.get_callstack().borrow_mut().pop_type(t); } fn go_to_start(&self) { @@ -730,12 +741,10 @@ impl StoryState { // 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) - - //TODO - // copy.variables_state = self.variables_state.; - // copy.variablesState.setCallStack(copy.getCallStack()); - // copy.variablesState.setPatch(copy.patch); + // (though the callstack will be modified) + copy.variables_state = self.variables_state.clone(); + copy.variables_state.set_callstack(copy.get_callstack().clone()); + copy.variables_state.patch = copy.patch.clone(); copy.evaluation_stack = self.evaluation_stack.clone(); @@ -775,21 +784,23 @@ impl StoryState { self.variables_state.apply_patch(); - if let Some(patch) = &self.patch { - for (container, count) in &patch.visit_counts { - self.apply_count_changes(container.clone(), *count, true); + if self.patch.is_some() { + for (path, count) in self.patch.as_ref().unwrap().visit_counts.clone().iter() { + self.apply_count_changes(path, *count, true); } - - for (container, index) in &patch.turn_indices { - self.apply_count_changes(container.clone(), *index, false); + + for (path, index) in self.patch.as_ref().unwrap().turn_indices.clone().iter() { + self.apply_count_changes(path, *index, false); } } self.patch = None; } - fn apply_count_changes(&self, clone: String, count: usize, arg: bool) { - todo!() + fn apply_count_changes(&mut self, container: &str, new_count: usize, is_visit: bool) { + let counts = if is_visit {&mut self.visit_counts} else {&mut self.turn_indices}; + + counts.insert(container.to_string(), new_count); } pub fn pop_from_output_stream(&mut self, count: usize) { @@ -832,4 +843,23 @@ impl StoryState { } } + pub(crate) fn force_end(&mut self) { + self.get_callstack().borrow_mut().reset(); + + self.current_flow.current_choices.clear(); + + self.set_current_pointer(pointer::NULL.clone()); + self.set_previous_pointer(pointer::NULL.clone()); + + self.set_did_safe_exit(true); + } + + // At 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. + fn trim_whitespace_from_function_end(&self) { + todo!() + } + } \ No newline at end of file diff --git a/src/value.rs b/src/value.rs index 1724377..1ff76b5 100644 --- a/src/value.rs +++ b/src/value.rs @@ -2,7 +2,7 @@ use std::{fmt}; -use crate::{object::{RTObject, Object}, path::Path}; +use crate::{object::{RTObject, Object}, path::Path, divert::Divert}; #[repr(i8)] pub enum ValueType { @@ -14,7 +14,7 @@ pub enum ValueType { // Not used for coersion described above DivertTarget(Path), - //VariablePointer, + VariablePointer(VariablePointerValue), } @@ -32,6 +32,17 @@ impl StringValue { } +#[derive(Clone)] +pub struct VariablePointerValue { + pub variable_name: String, + + // 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) + pub context_index: i32, +} + pub struct Value { obj: Object, pub value: ValueType, @@ -50,7 +61,8 @@ impl fmt::Display for Value { ValueType::Int(v) => write!(f, "{}", v), ValueType::Float(v) => write!(f, "{}", v), ValueType::String(v) => write!(f, "{}", v.string), - ValueType::DivertTarget(p) => write!(f, "DivertTargetValue({})", p.to_string()), + ValueType::DivertTarget(p) => write!(f, "DivertTargetValue({})", p), + ValueType::VariablePointer(v) => write!(f, "VariablePointerValue({})", v.variable_name), } } } @@ -92,13 +104,18 @@ impl Value { Value { obj: Object::new(), value: ValueType::DivertTarget(p) } } + pub fn new_variable_pointer(variable_name: &str, context_index: i32) -> Value { + Value { obj: Object::new(), value: ValueType::VariablePointer(VariablePointerValue { variable_name: variable_name.to_string(), context_index }) } + } + pub fn is_truthy(&self) -> bool { match &self.value { ValueType::Bool(v) => *v, ValueType::Int(v) => *v != 0, ValueType::Float(v) => *v != 0.0, ValueType::String(v) => v.string.len() > 0, - ValueType::DivertTarget(_) => false, // exception Shouldn't be checking the truthiness of a divert target?? + ValueType::DivertTarget(_) => panic!(), // exception Shouldn't be checking the truthiness of a divert target?? + ValueType::VariablePointer(_) => panic!(), // exception Shouldn't be checking the truthiness of a divert target?? } } @@ -111,4 +128,24 @@ impl Value { None => None, } } + + pub fn get_variable_pointer_value(o: &dyn RTObject) -> Option<&VariablePointerValue> { + match o.as_any().downcast_ref::() { + Some(v) => match &v.value { + ValueType::VariablePointer(v) => Some(v), + _ => None, + }, + None => None, + } + } + + pub fn get_divert_target_value(o: &dyn RTObject) -> Option<&Path> { + match o.as_any().downcast_ref::() { + Some(v) => match &v.value { + ValueType::DivertTarget(p) => Some(p), + _ => None, + }, + None => None, + } + } } \ No newline at end of file diff --git a/src/variable_assigment.rs b/src/variable_assigment.rs new file mode 100644 index 0000000..3638abe --- /dev/null +++ b/src/variable_assigment.rs @@ -0,0 +1,33 @@ +use std::fmt; + +use crate::object::{Object, RTObject}; + +pub struct VariableAssignment { + obj: Object, + pub is_global: bool, + pub is_new_declaration: bool, + pub variable_name: String, +} + +impl VariableAssignment { + pub fn new(variable_name: &str, is_new_declaration: bool, is_global: bool) -> Self { + VariableAssignment { + obj: Object::new(), + is_global, + is_new_declaration, + variable_name: variable_name.to_string(), + } + } +} + +impl RTObject for VariableAssignment { + fn get_object(&self) -> &Object { + &self.obj + } +} + +impl fmt::Display for VariableAssignment { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "VarAssign to {}", self.variable_name) + } +} \ No newline at end of file diff --git a/src/variables_state.rs b/src/variables_state.rs index 1790c0c..01d78e3 100644 --- a/src/variables_state.rs +++ b/src/variables_state.rs @@ -1,7 +1,9 @@ use std::{collections::{HashMap, HashSet}, rc::Rc, cell::RefCell}; -use crate::{object::RTObject, callstack::CallStack, state_patch::StatePatch}; +use crate::{object::RTObject, callstack::CallStack, state_patch::StatePatch, variable_assigment::VariableAssignment, value::{Value, VariablePointerValue}}; + +#[derive(Clone)] pub struct VariablesState { pub global_variables: HashMap>, pub default_global_variables: Option>>, @@ -50,7 +52,178 @@ impl VariablesState { self.default_global_variables = Some(self.global_variables.clone()); } - pub fn apply_patch(&self) { + pub fn apply_patch(&mut self) { + for (name, value) in self.patch.as_ref().unwrap().globals.iter() { + self.global_variables.insert(name.clone(), value.clone()); + } + + if let Some(changed_variables) = &mut self.changed_variables_for_batch_obs { + for name in self.patch.as_ref().unwrap().changed_variables.iter() { + changed_variables.insert(name.clone()); + } + } + + self.patch = None; + } + + pub fn assign ( + &self, + var_ass: &VariableAssignment, + value: Rc, + ) { + let mut name = var_ass.variable_name.to_string(); + let mut context_index = -1; + let mut set_global = false; + + // Are we assigning to a global variable? + if var_ass.is_new_declaration { + set_global = var_ass.is_global; + } else { + set_global = self.global_variable_exists_with_name(&name); + } + + let mut value = value; + // Constructing new variable pointer reference + if var_ass.is_new_declaration { + if let Some(var_pointer) = Value::get_variable_pointer_value(value.as_ref()){ + let fully_resolved_variable_pointer = + self.resolve_variable_pointer(var_pointer); + value = fully_resolved_variable_pointer; + } + } else { + // Assign to an existing variable pointer + // Then assign to the variable that the pointer is pointing to by name. + // De-reference variable reference to point to + loop { + let existing_pointer = self.get_raw_variable_with_name(&name, context_index); + + match existing_pointer { + Some(existing_pointer) => match Value::get_variable_pointer_value(existing_pointer.as_ref()) { + Some(pv) => { + name = pv.variable_name.to_string(); + context_index =pv.context_index; + set_global = context_index == 0; + }, + None => break, + }, + None => break, + } + } + } + + if set_global { + self.set_global(&name, value); + } else { + self.callstack.borrow_mut().set_temporary_variable(name, value, var_ass.is_new_declaration, context_index); + } + } + + fn global_variable_exists_with_name(&self, name: &str) -> bool { + self.global_variables.contains_key(name) + || self + .default_global_variables + .as_ref() + .map(|variables| variables.contains_key(name)) + .unwrap_or(false) + } + + // 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. + fn resolve_variable_pointer(&self, var_pointer: &VariablePointerValue) -> Rc { + let mut context_index = var_pointer.context_index; + if context_index == -1 { + context_index = self.get_context_index_of_variable_named(&var_pointer.variable_name); + } + + let value_of_variable_pointed_to = self.get_raw_variable_with_name(&var_pointer.variable_name, context_index); + // 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. + if let Some(value_of_variable_pointed_to) = value_of_variable_pointed_to { + if let Some(double_redirection_pointer) = Value::get_variable_pointer_value(value_of_variable_pointed_to.as_ref()) { + return value_of_variable_pointed_to; + } + } + + Rc::new(Value::new_variable_pointer(&var_pointer.variable_name, context_index)) + } + + // 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 + fn get_context_index_of_variable_named(&self, var_name: &str) -> i32 { + if self.global_variable_exists_with_name(var_name) { + return 0; + } + + return self.callstack.borrow().get_current_element_index(); + } + + fn get_raw_variable_with_name(&self, name: &str, context_index: i32) -> Option> { + // 0 context = global + if context_index == 0 || context_index == -1 { + if let Some(patch) = &self.patch { + if let Some(global) = patch.get_global(name) { + return Some(global); + } + } + + if let Some(global) = self.global_variables.get(name) { + return Some(global.clone()); + } + + // Getting variables can actually happen during globals set up since you can do + // VAR x = A_LIST_ITEM + // So _default_global_variables may be None. + // 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 let Some(default_globals) = &self.default_global_variables { + if let Some(default_global) = default_globals.get(name) { + return Some(default_global.clone()); + } + } + + //TODO + // if let Some(list_item_value) = self.list_defs_origin.find_single_item_list_with_name(name) { + // return Some(list_item_value.clone()); + // } + } + + // Temporary + let var_value = self.callstack.borrow().get_temporary_variable_with_name(name, context_index); + + var_value + } + + fn set_global(&self, name: &str, value: Rc) { todo!() } + + pub fn get_variable_with_name(&self, name: &str, context_index: i32) -> Option> { + let var_value = self.get_raw_variable_with_name(name, context_index); + // Get value from pointer? + if let Some(vv) = var_value.clone() { + if let Some(var_pointer) = Value::get_variable_pointer_value(vv.as_ref()) { + return self.value_at_variable_pointer(var_pointer); + } + } + + var_value + } + + fn value_at_variable_pointer(&self, pointer: &VariablePointerValue) -> Option> { + self.get_variable_with_name(&pointer.variable_name, pointer.context_index) + } + + pub fn set_callstack(&mut self, callstack: Rc>) { + self.callstack = callstack; + } } diff --git a/src/void.rs b/src/void.rs index 4fde905..4e868b4 100644 --- a/src/void.rs +++ b/src/void.rs @@ -10,8 +10,8 @@ pub struct Void { } impl Void { - pub fn new() -> Rc { - Rc::new(Void {obj: Object::new()}) + pub fn new() -> Self { + Void {obj: Object::new()} } } diff --git a/tests/basic_text_test.rs b/tests/basic_text_test.rs index 6c6a841..845e0b3 100644 --- a/tests/basic_text_test.rs +++ b/tests/basic_text_test.rs @@ -1,7 +1,7 @@ use std::fs; use bladeink::story::Story; -mod test_utils; +mod common; #[test] fn oneline_test() -> Result<(), String> { @@ -27,7 +27,7 @@ fn twolines_test() -> Result<(), String> { println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); - test_utils::next_all(&mut story, &mut text)?; + common::next_all(&mut story, &mut text)?; assert_eq!(2, text.len()); assert_eq!("Line.", text[0]); assert_eq!("Other line.", text[1]); diff --git a/tests/choice_test.rs b/tests/choice_test.rs index 1608a00..af5c822 100644 --- a/tests/choice_test.rs +++ b/tests/choice_test.rs @@ -1,14 +1,291 @@ +use bladeink::story::Story; -mod test_utils; + +mod common; #[test] fn no_choice_test() -> Result<(), String> { let mut errors:Vec = Vec::new(); - let text = test_utils::run_story("examples/inkfiles/choices/no-choice-text.ink.json", None, &mut errors)?; + let text = common::run_story("examples/inkfiles/choices/no-choice-text.ink.json", None, &mut errors)?; + + assert_eq!(0, errors.len()); + assert_eq!("Hello world!\nHello back!\n", common::join_text(&text)); + + Ok(()) +} + +#[test] +fn one_test() -> Result<(), String> { + let mut errors:Vec = Vec::new(); + + let text = common::run_story("examples/inkfiles/choices/one.ink.json", None, &mut errors)?; assert_eq!(0, errors.len()); - assert_eq!("Hello world!\nHello back!\n", test_utils::join_text(&text)); + assert_eq!("Hello world!\nHello back!\nHello back!\n", common::join_text(&text)); + + Ok(()) +} + +#[test] +fn multi_choice_test() -> Result<(), String> { + let mut errors:Vec = Vec::new(); + + let text = common::run_story("examples/inkfiles/choices/multi-choice.ink.json", Some(vec![0]), &mut errors)?; + + assert_eq!(0, errors.len()); + assert_eq!("Hello, world!\nHello back!\nGoodbye\nHello back!\nNice to hear from you\n", common::join_text(&text)); + + // Select second choice + let text = common::run_story("examples/inkfiles/choices/multi-choice.ink.json", Some(vec![1]), &mut errors)?; + + assert_eq!(0, errors.len()); + assert_eq!("Hello, world!\nHello back!\nGoodbye\nGoodbye\nSee you later\n", common::join_text(&text)); + + Ok(()) +} + +#[test] +fn single_choice1_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/choices/single-choice.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("Hello, world!", text[0]); + + Ok(()) +} + +#[test] +fn single_choic2_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/choices/single-choice.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(0); + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(2, text.len()); + assert_eq!("Hello back!", text[0]); + assert_eq!("Nice to hear from you", text[1]); + + Ok(()) +} + +#[test] +fn suppress_choice_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/choices/suppress-choice.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + assert_eq!("Hello back!", story.get_current_choices().get(0).unwrap().text); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("Nice to hear from you.", text[0]); + + + Ok(()) +} + +#[test] +fn mixed_choice_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/choices/mixed-choice.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + assert_eq!("Hello back!", story.get_current_choices().get(0).unwrap().text); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(2, text.len()); + assert_eq!("Hello right back to you!", text[0]); + assert_eq!("Nice to hear from you.", text[1]); + + + Ok(()) +} + +#[test] +fn varying_choice_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/choices/varying-choice.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + assert_eq!(2, story.get_current_choices().len()); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, story.get_current_choices().len()); + assert_eq!("The man with the briefcase?", story.get_current_choices()[0].text); + + + Ok(()) +} + +#[test] +fn sticky_choice_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/choices/sticky-choice.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + assert_eq!(2, story.get_current_choices().len()); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(2, story.get_current_choices().len()); + + Ok(()) +} + +#[test] +fn fallback_choice_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/choices/fallback-choice.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + assert_eq!(2, story.get_current_choices().len()); + + Ok(()) +} + +#[test] +fn fallback_choice2_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/choices/fallback-choice.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + assert_eq!(2, story.get_current_choices().len()); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(true, common::is_ended(&story)); + + Ok(()) +} + +#[test] +fn conditional_choice_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/choices/conditional-choice.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + assert_eq!(4, story.get_current_choices().len()); + + Ok(()) +} + +#[test] +fn label_flow_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/choices/label-flow.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + assert_eq!(2, story.get_current_choices().len()); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(2, story.get_current_choices().len()); + assert_eq!("\'Having a nice day?\'",story.get_current_choices()[0].text); Ok(()) } + +#[test] +fn label_flow2_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/choices/label-flow.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + assert_eq!(2, story.get_current_choices().len()); + story.choose_choice_index(1); + + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(2, story.get_current_choices().len()); + assert_eq!("Shove him aside",story.get_current_choices()[0].text); + + Ok(()) +} + +#[test] +fn label_scope_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/choices/label-scope.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, story.get_current_choices().len()); + assert_eq!("Found gatherpoint",story.get_current_choices()[0].text); + + Ok(()) +} + +#[test] +fn divert_choice_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/choices/divert-choice.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + assert_eq!(2, story.get_current_choices().len()); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(2, text.len()); + assert_eq!("You pull a face, and the soldier comes at you! You shove the guard to one side, but he comes back swinging.", text[0]); + + assert_eq!(1, story.get_current_choices().len()); + assert_eq!("Grapple and fight",story.get_current_choices()[0].text); + + Ok(()) +} \ No newline at end of file diff --git a/tests/test_utils.rs b/tests/common/mod.rs similarity index 93% rename from tests/test_utils.rs rename to tests/common/mod.rs index 6dca322..29cd794 100644 --- a/tests/test_utils.rs +++ b/tests/common/mod.rs @@ -48,6 +48,9 @@ pub fn run_story( let mut rng = rand::thread_rng(); while story.can_continue() || !story.get_current_choices().is_empty() { + + println!("{}", story.build_string_of_hierarchy()); + // 2) Game content, line by line while story.can_continue() { let line = story.cont()?; @@ -95,3 +98,7 @@ pub fn get_json_string(filename: &str) -> Result> { let json = fs::read_to_string(path)?; Ok(json) } + +pub fn is_ended(story: &Story) -> bool { + return !story.can_continue() && story.get_current_choices().is_empty(); +} diff --git a/tests/divert_test.rs b/tests/divert_test.rs new file mode 100644 index 0000000..9170bbe --- /dev/null +++ b/tests/divert_test.rs @@ -0,0 +1,19 @@ +use bladeink::story::Story; + +mod common; + +#[test] +fn simple_divert_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/divert/simple-divert.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + + let mut text: Vec = Vec::new(); + common::next_all(&mut story, &mut text)?; + assert_eq!(2, text.len()); + assert_eq!("We arrived into London at 9.45pm exactly.", text[0]); + assert_eq!("We hurried home to Savile Row as fast as we could.", text[1]); + + Ok(()) +} diff --git a/tests/glue_test.rs b/tests/glue_test.rs index add55a2..ed73951 100644 --- a/tests/glue_test.rs +++ b/tests/glue_test.rs @@ -1,16 +1,16 @@ use bladeink::story::Story; -mod test_utils; +mod common; #[test] fn simple_glue_test() -> Result<(), String> { let json_string = - test_utils::get_json_string("examples/inkfiles/glue/simple-glue.ink.json").unwrap(); + common::get_json_string("examples/inkfiles/glue/simple-glue.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); - test_utils::next_all(&mut story, &mut text)?; + common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("Some content with glue.", text[0]); From bcb3bfa44eb6dea7785c82d4fc2cc0d05e481344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Fri, 22 Sep 2023 20:57:48 +0000 Subject: [PATCH 20/91] Added GHA for ci + conditional test --- .devcontainer/Dockerfile | 2 +- .github/workflows/ci.yml | 25 ++++++++++++++++++ README.md | 16 ++++++++---- src/choice_point.rs | 2 +- src/json_serialization.rs | 24 ++++++++--------- src/lib.rs | 2 +- src/object.rs | 6 ++--- src/object_enum.rs | 7 ----- src/search_result.rs | 9 +++++++ src/variable_reference.rs | 54 +++++++++++++++++++++++++++++++++++++++ tests/conditional_test.rs | 19 ++++++++++++++ 11 files changed, 136 insertions(+), 30 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 src/object_enum.rs create mode 100644 src/variable_reference.rs create mode 100644 tests/conditional_test.rs diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 0629fa7..dd891ca 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -3,7 +3,7 @@ FROM buildpack-deps:bookworm ENV RUSTUP_HOME=/usr/local/rustup \ CARGO_HOME=/usr/local/cargo \ PATH=/usr/local/cargo/bin:$PATH \ - RUST_VERSION=1.72.0 + RUST_VERSION=1.72.1 RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ # Remove imagemagick due to https://security-tracker.debian.org/tracker/CVE-2019-10131 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b41ed50 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: Rust CI + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + + - name: Build + run: cargo build --verbose + - name: Test + run: cargo test --verbose diff --git a/README.md b/README.md index 0e46bb2..e4c0013 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,14 @@ Inkle Ink runtime implementation in Rust Currently under development. This is the implementation status: -- [ ] Loading .json file -- [ ] Show plain lines (no logic nor choices) -- [ ] Choices +- [x] Loading .json file +- [x] Show plain lines (no logic nor choices) +- [x] Choices - [ ] Knots and Stitches -- [ ] Diverts +- [x] Diverts - [ ] Variable Text - [ ] Conditional Text - [ ] Game Queries and Functions -- [ ] Diverts - [ ] Nested flows - [ ] Variables and Logic - [ ] Conditional blocks (if/else) @@ -22,4 +21,11 @@ Currently under development. This is the implementation status: - [ ] Threads - [ ] Lists +## TODO + +- [ ] Use OnceCell to lazy init the cache fields of RTObjects +- [ ] Error handling +- [ ] Split large files. ex. Get the error handling out of the Story class. The performLogic + + diff --git a/src/choice_point.rs b/src/choice_point.rs index 8cdcbf0..4ca02ef 100644 --- a/src/choice_point.rs +++ b/src/choice_point.rs @@ -95,7 +95,7 @@ impl ChoicePoint { } pub fn get_path_string_on_choice(self: &Rc) -> String { - Object::compact_path_string(self.clone(), self.get_path_on_choice()) + Object::compact_path_string(self.clone(), &self.get_path_on_choice()) } } diff --git a/src/json_serialization.rs b/src/json_serialization.rs index 5ce7cf3..5cf0439 100644 --- a/src/json_serialization.rs +++ b/src/json_serialization.rs @@ -4,7 +4,7 @@ use serde_json::Map; use crate::{ container::Container, - object::{self, RTObject}, control_command::{CommandType, ControlCommand}, value::Value, glue::Glue, path::Path, choice_point::ChoicePoint, choice::Choice, push_pop::PushPopType, divert::Divert, variable_assigment::VariableAssignment, void::Void, + object::{self, RTObject}, control_command::{CommandType, ControlCommand}, value::Value, glue::Glue, path::Path, choice_point::ChoicePoint, choice::Choice, push_pop::PushPopType, divert::Divert, variable_assigment::VariableAssignment, void::Void, variable_reference::VariableReference, }; pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) -> Result, String> { @@ -158,17 +158,17 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) } // // Variable reference - // prop_value = obj.get("VAR?"); - // if (prop_value != null) { - // return new VariableReference(prop_value.toString()); - // } else { - // prop_value = obj.get("CNT?"); - // if (prop_value != null) { - // VariableReference readCountVarRef = new VariableReference(); - // readCountVarRef.setPathStringForCount(prop_value.toString()); - // return readCountVarRef; - // } - // } + let prop_value = obj.get("VAR?"); + if let Some(name) = prop_value { + return Ok(Rc::new(VariableReference::new(name.as_str().unwrap()))); + } + + let prop_value = obj.get("CNT?"); + if let Some(v) = prop_value { + return Ok(Rc::new(VariableReference::from_path_for_count(v.as_str().unwrap()))); + } + + // // Variable assignment let mut is_var_ass = false; let mut is_global_var = false; diff --git a/src/lib.rs b/src/lib.rs index 52c0100..98a6728 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,6 @@ mod pointer; mod path; mod search_result; mod callstack; -mod object_enum; mod flow; mod push_pop; mod variables_state; @@ -22,5 +21,6 @@ mod choice_point; mod tag; mod divert; mod variable_assigment; +mod variable_reference; diff --git a/src/object.rs b/src/object.rs index 9dfc6c7..c9878cd 100644 --- a/src/object.rs +++ b/src/object.rs @@ -143,15 +143,15 @@ impl Object { Path::new(&new_path_comps, true) } - pub fn compact_path_string(rtobject: Rc, other_path: Path) -> String { + pub fn compact_path_string(rtobject: Rc, other_path: &Path) -> String { let global_path_str: String; let relative_path_str: String; if other_path.is_relative() { relative_path_str = other_path.get_components_string(); - global_path_str = Object::get_path(rtobject.as_ref()).path_by_appending_path(&other_path).get_components_string(); + global_path_str = Object::get_path(rtobject.as_ref()).path_by_appending_path(other_path).get_components_string(); } else { - let relative_path = Object::convert_path_to_relative(&rtobject, &other_path); + let relative_path = Object::convert_path_to_relative(&rtobject, other_path); relative_path_str = relative_path.get_components_string(); global_path_str = other_path.get_components_string(); } diff --git a/src/object_enum.rs b/src/object_enum.rs deleted file mode 100644 index 56eb35f..0000000 --- a/src/object_enum.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[derive(Clone)] -pub enum ObjectEnum { - Value, - Container, - ControlCommand, - Null -} \ No newline at end of file diff --git a/src/search_result.rs b/src/search_result.rs index b036144..6b02bc4 100644 --- a/src/search_result.rs +++ b/src/search_result.rs @@ -38,4 +38,13 @@ impl SearchResult { Err(_) => None, } } + + pub fn container(&self) -> Option> { + let c = self.obj.clone().into_any().downcast::(); + + match c { + Ok(c) => Some(c), + Err(_) => None, + } + } } \ No newline at end of file diff --git a/src/variable_reference.rs b/src/variable_reference.rs new file mode 100644 index 0000000..cb9e094 --- /dev/null +++ b/src/variable_reference.rs @@ -0,0 +1,54 @@ +use std::{fmt, rc::Rc}; + +use crate::{object::{Object, RTObject}, path::Path, container::Container}; + + +pub struct VariableReference { + obj: Object, + name: String, + path_for_count: Option, +} + +impl VariableReference { + pub fn new(name: &str) -> Self { + VariableReference {obj: Object::new(), name: name.to_string(), path_for_count: None} + } + + pub fn from_path_for_count(path_for_count: &str) -> Self { + VariableReference {obj: Object::new(), name: String::new(), path_for_count: Some(Path::new_with_components_string(Some(path_for_count)))} + } + + pub fn get_container_for_count(self: &Rc) -> Result, String> { + if let Some(path) = &self.path_for_count { + Ok(Object::resolve_path(self.clone(), path).container().unwrap()) + } else { + Err("Path for count is not set.".to_string()) + } + } + + pub fn get_path_string_for_count(self: Rc) -> Option { + if let Some(path_for_count) = &self.path_for_count { + Some(Object::compact_path_string(self.clone(), path_for_count)) + } else { + None + } + } +} + +impl RTObject for VariableReference { + fn get_object(&self) -> &Object { + &self.obj + } +} + +impl fmt::Display for VariableReference { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.name { + name if !name.is_empty() => write!(f, "var({})", name), + _ => match &self.path_for_count { + Some(path) => write!(f, "read_count({})", &path.to_string()), // TODO needs an RC path.compact_path_string(path)), + None => write!(f, "read_count(null)"), + }, + } + } +} \ No newline at end of file diff --git a/tests/conditional_test.rs b/tests/conditional_test.rs new file mode 100644 index 0000000..6a8da18 --- /dev/null +++ b/tests/conditional_test.rs @@ -0,0 +1,19 @@ +use bladeink::story::Story; + +mod common; + +#[test] +fn iftrue_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/conditional/iftrue.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + + let mut text: Vec = Vec::new(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The value is 1.", text[0]); + + Ok(()) +} From afbf6b840762a8b858409b72229294ce9ef1ae16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Sun, 24 Sep 2023 11:35:29 +0000 Subject: [PATCH 21/91] Complete 'And' NativeFunctionCall + conditional_choice test works --- src/control_command.rs | 34 +++++- src/json_serialization.rs | 52 ++------ src/lib.rs | 1 + src/native_function_call.rs | 229 ++++++++++++++++++++++++++++++++++++ src/story.rs | 22 +++- src/story_state.rs | 12 +- src/value.rs | 91 ++++++++++++-- 7 files changed, 378 insertions(+), 63 deletions(-) create mode 100644 src/native_function_call.rs diff --git a/src/control_command.rs b/src/control_command.rs index 9cfa4ca..905e748 100644 --- a/src/control_command.rs +++ b/src/control_command.rs @@ -42,8 +42,40 @@ pub struct ControlCommand { } impl ControlCommand { + pub fn new_from_name(name: &str) -> Option { + match name { + "ev" => Some(Self::new(CommandType::EvalStart)), + "out" => Some(Self::new(CommandType::EvalOutput)), + "/ev" => Some(Self::new(CommandType::EvalEnd)), + "du" => Some(Self::new(CommandType::Duplicate)), + "pop" => Some(Self::new(CommandType::PopEvaluatedValue)), + "~ret" => Some(Self::new(CommandType::PopFunction)), + "->->" => Some(Self::new(CommandType::PopTunnel)), + "str" => Some(Self::new(CommandType::BeginString)), + "/str" => Some(Self::new(CommandType::EndString)), + "nop" => Some(Self::new(CommandType::NoOp)), + "choiceCnt" => Some(Self::new(CommandType::ChoiceCount)), + "turn" => Some(Self::new(CommandType::Turns)), + "turns" => Some(Self::new(CommandType::TurnsSince)), + "readc" => Some(Self::new(CommandType::ReadCount)), + "rnd" => Some(Self::new(CommandType::Random)), + "srnd" => Some(Self::new(CommandType::SeedRandom)), + "visit" => Some(Self::new(CommandType::VisitIndex)), + "seq" => Some(Self::new(CommandType::SequenceShuffleIndex)), + "thread" => Some(Self::new(CommandType::StartThread)), + "done" => Some(Self::new(CommandType::Done)), + "end" => Some(Self::new(CommandType::End)), + "listInt" => Some(Self::new(CommandType::ListFromInt)), + "range" => Some(Self::new(CommandType::ListRange)), + "lrnd" => Some(Self::new(CommandType::ListRandom,)), + "#" => Some(Self::new(CommandType::BeginTag)), + "/#" => Some(Self::new(CommandType::EndTag)), + _ => None, + } + } + pub fn new(command_type: CommandType) -> Self { - ControlCommand {obj: Object::new(), command_type} + Self {obj: Object::new(), command_type} } } diff --git a/src/json_serialization.rs b/src/json_serialization.rs index 5cf0439..9f2eb5b 100644 --- a/src/json_serialization.rs +++ b/src/json_serialization.rs @@ -4,7 +4,7 @@ use serde_json::Map; use crate::{ container::Container, - object::{self, RTObject}, control_command::{CommandType, ControlCommand}, value::Value, glue::Glue, path::Path, choice_point::ChoicePoint, choice::Choice, push_pop::PushPopType, divert::Divert, variable_assigment::VariableAssignment, void::Void, variable_reference::VariableReference, + object::{self, RTObject}, control_command::{CommandType, ControlCommand}, value::Value, glue::Glue, path::Path, choice_point::ChoicePoint, choice::Choice, push_pop::PushPopType, divert::Divert, variable_assigment::VariableAssignment, void::Void, variable_reference::VariableReference, native_function_call::{self, NativeFunctionCall}, }; pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) -> Result, String> { @@ -22,7 +22,7 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) }, serde_json::Value::String(value) => { - let str = value; + let str = value.as_str(); // String value let first_char = str.chars().next().unwrap(); if first_char == '^' {return Ok(Rc::new(Value::new_string(&str[1..])));} @@ -33,23 +33,19 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) return Ok(Rc::new(Glue::new())); } - if let Some(control_command) = create_control_command(str) { + if let Some(control_command) = ControlCommand::new_from_name(str) { return Ok(Rc::new(control_command)); } - /* TODO - // 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^".eq(str)) {str = "^";} - if NativeFunctionCall.callExistsWithName(str) {return NativeFunctionCall.callWithName(str);} - - // Pop - if ("->->".eq(str)) {return CommandType.popTunnel();} - else if ("~ret".eq(str)) {return CommandType.popFunction();} - */ + let mut call_str = str; + if "L^".eq(str) {call_str = &"^";} + if let Some(native_function_call) = NativeFunctionCall::new_from_name(call_str) { + return Ok(Rc::new(native_function_call)); + } // Void if "void".eq(str) {return Ok(Rc::new(Void::new()));} @@ -290,38 +286,6 @@ fn jarray_to_runtime_obj_list(jarray: &Vec, skip_last: bool) Ok(list) } -fn create_control_command(name: &str) -> Option { - match name { - "ev" => Some(ControlCommand::new(CommandType::EvalStart)), - "out" => Some(ControlCommand::new(CommandType::EvalOutput)), - "/ev" => Some(ControlCommand::new(CommandType::EvalEnd)), - "du" => Some(ControlCommand::new(CommandType::Duplicate)), - "pop" => Some(ControlCommand::new(CommandType::PopEvaluatedValue)), - "~ret" => Some(ControlCommand::new(CommandType::PopFunction)), - "->->" => Some(ControlCommand::new(CommandType::PopTunnel)), - "str" => Some(ControlCommand::new(CommandType::BeginString)), - "/str" => Some(ControlCommand::new(CommandType::EndString)), - "nop" => Some(ControlCommand::new(CommandType::NoOp)), - "choiceCnt" => Some(ControlCommand::new(CommandType::ChoiceCount)), - "turn" => Some(ControlCommand::new(CommandType::Turns)), - "turns" => Some(ControlCommand::new(CommandType::TurnsSince)), - "readc" => Some(ControlCommand::new(CommandType::ReadCount)), - "rnd" => Some(ControlCommand::new(CommandType::Random)), - "srnd" => Some(ControlCommand::new(CommandType::SeedRandom)), - "visit" => Some(ControlCommand::new(CommandType::VisitIndex)), - "seq" => Some(ControlCommand::new(CommandType::SequenceShuffleIndex)), - "thread" => Some(ControlCommand::new(CommandType::StartThread)), - "done" => Some(ControlCommand::new(CommandType::Done)), - "end" => Some(ControlCommand::new(CommandType::End)), - "listInt" => Some(ControlCommand::new(CommandType::ListFromInt)), - "range" => Some(ControlCommand::new(CommandType::ListRange)), - "lrnd" => Some(ControlCommand::new(CommandType::ListRandom,)), - "#" => Some(ControlCommand::new(CommandType::BeginTag)), - "/#" => Some(ControlCommand::new(CommandType::EndTag)), - _ => None, - } -} - fn jobject_to_choice(obj: &Map) -> Result, String> { let text = obj.get("text").unwrap().as_str().unwrap(); let index = obj.get("index").unwrap().as_u64().unwrap() as usize; diff --git a/src/lib.rs b/src/lib.rs index 98a6728..bda97d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,5 +22,6 @@ mod tag; mod divert; mod variable_assigment; mod variable_reference; +mod native_function_call; diff --git a/src/native_function_call.rs b/src/native_function_call.rs new file mode 100644 index 0000000..c702c7d --- /dev/null +++ b/src/native_function_call.rs @@ -0,0 +1,229 @@ +use std::{fmt, collections::HashMap, rc::Rc}; + +use crate::{object::{Object, RTObject}, value::{Value, ValueType}}; + +#[derive(Debug)] +pub enum Op { + Add, + Subtract, + Divide, + Multiply, + Mod, + Negate, + + Equal, + Greater, + Less, + GreaterThanOrEquals, + LessThanOrEquals, + NotEquals, + Not, + + And, + Or, + + Min, + Max, + + Pow, + Floor, + Ceiling, + Int, + Float, + + Has, + Hasnt, + Intersect, + + ListMin, + ListMax, + All, + Count, + ValueOfList, + Invert, +} + +pub struct NativeFunctionCall { + obj: Object, + op: Op, + number_of_parameters: i32, +} + +impl NativeFunctionCall { + pub fn new_from_name(name: &str) -> Option { + match name { + "+" => Some(Self::new(Op::Add)), + "-" => Some(Self::new(Op::Subtract)), + "/" => Some(Self::new(Op::Divide)), + "*" => Some(Self::new(Op::Multiply)), + "%" => Some(Self::new(Op::Mod)), + "_" => Some(Self::new(Op::Negate)), + "==" => Some(Self::new(Op::Equal)), + ">" => Some(Self::new(Op::Greater)), + "<" => Some(Self::new(Op::Less)), + ">=" => Some(Self::new(Op::GreaterThanOrEquals)), + "<=" => Some(Self::new(Op::LessThanOrEquals)), + "!=" => Some(Self::new(Op::NotEquals)), + "!" => Some(Self::new(Op::Not)), + "&&" => Some(Self::new(Op::And)), + "||" => Some(Self::new(Op::Or)), + "MIN" => Some(Self::new(Op::Min)), + "MAX" => Some(Self::new(Op::Max)), + "POW" => Some(Self::new(Op::Pow)), + "FLOOR" => Some(Self::new(Op::Floor)), + "CEILING" => Some(Self::new(Op::Ceiling)), + "INT" => Some(Self::new(Op::Int)), + "FLOAT" => Some(Self::new(Op::Float)), + "?" => Some(Self::new(Op::Has)), + "!?" => Some(Self::new(Op::Hasnt,)), + "^" => Some(Self::new(Op::Intersect)), + "LIST_MIN" => Some(Self::new(Op::ListMin)), + "LIST_MAX" => Some(Self::new(Op::ListMax)), + "LIST_ALL" => Some(Self::new(Op::All)), + "LIST_COUNT" => Some(Self::new(Op::Count)), + "LIST_VALUE" => Some(Self::new(Op::ValueOfList)), + "LIST_INVERT" => Some(Self::new(Op::Invert)), + _ => None, + } + } + + pub fn new(op: Op) -> Self { + Self { + obj: Object::new(), + op, + number_of_parameters: 0, + } + } + + pub(crate) fn get_number_of_parameters(&self) -> usize { + match self.op { + Op::Add => 2, + Op::Subtract => 2, + Op::Divide => 2, + Op::Multiply => 2, + Op::Mod => 2, + Op::Negate => 1, + Op::Equal => 2, + Op::Greater => 2, + Op::Less => 2, + Op::GreaterThanOrEquals => 2, + Op::LessThanOrEquals => 2, + Op::NotEquals => 2, + Op::Not => 1, + Op::And => 2, + Op::Or => 2, + Op::Min => 2, + Op::Max => 2, + Op::Pow => 2, + Op::Floor => 1, + Op::Ceiling => 1, + Op::Int => 1, + Op::Float => 1, + Op::Has => 2, + Op::Hasnt => 2, + Op::Intersect => 2, + Op::ListMin => 1, + Op::ListMax => 1, + Op::All => 1, + Op::Count => 1, + Op::ValueOfList => 1, + Op::Invert => 1, + } + } + + pub(crate) fn call(&self, params: Vec>) -> std::rc::Rc { + + let coerced_params = self.coerce_values_to_single_type(params); + + match self.op { + Op::Add => todo!(), + Op::Subtract => todo!(), + Op::Divide => todo!(), + Op::Multiply => todo!(), + Op::Mod => todo!(), + Op::Negate => todo!(), + Op::Equal => todo!(), + Op::Greater => todo!(), + Op::Less => todo!(), + Op::GreaterThanOrEquals => todo!(), + Op::LessThanOrEquals => todo!(), + Op::NotEquals => todo!(), + Op::Not => todo!(), + Op::And => self.and_op(coerced_params), + Op::Or => todo!(), + Op::Min => todo!(), + Op::Max => todo!(), + Op::Pow => todo!(), + Op::Floor => todo!(), + Op::Ceiling => todo!(), + Op::Int => todo!(), + Op::Float => todo!(), + Op::Has => todo!(), + Op::Hasnt => todo!(), + Op::Intersect => todo!(), + Op::ListMin => todo!(), + Op::ListMax => todo!(), + Op::All => todo!(), + Op::Count => todo!(), + Op::ValueOfList => todo!(), + Op::Invert => todo!(), + } + } + + fn and_op(&self, params: Vec>) -> Rc { + match params[0].value { + ValueType::Bool(op1) => match params[1].value { + ValueType::Bool(op2) => Rc::new(Value::new_bool(op1 && op2)), + _ => panic!() + }, + ValueType::Int(op1) => match params[1].value { + ValueType::Int(op2) => Rc::new(Value::new_bool(op1 != 0 && op2 != 0)), + _ => panic!() + }, + ValueType::Float(op1) => match params[1].value { + ValueType::Float(op2) => Rc::new(Value::new_bool(op1 != 0.0 && op2 != 0.0)), + _ => panic!() + }, + ValueType::List() => todo!(), + _ => panic!() + } + } + + fn coerce_values_to_single_type(&self, params: Vec>) -> Vec> { + let mut dest_type = 1; // Int + let mut result: Vec> = Vec::new(); + + for obj in params.iter() { + // 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. + if let Some(v) = obj.as_ref().as_any().downcast_ref::() { + if v.get_cast_ordinal() > dest_type { + dest_type = v.get_cast_ordinal(); + } + } + } + + for obj in params.iter() { + if let Some(v) = obj.as_ref().as_any().downcast_ref::() { + let casted_value = v.cast(dest_type); + result.push(Rc::new(casted_value)); + } + } + + return result; + } +} + +impl RTObject for NativeFunctionCall { + fn get_object(&self) -> &Object { + &self.obj + } +} + +impl fmt::Display for NativeFunctionCall { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Native '{:?}'", self.op) + } +} \ No newline at end of file diff --git a/src/story.rs b/src/story.rs index 2bd712b..4b568f4 100644 --- a/src/story.rs +++ b/src/story.rs @@ -7,7 +7,7 @@ use crate::{ error::ErrorType, json_serialization, push_pop::PushPopType, - story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, search_result::SearchResult, variable_assigment::VariableAssignment, + story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, search_result::SearchResult, variable_assigment::VariableAssignment, native_function_call::{NativeFunctionCall, self}, }; const INK_VERSION_CURRENT: i32 = 21; @@ -894,6 +894,17 @@ impl Story { crate::control_command::CommandType::BeginTag => todo!(), crate::control_command::CommandType::EndTag => todo!(), } + + return true; + } + + + if let Some(func) = content_obj.as_ref().as_any().downcast_ref::() { + let func_params = self.state.as_mut().unwrap().pop_evaluation_stack_multiple(func.get_number_of_parameters()); + + let result = func.call(func_params); + self.state.as_mut().unwrap().push_evaluation_stack(result); + return true; } @@ -1115,11 +1126,10 @@ impl Story { // Don't create choice if player has already read this content if choice_point.once_only() { - //TODO - // let visitCount = state.visitCountForContainer(choicePoint.getChoiceTarget()); - // if (visitCount > 0) { - // showChoice = false; - // } + let visit_count = self.state.as_mut().unwrap().visit_count_for_container(choice_point.get_choice_target().as_ref().unwrap()); + if visit_count > 0 { + show_choice = false; + } } // We go through the full process of creating the choice above so diff --git a/src/story_state.rs b/src/story_state.rs index c68ed5c..3c6a08f 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -400,7 +400,7 @@ impl StoryState { } } - fn visit_count_for_container(&mut self, container: &Rc) -> usize { + pub fn visit_count_for_container(&mut self, container: &Rc) -> usize { if !container.visits_should_be_counted { // TODO @@ -815,14 +815,20 @@ impl StoryState { } pub fn pop_evaluation_stack(&mut self) -> Rc { - let obj = self.evaluation_stack.last().unwrap().clone(); - self.evaluation_stack.remove(self.evaluation_stack.len() - 1); + let obj = self.evaluation_stack.pop().unwrap(); println!("POP: {}", obj.to_string()); obj } + pub fn pop_evaluation_stack_multiple(&mut self, number_of_objects: usize) -> Vec> { + let start = self.evaluation_stack.len() - number_of_objects; + let obj: Vec> = self.evaluation_stack.drain(start..).collect(); + + obj + } + pub fn set_diverted_pointer(&mut self, p: Pointer) { self.diverted_pointer = p; } diff --git a/src/value.rs b/src/value.rs index 1ff76b5..32b8303 100644 --- a/src/value.rs +++ b/src/value.rs @@ -1,18 +1,16 @@ // enum with integers: https://enodev.fr/posts/rusticity-convert-an-integer-to-an-enum.html -use std::{fmt}; +use std::{fmt, rc::Rc}; -use crate::{object::{RTObject, Object}, path::Path, divert::Divert}; +use crate::{object::{RTObject, Object}, path::Path}; -#[repr(i8)] +#[repr(u8)] pub enum ValueType { - Bool(bool) = -1, + Bool(bool), Int(i32), Float(f32), - //List(List), + List(), String(StringValue), - - // Not used for coersion described above DivertTarget(Path), VariablePointer(VariablePointerValue), } @@ -63,6 +61,7 @@ impl fmt::Display for Value { ValueType::String(v) => write!(f, "{}", v.string), ValueType::DivertTarget(p) => write!(f, "DivertTargetValue({})", p), ValueType::VariablePointer(v) => write!(f, "VariablePointerValue({})", v.variable_name), + ValueType::List() => todo!(), } } } @@ -115,8 +114,9 @@ impl Value { ValueType::Float(v) => *v != 0.0, ValueType::String(v) => v.string.len() > 0, ValueType::DivertTarget(_) => panic!(), // exception Shouldn't be checking the truthiness of a divert target?? - ValueType::VariablePointer(_) => panic!(), // exception Shouldn't be checking the truthiness of a divert target?? - } + ValueType::VariablePointer(_) => panic!(), + ValueType::List() => todo!(), + } } pub fn get_string_value(o: &dyn RTObject) -> Option<&StringValue> { @@ -148,4 +148,77 @@ impl Value { None => None, } } + + pub fn get_cast_ordinal(&self) -> u8 { + let v = &self.value; + + let ptr_to_option = (v as *const ValueType) as *const u8; + unsafe { + *ptr_to_option + } + } + + pub fn cast(&self, ordinal_dest_type: u8) -> Value { + match &self.value { + ValueType::Bool(v) => { + match ordinal_dest_type { + 0 => Self::new_bool(*v), + 1 => if *v { + Self::new_int(1) + } else { + Self::new_int(0) + }, + 2 => if *v { + Self::new_float(1.0) + } else { + Self::new_float(0.0) + }, + 3 => if *v { + Self::new_string("true") + } else { + Self::new_string("false") + }, + _ => panic!(), + } + }, + ValueType::Int(v) => { + match ordinal_dest_type { + 0 => if *v == 0 { + Self::new_bool(false) + } else { + Self::new_bool(true) + }, + 1 => Self::new_int(*v), + 2 => Self::new_float(*v as f32), + 3 => Self::new_string(&*v.to_string()), + _ => panic!(), + } + }, + ValueType::Float(v) => { + match ordinal_dest_type { + 0 => if *v == 0.0 { + Self::new_bool(false) + } else { + Self::new_bool(true) + }, + 1 => Self::new_int(*v as i32), + 2 => Self::new_float(*v), + 3 => Self::new_string(&*v.to_string()), + _ => panic!(), + } + }, + ValueType::String(v) => { + match ordinal_dest_type { + 0 => panic!(), + 1 => Self::new_int(v.string.parse::().unwrap()), + 2 => Self::new_float(v.string.parse::().unwrap()), + 3 => Self::new_string(&v.string), + _ => panic!(), + } + }, + ValueType::DivertTarget(_) => panic!(), + ValueType::VariablePointer(_) => panic!(), + ValueType::List() => todo!(), + } + } } \ No newline at end of file From 8aaddfd224103f0a3e132659005167dbc4ab8313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Sun, 24 Sep 2023 16:18:41 +0000 Subject: [PATCH 22/91] All choice tests working + first conditional test --- src/container.rs | 2 +- src/native_function_call.rs | 76 ++++++++++++++++++++--------- src/state_patch.rs | 8 ++++ src/story.rs | 96 ++++++++++++++++++++++++++++--------- src/value.rs | 2 - src/variable_reference.rs | 4 +- src/variables_state.rs | 36 ++++++++++++-- tests/choice_test.rs | 2 +- 8 files changed, 173 insertions(+), 53 deletions(-) diff --git a/src/container.rs b/src/container.rs index 2ed1129..20f64a3 100644 --- a/src/container.rs +++ b/src/container.rs @@ -18,7 +18,7 @@ pub struct Container { obj: Object, pub name: Option, pub content: Vec>, - named_content: HashMap>, + pub named_content: HashMap>, pub visits_should_be_counted: bool, pub turn_index_should_be_counted: bool, pub counting_at_start_only: bool, diff --git a/src/native_function_call.rs b/src/native_function_call.rs index c702c7d..d3485ea 100644 --- a/src/native_function_call.rs +++ b/src/native_function_call.rs @@ -95,7 +95,7 @@ impl NativeFunctionCall { } } - pub(crate) fn get_number_of_parameters(&self) -> usize { + pub fn get_number_of_parameters(&self) -> usize { match self.op { Op::Add => 2, Op::Subtract => 2, @@ -137,13 +137,13 @@ impl NativeFunctionCall { match self.op { Op::Add => todo!(), - Op::Subtract => todo!(), + Op::Subtract => self.subtract_op(coerced_params), Op::Divide => todo!(), Op::Multiply => todo!(), Op::Mod => todo!(), Op::Negate => todo!(), Op::Equal => todo!(), - Op::Greater => todo!(), + Op::Greater => self.greater_op(coerced_params), Op::Less => todo!(), Op::GreaterThanOrEquals => todo!(), Op::LessThanOrEquals => todo!(), @@ -170,25 +170,6 @@ impl NativeFunctionCall { } } - fn and_op(&self, params: Vec>) -> Rc { - match params[0].value { - ValueType::Bool(op1) => match params[1].value { - ValueType::Bool(op2) => Rc::new(Value::new_bool(op1 && op2)), - _ => panic!() - }, - ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Rc::new(Value::new_bool(op1 != 0 && op2 != 0)), - _ => panic!() - }, - ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Rc::new(Value::new_bool(op1 != 0.0 && op2 != 0.0)), - _ => panic!() - }, - ValueType::List() => todo!(), - _ => panic!() - } - } - fn coerce_values_to_single_type(&self, params: Vec>) -> Vec> { let mut dest_type = 1; // Int let mut result: Vec> = Vec::new(); @@ -209,11 +190,62 @@ impl NativeFunctionCall { if let Some(v) = obj.as_ref().as_any().downcast_ref::() { let casted_value = v.cast(dest_type); result.push(Rc::new(casted_value)); + } else { + panic!("RTObject of type Value expected: {}", obj.to_string()) } } return result; } + + fn and_op(&self, params: Vec>) -> Rc { + match params[0].value { + ValueType::Bool(op1) => match params[1].value { + ValueType::Bool(op2) => Rc::new(Value::new_bool(op1 && op2)), + _ => panic!() + }, + ValueType::Int(op1) => match params[1].value { + ValueType::Int(op2) => Rc::new(Value::new_bool(op1 != 0 && op2 != 0)), + _ => panic!() + }, + ValueType::Float(op1) => match params[1].value { + ValueType::Float(op2) => Rc::new(Value::new_bool(op1 != 0.0 && op2 != 0.0)), + _ => panic!() + }, + ValueType::List() => todo!(), + _ => panic!() + } + } + + fn greater_op(&self, params: Vec>) -> Rc { + match params[0].value { + ValueType::Int(op1) => match params[1].value { + ValueType::Int(op2) => Rc::new(Value::new_bool(op1 > op2)), + _ => panic!() + }, + ValueType::Float(op1) => match params[1].value { + ValueType::Float(op2) => Rc::new(Value::new_bool(op1 > op2)), + _ => panic!() + }, + ValueType::List() => todo!(), + _ => panic!() + } + } + + fn subtract_op(&self, params: Vec>) -> Rc { + match params[0].value { + ValueType::Int(op1) => match params[1].value { + ValueType::Int(op2) => Rc::new(Value::new_int(op1 - op2)), + _ => panic!() + }, + ValueType::Float(op1) => match params[1].value { + ValueType::Float(op2) => Rc::new(Value::new_float(op1 - op2)), + _ => panic!() + }, + ValueType::List() => todo!(), + _ => panic!() + } + } } impl RTObject for NativeFunctionCall { diff --git a/src/state_patch.rs b/src/state_patch.rs index 8b5f2b4..2d69805 100644 --- a/src/state_patch.rs +++ b/src/state_patch.rs @@ -43,4 +43,12 @@ impl StatePatch { pub fn get_global(&self, name: &str) -> Option>{ self.globals.get(name).cloned() } + + pub fn set_global(&mut self, name: &str, value: Rc) { + self.globals.insert(name.to_string(), value); + } + + pub(crate) fn add_changed_variable(&mut self, name: &str) { + self.changed_variables.insert(name.to_string()); + } } \ No newline at end of file diff --git a/src/story.rs b/src/story.rs index 4b568f4..002d17d 100644 --- a/src/story.rs +++ b/src/story.rs @@ -7,7 +7,7 @@ use crate::{ error::ErrorType, json_serialization, push_pop::PushPopType, - story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, search_result::SearchResult, variable_assigment::VariableAssignment, native_function_call::{NativeFunctionCall, self}, + story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, search_result::SearchResult, variable_assigment::VariableAssignment, native_function_call::{NativeFunctionCall, self}, variable_reference::VariableReference, }; const INK_VERSION_CURRENT: i32 = 21; @@ -111,18 +111,17 @@ impl Story { } fn reset_globals(&mut self) { - // TODO - // if (self.main_content_container.get_named_content().containsKey("global decl")) { - // let originalPointer = self.state.as_ref().unwrap().get_current_pointer().clone(); + if self.main_content_container.named_content.contains_key("global decl") { + let original_pointer = self.state.as_ref().unwrap().get_current_pointer().clone(); - // self.choose_path(Path::new_with_components_string(Some("global decl".to_string())), false); + self.choose_path(&Path::new_with_components_string(Some("global decl")), false); - // // Continue, but without validating external bindings, - // // since we may be doing this reset at initialisation time. - // self.continue_internal(); + // Continue, but without validating external bindings, + // since we may be doing this reset at initialisation time. + self.continue_internal(0.0); - // self.state.as_ref().unwrap().set_current_pointer(originalPointer); - // } + self.state.as_ref().unwrap().set_current_pointer(original_pointer); + } self.state.as_mut().unwrap().get_variables_state_mut().snapshot_default_globals(); } @@ -794,7 +793,28 @@ impl Story { assert!(!self.state.as_ref().unwrap().get_in_expression_evaluation(), "Already in expression evaluation?"); self.state.as_ref().unwrap().set_in_expression_evaluation(true); }, - crate::control_command::CommandType::EvalOutput => todo!(), + crate::control_command::CommandType::EvalOutput => { + // If the expression turned out to be empty, there may not be + // anything on the stack + if self.state.as_ref().unwrap().evaluation_stack.len() > 0 { + + let output = self.state.as_mut().unwrap().pop_evaluation_stack(); + + // Functions may evaluate to Void, in which case we skip + // output + if let None = output.as_ref().as_any().downcast_ref::() { + // 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. + let text:Rc = Rc::new(Value::new_string(&output.to_string())); + + self.state.as_mut().unwrap().push_to_output_stream(text); + } + } + }, crate::control_command::CommandType::EvalEnd => { assert!(self.state.as_ref().unwrap().get_in_expression_evaluation(), "Not in expression evaluation mode"); self.state.as_ref().unwrap().set_in_expression_evaluation(false); @@ -861,7 +881,7 @@ impl Story { self.state.as_ref().unwrap().set_in_expression_evaluation(true); self.state.as_mut().unwrap().push_evaluation_stack(Rc::new(Value::new_string(&sb))); }, - crate::control_command::CommandType::NoOp => todo!(), + crate::control_command::CommandType::NoOp => {}, crate::control_command::CommandType::ChoiceCount => todo!(), crate::control_command::CommandType::Turns => todo!(), crate::control_command::CommandType::TurnsSince => todo!(), @@ -898,16 +918,6 @@ impl Story { return true; } - - if let Some(func) = content_obj.as_ref().as_any().downcast_ref::() { - let func_params = self.state.as_mut().unwrap().pop_evaluation_stack_multiple(func.get_number_of_parameters()); - - let result = func.call(func_params); - self.state.as_mut().unwrap().push_evaluation_stack(result); - - return true; - } - // Variable assignment if let Some(var_ass) = content_obj.as_ref().as_any().downcast_ref::() { let assigned_val = self.state.as_mut().unwrap().pop_evaluation_stack(); @@ -922,6 +932,48 @@ impl Story { return true; } + + // Variable reference + if let Ok(var_ref) = content_obj.clone().into_any().downcast::() { + let mut found_value: Option> = None; + + // Explicit read count value + if let Some(p) = &var_ref.path_for_count { + let container = var_ref.get_container_for_count(); + let count = self.state.as_mut().unwrap().visit_count_for_container(container.as_ref().unwrap()); + found_value = Some(Rc::new(Value::new_int(count as i32))); + } + + // Normal variable reference + else { + + found_value = self.state.as_ref().unwrap().get_variables_state().get_variable_with_name(&var_ref.name, -1); + + if let None = found_value { + // TODO + // self.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."); + + found_value = Some(Rc::new(Value::new_int(0))); + } + } + + self.state.as_mut().unwrap().push_evaluation_stack(found_value.unwrap()); + + return true; + } + + // Native function call + if let Some(func) = content_obj.as_ref().as_any().downcast_ref::() { + let func_params = self.state.as_mut().unwrap().pop_evaluation_stack_multiple(func.get_number_of_parameters()); + + let result = func.call(func_params); + self.state.as_mut().unwrap().push_evaluation_stack(result); + + return true; + } false diff --git a/src/value.rs b/src/value.rs index 32b8303..27214e2 100644 --- a/src/value.rs +++ b/src/value.rs @@ -1,5 +1,3 @@ -// enum with integers: https://enodev.fr/posts/rusticity-convert-an-integer-to-an-enum.html - use std::{fmt, rc::Rc}; use crate::{object::{RTObject, Object}, path::Path}; diff --git a/src/variable_reference.rs b/src/variable_reference.rs index cb9e094..687148e 100644 --- a/src/variable_reference.rs +++ b/src/variable_reference.rs @@ -5,8 +5,8 @@ use crate::{object::{Object, RTObject}, path::Path, container::Container}; pub struct VariableReference { obj: Object, - name: String, - path_for_count: Option, + pub name: String, + pub path_for_count: Option, } impl VariableReference { diff --git a/src/variables_state.rs b/src/variables_state.rs index 01d78e3..d8d0831 100644 --- a/src/variables_state.rs +++ b/src/variables_state.rs @@ -67,7 +67,7 @@ impl VariablesState { } pub fn assign ( - &self, + &mut self, var_ass: &VariableAssignment, value: Rc, ) { @@ -203,8 +203,38 @@ impl VariablesState { var_value } - fn set_global(&self, name: &str, value: Rc) { - todo!() + fn set_global(&mut self, name: &str, value: Rc) { + let mut old_value: Option> = None; + + if let Some(patch) = &self.patch { + old_value = patch.get_global(name); + } + + if old_value.is_none() { + old_value = self.global_variables.get(name).cloned(); + } + + // TODO ListValue::retain_list_origins_for_assignment(&mut old_value, &value); + + if let Some(patch) = &mut self.patch { + patch.set_global(name, value.clone()); + } else { + self.global_variables.insert(name.to_string(), value.clone()); + } + + if let Some(variable_changed_event) = &self.variable_changed_event { + if !Rc::ptr_eq(old_value.as_ref().unwrap(), &value) { + if self.batch_observing_variable_changes { + if let Some(patch) = &mut self.patch { + patch.add_changed_variable(name); + } else if let Some(changed_variables) = &mut self.changed_variables_for_batch_obs { + changed_variables.insert(name.to_string()); + } + } else { + variable_changed_event(name, value.as_ref()); + } + } + } } pub fn get_variable_with_name(&self, name: &str, context_index: i32) -> Option> { diff --git a/tests/choice_test.rs b/tests/choice_test.rs index af5c822..54ee219 100644 --- a/tests/choice_test.rs +++ b/tests/choice_test.rs @@ -244,7 +244,7 @@ fn label_flow2_test() -> Result<(), String> { common::next_all(&mut story, &mut text)?; assert_eq!(2, story.get_current_choices().len()); - assert_eq!("Shove him aside",story.get_current_choices()[0].text); + assert_eq!("Shove him aside",story.get_current_choices()[1].text); Ok(()) } From 9e155ad85a32025172f39ef4e6549a39e57dfb4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Sun, 24 Sep 2023 17:41:07 +0000 Subject: [PATCH 23/91] glue tests ok --- src/callstack.rs | 10 ++-- src/path.rs | 2 +- src/push_pop.rs | 2 +- src/story.rs | 120 ++++++++++++++++++++++++++------------ src/story_state.rs | 36 ++++++++++-- src/variable_reference.rs | 2 +- tests/divert_test.rs | 75 +++++++++++++++++++++++- tests/glue_test.rs | 64 +++++++++++++++++++- 8 files changed, 260 insertions(+), 51 deletions(-) diff --git a/src/callstack.rs b/src/callstack.rs index db95873..ae1fd54 100644 --- a/src/callstack.rs +++ b/src/callstack.rs @@ -126,7 +126,7 @@ impl CallStack { self.threads.remove(self.threads.len() - 1); Ok(()) } else { - Err("Can't pop thread".to_string()) + Err("Can't pop thread".to_owned()) } } @@ -134,17 +134,17 @@ impl CallStack { self.get_callstack().len() > 1 } - pub fn can_pop_type(&self, t: PushPopType) -> bool { + pub fn can_pop_type(&self, t: Option) -> bool { if !self.can_pop() { return false; } - //if t.is_some() {return true;} + if t.is_none() { return true; } - self.get_current_element().push_pop_type == t + self.get_current_element().push_pop_type == t.unwrap() } - pub fn pop_type(&mut self, t: PushPopType) { + pub fn pop(&mut self, t: Option) { if self.can_pop_type(t) { let l = self.get_callstack().len() - 1; self.get_callstack_mut().remove(l); diff --git a/src/path.rs b/src/path.rs index 31522d9..5b97c4c 100644 --- a/src/path.rs +++ b/src/path.rs @@ -153,7 +153,7 @@ impl Path { } if self.is_relative { - return ".".to_string() + &sb; + return ".".to_owned() + &sb; } sb diff --git a/src/push_pop.rs b/src/push_pop.rs index d799446..d72e406 100644 --- a/src/push_pop.rs +++ b/src/push_pop.rs @@ -1,4 +1,4 @@ -#[derive(PartialEq, Clone, Copy)] +#[derive(PartialEq, Clone, Copy, Eq, Hash, Debug)] pub enum PushPopType { Tunnel, Function, diff --git a/src/story.rs b/src/story.rs index 002d17d..23ae956 100644 --- a/src/story.rs +++ b/src/story.rs @@ -1,6 +1,6 @@ #![allow(unused_variables, dead_code)] -use std::{rc::Rc, time::Instant, collections::VecDeque}; +use std::{rc::Rc, time::Instant, collections::{VecDeque, HashMap}}; use crate::{ container::Container, @@ -37,7 +37,7 @@ impl Story { pub fn new(json_string: &str) -> Result { let json: serde_json::Value = match serde_json::from_str(json_string) { Ok(value) => value, - Err(_) => return Err("Story not in JSON format.".to_string()), + Err(_) => return Err("Story not in JSON format.".to_owned()), }; let version_opt = json.get("inkVersion"); @@ -52,9 +52,9 @@ impl Story { let version: i32 = version_opt.unwrap().as_i64().unwrap().try_into().unwrap(); if version > INK_VERSION_CURRENT { - return Err("Version of ink used to build story was newer than the current version of the engine".to_string()); + return Err("Version of ink used to build story was newer than the current version of the engine".to_owned()); } else if version < INK_VERSION_MINIMUM_COMPATIBLE { - return Err("Version of ink used to build story is too old to be loaded by this version of the engine".to_string()); + return Err("Version of ink used to build story is too old to be loaded by this version of the engine".to_owned()); } else if version != INK_VERSION_CURRENT { log::debug!("WARNING: Version of ink used to build story doesn't match current version of engine. Non-critical, but recommend synchronising."); } @@ -79,7 +79,7 @@ impl Story { let main_content_container = main_content_container.into_any().downcast::(); if main_content_container.is_err() { - return Err("Root node for ink is not a container?".to_string()); + return Err("Root node for ink is not a container?".to_owned()); }; let mut story = Story { @@ -171,7 +171,7 @@ impl Story { self.async_continue_active = is_async_time_limited; if !self.can_continue() { return Err( - "Can't continue - should check canContinue before calling Continue".to_string(), + "Can't continue - should check canContinue before calling Continue".to_owned(), ); } @@ -265,7 +265,7 @@ impl Story { .unwrap() .get_callstack() .borrow() - .can_pop_type(PushPopType::Tunnel) + .can_pop_type(Some(PushPopType::Tunnel)) { self.add_error("unexpectedly reached end of content. Do you need a '->->' to return from a tunnel?"); } else if self @@ -274,7 +274,7 @@ impl Story { .unwrap() .get_callstack() .borrow() - .can_pop_type(PushPopType::Function) + .can_pop_type(Some(PushPopType::Function)) { self.add_error( "unexpectedly reached end of content. Do you need a '~ return'?", @@ -788,12 +788,12 @@ impl Story { if let Some(eval_command) = content_obj.as_ref().as_any().downcast_ref::() { match eval_command.command_type { - crate::control_command::CommandType::NotSet => todo!(), - crate::control_command::CommandType::EvalStart => { + CommandType::NotSet => todo!(), + CommandType::EvalStart => { assert!(!self.state.as_ref().unwrap().get_in_expression_evaluation(), "Already in expression evaluation?"); self.state.as_ref().unwrap().set_in_expression_evaluation(true); }, - crate::control_command::CommandType::EvalOutput => { + CommandType::EvalOutput => { // If the expression turned out to be empty, there may not be // anything on the stack if self.state.as_ref().unwrap().evaluation_stack.len() > 0 { @@ -815,22 +815,68 @@ impl Story { } } }, - crate::control_command::CommandType::EvalEnd => { + CommandType::EvalEnd => { assert!(self.state.as_ref().unwrap().get_in_expression_evaluation(), "Not in expression evaluation mode"); self.state.as_ref().unwrap().set_in_expression_evaluation(false); }, - crate::control_command::CommandType::Duplicate => todo!(), - crate::control_command::CommandType::PopEvaluatedValue => todo!(), - crate::control_command::CommandType::PopFunction => todo!(), - crate::control_command::CommandType::PopTunnel => todo!(), - crate::control_command::CommandType::BeginString => { + CommandType::Duplicate => todo!(), + CommandType::PopEvaluatedValue => todo!(), + CommandType::PopFunction | CommandType::PopTunnel=> { + let pop_type = if let eval_command = CommandType::PopFunction { + PushPopType::Function + } else { + PushPopType::Tunnel + }; + + // Tunnel onwards is allowed to specify an optional override + // divert to go to immediately after returning: ->-> target + let mut override_tunnel_return_target = None; + if pop_type == PushPopType::Tunnel { + let popped = self.state.as_mut().unwrap().pop_evaluation_stack(); + + if let Some(v) = Value::get_divert_target_value(popped.as_ref()) { + override_tunnel_return_target = Some(v.clone()); + } + + if override_tunnel_return_target.is_none() { + assert!(popped.as_ref().as_any().downcast_ref::().is_some(), "Expected void if ->-> doesn't override target"); + } + } + + if self.state.as_mut().unwrap().try_exit_function_evaluation_from_game() { + return true; + } else if self.state.as_ref().unwrap().get_callstack().borrow().get_current_element().push_pop_type != pop_type + || !self.state.as_ref().unwrap().get_callstack().borrow().can_pop() { + + let mut names: HashMap = HashMap::new(); + names.insert(PushPopType::Function, "function return statement (~ return)".to_owned()); + names.insert(PushPopType::Tunnel, "tunnel onwards statement (->->)".to_owned()); + + let mut expected = names.get(&self.state.as_ref().unwrap().get_callstack().borrow().get_current_element().push_pop_type).cloned(); + if !self.state.as_ref().unwrap().get_callstack().borrow().can_pop() { + expected = Some("end of flow (-> END or choice)".to_owned()); + } + + panic!("Found {}, when expected {}", names.get(&pop_type).unwrap(), expected.unwrap()); + //TODO error(errorMsg); + } else { + self.state.as_mut().unwrap().pop_callstack(None); + + // Does tunnel onwards override by diverting to a new ->-> + // target? + if let Some(override_tunnel_return_target) = override_tunnel_return_target { + self.state.as_mut().unwrap().set_diverted_pointer(Self::pointer_at_path(&self.main_content_container, &override_tunnel_return_target)); + } + } + }, + CommandType::BeginString => { self.state.as_mut().unwrap().push_to_output_stream(content_obj.clone()); assert!(self.state.as_ref().unwrap().get_in_expression_evaluation(), "Expected to be in an expression when evaluating a string"); self.state.as_ref().unwrap().set_in_expression_evaluation(false); }, - crate::control_command::CommandType::EndString => { + CommandType::EndString => { // Since we're iterating backward through the content, // build a stack so that when we build the string, @@ -881,17 +927,17 @@ impl Story { self.state.as_ref().unwrap().set_in_expression_evaluation(true); self.state.as_mut().unwrap().push_evaluation_stack(Rc::new(Value::new_string(&sb))); }, - crate::control_command::CommandType::NoOp => {}, - crate::control_command::CommandType::ChoiceCount => todo!(), - crate::control_command::CommandType::Turns => todo!(), - crate::control_command::CommandType::TurnsSince => todo!(), - crate::control_command::CommandType::ReadCount => todo!(), - crate::control_command::CommandType::Random => todo!(), - crate::control_command::CommandType::SeedRandom => todo!(), - crate::control_command::CommandType::VisitIndex => todo!(), - crate::control_command::CommandType::SequenceShuffleIndex => todo!(), - crate::control_command::CommandType::StartThread => todo!(), - crate::control_command::CommandType::Done => { + CommandType::NoOp => {}, + CommandType::ChoiceCount => todo!(), + CommandType::Turns => todo!(), + CommandType::TurnsSince => todo!(), + CommandType::ReadCount => todo!(), + CommandType::Random => todo!(), + CommandType::SeedRandom => todo!(), + CommandType::VisitIndex => todo!(), + CommandType::SequenceShuffleIndex => todo!(), + CommandType::StartThread => todo!(), + CommandType::Done => { // We may exist in the context of the initial // act of creating the thread, or in the context of // evaluating the content. @@ -907,12 +953,12 @@ impl Story { self.state.as_ref().unwrap().set_current_pointer(pointer::NULL.clone()); } }, - crate::control_command::CommandType::End => self.state.as_mut().unwrap().force_end(), - crate::control_command::CommandType::ListFromInt => todo!(), - crate::control_command::CommandType::ListRange => todo!(), - crate::control_command::CommandType::ListRandom => todo!(), - crate::control_command::CommandType::BeginTag => todo!(), - crate::control_command::CommandType::EndTag => todo!(), + CommandType::End => self.state.as_mut().unwrap().force_end(), + CommandType::ListFromInt => todo!(), + CommandType::ListRange => todo!(), + CommandType::ListRandom => todo!(), + CommandType::BeginTag => todo!(), + CommandType::EndTag => todo!(), } return true; @@ -1014,11 +1060,11 @@ impl Story { let mut didPop = false; - let can_pop_type = self.state.as_ref().unwrap().get_callstack().as_ref().borrow().can_pop_type(PushPopType::Function); + let can_pop_type = self.state.as_ref().unwrap().get_callstack().as_ref().borrow().can_pop_type(Some(PushPopType::Function)); if can_pop_type { // Pop from the call stack - self.state.as_mut().unwrap().pop_callstack(PushPopType::Function); + self.state.as_mut().unwrap().pop_callstack(Some(PushPopType::Function)); // This pop was due to dropping off the end of a function that // didn't return anything, diff --git a/src/story_state.rs b/src/story_state.rs index 3c6a08f..a05ff26 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -681,11 +681,11 @@ impl StoryState { return false; } - pub fn pop_callstack(&self, t: PushPopType) { + pub fn pop_callstack(&mut self, t: Option) { // Add the end of a function call, trim any whitespace from the end. if self.get_callstack().borrow().get_current_element().push_pop_type == PushPopType::Function {self.trim_whitespace_from_function_end();} - self.get_callstack().borrow_mut().pop_type(t); + self.get_callstack().borrow_mut().pop(t); } fn go_to_start(&self) { @@ -864,8 +864,36 @@ impl StoryState { // 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. - fn trim_whitespace_from_function_end(&self) { - todo!() + fn trim_whitespace_from_function_end(&mut self) { + assert_eq!( + self.get_callstack().borrow().get_current_element().push_pop_type, + PushPopType::Function + ); + + let function_start_point = match self.get_callstack().borrow().get_current_element().function_start_in_output_stream { + -1 => 0, + start_point => start_point, + }; + + // Trim whitespace from END of function call + let mut i = self.get_output_stream().len() as isize - 1; + while i >= function_start_point as isize { + if let Some(obj) = self.get_output_stream().get(i as usize) { + if let Some(_) = obj.as_any().downcast_ref::() { + break; + } + + if let Some(txt) = Value::get_string_value(obj.as_ref()) { + if txt.is_newline || txt.is_inline_whitespace { + self.get_output_stream_mut().remove(i as usize); + self.output_stream_dirty(); + } else { + break; + } + } + } + i -= 1; + } } } \ No newline at end of file diff --git a/src/variable_reference.rs b/src/variable_reference.rs index 687148e..66999bb 100644 --- a/src/variable_reference.rs +++ b/src/variable_reference.rs @@ -22,7 +22,7 @@ impl VariableReference { if let Some(path) = &self.path_for_count { Ok(Object::resolve_path(self.clone(), path).container().unwrap()) } else { - Err("Path for count is not set.".to_string()) + Err("Path for count is not set.".to_owned()) } } diff --git a/tests/divert_test.rs b/tests/divert_test.rs index 9170bbe..c2b6738 100644 --- a/tests/divert_test.rs +++ b/tests/divert_test.rs @@ -7,7 +7,6 @@ fn simple_divert_test() -> Result<(), String> { let json_string = common::get_json_string("examples/inkfiles/divert/simple-divert.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); - println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -17,3 +16,77 @@ fn simple_divert_test() -> Result<(), String> { Ok(()) } + +#[test] +fn invisible_divert_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/divert/invisible-divert.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + + let mut text: Vec = Vec::new(); + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("We hurried home to Savile Row as fast as we could.", text[0]); + + Ok(()) +} + +#[test] +fn divert_on_choice_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/divert/divert-on-choice.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + + let mut text: Vec = Vec::new(); + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("You open the gate, and step out onto the path.", text[0]); + + Ok(()) +} + +#[test] +fn complex_branching1_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/divert/complex-branching.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(2, text.len()); + assert_eq!("\"There is not a moment to lose!\" I declared.", text[0]); + assert_eq!("We hurried home to Savile Row as fast as we could.", text[1]); + + Ok(()) +} + +#[test] +fn complex_branching2_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/divert/complex-branching.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(1); + + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(3, text.len()); + assert_eq!("\"Monsieur, let us savour this moment!\" I declared.", text[0]); + assert_eq!("My master clouted me firmly around the head and dragged me out of the door.", text[1]); + assert_eq!("He insisted that we hurried home to Savile Row as fast as we could.", text[2]); + + Ok(()) +} diff --git a/tests/glue_test.rs b/tests/glue_test.rs index ed73951..da42589 100644 --- a/tests/glue_test.rs +++ b/tests/glue_test.rs @@ -7,7 +7,6 @@ fn simple_glue_test() -> Result<(), String> { let json_string = common::get_json_string("examples/inkfiles/glue/simple-glue.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); - println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -16,3 +15,66 @@ fn simple_glue_test() -> Result<(), String> { Ok(()) } + +#[test] +fn glue_with_divert_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/glue/glue-with-divert.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("We hurried home to Savile Row as fast as we could.", text[0]); + + Ok(()) +} + +#[test] +fn has_left_right_glue_matching_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/glue/left-right-glue-matching.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + + assert_eq!(2, text.len()); + assert_eq!("A line.", text[0]); + assert_eq!("Another line.", text[1]); + + Ok(()) +} + +#[test] +fn bugfix1_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/glue/testbugfix1.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + + assert_eq!(2, text.len()); + assert_eq!("A", text[0]); + assert_eq!("C", text[1]); + + Ok(()) +} + +#[test] +fn bugfix2_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/glue/testbugfix2.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + + assert_eq!(2, text.len()); + //assert_eq!("A", text[0]); + assert_eq!("X", text[1]); + + Ok(()) +} From ec1b0da1d71d843e7c2f2a878e3f143977deff08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Sun, 24 Sep 2023 18:22:18 +0000 Subject: [PATCH 24/91] Stitch text completed + simple knot, gather and function test --- src/control_command.rs | 4 +- src/native_function_call.rs | 58 +++++++++++++++++++++++++-- src/story.rs | 1 - tests/function_test.rs | 17 ++++++++ tests/gather_test.rs | 24 +++++++++++ tests/knot_test.rs | 24 +++++++++++ tests/stitch_test.rs | 80 +++++++++++++++++++++++++++++++++++++ 7 files changed, 201 insertions(+), 7 deletions(-) create mode 100644 tests/function_test.rs create mode 100644 tests/gather_test.rs create mode 100644 tests/knot_test.rs create mode 100644 tests/stitch_test.rs diff --git a/src/control_command.rs b/src/control_command.rs index 905e748..2ed82b6 100644 --- a/src/control_command.rs +++ b/src/control_command.rs @@ -4,10 +4,8 @@ use strum::Display; use crate::object::{RTObject, Object}; -#[derive(Display)] -#[derive(PartialEq)] +#[derive(PartialEq, Display)] pub enum CommandType { - NotSet, EvalStart, EvalOutput, EvalEnd, diff --git a/src/native_function_call.rs b/src/native_function_call.rs index d3485ea..88b3247 100644 --- a/src/native_function_call.rs +++ b/src/native_function_call.rs @@ -136,10 +136,10 @@ impl NativeFunctionCall { let coerced_params = self.coerce_values_to_single_type(params); match self.op { - Op::Add => todo!(), + Op::Add => self.add_op(coerced_params), Op::Subtract => self.subtract_op(coerced_params), - Op::Divide => todo!(), - Op::Multiply => todo!(), + Op::Divide => self.divide_op(coerced_params), + Op::Multiply => self.multiply_op(coerced_params), Op::Mod => todo!(), Op::Negate => todo!(), Op::Equal => todo!(), @@ -246,6 +246,58 @@ impl NativeFunctionCall { _ => panic!() } } + + fn add_op(&self, params: Vec>) -> Rc { + match ¶ms[0].value { + ValueType::Int(op1) => match params[1].value { + ValueType::Int(op2) => Rc::new(Value::new_int(op1 + op2)), + _ => panic!() + }, + ValueType::Float(op1) => match params[1].value { + ValueType::Float(op2) => Rc::new(Value::new_float(op1 + op2)), + _ => panic!() + }, + ValueType::String(op1) => match ¶ms[1].value { + ValueType::String(op2) => { + let mut sb = String::new(); + sb.push_str(&op1.string); + sb.push_str(&op2.string); + Rc::new(Value::new_string(&sb)) + }, + _ => panic!() + }, + ValueType::List() => todo!(), + _ => panic!() + } + } + + fn divide_op(&self, params: Vec>) -> Rc { + match params[0].value { + ValueType::Int(op1) => match params[1].value { + ValueType::Int(op2) => Rc::new(Value::new_int(op1 / op2)), + _ => panic!() + }, + ValueType::Float(op1) => match params[1].value { + ValueType::Float(op2) => Rc::new(Value::new_float(op1 / op2)), + _ => panic!() + }, + _ => panic!() + } + } + + fn multiply_op(&self, params: Vec>) -> Rc { + match params[0].value { + ValueType::Int(op1) => match params[1].value { + ValueType::Int(op2) => Rc::new(Value::new_int(op1 * op2)), + _ => panic!() + }, + ValueType::Float(op1) => match params[1].value { + ValueType::Float(op2) => Rc::new(Value::new_float(op1 * op2)), + _ => panic!() + }, + _ => panic!() + } + } } impl RTObject for NativeFunctionCall { diff --git a/src/story.rs b/src/story.rs index 23ae956..96b53b0 100644 --- a/src/story.rs +++ b/src/story.rs @@ -788,7 +788,6 @@ impl Story { if let Some(eval_command) = content_obj.as_ref().as_any().downcast_ref::() { match eval_command.command_type { - CommandType::NotSet => todo!(), CommandType::EvalStart => { assert!(!self.state.as_ref().unwrap().get_in_expression_evaluation(), "Already in expression evaluation?"); self.state.as_ref().unwrap().set_in_expression_evaluation(true); diff --git a/tests/function_test.rs b/tests/function_test.rs new file mode 100644 index 0000000..ee20ffd --- /dev/null +++ b/tests/function_test.rs @@ -0,0 +1,17 @@ +use bladeink::story::Story; + +mod common; + +#[test] +fn fun_basic_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/function/func-basic.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The value of x is 4.4.", text[0]); + + Ok(()) +} \ No newline at end of file diff --git a/tests/gather_test.rs b/tests/gather_test.rs new file mode 100644 index 0000000..ca2975a --- /dev/null +++ b/tests/gather_test.rs @@ -0,0 +1,24 @@ +use bladeink::story::Story; + +mod common; + +#[test] +fn gather_basic_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/gather/gather-basic.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(1); + + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(3, text.len()); + assert_eq!("\"Nothing, Monsieur!\" I replied.", text[0]); + assert_eq!("\"Very good, then.\"", text[1]); + assert_eq!("With that Monsieur Fogg left the room.", text[2]); + + Ok(()) +} \ No newline at end of file diff --git a/tests/knot_test.rs b/tests/knot_test.rs new file mode 100644 index 0000000..ca2975a --- /dev/null +++ b/tests/knot_test.rs @@ -0,0 +1,24 @@ +use bladeink::story::Story; + +mod common; + +#[test] +fn gather_basic_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/gather/gather-basic.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(1); + + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(3, text.len()); + assert_eq!("\"Nothing, Monsieur!\" I replied.", text[0]); + assert_eq!("\"Very good, then.\"", text[1]); + assert_eq!("With that Monsieur Fogg left the room.", text[2]); + + Ok(()) +} \ No newline at end of file diff --git a/tests/stitch_test.rs b/tests/stitch_test.rs new file mode 100644 index 0000000..880b66e --- /dev/null +++ b/tests/stitch_test.rs @@ -0,0 +1,80 @@ +use bladeink::story::Story; + +mod common; + +#[test] +fn auto_stitch_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/stitch/auto-stitch.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("I settled my master.", text[0]); + + Ok(()) +} + +#[test] +fn auto_stitch2_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/stitch/auto-stitch.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(1); + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("I settled my master.", text[0]); + + Ok(()) +} + +#[test] +fn manual_stitch_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/stitch/manual-stitch.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("How shall we travel?", text[0]); + + story.choose_choice_index(1); + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("I put myself in third.", text[0]); + + Ok(()) +} + +#[test] +fn manual_stitch2_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/stitch/manual-stitch.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("How shall we travel?", text[0]); + + story.choose_choice_index(0); + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("I settled my master.", text[0]); + + Ok(()) +} \ No newline at end of file From c462e2e92d20a470f3f5eead521fbcd4b18a8491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Sun, 24 Sep 2023 18:50:07 +0000 Subject: [PATCH 25/91] Tunnel tests completed. Added simple variable test. --- README.md | 7 +++---- src/story.rs | 14 +++++++++++++- tests/tunnel_test.rs | 14 ++++++++++++++ tests/variable_test.rs | 17 +++++++++++++++++ 4 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 tests/tunnel_test.rs create mode 100644 tests/variable_test.rs diff --git a/README.md b/README.md index e4c0013..6118008 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Currently under development. This is the implementation status: - [x] Loading .json file - [x] Show plain lines (no logic nor choices) - [x] Choices -- [ ] Knots and Stitches +- [x] Knots and Stitches - [x] Diverts - [ ] Variable Text - [ ] Conditional Text @@ -14,10 +14,9 @@ Currently under development. This is the implementation status: - [ ] Nested flows - [ ] Variables and Logic - [ ] Conditional blocks (if/else) -- [ ] Variables and Logic - [ ] Temporary Variables -- [ ] Functions -- [ ] Tunnels +- [x] Functions +- [x] Tunnels - [ ] Threads - [ ] Lists diff --git a/src/story.rs b/src/story.rs index 96b53b0..f149ca8 100644 --- a/src/story.rs +++ b/src/story.rs @@ -151,6 +151,18 @@ impl Story { Ok(self.get_current_text()) } + pub fn continue_maximally(&mut self) -> Result { + // TODO self.ifAsyncWeCant("ContinueMaximally"); + + let mut sb = String::new(); + + while self.can_continue() { + sb.push_str(&self.cont()?); + } + + Ok(sb) + } + pub fn continue_async(&mut self, millisecs_limit_async: f32) -> Result<(), String> { // TODO: if (!hasValidatedExternals) validateExternalBindings(); @@ -821,7 +833,7 @@ impl Story { CommandType::Duplicate => todo!(), CommandType::PopEvaluatedValue => todo!(), CommandType::PopFunction | CommandType::PopTunnel=> { - let pop_type = if let eval_command = CommandType::PopFunction { + let pop_type = if CommandType::PopFunction == eval_command.command_type { PushPopType::Function } else { PushPopType::Tunnel diff --git a/tests/tunnel_test.rs b/tests/tunnel_test.rs new file mode 100644 index 0000000..7930198 --- /dev/null +++ b/tests/tunnel_test.rs @@ -0,0 +1,14 @@ +use bladeink::story::Story; + +mod common; + +#[test] +fn tunnel_onwards_divert_override_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/tunnels/tunnel-onwards-divert-override.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + + assert_eq!("This is A\nNow in B.\n", story.continue_maximally()?); + + Ok(()) +} \ No newline at end of file diff --git a/tests/variable_test.rs b/tests/variable_test.rs new file mode 100644 index 0000000..9fb0465 --- /dev/null +++ b/tests/variable_test.rs @@ -0,0 +1,17 @@ +use bladeink::story::Story; + +mod common; + +#[test] +fn variable_declaration_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/variable/variable-declaration.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("\"My name is Jean Passepartout, but my friend's call me Jackie. I'm 23 years old.\"", text[0]); + + Ok(()) +} \ No newline at end of file From bce0a3933df2693f7ec2c255b70624f1155b4554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Sun, 24 Sep 2023 19:27:02 +0000 Subject: [PATCH 26/91] Variable test ok. --- README.md | 1 + src/state_patch.rs | 5 +++++ src/story_state.rs | 12 +++++++--- src/value.rs | 12 ++++++---- tests/variable_test.rs | 51 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 74 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 6118008..06b3a02 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Currently under development. This is the implementation status: - [x] Tunnels - [ ] Threads - [ ] Lists +- [ ] Load/Save state ## TODO diff --git a/src/state_patch.rs b/src/state_patch.rs index 2d69805..5d9541f 100644 --- a/src/state_patch.rs +++ b/src/state_patch.rs @@ -51,4 +51,9 @@ impl StatePatch { pub(crate) fn add_changed_variable(&mut self, name: &str) { self.changed_variables.insert(name.to_string()); } + + pub(crate) fn set_turn_index(&mut self, container: &Container, index: i32) { + let key = Object::get_path(container).to_string(); + self.turn_indices.insert(key, index as usize); + } } \ No newline at end of file diff --git a/src/story_state.rs b/src/story_state.rs index a05ff26..069b566 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -2,7 +2,7 @@ use std::{rc::Rc, cell::RefCell, collections::HashMap}; -use crate::{pointer::{Pointer, self}, callstack::CallStack, flow::Flow, variables_state::VariablesState, choice::Choice, object::RTObject, value::{Value, ValueType}, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}, container::Container, state_patch::StatePatch, story::Story, path::Path}; +use crate::{pointer::{Pointer, self}, callstack::CallStack, flow::Flow, variables_state::VariablesState, choice::Choice, object::{RTObject, Object}, value::{Value, ValueType}, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}, container::Container, state_patch::StatePatch, story::Story, path::Path}; use rand::Rng; @@ -427,8 +427,14 @@ impl StoryState { 0 } - pub fn record_turn_index_visit_to_container(&self, container: &crate::container::Container) { - todo!() + pub fn record_turn_index_visit_to_container(&mut self, container: &Container) { + if let Some(patch) = &mut self.patch { + patch.set_turn_index(container, self.current_turn_index); + return; + } + + let container_path_str = Object::get_path(container).to_string(); + self.turn_indices.insert(container_path_str, self.current_turn_index as usize); } fn try_splitting_head_tail_whitespace(text: &str) -> Option> { diff --git a/src/value.rs b/src/value.rs index 27214e2..2e136ef 100644 --- a/src/value.rs +++ b/src/value.rs @@ -171,7 +171,8 @@ impl Value { } else { Self::new_float(0.0) }, - 3 => if *v { + 3 => panic!(), // LIST + 4 => if *v { Self::new_string("true") } else { Self::new_string("false") @@ -188,7 +189,8 @@ impl Value { }, 1 => Self::new_int(*v), 2 => Self::new_float(*v as f32), - 3 => Self::new_string(&*v.to_string()), + 3 => panic!(), // LIST + 4 => Self::new_string(&*v.to_string()), _ => panic!(), } }, @@ -201,7 +203,8 @@ impl Value { }, 1 => Self::new_int(*v as i32), 2 => Self::new_float(*v), - 3 => Self::new_string(&*v.to_string()), + 3 => panic!(), // LIST + 4 => Self::new_string(&*v.to_string()), _ => panic!(), } }, @@ -210,7 +213,8 @@ impl Value { 0 => panic!(), 1 => Self::new_int(v.string.parse::().unwrap()), 2 => Self::new_float(v.string.parse::().unwrap()), - 3 => Self::new_string(&v.string), + 3 => panic!(), // LIST + 4 => Self::new_string(&v.string), _ => panic!(), } }, diff --git a/tests/variable_test.rs b/tests/variable_test.rs index 9fb0465..cc43f50 100644 --- a/tests/variable_test.rs +++ b/tests/variable_test.rs @@ -13,5 +13,56 @@ fn variable_declaration_test() -> Result<(), String> { assert_eq!(1, text.len()); assert_eq!("\"My name is Jean Passepartout, but my friend's call me Jackie. I'm 23 years old.\"", text[0]); + Ok(()) +} + +#[test] +fn var_calc_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/variable/varcalc.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The values are true and -1 and -6 and aa.", text[0]); + + Ok(()) +} + +#[test] +fn var_string_ink_bug_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/variable/varstringinc.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(0); + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(2, text.len()); + assert_eq!("ab.", text[1]); + + Ok(()) +} + +#[test] +fn var_divert_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/variable/var-divert.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(1); + + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("Everybody dies.", text[0]); + Ok(()) } \ No newline at end of file From e01757edc72c5b3e765e71f2ba2fcab2ca2781ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Sun, 24 Sep 2023 20:47:40 +0000 Subject: [PATCH 27/91] all variable text tests ok. --- README.md | 2 +- src/native_function_call.rs | 121 +++++++++++++++++++++-- src/story.rs | 17 +++- src/story_state.rs | 4 + tests/variable_text_test.rs | 189 ++++++++++++++++++++++++++++++++++++ 5 files changed, 323 insertions(+), 10 deletions(-) create mode 100644 tests/variable_text_test.rs diff --git a/README.md b/README.md index 06b3a02..bbda146 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Currently under development. This is the implementation status: - [x] Choices - [x] Knots and Stitches - [x] Diverts -- [ ] Variable Text +- [x] Variable Text - [ ] Conditional Text - [ ] Game Queries and Functions - [ ] Nested flows diff --git a/src/native_function_call.rs b/src/native_function_call.rs index 88b3247..3c5f060 100644 --- a/src/native_function_call.rs +++ b/src/native_function_call.rs @@ -140,19 +140,19 @@ impl NativeFunctionCall { Op::Subtract => self.subtract_op(coerced_params), Op::Divide => self.divide_op(coerced_params), Op::Multiply => self.multiply_op(coerced_params), - Op::Mod => todo!(), + Op::Mod => self.mod_op(coerced_params), Op::Negate => todo!(), - Op::Equal => todo!(), + Op::Equal => self.equal_op(coerced_params), Op::Greater => self.greater_op(coerced_params), Op::Less => todo!(), Op::GreaterThanOrEquals => todo!(), Op::LessThanOrEquals => todo!(), - Op::NotEquals => todo!(), + Op::NotEquals => self.not_equals_op(coerced_params), Op::Not => todo!(), Op::And => self.and_op(coerced_params), - Op::Or => todo!(), - Op::Min => todo!(), - Op::Max => todo!(), + Op::Or => self.or_op(coerced_params), + Op::Min => self.min_op(coerced_params), + Op::Max => self.max_op(coerced_params), Op::Pow => todo!(), Op::Floor => todo!(), Op::Ceiling => todo!(), @@ -298,6 +298,115 @@ impl NativeFunctionCall { _ => panic!() } } + + fn or_op(&self, params: Vec>) -> Rc { + match params[0].value { + ValueType::Bool(op1) => match params[1].value { + ValueType::Bool(op2) => Rc::new(Value::new_bool(op1 || op2)), + _ => panic!() + }, + ValueType::Int(op1) => match params[1].value { + ValueType::Int(op2) => Rc::new(Value::new_bool(op1 != 0 || op2 != 0)), + _ => panic!() + }, + ValueType::Float(op1) => match params[1].value { + ValueType::Float(op2) => Rc::new(Value::new_bool(op1 != 0.0 || op2 != 0.0)), + _ => panic!() + }, + ValueType::List() => todo!(), + _ => panic!() + } + } + + fn min_op(&self, params: Vec>) -> Rc { + match params[0].value { + ValueType::Int(op1) => match params[1].value { + ValueType::Int(op2) => Rc::new(Value::new_int(i32::min(op1, op2))), + _ => panic!() + }, + ValueType::Float(op1) => match params[1].value { + ValueType::Float(op2) => Rc::new(Value::new_float(f32::min(op1, op2))), + _ => panic!() + }, + ValueType::List() => todo!(), + _ => panic!() + } + } + + fn max_op(&self, params: Vec>) -> Rc { + match params[0].value { + ValueType::Int(op1) => match params[1].value { + ValueType::Int(op2) => Rc::new(Value::new_int(i32::max(op1, op2))), + _ => panic!() + }, + ValueType::Float(op1) => match params[1].value { + ValueType::Float(op2) => Rc::new(Value::new_float(f32::max(op1, op2))), + _ => panic!() + }, + ValueType::List() => todo!(), + _ => panic!() + } + } + + fn equal_op(&self, params: Vec>) -> Rc { + match ¶ms[0].value { + ValueType::Bool(op1) => match params[1].value { + ValueType::Bool(op2) => Rc::new(Value::new_bool(*op1 == op2)), + _ => panic!() + }, + ValueType::Int(op1) => match params[1].value { + ValueType::Int(op2) => Rc::new(Value::new_bool(*op1 == op2)), + _ => panic!() + }, + ValueType::Float(op1) => match params[1].value { + ValueType::Float(op2) => Rc::new(Value::new_bool(*op1 == op2)), + _ => panic!() + }, + ValueType::String(op1) => match ¶ms[1].value { + ValueType::String(op2) => Rc::new(Value::new_bool(op1.string.eq(&op2.string))), + _ => panic!() + }, + ValueType::List() => todo!(), + _ => panic!() + } + } + + fn not_equals_op(&self, params: Vec>) -> Rc { + match ¶ms[0].value { + ValueType::Bool(op1) => match params[1].value { + ValueType::Bool(op2) => Rc::new(Value::new_bool(*op1 != op2)), + _ => panic!() + }, + ValueType::Int(op1) => match params[1].value { + ValueType::Int(op2) => Rc::new(Value::new_bool(*op1 != op2)), + _ => panic!() + }, + ValueType::Float(op1) => match params[1].value { + ValueType::Float(op2) => Rc::new(Value::new_bool(*op1 != op2)), + _ => panic!() + }, + ValueType::String(op1) => match ¶ms[1].value { + ValueType::String(op2) => Rc::new(Value::new_bool(!op1.string.eq(&op2.string))), + _ => panic!() + }, + ValueType::List() => todo!(), + _ => panic!() + } + } + + fn mod_op(&self, params: Vec>) -> Rc { + match params[0].value { + ValueType::Int(op1) => match params[1].value { + ValueType::Int(op2) => Rc::new(Value::new_int(op1 % op2)), + _ => panic!() + }, + ValueType::Float(op1) => match params[1].value { + ValueType::Float(op2) => Rc::new(Value::new_float(op1 % op2)), + _ => panic!() + }, + _ => panic!() + } + } } impl RTObject for NativeFunctionCall { diff --git a/src/story.rs b/src/story.rs index f149ca8..20e7682 100644 --- a/src/story.rs +++ b/src/story.rs @@ -830,8 +830,13 @@ impl Story { assert!(self.state.as_ref().unwrap().get_in_expression_evaluation(), "Not in expression evaluation mode"); self.state.as_ref().unwrap().set_in_expression_evaluation(false); }, - CommandType::Duplicate => todo!(), - CommandType::PopEvaluatedValue => todo!(), + CommandType::Duplicate => { + let obj = self.state.as_ref().unwrap().peek_evaluation_stack().unwrap().clone(); + self.state.as_mut().unwrap().push_evaluation_stack(obj); + }, + CommandType::PopEvaluatedValue => { + self.state.as_mut().unwrap().pop_evaluation_stack(); + }, CommandType::PopFunction | CommandType::PopTunnel=> { let pop_type = if CommandType::PopFunction == eval_command.command_type { PushPopType::Function @@ -945,7 +950,13 @@ impl Story { CommandType::ReadCount => todo!(), CommandType::Random => todo!(), CommandType::SeedRandom => todo!(), - CommandType::VisitIndex => todo!(), + CommandType::VisitIndex => { + let cpc = self.state.as_ref().unwrap().get_current_pointer().container.unwrap(); + let count = self.state.as_mut().unwrap().visit_count_for_container(&cpc) - 1; // index + // not + // count + self.state.as_mut().unwrap().push_evaluation_stack(Rc::new(Value::new_int(count as i32))); + }, CommandType::SequenceShuffleIndex => todo!(), CommandType::StartThread => todo!(), CommandType::Done => { diff --git a/src/story_state.rs b/src/story_state.rs index 069b566..0e4f05a 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -902,4 +902,8 @@ impl StoryState { } } + pub fn peek_evaluation_stack(&self) -> Option<&Rc> { + self.evaluation_stack.last() + } + } \ No newline at end of file diff --git a/tests/variable_text_test.rs b/tests/variable_text_test.rs new file mode 100644 index 0000000..e35f0e5 --- /dev/null +++ b/tests/variable_text_test.rs @@ -0,0 +1,189 @@ +use bladeink::story::Story; + +mod common; + +#[test] +fn sequence_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/variabletext/sequence.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The radio hissed into life. \"Three!\"", text[0]); + + story.choose_choice_index(0); + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The radio hissed into life. \"Two!\"", text[0]); + + story.choose_choice_index(0); + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The radio hissed into life. \"One!\"", text[0]); + + story.choose_choice_index(0); + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The radio hissed into life. There was the white noise racket of an explosion.", text[0]); + + story.choose_choice_index(0); + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The radio hissed into life. There was the white noise racket of an explosion.", text[0]); + + Ok(()) +} + +#[test] +fn cycle_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/variabletext/cycle.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The radio hissed into life. \"Three!\"", text[0]); + + story.choose_choice_index(0); + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The radio hissed into life. \"Two!\"", text[0]); + + story.choose_choice_index(0); + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The radio hissed into life. \"One!\"", text[0]); + + story.choose_choice_index(0); + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The radio hissed into life. \"Three!\"", text[0]); + + story.choose_choice_index(0); + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The radio hissed into life. \"Two!\"", text[0]); + + Ok(()) +} + +#[test] +fn once_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/variabletext/once.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The radio hissed into life. \"Three!\"", text[0]); + + story.choose_choice_index(0); + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The radio hissed into life. \"Two!\"", text[0]); + + story.choose_choice_index(0); + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The radio hissed into life. \"One!\"", text[0]); + + story.choose_choice_index(0); + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The radio hissed into life.", text[0]); + + story.choose_choice_index(0); + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The radio hissed into life.", text[0]); + + Ok(()) +} + +#[test] +fn empty_elements_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/variabletext/empty-elements.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The radio hissed into life.", text[0]); + + story.choose_choice_index(0); + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The radio hissed into life.", text[0]); + + story.choose_choice_index(0); + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The radio hissed into life. \"One!\"", text[0]); + + Ok(()) +} + +#[test] +fn list_in_choice_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/variabletext/list-in-choice.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("\"Hello, Master!\"", story.get_current_choices()[0].text); + + story.choose_choice_index(0); + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("\"Hello, Monsieur!\"", story.get_current_choices()[0].text); + + story.choose_choice_index(0); + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("\"Hello, you!\"", story.get_current_choices()[0].text); + + Ok(()) +} \ No newline at end of file From 06297609ba2c7df2bb18545fc6cba50d82cf50d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Sun, 24 Sep 2023 21:08:34 +0000 Subject: [PATCH 28/91] all knot tests passes. --- README.md | 2 +- tests/knot_test.rs | 150 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 145 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index bbda146..8a4f832 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Currently under development. This is the implementation status: - [x] Knots and Stitches - [x] Diverts - [x] Variable Text -- [ ] Conditional Text +- [x] Conditional Text - [ ] Game Queries and Functions - [ ] Nested flows - [ ] Variables and Logic diff --git a/tests/knot_test.rs b/tests/knot_test.rs index ca2975a..2f863e3 100644 --- a/tests/knot_test.rs +++ b/tests/knot_test.rs @@ -3,9 +3,77 @@ use bladeink::story::Story; mod common; #[test] -fn gather_basic_test() -> Result<(), String> { +fn single_line_test() -> Result<(), String> { let json_string = - common::get_json_string("examples/inkfiles/gather/gather-basic.ink.json").unwrap(); + common::get_json_string("examples/inkfiles/knot/single-line.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("Hello, world!", text[0]); + + Ok(()) +} + +#[test] +fn multi_line_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/knot/multi-line.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + + assert_eq!(3, text.len()); + assert_eq!("Hello, world!", text[0]); + assert_eq!("Hello?", text[1]); + assert_eq!("Hello, are you there?", text[2]); + + Ok(()) +} + +#[test] +fn strip_empty_lines_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/knot/strip-empty-lines.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + + assert_eq!(3, text.len()); + assert_eq!("Hello, world!", text[0]); + assert_eq!("Hello?", text[1]); + assert_eq!("Hello, are you there?", text[2]); + + Ok(()) +} + +#[test] +fn param_strings_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/knot/param-strings.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(2); + + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("\"I accuse myself!\" Poirot declared.", text[0]); + + Ok(()) +} + +#[test] +fn param_ints_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/knot/param-ints.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -15,10 +83,80 @@ fn gather_basic_test() -> Result<(), String> { text.clear(); common::next_all(&mut story, &mut text)?; - assert_eq!(3, text.len()); - assert_eq!("\"Nothing, Monsieur!\" I replied.", text[0]); - assert_eq!("\"Very good, then.\"", text[1]); - assert_eq!("With that Monsieur Fogg left the room.", text[2]); + assert_eq!(1, text.len()); + assert_eq!("You give 2 dollars.", text[0]); + + Ok(()) +} + +#[test] +fn param_floats_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/knot/param-floats.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(1); + + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("You give 2.5 dollars.", text[0]); + + Ok(()) +} + +#[test] +fn param_vars_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/knot/param-vars.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(1); + + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("You give 2 dollars.", text[0]); + + Ok(()) +} + +#[test] +fn param_multi_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/knot/param-multi.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("You give 1 or 2 dollars. Hmm.", text[0]); + + Ok(()) +} + +#[test] +fn param_recurse_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/knot/param-recurse.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + + assert_eq!(2, text.len()); + assert_eq!("\"The result is 120!\" you announce.", text[0]); Ok(()) } \ No newline at end of file From f4db1a2c0b8bf96435dd8c049352399252e87889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Sun, 24 Sep 2023 21:28:27 +0000 Subject: [PATCH 29/91] All gather tests passes. --- tests/gather_test.rs | 137 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/tests/gather_test.rs b/tests/gather_test.rs index ca2975a..bc81af3 100644 --- a/tests/gather_test.rs +++ b/tests/gather_test.rs @@ -20,5 +20,142 @@ fn gather_basic_test() -> Result<(), String> { assert_eq!("\"Very good, then.\"", text[1]); assert_eq!("With that Monsieur Fogg left the room.", text[2]); + Ok(()) +} + +#[test] +fn gather_chain_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/gather/gather-chain.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + assert_eq!(3, story.get_current_choices().len()); + story.choose_choice_index(1); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!( "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[0]); + assert_eq!(2, story.get_current_choices().len()); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(2, text.len()); + assert_eq!("I reached the road and looked about. And would you believe it?", text[0]); + assert_eq!("The road was empty. Mackie was nowhere to be seen.", text[1]); + + Ok(()) +} + +#[test] +fn nested_flow_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/gather/nested-flow.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(2); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(2, text.len()); + assert_eq!("\"Myself!\"", text[0]); + assert_eq!("Mrs. Christie lowered her manuscript a moment. The rest of the writing group sat, open-mouthed.", text[1]); + + Ok(()) +} + +#[test] +fn deep_nesting_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/gather/deep-nesting.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(2, text.len()); + assert_eq!("\"...Tell us a tale Captain!\"", text[0]); + assert_eq!("To a man, the crew began to yawn.", text[1]); + + Ok(()) +} + + +#[test] +fn complex_flow1_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/gather/complex-flow.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(1); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("... but I said nothing and we passed the day in silence.", text[0]); + + Ok(()) +} + +#[test] +fn complex_flow2_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/gather/complex-flow.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(3, text.len()); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(2, text.len()); + story.choose_choice_index(1); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(2, text.len()); + story.choose_choice_index(1); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(3, text.len()); + Ok(()) } \ No newline at end of file From 0960eb76a34b89117583c2b7c6ef45f1b2450c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Mon, 25 Sep 2023 09:20:51 +0000 Subject: [PATCH 30/91] All function tests passes. --- src/story.rs | 377 +++++++++++++++++++++++++++-------------- src/story_state.rs | 89 +++++++++- src/value.rs | 10 ++ tests/function_test.rs | 120 +++++++++++++ 4 files changed, 467 insertions(+), 129 deletions(-) diff --git a/src/story.rs b/src/story.rs index 20e7682..2e1ab07 100644 --- a/src/story.rs +++ b/src/story.rs @@ -2,12 +2,14 @@ use std::{rc::Rc, time::Instant, collections::{VecDeque, HashMap}}; +use rand::{Rng, rngs::StdRng, SeedableRng}; + use crate::{ container::Container, error::ErrorType, json_serialization, push_pop::PushPopType, - story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, search_result::SearchResult, variable_assigment::VariableAssignment, native_function_call::{NativeFunctionCall, self}, variable_reference::VariableReference, + story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, search_result::SearchResult, variable_assigment::VariableAssignment, native_function_call::NativeFunctionCall, variable_reference::VariableReference, }; const INK_VERSION_CURRENT: i32 = 21; @@ -100,6 +102,14 @@ impl Story { Ok(story) } + fn get_state(&self) -> &StoryState { + self.state.as_ref().unwrap() + } + + fn get_state_mut(&mut self) -> &mut StoryState { + self.state.as_mut().unwrap() + } + fn reset_state(&mut self) { //TODO ifAsyncWeCant("ResetState"); @@ -112,7 +122,7 @@ impl Story { fn reset_globals(&mut self) { if self.main_content_container.named_content.contains_key("global decl") { - let original_pointer = self.state.as_ref().unwrap().get_current_pointer().clone(); + let original_pointer = self.get_state().get_current_pointer().clone(); self.choose_path(&Path::new_with_components_string(Some("global decl")), false); @@ -120,16 +130,16 @@ impl Story { // since we may be doing this reset at initialisation time. self.continue_internal(0.0); - self.state.as_ref().unwrap().set_current_pointer(original_pointer); + self.get_state().set_current_pointer(original_pointer); } - self.state.as_mut().unwrap().get_variables_state_mut().snapshot_default_globals(); + self.get_state_mut().get_variables_state_mut().snapshot_default_globals(); } pub fn build_string_of_hierarchy(&self) -> String { let mut sb = String::new(); - let cp = self.state.as_ref().unwrap().get_current_pointer().resolve(); + let cp = self.get_state().get_current_pointer().resolve(); let cp = match cp { Some(_) => Some(cp.as_ref().unwrap().as_ref()), @@ -143,7 +153,7 @@ impl Story { } pub fn can_continue(&self) -> bool { - self.state.as_ref().unwrap().can_continue() + self.get_state().can_continue() } pub fn cont(&mut self) -> Result { @@ -187,9 +197,9 @@ impl Story { ); } - self.state.as_mut().unwrap().set_did_safe_exit(false); + self.get_state_mut().set_did_safe_exit(false); - self.state.as_mut().unwrap().reset_output(None); + self.get_state_mut().reset_output(None); // 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 @@ -268,7 +278,7 @@ impl Story { .unwrap() .get_generated_choices() .is_empty() - && !self.state.as_ref().unwrap().is_did_safe_exit() + && !self.get_state().is_did_safe_exit() && self.temporaty_evaluation_container.is_none() { if self @@ -291,14 +301,14 @@ impl Story { self.add_error( "unexpectedly reached end of content. Do you need a '~ return'?", ); - } else if !self.state.as_ref().unwrap().get_callstack().borrow().can_pop() { + } else if !self.get_state().get_callstack().borrow().can_pop() { self.add_error("ran out of content. Do you need a '-> DONE' or '-> END'?"); } else { self.add_error("unexpectedly reached end of content for unknown reason. Please debug compiler!"); } } } - self.state.as_mut().unwrap().set_did_safe_exit(false); + self.get_state_mut().set_did_safe_exit(false); self.saw_lookahead_unsafe_function_after_new_line = false; if self.recursive_continue_count == 1 { @@ -317,17 +327,17 @@ impl Story { // 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 self.state.as_ref().unwrap().has_error() || self.state.as_ref().unwrap().has_warning() { + if self.get_state().has_error() || self.get_state().has_warning() { match self.on_error { Some(on_err) => { - if self.state.as_ref().unwrap().has_error() { - for err in self.state.as_ref().unwrap().get_current_errors() { + if self.get_state().has_error() { + for err in self.get_state().get_current_errors() { (on_err)(&err, ErrorType::Error); } } - if self.state.as_ref().unwrap().has_warning() { - for err in self.state.as_ref().unwrap().get_current_warnings() { + if self.get_state().has_warning() { + for err in self.get_state().get_current_warnings() { (on_err)(&err, ErrorType::Warning); } } @@ -339,23 +349,23 @@ impl Story { let mut sb = String::new(); sb.push_str("Ink had "); - if self.state.as_ref().unwrap().has_error() { - sb.push_str(&self.state.as_ref().unwrap().get_current_errors().len().to_string()); + if self.get_state().has_error() { + sb.push_str(&self.get_state().get_current_errors().len().to_string()); - if self.state.as_ref().unwrap().get_current_errors().len() == 1 { + if self.get_state().get_current_errors().len() == 1 { sb.push_str(" error"); } else { sb.push_str(" errors"); } - if self.state.as_ref().unwrap().has_warning() { + if self.get_state().has_warning() { sb.push_str(" and "); } } - if self.state.as_ref().unwrap().has_warning() { - sb.push_str(self.state.as_ref().unwrap().get_current_warnings().len().to_string().as_str()); - if self.state.as_ref().unwrap().get_current_errors().len() == 1 { + if self.get_state().has_warning() { + sb.push_str(self.get_state().get_current_warnings().len().to_string().as_str()); + if self.get_state().get_current_errors().len() == 1 { sb.push_str(" warning"); } else { sb.push_str(" warnings"); @@ -364,10 +374,10 @@ impl Story { sb.push_str(". It is strongly suggested that you assign an error handler to story.onError. The first issue was: "); - if self.state.as_ref().unwrap().has_error() { - sb.push_str(self.state.as_ref().unwrap().get_current_errors()[0].as_str()); + if self.get_state().has_error() { + sb.push_str(self.get_state().get_current_errors()[0].as_str()); } else { - sb.push_str(self.state.as_ref().unwrap().get_current_warnings()[0].to_string().as_str()); + sb.push_str(self.get_state().get_current_warnings()[0].to_string().as_str()); } return Err(sb); @@ -383,12 +393,12 @@ impl Story { self.step(); // Run out of content and we have a default invisible choice that we can follow? - if !self.can_continue() && !self.state.as_ref().unwrap().get_callstack().borrow().element_is_evaluate_from_game() { + if !self.can_continue() && !self.get_state().get_callstack().borrow().element_is_evaluate_from_game() { self.try_follow_default_invisible_choice(); } // Don't save/rewind during string evaluation, which is e.g. used for choices - if !self.state.as_ref().unwrap().in_string_evaluation(){ + if !self.get_state().in_string_evaluation() { // We previously found a newline, but were we just double checking that // it wouldn't immediately be removed by glue? @@ -420,7 +430,7 @@ impl Story { } // Current content ends in a newline - approaching end of our evaluation - if self.state.as_ref().unwrap().output_stream_ends_in_newline() { + if self.get_state().output_stream_ends_in_newline() { // If we can continue evaluation for a bit: // Create a snapshot in case we need to rewind. @@ -451,7 +461,7 @@ impl Story { pub fn get_current_text(&mut self) -> String { //TODO ifAsyncWeCant("call currentText since it's a work in progress"); - self.state.as_mut().unwrap().get_current_text() + self.get_state_mut().get_current_text() } pub fn get_main_content_container(&self) -> Rc { @@ -475,7 +485,7 @@ impl Story { // active, we need to apply any changes made since // the save was started but before the snapshot was made. if !self.async_saving { - self.state.as_mut().unwrap().apply_any_patch(); + self.get_state_mut().apply_any_patch(); } } @@ -491,7 +501,7 @@ impl Story { let mut should_add_to_stream = true; // Get current content - let mut pointer = self.state.as_ref().unwrap().get_current_pointer().clone(); + let mut pointer = self.get_state().get_current_pointer().clone(); if pointer.is_null() { return; @@ -532,7 +542,7 @@ impl Story { }; } - self.state.as_mut().unwrap().set_current_pointer(pointer.clone()); + self.get_state_mut().set_current_pointer(pointer.clone()); // Is the current content Object: // - Normal content @@ -545,7 +555,7 @@ impl Story { let is_logic_or_flow_control = self.perform_logic_and_flow_control(¤t_content_obj); // Has flow been forced to end by flow control above? - if self.state.as_ref().unwrap().get_current_pointer().is_null() { + if self.get_state().get_current_pointer().is_null() { return; } @@ -559,7 +569,7 @@ impl Story { let choice = self.process_choice(&choice_point); if choice.is_some() { - self.state.as_mut().unwrap().get_generated_choices_mut().push(choice.unwrap()); + self.get_state_mut().get_generated_choices_mut().push(choice.unwrap()); } current_content_obj = None; @@ -588,17 +598,17 @@ impl Story { // Create new Object so we're not overwriting the story's own // data - let context_idx = self.state.as_ref().unwrap().get_callstack().borrow().context_for_variable_named(&var_pointer.unwrap().variable_name); + let context_idx = self.get_state().get_callstack().borrow().context_for_variable_named(&var_pointer.unwrap().variable_name); current_content_obj = Some(Rc::new(Value::new_variable_pointer(&var_pointer.unwrap().variable_name, context_idx as i32))); } // Expression evaluation content - if self.state.as_ref().unwrap().get_in_expression_evaluation() { - self.state.as_mut().unwrap().push_evaluation_stack(current_content_obj.unwrap()); + if self.get_state().get_in_expression_evaluation() { + self.get_state_mut().push_evaluation_stack(current_content_obj.unwrap()); } // Output stream content (i.e. not expression evaluation) else { - self.state.as_mut().unwrap().push_to_output_stream(current_content_obj.unwrap()); + self.get_state_mut().push_to_output_stream(current_content_obj.unwrap()); } } @@ -619,7 +629,7 @@ impl Story { } fn try_follow_default_invisible_choice(&mut self) { - let all_choices = match self.state.as_ref().unwrap().get_current_choices() { + let all_choices = match self.get_state().get_current_choices() { Some(c) => c, None => return, }; @@ -642,14 +652,14 @@ impl Story { // Invisible choice may have been generated on a different thread, // in which case we need to restore it before we continue - self.state.as_ref().unwrap().get_callstack().as_ref().borrow_mut().set_current_thread(choice.get_thread_at_generation().unwrap().copy()); + self.get_state().get_callstack().as_ref().borrow_mut().set_current_thread(choice.get_thread_at_generation().unwrap().copy()); // 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 self.state_snapshot_at_last_new_line.is_some() { - let fork_thread = self.state.as_ref().unwrap().get_callstack().as_ref().borrow_mut().fork_thread(); - self.state.as_ref().unwrap().get_callstack().as_ref().borrow_mut().set_current_thread(fork_thread); + let fork_thread = self.get_state().get_callstack().as_ref().borrow_mut().fork_thread(); + self.get_state().get_callstack().as_ref().borrow_mut().set_current_thread(fork_thread); } self.choose_path(&choice.target_path, false); @@ -718,11 +728,11 @@ impl Story { fn visit_container(&mut self, container: &Rc, at_start: bool) { if !container.counting_at_start_only || at_start { if container.visits_should_be_counted { - self.state.as_mut().unwrap().increment_visit_count_for_container(container); + self.get_state_mut().increment_visit_count_for_container(container); } if container.turn_index_should_be_counted { - self.state.as_mut().unwrap().record_turn_index_visit_to_container(container); + self.get_state_mut().record_turn_index_visit_to_container(container); } } } @@ -738,7 +748,7 @@ impl Story { // Divert if let Ok(current_divert) = content_obj.clone().into_any().downcast::() { if current_divert.is_conditional { - let o = self.state.as_mut().unwrap().pop_evaluation_stack(); + let o = self.get_state_mut().pop_evaluation_stack(); if !self.is_truthy(o) { return true; } @@ -746,10 +756,11 @@ impl Story { if current_divert.has_variable_target() { let var_name = ¤t_divert.variable_divert_name; - if let Some(var_contents) = self.state.as_ref().unwrap().get_variables_state().get_variable_with_name(var_name.as_ref().unwrap(), -1) { + if let Some(var_contents) = self.get_state().get_variables_state().get_variable_with_name(var_name.as_ref().unwrap(), -1) { if let Some(target) = Value::get_divert_target_value(var_contents.as_ref()) { - self.state.as_mut().unwrap().set_diverted_pointer(Self::pointer_at_path(&self.main_content_container, target)); - println!("SET DIVERTED POINTER: {} PATH: {}", self.state.as_mut().unwrap().diverted_pointer, target); + let p = Self::pointer_at_path(&self.main_content_container, target); + self.get_state_mut().set_diverted_pointer(p); + println!("SET DIVERTED POINTER: {} PATH: {}", self.get_state_mut().diverted_pointer, target); } else { // TODO // let int_content = var_contents.downcast_ref::(); @@ -782,16 +793,16 @@ impl Story { //call_external_function(¤t_divert.get_target_path_string(), current_divert.get_external_args()); return true; } else { - self.state.as_mut().unwrap().set_diverted_pointer(current_divert.get_target_pointer()); + self.get_state_mut().set_diverted_pointer(current_divert.get_target_pointer()); } if current_divert.pushes_to_stack { - self.state.as_ref().unwrap() + self.get_state() .get_callstack().borrow_mut() - .push(current_divert.stack_push_type, 0, self.state.as_ref().unwrap().get_output_stream().len() as i32); + .push(current_divert.stack_push_type, 0, self.get_state().get_output_stream().len() as i32); } - if self.state.as_ref().unwrap().diverted_pointer.is_null() && !current_divert.is_external { + if self.get_state().diverted_pointer.is_null() && !current_divert.is_external { // error(format!("Divert resolution failed: {:?}", current_divert)); } @@ -801,15 +812,15 @@ impl Story { if let Some(eval_command) = content_obj.as_ref().as_any().downcast_ref::() { match eval_command.command_type { CommandType::EvalStart => { - assert!(!self.state.as_ref().unwrap().get_in_expression_evaluation(), "Already in expression evaluation?"); - self.state.as_ref().unwrap().set_in_expression_evaluation(true); + assert!(!self.get_state().get_in_expression_evaluation(), "Already in expression evaluation?"); + self.get_state().set_in_expression_evaluation(true); }, CommandType::EvalOutput => { // If the expression turned out to be empty, there may not be // anything on the stack - if self.state.as_ref().unwrap().evaluation_stack.len() > 0 { + if self.get_state().evaluation_stack.len() > 0 { - let output = self.state.as_mut().unwrap().pop_evaluation_stack(); + let output = self.get_state_mut().pop_evaluation_stack(); // Functions may evaluate to Void, in which case we skip // output @@ -822,20 +833,20 @@ impl Story { // skips over numbers etc. let text:Rc = Rc::new(Value::new_string(&output.to_string())); - self.state.as_mut().unwrap().push_to_output_stream(text); + self.get_state_mut().push_to_output_stream(text); } } }, CommandType::EvalEnd => { - assert!(self.state.as_ref().unwrap().get_in_expression_evaluation(), "Not in expression evaluation mode"); - self.state.as_ref().unwrap().set_in_expression_evaluation(false); + assert!(self.get_state().get_in_expression_evaluation(), "Not in expression evaluation mode"); + self.get_state().set_in_expression_evaluation(false); }, CommandType::Duplicate => { - let obj = self.state.as_ref().unwrap().peek_evaluation_stack().unwrap().clone(); - self.state.as_mut().unwrap().push_evaluation_stack(obj); + let obj = self.get_state().peek_evaluation_stack().unwrap().clone(); + self.get_state_mut().push_evaluation_stack(obj); }, CommandType::PopEvaluatedValue => { - self.state.as_mut().unwrap().pop_evaluation_stack(); + self.get_state_mut().pop_evaluation_stack(); }, CommandType::PopFunction | CommandType::PopTunnel=> { let pop_type = if CommandType::PopFunction == eval_command.command_type { @@ -848,7 +859,7 @@ impl Story { // divert to go to immediately after returning: ->-> target let mut override_tunnel_return_target = None; if pop_type == PushPopType::Tunnel { - let popped = self.state.as_mut().unwrap().pop_evaluation_stack(); + let popped = self.get_state_mut().pop_evaluation_stack(); if let Some(v) = Value::get_divert_target_value(popped.as_ref()) { override_tunnel_return_target = Some(v.clone()); @@ -859,38 +870,39 @@ impl Story { } } - if self.state.as_mut().unwrap().try_exit_function_evaluation_from_game() { + if self.get_state_mut().try_exit_function_evaluation_from_game() { return true; - } else if self.state.as_ref().unwrap().get_callstack().borrow().get_current_element().push_pop_type != pop_type - || !self.state.as_ref().unwrap().get_callstack().borrow().can_pop() { + } else if self.get_state().get_callstack().borrow().get_current_element().push_pop_type != pop_type + || !self.get_state().get_callstack().borrow().can_pop() { let mut names: HashMap = HashMap::new(); names.insert(PushPopType::Function, "function return statement (~ return)".to_owned()); names.insert(PushPopType::Tunnel, "tunnel onwards statement (->->)".to_owned()); - let mut expected = names.get(&self.state.as_ref().unwrap().get_callstack().borrow().get_current_element().push_pop_type).cloned(); - if !self.state.as_ref().unwrap().get_callstack().borrow().can_pop() { + let mut expected = names.get(&self.get_state().get_callstack().borrow().get_current_element().push_pop_type).cloned(); + if !self.get_state().get_callstack().borrow().can_pop() { expected = Some("end of flow (-> END or choice)".to_owned()); } panic!("Found {}, when expected {}", names.get(&pop_type).unwrap(), expected.unwrap()); //TODO error(errorMsg); } else { - self.state.as_mut().unwrap().pop_callstack(None); + self.get_state_mut().pop_callstack(None); // Does tunnel onwards override by diverting to a new ->-> // target? if let Some(override_tunnel_return_target) = override_tunnel_return_target { - self.state.as_mut().unwrap().set_diverted_pointer(Self::pointer_at_path(&self.main_content_container, &override_tunnel_return_target)); + let p = Self::pointer_at_path(&self.main_content_container, &override_tunnel_return_target); + self.get_state_mut().set_diverted_pointer(p); } } }, CommandType::BeginString => { - self.state.as_mut().unwrap().push_to_output_stream(content_obj.clone()); + self.get_state_mut().push_to_output_stream(content_obj.clone()); - assert!(self.state.as_ref().unwrap().get_in_expression_evaluation(), + assert!(self.get_state().get_in_expression_evaluation(), "Expected to be in an expression when evaluating a string"); - self.state.as_ref().unwrap().set_in_expression_evaluation(false); + self.get_state().set_in_expression_evaluation(false); }, CommandType::EndString => { @@ -902,8 +914,8 @@ impl Story { let mut output_count_consumed = 0; - for i in (0..self.state.as_ref().unwrap().get_output_stream().len()).rev() { - let obj = &self.state.as_ref().unwrap().get_output_stream()[i]; + for i in (0..self.get_state().get_output_stream().len()).rev() { + let obj = &self.get_state().get_output_stream()[i]; output_count_consumed += 1; if let Some(command) = obj.as_ref().as_any().downcast_ref::() { @@ -922,14 +934,14 @@ impl Story { } // Consume the content that was produced for this string - self.state.as_mut().unwrap().pop_from_output_stream(output_count_consumed); + self.get_state_mut().pop_from_output_stream(output_count_consumed); // 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 rescued_tag in content_to_retain.iter() { - self.state.as_mut().unwrap().push_to_output_stream(rescued_tag.clone()); + self.get_state_mut().push_to_output_stream(rescued_tag.clone()); } // Build string out of the content we collected @@ -940,22 +952,87 @@ impl Story { } // Return to expression evaluation (from content mode) - self.state.as_ref().unwrap().set_in_expression_evaluation(true); - self.state.as_mut().unwrap().push_evaluation_stack(Rc::new(Value::new_string(&sb))); + self.get_state().set_in_expression_evaluation(true); + self.get_state_mut().push_evaluation_stack(Rc::new(Value::new_string(&sb))); }, CommandType::NoOp => {}, CommandType::ChoiceCount => todo!(), CommandType::Turns => todo!(), CommandType::TurnsSince => todo!(), CommandType::ReadCount => todo!(), - CommandType::Random => todo!(), - CommandType::SeedRandom => todo!(), + CommandType::Random => { + let mut max_int = None; + let o = self.get_state_mut().pop_evaluation_stack(); + + if let Some(v) = Value::get_int_value(o.as_ref()) { + max_int = Some(v); + } + + let o = self.get_state_mut().pop_evaluation_stack(); + + let mut min_int = None; + if let Some(v) = Value::get_int_value(o.as_ref()) { + min_int = Some(v); + } + + if min_int.is_none() { + panic!("Invalid value for the minimum parameter of RANDOM(min, max)"); + } + + if max_int.is_none() { + panic!("Invalid value for the maximum parameter of RANDOM(min, max)"); + } + + let min_value = min_int.unwrap(); + let max_value = max_int.unwrap(); + + let random_range = max_value - min_value + 1; + + if random_range <= 0 { + // TODO + // panic!(format!( + // "RANDOM was called with minimum as {} and maximum as {}. The maximum must be larger", + // min_value.to_string(), max_value.to_string() + // )); + + panic!(); + } + + let result_seed = self.get_state().story_seed + self.get_state().previous_random; + + let mut rng = StdRng::seed_from_u64(result_seed as u64); + let next_random = rng.gen::(); + + let chosen_value = (next_random % random_range as u32) as i32 + min_value; + + self.get_state_mut().push_evaluation_stack(Rc::new(Value::new_int(chosen_value))); + + self.get_state_mut().previous_random = self.get_state().previous_random + 1; + }, + CommandType::SeedRandom => { + let mut seed: Option = None; + + let o = self.get_state_mut().pop_evaluation_stack(); + + if let Some(v) = Value::get_int_value(o.as_ref()) {seed = Some(v);} + + if seed.is_none() { + panic!("Invalid value passed to SEED_RANDOM"); + } + + // Story seed affects both RANDOM and shuffle behaviour + self.get_state_mut().story_seed = seed.unwrap(); + self.get_state_mut().previous_random = 0; + + // SEED_RANDOM returns nothing. + self.get_state_mut().push_evaluation_stack(Rc::new(Void::new())); + }, CommandType::VisitIndex => { - let cpc = self.state.as_ref().unwrap().get_current_pointer().container.unwrap(); - let count = self.state.as_mut().unwrap().visit_count_for_container(&cpc) - 1; // index + let cpc = self.get_state().get_current_pointer().container.unwrap(); + let count = self.get_state_mut().visit_count_for_container(&cpc) - 1; // index // not // count - self.state.as_mut().unwrap().push_evaluation_stack(Rc::new(Value::new_int(count as i32))); + self.get_state_mut().push_evaluation_stack(Rc::new(Value::new_int(count as i32))); }, CommandType::SequenceShuffleIndex => todo!(), CommandType::StartThread => todo!(), @@ -963,19 +1040,19 @@ impl Story { // We may exist in the context of the initial // act of creating the thread, or in the context of // evaluating the content. - if self.state.as_ref().unwrap().get_callstack().borrow().can_pop_thread() { - self.state.as_ref().unwrap().get_callstack().as_ref().borrow_mut().pop_thread(); + if self.get_state().get_callstack().borrow().can_pop_thread() { + self.get_state().get_callstack().as_ref().borrow_mut().pop_thread(); } // In normal flow - allow safe exit without warning else { - self.state.as_mut().unwrap().set_did_safe_exit(true); + self.get_state_mut().set_did_safe_exit(true); // Stop flow in current thread - self.state.as_ref().unwrap().set_current_pointer(pointer::NULL.clone()); + self.get_state().set_current_pointer(pointer::NULL.clone()); } }, - CommandType::End => self.state.as_mut().unwrap().force_end(), + CommandType::End => self.get_state_mut().force_end(), CommandType::ListFromInt => todo!(), CommandType::ListRange => todo!(), CommandType::ListRandom => todo!(), @@ -988,7 +1065,7 @@ impl Story { // Variable assignment if let Some(var_ass) = content_obj.as_ref().as_any().downcast_ref::() { - let assigned_val = self.state.as_mut().unwrap().pop_evaluation_stack(); + let assigned_val = self.get_state_mut().pop_evaluation_stack(); // When in temporary evaluation, don't create new variables purely // within @@ -996,7 +1073,7 @@ impl Story { // var prioritiseHigherInCallStack = _temporaryEvaluationContainer // != null; - self.state.as_mut().unwrap().get_variables_state_mut().assign( var_ass, assigned_val); + self.get_state_mut().get_variables_state_mut().assign( var_ass, assigned_val); return true; } @@ -1008,14 +1085,14 @@ impl Story { // Explicit read count value if let Some(p) = &var_ref.path_for_count { let container = var_ref.get_container_for_count(); - let count = self.state.as_mut().unwrap().visit_count_for_container(container.as_ref().unwrap()); + let count = self.get_state_mut().visit_count_for_container(container.as_ref().unwrap()); found_value = Some(Rc::new(Value::new_int(count as i32))); } // Normal variable reference else { - found_value = self.state.as_ref().unwrap().get_variables_state().get_variable_with_name(&var_ref.name, -1); + found_value = self.get_state().get_variables_state().get_variable_with_name(&var_ref.name, -1); if let None = found_value { // TODO @@ -1028,17 +1105,17 @@ impl Story { } } - self.state.as_mut().unwrap().push_evaluation_stack(found_value.unwrap()); + self.get_state_mut().push_evaluation_stack(found_value.unwrap()); return true; } // Native function call if let Some(func) = content_obj.as_ref().as_any().downcast_ref::() { - let func_params = self.state.as_mut().unwrap().pop_evaluation_stack_multiple(func.get_number_of_parameters()); + let func_params = self.get_state_mut().pop_evaluation_stack_multiple(func.get_number_of_parameters()); let result = func.call(func_params); - self.state.as_mut().unwrap().push_evaluation_stack(result); + self.get_state_mut().push_evaluation_stack(result); return true; } @@ -1050,21 +1127,21 @@ impl Story { fn next_content(&mut self) { // Setting previousContentObject is critical for // VisitChangedContainersDueToDivert - let cp = self.state.as_ref().unwrap().get_current_pointer(); - self.state.as_mut().unwrap().set_previous_pointer(cp); + let cp = self.get_state().get_current_pointer(); + self.get_state_mut().set_previous_pointer(cp); // Divert step? - if !self.state.as_ref().unwrap().diverted_pointer.is_null() { - let dp = self.state.as_ref().unwrap().diverted_pointer.clone(); - self.state.as_mut().unwrap().set_current_pointer(dp); - self.state.as_mut().unwrap().set_diverted_pointer(pointer::NULL.clone()); + if !self.get_state().diverted_pointer.is_null() { + let dp = self.get_state().diverted_pointer.clone(); + self.get_state_mut().set_current_pointer(dp); + self.get_state_mut().set_diverted_pointer(pointer::NULL.clone()); // Internally uses state.previousContentObject and // state.currentContentObject self.visit_changed_containers_due_to_divert(); // Diverted location has valid content? - if !self.state.as_ref().unwrap().get_current_pointer().is_null() { + if !self.get_state().get_current_pointer().is_null() { return; } @@ -1082,31 +1159,31 @@ impl Story { let mut didPop = false; - let can_pop_type = self.state.as_ref().unwrap().get_callstack().as_ref().borrow().can_pop_type(Some(PushPopType::Function)); + let can_pop_type = self.get_state().get_callstack().as_ref().borrow().can_pop_type(Some(PushPopType::Function)); if can_pop_type { // Pop from the call stack - self.state.as_mut().unwrap().pop_callstack(Some(PushPopType::Function)); + self.get_state_mut().pop_callstack(Some(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 self.state.as_ref().unwrap().get_in_expression_evaluation() { - self.state.as_mut().unwrap().push_evaluation_stack(Rc::new(Void::new())); + if self.get_state().get_in_expression_evaluation() { + self.get_state_mut().push_evaluation_stack(Rc::new(Void::new())); } didPop = true; - } else if self.state.as_ref().unwrap().get_callstack().as_ref().borrow().can_pop_thread() { - self.state.as_ref().unwrap().get_callstack().as_ref().borrow_mut().pop_thread(); + } else if self.get_state().get_callstack().as_ref().borrow().can_pop_thread() { + self.get_state().get_callstack().as_ref().borrow_mut().pop_thread(); didPop = true; } else { - self.state.as_mut().unwrap().try_exit_function_evaluation_from_game(); + self.get_state_mut().try_exit_function_evaluation_from_game(); } // Step past the point where we last called out - if didPop && !self.state.as_ref().unwrap().get_current_pointer().is_null() { + if didPop && !self.get_state().get_current_pointer().is_null() { self.next_content(); } } @@ -1115,7 +1192,7 @@ impl Story { fn increment_content_pointer(&self) -> bool { let mut successful_increment = true; - let mut pointer = self.state.as_ref().unwrap().get_callstack().as_ref().borrow().get_current_element().current_pointer.clone(); + let mut pointer = self.get_state().get_callstack().as_ref().borrow().get_current_element().current_pointer.clone(); pointer.index += 1; let mut container= pointer.container.as_ref().unwrap().clone(); @@ -1152,7 +1229,7 @@ impl Story { pointer = pointer::NULL.clone(); } - self.state.as_ref().unwrap().get_callstack().as_ref().borrow_mut().get_current_element_mut().current_pointer = pointer; + self.get_state().get_callstack().as_ref().borrow_mut().get_current_element_mut().current_pointer = pointer; return successful_increment; } @@ -1161,7 +1238,7 @@ impl Story { // Don't include invisible choices for external usage. let mut choices = Vec::new(); - if let Some(current_choices) = self.state.as_ref().unwrap().get_current_choices() { + if let Some(current_choices) = self.get_state().get_current_choices() { for c in current_choices { if !c.is_invisible_default { c.index.replace(choices.len()); @@ -1174,11 +1251,11 @@ impl Story { } pub fn has_error(&self) -> bool { - self.state.as_ref().unwrap().has_error() + self.get_state().has_error() } pub fn get_current_errors(&self) -> &Vec { - self.state.as_ref().unwrap().get_current_errors() + self.get_state().get_current_errors() } pub fn choose_choice_index(&mut self, choice_index: usize) { @@ -1191,13 +1268,13 @@ impl Story { // can create multiple leading edges for the story, each of // which has its own context. let choice_to_choose = choices.get(choice_index).unwrap(); - self.state.as_ref().unwrap().get_callstack().borrow_mut().set_current_thread(choice_to_choose.get_thread_at_generation().unwrap()); + self.get_state().get_callstack().borrow_mut().set_current_thread(choice_to_choose.get_thread_at_generation().unwrap()); self.choose_path(&choice_to_choose.target_path, true); } fn choose_path(&mut self, p: &Path, incrementing_turn_index: bool) { - self.state.as_mut().unwrap().set_chosen_path( &p, incrementing_turn_index); + self.get_state_mut().set_chosen_path( &p, incrementing_turn_index); // Take a note of newly visited containers for read counts etc self.visit_changed_containers_due_to_divert(); @@ -1226,7 +1303,7 @@ impl Story { // Don't create choice if choice point doesn't pass conditional if choice_point.has_condition() { - let condition_value = self.state.as_mut().unwrap().pop_evaluation_stack(); + let condition_value = self.get_state_mut().pop_evaluation_stack(); if !self.is_truthy(condition_value) { show_choice = false; } @@ -1246,7 +1323,7 @@ impl Story { // Don't create choice if player has already read this content if choice_point.once_only() { - let visit_count = self.state.as_mut().unwrap().visit_count_for_container(choice_point.get_choice_target().as_ref().unwrap()); + let visit_count = self.get_state_mut().visit_count_for_container(choice_point.get_choice_target().as_ref().unwrap()); if visit_count > 0 { show_choice = false; } @@ -1261,18 +1338,18 @@ impl Story { start_text.push_str(&choice_only_text); - let choice = Rc::new(Choice::new(choice_point.get_path_on_choice(), Object::get_path(choice_point.as_ref()).to_string(), choice_point.is_invisible_default(), tags, self.state.as_ref().unwrap().get_callstack().borrow_mut().fork_thread(), start_text.trim().to_string(), 0, 0)); + let choice = Rc::new(Choice::new(choice_point.get_path_on_choice(), Object::get_path(choice_point.as_ref()).to_string(), choice_point.is_invisible_default(), tags, self.get_state().get_callstack().borrow_mut().fork_thread(), start_text.trim().to_string(), 0, 0)); Some(choice) } fn pop_choice_string_and_tags(&mut self, tags: &[String]) -> String { - let obj = self.state.as_mut().unwrap().pop_evaluation_stack(); + let obj = self.get_state_mut().pop_evaluation_stack(); let choice_only_str_val = Value::get_string_value(obj.as_ref()).unwrap(); // TODO - // while (self.state.as_ref().unwrap().evaluation_stack.len() > 0 && self.state.as_ref().unwrap().peek_evaluation_stack() instanceof Tag) { + // while (self.get_state().evaluation_stack.len() > 0 && self.get_state().peek_evaluation_stack() instanceof Tag) { // Tag tag = (Tag) state.popEvaluationStack(); // tags.add(0, tag.getText()); // popped in reverse order // } @@ -1324,8 +1401,8 @@ impl Story { } fn visit_changed_containers_due_to_divert(&mut self) { - let previous_pointer = self.state.as_ref().unwrap().get_previous_pointer(); - let pointer = self.state.as_ref().unwrap().get_current_pointer(); + let previous_pointer = self.get_state().get_previous_pointer(); + let pointer = self.get_state().get_current_pointer(); // 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. @@ -1398,6 +1475,54 @@ impl Story { } } } + + // TODO: The result and the args should be an object not a String + pub fn evaluate_function(&mut self, func_name: &str, args: Option<&Vec>, text_output: &mut String) -> Result, String> { + // TODO ifAsyncWeCant("evaluate a function"); + + if func_name.trim().is_empty() { + return Err("Function is empty or white space.".to_owned()); + } + + // Get the content that we need to run + let func_container = self.knot_container_with_name(func_name); + if func_container.is_none() { + let mut e = "Function doesn't exist: '".to_owned(); + e.push_str(func_name); + e.push_str("'"); + + return Err(e); + } + + // Snapshot the output stream + let output_stream_before = self.get_state().get_output_stream().clone(); + self.get_state_mut().reset_output(None); + + // State will temporarily replace the callstack in order to evaluate + self.get_state_mut().start_function_evaluation_from_game(func_container.unwrap(), args); + + // Evaluate the function, and collect the string output + while self.can_continue() { + let text = self.cont()?; + + text_output.push_str(&text); + } + + // Restore the output stream in case this was called + // during main story evaluation. + self.get_state_mut().reset_output(Some(output_stream_before)); + + // Finish evaluation, and see whether anything was produced + let result = self.get_state_mut().complete_function_evaluation_from_game(); + + return result; + } + + fn knot_container_with_name(&self, name: &str) -> Option> { + let named_container = self.main_content_container.named_content.get(name); + + named_container.cloned() + } } diff --git a/src/story_state.rs b/src/story_state.rs index 0e4f05a..7509246 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -2,7 +2,7 @@ use std::{rc::Rc, cell::RefCell, collections::HashMap}; -use crate::{pointer::{Pointer, self}, callstack::CallStack, flow::Flow, variables_state::VariablesState, choice::Choice, object::{RTObject, Object}, value::{Value, ValueType}, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}, container::Container, state_patch::StatePatch, story::Story, path::Path}; +use crate::{pointer::{Pointer, self}, callstack::CallStack, flow::Flow, variables_state::VariablesState, choice::Choice, object::{RTObject, Object}, value::{Value, ValueType}, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}, container::Container, state_patch::StatePatch, story::Story, path::Path, void::Void}; use rand::Rng; @@ -29,8 +29,8 @@ pub struct StoryState { visit_counts: HashMap, turn_indices: HashMap, current_turn_index: i32, - story_seed: i32, - previous_random: i32, + pub story_seed: i32, + pub previous_random: i32, } impl StoryState { @@ -906,4 +906,87 @@ impl StoryState { self.evaluation_stack.last() } + pub fn start_function_evaluation_from_game(&mut self, func_container: Rc, arguments: Option<&Vec>) -> Result<(), String> { + self.get_callstack().borrow_mut().push(PushPopType::FunctionEvaluationFromGame, self.evaluation_stack.len(), 0); + self.get_callstack().borrow_mut().get_current_element_mut().current_pointer = Pointer::start_of(func_container); + + self.pass_arguments_to_evaluation_stack(arguments)?; + + Ok(()) + } + + fn pass_arguments_to_evaluation_stack(&mut self, arguments: Option<&Vec>) -> Result<(), String> { + // Pass arguments onto the evaluation stack + if let Some(arguments) = arguments { + for arg in arguments { + // TODO + + // 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())); + // } + + self.push_evaluation_stack(Rc::new(Value::new_string(arg))); + } + } + + Ok(()) + } + + pub fn complete_function_evaluation_from_game(&mut self) -> Result, String> { + if self.get_callstack().borrow().get_current_element().push_pop_type != PushPopType::FunctionEvaluationFromGame { + // TODO + // return Err(format!("Expected external function evaluation to be complete. Stack trace: {}", getCallStack().getCallStackTrace()); + + return Err("Expected external function evaluation to be complete. Stack trace".to_owned()); + } + + let original_evaluation_stack_height = self.get_callstack().borrow().get_current_element().evaluation_stack_height_when_pushed; + + // 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) + let mut returned_obj = None; + while self.evaluation_stack.len() > original_evaluation_stack_height { + let popped_obj = self.pop_evaluation_stack(); + if returned_obj.is_none() { + returned_obj = Some(popped_obj); + } + } + + // Finally, pop the external function evaluation + self.get_callstack().borrow_mut().pop(Some(PushPopType::FunctionEvaluationFromGame)); + + // What did we get back? + if let Some(returned_obj) = returned_obj{ + if let Some(_) = returned_obj.as_ref().as_any().downcast_ref::() { return Ok(None); } + + // Some kind of value, if not void + if let Some(return_val) = returned_obj.as_ref().as_any().downcast_ref::() { + // DivertTargets get returned as the string of components + // (rather than a Path, which isn't public) + if let ValueType::DivertTarget(p) = &return_val.value { + return Ok(Some(p.to_string())); + } + + // Other types can ust have their exact object type: + // int, float, string. VariablePointers get returned as strings. + // TODO + return Ok(Some(return_val.to_string())); + } + } + + Ok(None) + } + } \ No newline at end of file diff --git a/src/value.rs b/src/value.rs index 2e136ef..85e2447 100644 --- a/src/value.rs +++ b/src/value.rs @@ -147,6 +147,16 @@ impl Value { } } + pub fn get_int_value(o: &dyn RTObject) -> Option { + match o.as_any().downcast_ref::() { + Some(v) => match &v.value { + ValueType::Int(v) => Some(*v), + _ => None, + }, + None => None, + } + } + pub fn get_cast_ordinal(&self) -> u8 { let v = &self.value; diff --git a/tests/function_test.rs b/tests/function_test.rs index ee20ffd..34b44ce 100644 --- a/tests/function_test.rs +++ b/tests/function_test.rs @@ -13,5 +13,125 @@ fn fun_basic_test() -> Result<(), String> { assert_eq!(1, text.len()); assert_eq!("The value of x is 4.4.", text[0]); + Ok(()) +} + +#[test] +fn fun_none_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/function/func-none.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The value of x is 3.8.", text[0]); + + Ok(()) +} + +#[test] +fn fun_inline_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/function/func-inline.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The value of x is 4.4.", text[0]); + + Ok(()) +} + +#[test] +fn setvar_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/function/setvar-func.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The value is 6.", text[0]); + + Ok(()) +} + +#[test] +fn complex_func1_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/function/complex-func1.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The values are 6 and 10.", text[0]); + + Ok(()) +} + +#[test] +fn complex_func2_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/function/complex-func2.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The values are -1 and 0 and 1.", text[0]); + + Ok(()) +} + +#[test] +fn complex_func3_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/function/complex-func3.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("\"I will pay you 120 reales if you get the goods to their destination. The goods will take up 20 cargo spaces.\"", + text[0]); + + Ok(()) +} + +#[test] +fn rnd() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/function/rnd-func.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(4, text.len()); + assert_eq!("Rolling dice 1: 1.", text[0]); + assert_eq!("Rolling dice 2: 4.", text[1]); + assert_eq!("Rolling dice 3: 4.", text[2]); + assert_eq!("Rolling dice 4: 1.", text[3]); + + Ok(()) +} + +#[test] +fn evaluating_function_variable_state_bug_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/function/evaluating-function-variablestate-bug.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + + assert_eq!("Start\n", story.cont()?); + assert_eq!("In tunnel.\n", story.cont()?); + + let mut output = String::new(); + let result = story.evaluate_function("function_to_evaluate", None, &mut output); + + assert_eq!("RIGHT", result?.unwrap()); + assert_eq!("End\n", story.cont()?); + Ok(()) } \ No newline at end of file From 9d50a33b69ecfe4eb62e49793b4800796767e2e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Mon, 25 Sep 2023 10:24:37 +0000 Subject: [PATCH 31/91] All conditional tests pass. --- src/story.rs | 51 ++++- src/story_state.rs | 1 - tests/conditional_test.rs | 466 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 515 insertions(+), 3 deletions(-) diff --git a/src/story.rs b/src/story.rs index 2e1ab07..9fcd112 100644 --- a/src/story.rs +++ b/src/story.rs @@ -760,7 +760,6 @@ impl Story { if let Some(target) = Value::get_divert_target_value(var_contents.as_ref()) { let p = Self::pointer_at_path(&self.main_content_container, target); self.get_state_mut().set_diverted_pointer(p); - println!("SET DIVERTED POINTER: {} PATH: {}", self.get_state_mut().diverted_pointer, target); } else { // TODO // let int_content = var_contents.downcast_ref::(); @@ -1034,7 +1033,11 @@ impl Story { // count self.get_state_mut().push_evaluation_stack(Rc::new(Value::new_int(count as i32))); }, - CommandType::SequenceShuffleIndex => todo!(), + CommandType::SequenceShuffleIndex => { + let shuffle_index = self.next_sequence_shuffle_index(); + let v = Rc::new(Value::new_int(shuffle_index)); + self.get_state_mut().push_evaluation_stack(v); + }, CommandType::StartThread => todo!(), CommandType::Done => { // We may exist in the context of the initial @@ -1523,6 +1526,50 @@ impl Story { named_container.cloned() } + + fn next_sequence_shuffle_index(&mut self) -> i32 { + let pop_evaluation_stack = self.get_state_mut().pop_evaluation_stack(); + let num_elements = if let Some(v) = Value::get_int_value(pop_evaluation_stack.as_ref()) { + v + } else { + panic!("Expected number of elements in sequence for shuffle index"); + }; + + let seq_container = self.get_state().get_current_pointer().container.unwrap(); + + let seq_count = if let Some(v) = Value::get_int_value(pop_evaluation_stack.as_ref()) { + v + } else { + panic!("Expected sequence count value for shuffle index"); + }; + + let loop_index = seq_count / num_elements; + let iteration_index = seq_count % num_elements; + + // 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 + let seq_path_str = Object::get_path(seq_container.as_ref()).to_string(); + let sequence_hash: i32 = seq_path_str.chars().map(|c| c as i32).sum(); + let random_seed = sequence_hash + loop_index + self.get_state().story_seed; + + let mut rng = StdRng::seed_from_u64(random_seed as u64); + + let mut unpicked_indices: Vec = (0..num_elements).collect(); + + for i in 0..=iteration_index { + let chosen = rng.gen::().rem_euclid(unpicked_indices.len() as i32); + let chosen_index = unpicked_indices[chosen as usize]; + unpicked_indices.retain(|&x| x != chosen_index); + + if i == iteration_index { + return chosen_index; + } + } + + panic!("Should never reach here"); + } } diff --git a/src/story_state.rs b/src/story_state.rs index 7509246..2bb5518 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -308,7 +308,6 @@ impl StoryState { panic!() } - println!("POINTER: {}", pointer.to_string()); self.get_callstack().as_ref().borrow_mut().get_current_element_mut().current_pointer = pointer; } diff --git a/tests/conditional_test.rs b/tests/conditional_test.rs index 6a8da18..cc1b249 100644 --- a/tests/conditional_test.rs +++ b/tests/conditional_test.rs @@ -17,3 +17,469 @@ fn iftrue_test() -> Result<(), String> { Ok(()) } + +#[test] +fn iffalse_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/conditional/iffalse.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + + let mut text: Vec = Vec::new(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The value is 3.", text[0]); + + Ok(()) +} + +#[test] +fn ifelse_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/conditional/ifelse.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + + let mut text: Vec = Vec::new(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The value is 1.", text[0]); + + Ok(()) +} + +#[test] +fn ifelse_ext_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/conditional/ifelse-ext.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + + let mut text: Vec = Vec::new(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("The value is -1.", text[0]); + + Ok(()) +} + +#[test] +fn ifelse_ext_text1_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/conditional/ifelse-ext-text1.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + + let mut text: Vec = Vec::new(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("This is text 1.", text[0]); + assert_eq!(1, story.get_current_choices().len()); + story.choose_choice_index(0); + + common::next_all(&mut story, &mut text)?; + assert_eq!(2, text.len()); + assert_eq!("This is the end.", text[1]); + + Ok(()) +} + +#[test] +fn ifelse_ext_text2_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/conditional/ifelse-ext-text2.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + + let mut text: Vec = Vec::new(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("This is text 2.", text[0]); + assert_eq!(1, story.get_current_choices().len()); + story.choose_choice_index(0); + + common::next_all(&mut story, &mut text)?; + assert_eq!(2, text.len()); + assert_eq!("This is the end.", text[1]); + + Ok(()) +} + +#[test] +fn ifelse_ext_text3_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/conditional/ifelse-ext-text3.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + + let mut text: Vec = Vec::new(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("This is text 3.", text[0]); + assert_eq!(1, story.get_current_choices().len()); + story.choose_choice_index(0); + + common::next_all(&mut story, &mut text)?; + assert_eq!(2, text.len()); + assert_eq!("This is the end.", text[1]); + + Ok(()) +} + +#[test] +fn cond_text1_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/conditional/condtext.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(0); + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(3, text.len()); + assert_eq!("I stared at Monsieur Fogg. \"But surely you are not serious?\" I demanded.", text[1]); + + Ok(()) +} + +#[test] +fn cond_text2_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/conditional/condtext.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(1); + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(2, text.len()); + assert_eq!("I stared at Monsieur Fogg. \"But there must be a reason for this trip,\" I observed.", text[0]); + + Ok(()) +} + +#[test] +fn cond_opt1_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/conditional/condopt.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(0); + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, story.get_current_choices().len()); + + Ok(()) +} + +#[test] +fn cond_opt2_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/conditional/condopt.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(1); + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(2, story.get_current_choices().len()); + + Ok(()) +} + +#[test] +fn stopping_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/conditional/stopping.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("I entered the casino.", text[0]); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("I entered the casino again.", text[0]); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("Once more, I went inside.", text[0]); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("Once more, I went inside.", text[0]); + story.choose_choice_index(0); + + Ok(()) +} + +#[test] +fn cycle_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/conditional/cycle.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("I held my breath.", text[0]); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("I waited impatiently.", text[0]); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("I paused.", text[0]); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("I held my breath.", text[0]); + story.choose_choice_index(0); + + Ok(()) +} + +#[test] +fn once_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/conditional/once.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("Would my luck hold?", text[0]); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("Could I win the hand?", text[0]); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(0, text.len()); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(0, text.len()); + + Ok(()) +} + +#[test] +fn shuffle_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/conditional/shuffle.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + + // No check of the result, as that is random + + Ok(()) +} + +#[test] +fn shuffle_stopping() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/conditional/shuffle_stopping.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("final", text[0]); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("final", text[0]); + + // No check of the result, as that is random + + Ok(()) +} + +#[test] +fn shuffle_once() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/conditional/shuffle_once.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(0, text.len()); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(0, text.len()); + + // No check of the result, as that is random + + Ok(()) +} + +#[test] +fn multiline_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/conditional/multiline.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("At the table, I drew a card. Ace of Hearts.", text[0]); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(2, text.len()); + assert_eq!("I drew a card. 2 of Diamonds.", text[0]); + assert_eq!("\"Should I hit you again,\" the croupier asks.", text[1]); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(2, text.len()); + assert_eq!("I drew a card. King of Spades.", text[0]); + assert_eq!("\"You lose,\" he crowed.", text[1]); + + Ok(()) +} + +#[test] +fn multiline_divert_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/conditional/multiline-divert.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("At the table, I drew a card. Ace of Hearts.", text[0]); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(2, text.len()); + assert_eq!("I drew a card. 2 of Diamonds.", text[0]); + assert_eq!("\"Should I hit you again,\" the croupier asks.", text[1]); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(2, text.len()); + assert_eq!("I drew a card. King of Spades.", text[0]); + assert_eq!("\"You lose,\" he crowed.", text[1]); + + Ok(()) +} + +#[test] +fn multiline_choice_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/conditional/multiline-choice.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("At the table, I drew a card. Ace of Hearts.", text[0]); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(2, story.get_current_choices().len()); + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("I left the table.", text[0]); + + Ok(()) +} \ No newline at end of file From cde2f25e580cf8ad5d1afdfe867ba20534f8098e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Mon, 25 Sep 2023 10:53:39 +0000 Subject: [PATCH 32/91] Threads are working! --- src/callstack.rs | 7 ++++++ src/story.rs | 26 +++++++++++---------- tests/thread_test.rs | 55 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 12 deletions(-) create mode 100644 tests/thread_test.rs diff --git a/src/callstack.rs b/src/callstack.rs index ae1fd54..3eab835 100644 --- a/src/callstack.rs +++ b/src/callstack.rs @@ -130,6 +130,13 @@ impl CallStack { } } + pub fn push_thread(&mut self) { + let mut newThread = self.get_current_thread().copy(); + self.thread_counter += 1; + newThread.thread_index = self.thread_counter; + self.threads.push(newThread); + } + pub fn can_pop(&self) -> bool { self.get_callstack().len() > 1 } diff --git a/src/story.rs b/src/story.rs index 9fcd112..5a38b33 100644 --- a/src/story.rs +++ b/src/story.rs @@ -604,11 +604,11 @@ impl Story { // Expression evaluation content if self.get_state().get_in_expression_evaluation() { - self.get_state_mut().push_evaluation_stack(current_content_obj.unwrap()); + self.get_state_mut().push_evaluation_stack(current_content_obj.as_ref().unwrap().clone()); } // Output stream content (i.e. not expression evaluation) else { - self.get_state_mut().push_to_output_stream(current_content_obj.unwrap()); + self.get_state_mut().push_to_output_stream(current_content_obj.as_ref().unwrap().clone()); } } @@ -619,13 +619,14 @@ impl Story { // pointer, // so that when returning from the thread, it returns to the content // after this instruction. - - // TODO - // let controlCmd = - // currentContentObj instanceof ControlCommand ? (ControlCommand) currentContentObj : null; - // if (controlCmd != null && controlCmd.getCommandType() == ControlCommand.CommandType.StartThread) { - // state.getCallStack().pushThread(); - // } + if current_content_obj.is_some() { + if let Some(control_cmd) = current_content_obj.as_ref().unwrap().as_any().downcast_ref::() { + if control_cmd.command_type == CommandType::StartThread { + self.get_state().get_callstack().borrow_mut().push_thread(); + } + } + } + } fn try_follow_default_invisible_choice(&mut self) { @@ -957,8 +958,7 @@ impl Story { CommandType::NoOp => {}, CommandType::ChoiceCount => todo!(), CommandType::Turns => todo!(), - CommandType::TurnsSince => todo!(), - CommandType::ReadCount => todo!(), + CommandType::TurnsSince | CommandType::ReadCount => todo!(), CommandType::Random => { let mut max_int = None; let o = self.get_state_mut().pop_evaluation_stack(); @@ -1038,7 +1038,9 @@ impl Story { let v = Rc::new(Value::new_int(shuffle_index)); self.get_state_mut().push_evaluation_stack(v); }, - CommandType::StartThread => todo!(), + CommandType::StartThread => { + // Handled in main step function + }, CommandType::Done => { // We may exist in the context of the initial // act of creating the thread, or in the context of diff --git a/tests/thread_test.rs b/tests/thread_test.rs new file mode 100644 index 0000000..b91ae34 --- /dev/null +++ b/tests/thread_test.rs @@ -0,0 +1,55 @@ +use bladeink::story::Story; + +mod common; + +#[test] +fn thread_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/threads/thread-bug.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); + + assert_eq!("Here is some gold. Do you want it?\n", story.continue_maximally()?); + assert_eq!(2, story.get_current_choices().len()); + assert_eq!("No", story.get_current_choices()[0].text); + assert_eq!("Yes", story.get_current_choices()[1].text); + story.choose_choice_index(0); + + assert_eq!("No\nTry again!\n", story.continue_maximally()?); + assert_eq!(2, story.get_current_choices().len()); + assert_eq!("No", story.get_current_choices()[0].text); + assert_eq!("Yes", story.get_current_choices()[1].text); + story.choose_choice_index(1); + + assert_eq!("Yes\nYou win!\n", story.continue_maximally()?); + + + Ok(()) +} + +#[test] +fn thread_test_bug() -> Result<(), String> { + //TODO + + // let json_string = + // common::get_json_string("examples/inkfiles/threads/thread-bug.ink.json").unwrap(); + // let mut story = Story::new(&json_string).unwrap(); + // println!("{}", story.build_string_of_hierarchy()); + + // assert_eq!("Here is some gold. Do you want it?\n", story.continue_maximally()?); + // assert_eq!(2, story.get_current_choices().len()); + // assert_eq!("No", story.get_current_choices()[0].text); + // assert_eq!("Yes", story.get_current_choices()[1].text); + // story.choose_choice_index(0); + + // assert_eq!("No\nTry again!\n", story.continue_maximally()?); + // assert_eq!(2, story.get_current_choices().len()); + // assert_eq!("No", story.get_current_choices()[0].text); + // assert_eq!("Yes", story.get_current_choices()[1].text); + // story.choose_choice_index(1); + + // assert_eq!("Yes\nYou win!\n", story.continue_maximally()?); + + + Ok(()) +} \ No newline at end of file From 9ede7a2aa23f7c7ea3d00646dee2796c34b34406 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Mon, 25 Sep 2023 16:19:10 +0000 Subject: [PATCH 33/91] All tags tests passed. --- README.md | 7 +- src/flow.rs | 1 + src/story.rs | 187 ++++++++++++++++++++++++++++++++++++++++++--- src/story_state.rs | 105 ++++++++++++------------- src/tag.rs | 4 +- tests/tag_test.rs | 102 +++++++++++++++++++++++++ 6 files changed, 336 insertions(+), 70 deletions(-) create mode 100644 tests/tag_test.rs diff --git a/README.md b/README.md index 8a4f832..b4f8647 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,14 @@ Currently under development. This is the implementation status: - [x] Variable Text - [x] Conditional Text - [ ] Game Queries and Functions -- [ ] Nested flows +- [x] Nested flows - [ ] Variables and Logic -- [ ] Conditional blocks (if/else) +- [x] Conditional blocks (if/else) - [ ] Temporary Variables - [x] Functions - [x] Tunnels -- [ ] Threads +- [x] Threads +- [x] Tags - [ ] Lists - [ ] Load/Save state diff --git a/src/flow.rs b/src/flow.rs index b66b536..7a6718d 100644 --- a/src/flow.rs +++ b/src/flow.rs @@ -2,6 +2,7 @@ use std::{rc::Rc, cell::RefCell}; use crate::{callstack::CallStack, choice::Choice, object::RTObject, container::Container}; +#[derive(Clone)] pub struct Flow { pub name: String, pub callstack: Rc>, diff --git a/src/story.rs b/src/story.rs index 5a38b33..46dd31e 100644 --- a/src/story.rs +++ b/src/story.rs @@ -1061,8 +1061,80 @@ impl Story { CommandType::ListFromInt => todo!(), CommandType::ListRange => todo!(), CommandType::ListRandom => todo!(), - CommandType::BeginTag => todo!(), - CommandType::EndTag => todo!(), + CommandType::BeginTag => self.get_state_mut().push_to_output_stream(content_obj.clone()), + CommandType::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 self.get_state().in_string_evaluation() { + + let mut content_stack_for_tag: Vec = Vec::new(); + let mut output_count_consumed = 0; + + for i in (0..self.get_state().get_output_stream().len()).rev() { + let obj = &self.get_state().get_output_stream()[i]; + + output_count_consumed += 1; + + if let Some(command) = obj.as_ref().as_any().downcast_ref::() { + if command.command_type == CommandType::BeginTag { + break; + } else { + panic!("Unexpected ControlCommand while extracting tag from choice"); + //break; + } + } + + if let Some(sv) = Value::get_string_value(obj.as_ref()) { + content_stack_for_tag.push(sv.string.clone()); + } + } + + // Consume the content that was produced for this string + self.get_state_mut().pop_from_output_stream(output_count_consumed); + + let mut sb = String::new(); + for str_val in &content_stack_for_tag { + sb.push_str(&str_val); + } + + let choice_tag = Rc::new(Tag::new(&StoryState::clean_output_whitespace(&sb))); + // Pushing to the evaluation stack means it gets picked up + // when a Choice is generated from the next Choice Point. + self.get_state_mut().push_evaluation_stack(choice_tag); + } + + // Otherwise! Simply push EndTag, so that in the output stream we + // have a structure of: [BeginTag, "the tag content", EndTag] + else { + self.get_state_mut().push_to_output_stream(content_obj.clone()); + } + }, } return true; @@ -1348,16 +1420,14 @@ impl Story { Some(choice) } - fn pop_choice_string_and_tags(&mut self, tags: &[String]) -> String { + fn pop_choice_string_and_tags(&mut self, tags: &mut Vec) -> String { let obj = self.get_state_mut().pop_evaluation_stack(); let choice_only_str_val = Value::get_string_value(obj.as_ref()).unwrap(); - // TODO - - // while (self.get_state().evaluation_stack.len() > 0 && self.get_state().peek_evaluation_stack() instanceof Tag) { - // Tag tag = (Tag) state.popEvaluationStack(); - // tags.add(0, tag.getText()); // popped in reverse order - // } + while self.get_state().evaluation_stack.len() > 0 && self.get_state().peek_evaluation_stack().unwrap().as_any().is::() { + let tag = self.get_state_mut().pop_evaluation_stack().into_any().downcast::().unwrap(); + tags.insert(0, tag.get_text().clone()); // popped in reverse order + } return choice_only_str_val.string.to_string(); } @@ -1572,6 +1642,105 @@ impl Story { panic!("Should never reach here"); } + + pub fn get_global_tags(&self) -> Result, String> { + self.tags_at_start_of_flow_container_with_path_string("") + } + + pub fn tags_for_content_at_path(&self, path: &str) -> Result, String> { + self.tags_at_start_of_flow_container_with_path_string(path) + } + + fn tags_at_start_of_flow_container_with_path_string(&self, path_string: &str) -> Result, String> { + let path = Path::new_with_components_string(Some(path_string)); + + // Expected to be global story, knot, or stitch + let mut flow_container = self.content_at_path(&path).container().unwrap(); + + while let Some(first_content) = flow_container.content.get(0) { + if let Ok(container) = first_content.clone().into_any().downcast::() { + flow_container = container; + } else { + break; + } + } + + // Any initial tag objects count as the "main tags" associated with that + // story/knot/stitch + let mut in_tag = false; + let mut tags = Vec::new(); + + for content in &flow_container.content { + match content.as_ref().as_any().downcast_ref::() { + Some(command) => { + match command.command_type { + CommandType::BeginTag => in_tag = true, + CommandType::EndTag => in_tag = false, + _ => {} + } + } + _ => { + if in_tag { + if let Some(string_value) = Value::get_string_value(content.as_ref()) { + tags.push(string_value.string.clone()); + } else { + return Err( + "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()".to_owned(), + ); + } + } else { + break; + } + } + } + } + + Ok(tags) + } + + fn content_at_path(&self, path: &Path) -> SearchResult { + self.main_content_container.content_at_path(path, 0, -1) + } + + pub fn get_current_tags(&mut self) -> Vec { + // TODO ifAsyncWeCant("call currentTags since it's a work in progress"); + return self.get_state_mut().get_current_tags(); + } + + pub fn choose_path_string(&mut self, path: &str, reset_call_stack: bool, args: Option<&Vec>) -> Result<(), String> { + // TODO ifAsyncWeCant("call ChoosePathString right now"); + + if reset_call_stack { + self.reset_callstack(); + } 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 self.get_state().get_callstack().borrow().get_current_element().push_pop_type == PushPopType::Function { + let mut func_detail = "".to_owned(); + let container = self.get_state().get_callstack().borrow().get_current_element().current_pointer.container.clone(); + if let Some(container) = container { + func_detail = format!("({})", Object::get_path(container.as_ref()).to_string()); + } + + // Err("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()); + return Err("Story was running a function".to_owned()); + } + } + + self.get_state_mut().pass_arguments_to_evaluation_stack(args); + self.choose_path(&Path::new_with_components_string(Some(path)), true); + + Ok(()) + } + + fn reset_callstack(&mut self) { + // TODO ifAsyncWeCant("ResetCallstack"); + + self.get_state_mut().force_end(); + } } diff --git a/src/story_state.rs b/src/story_state.rs index 2bb5518..b1d2b21 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -2,7 +2,7 @@ use std::{rc::Rc, cell::RefCell, collections::HashMap}; -use crate::{pointer::{Pointer, self}, callstack::CallStack, flow::Flow, variables_state::VariablesState, choice::Choice, object::{RTObject, Object}, value::{Value, ValueType}, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}, container::Container, state_patch::StatePatch, story::Story, path::Path, void::Void}; +use crate::{pointer::{Pointer, self}, callstack::CallStack, flow::Flow, variables_state::VariablesState, choice::Choice, object::{RTObject, Object}, value::{Value, ValueType}, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}, container::Container, state_patch::StatePatch, story::Story, path::Path, void::Void, tag::Tag}; use rand::Rng; @@ -31,6 +31,7 @@ pub struct StoryState { current_turn_index: i32, pub story_seed: i32, pub previous_random: i32, + current_tags: Vec, } impl StoryState { @@ -61,6 +62,7 @@ impl StoryState { current_turn_index: -1, story_seed: story_seed, previous_random: 0, + current_tags: Vec::with_capacity(0), }; state.go_to_start(); @@ -183,7 +185,7 @@ impl StoryState { self.current_text = Some(StoryState::clean_output_whitespace(&sb)); - self.output_stream_tags_dirty = false; + self.output_stream_text_dirty = false; } self.current_text.as_ref().unwrap().to_string() @@ -191,65 +193,57 @@ impl StoryState { pub fn get_current_tags(&mut self) -> Vec { if self.output_stream_tags_dirty { - let mut current_tags = Vec::new(); + self.current_tags.clear(); + let mut in_tag = false; let mut sb = String::new(); - - // TODO - // for output_obj in self.get_output_stream().iter() { - // match output_obj { - // RTObject::ControlCommand(control_command) => { - // match control_command.get_command_type() { - // ControlCommandType::BeginTag => { - // if in_tag && !sb.is_empty() { - // let txt = clean_output_whitespace(&sb); - // current_tags.push(txt); - // sb.clear(); - // } - // in_tag = true; - // } - // ControlCommandType::EndTag => { - // if !sb.is_empty() { - // let txt = clean_output_whitespace(&sb); - // current_tags.push(txt); - // sb.clear(); - // } - // in_tag = false; - // } - // _ => {} - // } - // } - // RTObject::StringValue(str_val) => { - // if in_tag { - // sb.push_str(&str_val.value); - // } - // } - // RTObject::Tag(tag) => { - // if let Some(text) = &tag.get_text() { - // if !text.is_empty() { - // current_tags.push(text.clone()); // tag.text has whitespace already cleaned - // } - // } - // } - // _ => {} - // } - // } + for output_obj in self.get_output_stream().clone() { + if let Some(control_command) = output_obj.as_ref().as_any().downcast_ref::() { + match control_command.command_type { + CommandType::BeginTag => { + if in_tag && !sb.is_empty() { + let txt = Self::clean_output_whitespace(&sb); + self.current_tags.push(txt); + sb.clear(); + } + in_tag = true; + }, + CommandType::EndTag => { + if !sb.is_empty() { + let txt = Self::clean_output_whitespace(&sb); + self.current_tags.push(txt); + sb.clear(); + } + in_tag = false; + }, + _ => {}, + } + } else if in_tag { + if let Some(string_value) = Value::get_string_value(output_obj.as_ref()) { + sb.push_str(&string_value.string); + } + if let Some(tag) = output_obj.as_ref().as_any().downcast_ref::() { + if !tag.get_text().is_empty() { + self.current_tags.push(tag.get_text().clone()); // tag.text has whitespace already cleaned + } + } + } + } if !sb.is_empty() { - let txt = StoryState::clean_output_whitespace(&sb); - current_tags.push(txt); + let txt = Self::clean_output_whitespace(&sb); + self.current_tags.push(txt); sb.clear(); } self.output_stream_tags_dirty = false; - current_tags - } else { - Vec::new() } + + self.current_tags.clone() } - fn clean_output_whitespace(input_str: &str) -> String { + pub fn clean_output_whitespace(input_str: &str) -> String { let mut sb = String::with_capacity(input_str.len()); let mut current_whitespace_start = -1; let mut start_of_line = 0; @@ -727,12 +721,11 @@ impl StoryState { // (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 let Some(named_flows) = &self.named_flows { - // TODO - // 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; + let mut nf = self.named_flows.clone(); + nf.as_mut().unwrap().insert(copy.current_flow.name.to_string(), copy.current_flow.clone()); + copy.alive_flow_names_dirty = true; + + copy.named_flows = nf; } if self.has_error() { @@ -914,7 +907,7 @@ impl StoryState { Ok(()) } - fn pass_arguments_to_evaluation_stack(&mut self, arguments: Option<&Vec>) -> Result<(), String> { + pub fn pass_arguments_to_evaluation_stack(&mut self, arguments: Option<&Vec>) -> Result<(), String> { // Pass arguments onto the evaluation stack if let Some(arguments) = arguments { for arg in arguments { diff --git a/src/tag.rs b/src/tag.rs index e588cc0..b933cb9 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -12,8 +12,8 @@ impl Tag { Tag {obj: Object::new(), text: text.to_string()} } - pub fn get_text(&self) -> String { - self.text.clone() + pub fn get_text(&self) -> &String { + &self.text } } diff --git a/tests/tag_test.rs b/tests/tag_test.rs new file mode 100644 index 0000000..29bffe9 --- /dev/null +++ b/tests/tag_test.rs @@ -0,0 +1,102 @@ +use bladeink::story::Story; + +mod common; + +#[test] +fn tags_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/tags/tags.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + + let global_tags = story.get_global_tags()?; + assert_eq!(2, global_tags.len()); + assert_eq!("author: Joe", global_tags[0]); + assert_eq!("title: My Great Story", global_tags[1]); + + assert_eq!("This is the content\n", story.cont()?); + + let current_tags = story.get_current_tags(); + assert_eq!(2, current_tags.len()); + assert_eq!("author: Joe", current_tags[0]); + assert_eq!("title: My Great Story", current_tags[1]); + + let current_tags = story.tags_for_content_at_path("knot")?; + assert_eq!(1, current_tags.len()); + assert_eq!("knot tag", current_tags[0]); + + let current_tags = story.tags_for_content_at_path("knot.stitch")?; + assert_eq!(1, current_tags.len()); + assert_eq!("stitch tag", current_tags[0]); + + story.choose_path_string("knot", false, None)?; + assert_eq!("Knot content\n", story.cont()?); + let current_tags = story.get_current_tags(); + assert_eq!(1, current_tags.len()); + assert_eq!("knot tag", current_tags[0]); + + assert_eq!("", story.cont()?); + let current_tags = story.get_current_tags(); + assert_eq!("end of knot tag", current_tags[0]); + + Ok(()) +} + +#[test] +fn tags_in_seq_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/tags/tagsInSeq.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + + assert_eq!("A red sequence.\n", story.cont()?); + let current_tags = story.get_current_tags(); + assert_eq!(1, current_tags.len()); + assert_eq!("red", current_tags[0]); + + assert_eq!("A white sequence.\n", story.cont()?); + let current_tags = story.get_current_tags(); + assert_eq!(1, current_tags.len()); + assert_eq!("white", current_tags[0]); + + Ok(()) +} + +#[test] +fn tags_in_choice_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/tags/tagsInChoice.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + + story.cont()?; + let current_tags = story.get_current_tags(); + assert_eq!(0, current_tags.len()); + assert_eq!(1, story.get_current_choices().len()); + assert_eq!(2, story.get_current_choices()[0].tags.len()); + assert_eq!("one", story.get_current_choices()[0].tags[0]); + assert_eq!("two", story.get_current_choices()[0].tags[1]); + + story.choose_choice_index(0); + + assert_eq!("one three", story.cont()?); + let current_tags = story.get_current_tags(); + assert_eq!(2, current_tags.len()); + assert_eq!("one", current_tags[0]); + assert_eq!("three", current_tags[1]); + + + Ok(()) +} + +#[test] +fn tags_dynamic_content_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/tags/tagsDynamicContent.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + + assert_eq!("tag\n", story.cont()?); + let current_tags = story.get_current_tags(); + assert_eq!(1, current_tags.len()); + assert_eq!("pic8red.jpg", current_tags[0]); + + Ok(()) +} + From 0a3fcbafd1faafdf3fc6e674f697c71ab98beb8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Tue, 26 Sep 2023 22:51:14 +0000 Subject: [PATCH 34/91] First list test passed --- .devcontainer/Dockerfile | 1 + .devcontainer/devcontainer.json | 27 ++++- src/ink_list.rs | 200 ++++++++++++++++++++++++++++++++ src/ink_list_item.rs | 58 +++++++++ src/json_serialization.rs | 63 +++++----- src/lib.rs | 3 + src/list_definition.rs | 59 ++++++++++ src/native_function_call.rs | 165 ++++++++++++++++---------- src/value.rs | 172 ++++++++++++++++++--------- tests/list_test.rs | 14 +++ 10 files changed, 619 insertions(+), 143 deletions(-) create mode 100644 src/ink_list.rs create mode 100644 src/ink_list_item.rs create mode 100644 src/list_definition.rs create mode 100644 tests/list_test.rs diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index dd891ca..29423c1 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -26,6 +26,7 @@ RUN set -eux; \ rm rustup-init; \ chmod -R a+w $RUSTUP_HOME $CARGO_HOME; \ rustup component add rustfmt; \ + rustup component add clippy; \ rustup --version; \ cargo --version; \ rustc --version; \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 27c7a84..ae7b2cd 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,7 +8,32 @@ "build": { // Path is relative to the devcontainer.json file. "dockerfile": "Dockerfile" - } + }, + + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "lldb.executable": "/usr/bin/lldb", + // VS Code don't watch files under ./target + "files.watcherExclude": { + "**/target/**": true + }, + "rust-analyzer.checkOnSave.command": "clippy" + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "vadimcn.vscode-lldb", + "mutantdino.resourcemonitor", + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml", + "serayuzgur.crates" + ] + } + }, // Features to add to the dev container. More info: https://containers.dev/features. // "features": {}, diff --git a/src/ink_list.rs b/src/ink_list.rs new file mode 100644 index 0000000..55ddab5 --- /dev/null +++ b/src/ink_list.rs @@ -0,0 +1,200 @@ +use core::fmt; +use std::collections::HashMap; + +use crate::{ink_list_item::InkListItem, list_definition::ListDefinition, story::Story}; + +pub struct InkList { + pub items: HashMap, + origins: Vec, + origin_names: Option>, +} + +impl InkList { + pub fn new() -> Self { + Self { + items: HashMap::new(), + origins: Vec::new(), + origin_names: None, + } + } + + pub fn from_single_element(single_element: (InkListItem, i32)) -> Self { + // let mut items = HashMap::new(); + // items.insert(single_element.0.clone(), single_element.1); + + // let mut origins = Vec::new(); + // if let Some(origin_name) = single_element.0.get_origin_name() { + // let def = origin_story.get_list_definitions().get_list_definition(origin_name); + + // if let Some(list_def) = def { + // origins.push(list_def.clone()); + // } else { + // panic!( + // "InkList origin could not be found in story when constructing new list: {}", + // origin_name + // ); + // } + // } + + // Self { + // items, + // origins, + // origin_names: None, + // } + + todo!() + } + + pub fn from_single_origin_list_name( + single_origin_list_name: &str, + origin_story: &Story, + ) -> Result { + // let mut ink_list = InkList::new(); + // ink_list.set_initial_origin_name(single_origin_list_name, origin_story)?; + // Ok(ink_list) + + todo!() + } + + fn from_other_list(other_list: &InkList) -> Self { + let mut ink_list = InkList::new(); + + for (item, value) in &other_list.items { + ink_list.items.insert(item.clone(), *value); + } + + if let Some(names) = &other_list.origin_names { + ink_list.origin_names = Some(names.clone()); + } + + ink_list.origins = other_list.origins.clone(); + + ink_list + } + + fn get_ordered_items(&self) -> Vec<(&InkListItem, &i32)> { + let mut ordered: Vec<_> = self.items.iter().collect(); + ordered.sort_by(|a, b| { + if a.1 == b.1 { + a.0.get_origin_name() + .cmp(&b.0.get_origin_name()) + } else { + a.1.cmp(b.1) + } + }); + ordered + } + + pub fn get_max_item(&self) -> (Option<&InkListItem>, i32) { + let mut max = (None, 0); + + for (k,v) in &self.items { + if max.0.is_none() || *v > max.1 { + max = (Some(k), *v); + } + + } + + max + } + + pub fn get_min_item(&self) -> (Option<&InkListItem>, i32) { + let mut min = (None, 0); + + for (k,v) in &self.items { + if min.0.is_none() || *v < min.1 { + min = (Some(k), *v); + } + + } + + min + } + + pub fn set_initial_origin_names(&mut self, initial_origin_names: Option>) { + match &initial_origin_names { + Some(_) => { + self.origin_names = initial_origin_names; + }, + None => self.origin_names = None, + }; + } + + pub fn get_origin_names(&mut self) -> &Option> { + if self.items.len() > 0 { + + if self.origin_names.is_none() && self.items.len() > 0 { + self.origin_names = Some(Vec::new()); + } else { + self.origin_names.as_mut().unwrap().clear(); + } + + for k in self.items.keys() { + self.origin_names.as_mut().unwrap().push(k.get_origin_name().unwrap().clone()); + } + } + + return &self.origin_names; + } + + pub fn union(&self, other_list: &InkList) -> InkList { + let mut union = InkList::from_other_list(self); + + for (key, value) in &other_list.items { + union.items.insert(key.clone(), *value); + } + + union + } + + pub fn intersect(&self, other_list: &InkList) -> InkList { + let mut intersection = InkList::new(); + + for (k, v) in &self.items { + if other_list.items.contains_key(k) { + intersection.items.insert(k.clone(), *v); + } + } + + intersection + } + + pub fn has(&self, other_list: &InkList) -> InkList { + let mut intersection = InkList::new(); + + for (k, v) in &self.items { + if other_list.items.contains_key(k) { + intersection.items.insert(k.clone(), *v); + } + } + + intersection + } + + pub fn contains(&self, other_list: &InkList) -> bool { + if other_list.items.len() == 0 || self.items.len() == 0 { return false; } + + for k in other_list.items.keys() { + if !self.items.contains_key(k) { return false; } + } + + true + } +} + +impl fmt::Display for InkList { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + + let ordered = self.get_ordered_items(); + let mut result = String::new(); + + for (i, (item, _)) in ordered.iter().enumerate() { + if i > 0 { + result.push_str(", "); + } + result.push_str(item.get_item_name()); + } + + write!(f, "{}", result) + } +} \ No newline at end of file diff --git a/src/ink_list_item.rs b/src/ink_list_item.rs new file mode 100644 index 0000000..7d7affb --- /dev/null +++ b/src/ink_list_item.rs @@ -0,0 +1,58 @@ +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub struct InkListItem { + origin_name: Option, + item_name: String, +} + +impl InkListItem { + pub fn new(origin_name: Option, item_name: String) -> Self { + Self { + origin_name, + item_name, + } + } + + pub fn from_full_name(full_name: &str) -> Self { + let name_parts: Vec<&str> = full_name.split('.').collect(); + let origin_name = if name_parts.len() > 1 { + Some(name_parts[0].to_string()) + } else { + None + }; + let item_name = name_parts.last().unwrap_or(&"").to_string(); + Self { + origin_name, + item_name, + } + } + + pub fn get_null() -> Self { + Self { + origin_name: None, + item_name: String::new(), + } + } + + pub fn get_origin_name(&self) -> Option<&String> { + self.origin_name.as_ref() + } + + pub fn get_item_name(&self) -> &str { + &self.item_name + } + + pub fn get_full_name(&self) -> String { + let origin = self.origin_name.as_ref().map(|s| s.as_str()).unwrap_or("?"); + format!("{}.{}", origin, self.item_name) + } + + pub fn is_null(&self) -> bool { + self.origin_name.is_none() && self.item_name.is_empty() + } +} + +impl std::fmt::Display for InkListItem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.get_full_name()) + } +} diff --git a/src/json_serialization.rs b/src/json_serialization.rs index 9f2eb5b..cf49506 100644 --- a/src/json_serialization.rs +++ b/src/json_serialization.rs @@ -4,7 +4,7 @@ use serde_json::Map; use crate::{ container::Container, - object::{self, RTObject}, control_command::{CommandType, ControlCommand}, value::Value, glue::Glue, path::Path, choice_point::ChoicePoint, choice::Choice, push_pop::PushPopType, divert::Divert, variable_assigment::VariableAssignment, void::Void, variable_reference::VariableReference, native_function_call::{self, NativeFunctionCall}, + object::{self, RTObject}, control_command::ControlCommand, value::Value, glue::Glue, path::Path, choice_point::ChoicePoint, choice::Choice, push_pop::PushPopType, divert::Divert, variable_assigment::VariableAssignment, void::Void, variable_reference::VariableReference, native_function_call::NativeFunctionCall, tag::Tag, ink_list::InkList, ink_list_item::InkListItem, }; pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) -> Result, String> { @@ -42,7 +42,7 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) // we know it's not a string, we can convert back to the proper // symbol for the operator. let mut call_str = str; - if "L^".eq(str) {call_str = &"^";} + if "L^".eq(str) {call_str = "^";} if let Some(native_function_call) = NativeFunctionCall::new_from_name(call_str) { return Ok(Rc::new(native_function_call)); } @@ -51,15 +51,15 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) if "void".eq(str) {return Ok(Rc::new(Void::new()));} - return Err(format!("Failed to convert token to runtime RTObject: {}", &token.to_string())); + Err(format!("Failed to convert token to runtime RTObject: {}", &token.to_string())) }, serde_json::Value::Array(value) => Ok(jarray_to_container(value, name)?), serde_json::Value::Object(obj) => { // Divert target value to path let prop_value = obj.get("^->"); - if prop_value.is_some() { - return Ok(Rc::new(Value::new_divert_target(Path::new_with_components_string(prop_value.unwrap().as_str())))); + if let Some(prop_value) = prop_value { + return Ok(Rc::new(Value::new_divert_target(Path::new_with_components_string(prop_value.as_str())))); } // // VariablePointerValue @@ -132,8 +132,8 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) if external { prop_value = obj.get("exArgs"); - if prop_value.is_some() { - external_args = prop_value.unwrap().as_i64().unwrap() as i32; + if let Some(prop_value) = prop_value { + external_args = prop_value.as_i64().unwrap() as i32; } } @@ -177,7 +177,7 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) }, None => { prop_value = obj.get("temp="); - if let Some(_) = prop_value { + if prop_value.is_some() { is_var_ass = true; is_global_var = false; } @@ -193,42 +193,43 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) return Ok(var_ass); } - // // Legacy Tag - // prop_value = obj.get("#"); - // if (prop_value != null) { - // return new Tag((String) prop_value); - // } + // Legacy Tag + prop_value = obj.get("#"); + if let Some(prop_value) = prop_value { + return Ok(Rc::new(Tag::new(prop_value.as_str().unwrap()))); + } + + // List value + prop_value = obj.get("list"); - // // List value - // prop_value = obj.get("list"); + if let Some(pv) = prop_value { + let list_content = pv.as_object().unwrap(); + let mut raw_list = InkList::new(); - // if (prop_value != null) { - // HashMap listContent = (HashMap) prop_value; - // InkList rawList = new InkList(); + prop_value = obj.get("origins"); - // prop_value = obj.get("origins"); + if let Some(o) = prop_value { + let names_as_objs = o.as_array().unwrap(); - // if (prop_value != null) { - // List namesAsObjs = (List) prop_value; + let names = names_as_objs.iter().map(|e| e.as_str().unwrap().to_string()).collect(); - // rawList.setInitialOriginNames(namesAsObjs); - // } + raw_list.set_initial_origin_names(Some(names)); + } - // for (Entry nameToVal : listContent.entrySet()) { - // InkListItem item = new InkListItem(nameToVal.getKey()); - // int val = (int) nameToVal.getValue(); - // rawList.put(item, val); - // } + for (k,v) in list_content { + let item = InkListItem::from_full_name(k); + raw_list.items.insert(item, v.as_i64().unwrap() as i32); + } - // return new ListValue(rawList); - // } + return Ok(Rc::new(Value::new_list(raw_list))); + } // Used when serialising save state only if obj.get("originalChoicePath").is_some() { return jobject_to_choice(obj); } - return Err(format!("Failed to convert token to runtime RTObject: {}", &token.to_string())); + Err(format!("Failed to convert token to runtime RTObject: {}", &token.to_string())) }, } diff --git a/src/lib.rs b/src/lib.rs index bda97d5..59add19 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,5 +23,8 @@ mod divert; mod variable_assigment; mod variable_reference; mod native_function_call; +mod ink_list; +mod ink_list_item; +mod list_definition; diff --git a/src/list_definition.rs b/src/list_definition.rs new file mode 100644 index 0000000..c4ac4cd --- /dev/null +++ b/src/list_definition.rs @@ -0,0 +1,59 @@ +use std::collections::HashMap; + +use crate::ink_list_item::InkListItem; + + +#[derive(Clone)] +pub struct ListDefinition { + name: String, + items: Option>, + item_name_to_values: HashMap, +} + +impl ListDefinition { + pub fn new(name: String, items: HashMap) -> Self { + Self { + name, + items: None, + item_name_to_values: items, + } + } + + pub fn get_items(&mut self) -> &HashMap { + if self.items.is_none() { + let mut new_items = HashMap::new(); + for (item_name, value) in &self.item_name_to_values { + let item = InkListItem::new(Some(self.name.clone()), item_name.clone()); + new_items.insert(item, *value); + } + self.items = Some(new_items); + } + + self.items.as_ref().unwrap() + } + + pub fn get_name(&self) -> &str { + &self.name + } + + pub fn get_value_for_item(&self, item: &InkListItem) -> Option<&i32> { + self.item_name_to_values.get(item.get_item_name()) + } + + pub fn contains_item(&self, item: &InkListItem) -> bool { + item.get_origin_name() == Some(&self.name) && self.item_name_to_values.contains_key(item.get_item_name()) + } + + pub fn contains_item_with_name(&self, item_name: &str) -> bool { + self.item_name_to_values.contains_key(item_name) + } + + pub fn get_item_with_value(&self, val: i32) -> Option { + for (item_name, value) in &self.item_name_to_values { + if *value == val { + return Some(InkListItem::new(Some(self.name.clone()), item_name.clone())); + } + } + None + } +} diff --git a/src/native_function_call.rs b/src/native_function_call.rs index 3c5f060..f6ecda0 100644 --- a/src/native_function_call.rs +++ b/src/native_function_call.rs @@ -136,31 +136,31 @@ impl NativeFunctionCall { let coerced_params = self.coerce_values_to_single_type(params); match self.op { - Op::Add => self.add_op(coerced_params), - Op::Subtract => self.subtract_op(coerced_params), - Op::Divide => self.divide_op(coerced_params), - Op::Multiply => self.multiply_op(coerced_params), - Op::Mod => self.mod_op(coerced_params), + Op::Add => self.add_op(&coerced_params), + Op::Subtract => self.subtract_op(&coerced_params), + Op::Divide => self.divide_op(&coerced_params), + Op::Multiply => self.multiply_op(&coerced_params), + Op::Mod => self.mod_op(&coerced_params), Op::Negate => todo!(), - Op::Equal => self.equal_op(coerced_params), - Op::Greater => self.greater_op(coerced_params), + Op::Equal => self.equal_op(&coerced_params), + Op::Greater => self.greater_op(&coerced_params), Op::Less => todo!(), Op::GreaterThanOrEquals => todo!(), Op::LessThanOrEquals => todo!(), - Op::NotEquals => self.not_equals_op(coerced_params), + Op::NotEquals => self.not_equals_op(&coerced_params), Op::Not => todo!(), - Op::And => self.and_op(coerced_params), - Op::Or => self.or_op(coerced_params), - Op::Min => self.min_op(coerced_params), - Op::Max => self.max_op(coerced_params), + Op::And => self.and_op(&coerced_params), + Op::Or => self.or_op(&coerced_params), + Op::Min => self.min_op(&coerced_params), + Op::Max => self.max_op(&coerced_params), Op::Pow => todo!(), Op::Floor => todo!(), Op::Ceiling => todo!(), Op::Int => todo!(), Op::Float => todo!(), - Op::Has => todo!(), - Op::Hasnt => todo!(), - Op::Intersect => todo!(), + Op::Has => self.has(&coerced_params), + Op::Hasnt => self.hasnt(&coerced_params), + Op::Intersect => self.intersect_op(&coerced_params), Op::ListMin => todo!(), Op::ListMax => todo!(), Op::All => todo!(), @@ -187,9 +187,15 @@ impl NativeFunctionCall { } for obj in params.iter() { - if let Some(v) = obj.as_ref().as_any().downcast_ref::() { - let casted_value = v.cast(dest_type); - result.push(Rc::new(casted_value)); + if let Some(v) = obj.as_ref().as_any().downcast_ref::() { + match v.cast(dest_type) { + Some(casted_value) => result.push(Rc::new(casted_value)), + None => { + if let Ok(obj) = obj.clone().into_any().downcast::() { + result.push(obj); + } + }, + } } else { panic!("RTObject of type Value expected: {}", obj.to_string()) } @@ -198,56 +204,56 @@ impl NativeFunctionCall { return result; } - fn and_op(&self, params: Vec>) -> Rc { - match params[0].value { + fn and_op(&self, params: &Vec>) -> Rc { + match ¶ms[0].value { ValueType::Bool(op1) => match params[1].value { - ValueType::Bool(op2) => Rc::new(Value::new_bool(op1 && op2)), + ValueType::Bool(op2) => Rc::new(Value::new_bool(*op1 && op2)), _ => panic!() }, ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Rc::new(Value::new_bool(op1 != 0 && op2 != 0)), + ValueType::Int(op2) => Rc::new(Value::new_bool(*op1 != 0 && op2 != 0)), _ => panic!() }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Rc::new(Value::new_bool(op1 != 0.0 && op2 != 0.0)), + ValueType::Float(op2) => Rc::new(Value::new_bool(*op1 != 0.0 && op2 != 0.0)), _ => panic!() }, - ValueType::List() => todo!(), + ValueType::List(l) => todo!(), _ => panic!() } } - fn greater_op(&self, params: Vec>) -> Rc { - match params[0].value { + fn greater_op(&self, params: &Vec>) -> Rc { + match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Rc::new(Value::new_bool(op1 > op2)), + ValueType::Int(op2) => Rc::new(Value::new_bool(*op1 > op2)), _ => panic!() }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Rc::new(Value::new_bool(op1 > op2)), + ValueType::Float(op2) => Rc::new(Value::new_bool(*op1 > op2)), _ => panic!() }, - ValueType::List() => todo!(), + ValueType::List(l) => todo!(), _ => panic!() } } - fn subtract_op(&self, params: Vec>) -> Rc { - match params[0].value { + fn subtract_op(&self, params: &Vec>) -> Rc { + match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Rc::new(Value::new_int(op1 - op2)), + ValueType::Int(op2) => Rc::new(Value::new_int(*op1 - op2)), _ => panic!() }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Rc::new(Value::new_float(op1 - op2)), + ValueType::Float(op2) => Rc::new(Value::new_float(*op1 - op2)), _ => panic!() }, - ValueType::List() => todo!(), + ValueType::List(op1) => todo!(), _ => panic!() } } - fn add_op(&self, params: Vec>) -> Rc { + fn add_op(&self, params: &Vec>) -> Rc { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Rc::new(Value::new_int(op1 + op2)), @@ -266,12 +272,15 @@ impl NativeFunctionCall { }, _ => panic!() }, - ValueType::List() => todo!(), + ValueType::List(op1) => match ¶ms[1].value { + ValueType::List(op2) => Rc::new(Value::new_list(op1.union(op2))), + _ => panic!() + }, _ => panic!() } } - fn divide_op(&self, params: Vec>) -> Rc { + fn divide_op(&self, params: &Vec>) -> Rc { match params[0].value { ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Rc::new(Value::new_int(op1 / op2)), @@ -285,7 +294,7 @@ impl NativeFunctionCall { } } - fn multiply_op(&self, params: Vec>) -> Rc { + fn multiply_op(&self, params: &Vec>) -> Rc { match params[0].value { ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Rc::new(Value::new_int(op1 * op2)), @@ -299,56 +308,56 @@ impl NativeFunctionCall { } } - fn or_op(&self, params: Vec>) -> Rc { - match params[0].value { + fn or_op(&self, params: &Vec>) -> Rc { + match ¶ms[0].value { ValueType::Bool(op1) => match params[1].value { - ValueType::Bool(op2) => Rc::new(Value::new_bool(op1 || op2)), + ValueType::Bool(op2) => Rc::new(Value::new_bool(*op1 || op2)), _ => panic!() }, ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Rc::new(Value::new_bool(op1 != 0 || op2 != 0)), + ValueType::Int(op2) => Rc::new(Value::new_bool(*op1 != 0 || op2 != 0)), _ => panic!() }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Rc::new(Value::new_bool(op1 != 0.0 || op2 != 0.0)), + ValueType::Float(op2) => Rc::new(Value::new_bool(*op1 != 0.0 || op2 != 0.0)), _ => panic!() }, - ValueType::List() => todo!(), + ValueType::List(l) => todo!(), _ => panic!() } } - fn min_op(&self, params: Vec>) -> Rc { - match params[0].value { + fn min_op(&self, params: &Vec>) -> Rc { + match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Rc::new(Value::new_int(i32::min(op1, op2))), + ValueType::Int(op2) => Rc::new(Value::new_int(i32::min(*op1, op2))), _ => panic!() }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Rc::new(Value::new_float(f32::min(op1, op2))), + ValueType::Float(op2) => Rc::new(Value::new_float(f32::min(*op1, op2))), _ => panic!() }, - ValueType::List() => todo!(), + ValueType::List(l) => todo!(), _ => panic!() } } - fn max_op(&self, params: Vec>) -> Rc { - match params[0].value { + fn max_op(&self, params: &Vec>) -> Rc { + match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Rc::new(Value::new_int(i32::max(op1, op2))), + ValueType::Int(op2) => Rc::new(Value::new_int(i32::max(*op1, op2))), _ => panic!() }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Rc::new(Value::new_float(f32::max(op1, op2))), + ValueType::Float(op2) => Rc::new(Value::new_float(f32::max(*op1, op2))), _ => panic!() }, - ValueType::List() => todo!(), + ValueType::List(l) => todo!(), _ => panic!() } } - fn equal_op(&self, params: Vec>) -> Rc { + fn equal_op(&self, params: &Vec>) -> Rc { match ¶ms[0].value { ValueType::Bool(op1) => match params[1].value { ValueType::Bool(op2) => Rc::new(Value::new_bool(*op1 == op2)), @@ -366,12 +375,12 @@ impl NativeFunctionCall { ValueType::String(op2) => Rc::new(Value::new_bool(op1.string.eq(&op2.string))), _ => panic!() }, - ValueType::List() => todo!(), + ValueType::List(l) => todo!(), _ => panic!() } } - fn not_equals_op(&self, params: Vec>) -> Rc { + fn not_equals_op(&self, params: &Vec>) -> Rc { match ¶ms[0].value { ValueType::Bool(op1) => match params[1].value { ValueType::Bool(op2) => Rc::new(Value::new_bool(*op1 != op2)), @@ -389,12 +398,12 @@ impl NativeFunctionCall { ValueType::String(op2) => Rc::new(Value::new_bool(!op1.string.eq(&op2.string))), _ => panic!() }, - ValueType::List() => todo!(), + ValueType::List(l) => todo!(), _ => panic!() } } - fn mod_op(&self, params: Vec>) -> Rc { + fn mod_op(&self, params: &Vec>) -> Rc { match params[0].value { ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Rc::new(Value::new_int(op1 % op2)), @@ -407,6 +416,44 @@ impl NativeFunctionCall { _ => panic!() } } + + fn intersect_op(&self, params: &[Rc]) -> Rc { + match ¶ms[0].value { + ValueType::List(op1) => match ¶ms[1].value { + ValueType::List(op2) => Rc::new(Value::new_list(op1.intersect(op2))), + _ => panic!() + }, + _ => panic!() + } + } + + fn has(&self, params: &[Rc]) -> Rc { + match ¶ms[0].value { + ValueType::String(op1) => match ¶ms[1].value { + ValueType::String(op2) => Rc::new(Value::new_bool(op1.string.contains(&op2.string))), + _ => panic!() + }, + ValueType::List(op1) => match ¶ms[1].value { + ValueType::List(op2) => Rc::new(Value::new_bool(op1.contains(op2))), + _ => panic!() + }, + _ => panic!() + } + } + + fn hasnt(&self, params: &[Rc]) -> Rc { + match ¶ms[0].value { + ValueType::String(op1) => match ¶ms[1].value { + ValueType::String(op2) => Rc::new(Value::new_bool(!op1.string.contains(&op2.string))), + _ => panic!() + }, + ValueType::List(op1) => match ¶ms[1].value { + ValueType::List(op2) => Rc::new(Value::new_bool(!op1.contains(op2))), + _ => panic!() + }, + _ => panic!() + } + } } impl RTObject for NativeFunctionCall { diff --git a/src/value.rs b/src/value.rs index 85e2447..75244e9 100644 --- a/src/value.rs +++ b/src/value.rs @@ -1,13 +1,21 @@ use std::{fmt, rc::Rc}; -use crate::{object::{RTObject, Object}, path::Path}; +use as_any::Downcast; + +use crate::{object::{RTObject, Object}, path::Path, ink_list::InkList}; + +const CAST_BOOL: u8 = 0; +const CAST_INT: u8 = 1; +const CAST_FLOAT: u8 = 2; +const CAST_LIST: u8 = 3; +const CAST_STRING: u8 = 4; #[repr(u8)] pub enum ValueType { Bool(bool), Int(i32), Float(f32), - List(), + List(InkList), String(StringValue), DivertTarget(Path), VariablePointer(VariablePointerValue), @@ -59,25 +67,25 @@ impl fmt::Display for Value { ValueType::String(v) => write!(f, "{}", v.string), ValueType::DivertTarget(p) => write!(f, "DivertTargetValue({})", p), ValueType::VariablePointer(v) => write!(f, "VariablePointerValue({})", v.variable_name), - ValueType::List() => todo!(), + ValueType::List(l) => write!(f, "{}", l), } } } impl Value { - pub fn new_bool(v:bool) -> Value { - Value { obj: Object::new(), value: ValueType::Bool(v) } + pub fn new_bool(v:bool) -> Self { + Self { obj: Object::new(), value: ValueType::Bool(v) } } - pub fn new_int(v:i32) -> Value { - Value { obj: Object::new(), value: ValueType::Int(v) } + pub fn new_int(v:i32) -> Self { + Self { obj: Object::new(), value: ValueType::Int(v) } } - pub fn new_float(v:f32) -> Value { - Value { obj: Object::new(), value: ValueType::Float(v) } + pub fn new_float(v:f32) -> Self { + Self { obj: Object::new(), value: ValueType::Float(v) } } - pub fn new_string(v:&str) -> Value { + pub fn new_string(v:&str) -> Self { let mut inline_ws = true; @@ -88,7 +96,7 @@ impl Value { } } - Value { + Self { obj: Object::new(), value: ValueType::String(StringValue { string: v.to_string(), @@ -97,12 +105,16 @@ impl Value { } } - pub fn new_divert_target(p:Path) -> Value { - Value { obj: Object::new(), value: ValueType::DivertTarget(p) } + pub fn new_divert_target(p:Path) -> Self { + Self { obj: Object::new(), value: ValueType::DivertTarget(p) } + } + + pub fn new_variable_pointer(variable_name: &str, context_index: i32) -> Self { + Self { obj: Object::new(), value: ValueType::VariablePointer(VariablePointerValue { variable_name: variable_name.to_string(), context_index }) } } - pub fn new_variable_pointer(variable_name: &str, context_index: i32) -> Value { - Value { obj: Object::new(), value: ValueType::VariablePointer(VariablePointerValue { variable_name: variable_name.to_string(), context_index }) } + pub fn new_list(l: InkList) -> Self { + Self { obj: Object::new(), value: ValueType::List(l) } } pub fn is_truthy(&self) -> bool { @@ -113,7 +125,7 @@ impl Value { ValueType::String(v) => v.string.len() > 0, ValueType::DivertTarget(_) => panic!(), // exception Shouldn't be checking the truthiness of a divert target?? ValueType::VariablePointer(_) => panic!(), - ValueType::List() => todo!(), + ValueType::List(l) => l.items.len() > 0, } } @@ -157,6 +169,37 @@ impl Value { } } + pub fn get_list_value_mut(o: &mut dyn RTObject) -> Option<&mut InkList> { + match o.as_any_mut().downcast_mut::() { + Some(v) => match &mut v.value { + ValueType::List(v) => Some(v), + _ => None, + }, + None => None, + } + } + + pub fn get_list_value(o: &dyn RTObject) -> Option<&InkList> { + match o.as_any().downcast_ref::() { + Some(v) => match &v.value { + ValueType::List(v) => Some(v), + _ => None, + }, + None => None, + } + } + + pub fn retain_list_origins_for_assignment(old_value: &mut dyn RTObject, new_value: &mut dyn RTObject) { + + if let Some(old_list) = Self::get_list_value_mut(old_value) { + if let Some(new_list) = Self::get_list_value_mut(new_value) { + if new_list.items.len() == 0 { + new_list.set_initial_origin_names(old_list.get_origin_names().clone()); + } + } + } + } + pub fn get_cast_ordinal(&self) -> u8 { let v = &self.value; @@ -166,71 +209,96 @@ impl Value { } } - pub fn cast(&self, ordinal_dest_type: u8) -> Value { + // If None is returned means that casting is not needed + pub fn cast(&self, cast_dest_type: u8) -> Option { match &self.value { ValueType::Bool(v) => { - match ordinal_dest_type { - 0 => Self::new_bool(*v), - 1 => if *v { - Self::new_int(1) + match cast_dest_type { + CAST_BOOL => None, + CAST_INT => if *v { + Some(Self::new_int(1)) } else { - Self::new_int(0) + Some(Self::new_int(0)) }, - 2 => if *v { - Self::new_float(1.0) + CAST_FLOAT => if *v { + Some(Self::new_float(1.0)) } else { - Self::new_float(0.0) + Some(Self::new_float(0.0)) }, - 3 => panic!(), // LIST - 4 => if *v { - Self::new_string("true") + CAST_STRING => if *v { + Some(Self::new_string("true")) } else { - Self::new_string("false") + Some(Self::new_string("false")) }, _ => panic!(), } }, ValueType::Int(v) => { - match ordinal_dest_type { - 0 => if *v == 0 { - Self::new_bool(false) + match cast_dest_type { + CAST_BOOL => if *v == 0 { + Some(Self::new_bool(false)) } else { - Self::new_bool(true) + Some(Self::new_bool(true)) }, - 1 => Self::new_int(*v), - 2 => Self::new_float(*v as f32), - 3 => panic!(), // LIST - 4 => Self::new_string(&*v.to_string()), + CAST_INT => None, + CAST_FLOAT => Some(Self::new_float(*v as f32)), + CAST_STRING => Some(Self::new_string(&*v.to_string())), _ => panic!(), } }, ValueType::Float(v) => { - match ordinal_dest_type { - 0 => if *v == 0.0 { - Self::new_bool(false) + match cast_dest_type { + CAST_BOOL => if *v == 0.0 { + Some(Self::new_bool(false)) } else { - Self::new_bool(true) + Some(Self::new_bool(true)) }, - 1 => Self::new_int(*v as i32), - 2 => Self::new_float(*v), - 3 => panic!(), // LIST - 4 => Self::new_string(&*v.to_string()), + CAST_INT => Some(Self::new_int(*v as i32)), + CAST_FLOAT => None, + CAST_STRING => Some(Self::new_string(&*v.to_string())), _ => panic!(), } }, ValueType::String(v) => { - match ordinal_dest_type { - 0 => panic!(), - 1 => Self::new_int(v.string.parse::().unwrap()), - 2 => Self::new_float(v.string.parse::().unwrap()), - 3 => panic!(), // LIST - 4 => Self::new_string(&v.string), + match cast_dest_type { + CAST_INT => Some(Self::new_int(v.string.parse::().unwrap())), + CAST_FLOAT => Some(Self::new_float(v.string.parse::().unwrap())), + CAST_STRING => None, + _ => panic!(), + } + }, + ValueType::List(l) => { + match cast_dest_type { + CAST_INT => { + let max = l.get_max_item(); + if max.0.is_none() { + Some(Self::new_int(0)) + } else { + Some(Self::new_int(max.1)) + } + }, + CAST_FLOAT => { + let max = l.get_max_item(); + if max.0.is_none() { + Some(Self::new_float(0.0)) + } else { + Some(Self::new_float(max.1 as f32)) + } + }, + CAST_LIST => None, + CAST_STRING => { + let max = l.get_max_item(); + if max.0.is_none() { + Some(Self::new_string("")) + } else { + Some(Self::new_string(&max.0.unwrap().to_string())) + } + }, _ => panic!(), } }, ValueType::DivertTarget(_) => panic!(), ValueType::VariablePointer(_) => panic!(), - ValueType::List() => todo!(), } } } \ No newline at end of file diff --git a/tests/list_test.rs b/tests/list_test.rs new file mode 100644 index 0000000..047240c --- /dev/null +++ b/tests/list_test.rs @@ -0,0 +1,14 @@ +use bladeink::story::Story; + +mod common; + +#[test] +fn list_basic_operations_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/lists/basic-operations.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + + assert_eq!("b, d\na, b, c, e\nb, c\nfalse\ntrue\ntrue\n", &story.continue_maximally()?); + + Ok(()) +} From af5fc40caf02cf7d445ed6a92c5a28c66d548e7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Wed, 27 Sep 2023 18:36:16 +0000 Subject: [PATCH 35/91] WIP lists. Passed 5/8 tests --- src/container.rs | 22 ++--- src/ink_list.rs | 143 +++++++++++++++++++-------------- src/json_serialization.rs | 25 +++++- src/lib.rs | 1 + src/list_definitions_origin.rs | 50 ++++++++++++ src/native_function_call.rs | 66 +++++++++++---- src/story.rs | 78 ++++++++++++++++-- src/story_state.rs | 66 ++++++--------- src/value.rs | 2 +- src/variables_state.rs | 16 ++-- tests/list_test.rs | 83 +++++++++++++++++++ 11 files changed, 406 insertions(+), 146 deletions(-) create mode 100644 src/list_definitions_origin.rs diff --git a/src/container.rs b/src/container.rs index 20f64a3..3d4cadf 100644 --- a/src/container.rs +++ b/src/container.rs @@ -44,9 +44,9 @@ impl Container { content, named_content, name, - visits_should_be_counted: visits_should_be_counted, - turn_index_should_be_counted: turn_index_should_be_counted, - counting_at_start_only: counting_at_start_only, + visits_should_be_counted, + turn_index_should_be_counted, + counting_at_start_only, }); c.content.iter().for_each(|o| o.get_object().set_parent(&c)); @@ -93,7 +93,7 @@ impl Container { Container::append_indentation(sb, indentation); if let ValueType::String(s) = &v.value { sb.push('\"'); - sb.push_str(&&s.string.replace('\n', "\\n")); + sb.push_str(&s.string.replace('\n', "\\n")); sb.push('\"'); } else { sb.push_str(&v.to_string()); @@ -109,7 +109,9 @@ impl Container { if let Some(pointed_obj) = pointed_obj { if !pointed_obj.is::() { - if std::ptr::eq(obj.as_ref(), pointed_obj) { + let a = obj.as_ref() as *const _ as *const (); + let b = pointed_obj as *const _ as *const (); + if std::ptr::eq(a, b) { sb.push_str(" <---"); } } @@ -135,7 +137,7 @@ impl Container { - if only_named.len() > 0 { + if !only_named.is_empty() { Container::append_indentation(sb, indentation); sb.push_str("-- named: --\n"); @@ -239,16 +241,16 @@ impl Container { flags = 0; } - return flags; + flags } fn split_count_flags(value: i32) -> (bool, bool, bool) { - let visits_should_be_counted = if (value & COUNTFLAGS_VISITS) > 0 { true } else { false} ; + let visits_should_be_counted = (value & COUNTFLAGS_VISITS) > 0 ; - let turn_index_should_be_counted = if (value & COUNTFLAGS_TURNS) > 0 { true } else { false} ; + let turn_index_should_be_counted = (value & COUNTFLAGS_TURNS) > 0 ; - let counting_at_start_only = if (value & COUNTFLAGS_COUNTSTARTONLY) > 0 { true } else { false} ; + let counting_at_start_only = (value & COUNTFLAGS_COUNTSTARTONLY) > 0 ; (visits_should_be_counted, turn_index_should_be_counted, counting_at_start_only) } diff --git a/src/ink_list.rs b/src/ink_list.rs index 55ddab5..1f1d613 100644 --- a/src/ink_list.rs +++ b/src/ink_list.rs @@ -1,59 +1,48 @@ use core::fmt; -use std::collections::HashMap; +use std::{collections::HashMap, cell::RefCell}; -use crate::{ink_list_item::InkListItem, list_definition::ListDefinition, story::Story}; +use crate::{ink_list_item::InkListItem, list_definition::ListDefinition, list_definitions_origin::ListDefinitionsOrigin, value::ValueType}; pub struct InkList { pub items: HashMap, - origins: Vec, - origin_names: Option>, + pub origins: RefCell>, + // we need an origin when we only have the definition (the list has not elemetns) + initial_origin_names:Vec, } impl InkList { pub fn new() -> Self { Self { items: HashMap::new(), - origins: Vec::new(), - origin_names: None, + origins: RefCell::new(Vec::with_capacity(0)), + initial_origin_names: Vec::with_capacity(0), } } - pub fn from_single_element(single_element: (InkListItem, i32)) -> Self { - // let mut items = HashMap::new(); - // items.insert(single_element.0.clone(), single_element.1); - - // let mut origins = Vec::new(); - // if let Some(origin_name) = single_element.0.get_origin_name() { - // let def = origin_story.get_list_definitions().get_list_definition(origin_name); - - // if let Some(list_def) = def { - // origins.push(list_def.clone()); - // } else { - // panic!( - // "InkList origin could not be found in story when constructing new list: {}", - // origin_name - // ); - // } - // } - - // Self { - // items, - // origins, - // origin_names: None, - // } - - todo!() + pub fn from_single_element(single_element: (InkListItem, i32)) -> Self { + let mut l = Self::new(); + l.items.insert(single_element.0, single_element.1); + + l } - pub fn from_single_origin_list_name( - single_origin_list_name: &str, - origin_story: &Story, - ) -> Result { - // let mut ink_list = InkList::new(); - // ink_list.set_initial_origin_name(single_origin_list_name, origin_story)?; - // Ok(ink_list) + pub fn from_single_origin(single_origin: String, list_definitions: ListDefinitionsOrigin) -> Self { + let mut l = Self::new(); + + l.initial_origin_names.push(single_origin); - todo!() + let def = list_definitions.get_list_definition(&l.initial_origin_names[0]); + + if let Some(list_def) = def { + l.origins.borrow_mut().push(list_def.clone()); + } else { + panic!( + "InkList origin could not be found in story when constructing new list: {}", + &l.initial_origin_names[0] + ); + } + + l } fn from_other_list(other_list: &InkList) -> Self { @@ -63,9 +52,7 @@ impl InkList { ink_list.items.insert(item.clone(), *value); } - if let Some(names) = &other_list.origin_names { - ink_list.origin_names = Some(names.clone()); - } + ink_list.initial_origin_names = other_list.initial_origin_names.clone(); ink_list.origins = other_list.origins.clone(); @@ -111,30 +98,23 @@ impl InkList { min } - pub fn set_initial_origin_names(&mut self, initial_origin_names: Option>) { - match &initial_origin_names { - Some(_) => { - self.origin_names = initial_origin_names; - }, - None => self.origin_names = None, - }; + pub fn set_initial_origin_names(&mut self, initial_origin_names: Vec) { + self.initial_origin_names = initial_origin_names; } - pub fn get_origin_names(&mut self) -> &Option> { - if self.items.len() > 0 { + pub fn get_origin_names(&self) -> Vec { + if !self.items.is_empty() { - if self.origin_names.is_none() && self.items.len() > 0 { - self.origin_names = Some(Vec::new()); - } else { - self.origin_names.as_mut().unwrap().clear(); - } + let mut names = Vec::new(); for k in self.items.keys() { - self.origin_names.as_mut().unwrap().push(k.get_origin_name().unwrap().clone()); + names.push(k.get_origin_name().unwrap().clone()); } + + return names; } - return &self.origin_names; + self.initial_origin_names.clone() } pub fn union(&self, other_list: &InkList) -> InkList { @@ -172,13 +152,58 @@ impl InkList { } pub fn contains(&self, other_list: &InkList) -> bool { - if other_list.items.len() == 0 || self.items.len() == 0 { return false; } + if other_list.items.is_empty() || self.items.is_empty() { return false; } for k in other_list.items.keys() { if !self.items.contains_key(k) { return false; } } true + } + + pub(crate) fn get_all(&self) -> InkList { + let mut list = InkList::new(); + + for origin in self.origins.borrow_mut().iter_mut() { + list.items = origin.get_items().clone() + } + + list + } + + pub(crate) fn list_with_sub_range(&self, min_bound: &ValueType, max_bound: &ValueType) -> InkList { + if self.items.is_empty() {return InkList::new();} + + let ordered = self.get_ordered_items(); + let mut min_value = 0; + let mut max_value = i32::MAX; + + if let ValueType::Int(v) = min_bound { + min_value = *v; + } else if let ValueType::List(l) = min_bound { + if !l.items.is_empty() { + min_value = l.get_min_item().1; + } + } + + if let ValueType::Int(v) = max_bound { + max_value = *v; + } else if let ValueType::List(l) = max_bound { + if !l.items.is_empty() { + max_value = l.get_min_item().1; + } + } + + let mut sub_list = InkList::new(); + sub_list.set_initial_origin_names(self.initial_origin_names.clone()); + + for (k, v) in ordered { + if *v >= min_value && *v <= max_value { + sub_list.items.insert(k.clone(), *v); + } + } + + return sub_list; } } diff --git a/src/json_serialization.rs b/src/json_serialization.rs index cf49506..9e55f0f 100644 --- a/src/json_serialization.rs +++ b/src/json_serialization.rs @@ -4,7 +4,7 @@ use serde_json::Map; use crate::{ container::Container, - object::{self, RTObject}, control_command::ControlCommand, value::Value, glue::Glue, path::Path, choice_point::ChoicePoint, choice::Choice, push_pop::PushPopType, divert::Divert, variable_assigment::VariableAssignment, void::Void, variable_reference::VariableReference, native_function_call::NativeFunctionCall, tag::Tag, ink_list::InkList, ink_list_item::InkListItem, + object::{self, RTObject}, control_command::ControlCommand, value::Value, glue::Glue, path::Path, choice_point::ChoicePoint, choice::Choice, push_pop::PushPopType, divert::Divert, variable_assigment::VariableAssignment, void::Void, variable_reference::VariableReference, native_function_call::NativeFunctionCall, tag::Tag, ink_list::InkList, ink_list_item::InkListItem, list_definitions_origin::ListDefinitionsOrigin, list_definition::ListDefinition, }; pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) -> Result, String> { @@ -213,7 +213,7 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) let names = names_as_objs.iter().map(|e| e.as_str().unwrap().to_string()).collect(); - raw_list.set_initial_origin_names(Some(names)); + raw_list.set_initial_origin_names(names); } for (k,v) in list_content { @@ -294,5 +294,24 @@ fn jobject_to_choice(obj: &Map) -> Result Result { + + let mut all_defs: Vec = Vec::with_capacity(0); + + for (name, list_def_json) in def.as_object().unwrap() { + + // Cast (string, object) to (string, int) for items + let mut items: HashMap = HashMap::new(); + for (k, v) in list_def_json.as_object().unwrap() { + items.insert(k.clone(), v.as_u64().unwrap() as i32); + } + + let def = ListDefinition::new(name.clone(), items); + all_defs.push(def); + } + + Ok(ListDefinitionsOrigin::new(&mut all_defs)) } diff --git a/src/lib.rs b/src/lib.rs index 59add19..36ba018 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,5 +26,6 @@ mod native_function_call; mod ink_list; mod ink_list_item; mod list_definition; +mod list_definitions_origin; diff --git a/src/list_definitions_origin.rs b/src/list_definitions_origin.rs new file mode 100644 index 0000000..bb9cd7a --- /dev/null +++ b/src/list_definitions_origin.rs @@ -0,0 +1,50 @@ +use std::{collections::HashMap, rc::Rc}; + +use crate::{list_definition::ListDefinition, value::Value, ink_list::InkList}; + +#[derive(Clone)] +pub struct ListDefinitionsOrigin { + lists: HashMap, + all_unambiguous_list_value_cache: HashMap>, +} + +impl ListDefinitionsOrigin { + pub fn new(lists: &mut Vec) -> Self { + let mut list_definitions_origin = ListDefinitionsOrigin { + lists: HashMap::new(), + all_unambiguous_list_value_cache: HashMap::new(), + }; + + for list in lists { + list_definitions_origin.lists.insert(list.get_name().to_string(), list.clone()); + + for (key, val) in list.get_items() { + let mut l = InkList::new(); + l.items.insert(key.clone(), *val); + + let list_value = Rc::new(Value::new_list(l)); + + list_definitions_origin + .all_unambiguous_list_value_cache + .insert(key.get_item_name().to_string(), list_value.clone()); + list_definitions_origin + .all_unambiguous_list_value_cache + .insert(key.get_full_name().to_string(), list_value.clone()); + } + } + + list_definitions_origin + } + + pub fn get_list_definition(&self, name: &str) -> Option<&ListDefinition> { + self.lists.get(name) + } + + fn get_lists(&self) -> Vec<&ListDefinition> { + self.lists.values().collect() + } + + pub fn find_single_item_list_with_name(&self, name: &str) -> Option<&Rc> { + self.all_unambiguous_list_value_cache.get(name) + } +} diff --git a/src/native_function_call.rs b/src/native_function_call.rs index f6ecda0..489ecd2 100644 --- a/src/native_function_call.rs +++ b/src/native_function_call.rs @@ -1,6 +1,6 @@ use std::{fmt, collections::HashMap, rc::Rc}; -use crate::{object::{Object, RTObject}, value::{Value, ValueType}}; +use crate::{object::{Object, RTObject}, value::{Value, ValueType}, void::Void}; #[derive(Debug)] pub enum Op { @@ -46,7 +46,6 @@ pub enum Op { pub struct NativeFunctionCall { obj: Object, op: Op, - number_of_parameters: i32, } impl NativeFunctionCall { @@ -91,7 +90,6 @@ impl NativeFunctionCall { Self { obj: Object::new(), op, - number_of_parameters: 0, } } @@ -133,6 +131,22 @@ impl NativeFunctionCall { pub(crate) fn call(&self, params: Vec>) -> std::rc::Rc { + if self.get_number_of_parameters() != params.len() || params.len() < 1 || params.len() > 2 { + panic!("Unexpected number of parameters"); + } + + let mut has_list = false; + + for p in ¶ms { + if p.as_ref().as_any().is::() { + panic!("Attempting to perform operation on a void value. Did you forget to 'return' a value from a function you called here?"); + } + + if Value::get_list_value(p.as_ref()).is_some() { + has_list = true; + } + } + let coerced_params = self.coerce_values_to_single_type(params); match self.op { @@ -163,9 +177,9 @@ impl NativeFunctionCall { Op::Intersect => self.intersect_op(&coerced_params), Op::ListMin => todo!(), Op::ListMax => todo!(), - Op::All => todo!(), + Op::All => self.all_op(&coerced_params), Op::Count => todo!(), - Op::ValueOfList => todo!(), + Op::ValueOfList => self.value_of_list_op(&coerced_params), Op::Invert => todo!(), } } @@ -204,7 +218,7 @@ impl NativeFunctionCall { return result; } - fn and_op(&self, params: &Vec>) -> Rc { + fn and_op(&self, params: &[Rc]) -> Rc { match ¶ms[0].value { ValueType::Bool(op1) => match params[1].value { ValueType::Bool(op2) => Rc::new(Value::new_bool(*op1 && op2)), @@ -223,7 +237,7 @@ impl NativeFunctionCall { } } - fn greater_op(&self, params: &Vec>) -> Rc { + fn greater_op(&self, params: &[Rc]) -> Rc { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Rc::new(Value::new_bool(*op1 > op2)), @@ -238,7 +252,7 @@ impl NativeFunctionCall { } } - fn subtract_op(&self, params: &Vec>) -> Rc { + fn subtract_op(&self, params: &[Rc]) -> Rc { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Rc::new(Value::new_int(*op1 - op2)), @@ -253,7 +267,7 @@ impl NativeFunctionCall { } } - fn add_op(&self, params: &Vec>) -> Rc { + fn add_op(&self, params: &[Rc]) -> Rc { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Rc::new(Value::new_int(op1 + op2)), @@ -280,7 +294,7 @@ impl NativeFunctionCall { } } - fn divide_op(&self, params: &Vec>) -> Rc { + fn divide_op(&self, params: &[Rc]) -> Rc { match params[0].value { ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Rc::new(Value::new_int(op1 / op2)), @@ -294,7 +308,7 @@ impl NativeFunctionCall { } } - fn multiply_op(&self, params: &Vec>) -> Rc { + fn multiply_op(&self, params: &[Rc]) -> Rc { match params[0].value { ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Rc::new(Value::new_int(op1 * op2)), @@ -308,7 +322,7 @@ impl NativeFunctionCall { } } - fn or_op(&self, params: &Vec>) -> Rc { + fn or_op(&self, params: &[Rc]) -> Rc { match ¶ms[0].value { ValueType::Bool(op1) => match params[1].value { ValueType::Bool(op2) => Rc::new(Value::new_bool(*op1 || op2)), @@ -327,7 +341,7 @@ impl NativeFunctionCall { } } - fn min_op(&self, params: &Vec>) -> Rc { + fn min_op(&self, params: &[Rc]) -> Rc { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Rc::new(Value::new_int(i32::min(*op1, op2))), @@ -342,7 +356,7 @@ impl NativeFunctionCall { } } - fn max_op(&self, params: &Vec>) -> Rc { + fn max_op(&self, params: &[Rc]) -> Rc { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Rc::new(Value::new_int(i32::max(*op1, op2))), @@ -357,7 +371,7 @@ impl NativeFunctionCall { } } - fn equal_op(&self, params: &Vec>) -> Rc { + fn equal_op(&self, params: &[Rc]) -> Rc { match ¶ms[0].value { ValueType::Bool(op1) => match params[1].value { ValueType::Bool(op2) => Rc::new(Value::new_bool(*op1 == op2)), @@ -380,7 +394,7 @@ impl NativeFunctionCall { } } - fn not_equals_op(&self, params: &Vec>) -> Rc { + fn not_equals_op(&self, params: &[Rc]) -> Rc { match ¶ms[0].value { ValueType::Bool(op1) => match params[1].value { ValueType::Bool(op2) => Rc::new(Value::new_bool(*op1 != op2)), @@ -403,7 +417,7 @@ impl NativeFunctionCall { } } - fn mod_op(&self, params: &Vec>) -> Rc { + fn mod_op(&self, params: &[Rc]) -> Rc { match params[0].value { ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Rc::new(Value::new_int(op1 % op2)), @@ -454,6 +468,24 @@ impl NativeFunctionCall { _ => panic!() } } + + fn value_of_list_op(&self, params: &[Rc]) -> Rc { + match ¶ms[0].value { + ValueType::List(op1) => { + Rc::new(Value::new_int(op1.get_max_item().1)) + }, + _ => panic!() + } + } + + fn all_op(&self, params: &[Rc]) -> Rc { + match ¶ms[0].value { + ValueType::List(op1) => { + Rc::new(Value::new_list(op1.get_all())) + }, + _ => panic!() + } + } } impl RTObject for NativeFunctionCall { diff --git a/src/story.rs b/src/story.rs index 46dd31e..4e4ee47 100644 --- a/src/story.rs +++ b/src/story.rs @@ -9,7 +9,7 @@ use crate::{ error::ErrorType, json_serialization, push_pop::PushPopType, - story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, search_result::SearchResult, variable_assigment::VariableAssignment, native_function_call::NativeFunctionCall, variable_reference::VariableReference, + story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, search_result::SearchResult, variable_assigment::VariableAssignment, native_function_call::NativeFunctionCall, variable_reference::VariableReference, list_definitions_origin::ListDefinitionsOrigin, ink_list::InkList, }; const INK_VERSION_CURRENT: i32 = 21; @@ -33,6 +33,7 @@ pub struct Story { state_snapshot_at_last_new_line: Option, on_error: Option, prev_containers: Vec>, + list_definitions: Rc, } impl Story { @@ -71,10 +72,15 @@ impl Story { } }; - //object listDefsObj; - //if (rootObject.TryGetValue ("listDefs", out listDefsObj)) { - // _listDefinitions = Json.JTokenToListDefinitions (listDefsObj); - //} + let list_definitions = match json.get("listDefs") { + Some(def) => Rc::new(json_serialization::jtoken_to_list_definitions(def)?), + None => { + return Err( + "List Definitions node for ink not found. Are you sure it's a valid .ink.json file?" + .to_string(), + ) + } + }; let main_content_container = json_serialization::jtoken_to_runtime_object(root_token, None)?; @@ -95,6 +101,7 @@ impl Story { state_snapshot_at_last_new_line: None, on_error: None, prev_containers: Vec::new(), + list_definitions, }; story.reset_state(); @@ -113,7 +120,7 @@ impl Story { fn reset_state(&mut self) { //TODO ifAsyncWeCant("ResetState"); - self.state = Some(StoryState::new(self.main_content_container.clone())); + self.state = Some(StoryState::new(self.main_content_container.clone(), self.list_definitions.clone())); // TODO state.getVariablesState().setVariableChangedEvent(this); @@ -1058,8 +1065,62 @@ impl Story { } }, CommandType::End => self.get_state_mut().force_end(), - CommandType::ListFromInt => todo!(), - CommandType::ListRange => todo!(), + CommandType::ListFromInt => { + let mut int_val: Option = None; + let mut list_name_val: Option<&String> = None; + + let o = self.get_state_mut().pop_evaluation_stack(); + + if let Some(v) = Value::get_int_value(o.as_ref()) { + int_val = Some(v); + } + + let o = self.get_state_mut().pop_evaluation_stack(); + + if let Some(s) = Value::get_string_value(o.as_ref()) { + list_name_val = Some(&s.string); + } + + if int_val.is_none() { + panic!("Passed non-integer when creating a list element from a numerical value."); + } + + let mut generated_list_value: Option = None; + + if let Some(found_list_def) = self.list_definitions.as_ref().get_list_definition(&list_name_val.as_ref().unwrap()) { + if let Some(found_item) = found_list_def.get_item_with_value(int_val.unwrap()) { + let l = InkList::from_single_element((found_item.clone(), int_val.unwrap())); + generated_list_value = Some(Value::new_list(l)); + } + } else { + //panic!(format!("Failed to find List called {}", list_name_val.as_ref().unwrap())); + panic!(); + } + + if generated_list_value.is_none() { + generated_list_value = Some(Value::new_list(InkList::new())); + } + + self.get_state_mut().push_evaluation_stack(Rc::new(generated_list_value.unwrap())); + + }, + CommandType::ListRange => { + let mut p = self.get_state_mut().pop_evaluation_stack(); + let max = p.into_any().downcast::(); + + p = self.get_state_mut().pop_evaluation_stack(); + let min = p.into_any().downcast::(); + + p = self.get_state_mut().pop_evaluation_stack(); + let target_list = Value::get_list_value(p.as_ref()); + + if target_list.is_none() || min.is_err() || max.is_err() + {panic!("Expected List, minimum and maximum for LIST_RANGE");} + + let result = target_list.unwrap().list_with_sub_range(&min.unwrap().value, &max.unwrap().value); + + self.get_state_mut().push_evaluation_stack(Rc::new(Value::new_list(result))); + }, CommandType::ListRandom => todo!(), CommandType::BeginTag => self.get_state_mut().push_to_output_stream(content_obj.clone()), CommandType::EndTag => { @@ -1464,6 +1525,7 @@ impl Story { // "Failed to find content at path '{}', and no approximation of it was possible.", // path // )); + panic!() } else if result.approximate { // warning(format!( // "Failed to find content at path '{}', so it was approximated to: '{}'.", diff --git a/src/story_state.rs b/src/story_state.rs index b1d2b21..0ab3741 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -2,7 +2,7 @@ use std::{rc::Rc, cell::RefCell, collections::HashMap}; -use crate::{pointer::{Pointer, self}, callstack::CallStack, flow::Flow, variables_state::VariablesState, choice::Choice, object::{RTObject, Object}, value::{Value, ValueType}, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}, container::Container, state_patch::StatePatch, story::Story, path::Path, void::Void, tag::Tag}; +use crate::{pointer::{Pointer, self}, callstack::CallStack, flow::Flow, variables_state::VariablesState, choice::Choice, object::{RTObject, Object}, value::{Value, ValueType}, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}, container::Container, state_patch::StatePatch, story::Story, path::Path, void::Void, tag::Tag, list_definitions_origin::ListDefinitionsOrigin}; use rand::Rng; @@ -32,10 +32,11 @@ pub struct StoryState { pub story_seed: i32, pub previous_random: i32, current_tags: Vec, + list_definitions: Rc, } impl StoryState { - pub fn new(main_content_container: Rc) -> StoryState { + pub fn new(main_content_container: Rc, list_definitions: Rc) -> StoryState { let current_flow = Flow::new(DEFAULT_FLOW_NAME, main_content_container.clone()); let callstack = current_flow.callstack.clone(); @@ -43,14 +44,14 @@ impl StoryState { let story_seed = rng.gen_range(0..100); let mut state = StoryState { - current_flow: current_flow, + current_flow, did_safe_exit: false, output_stream_text_dirty: true, output_stream_tags_dirty: true, - variables_state: VariablesState::new(callstack), + variables_state: VariablesState::new(callstack, list_definitions.clone()), alive_flow_names_dirty: true, evaluation_stack: Vec::new(), - main_content_container: main_content_container, + main_content_container, current_errors: Vec::with_capacity(0), current_warnings: Vec::with_capacity(0), current_text: None, @@ -60,9 +61,10 @@ impl StoryState { visit_counts: HashMap::new(), turn_indices: HashMap::new(), current_turn_index: -1, - story_seed: story_seed, + story_seed, previous_random: 0, current_tags: Vec::with_capacity(0), + list_definitions, }; state.go_to_start(); @@ -172,13 +174,11 @@ impl StoryState { if !in_tag && text_content.is_some() { sb.push_str(&text_content.unwrap().string); - } else { - if let Some(control_command) = output_obj.as_ref().as_any().downcast_ref::() { - if control_command.command_type == CommandType::BeginTag { - in_tag = true; - } else if control_command.command_type == CommandType::EndTag { - in_tag = false; - } + } else if let Some(control_command) = output_obj.as_ref().as_any().downcast_ref::() { + if control_command.command_type == CommandType::BeginTag { + in_tag = true; + } else if control_command.command_type == CommandType::EndTag { + in_tag = false; } } } @@ -314,33 +314,19 @@ impl StoryState { } pub fn push_evaluation_stack(&mut self, obj: Rc) { + + if let Some(list) = Value::get_list_value(obj.as_ref()) { + let origin_names = list.get_origin_names(); - // TODO - - // let list_value = if let RTObject::ListValue(list_val) = &obj { - // Some(list_val) - // } else { - // None - // }; - - // if let Some(list_val) = list_value { - // if let InkList { - // origin_names: Some(origin_names), - // origins: Some(origins), - // .. - // } = &mut list_val.value - // { - // origins.clear(); - - // for name in origin_names.iter() { - // if let Some(def) = self.story.list_definitions.get_list_definition(name) { - // if !origins.contains(def) { - // origins.push(def.clone()); - // } - // } - // } - // } - // } + list.origins.borrow_mut().clear(); + + for name in &origin_names { + let def = self.list_definitions.get_list_definition(name).unwrap(); + if !list.origins.borrow().iter().any(|e| std::ptr::eq(e, def)){ + list.origins.borrow_mut().push(def.clone()); + } + } + } println!("PUSH: {}", obj.as_ref()); self.evaluation_stack.push(obj); @@ -703,7 +689,7 @@ impl StoryState { } pub fn copy_and_start_patching(&self) -> StoryState { - let mut copy = StoryState::new(self.main_content_container.clone()); + let mut copy = StoryState::new(self.main_content_container.clone(), self.list_definitions.clone()); copy.patch = Some(StatePatch::new(self.patch.as_ref())); diff --git a/src/value.rs b/src/value.rs index 75244e9..2df7df6 100644 --- a/src/value.rs +++ b/src/value.rs @@ -194,7 +194,7 @@ impl Value { if let Some(old_list) = Self::get_list_value_mut(old_value) { if let Some(new_list) = Self::get_list_value_mut(new_value) { if new_list.items.len() == 0 { - new_list.set_initial_origin_names(old_list.get_origin_names().clone()); + new_list.set_initial_origin_names(old_list.get_origin_names()); } } } diff --git a/src/variables_state.rs b/src/variables_state.rs index d8d0831..148162b 100644 --- a/src/variables_state.rs +++ b/src/variables_state.rs @@ -1,6 +1,6 @@ use std::{collections::{HashMap, HashSet}, rc::Rc, cell::RefCell}; -use crate::{object::RTObject, callstack::CallStack, state_patch::StatePatch, variable_assigment::VariableAssignment, value::{Value, VariablePointerValue}}; +use crate::{object::RTObject, callstack::CallStack, state_patch::StatePatch, variable_assigment::VariableAssignment, value::{Value, VariablePointerValue}, list_definitions_origin::ListDefinitionsOrigin}; #[derive(Clone)] @@ -11,20 +11,21 @@ pub struct VariablesState { pub callstack: Rc>, pub changed_variables_for_batch_obs: Option>, pub variable_changed_event: Option, - //TODO listDefsOrigin: ListDefinitionsOrigin + list_defs_origin: Rc, pub patch: Option, } impl VariablesState { - pub fn new(callstack: Rc>) -> VariablesState { + pub fn new(callstack: Rc>, list_defs_origin: Rc) -> VariablesState { VariablesState { global_variables: HashMap::new(), default_global_variables: None, batch_observing_variable_changes: false, - callstack: callstack, + callstack, changed_variables_for_batch_obs: None, variable_changed_event: None, patch: None, + list_defs_origin, } } @@ -191,10 +192,9 @@ impl VariablesState { } } - //TODO - // if let Some(list_item_value) = self.list_defs_origin.find_single_item_list_with_name(name) { - // return Some(list_item_value.clone()); - // } + if let Some(list_item_value) = self.list_defs_origin.find_single_item_list_with_name(name) { + return Some(list_item_value.clone()); + } } // Temporary diff --git a/tests/list_test.rs b/tests/list_test.rs index 047240c..037d220 100644 --- a/tests/list_test.rs +++ b/tests/list_test.rs @@ -12,3 +12,86 @@ fn list_basic_operations_test() -> Result<(), String> { Ok(()) } + +#[test] +fn list_mixed_items_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/lists/list-mixed-items.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + + assert_eq!("a, y, c\n", &story.continue_maximally()?); + + Ok(()) +} + +#[test] +fn more_list_operation_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/lists/more-list-operations.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + + assert_eq!("1\nl\nn\nl, m\nn\n", &story.continue_maximally()?); + + Ok(()) +} + +#[test] +fn empty_list_origin_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/lists/empty-list-origin.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + + assert_eq!("a, b\n", &story.continue_maximally()?); + + Ok(()) +} + +#[test] +fn list_save_load_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/lists/list-save-load.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + + //TODO + + Ok(()) +} + +#[test] +fn empty_list_origin_after_assinment_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/lists/empty-list-origin-after-assignment.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + + assert_eq!("a, b, c\n", &story.continue_maximally()?); + + Ok(()) +} + +#[test] +fn list_range_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/lists/list-range.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + + assert_eq!("Pound, Pizza, Euro, Pasta, Dollar, Curry, Paella\nEuro, Pasta, Dollar, Curry\nTwo, Three, Four, Five, Six\nPizza, Pasta\n", &story.continue_maximally()?); + + Ok(()) +} + +#[test] +fn list_bug_adding_element_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/lists/bug-adding-element.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + + assert_eq!("", &story.continue_maximally()?); + + story.choose_choice_index(0); + assert_eq!("a\n", &story.continue_maximally()?); + + story.choose_choice_index(1); + assert_eq!("OK\n", &story.continue_maximally()?); + + Ok(()) +} From 9fd29899adf4766989c49e64d745763343872413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Wed, 27 Sep 2023 22:48:06 +0000 Subject: [PATCH 36/91] All list tests passed except load/save state --- src/callstack.rs | 6 ++--- src/ink_list.rs | 44 ++++++++++++++++++++++++++++--------- src/native_function_call.rs | 16 ++++++++++++-- src/value.rs | 6 ++--- src/variables_state.rs | 4 +++- 5 files changed, 57 insertions(+), 19 deletions(-) diff --git a/src/callstack.rs b/src/callstack.rs index 3eab835..70aba4d 100644 --- a/src/callstack.rs +++ b/src/callstack.rs @@ -1,6 +1,6 @@ use std::{collections::HashMap, rc::Rc}; -use crate::{pointer::{Pointer, self}, object::RTObject, push_pop::PushPopType, story::Story, container::Container}; +use crate::{pointer::{Pointer, self}, object::RTObject, push_pop::PushPopType, story::Story, container::Container, value::Value}; pub struct Element { pub current_pointer: Pointer, @@ -223,8 +223,8 @@ impl CallStack { let old_value = context_element.temporary_variables.get(&name).cloned(); if let Some(old_value) = &old_value { - // TODO - //ListValue::retain_list_origins_for_assignment(old_value, &value); + Value::retain_list_origins_for_assignment(old_value.as_ref(), value.as_ref()); + } context_element.temporary_variables.insert(name, value); diff --git a/src/ink_list.rs b/src/ink_list.rs index 1f1d613..3312769 100644 --- a/src/ink_list.rs +++ b/src/ink_list.rs @@ -7,7 +7,7 @@ pub struct InkList { pub items: HashMap, pub origins: RefCell>, // we need an origin when we only have the definition (the list has not elemetns) - initial_origin_names:Vec, + initial_origin_names: RefCell>, } impl InkList { @@ -15,7 +15,7 @@ impl InkList { Self { items: HashMap::new(), origins: RefCell::new(Vec::with_capacity(0)), - initial_origin_names: Vec::with_capacity(0), + initial_origin_names: RefCell::new(Vec::with_capacity(0)), } } @@ -27,18 +27,18 @@ impl InkList { } pub fn from_single_origin(single_origin: String, list_definitions: ListDefinitionsOrigin) -> Self { - let mut l = Self::new(); + let l = Self::new(); - l.initial_origin_names.push(single_origin); + l.initial_origin_names.borrow_mut().push(single_origin); - let def = list_definitions.get_list_definition(&l.initial_origin_names[0]); + let def = list_definitions.get_list_definition(&l.initial_origin_names.borrow()[0]); if let Some(list_def) = def { l.origins.borrow_mut().push(list_def.clone()); } else { panic!( "InkList origin could not be found in story when constructing new list: {}", - &l.initial_origin_names[0] + &l.initial_origin_names.borrow()[0] ); } @@ -98,8 +98,8 @@ impl InkList { min } - pub fn set_initial_origin_names(&mut self, initial_origin_names: Vec) { - self.initial_origin_names = initial_origin_names; + pub fn set_initial_origin_names(&self, initial_origin_names: Vec) { + self.initial_origin_names.replace(initial_origin_names); } pub fn get_origin_names(&self) -> Vec { @@ -114,7 +114,7 @@ impl InkList { return names; } - self.initial_origin_names.clone() + self.initial_origin_names.borrow().clone() } pub fn union(&self, other_list: &InkList) -> InkList { @@ -127,6 +127,16 @@ impl InkList { union } + pub fn without(&self, other_list: &InkList) -> InkList { + let mut result = InkList::from_other_list(self); + + for (key, value) in &other_list.items { + result.items.remove(key); + } + + result + } + pub fn intersect(&self, other_list: &InkList) -> InkList { let mut intersection = InkList::new(); @@ -195,7 +205,7 @@ impl InkList { } let mut sub_list = InkList::new(); - sub_list.set_initial_origin_names(self.initial_origin_names.clone()); + sub_list.set_initial_origin_names(self.initial_origin_names.borrow().clone()); for (k, v) in ordered { if *v >= min_value && *v <= max_value { @@ -204,6 +214,20 @@ impl InkList { } return sub_list; + } + + pub fn inverse(&self) -> InkList { + let mut list = InkList::new(); + + for origin in self.origins.borrow_mut().iter_mut() { + for (k, v) in origin.get_items() { + if !self.items.contains_key(k) { + list.items.insert(k.clone(), *v); + } + } + } + + list } } diff --git a/src/native_function_call.rs b/src/native_function_call.rs index 489ecd2..5676c60 100644 --- a/src/native_function_call.rs +++ b/src/native_function_call.rs @@ -180,7 +180,7 @@ impl NativeFunctionCall { Op::All => self.all_op(&coerced_params), Op::Count => todo!(), Op::ValueOfList => self.value_of_list_op(&coerced_params), - Op::Invert => todo!(), + Op::Invert => self.inverse_op(&coerced_params), } } @@ -262,7 +262,10 @@ impl NativeFunctionCall { ValueType::Float(op2) => Rc::new(Value::new_float(*op1 - op2)), _ => panic!() }, - ValueType::List(op1) => todo!(), + ValueType::List(op1) => match ¶ms[1].value { + ValueType::List(op2) => Rc::new(Value::new_list(op1.without(op2))), + _ => panic!() + }, _ => panic!() } } @@ -486,6 +489,15 @@ impl NativeFunctionCall { _ => panic!() } } + + fn inverse_op(&self, params: &[Rc]) -> Rc { + match ¶ms[0].value { + ValueType::List(op1) => { + Rc::new(Value::new_list(op1.inverse())) + }, + _ => panic!() + } + } } impl RTObject for NativeFunctionCall { diff --git a/src/value.rs b/src/value.rs index 2df7df6..df98b44 100644 --- a/src/value.rs +++ b/src/value.rs @@ -189,10 +189,10 @@ impl Value { } } - pub fn retain_list_origins_for_assignment(old_value: &mut dyn RTObject, new_value: &mut dyn RTObject) { + pub fn retain_list_origins_for_assignment(old_value: &dyn RTObject, new_value: &dyn RTObject) { - if let Some(old_list) = Self::get_list_value_mut(old_value) { - if let Some(new_list) = Self::get_list_value_mut(new_value) { + if let Some(old_list) = Self::get_list_value(old_value) { + if let Some(new_list) = Self::get_list_value(new_value) { if new_list.items.len() == 0 { new_list.set_initial_origin_names(old_list.get_origin_names()); } diff --git a/src/variables_state.rs b/src/variables_state.rs index 148162b..eecb754 100644 --- a/src/variables_state.rs +++ b/src/variables_state.rs @@ -214,7 +214,9 @@ impl VariablesState { old_value = self.global_variables.get(name).cloned(); } - // TODO ListValue::retain_list_origins_for_assignment(&mut old_value, &value); + if let Some(old_value) = &old_value { + Value::retain_list_origins_for_assignment(old_value.as_ref(), value.as_ref()); + } if let Some(patch) = &mut self.patch { patch.set_global(name, value.clone()); From f83b6aed73b3b5e9181a6b592ac5fdbf799ca48b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Thu, 28 Sep 2023 12:14:29 +0000 Subject: [PATCH 37/91] Added new test to complete operation tests on lists. --- .../inkfiles/lists/more-list-operations2.ink | 41 +++++ .../lists/more-list-operations2.ink.json | 1 + src/ink_list.rs | 118 +++++++++++++-- src/native_function_call.rs | 140 ++++++++++++++++-- src/story.rs | 47 +++++- src/value.rs | 21 ++- tests/list_test.rs | 13 +- 7 files changed, 338 insertions(+), 43 deletions(-) create mode 100644 examples/inkfiles/lists/more-list-operations2.ink create mode 100644 examples/inkfiles/lists/more-list-operations2.ink.json diff --git a/examples/inkfiles/lists/more-list-operations2.ink b/examples/inkfiles/lists/more-list-operations2.ink new file mode 100644 index 0000000..d21346f --- /dev/null +++ b/examples/inkfiles/lists/more-list-operations2.ink @@ -0,0 +1,41 @@ +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)} + + diff --git a/examples/inkfiles/lists/more-list-operations2.ink.json b/examples/inkfiles/lists/more-list-operations2.ink.json new file mode 100644 index 0000000..969037a --- /dev/null +++ b/examples/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",["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/ink_list.rs b/src/ink_list.rs index 3312769..3f9c257 100644 --- a/src/ink_list.rs +++ b/src/ink_list.rs @@ -26,7 +26,7 @@ impl InkList { l } - pub fn from_single_origin(single_origin: String, list_definitions: ListDefinitionsOrigin) -> Self { + pub fn from_single_origin(single_origin: String, list_definitions: &ListDefinitionsOrigin) -> Self { let l = Self::new(); l.initial_origin_names.borrow_mut().push(single_origin); @@ -72,12 +72,12 @@ impl InkList { ordered } - pub fn get_max_item(&self) -> (Option<&InkListItem>, i32) { - let mut max = (None, 0); + pub fn get_max_item(&self) -> Option<(&InkListItem, i32)> { + let mut max: Option<(&InkListItem, i32)> = None; for (k,v) in &self.items { - if max.0.is_none() || *v > max.1 { - max = (Some(k), *v); + if max.is_none() || *v > max.as_ref().unwrap().1 { + max = Some((k, *v)); } } @@ -85,12 +85,12 @@ impl InkList { max } - pub fn get_min_item(&self) -> (Option<&InkListItem>, i32) { - let mut min = (None, 0); + pub fn get_min_item(&self) -> Option<(&InkListItem, i32)> { + let mut min: Option<(&InkListItem, i32)> = None; for (k,v) in &self.items { - if min.0.is_none() || *v < min.1 { - min = (Some(k), *v); + if min.is_none() || *v < min.as_ref().unwrap().1 { + min = Some((k, *v)); } } @@ -150,15 +150,15 @@ impl InkList { } pub fn has(&self, other_list: &InkList) -> InkList { - let mut intersection = InkList::new(); + let mut result = InkList::new(); for (k, v) in &self.items { if other_list.items.contains_key(k) { - intersection.items.insert(k.clone(), *v); + result.items.insert(k.clone(), *v); } } - intersection + result } pub fn contains(&self, other_list: &InkList) -> bool { @@ -192,7 +192,7 @@ impl InkList { min_value = *v; } else if let ValueType::List(l) = min_bound { if !l.items.is_empty() { - min_value = l.get_min_item().1; + min_value = l.get_min_item().unwrap().1; } } @@ -200,7 +200,7 @@ impl InkList { max_value = *v; } else if let ValueType::List(l) = max_bound { if !l.items.is_empty() { - max_value = l.get_min_item().1; + max_value = l.get_max_item().unwrap().1; } } @@ -228,7 +228,95 @@ impl InkList { } list - } + } + + pub fn max_as_list(&self) -> InkList { + match self.items.is_empty() { + true => InkList::new(), + false => { + let item = self.get_max_item(); + InkList::from_single_element((item.as_ref().unwrap().0.clone(), item.as_ref().unwrap().1)) + }, + } + } + + pub fn min_as_list(&self) -> InkList { + match self.items.is_empty() { + true => InkList::new(), + false => { + let item = self.get_min_item(); + InkList::from_single_element((item.as_ref().unwrap().0.clone(), item.as_ref().unwrap().1)) + }, + } + } + + // Returns true if all the item values in the current list are greater than all + // the item values in the passed-in list. + pub fn greater_than(&self, other_list: &InkList) -> bool { + if self.items.is_empty() { + return false; + } + if other_list.items.is_empty() { + return true; + } + + // All greater + self.get_min_item().unwrap().1 > other_list.get_max_item().unwrap().1 + } + + // Returns true if the item values in the current list overlap or are all + // greater than the item values in the passed-in list. + pub fn greater_than_or_equals(&self, other_list: &InkList) -> bool { + if self.items.is_empty() { + return false; + } + if other_list.items.is_empty() { + return true; + } + + // All greater + self.get_min_item().unwrap().1 >= other_list.get_min_item().unwrap().1 + && self.get_max_item().unwrap().1 >= other_list.get_max_item().unwrap().1 + } + + // Returns true if all the item values in the current list are less than all the + // item values in the passed-in list. + pub fn less_than(&self, other_list: &InkList) -> bool { + if other_list.items.is_empty() { + return false; + } + if self.items.is_empty() { + return true; + } + + self.get_max_item().unwrap().1 < other_list.get_min_item().unwrap().1 + } + + // Returns true if the item values in the current list overlap or are all less + // than the item values in the passed-in list. + pub fn less_than_or_equals(&self, other_list: &InkList) -> bool { + if other_list.items.is_empty() { + return false; + } + if self.items.is_empty() { + return true; + } + + self.get_max_item().unwrap().1 <= other_list.get_max_item().unwrap().1 + && self.get_min_item().unwrap().1 <= other_list.get_min_item().unwrap().1 + } +} + +impl PartialEq for InkList { + fn eq(&self, other: &Self) -> bool { + if other.items.len() != self.items.len() {return false;} + + for key in self.items.keys() { + if !other.items.contains_key(key) {return false;} + } + + true + } } impl fmt::Display for InkList { diff --git a/src/native_function_call.rs b/src/native_function_call.rs index 5676c60..06b9155 100644 --- a/src/native_function_call.rs +++ b/src/native_function_call.rs @@ -158,11 +158,11 @@ impl NativeFunctionCall { Op::Negate => todo!(), Op::Equal => self.equal_op(&coerced_params), Op::Greater => self.greater_op(&coerced_params), - Op::Less => todo!(), - Op::GreaterThanOrEquals => todo!(), - Op::LessThanOrEquals => todo!(), + Op::Less => self.less_op(&coerced_params), + Op::GreaterThanOrEquals => self.greater_than_or_equals_op(&coerced_params), + Op::LessThanOrEquals => self.less_than_or_equals_op(&coerced_params), Op::NotEquals => self.not_equals_op(&coerced_params), - Op::Not => todo!(), + Op::Not => self.not_op(&coerced_params), Op::And => self.and_op(&coerced_params), Op::Or => self.or_op(&coerced_params), Op::Min => self.min_op(&coerced_params), @@ -175,10 +175,10 @@ impl NativeFunctionCall { Op::Has => self.has(&coerced_params), Op::Hasnt => self.hasnt(&coerced_params), Op::Intersect => self.intersect_op(&coerced_params), - Op::ListMin => todo!(), - Op::ListMax => todo!(), + Op::ListMin => self.list_min_op(&coerced_params), + Op::ListMax => self.list_max_op(&coerced_params), Op::All => self.all_op(&coerced_params), - Op::Count => todo!(), + Op::Count => self.count_op(&coerced_params), Op::ValueOfList => self.value_of_list_op(&coerced_params), Op::Invert => self.inverse_op(&coerced_params), } @@ -215,7 +215,7 @@ impl NativeFunctionCall { } } - return result; + result } fn and_op(&self, params: &[Rc]) -> Rc { @@ -232,7 +232,10 @@ impl NativeFunctionCall { ValueType::Float(op2) => Rc::new(Value::new_bool(*op1 != 0.0 && op2 != 0.0)), _ => panic!() }, - ValueType::List(l) => todo!(), + ValueType::List(op1) => match ¶ms[1].value { + ValueType::List(op2) => Rc::new(Value::new_bool(!op1.items.is_empty() && !op2.items.is_empty())), + _ => panic!() + }, _ => panic!() } } @@ -247,7 +250,65 @@ impl NativeFunctionCall { ValueType::Float(op2) => Rc::new(Value::new_bool(*op1 > op2)), _ => panic!() }, - ValueType::List(l) => todo!(), + ValueType::List(op1) => match ¶ms[1].value { + ValueType::List(op2) => Rc::new(Value::new_bool(op1.greater_than(op2))), + _ => panic!() + }, + _ => panic!() + } + } + + + fn less_op(&self, params: &[Rc]) -> Rc { + match ¶ms[0].value { + ValueType::Int(op1) => match params[1].value { + ValueType::Int(op2) => Rc::new(Value::new_bool(*op1 < op2)), + _ => panic!() + }, + ValueType::Float(op1) => match params[1].value { + ValueType::Float(op2) => Rc::new(Value::new_bool(*op1 < op2)), + _ => panic!() + }, + ValueType::List(op1) => match ¶ms[1].value { + ValueType::List(op2) => Rc::new(Value::new_bool(op1.less_than(op2))), + _ => panic!() + }, + _ => panic!() + } + } + + fn greater_than_or_equals_op(&self, params: &[Rc]) -> Rc { + match ¶ms[0].value { + ValueType::Int(op1) => match params[1].value { + ValueType::Int(op2) => Rc::new(Value::new_bool(*op1 >= op2)), + _ => panic!() + }, + ValueType::Float(op1) => match params[1].value { + ValueType::Float(op2) => Rc::new(Value::new_bool(*op1 >= op2)), + _ => panic!() + }, + ValueType::List(op1) => match ¶ms[1].value { + ValueType::List(op2) => Rc::new(Value::new_bool(op1.greater_than_or_equals(op2))), + _ => panic!() + }, + _ => panic!() + } + } + + fn less_than_or_equals_op(&self, params: &[Rc]) -> Rc { + match ¶ms[0].value { + ValueType::Int(op1) => match params[1].value { + ValueType::Int(op2) => Rc::new(Value::new_bool(*op1 <= op2)), + _ => panic!() + }, + ValueType::Float(op1) => match params[1].value { + ValueType::Float(op2) => Rc::new(Value::new_bool(*op1 <= op2)), + _ => panic!() + }, + ValueType::List(op1) => match ¶ms[1].value { + ValueType::List(op2) => Rc::new(Value::new_bool(op1.less_than_or_equals(op2))), + _ => panic!() + }, _ => panic!() } } @@ -339,7 +400,22 @@ impl NativeFunctionCall { ValueType::Float(op2) => Rc::new(Value::new_bool(*op1 != 0.0 || op2 != 0.0)), _ => panic!() }, - ValueType::List(l) => todo!(), + ValueType::List(op1) => match ¶ms[1].value { + ValueType::List(op2) => Rc::new(Value::new_bool(!op1.items.is_empty() || !op2.items.is_empty())), + _ => panic!() + }, + _ => panic!() + } + } + + fn not_op(&self, params: &[Rc]) -> Rc { + match ¶ms[0].value { + ValueType::Int(op1) => Rc::new(Value::new_bool(*op1 == 0)), + ValueType::Float(op1) => Rc::new(Value::new_bool(*op1 == 0.0)), + ValueType::List(op1) => Rc::new(Value::new_int(match op1.items.is_empty() { + true => 1, + false => 0, + } )), _ => panic!() } } @@ -392,7 +468,10 @@ impl NativeFunctionCall { ValueType::String(op2) => Rc::new(Value::new_bool(op1.string.eq(&op2.string))), _ => panic!() }, - ValueType::List(l) => todo!(), + ValueType::List(op1) => match ¶ms[1].value { + ValueType::List(op2) => Rc::new(Value::new_bool(op1.eq(op2))), + _ => panic!() + }, _ => panic!() } } @@ -415,7 +494,10 @@ impl NativeFunctionCall { ValueType::String(op2) => Rc::new(Value::new_bool(!op1.string.eq(&op2.string))), _ => panic!() }, - ValueType::List(l) => todo!(), + ValueType::List(op1) => match ¶ms[1].value { + ValueType::List(op2) => Rc::new(Value::new_bool(!op1.eq(op2))), + _ => panic!() + }, _ => panic!() } } @@ -475,7 +557,10 @@ impl NativeFunctionCall { fn value_of_list_op(&self, params: &[Rc]) -> Rc { match ¶ms[0].value { ValueType::List(op1) => { - Rc::new(Value::new_int(op1.get_max_item().1)) + match op1.get_max_item() { + Some(i) => Rc::new(Value::new_int(i.1)), + None => Rc::new(Value::new_int(0)), + } }, _ => panic!() } @@ -498,6 +583,33 @@ impl NativeFunctionCall { _ => panic!() } } + + fn count_op(&self, params: &[Rc]) -> Rc { + match ¶ms[0].value { + ValueType::List(op1) => { + Rc::new(Value::new_int(op1.items.len() as i32)) + }, + _ => panic!() + } + } + + fn list_max_op(&self, params: &[Rc]) -> Rc { + match ¶ms[0].value { + ValueType::List(op1) => { + Rc::new(Value::new_list(op1.max_as_list())) + }, + _ => panic!() + } + } + + fn list_min_op(&self, params: &[Rc]) -> Rc { + match ¶ms[0].value { + ValueType::List(op1) => { + Rc::new(Value::new_list(op1.min_as_list())) + }, + _ => panic!() + } + } } impl RTObject for NativeFunctionCall { diff --git a/src/story.rs b/src/story.rs index 4e4ee47..8ef31cc 100644 --- a/src/story.rs +++ b/src/story.rs @@ -1121,7 +1121,52 @@ impl Story { self.get_state_mut().push_evaluation_stack(Rc::new(Value::new_list(result))); }, - CommandType::ListRandom => todo!(), + CommandType::ListRandom => { + let o = self.get_state_mut().pop_evaluation_stack(); + let list = Value::get_list_value(o.as_ref()); + + if list.is_none() {panic!("Expected list for LIST_RANDOM");} + + let list = list.unwrap(); + + let new_list = { + // List was empty: return empty list + if list.items.is_empty() { + InkList::new() + } + // Non-empty source list + else { + // Generate a random index for the element to take + let result_seed = self.get_state().story_seed + self.get_state().previous_random; + let mut rng = StdRng::seed_from_u64(result_seed as u64); + let next_random = rng.gen::(); + let list_item_index = (next_random as usize) % list.items.len(); + + // Iterate through to get the random element + let mut list_enumerator = list.items.iter(); + let mut random_item = None; + + for (i, (key, value)) in list_enumerator.enumerate() { + if i == list_item_index { + random_item = Some((key.clone(), *value)); + break; + } + } + + let random_item = random_item.unwrap(); + + // Origin list is simply the origin of the one element + let mut new_list = InkList::from_single_origin(random_item.0.get_origin_name().unwrap().clone(), self.list_definitions.as_ref()); + new_list.items.insert(random_item.0.clone(), random_item.1); + + self.get_state_mut().previous_random = next_random as i32; + + new_list + } + }; + + self.get_state_mut().push_evaluation_stack(Rc::new(Value::new_list(new_list))); + }, CommandType::BeginTag => self.get_state_mut().push_to_output_stream(content_obj.clone()), CommandType::EndTag => { diff --git a/src/value.rs b/src/value.rs index df98b44..54da5fd 100644 --- a/src/value.rs +++ b/src/value.rs @@ -271,27 +271,24 @@ impl Value { match cast_dest_type { CAST_INT => { let max = l.get_max_item(); - if max.0.is_none() { - Some(Self::new_int(0)) - } else { - Some(Self::new_int(max.1)) + match max { + Some(i) => Some(Self::new_int(i.1)), + None => Some(Self::new_int(0)) } }, CAST_FLOAT => { let max = l.get_max_item(); - if max.0.is_none() { - Some(Self::new_float(0.0)) - } else { - Some(Self::new_float(max.1 as f32)) + match max { + Some(i) => Some(Self::new_float(i.1 as f32)), + None => Some(Self::new_float(0.0)) } }, CAST_LIST => None, CAST_STRING => { let max = l.get_max_item(); - if max.0.is_none() { - Some(Self::new_string("")) - } else { - Some(Self::new_string(&max.0.unwrap().to_string())) + match max { + Some(i) => Some(Self::new_string(&i.0.to_string())), + None => Some(Self::new_string("")) } }, _ => panic!(), diff --git a/tests/list_test.rs b/tests/list_test.rs index 037d220..5048ada 100644 --- a/tests/list_test.rs +++ b/tests/list_test.rs @@ -25,7 +25,7 @@ fn list_mixed_items_test() -> Result<(), String> { } #[test] -fn more_list_operation_test() -> Result<(), String> { +fn more_list_operations_test() -> Result<(), String> { let json_string = common::get_json_string("examples/inkfiles/lists/more-list-operations.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -95,3 +95,14 @@ fn list_bug_adding_element_test() -> Result<(), String> { Ok(()) } + +#[test] +fn more_list_operations2_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/lists/more-list-operations2.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + + assert_eq!("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:c2\n", &story.continue_maximally()?); + + Ok(()) +} From 3ecc7ba4041fb4c59cbbd88f7252e62af1c75337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Thu, 28 Sep 2023 16:30:15 +0000 Subject: [PATCH 38/91] Support special list operations. --- .../inkfiles/lists/more-list-operations2.ink | 2 + .../lists/more-list-operations2.ink.json | 2 +- src/native_function_call.rs | 96 ++++++++++++++++++- tests/list_test.rs | 2 +- 4 files changed, 95 insertions(+), 7 deletions(-) diff --git a/examples/inkfiles/lists/more-list-operations2.ink b/examples/inkfiles/lists/more-list-operations2.ink index d21346f..4c30ed0 100644 --- a/examples/inkfiles/lists/more-list-operations2.ink +++ b/examples/inkfiles/lists/more-list-operations2.ink @@ -38,4 +38,6 @@ subtract:{(a1,b1,c1) - (b1)} ~ SEED_RANDOM(10) random:{LIST_RANDOM(t)} +listinc:{(a1) + 1} + diff --git a/examples/inkfiles/lists/more-list-operations2.ink.json b/examples/inkfiles/lists/more-list-operations2.ink.json index 969037a..47a4075 100644 --- a/examples/inkfiles/lists/more-list-operations2.ink.json +++ b/examples/inkfiles/lists/more-list-operations2.ink.json @@ -1 +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",["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 +{"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/native_function_call.rs b/src/native_function_call.rs index 06b9155..ea83cc8 100644 --- a/src/native_function_call.rs +++ b/src/native_function_call.rs @@ -1,8 +1,8 @@ -use std::{fmt, collections::HashMap, rc::Rc}; +use std::{fmt, rc::Rc}; -use crate::{object::{Object, RTObject}, value::{Value, ValueType}, void::Void}; +use crate::{object::{Object, RTObject}, value::{Value, ValueType}, void::Void, ink_list::InkList}; -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum Op { Add, Subtract, @@ -129,9 +129,9 @@ impl NativeFunctionCall { } } - pub(crate) fn call(&self, params: Vec>) -> std::rc::Rc { + pub(crate) fn call(&self, params: Vec>) -> Rc { - if self.get_number_of_parameters() != params.len() || params.len() < 1 || params.len() > 2 { + if self.get_number_of_parameters() != params.len() { panic!("Unexpected number of parameters"); } @@ -147,8 +147,94 @@ impl NativeFunctionCall { } } + // Binary operations on lists are treated outside of the standard + // coerscion rules + if params.len() == 2 && has_list { + return self.call_binary_list_operation(¶ms); + } + let coerced_params = self.coerce_values_to_single_type(params); + self.call_type(coerced_params) + } + + fn call_binary_list_operation(&self, params: &Vec>) -> Rc { + // List-Int addition/subtraction returns a List (e.g., "alpha" + 1 = "beta") + if (self.op == Op::Add || self.op == Op::Subtract) && + Value::get_list_value(params[0].as_ref()).is_some() && + Value::get_int_value(params[1].as_ref()).is_some() { + return self.call_list_increment_operation(params); + } + + let v1 = params[0].clone().into_any().downcast::().unwrap(); + let v2 = params[1].clone().into_any().downcast::().unwrap(); + + // And/or with any other type requires coercion to bool + if (self.op == Op::And || self.op == Op::Or) && + ( Value::get_list_value(params[0].as_ref()).is_none() || + Value::get_list_value(params[1].as_ref()).is_none()) { + + let result = { + if self.op == Op::And { + v1.is_truthy() && v2.is_truthy() + } else { + v1.is_truthy() || v2.is_truthy() + } + }; + + return Rc::new(Value::new_bool(result)); + } + + // Normal (list • list) operation + if Value::get_list_value(params[0].as_ref()).is_some() && + Value::get_list_value(params[1].as_ref()).is_some() { + let mut p = vec![v1.clone(), v2.clone()]; + + return self.call_type(p); + } + + // Err(StoryError::new(format!( + // "Can not call use '{}' operation on {} and {}", + // self.name, + // v1.value_type(), + // v2.value_type() + // ))) + panic!() + } + + fn call_list_increment_operation(&self, list_int_params: &Vec>) -> Rc { + let list_val = Value::get_list_value(list_int_params[0].as_ref()).unwrap(); + let int_val = Value::get_int_value(list_int_params[1].as_ref()).unwrap(); + + let mut result_raw_list = InkList::new(); + + for (list_item, list_item_value) in list_val.items.iter() { + + let target_int = { + if self.op == Op::Add { + list_item_value + int_val + } else { + list_item_value - int_val + } + }; + + let origins = list_val.origins.borrow(); + + let item_origin = origins.iter().find(|origin| { + origin.get_name() == list_item.get_origin_name().unwrap_or(&"".to_owned()) + }); + + if let Some(item_origin) = item_origin { + if let Some(incremented_item) = item_origin.get_item_with_value(target_int) { + result_raw_list.items.insert(incremented_item.clone(), target_int); + } + } + } + + Rc::new(Value::new_list(result_raw_list)) + } + + fn call_type(&self, coerced_params: Vec>) -> Rc { match self.op { Op::Add => self.add_op(&coerced_params), Op::Subtract => self.subtract_op(&coerced_params), diff --git a/tests/list_test.rs b/tests/list_test.rs index 5048ada..3c24961 100644 --- a/tests/list_test.rs +++ b/tests/list_test.rs @@ -102,7 +102,7 @@ fn more_list_operations2_test() -> Result<(), String> { common::get_json_string("examples/inkfiles/lists/more-list-operations2.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); - assert_eq!("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:c2\n", &story.continue_maximally()?); + assert_eq!("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:c2\nlistinc:b1\n", &story.continue_maximally()?); Ok(()) } From e986514079e5034a7741086be4e080f43947479f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Thu, 28 Sep 2023 16:53:34 +0000 Subject: [PATCH 39/91] fix ListRandom for predictibility --- src/story.rs | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/story.rs b/src/story.rs index 8ef31cc..2f791c6 100644 --- a/src/story.rs +++ b/src/story.rs @@ -9,7 +9,7 @@ use crate::{ error::ErrorType, json_serialization, push_pop::PushPopType, - story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, search_result::SearchResult, variable_assigment::VariableAssignment, native_function_call::NativeFunctionCall, variable_reference::VariableReference, list_definitions_origin::ListDefinitionsOrigin, ink_list::InkList, + story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, search_result::SearchResult, variable_assigment::VariableAssignment, native_function_call::NativeFunctionCall, variable_reference::VariableReference, list_definitions_origin::ListDefinitionsOrigin, ink_list::InkList, ink_list_item::InkListItem, }; const INK_VERSION_CURRENT: i32 = 21; @@ -1142,22 +1142,14 @@ impl Story { let next_random = rng.gen::(); let list_item_index = (next_random as usize) % list.items.len(); - // Iterate through to get the random element - let mut list_enumerator = list.items.iter(); - let mut random_item = None; - - for (i, (key, value)) in list_enumerator.enumerate() { - if i == list_item_index { - random_item = Some((key.clone(), *value)); - break; - } - } - - let random_item = random_item.unwrap(); + // Iterate through to get the random element, sorted for predictibility + let mut sorted: Vec<(&InkListItem, &i32)> = list.items.iter().collect(); + sorted.sort_by(|a, b| b.1.cmp(a.1)); + let random_item = sorted[list_item_index]; // Origin list is simply the origin of the one element let mut new_list = InkList::from_single_origin(random_item.0.get_origin_name().unwrap().clone(), self.list_definitions.as_ref()); - new_list.items.insert(random_item.0.clone(), random_item.1); + new_list.items.insert(random_item.0.clone(), *random_item.1); self.get_state_mut().previous_random = next_random as i32; From b9e79a0b5c3e384a4eb7115a2d5bc6ca753d0250 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Thu, 28 Sep 2023 23:53:54 +0000 Subject: [PATCH 40/91] Added missing operations and test --- README.md | 6 +- examples/inkfiles/misc/operations.ink | 9 +++ examples/inkfiles/misc/operations.ink.json | 1 + src/native_function_call.rs | 86 ++++++++++++++++++++-- src/state_patch.rs | 5 ++ src/story.rs | 63 +++++++++++++++- src/story_state.rs | 36 +++++++-- src/variables_state.rs | 39 ++++++++++ tests/misc_test.rs | 39 ++++++++++ 9 files changed, 265 insertions(+), 19 deletions(-) create mode 100644 examples/inkfiles/misc/operations.ink create mode 100644 examples/inkfiles/misc/operations.ink.json create mode 100644 tests/misc_test.rs diff --git a/README.md b/README.md index b4f8647..91f559c 100644 --- a/README.md +++ b/README.md @@ -14,16 +14,18 @@ Currently under development. This is the implementation status: - [x] Nested flows - [ ] Variables and Logic - [x] Conditional blocks (if/else) -- [ ] Temporary Variables +- [x] Temporary Variables - [x] Functions - [x] Tunnels - [x] Threads - [x] Tags -- [ ] Lists +- [x] Lists - [ ] Load/Save state ## TODO +- [ ] Test for visit counts +- [ ] Test for flow - [ ] Use OnceCell to lazy init the cache fields of RTObjects - [ ] Error handling - [ ] Split large files. ex. Get the error handling out of the Story class. The performLogic diff --git a/examples/inkfiles/misc/operations.ink b/examples/inkfiles/misc/operations.ink new file mode 100644 index 0000000..7f71627 --- /dev/null +++ b/examples/inkfiles/misc/operations.ink @@ -0,0 +1,9 @@ +VAR x = 3 + +neg:{-x} +mod:{x%2} +pow:{POW(x,x)} +floor:{FLOOR(3.5)} +ceiling:{CEILING(3.5)} +int:{INT(3.5)} +float:{FLOAT(true)} \ No newline at end of file diff --git a/examples/inkfiles/misc/operations.ink.json b/examples/inkfiles/misc/operations.ink.json new file mode 100644 index 0000000..f3df00b --- /dev/null +++ b/examples/inkfiles/misc/operations.ink.json @@ -0,0 +1 @@ +{"inkVersion":21,"root":[["^neg:","ev",{"VAR?":"x"},"_","out","/ev","\n","^mod:","ev",{"VAR?":"x"},2,"%","out","/ev","\n","^pow:","ev",{"VAR?":"x"},{"VAR?":"x"},"POW","out","/ev","\n","^floor:","ev",3.5,"FLOOR","out","/ev","\n","^ceiling:","ev",3.5,"CEILING","out","/ev","\n","^int:","ev",3.5,"INT","out","/ev","\n","^float:","ev",true,"FLOAT","out","/ev","\n",["done",{"#f":5,"#n":"g-0"}],null],"done",{"global decl":["ev",3,{"VAR=":"x"},"/ev","end",null],"#f":1}],"listDefs":{}} \ No newline at end of file diff --git a/src/native_function_call.rs b/src/native_function_call.rs index ea83cc8..0eda5dc 100644 --- a/src/native_function_call.rs +++ b/src/native_function_call.rs @@ -241,7 +241,7 @@ impl NativeFunctionCall { Op::Divide => self.divide_op(&coerced_params), Op::Multiply => self.multiply_op(&coerced_params), Op::Mod => self.mod_op(&coerced_params), - Op::Negate => todo!(), + Op::Negate => self.negate_op(&coerced_params), Op::Equal => self.equal_op(&coerced_params), Op::Greater => self.greater_op(&coerced_params), Op::Less => self.less_op(&coerced_params), @@ -253,11 +253,11 @@ impl NativeFunctionCall { Op::Or => self.or_op(&coerced_params), Op::Min => self.min_op(&coerced_params), Op::Max => self.max_op(&coerced_params), - Op::Pow => todo!(), - Op::Floor => todo!(), - Op::Ceiling => todo!(), - Op::Int => todo!(), - Op::Float => todo!(), + Op::Pow => self.pow_op(&coerced_params), + Op::Floor => self.floor_op(&coerced_params), + Op::Ceiling => self.ceiling_op(&coerced_params), + Op::Int => self.int_op(&coerced_params), + Op::Float => self.float_op(&coerced_params), Op::Has => self.has(&coerced_params), Op::Hasnt => self.hasnt(&coerced_params), Op::Intersect => self.intersect_op(&coerced_params), @@ -458,6 +458,20 @@ impl NativeFunctionCall { } } + fn pow_op(&self, params: &[Rc]) -> Rc { + match params[0].value { + ValueType::Int(op1) => match params[1].value { + ValueType::Int(op2) => Rc::new(Value::new_float((op1 as f32).powf(op2 as f32))), + _ => panic!() + }, + ValueType::Float(op1) => match params[1].value { + ValueType::Float(op2) => Rc::new(Value::new_float(op1.powf(op2))), + _ => panic!() + }, + _ => panic!() + } + } + fn multiply_op(&self, params: &[Rc]) -> Rc { match params[0].value { ValueType::Int(op1) => match params[1].value { @@ -696,6 +710,66 @@ impl NativeFunctionCall { _ => panic!() } } + + fn negate_op(&self, params: &[Rc]) -> Rc { + match ¶ms[0].value { + ValueType::Int(op1) => { + Rc::new(Value::new_int(-op1)) + }, + ValueType::Float(op1) => { + Rc::new(Value::new_float(-op1)) + }, + _ => panic!() + } + } + + fn floor_op(&self, params: &[Rc]) -> Rc { + match ¶ms[0].value { + ValueType::Int(op1) => { + Rc::new(Value::new_int(*op1)) + }, + ValueType::Float(op1) => { + Rc::new(Value::new_float(op1.floor())) + }, + _ => panic!() + } + } + + fn ceiling_op(&self, params: &[Rc]) -> Rc { + match ¶ms[0].value { + ValueType::Int(op1) => { + Rc::new(Value::new_int(*op1)) + }, + ValueType::Float(op1) => { + Rc::new(Value::new_float(op1.ceil())) + }, + _ => panic!() + } + } + + fn int_op(&self, params: &[Rc]) -> Rc { + match ¶ms[0].value { + ValueType::Int(op1) => { + Rc::new(Value::new_int(*op1)) + }, + ValueType::Float(op1) => { + Rc::new(Value::new_int(*op1 as i32)) + }, + _ => panic!() + } + } + + fn float_op(&self, params: &[Rc]) -> Rc { + match ¶ms[0].value { + ValueType::Int(op1) => { + Rc::new(Value::new_float(*op1 as f32)) + }, + ValueType::Float(op1) => { + Rc::new(Value::new_float(*op1)) + }, + _ => panic!() + } + } } impl RTObject for NativeFunctionCall { diff --git a/src/state_patch.rs b/src/state_patch.rs index 5d9541f..281ae87 100644 --- a/src/state_patch.rs +++ b/src/state_patch.rs @@ -56,4 +56,9 @@ impl StatePatch { let key = Object::get_path(container).to_string(); self.turn_indices.insert(key, index as usize); } + + pub(crate) fn get_turn_index(&self, container: &Container) -> Option<&usize> { + let key = Object::get_path(container).to_string(); + return self.turn_indices.get(&key); + } } \ No newline at end of file diff --git a/src/story.rs b/src/story.rs index 2f791c6..77e26ce 100644 --- a/src/story.rs +++ b/src/story.rs @@ -9,7 +9,7 @@ use crate::{ error::ErrorType, json_serialization, push_pop::PushPopType, - story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, search_result::SearchResult, variable_assigment::VariableAssignment, native_function_call::NativeFunctionCall, variable_reference::VariableReference, list_definitions_origin::ListDefinitionsOrigin, ink_list::InkList, ink_list_item::InkListItem, + story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, search_result::SearchResult, variable_assigment::VariableAssignment, native_function_call::NativeFunctionCall, variable_reference::VariableReference, list_definitions_origin::ListDefinitionsOrigin, ink_list::InkList, ink_list_item::InkListItem, variables_state::VariablesState, }; const INK_VERSION_CURRENT: i32 = 21; @@ -745,6 +745,14 @@ impl Story { } } + pub fn get_variables_state(&self) -> &VariablesState { + self.get_state().get_variables_state() + } + + pub fn get_variables_state_mut(&mut self) -> &mut VariablesState { + self.get_state_mut().get_variables_state_mut() + } + fn perform_logic_and_flow_control(&mut self, content_obj: &Option>) -> bool { let content_obj = match content_obj { Some(content_obj) => { @@ -963,9 +971,56 @@ impl Story { self.get_state_mut().push_evaluation_stack(Rc::new(Value::new_string(&sb))); }, CommandType::NoOp => {}, - CommandType::ChoiceCount => todo!(), - CommandType::Turns => todo!(), - CommandType::TurnsSince | CommandType::ReadCount => todo!(), + CommandType::ChoiceCount => { + let choice_count = self.get_state().get_generated_choices().len(); + self.get_state_mut().push_evaluation_stack(Rc::new(Value::new_int(choice_count as i32))); + }, + CommandType::Turns => { + let current_turn = self.get_state().current_turn_index; + self.get_state_mut().push_evaluation_stack(Rc::new(Value::new_int( current_turn + 1))); + }, + CommandType::TurnsSince | CommandType::ReadCount => { + let target = self.get_state_mut().pop_evaluation_stack(); + if Value::get_divert_target_value(target.as_ref()).is_none() { + let extra_note = ""; + if Value::get_int_value(target.as_ref()).is_some() { + // 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 + // + extra_note); + panic!(); + } + + let target = Value::get_divert_target_value(target.as_ref()).unwrap(); + + let otmp = self.content_at_path(target).correct_obj(); + let container = match &otmp { + Some(o) => o.clone().into_any().downcast::().ok(), + None => None, + }; + + let mut either_count = 0; + + match container { + Some(container) => { + if eval_command.command_type == CommandType::TurnsSince { + either_count = self.get_state().turns_since_for_container(container.as_ref()); + } else {either_count = self.get_state_mut().visit_count_for_container(&container) as i32;} + }, + None => { + if eval_command.command_type == CommandType::TurnsSince { + either_count = -1; // turn count, default to never/unknown + } else { either_count = 0; } // visit count, assume 0 to default to allowing entry + + // warning("Failed to find container for " + evalCommand.toString() + " lookup at " + // + divertTarget.getTargetPath().toString()); + panic!() + } + } + + self.get_state_mut().push_evaluation_stack(Rc::new(Value::new_int(either_count))); + }, CommandType::Random => { let mut max_int = None; let o = self.get_state_mut().pop_evaluation_stack(); diff --git a/src/story_state.rs b/src/story_state.rs index 0ab3741..1a64044 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -26,9 +26,9 @@ pub struct StoryState { patch: Option, named_flows: Option>, pub diverted_pointer: Pointer, - visit_counts: HashMap, - turn_indices: HashMap, - current_turn_index: i32, + pub visit_counts: HashMap, + pub turn_indices: HashMap, + pub current_turn_index: i32, pub story_seed: i32, pub previous_random: i32, current_tags: Vec, @@ -562,10 +562,8 @@ impl StoryState { } } } - } else if text.is_newline { - if self.output_stream_ends_in_newline() || !self.output_stream_contains_content() { - include_in_output = false; - } + } else if text.is_newline && (self.output_stream_ends_in_newline() || !self.output_stream_contains_content()) { + include_in_output = false; } } @@ -967,4 +965,28 @@ impl StoryState { Ok(None) } + pub(crate) fn turns_since_for_container(&self, container: &Container) -> i32 { + if !container.turn_index_should_be_counted { + // story.error("TURNS_SINCE() for target (" + container.getName() + " - on " + container.getDebugMetadata() + // + ") unknown."); + panic!() + } + + let mut index = 0; + + if self.patch.is_some() && self.patch.as_ref().unwrap().get_turn_index(container).is_some() { + index = *self.patch.as_ref().unwrap().get_turn_index(container).unwrap() as i32; + return self.current_turn_index - index; + } + + let container_path_str = Object::get_path(container).to_string(); + + if self.turn_indices.contains_key(&container_path_str) { + index = *self.turn_indices.get(&container_path_str).unwrap() as i32; + return self.current_turn_index - index; + } else { + return -1; + } + } + } \ No newline at end of file diff --git a/src/variables_state.rs b/src/variables_state.rs index eecb754..873b927 100644 --- a/src/variables_state.rs +++ b/src/variables_state.rs @@ -154,6 +154,45 @@ impl VariablesState { Rc::new(Value::new_variable_pointer(&var_pointer.variable_name, context_index)) } + pub fn set_str(&mut self, variable_name: &str, value: &str) { + + if !self.default_global_variables.as_ref().unwrap().contains_key(variable_name) { + // throw new StoryException( + // "Cannot assign to a variable (" + variableName + ") that hasn't been declared in the story"); + panic!() + } + + let val = Value::new_string(value); + + self.set_global(variable_name, Rc::new(val)); + } + + pub fn set_int(&mut self, variable_name: &str, value: i32) { + + if !self.default_global_variables.as_ref().unwrap().contains_key(variable_name) { + // throw new StoryException( + // "Cannot assign to a variable (" + variableName + ") that hasn't been declared in the story"); + panic!() + } + + let val = Value::new_int(value); + + self.set_global(variable_name, Rc::new(val)); + } + + pub fn set_float(&mut self, variable_name: &str, value: f32) { + + if !self.default_global_variables.as_ref().unwrap().contains_key(variable_name) { + // throw new StoryException( + // "Cannot assign to a variable (" + variableName + ") that hasn't been declared in the story"); + panic!() + } + + let val = Value::new_float(value); + + self.set_global(variable_name, Rc::new(val)); + } + // 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. diff --git a/tests/misc_test.rs b/tests/misc_test.rs new file mode 100644 index 0000000..c41b590 --- /dev/null +++ b/tests/misc_test.rs @@ -0,0 +1,39 @@ +use bladeink::story::Story; + +mod common; + +#[test] +fn operations_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/misc/operations.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + + assert_eq!("neg:-3\nmod:1\npow:27\nfloor:3\nceiling:4\nint:3\nfloat:1\n", &story.continue_maximally()?); + + Ok(()) +} + +/** + * Issue: https://github.com/bladecoder/blade-ink/issues/15 + */ +#[test] +fn issue15_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/misc/issue15.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + + assert_eq!("This is a test\n", story.cont()?); + + while story.can_continue() { + // println!(story.buildStringOfHierarchy()); + let line = &story.cont()?; + + if line.starts_with("SET_X:") { + story.get_variables_state_mut().set_int("x", 100); + } else { + assert_eq!("X is set\n", line); + } + } + + Ok(()) +} \ No newline at end of file From 206c22e8163df4ba9a8186663c78ae4605637d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Fri, 29 Sep 2023 17:31:14 +0000 Subject: [PATCH 41/91] More tests and missing divert operations --- src/native_function_call.rs | 8 ++++++++ tests/misc_test.rs | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/native_function_call.rs b/src/native_function_call.rs index 0eda5dc..2d85293 100644 --- a/src/native_function_call.rs +++ b/src/native_function_call.rs @@ -572,6 +572,10 @@ impl NativeFunctionCall { ValueType::List(op2) => Rc::new(Value::new_bool(op1.eq(op2))), _ => panic!() }, + ValueType::DivertTarget(op1) => match ¶ms[1].value { + ValueType::DivertTarget(op2) => Rc::new(Value::new_bool(op1.eq(op2))), + _ => panic!() + }, _ => panic!() } } @@ -598,6 +602,10 @@ impl NativeFunctionCall { ValueType::List(op2) => Rc::new(Value::new_bool(!op1.eq(op2))), _ => panic!() }, + ValueType::DivertTarget(op1) => match ¶ms[1].value { + ValueType::DivertTarget(op2) => Rc::new(Value::new_bool(!op1.eq(op2))), + _ => panic!() + }, _ => panic!() } } diff --git a/tests/misc_test.rs b/tests/misc_test.rs index c41b590..b919ad2 100644 --- a/tests/misc_test.rs +++ b/tests/misc_test.rs @@ -13,6 +13,30 @@ fn operations_test() -> Result<(), String> { Ok(()) } +#[test] +fn read_counts_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/misc/read-counts.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + + assert_eq!("Count start: 0 0 0\n1\n2\n3\nCount end: 3 3 3\n", &story.continue_maximally()?); + + Ok(()) +} + +#[test] +fn turns_since_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/misc/turns-since.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + + assert_eq!("0\n0\n", &story.continue_maximally()?); + story.choose_choice_index(0); + assert_eq!("1\n", &story.continue_maximally()?); + + Ok(()) +} + /** * Issue: https://github.com/bladecoder/blade-ink/issues/15 */ From 5f0b1817ad82c3462e0b8cc79f4d7098fa89138f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Fri, 29 Sep 2023 19:13:28 +0000 Subject: [PATCH 42/91] Read counts/turns tests + named flow basic test --- README.md | 1 - examples/inkfiles/misc/read-counts.ink | 15 ++++++ examples/inkfiles/misc/read-counts.ink.json | 1 + examples/inkfiles/misc/turns-since.ink | 12 +++++ examples/inkfiles/misc/turns-since.ink.json | 1 + src/story.rs | 11 +++- src/story_state.rs | 40 ++++++++++++-- tests/multi_flow_test.rs | 59 +++++++++++++++++++++ 8 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 examples/inkfiles/misc/read-counts.ink create mode 100644 examples/inkfiles/misc/read-counts.ink.json create mode 100644 examples/inkfiles/misc/turns-since.ink create mode 100644 examples/inkfiles/misc/turns-since.ink.json create mode 100644 tests/multi_flow_test.rs diff --git a/README.md b/README.md index 91f559c..bb7264c 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,6 @@ Currently under development. This is the implementation status: ## TODO -- [ ] Test for visit counts - [ ] Test for flow - [ ] Use OnceCell to lazy init the cache fields of RTObjects - [ ] Error handling diff --git a/examples/inkfiles/misc/read-counts.ink b/examples/inkfiles/misc/read-counts.ink new file mode 100644 index 0000000..fa2f804 --- /dev/null +++ b/examples/inkfiles/misc/read-counts.ink @@ -0,0 +1,15 @@ +VAR x = ->knot + +Count start: {READ_COUNT (x)} {READ_COUNT (-> knot)} {knot} + +-> x (1) -> +-> x (2) -> +-> x (3) -> + +Count end: {READ_COUNT (x)} {READ_COUNT (-> knot)} {knot} +-> END + + +== knot (a) == +{a} +->-> diff --git a/examples/inkfiles/misc/read-counts.ink.json b/examples/inkfiles/misc/read-counts.ink.json new file mode 100644 index 0000000..ae262e1 --- /dev/null +++ b/examples/inkfiles/misc/read-counts.ink.json @@ -0,0 +1 @@ +{"inkVersion":21,"root":[["^Count start: ","ev",{"VAR?":"x"},"readc","out","/ev","^ ","ev",{"^->":"knot"},"readc","out","/ev","^ ","ev",{"CNT?":"knot"},"out","/ev","\n","ev",1,"/ev",{"->t->":"x","var":true},"ev",2,"/ev",{"->t->":"x","var":true},"ev",3,"/ev",{"->t->":"x","var":true},"^Count end: ","ev",{"VAR?":"x"},"readc","out","/ev","^ ","ev",{"^->":"knot"},"readc","out","/ev","^ ","ev",{"CNT?":"knot"},"out","/ev","\n","end",["done",{"#f":5,"#n":"g-0"}],null],"done",{"knot":[{"temp=":"a"},"ev",{"VAR?":"a"},"out","/ev","\n","ev","void","/ev","->->",{"#f":3}],"global decl":["ev",{"^->":"knot"},{"VAR=":"x"},"/ev","end",null],"#f":1}],"listDefs":{}} \ No newline at end of file diff --git a/examples/inkfiles/misc/turns-since.ink b/examples/inkfiles/misc/turns-since.ink new file mode 100644 index 0000000..67a023d --- /dev/null +++ b/examples/inkfiles/misc/turns-since.ink @@ -0,0 +1,12 @@ +-> start + +=== start === + {beats(-> start)} + {beats(-> start)} + * [Choice] -> next += next + {beats(-> start)} + -> END + +=== function beats(x) === + ~ return TURNS_SINCE(x) diff --git a/examples/inkfiles/misc/turns-since.ink.json b/examples/inkfiles/misc/turns-since.ink.json new file mode 100644 index 0000000..bf49bd1 --- /dev/null +++ b/examples/inkfiles/misc/turns-since.ink.json @@ -0,0 +1 @@ +{"inkVersion":21,"root":[[{"->":"start"},["done",{"#f":5,"#n":"g-0"}],null],"done",{"start":[["ev",{"^->":"start"},{"f()":"beats"},"out","/ev","\n","ev",{"^->":"start"},{"f()":"beats"},"out","/ev","\n","ev","str","^Choice","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["^ ",{"->":"start.next"},"\n",{"#f":5}]}],{"next":["ev",{"^->":"start"},{"f()":"beats"},"out","/ev","\n","end",{"#f":1}],"#f":3}],"beats":[{"temp=":"x"},"ev",{"VAR?":"x"},"turns","/ev","~ret","\n",{"#f":1}],"#f":1}],"listDefs":{}} \ No newline at end of file diff --git a/src/story.rs b/src/story.rs index 77e26ce..bbe3c0b 100644 --- a/src/story.rs +++ b/src/story.rs @@ -1895,6 +1895,15 @@ impl Story { self.get_state_mut().force_end(); } - + + pub fn switch_flow(&mut self, flow_name: &str) { + // TODO + // ifAsyncWeCant("switch flow"); + + // if (asyncSaving) + // throw new Exception("Story is already in background saving mode, can't switch flow to " + flowName); + + self.get_state_mut().switch_flow_internal(flow_name); + } } diff --git a/src/story_state.rs b/src/story_state.rs index 1a64044..fa7d275 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -43,8 +43,8 @@ impl StoryState { let mut rng = rand::thread_rng(); let story_seed = rng.gen_range(0..100); - let mut state = StoryState { - current_flow, + let state = StoryState { + current_flow, did_safe_exit: false, output_stream_text_dirty: true, output_stream_tags_dirty: true, @@ -983,10 +983,42 @@ impl StoryState { if self.turn_indices.contains_key(&container_path_str) { index = *self.turn_indices.get(&container_path_str).unwrap() as i32; - return self.current_turn_index - index; + self.current_turn_index - index } else { - return -1; + -1 } } + pub(crate) fn switch_flow_internal(&mut self, flow_name: &str) { + + if flow_name.eq(&self.current_flow.name) { + return; + } + + if self.named_flows.is_none() { + self.named_flows = Some(HashMap::new()); + } + + let named_flows = self.named_flows.as_mut().unwrap(); + + // store the current flow and retrieve and remove the next flow + let flow = named_flows.remove(flow_name); + + let mut next_flow = match flow { + Some(f) => f, + None => { + self.alive_flow_names_dirty = true; + Flow::new(flow_name, self.main_content_container.clone()) + } + }; + + std::mem::swap(&mut self.current_flow, &mut next_flow); + named_flows.insert(next_flow.name.clone(), next_flow); + + self.variables_state.set_callstack(self.current_flow.callstack.clone()); + + // Cause text to be regenerated from output stream if necessary + self.output_stream_dirty(); + } + } \ No newline at end of file diff --git a/tests/multi_flow_test.rs b/tests/multi_flow_test.rs new file mode 100644 index 0000000..42b4400 --- /dev/null +++ b/tests/multi_flow_test.rs @@ -0,0 +1,59 @@ +use bladeink::story::Story; + +mod common; + +#[test] +fn basics_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/runtime/multiflow-basics.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + + story.switch_flow("First"); + story.choose_path_string("knot1", true, None)?; + assert_eq!("knot 1 line 1\n", story.cont()?); + + story.switch_flow("Second"); + story.choose_path_string("knot2", true, None)?; + assert_eq!("knot 2 line 1\n", story.cont()?); + + story.switch_flow("First"); + assert_eq!("knot 1 line 2\n", story.cont()?); + + story.switch_flow("Second"); + assert_eq!("knot 2 line 2\n", story.cont()?); + + Ok(()) +} + +#[test] +fn multiflow_save_load_threads() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/runtime/multiflow-saveloadthreads.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + + // Default flow + assert_eq!("Default line 1\n", story.cont()?); + + story.switch_flow("Blue Flow"); + story.choose_path_string("blue", true, None)?; + assert_eq!("Hello I'm blue\n", story.cont()?); + + story.switch_flow("Red Flow"); + story.choose_path_string("red", true, None)?; + assert_eq!("Hello I'm red\n", story.cont()?); + + // Test existing state remains after switch (blue) + story.switch_flow("Blue Flow"); + assert_eq!("Hello I'm blue\n", story.get_current_text()); + assert_eq!("Thread 1 blue choice", story.get_current_choices()[0].text); + + // Test existing state remains after switch (red) + story.switch_flow("Red Flow"); + assert_eq!("Hello I'm red\n", story.get_current_text()); + assert_eq!("Thread 1 red choice", story.get_current_choices()[0].text); + + // Save/load test + // let saved = story.getState().toJson(); + + Ok(()) +} From 25d4cd1be3b4ba5536d5be5e25769bfaef31b4ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Fri, 29 Sep 2023 20:22:07 +0000 Subject: [PATCH 43/91] if_async_we_cant impl --- README.md | 4 ++-- src/story.rs | 46 ++++++++++++++++++++++++++-------------------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index bb7264c..1dec1c7 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,9 @@ Currently under development. This is the implementation status: ## TODO -- [ ] Test for flow -- [ ] Use OnceCell to lazy init the cache fields of RTObjects - [ ] Error handling +- [ ] Cache components string in Path +- [ ] Use OnceCell to lazy init the cache fields of RTObjects - [ ] Split large files. ex. Get the error handling out of the Story class. The performLogic diff --git a/src/story.rs b/src/story.rs index bbe3c0b..7461a27 100644 --- a/src/story.rs +++ b/src/story.rs @@ -118,11 +118,11 @@ impl Story { } fn reset_state(&mut self) { - //TODO ifAsyncWeCant("ResetState"); + self.if_async_we_cant("ResetState"); self.state = Some(StoryState::new(self.main_content_container.clone(), self.list_definitions.clone())); - // TODO state.getVariablesState().setVariableChangedEvent(this); + // TODO self.get_state_mut().get_variables_state().setVariableChangedEvent(this); self.reset_globals(); } @@ -169,7 +169,7 @@ impl Story { } pub fn continue_maximally(&mut self) -> Result { - // TODO self.ifAsyncWeCant("ContinueMaximally"); + self.if_async_we_cant("continue_maximally"); let mut sb = String::new(); @@ -200,7 +200,7 @@ impl Story { self.async_continue_active = is_async_time_limited; if !self.can_continue() { return Err( - "Can't continue - should check canContinue before calling Continue".to_owned(), + "Can't continue - should check can_continue before calling Continue".to_owned(), ); } @@ -462,16 +462,15 @@ impl Story { } } - // outputStreamEndsInNewline = false - return Ok(false); + Ok(false) } pub fn get_current_text(&mut self) -> String { - //TODO ifAsyncWeCant("call currentText since it's a work in progress"); + self.if_async_we_cant("call currentText since it's a work in progress"); self.get_state_mut().get_current_text() } - pub fn get_main_content_container(&self) -> Rc { + pub(crate) fn get_main_content_container(&self) -> Rc { match self.temporaty_evaluation_container.as_ref() { Some(c) => c.clone(), None => self.main_content_container.clone(), @@ -726,8 +725,9 @@ impl Story { // saving, we simply stay in a "patching" state, // albeit with the newer cloned patch. - // TODO - //if (!asyncSaving) state.applyAnyPatch(); + if !self.async_saving { + self.get_state_mut().apply_any_patch(); + } // No longer need the snapshot. self.state_snapshot_at_last_new_line = None; @@ -1707,7 +1707,7 @@ impl Story { // TODO: The result and the args should be an object not a String pub fn evaluate_function(&mut self, func_name: &str, args: Option<&Vec>, text_output: &mut String) -> Result, String> { - // TODO ifAsyncWeCant("evaluate a function"); + self.if_async_we_cant("evaluate a function"); if func_name.trim().is_empty() { return Err("Function is empty or white space.".to_owned()); @@ -1718,7 +1718,7 @@ impl Story { if func_container.is_none() { let mut e = "Function doesn't exist: '".to_owned(); e.push_str(func_name); - e.push_str("'"); + e.push('\''); return Err(e); } @@ -1857,12 +1857,12 @@ impl Story { } pub fn get_current_tags(&mut self) -> Vec { - // TODO ifAsyncWeCant("call currentTags since it's a work in progress"); + self.if_async_we_cant("call currentTags since it's a work in progress"); return self.get_state_mut().get_current_tags(); } pub fn choose_path_string(&mut self, path: &str, reset_call_stack: bool, args: Option<&Vec>) -> Result<(), String> { - // TODO ifAsyncWeCant("call ChoosePathString right now"); + self.if_async_we_cant("call ChoosePathString right now"); if reset_call_stack { self.reset_callstack(); @@ -1884,26 +1884,32 @@ impl Story { } } - self.get_state_mut().pass_arguments_to_evaluation_stack(args); + self.get_state_mut().pass_arguments_to_evaluation_stack(args)?; self.choose_path(&Path::new_with_components_string(Some(path)), true); Ok(()) } fn reset_callstack(&mut self) { - // TODO ifAsyncWeCant("ResetCallstack"); + self.if_async_we_cant("ResetCallstack"); self.get_state_mut().force_end(); } pub fn switch_flow(&mut self, flow_name: &str) { - // TODO - // ifAsyncWeCant("switch flow"); + self.if_async_we_cant("switch flow"); - // if (asyncSaving) - // throw new Exception("Story is already in background saving mode, can't switch flow to " + flowName); + if self.async_saving { + panic!("Story is already in background saving mode, can't switch flow to {}", flow_name); + } self.get_state_mut().switch_flow_internal(flow_name); } + + fn if_async_we_cant(&self, activity_str: &str) { + if self.async_continue_active { + panic!("Can't {}. Story is in the middle of a ContinueAsync(). Make more continue_async() calls or a single cont() call beforehand.", activity_str); + } + } } From bb783d87b6642fa469c7d841898c90a2b33f7c13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Fri, 29 Sep 2023 21:49:21 +0000 Subject: [PATCH 44/91] Set/Get variables from RT impl. --- src/callstack.rs | 7 ++-- src/container.rs | 2 +- src/ink_list.rs | 4 ++- src/lib.rs | 1 + src/native_function_call.rs | 2 +- src/state_patch.rs | 8 ++--- src/story.rs | 4 +-- src/story_state.rs | 2 +- src/value.rs | 51 +++++---------------------- src/value_type.rs | 70 +++++++++++++++++++++++++++++++++++++ src/variables_state.rs | 63 +++++++++++++++------------------ tests/misc_test.rs | 4 +-- tests/runtime_test.rs | 32 +++++++++++++++++ 13 files changed, 157 insertions(+), 93 deletions(-) create mode 100644 src/value_type.rs create mode 100644 tests/runtime_test.rs diff --git a/src/callstack.rs b/src/callstack.rs index 70aba4d..d61f66c 100644 --- a/src/callstack.rs +++ b/src/callstack.rs @@ -5,7 +5,7 @@ use crate::{pointer::{Pointer, self}, object::RTObject, push_pop::PushPopType, s pub struct Element { pub current_pointer: Pointer, pub in_expression_evaluation: bool, - pub temporary_variables: HashMap>, + pub temporary_variables: HashMap>, pub push_pop_type: PushPopType, pub evaluation_stack_height_when_pushed: usize, pub function_start_in_output_stream: i32, @@ -206,7 +206,7 @@ impl CallStack { pub fn set_temporary_variable( &mut self, name: String, - value: Rc, + value: Rc, declare_new: bool, mut context_index: i32, ) -> Result<(), String> { @@ -224,7 +224,6 @@ impl CallStack { if let Some(old_value) = &old_value { Value::retain_list_origins_for_assignment(old_value.as_ref(), value.as_ref()); - } context_element.temporary_variables.insert(name, value); @@ -242,7 +241,7 @@ impl CallStack { 0 } - pub fn get_temporary_variable_with_name(&self, name: &str, context_index: i32) -> Option> { + pub fn get_temporary_variable_with_name(&self, name: &str, context_index: i32) -> Option> { let mut context_index = context_index; if context_index == -1 { context_index = self.get_current_element_index() + 1; diff --git a/src/container.rs b/src/container.rs index 3d4cadf..e0cbda7 100644 --- a/src/container.rs +++ b/src/container.rs @@ -7,7 +7,7 @@ use as_any::Downcast; use crate::{ object::{Object, RTObject}, - value::{ValueType, Value}, path::{Path, Component}, search_result::SearchResult, + value::Value, path::{Path, Component}, search_result::SearchResult, value_type::ValueType, }; const COUNTFLAGS_VISITS: i32 = 1; diff --git a/src/ink_list.rs b/src/ink_list.rs index 3f9c257..e6ae732 100644 --- a/src/ink_list.rs +++ b/src/ink_list.rs @@ -1,8 +1,10 @@ use core::fmt; use std::{collections::HashMap, cell::RefCell}; -use crate::{ink_list_item::InkListItem, list_definition::ListDefinition, list_definitions_origin::ListDefinitionsOrigin, value::ValueType}; +use crate::{ink_list_item::InkListItem, list_definition::ListDefinition, list_definitions_origin::ListDefinitionsOrigin, value_type::ValueType}; + +#[derive(Clone)] pub struct InkList { pub items: HashMap, pub origins: RefCell>, diff --git a/src/lib.rs b/src/lib.rs index 36ba018..dbb72fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod story; +pub mod value_type; mod json_serialization; mod object; mod value; diff --git a/src/native_function_call.rs b/src/native_function_call.rs index 2d85293..7a7c5b7 100644 --- a/src/native_function_call.rs +++ b/src/native_function_call.rs @@ -1,6 +1,6 @@ use std::{fmt, rc::Rc}; -use crate::{object::{Object, RTObject}, value::{Value, ValueType}, void::Void, ink_list::InkList}; +use crate::{object::{Object, RTObject}, value::Value, void::Void, ink_list::InkList, value_type::ValueType}; #[derive(Debug, PartialEq)] pub enum Op { diff --git a/src/state_patch.rs b/src/state_patch.rs index 281ae87..a112af0 100644 --- a/src/state_patch.rs +++ b/src/state_patch.rs @@ -2,11 +2,11 @@ use std::{ rc::Rc, collections::{HashMap, HashSet}, }; -use crate::{object::{RTObject, Object}, container::Container}; +use crate::{object::{RTObject, Object}, container::Container, value::Value}; #[derive(Clone)] pub struct StatePatch { - pub globals: HashMap>, + pub globals: HashMap>, pub changed_variables: HashSet, pub visit_counts: HashMap, pub turn_indices: HashMap, @@ -40,11 +40,11 @@ impl StatePatch { self.visit_counts.insert(key, count); } - pub fn get_global(&self, name: &str) -> Option>{ + pub fn get_global(&self, name: &str) -> Option>{ self.globals.get(name).cloned() } - pub fn set_global(&mut self, name: &str, value: Rc) { + pub fn set_global(&mut self, name: &str, value: Rc) { self.globals.insert(name.to_string(), value); } diff --git a/src/story.rs b/src/story.rs index 7461a27..3123748 100644 --- a/src/story.rs +++ b/src/story.rs @@ -1302,7 +1302,7 @@ impl Story { // the temporary context, but attempt to create them globally // var prioritiseHigherInCallStack = _temporaryEvaluationContainer // != null; - + let assigned_val = assigned_val.into_any().downcast::().unwrap(); self.get_state_mut().get_variables_state_mut().assign( var_ass, assigned_val); return true; @@ -1310,7 +1310,7 @@ impl Story { // Variable reference if let Ok(var_ref) = content_obj.clone().into_any().downcast::() { - let mut found_value: Option> = None; + let mut found_value: Option> = None; // Explicit read count value if let Some(p) = &var_ref.path_for_count { diff --git a/src/story_state.rs b/src/story_state.rs index fa7d275..8057c76 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -2,7 +2,7 @@ use std::{rc::Rc, cell::RefCell, collections::HashMap}; -use crate::{pointer::{Pointer, self}, callstack::CallStack, flow::Flow, variables_state::VariablesState, choice::Choice, object::{RTObject, Object}, value::{Value, ValueType}, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}, container::Container, state_patch::StatePatch, story::Story, path::Path, void::Void, tag::Tag, list_definitions_origin::ListDefinitionsOrigin}; +use crate::{pointer::{Pointer, self}, callstack::CallStack, flow::Flow, variables_state::VariablesState, choice::Choice, object::{RTObject, Object}, value::Value, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}, container::Container, state_patch::StatePatch, story::Story, path::Path, void::Void, tag::Tag, list_definitions_origin::ListDefinitionsOrigin, value_type::ValueType}; use rand::Rng; diff --git a/src/value.rs b/src/value.rs index 54da5fd..dd0c3ab 100644 --- a/src/value.rs +++ b/src/value.rs @@ -1,8 +1,6 @@ -use std::{fmt, rc::Rc}; +use std::fmt; -use as_any::Downcast; - -use crate::{object::{RTObject, Object}, path::Path, ink_list::InkList}; +use crate::{object::{RTObject, Object}, path::Path, ink_list::InkList, value_type::{StringValue, ValueType, VariablePointerValue}}; const CAST_BOOL: u8 = 0; const CAST_INT: u8 = 1; @@ -10,43 +8,6 @@ const CAST_FLOAT: u8 = 2; const CAST_LIST: u8 = 3; const CAST_STRING: u8 = 4; -#[repr(u8)] -pub enum ValueType { - Bool(bool), - Int(i32), - Float(f32), - List(InkList), - String(StringValue), - DivertTarget(Path), - VariablePointer(VariablePointerValue), -} - - -#[derive(Clone)] -pub struct StringValue { - pub string: String, - pub is_inline_whitespace: bool, - pub is_newline: bool -} - -impl StringValue { - pub fn is_non_whitespace(&self) -> bool { - return !self.is_newline && !self.is_inline_whitespace; - } - -} - -#[derive(Clone)] -pub struct VariablePointerValue { - pub variable_name: String, - - // 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) - pub context_index: i32, -} - pub struct Value { obj: Object, pub value: ValueType, @@ -117,15 +78,19 @@ impl Value { Self { obj: Object::new(), value: ValueType::List(l) } } + pub fn from_value_type(value_type: ValueType) -> Self { + Self { obj: Object::new(), value: value_type } + } + pub fn is_truthy(&self) -> bool { match &self.value { ValueType::Bool(v) => *v, ValueType::Int(v) => *v != 0, ValueType::Float(v) => *v != 0.0, - ValueType::String(v) => v.string.len() > 0, + ValueType::String(v) => !v.string.is_empty(), ValueType::DivertTarget(_) => panic!(), // exception Shouldn't be checking the truthiness of a divert target?? ValueType::VariablePointer(_) => panic!(), - ValueType::List(l) => l.items.len() > 0, + ValueType::List(l) => !l.items.is_empty(), } } diff --git a/src/value_type.rs b/src/value_type.rs new file mode 100644 index 0000000..9b892d8 --- /dev/null +++ b/src/value_type.rs @@ -0,0 +1,70 @@ +use std::fmt; + +use crate::{object::{RTObject, Object}, path::Path, ink_list::InkList}; + +#[repr(u8)] +#[derive(Clone)] +pub enum ValueType { + Bool(bool), + Int(i32), + Float(f32), + List(InkList), + String(StringValue), + DivertTarget(Path), + VariablePointer(VariablePointerValue), +} + +impl ValueType { + pub fn get_bool(&self) -> Option { + match self { + ValueType::Bool(v) => Some(*v), + _ => None, + } + } + + pub fn get_int(&self) -> Option { + match self { + ValueType::Int(v) => Some(*v), + _ => None, + } + } + + pub fn get_float(&self) -> Option { + match self { + ValueType::Float(v) => Some(*v), + _ => None, + } + } + + pub fn get_str(&self) -> Option<&str> { + match self { + ValueType::String(v) => Some(&v.string), + _ => None, + } + } +} + + +#[derive(Clone)] +pub struct StringValue { + pub string: String, + pub is_inline_whitespace: bool, + pub is_newline: bool +} + +impl StringValue { + pub fn is_non_whitespace(&self) -> bool { + !self.is_newline && !self.is_inline_whitespace + } +} + +#[derive(Clone)] +pub struct VariablePointerValue { + pub variable_name: String, + + // 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) + pub context_index: i32, +} diff --git a/src/variables_state.rs b/src/variables_state.rs index 873b927..baf135c 100644 --- a/src/variables_state.rs +++ b/src/variables_state.rs @@ -1,16 +1,16 @@ use std::{collections::{HashMap, HashSet}, rc::Rc, cell::RefCell}; -use crate::{object::RTObject, callstack::CallStack, state_patch::StatePatch, variable_assigment::VariableAssignment, value::{Value, VariablePointerValue}, list_definitions_origin::ListDefinitionsOrigin}; +use crate::{callstack::CallStack, state_patch::StatePatch, variable_assigment::VariableAssignment, value::Value, list_definitions_origin::ListDefinitionsOrigin, value_type::{VariablePointerValue, ValueType}}; #[derive(Clone)] pub struct VariablesState { - pub global_variables: HashMap>, - pub default_global_variables: Option>>, + pub global_variables: HashMap>, + pub default_global_variables: Option>>, pub batch_observing_variable_changes: bool, pub callstack: Rc>, pub changed_variables_for_batch_obs: Option>, - pub variable_changed_event: Option, + pub variable_changed_event: Option, list_defs_origin: Rc, pub patch: Option, } @@ -70,7 +70,7 @@ impl VariablesState { pub fn assign ( &mut self, var_ass: &VariableAssignment, - value: Rc, + value: Rc, ) { let mut name = var_ass.variable_name.to_string(); let mut context_index = -1; @@ -87,9 +87,8 @@ impl VariablesState { // Constructing new variable pointer reference if var_ass.is_new_declaration { if let Some(var_pointer) = Value::get_variable_pointer_value(value.as_ref()){ - let fully_resolved_variable_pointer = + value = self.resolve_variable_pointer(var_pointer); - value = fully_resolved_variable_pointer; } } else { // Assign to an existing variable pointer @@ -133,7 +132,7 @@ impl VariablesState { // pointer that more specifically points to the exact instance: whether it's // global, // or the exact position of a temporary on the callstack. - fn resolve_variable_pointer(&self, var_pointer: &VariablePointerValue) -> Rc { + fn resolve_variable_pointer(&self, var_pointer: &VariablePointerValue) -> Rc { let mut context_index = var_pointer.context_index; if context_index == -1 { context_index = self.get_context_index_of_variable_named(&var_pointer.variable_name); @@ -154,7 +153,7 @@ impl VariablesState { Rc::new(Value::new_variable_pointer(&var_pointer.variable_name, context_index)) } - pub fn set_str(&mut self, variable_name: &str, value: &str) { + pub fn set(&mut self, variable_name: &str, value_type: ValueType) { if !self.default_global_variables.as_ref().unwrap().contains_key(variable_name) { // throw new StoryException( @@ -162,38 +161,34 @@ impl VariablesState { panic!() } - let val = Value::new_string(value); + let val = Value::from_value_type(value_type); self.set_global(variable_name, Rc::new(val)); } - pub fn set_int(&mut self, variable_name: &str, value: i32) { + pub fn get(&self, variable_name: &str) -> Option { - if !self.default_global_variables.as_ref().unwrap().contains_key(variable_name) { - // throw new StoryException( - // "Cannot assign to a variable (" + variableName + ") that hasn't been declared in the story"); - panic!() + if self.patch.is_some() { + if let Some(var) = self.patch.as_ref().unwrap().get_global(variable_name) { + return Some(var.value.clone()); + } } - let val = Value::new_int(value); - - self.set_global(variable_name, Rc::new(val)); - } - - pub fn set_float(&mut self, variable_name: &str, value: f32) { - - if !self.default_global_variables.as_ref().unwrap().contains_key(variable_name) { - // throw new StoryException( - // "Cannot assign to a variable (" + variableName + ") that hasn't been declared in the story"); - panic!() + // 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 let Some(var_contents) = self.global_variables.get(variable_name) { + return Some(var_contents.value.clone()); + } else if let Some(var_contents) = self.default_global_variables.as_ref().unwrap().get(variable_name) { + return Some(var_contents.value.clone()); } - let val = Value::new_float(value); + None - self.set_global(variable_name, Rc::new(val)); } - // Make copy of the variable pointer so we're not using the value direct + // 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 @@ -206,7 +201,7 @@ impl VariablesState { return self.callstack.borrow().get_current_element_index(); } - fn get_raw_variable_with_name(&self, name: &str, context_index: i32) -> Option> { + fn get_raw_variable_with_name(&self, name: &str, context_index: i32) -> Option> { // 0 context = global if context_index == 0 || context_index == -1 { if let Some(patch) = &self.patch { @@ -242,8 +237,8 @@ impl VariablesState { var_value } - fn set_global(&mut self, name: &str, value: Rc) { - let mut old_value: Option> = None; + fn set_global(&mut self, name: &str, value: Rc) { + let mut old_value: Option> = None; if let Some(patch) = &self.patch { old_value = patch.get_global(name); @@ -278,7 +273,7 @@ impl VariablesState { } } - pub fn get_variable_with_name(&self, name: &str, context_index: i32) -> Option> { + pub fn get_variable_with_name(&self, name: &str, context_index: i32) -> Option> { let var_value = self.get_raw_variable_with_name(name, context_index); // Get value from pointer? if let Some(vv) = var_value.clone() { @@ -290,7 +285,7 @@ impl VariablesState { var_value } - fn value_at_variable_pointer(&self, pointer: &VariablePointerValue) -> Option> { + fn value_at_variable_pointer(&self, pointer: &VariablePointerValue) -> Option> { self.get_variable_with_name(&pointer.variable_name, pointer.context_index) } diff --git a/tests/misc_test.rs b/tests/misc_test.rs index b919ad2..a5acdb2 100644 --- a/tests/misc_test.rs +++ b/tests/misc_test.rs @@ -1,4 +1,4 @@ -use bladeink::story::Story; +use bladeink::{story::Story, value_type::ValueType}; mod common; @@ -53,7 +53,7 @@ fn issue15_test() -> Result<(), String> { let line = &story.cont()?; if line.starts_with("SET_X:") { - story.get_variables_state_mut().set_int("x", 100); + story.get_variables_state_mut().set("x", ValueType::Int(100)); } else { assert_eq!("X is set\n", line); } diff --git a/tests/runtime_test.rs b/tests/runtime_test.rs new file mode 100644 index 0000000..593fe63 --- /dev/null +++ b/tests/runtime_test.rs @@ -0,0 +1,32 @@ +use bladeink::{story::Story, value_type::ValueType}; + +mod common; + +#[test] +fn set_and_get_variable_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/runtime/set-get-variables.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + assert_eq!(10, story.get_variables_state().get("x").unwrap().get_int().unwrap()); + + story.get_variables_state_mut().set("x", ValueType::Int(15)); + + assert_eq!(15, story.get_variables_state().get("x").unwrap().get_int().unwrap()); + + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("OK", text[0]); + + Ok(()) +} + + +// TODO external functions + variable observers + From e7aa1d9b3178115ef55884cfc71d9ceaaf9b7b6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Fri, 29 Sep 2023 22:00:24 +0000 Subject: [PATCH 45/91] Cast for DivertTarget and VariablePointer --- src/value.rs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/value.rs b/src/value.rs index dd0c3ab..ba7aecb 100644 --- a/src/value.rs +++ b/src/value.rs @@ -7,6 +7,8 @@ const CAST_INT: u8 = 1; const CAST_FLOAT: u8 = 2; const CAST_LIST: u8 = 3; const CAST_STRING: u8 = 4; +const CAST_DIVERT_TARGET: u8 = 5; +const CAST_VARIABLE_POINTER: u8 = 6; pub struct Value { obj: Object, @@ -259,8 +261,22 @@ impl Value { _ => panic!(), } }, - ValueType::DivertTarget(_) => panic!(), - ValueType::VariablePointer(_) => panic!(), + ValueType::DivertTarget(_) => { + match cast_dest_type { + CAST_DIVERT_TARGET => { + None + }, + _ => panic!(), + } + }, + ValueType::VariablePointer(_) => { + match cast_dest_type { + CAST_VARIABLE_POINTER => { + None + }, + _ => panic!(), + } + }, } } } \ No newline at end of file From 7a0213a768c1090781205766957f1b62d0140227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Sat, 30 Sep 2023 16:00:46 +0000 Subject: [PATCH 46/91] Working save state. Missing load state. --- README.md | 6 +- src/callstack.rs | 65 ++++- src/control_command.rs | 35 ++- src/divert.rs | 2 +- src/flow.rs | 45 +++- src/{json_serialization.rs => json_read.rs} | 0 src/json_write.rs | 268 ++++++++++++++++++++ src/lib.rs | 3 +- src/native_function_call.rs | 48 +++- src/story.rs | 10 +- src/story_state.rs | 69 ++++- src/value.rs | 46 +++- src/value_type.rs | 18 +- src/variable_reference.rs | 2 +- src/variables_state.rs | 63 ++++- tests/runtime_test.rs | 140 +++++++++- 16 files changed, 776 insertions(+), 44 deletions(-) rename src/{json_serialization.rs => json_read.rs} (100%) create mode 100644 src/json_write.rs diff --git a/README.md b/README.md index 1dec1c7..0fd0d5e 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ Currently under development. This is the implementation status: - [x] Diverts - [x] Variable Text - [x] Conditional Text -- [ ] Game Queries and Functions +- [x] Game Queries and Functions - [x] Nested flows -- [ ] Variables and Logic +- [x] Variables and Logic - [x] Conditional blocks (if/else) - [x] Temporary Variables - [x] Functions @@ -24,10 +24,12 @@ Currently under development. This is the implementation status: ## TODO +- [ ] Optimize control command getname. Use static string array and address it by order. - [ ] Error handling - [ ] Cache components string in Path - [ ] Use OnceCell to lazy init the cache fields of RTObjects - [ ] Split large files. ex. Get the error handling out of the Story class. The performLogic +- [ ] Story.state y VariablesState.default_global_variables shouldn't be optionals. diff --git a/src/callstack.rs b/src/callstack.rs index d61f66c..9b88e99 100644 --- a/src/callstack.rs +++ b/src/callstack.rs @@ -1,6 +1,8 @@ use std::{collections::HashMap, rc::Rc}; -use crate::{pointer::{Pointer, self}, object::RTObject, push_pop::PushPopType, story::Story, container::Container, value::Value}; +use serde_json::{Map, json}; + +use crate::{pointer::{Pointer, self}, push_pop::PushPopType, container::Container, value::Value, object::Object, json_read, json_write}; pub struct Element { pub current_pointer: Pointer, @@ -36,7 +38,7 @@ impl Element { pub struct Thread { pub callstack: Vec, pub previous_pointer: Pointer, - thread_index: usize + pub thread_index: usize } impl Thread { @@ -60,6 +62,38 @@ impl Thread { copy } + + pub(crate) fn write_json(&self) -> serde_json::Value { + let mut thread: Map = Map::new(); + + let mut cs_array: Vec = Vec::new(); + + for el in self.callstack.iter() { + let mut el_map: Map = Map::new(); + + if !el.current_pointer.is_null() { + el_map.insert("cPath".to_owned(), json!(Object::get_path(el.current_pointer.container.as_ref().unwrap().as_ref()).get_components_string())); + el_map.insert("idx".to_owned(), json!(el.current_pointer.index)); + } + el_map.insert("exp".to_owned(), json!(el.in_expression_evaluation)); + el_map.insert("type".to_owned(), json!(el.push_pop_type as u32)); + + if el.temporary_variables.len() > 0 { + el_map.insert("exp".to_owned(), json_write::write_dictionary_values(&el.temporary_variables)); + } + + cs_array.push(serde_json::Value::Object(el_map)); + } + + thread.insert("callstack".to_owned(), serde_json::Value::Array(cs_array)); + thread.insert("threadIndex".to_owned(), json!(self.thread_index)); + + if !self.previous_pointer.is_null() { + thread.insert("previousContentObject".to_owned(), json!(Object::get_path(self.previous_pointer.resolve().unwrap().as_ref()).to_string())); + } + + serde_json::Value::Object(thread) + } } pub struct CallStack { @@ -264,4 +298,31 @@ impl CallStack { self.get_callstack_mut().push(element); } + + pub(crate) fn write_json(&self) -> serde_json::Value { + let mut cs: Map = Map::new(); + + let mut treads_array: Vec = Vec::new(); + + for thread in &self.threads { + treads_array.push(thread.write_json()); + } + + cs.insert("threads".to_owned(), serde_json::Value::Array(treads_array)); + cs.insert("threadCounter".to_owned(), json!(self.thread_counter)); + + serde_json::Value::Object(cs) + } + + pub fn get_thread_with_index(&self, index: usize) -> Option<&Thread> { + // return threads.Find (t => t.threadIndex == index); + + for t in self.threads.iter() { + if t.thread_index == index { + return Some(t); + } + } + + return None; + } } \ No newline at end of file diff --git a/src/control_command.rs b/src/control_command.rs index 2ed82b6..6c4e077 100644 --- a/src/control_command.rs +++ b/src/control_command.rs @@ -4,7 +4,7 @@ use strum::Display; use crate::object::{RTObject, Object}; -#[derive(PartialEq, Display)] +#[derive(PartialEq, Display, Clone, Copy)] pub enum CommandType { EvalStart, EvalOutput, @@ -40,6 +40,7 @@ pub struct ControlCommand { } impl ControlCommand { + pub fn new_from_name(name: &str) -> Option { match name { "ev" => Some(Self::new(CommandType::EvalStart)), @@ -70,6 +71,38 @@ impl ControlCommand { "/#" => Some(Self::new(CommandType::EndTag)), _ => None, } + + } + + pub fn get_name(c: CommandType) -> String { + match c { + CommandType::EvalStart => "ev".to_owned(), + CommandType::EvalOutput => "out".to_owned(), + CommandType::EvalEnd => "/ev".to_owned(), + CommandType::Duplicate => "du".to_owned(), + CommandType::PopEvaluatedValue => "pop".to_owned(), + CommandType::PopFunction => "~ret".to_owned(), + CommandType::PopTunnel => "->->".to_owned(), + CommandType::BeginString => "str".to_owned(), + CommandType::EndString => "/str".to_owned(), + CommandType::NoOp => "nop".to_owned(), + CommandType::ChoiceCount => "choiceCnt".to_owned(), + CommandType::Turns => "turn".to_owned(), + CommandType::TurnsSince => "turns".to_owned(), + CommandType::ReadCount => "readc".to_owned(), + CommandType::Random => "rnd".to_owned(), + CommandType::SeedRandom => "srnd".to_owned(), + CommandType::VisitIndex => "visit".to_owned(), + CommandType::SequenceShuffleIndex => "seq".to_owned(), + CommandType::StartThread => "thread".to_owned(), + CommandType::Done => "done".to_owned(), + CommandType::End => "end".to_owned(), + CommandType::ListFromInt => "listInt".to_owned(), + CommandType::ListRange => "range".to_owned(), + CommandType::ListRandom => "lrnd".to_owned(), + CommandType::BeginTag => "#".to_owned(), + CommandType::EndTag => "/#".to_owned(), + } } pub fn new(command_type: CommandType) -> Self { diff --git a/src/divert.rs b/src/divert.rs index 5ba173e..c9f0279 100644 --- a/src/divert.rs +++ b/src/divert.rs @@ -40,7 +40,7 @@ impl Divert { } } - fn get_target_path_string(&self) -> Option { + pub fn get_target_path_string(&self) -> Option { match self.target_path.borrow().as_ref() { Some(p) => Some(self.compact_path_string(p)), None => None, diff --git a/src/flow.rs b/src/flow.rs index 7a6718d..a895ea7 100644 --- a/src/flow.rs +++ b/src/flow.rs @@ -1,6 +1,8 @@ use std::{rc::Rc, cell::RefCell}; -use crate::{callstack::CallStack, choice::Choice, object::RTObject, container::Container}; +use serde_json::Map; + +use crate::{callstack::CallStack, choice::Choice, object::RTObject, container::Container, json_write}; #[derive(Clone)] pub struct Flow { @@ -19,4 +21,45 @@ impl Flow { current_choices: Vec::new() } } + + pub(crate) fn write_json(&self) -> serde_json::Value { + let mut flow: Map = Map::new(); + + flow.insert("callstack".to_owned(), self.callstack.borrow().write_json()); + flow.insert("outputStream".to_owned(), json_write::write_list_rt_objs(&self.output_stream)); + + // choiceThreads: optional + // Has to come BEFORE the choices themselves are written out + // since the originalThreadIndex of each choice needs to be set + let mut has_choice_threads = false; + let mut jct: Map = Map::new(); + for c in self.current_choices.iter() { + // c.original_thread_index = c.get_thread_at_generation().unwrap().thread_index; + let original_thread_index = match c.get_thread_at_generation() { + Some(t) => Some(t.thread_index), + None => None, + }.unwrap(); + + if self.callstack.borrow().get_thread_with_index(original_thread_index).is_none() { + if !has_choice_threads { + has_choice_threads = true; + } + + jct.insert(original_thread_index.to_string(), c.get_thread_at_generation().unwrap().write_json()); + } + } + + if (has_choice_threads) { + flow.insert("choiceThreads".to_owned(), serde_json::Value::Object(jct)); + } + + let mut c_array: Vec = Vec::new(); + for c in self.current_choices.iter() { + c_array.push(json_write::write_choice(c)); + } + + flow.insert("currentChoices".to_owned(), serde_json::Value::Array(c_array)); + + serde_json::Value::Object(flow) + } } \ No newline at end of file diff --git a/src/json_serialization.rs b/src/json_read.rs similarity index 100% rename from src/json_serialization.rs rename to src/json_read.rs diff --git a/src/json_write.rs b/src/json_write.rs new file mode 100644 index 0000000..2c6a97b --- /dev/null +++ b/src/json_write.rs @@ -0,0 +1,268 @@ +use std::{collections::HashMap, rc::Rc}; + +use serde_json::{Map, json}; + +use crate::{ + container::Container, + object::RTObject, value::Value, glue::Glue, choice_point::ChoicePoint, push_pop::PushPopType, divert::Divert, ink_list::InkList, control_command::ControlCommand, native_function_call::NativeFunctionCall, variable_reference::VariableReference, variable_assigment::VariableAssignment, tag::Tag, void::Void, choice::Choice, +}; + +pub fn write_dictionary_runtime_objs(objs: &HashMap>) -> serde_json::Value { + let mut jobjs: Map = Map::new(); + + for (k,o) in objs { + jobjs.insert(k.clone(), write_rtobject(o.clone())); + } + + serde_json::Value::Object(jobjs) +} + +pub fn write_dictionary_values(objs: &HashMap>) -> serde_json::Value { + let mut jobjs: Map = Map::new(); + + for (k,o) in objs { + jobjs.insert(k.clone(), write_rtobject(o.clone())); + } + + serde_json::Value::Object(jobjs) +} + +pub fn write_rtobject(o: Rc) -> serde_json::Value { + if let Some(c) = o.as_any().downcast_ref::() { + return write_rt_container(c, false); + } + + if let Some(divert) = o.as_any().downcast_ref::() { + let mut div_type_key = "->"; + + if divert.is_external { div_type_key = "x()"; } + else if divert.pushes_to_stack { + if divert.stack_push_type == PushPopType::Function {div_type_key = "f()";} + else if divert.stack_push_type == PushPopType::Tunnel {div_type_key = "->t->";} + } + + let target_str = + if divert.has_variable_target() {divert.variable_divert_name.clone().unwrap()} + else {divert.get_target_path_string().unwrap()}; + + let mut jobj: Map = Map::new(); + + jobj.insert(div_type_key.to_string(), json!(target_str)); + + if divert.has_variable_target() {jobj.insert("var".to_owned(), json!(true));} + + if divert.is_conditional {jobj.insert("c".to_owned(), json!(true));} + + if divert.external_args > 0 {jobj.insert("exArgs".to_owned(), json!(divert.external_args));} + + return serde_json::Value::Object(jobj); + } + + if let Ok(cp) = o.clone().into_any().downcast::() { + let mut jobj: Map = Map::new(); + jobj.insert("*".to_owned(), json!(ChoicePoint::get_path_string_on_choice(&cp))); + jobj.insert("flg".to_owned(), json!(cp.get_flags())); + return serde_json::Value::Object(jobj); + } + + if let Some(v) = Value::get_bool_value(o.as_ref()) { + return json!(v); + } + + if let Some(v) = Value::get_int_value(o.as_ref()) { + return json!(v); + } + + if let Some(v) = Value::get_float_value(o.as_ref()) { + return json!(v); + } + + if let Some(v) = Value::get_string_value(o.as_ref()) { + let mut s = String::new(); + + if v.is_newline { + s.push_str("\\n"); + } else { + s.push('^'); + s.push_str(&v.string); + } + + return json!(s); + } + + if let Some(v) = Value::get_list_value(o.as_ref()) { + write_ink_list(v); + } + + if let Some(v) = Value::get_divert_target_value(o.as_ref()) { + let mut jobj: Map = Map::new(); + jobj.insert("^->".to_owned(), json!(v.get_components_string())); + return serde_json::Value::Object(jobj); + } + + if let Some(v) = Value::get_variable_pointer_value(o.as_ref()) { + let mut jobj: Map = Map::new(); + jobj.insert("^var".to_owned(), json!(v.variable_name)); + jobj.insert("ci".to_owned(), json!(v.context_index)); + return serde_json::Value::Object(jobj); + } + + if o.as_any().is::() { + return json!("<>") + } + + if let Some(cc) = o.as_any().downcast_ref::() { + return json!(ControlCommand::get_name(cc.command_type)); + } + + if let Some(f) = o.as_any().downcast_ref::() { + let mut name = NativeFunctionCall::get_name(f.op); + + // Avoid collision with ^ used to indicate a string + if "^".eq(&name) {name = "L^".to_owned();} + + return json!(name); + } + + if let Ok(var_ref) = o.clone().into_any().downcast::() { + + let mut jobj: Map = Map::new(); + + let read_count_path = var_ref.get_path_string_for_count(); + if read_count_path.is_some() { + jobj.insert("CNT?".to_owned(), json!(read_count_path)); + } else { + jobj.insert("VAR?".to_owned(), json!(var_ref.name.clone())); + } + + return serde_json::Value::Object(jobj); + } + + if let Some(var_ass) = o.as_any().downcast_ref::() { + let mut jobj: Map = Map::new(); + + let key = if var_ass.is_global {"VAR=".to_owned()} else {"temp=".to_owned()}; + jobj.insert(key, json!(var_ass.variable_name)); + + // Reassignment? + if !var_ass.is_new_declaration {jobj.insert("re".to_owned(), json!(true));} + + return serde_json::Value::Object(jobj); + } + + if o.as_any().is::() { + return json!("void") + } + + if let Some(tag) = o.as_any().downcast_ref::() { + let mut jobj: Map = Map::new(); + + jobj.insert("#".to_owned(), json!(tag.get_text())); + + return serde_json::Value::Object(jobj); + } + + if let Some(choice) = o.as_any().downcast_ref::() { + return write_choice(choice); + } + + panic!("Failed to write runtime object to JSON: {}", o.to_string()); +} + +pub fn write_rt_container(container: &Container, without_name: bool) -> serde_json::Value { + let mut c_array: Vec = Vec::new(); + + for c in container.content.iter() { + c_array.push(write_rtobject(c.clone())); + } + + // 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 + let named_only_content = &container.get_named_only_content(); + let count_flags = container.get_count_flags(); + let has_name_property = container.name.is_some() && !without_name; + + let has_terminator = !named_only_content.is_empty() || count_flags > 0 || has_name_property; + + if has_terminator { + let mut t_obj: Map = Map::new(); + + for (name, c) in named_only_content { + t_obj.insert(name.clone(), write_rt_container(c.as_ref(), true)); + } + + if count_flags > 0 { + t_obj.insert("#f".to_owned(), json!(count_flags)); + } + + if has_name_property { + t_obj.insert("#n".to_owned(), json!(container.name)); + } + + c_array.push(serde_json::Value::Object(t_obj)); + } else { + c_array.push(serde_json::Value::Null); + } + + serde_json::Value::Array(c_array) +} + +pub fn write_ink_list(list: &InkList) -> serde_json::Value { + let mut jobj: Map = Map::new(); + + let mut jlist: Map = Map::new(); + for (item, v) in list.items.iter() { + + let mut name = String::new(); + + match item.get_origin_name() { + Some(n) => name.push_str(n), + None => name.push('?'), + } + + name.push('.'); + name.push_str(item.get_item_name()); + + jlist.insert(name, json!(v)); + } + + jobj.insert("list".to_owned(), serde_json::Value::Object(jlist)); + + + serde_json::Value::Object(jobj) +} + +pub fn write_choice(choice: &Choice) -> serde_json::Value { + let mut jobj: Map = Map::new(); + + jobj.insert("text".to_owned(), json!(choice.text)); + jobj.insert("index".to_owned(), json!(choice.index)); + jobj.insert("originalChoicePath".to_owned(), json!(choice.source_path)); + jobj.insert("originalThreadIndex".to_owned(), json!(choice.original_thread_index)); + jobj.insert("targetPath".to_owned(), json!(choice.target_path.to_string())); + + serde_json::Value::Object(jobj) +} + +pub(crate) fn write_list_rt_objs(objs: &[Rc]) -> serde_json::Value { + let mut c_array: Vec = Vec::new(); + + for o in objs { + c_array.push(write_rtobject(o.clone())); + } + + serde_json::Value::Array(c_array) +} + +pub(crate) fn write_int_dictionary(map: &HashMap) -> serde_json::Value { + let mut jobj: Map = Map::new(); + + for (key, val) in map { + jobj.insert(key.clone(), json!(*val)); + } + + serde_json::Value::Object(jobj) +} diff --git a/src/lib.rs b/src/lib.rs index dbb72fa..a0d669b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ pub mod story; pub mod value_type; -mod json_serialization; +mod json_read; +mod json_write; mod object; mod value; mod container; diff --git a/src/native_function_call.rs b/src/native_function_call.rs index 7a7c5b7..a9d7c7e 100644 --- a/src/native_function_call.rs +++ b/src/native_function_call.rs @@ -2,7 +2,7 @@ use std::{fmt, rc::Rc}; use crate::{object::{Object, RTObject}, value::Value, void::Void, ink_list::InkList, value_type::ValueType}; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone, Copy)] pub enum Op { Add, Subtract, @@ -45,10 +45,17 @@ pub enum Op { pub struct NativeFunctionCall { obj: Object, - op: Op, + pub op: Op, } impl NativeFunctionCall { + pub fn new(op: Op) -> Self { + Self { + obj: Object::new(), + op, + } + } + pub fn new_from_name(name: &str) -> Option { match name { "+" => Some(Self::new(Op::Add)), @@ -86,10 +93,39 @@ impl NativeFunctionCall { } } - pub fn new(op: Op) -> Self { - Self { - obj: Object::new(), - op, + pub fn get_name(op: Op) -> String { + match op { + Op::Add => "+".to_owned(), + Op::Subtract => "-".to_owned(), + Op::Divide => "/".to_owned(), + Op::Multiply => "*".to_owned(), + Op::Mod => "%".to_owned(), + Op::Negate => "_".to_owned(), + Op::Equal => "==".to_owned(), + Op::Greater => ">".to_owned(), + Op::Less => "<".to_owned(), + Op::GreaterThanOrEquals => ">=".to_owned(), + Op::LessThanOrEquals => "<=".to_owned(), + Op::NotEquals => "!=".to_owned(), + Op::Not => "!".to_owned(), + Op::And => "&&".to_owned(), + Op::Or => "||".to_owned(), + Op::Min => "MIN".to_owned(), + Op::Max => "MAX".to_owned(), + Op::Pow => "POW".to_owned(), + Op::Floor => "FLOOR".to_owned(), + Op::Ceiling => "CEILING".to_owned(), + Op::Int => "INT".to_owned(), + Op::Float => "FLOAT".to_owned(), + Op::Has => "?".to_owned(), + Op::Hasnt => "!?".to_owned(), + Op::Intersect => "^".to_owned(), + Op::ListMin => "LIST_MIN".to_owned(), + Op::ListMax => "LIST_MAX".to_owned(), + Op::All => "LIST_ALL".to_owned(), + Op::Count => "LIST_COUNT".to_owned(), + Op::ValueOfList => "LIST_VALUE".to_owned(), + Op::Invert => "LIST_INVERT".to_owned(), } } diff --git a/src/story.rs b/src/story.rs index 3123748..e6385df 100644 --- a/src/story.rs +++ b/src/story.rs @@ -7,12 +7,12 @@ use rand::{Rng, rngs::StdRng, SeedableRng}; use crate::{ container::Container, error::ErrorType, - json_serialization, + json_read, push_pop::PushPopType, story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, search_result::SearchResult, variable_assigment::VariableAssignment, native_function_call::NativeFunctionCall, variable_reference::VariableReference, list_definitions_origin::ListDefinitionsOrigin, ink_list::InkList, ink_list_item::InkListItem, variables_state::VariablesState, }; -const INK_VERSION_CURRENT: i32 = 21; +pub const INK_VERSION_CURRENT: i32 = 21; const INK_VERSION_MINIMUM_COMPATIBLE: i32 = 18; #[derive(PartialEq)] @@ -73,7 +73,7 @@ impl Story { }; let list_definitions = match json.get("listDefs") { - Some(def) => Rc::new(json_serialization::jtoken_to_list_definitions(def)?), + Some(def) => Rc::new(json_read::jtoken_to_list_definitions(def)?), None => { return Err( "List Definitions node for ink not found. Are you sure it's a valid .ink.json file?" @@ -82,7 +82,7 @@ impl Story { } }; - let main_content_container = json_serialization::jtoken_to_runtime_object(root_token, None)?; + let main_content_container = json_read::jtoken_to_runtime_object(root_token, None)?; let main_content_container = main_content_container.into_any().downcast::(); @@ -109,7 +109,7 @@ impl Story { Ok(story) } - fn get_state(&self) -> &StoryState { + pub fn get_state(&self) -> &StoryState { self.state.as_ref().unwrap() } diff --git a/src/story_state.rs b/src/story_state.rs index 8057c76..c6ddbd8 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -2,9 +2,10 @@ use std::{rc::Rc, cell::RefCell, collections::HashMap}; -use crate::{pointer::{Pointer, self}, callstack::CallStack, flow::Flow, variables_state::VariablesState, choice::Choice, object::{RTObject, Object}, value::Value, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}, container::Container, state_patch::StatePatch, story::Story, path::Path, void::Void, tag::Tag, list_definitions_origin::ListDefinitionsOrigin, value_type::ValueType}; +use crate::{pointer::{Pointer, self}, callstack::CallStack, flow::Flow, variables_state::VariablesState, choice::Choice, object::{RTObject, Object}, value::Value, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}, container::Container, state_patch::StatePatch, story::{Story, INK_VERSION_CURRENT}, path::Path, void::Void, tag::Tag, list_definitions_origin::ListDefinitionsOrigin, value_type::ValueType, json_write}; use rand::Rng; +use serde_json::{json, Map}; pub const INK_SAVE_STATE_VERSION: u32 = 10; pub const MIN_COMPATIBLE_LOAD_VERSION: u32 = 8; @@ -1021,4 +1022,70 @@ impl StoryState { self.output_stream_dirty(); } + pub fn visit_count_at_path_string(&self, path_string: &str) -> usize { + let mut visit_count_out = None; + + if self.patch.is_some() { + let container = self.main_content_container.content_at_path(&Path::new_with_components_string(Some(path_string)), 0, -1).get_container(); + if container.is_none() { panic!("Content at path not found: {}", path_string);} + + visit_count_out = self.patch.as_ref().unwrap().get_visit_count(container.as_ref().unwrap()); + if let Some(visit_count_out) = visit_count_out {return visit_count_out;} + } + + visit_count_out = self.visit_counts.get(path_string).copied(); + if let Some(visit_count_out) = visit_count_out {return visit_count_out;} + + 0 + } + + pub fn to_json(&self) -> String { + self.write_json().to_string() + } + + pub fn load_json(&self, save_string: &str) { + todo!() + } + + fn write_json(&self) -> serde_json::Value { + let mut obj: Map = Map::new(); + + // Flows + let mut flows: Map = Map::new(); + + // current flow + flows.insert(self.current_flow.name.clone(), self.current_flow.write_json()); + + // named flows + if let Some(named_flows) = &self.named_flows { + for (k,v) in named_flows { + flows.insert(k.clone(), v.write_json()); + } + } + + obj.insert("flows".to_owned(), serde_json::Value::Object(flows)); + + + obj.insert("currentFlowName".to_owned(), json!(self.current_flow.name)); + obj.insert("variablesState".to_owned(), self.variables_state.write_json()); + obj.insert("evalStack".to_owned(), json_write::write_list_rt_objs(&self.evaluation_stack)); + + if !self.diverted_pointer.is_null() { + obj.insert("currentDivertTarget".to_owned(), json!(self.diverted_pointer.get_path().unwrap().get_components_string())); + } + + obj.insert("visitCounts".to_owned(), json_write::write_int_dictionary(&self.visit_counts)); + obj.insert("turnIndices".to_owned(), json_write::write_int_dictionary(&self.turn_indices)); + + obj.insert("turnIdx".to_owned(), json!(self.current_turn_index)); + obj.insert("storySeed".to_owned(), json!(self.story_seed)); + obj.insert("previousRandom".to_owned(), json!(self.previous_random)); + + obj.insert("inkSaveVersion".to_owned(), json!(INK_SAVE_STATE_VERSION)); + + // Not using this right now, but could do in future. + obj.insert("inkFormatVersion".to_owned(), json!(INK_VERSION_CURRENT)); + + serde_json::Value::Object(obj) + } } \ No newline at end of file diff --git a/src/value.rs b/src/value.rs index ba7aecb..5725eda 100644 --- a/src/value.rs +++ b/src/value.rs @@ -49,23 +49,11 @@ impl Value { } pub fn new_string(v:&str) -> Self { - - let mut inline_ws = true; - - for c in v.chars() { - if c != ' ' && c != '\t' { - inline_ws = false; - break; - } - } Self { obj: Object::new(), - value: ValueType::String(StringValue { - string: v.to_string(), - is_inline_whitespace: inline_ws, - is_newline: v.eq("\n")}) - } + value: ValueType::new_string(v), + } } pub fn new_divert_target(p:Path) -> Self { @@ -126,6 +114,16 @@ impl Value { } } + pub(crate) fn get_bool_value(o: &dyn RTObject) -> Option { + match o.as_any().downcast_ref::() { + Some(v) => match &v.value { + ValueType::Bool(v) => Some(*v), + _ => None, + }, + None => None, + } + } + pub fn get_int_value(o: &dyn RTObject) -> Option { match o.as_any().downcast_ref::() { Some(v) => match &v.value { @@ -136,6 +134,16 @@ impl Value { } } + pub fn get_float_value(o: &dyn RTObject) -> Option { + match o.as_any().downcast_ref::() { + Some(v) => match &v.value { + ValueType::Float(v) => Some(*v), + _ => None, + }, + None => None, + } + } + pub fn get_list_value_mut(o: &mut dyn RTObject) -> Option<&mut InkList> { match o.as_any_mut().downcast_mut::() { Some(v) => match &mut v.value { @@ -156,6 +164,16 @@ impl Value { } } + pub fn get_divert_value(o: &dyn RTObject) -> Option<&Path> { + match o.as_any().downcast_ref::() { + Some(v) => match &v.value { + ValueType::DivertTarget(v) => Some(v), + _ => None, + }, + None => None, + } + } + pub fn retain_list_origins_for_assignment(old_value: &dyn RTObject, new_value: &dyn RTObject) { if let Some(old_list) = Self::get_list_value(old_value) { diff --git a/src/value_type.rs b/src/value_type.rs index 9b892d8..2297164 100644 --- a/src/value_type.rs +++ b/src/value_type.rs @@ -15,6 +15,22 @@ pub enum ValueType { } impl ValueType { + pub fn new_string(str: &str) -> ValueType { + let mut inline_ws = true; + + for c in str.chars() { + if c != ' ' && c != '\t' { + inline_ws = false; + break; + } + } + + ValueType::String(StringValue { + string: str.to_string(), + is_inline_whitespace: inline_ws, + is_newline: str.eq("\n")}) + } + pub fn get_bool(&self) -> Option { match self { ValueType::Bool(v) => Some(*v), @@ -58,7 +74,7 @@ impl StringValue { } } -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub struct VariablePointerValue { pub variable_name: String, diff --git a/src/variable_reference.rs b/src/variable_reference.rs index 66999bb..2fba21b 100644 --- a/src/variable_reference.rs +++ b/src/variable_reference.rs @@ -26,7 +26,7 @@ impl VariableReference { } } - pub fn get_path_string_for_count(self: Rc) -> Option { + pub fn get_path_string_for_count(self: &Rc) -> Option { if let Some(path_for_count) = &self.path_for_count { Some(Object::compact_path_string(self.clone(), path_for_count)) } else { diff --git a/src/variables_state.rs b/src/variables_state.rs index baf135c..49cb6aa 100644 --- a/src/variables_state.rs +++ b/src/variables_state.rs @@ -1,6 +1,8 @@ use std::{collections::{HashMap, HashSet}, rc::Rc, cell::RefCell}; -use crate::{callstack::CallStack, state_patch::StatePatch, variable_assigment::VariableAssignment, value::Value, list_definitions_origin::ListDefinitionsOrigin, value_type::{VariablePointerValue, ValueType}}; +use serde_json::Map; + +use crate::{callstack::CallStack, state_patch::StatePatch, variable_assigment::VariableAssignment, value::Value, list_definitions_origin::ListDefinitionsOrigin, value_type::{VariablePointerValue, ValueType}, json_write}; #[derive(Clone)] @@ -153,17 +155,17 @@ impl VariablesState { Rc::new(Value::new_variable_pointer(&var_pointer.variable_name, context_index)) } - pub fn set(&mut self, variable_name: &str, value_type: ValueType) { + pub fn set(&mut self, variable_name: &str, value_type: ValueType) -> Result<(), String> { if !self.default_global_variables.as_ref().unwrap().contains_key(variable_name) { - // throw new StoryException( - // "Cannot assign to a variable (" + variableName + ") that hasn't been declared in the story"); - panic!() + return Err(format!("Cannot assign to a variable {} that hasn't been declared in the story", variable_name)); } let val = Value::from_value_type(value_type); self.set_global(variable_name, Rc::new(val)); + + Ok(()) } pub fn get(&self, variable_name: &str) -> Option { @@ -292,4 +294,55 @@ impl VariablesState { pub fn set_callstack(&mut self, callstack: Rc>) { self.callstack = callstack; } + + pub(crate) fn write_json(&self) -> serde_json::Value { + let mut jobj: Map = Map::new(); + + for (name, val) in self.global_variables.iter() { + + // Don't write out values that are the same as the default global values + let default_val = self.default_global_variables.as_ref().unwrap().get(name); + if let Some(default_val) = default_val { + if self.val_equal(val, default_val) {continue;} + } + + jobj.insert(name.clone(), json_write::write_rtobject(val.clone())); + } + + serde_json::Value::Object(jobj) + } + + fn val_equal(&self, val: &Value, default_val: &Value) -> bool { + match &val.value { + ValueType::Bool(val) => match default_val.value { + ValueType::Bool(default_val) => *val == default_val, + _ => false, + }, + ValueType::Int(val) => match default_val.value { + ValueType::Int(default_val) => *val == default_val, + _ => false, + }, + ValueType::Float(val) => match default_val.value { + ValueType::Float(default_val) => *val == default_val, + _ => false, + }, + ValueType::List(val) => match &default_val.value { + ValueType::List(default_val) => *val == *default_val, + _ => false, + }, + ValueType::String(val) => match &default_val.value { + ValueType::String(default_val) => val.string.eq(&default_val.string), + _ => false, + }, + ValueType::DivertTarget(val) => match &default_val.value { + ValueType::DivertTarget(default_val) => *val == *default_val, + _ => false, + }, + ValueType::VariablePointer(val) => match &default_val.value { + ValueType::VariablePointer(default_val) => *val == *default_val, + _ => false, + }, + } + } + } diff --git a/tests/runtime_test.rs b/tests/runtime_test.rs index 593fe63..7399608 100644 --- a/tests/runtime_test.rs +++ b/tests/runtime_test.rs @@ -1,7 +1,9 @@ -use bladeink::{story::Story, value_type::ValueType}; +use bladeink::{story::Story, value_type::{ValueType, StringValue}}; mod common; +// TODO external functions + variable observers + #[test] fn set_and_get_variable_test() -> Result<(), String> { let json_string = @@ -12,7 +14,7 @@ fn set_and_get_variable_test() -> Result<(), String> { common::next_all(&mut story, &mut text)?; assert_eq!(10, story.get_variables_state().get("x").unwrap().get_int().unwrap()); - story.get_variables_state_mut().set("x", ValueType::Int(15)); + story.get_variables_state_mut().set("x", ValueType::Int(15))?; assert_eq!(15, story.get_variables_state().get("x").unwrap().get_int().unwrap()); @@ -28,5 +30,137 @@ fn set_and_get_variable_test() -> Result<(), String> { } -// TODO external functions + variable observers +#[test] +fn set_non_existant_variable_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/runtime/set-get-variables.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + + let result = story.get_variables_state_mut().set("y", ValueType::new_string("earth")); + assert!(result.is_err()); + + assert_eq!(10, story.get_variables_state().get("x").unwrap().get_int().unwrap()); + + story.get_variables_state_mut().set("x", ValueType::Int(15))?; + + assert_eq!(15, story.get_variables_state().get("x").unwrap().get_int().unwrap()); + + story.choose_choice_index(0); + + text.clear(); + common::next_all(&mut story, &mut text)?; + + assert_eq!(1, text.len()); + assert_eq!("OK", text[0]); + + Ok(()) +} + +#[test] +fn jump_knot_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/runtime/jump-knot.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + story.choose_path_string("two", true, None)?; + common::next_all(&mut story, &mut text)?; + assert_eq!("Two", text.get(0).unwrap()); + + text.clear(); + story.choose_path_string("three", true, None)?; + common::next_all(&mut story, &mut text)?; + assert_eq!("Three", text.get(0).unwrap()); + + text.clear(); + story.choose_path_string("one", true, None)?; + common::next_all(&mut story, &mut text)?; + assert_eq!("One", text.get(0).unwrap()); + + text.clear(); + story.choose_path_string("two", true, None)?; + common::next_all(&mut story, &mut text)?; + assert_eq!("Two", text.get(0).unwrap()); + + Ok(()) +} + +#[test] +fn jump_stitch_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/runtime/jump-stitch.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + story.choose_path_string("two.sthree", true, None)?; + common::next_all(&mut story, &mut text)?; + assert_eq!("Two.3", text.get(0).unwrap()); + + text.clear(); + story.choose_path_string("one.stwo", true, None)?; + common::next_all(&mut story, &mut text)?; + assert_eq!("One.2", text.get(0).unwrap()); + + text.clear(); + story.choose_path_string("one.sone", true, None)?; + common::next_all(&mut story, &mut text)?; + assert_eq!("One.1", text.get(0).unwrap()); + + text.clear(); + story.choose_path_string("two.stwo", true, None)?; + common::next_all(&mut story, &mut text)?; + assert_eq!("Two.2", text.get(0).unwrap()); + + Ok(()) +} + +#[test] +fn read_visit_counts_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/runtime/read-visit-counts.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + assert_eq!(4, story.get_state().visit_count_at_path_string("two.s2")); + assert_eq!(5, story.get_state().visit_count_at_path_string("two")); + + Ok(()) +} + +#[test] +fn load_save_test() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/runtime/load-save.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("We arrived into London at 9.45pm exactly.", text.get(0).unwrap()); + + // save the game state + let save_string = story.get_state().to_json(); + + // recreate game and load state + Story::new(&json_string).unwrap(); + story.get_state().load_json(&save_string); + + story.choose_choice_index(0); + + common::next_all(&mut story, &mut text)?; + assert_eq!("\"There is not a moment to lose!\" I declared.", text.get(1).unwrap()); + assert_eq!("We hurried home to Savile Row as fast as we could.", text.get(2).unwrap()); + + // check that we are at the end + assert!(!story.can_continue()); + assert_eq!(0, story.get_current_choices().len()); + + Ok(()) +} + + From 2895c5153a94c40f4a5863ad8d3524dfa63d2770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Sun, 1 Oct 2023 01:54:12 +0000 Subject: [PATCH 47/91] save state completed! --- README.md | 4 +- src/callstack.rs | 86 +++++++++++- src/choice.rs | 2 +- src/choice_point.rs | 2 +- src/flow.rs | 37 +++++- src/json_read.rs | 80 ++++++++++- src/{json_write.rs => json_write_state.rs} | 0 src/lib.rs | 2 +- src/push_pop.rs | 11 ++ src/search_result.rs | 7 - src/story.rs | 6 +- src/story_state.rs | 146 ++++++++++++++++++++- src/variables_state.rs | 20 ++- tests/runtime_test.rs | 4 +- 14 files changed, 370 insertions(+), 37 deletions(-) rename src/{json_write.rs => json_write_state.rs} (100%) diff --git a/README.md b/README.md index 0fd0d5e..5c62d0d 100644 --- a/README.md +++ b/README.md @@ -24,12 +24,14 @@ Currently under development. This is the implementation status: ## TODO +- [ ] Al cargar los flows, el current flow hay que eliminarlo. - [ ] Optimize control command getname. Use static string array and address it by order. - [ ] Error handling - [ ] Cache components string in Path - [ ] Use OnceCell to lazy init the cache fields of RTObjects - [ ] Split large files. ex. Get the error handling out of the Story class. The performLogic - [ ] Story.state y VariablesState.default_global_variables shouldn't be optionals. - +- [ ] Review all the .unwrap() and change it by .ok_or("xxx"). We need to avoid panics! +- [ ] Multi-flow methods. diff --git a/src/callstack.rs b/src/callstack.rs index 9b88e99..39bf13d 100644 --- a/src/callstack.rs +++ b/src/callstack.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, rc::Rc}; use serde_json::{Map, json}; -use crate::{pointer::{Pointer, self}, push_pop::PushPopType, container::Container, value::Value, object::Object, json_read, json_write}; +use crate::{pointer::{Pointer, self}, push_pop::PushPopType, container::{Container, self}, value::Value, object::Object, json_read, json_write_state, path::Path, story::Story}; pub struct Element { pub current_pointer: Pointer, @@ -17,9 +17,9 @@ impl Element { fn new(push_pop_type: PushPopType, pointer: Pointer, in_expression_evaluation: bool) -> Element { Element { current_pointer: pointer, - in_expression_evaluation: in_expression_evaluation, + in_expression_evaluation, temporary_variables: HashMap::new(), - push_pop_type: push_pop_type, + push_pop_type, evaluation_stack_height_when_pushed:0, function_start_in_output_stream: 0 } @@ -50,6 +50,60 @@ impl Thread { } } + pub fn from_json(main_content_container: &Rc, j_obj: &Map) -> Result { + let mut thread = Thread::new(); + + thread.thread_index = j_obj.get("threadIndex").and_then(|i| i.as_i64()).ok_or("Invalid thread index")? as usize; + + if let Some(j_thread_callstack) = j_obj.get("callstack").and_then(|callstack| callstack.as_array()) { + + for j_el_tok in j_thread_callstack.iter() { + if let Some(j_element_obj) = j_el_tok.as_object() { + let push_pop_type = PushPopType::from_value(j_element_obj.get("type").and_then(|t| t.as_i64()).ok_or("Invalid push/pop type")? as usize); + + let mut pointer = pointer::NULL.clone(); + + let current_container_path_str = j_element_obj.get("cPath").and_then(|c| c.as_str()); + if current_container_path_str.is_some() { + let thread_pointer_result = main_content_container.content_at_path(&Path::new_with_components_string (current_container_path_str), 0, -1); + + pointer.container = thread_pointer_result.container(); + let pointer_index = j_element_obj.get("idx").and_then(|i| i.as_i64()).ok_or("Invalid pointer index")? as i32; + pointer.index = pointer_index; + + // TODO + // if thread_pointer_result.obj.is_none() { + // return Err(format!("When loading state, internal story location couldn't be found: {}. Has the story changed since this save data was created?", current_container_path_str)); + // } else + + if thread_pointer_result.approximate { + // story_context.warning(format!("When loading state, exact internal story location couldn't be found: '{}', so it was approximated to '{}' to recover. Has the story changed since this save data was created?", current_container_path_str, pointer_container.get_path().to_string())); + } + } + + let in_expression_evaluation = j_element_obj.get("exp").and_then(|exp| exp.as_bool()).unwrap_or(false); + + let mut el = Element::new(push_pop_type, pointer, in_expression_evaluation); + + if let Some(temps) = j_element_obj.get("temp").and_then(|temp| temp.as_object()) { + el.temporary_variables = json_read::jobject_to_hashmap_values(temps)?; + } else { + el.temporary_variables.clear(); + } + + thread.callstack.push(el); + } + } + } + + if let Some(prev_content_obj_path) = j_obj.get("previousContentObject").and_then(|p| p.as_str()) { + let prev_path = Path::new_with_components_string(Some(prev_content_obj_path)); + thread.previous_pointer = Story::pointer_at_path(main_content_container, &prev_path); + } + + Ok(thread) + } + pub fn copy(&self) -> Thread { let mut copy = Thread::new(); copy.thread_index = self.thread_index; @@ -79,7 +133,7 @@ impl Thread { el_map.insert("type".to_owned(), json!(el.push_pop_type as u32)); if el.temporary_variables.len() > 0 { - el_map.insert("exp".to_owned(), json_write::write_dictionary_values(&el.temporary_variables)); + el_map.insert("exp".to_owned(), json_write_state::write_dictionary_values(&el.temporary_variables)); } cs_array.push(serde_json::Value::Object(el_map)); @@ -165,10 +219,10 @@ impl CallStack { } pub fn push_thread(&mut self) { - let mut newThread = self.get_current_thread().copy(); + let mut new_thread = self.get_current_thread().copy(); self.thread_counter += 1; - newThread.thread_index = self.thread_counter; - self.threads.push(newThread); + new_thread.thread_index = self.thread_counter; + self.threads.push(new_thread); } pub fn can_pop(&self) -> bool { @@ -325,4 +379,22 @@ impl CallStack { return None; } + + pub fn load_json(&mut self, main_content_container: &Rc, j_obj: &Map) -> Result<(), String> { + + self.threads.clear(); + + let j_threads = j_obj.get("threads").unwrap(); + + for jThreadTok in j_threads.as_array().unwrap().iter() { + let j_thread_obj = jThreadTok.as_object().unwrap(); + let thread = Thread::from_json(main_content_container, j_thread_obj)?; + self.threads.push(thread); + } + + self.thread_counter = j_obj.get("threadCounter").unwrap().as_i64().unwrap() as usize; + self.start_of_root = Pointer::start_of(main_content_container.clone()).clone(); + + Ok(()) + } } \ No newline at end of file diff --git a/src/choice.rs b/src/choice.rs index b529831..1748118 100644 --- a/src/choice.rs +++ b/src/choice.rs @@ -45,7 +45,7 @@ impl Choice { } } - pub fn set_thread_at_generation(&mut self, thread: Thread) { + pub fn set_thread_at_generation(&self, thread: Thread) { self.thread_at_generation.replace(Some(thread)); } diff --git a/src/choice_point.rs b/src/choice_point.rs index 4ca02ef..0740956 100644 --- a/src/choice_point.rs +++ b/src/choice_point.rs @@ -39,7 +39,7 @@ impl ChoicePoint { } pub fn get_choice_target(self: &Rc) -> Option> { - Object::resolve_path(self.clone(), &self.path_on_choice.borrow().as_ref().unwrap()).get_container() + Object::resolve_path(self.clone(), &self.path_on_choice.borrow().as_ref().unwrap()).container() } pub fn get_flags(&self) -> i32 { diff --git a/src/flow.rs b/src/flow.rs index a895ea7..edc4af2 100644 --- a/src/flow.rs +++ b/src/flow.rs @@ -2,7 +2,7 @@ use std::{rc::Rc, cell::RefCell}; use serde_json::Map; -use crate::{callstack::CallStack, choice::Choice, object::RTObject, container::Container, json_write}; +use crate::{callstack::{CallStack, Thread}, choice::Choice, object::RTObject, container::Container, json_write_state, json_read}; #[derive(Clone)] pub struct Flow { @@ -22,11 +22,27 @@ impl Flow { } } + pub fn from_json(name: &str, main_content_container: Rc, j_obj: &Map) -> Result { + let mut flow = Self { + name: name.to_string(), + callstack: Rc::new(RefCell::new(CallStack::new(main_content_container.clone()))), + output_stream: json_read::jarray_to_runtime_obj_list(j_obj.get("outputStream").ok_or("outputStream not found.")?.as_array().unwrap(), false)?, + current_choices: json_read::jarray_to_runtime_obj_list(j_obj.get("currentChoices").ok_or("currentChoices not found.")?.as_array().unwrap(), false)?.iter().map(|o| o.clone().into_any().downcast::().unwrap()).collect::>>(), + }; + + flow.callstack.borrow_mut().load_json(&main_content_container, j_obj.get("callstack").ok_or("loading callstack")?.as_object().unwrap())?; + let j_choice_threads = j_obj.get("choiceThreads").ok_or("loading choice threads")?; + + flow.load_flow_choice_threads(j_choice_threads, main_content_container)?; + + Ok(flow) + } + pub(crate) fn write_json(&self) -> serde_json::Value { let mut flow: Map = Map::new(); flow.insert("callstack".to_owned(), self.callstack.borrow().write_json()); - flow.insert("outputStream".to_owned(), json_write::write_list_rt_objs(&self.output_stream)); + flow.insert("outputStream".to_owned(), json_write_state::write_list_rt_objs(&self.output_stream)); // choiceThreads: optional // Has to come BEFORE the choices themselves are written out @@ -49,17 +65,30 @@ impl Flow { } } - if (has_choice_threads) { + if has_choice_threads { flow.insert("choiceThreads".to_owned(), serde_json::Value::Object(jct)); } let mut c_array: Vec = Vec::new(); for c in self.current_choices.iter() { - c_array.push(json_write::write_choice(c)); + c_array.push(json_write_state::write_choice(c)); } flow.insert("currentChoices".to_owned(), serde_json::Value::Array(c_array)); serde_json::Value::Object(flow) } + + pub fn load_flow_choice_threads(&mut self, j_choice_threads: &serde_json::Value, main_content_container: Rc) -> Result<(), String>{ + for choice in self.current_choices.iter_mut() { + self.callstack.borrow().get_thread_with_index(choice.original_thread_index).map(|o| choice.set_thread_at_generation(o.copy())).or_else(|| { + let j_saved_choice_thread = + j_choice_threads.get(&choice.original_thread_index.to_string()).ok_or("loading choice threads").unwrap(); + choice.set_thread_at_generation(Thread::from_json(&main_content_container, j_saved_choice_thread.as_object().unwrap()).unwrap()); + Some(()) + }).unwrap(); + } + + Ok(()) + } } \ No newline at end of file diff --git a/src/json_read.rs b/src/json_read.rs index 9e55f0f..32b5cae 100644 --- a/src/json_read.rs +++ b/src/json_read.rs @@ -22,7 +22,9 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) }, serde_json::Value::String(value) => { - let str = value.as_str(); + let unscaped = unscape_string(value)?; + let str = unscaped.as_str(); + // String value let first_char = str.chars().next().unwrap(); if first_char == '^' {return Ok(Rc::new(Value::new_string(&str[1..])));} @@ -235,6 +237,49 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) } +fn unscape_string(text: &str) -> Result { + let mut sb = String::new(); + let mut offset = 0; + + while offset < text.len() { + let c = text.chars().nth(offset).unwrap(); + offset += 1; + + if c == '\\' { + // Escaped character + if offset >= text.len() { + return Err("Unexpected EOF while reading string".to_string()); + } + let escaped_char = text.chars().nth(offset).unwrap(); + offset += 1; + match escaped_char { + '"' | '\\' | '/' => sb.push(escaped_char), + 'n' => sb.push('\n'), + 't' => sb.push('\t'), + 'r' | 'b' | 'f' => { /* Ignore other control characters */ } + 'u' => { + // 4-digit Unicode + if offset + 4 >= text.len() { + return Err("Unexpected EOF while reading string".to_string()); + } + let digits = &text[offset..offset + 4]; + if let Ok(uchar) = u32::from_str_radix(digits, 16) { + sb.push(char::from_u32(uchar).ok_or(format!("Invalid Unicode escape character at offset {}", offset - 1))?); + offset += 4; + } else { + return Err(format!("Invalid Unicode escape character at offset {}", offset - 1)); + } + } + _ => return Err(format!("Invalid Unicode escape character at offset {}", offset - 1)), + } + } else { + sb.push(c); + } + } + + Ok(sb) +} + fn jarray_to_container(jarray: &Vec, name: Option) -> Result, String> { // Final object in the array is always a combination of // - named content @@ -269,7 +314,7 @@ fn jarray_to_container(jarray: &Vec, name: Option) -> Ok(container) } -fn jarray_to_runtime_obj_list(jarray: &Vec, skip_last: bool) -> Result>, String> { +pub fn jarray_to_runtime_obj_list(jarray: &Vec, skip_last: bool) -> Result>, String> { let mut count = jarray.len(); if skip_last { @@ -315,3 +360,34 @@ pub fn jtoken_to_list_definitions(def: &serde_json::Value) -> Result) -> Result>, String> { + let mut dict: HashMap> = HashMap::new(); + + for (k, v) in jobj.iter() { + dict.insert(k.clone(), jtoken_to_runtime_object(v, None)?.into_any().downcast::().unwrap()); + } + + Ok(dict) +} + +pub(crate) fn jobject_to_hashmap_rtobjects(jobj: &Map) -> Result>, String> { + + let mut dict: HashMap> = HashMap::new(); + + for (k, v) in jobj.iter() { + dict.insert(k.clone(), jtoken_to_runtime_object(v, None)?); + } + + Ok(dict) +} + +pub(crate) fn jobject_to_usize_hashmap(jobj: &Map) -> Result, String> { + let mut dict: HashMap = HashMap::new(); + + for (k, v) in jobj.iter() { + dict.insert(k.clone(), v.as_i64().unwrap() as usize); + } + + Ok(dict) +} diff --git a/src/json_write.rs b/src/json_write_state.rs similarity index 100% rename from src/json_write.rs rename to src/json_write_state.rs diff --git a/src/lib.rs b/src/lib.rs index a0d669b..44a37c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ pub mod story; pub mod value_type; mod json_read; -mod json_write; +mod json_write_state; mod object; mod value; mod container; diff --git a/src/push_pop.rs b/src/push_pop.rs index d72e406..92c27b1 100644 --- a/src/push_pop.rs +++ b/src/push_pop.rs @@ -3,4 +3,15 @@ pub enum PushPopType { Tunnel, Function, FunctionEvaluationFromGame +} + +impl PushPopType { + pub(crate) fn from_value(value: usize) -> PushPopType { + match value { + 0 => PushPopType::Tunnel, + 1 => PushPopType::Function, + 2 => PushPopType::FunctionEvaluationFromGame, + _ => panic!("Unexpected PushPopType value") + } + } } \ No newline at end of file diff --git a/src/search_result.rs b/src/search_result.rs index 6b02bc4..dc5be91 100644 --- a/src/search_result.rs +++ b/src/search_result.rs @@ -32,13 +32,6 @@ impl SearchResult { } } - pub fn get_container(&self) -> Option> { - match self.obj.clone().into_any().downcast::() { - Ok(c) => Some(c), - Err(_) => None, - } - } - pub fn container(&self) -> Option> { let c = self.obj.clone().into_any().downcast::(); diff --git a/src/story.rs b/src/story.rs index e6385df..7f6ba2a 100644 --- a/src/story.rs +++ b/src/story.rs @@ -113,7 +113,7 @@ impl Story { self.state.as_ref().unwrap() } - fn get_state_mut(&mut self) -> &mut StoryState { + pub fn get_state_mut(&mut self) -> &mut StoryState { self.state.as_mut().unwrap() } @@ -1598,13 +1598,13 @@ impl Story { if path.get_last_component().unwrap().is_index() { path_length_to_use -= 1; let result = SearchResult::from_search_result(&main_content_container.content_at_path(path, 0, path_length_to_use)); - p.container = result.get_container(); + p.container = result.container(); p.index = path.get_last_component().unwrap().index.unwrap() as i32; result } else { let result = SearchResult::from_search_result(&main_content_container.content_at_path(path, 0, -1)); - p.container = result.get_container(); + p.container = result.container(); p.index = -1; result diff --git a/src/story_state.rs b/src/story_state.rs index c6ddbd8..5321129 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -2,7 +2,7 @@ use std::{rc::Rc, cell::RefCell, collections::HashMap}; -use crate::{pointer::{Pointer, self}, callstack::CallStack, flow::Flow, variables_state::VariablesState, choice::Choice, object::{RTObject, Object}, value::Value, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}, container::Container, state_patch::StatePatch, story::{Story, INK_VERSION_CURRENT}, path::Path, void::Void, tag::Tag, list_definitions_origin::ListDefinitionsOrigin, value_type::ValueType, json_write}; +use crate::{pointer::{Pointer, self}, callstack::CallStack, flow::Flow, variables_state::VariablesState, choice::Choice, object::{RTObject, Object}, value::Value, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}, container::Container, state_patch::StatePatch, story::{Story, INK_VERSION_CURRENT}, path::Path, void::Void, tag::Tag, list_definitions_origin::ListDefinitionsOrigin, value_type::ValueType, json_write_state, json_read}; use rand::Rng; use serde_json::{json, Map}; @@ -1026,7 +1026,7 @@ impl StoryState { let mut visit_count_out = None; if self.patch.is_some() { - let container = self.main_content_container.content_at_path(&Path::new_with_components_string(Some(path_string)), 0, -1).get_container(); + let container = self.main_content_container.content_at_path(&Path::new_with_components_string(Some(path_string)), 0, -1).container(); if container.is_none() { panic!("Content at path not found: {}", path_string);} visit_count_out = self.patch.as_ref().unwrap().get_visit_count(container.as_ref().unwrap()); @@ -1043,8 +1043,11 @@ impl StoryState { self.write_json().to_string() } - pub fn load_json(&self, save_string: &str) { - todo!() + pub fn load_json(&mut self, save_string: &str) -> Result<(), String> { + match serde_json::from_str(save_string) { + Ok(value) => self.load_json_obj(value), + Err(_) => Err("State not in JSON format.".to_owned()), + } } fn write_json(&self) -> serde_json::Value { @@ -1068,14 +1071,14 @@ impl StoryState { obj.insert("currentFlowName".to_owned(), json!(self.current_flow.name)); obj.insert("variablesState".to_owned(), self.variables_state.write_json()); - obj.insert("evalStack".to_owned(), json_write::write_list_rt_objs(&self.evaluation_stack)); + obj.insert("evalStack".to_owned(), json_write_state::write_list_rt_objs(&self.evaluation_stack)); if !self.diverted_pointer.is_null() { obj.insert("currentDivertTarget".to_owned(), json!(self.diverted_pointer.get_path().unwrap().get_components_string())); } - obj.insert("visitCounts".to_owned(), json_write::write_int_dictionary(&self.visit_counts)); - obj.insert("turnIndices".to_owned(), json_write::write_int_dictionary(&self.turn_indices)); + obj.insert("visitCounts".to_owned(), json_write_state::write_int_dictionary(&self.visit_counts)); + obj.insert("turnIndices".to_owned(), json_write_state::write_int_dictionary(&self.turn_indices)); obj.insert("turnIdx".to_owned(), json!(self.current_turn_index)); obj.insert("storySeed".to_owned(), json!(self.story_seed)); @@ -1088,4 +1091,133 @@ impl StoryState { serde_json::Value::Object(obj) } + + fn load_json_obj(&mut self, j_object: serde_json::Value) -> Result<(), String> { + let j_save_version = match j_object.get("inkSaveVersion") { + Some(version) => version, + None => return Err("ink save format incorrect, can't load.".to_string()), + }; + + if let Some(version) = j_save_version.as_i64() { + if version < MIN_COMPATIBLE_LOAD_VERSION as i64 { + return Err(format!( + "Ink save format isn't compatible with the current version (saw '{}', but minimum is {}), so can't load.", + version, + MIN_COMPATIBLE_LOAD_VERSION + )); + } + } + + // Flows: Always exists in latest format (even if there's just one default) + // but this dictionary doesn't exist in prev format + if let Some(flows_obj) = j_object.get("flows") { + let flows_obj_dict = flows_obj.as_object().ok_or_else(|| "Invalid flows object".to_string())?; + + // Single default flow + if flows_obj_dict.len() == 1 { + self.named_flows = None; + } + // Multi-flow, need to create flows dict + else if self.named_flows.is_none() { + self.named_flows = Some(HashMap::new()); + } + // Multi-flow, already have a flows dict + else { + self.named_flows.as_mut().unwrap().clear(); + } + + // Load up each flow (there may only be one) + for (named_flow_name, named_flow_obj) in flows_obj_dict.iter() { + let name = named_flow_name.clone(); + let flow_obj = named_flow_obj.as_object().ok_or_else(|| "Invalid flow object".to_string())?; + + // Load up this flow using JSON data + let flow = Flow::from_json(&name, self.main_content_container.clone(), flow_obj)?; + + if flows_obj_dict.len() == 1 { + self.current_flow = Flow::from_json(&name, self.main_content_container.clone(), flow_obj)?; + } else { + self.named_flows + .as_mut() + .ok_or_else(|| "Named flows should be initialized".to_string())? + .insert(name, flow); + } + } + + if let Some(named_flows) = &self.named_flows { + if named_flows.len() > 1 { + if let Some(current_flow_name) = j_object.get("currentFlowName") { + if let Some(curr_flow_name) = current_flow_name.as_str() { + if let Some(curr_flow) = named_flows.get(curr_flow_name) { + self.current_flow = curr_flow.clone(); + } + } + } + } + } + } + // Old format: individually load up callstack, output stream, choices in + // current/default flow + else { + self.named_flows = None; + self.current_flow.name = "default".to_string(); // Replace with the default flow name + self.current_flow + .callstack + .borrow_mut().load_json(&self.main_content_container, j_object.get("callstackThreads").and_then(|o| o.as_object()).ok_or("loading callstack threads")?); + + if let Some(output_stream_obj) = j_object.get("outputStream") { + self.current_flow.output_stream = json_read::jarray_to_runtime_obj_list(&output_stream_obj.as_array().unwrap(), false)?; + } + + if let Some(current_choices_obj) = j_object.get("currentChoices") { + self.current_flow.current_choices = json_read::jarray_to_runtime_obj_list(¤t_choices_obj.as_array().unwrap(), false)?.iter().map(|o| o.clone().into_any().downcast::().unwrap()).collect(); + } + + if let Some(j_choice_threads_obj) = j_object.get("choiceThreads") { + self.current_flow.load_flow_choice_threads(j_choice_threads_obj, self.main_content_container.clone())?; + } + } + + self.output_stream_dirty(); + self.alive_flow_names_dirty = true; + + if let Some(variables_state_obj) = j_object.get("variablesState") { + self.variables_state.load_json(variables_state_obj.as_object().ok_or_else(|| "Invalid variables state object".to_string())?)?; + self.variables_state.set_callstack(self.current_flow.callstack.clone()); + } + + if let Some(eval_stack_obj) = j_object.get("evalStack") { + self.evaluation_stack = json_read::jarray_to_runtime_obj_list(eval_stack_obj.as_array().unwrap(), false)?; + } + + if let Some(current_divert_target_path) = j_object.get("currentDivertTarget") { + let divert_path = Path::new_with_components_string(current_divert_target_path.as_str()); + self.diverted_pointer = Story::pointer_at_path(&self.main_content_container, &divert_path).clone(); + } + + if let Some(visit_counts_obj) = j_object.get("visitCounts") { + self.visit_counts = json_read::jobject_to_usize_hashmap(visit_counts_obj.as_object().ok_or_else(|| "Invalid visit counts object".to_string())?)?; + } + + if let Some(turn_indices_obj) = j_object.get("turnIndices") { + self.turn_indices = json_read::jobject_to_usize_hashmap(turn_indices_obj.as_object().ok_or_else(|| "Invalid turn indices object".to_string())?)?; + } + + if let Some(current_turn_index) = j_object.get("turnIdx") { + self.current_turn_index = current_turn_index.as_i64().ok_or_else(|| "Invalid current turn index".to_string())? as i32; + } + + if let Some(story_seed) = j_object.get("storySeed") { + self.story_seed = story_seed.as_i64().ok_or_else(|| "Invalid story seed".to_string())? as i32; + } + + // Not optional, but bug in inkjs means it's actually missing in inkjs saves + if let Some(previous_random_obj) = j_object.get("previousRandom") { + self.previous_random = previous_random_obj.as_i64().ok_or_else(|| "Invalid previous random value".to_string())? as i32; + } else { + self.previous_random = 0; + } + + Ok(()) + } } \ No newline at end of file diff --git a/src/variables_state.rs b/src/variables_state.rs index 49cb6aa..cd04b2c 100644 --- a/src/variables_state.rs +++ b/src/variables_state.rs @@ -2,7 +2,7 @@ use std::{collections::{HashMap, HashSet}, rc::Rc, cell::RefCell}; use serde_json::Map; -use crate::{callstack::CallStack, state_patch::StatePatch, variable_assigment::VariableAssignment, value::Value, list_definitions_origin::ListDefinitionsOrigin, value_type::{VariablePointerValue, ValueType}, json_write}; +use crate::{callstack::CallStack, state_patch::StatePatch, variable_assigment::VariableAssignment, value::Value, list_definitions_origin::ListDefinitionsOrigin, value_type::{VariablePointerValue, ValueType}, json_write_state, json_read}; #[derive(Clone)] @@ -306,7 +306,7 @@ impl VariablesState { if self.val_equal(val, default_val) {continue;} } - jobj.insert(name.clone(), json_write::write_rtobject(val.clone())); + jobj.insert(name.clone(), json_write_state::write_rtobject(val.clone())); } serde_json::Value::Object(jobj) @@ -345,4 +345,20 @@ impl VariablesState { } } + pub(crate) fn load_json(&mut self, jobj: &Map) -> Result<(), String> { + self.global_variables.clear(); + + for (k, v) in self.default_global_variables.as_ref().unwrap().iter() { + let loaded_token = jobj.get(k); + + if let Some(loaded_token) = loaded_token { + self.global_variables.insert(k.to_string(), json_read::jtoken_to_runtime_object(loaded_token, None)?.into_any().downcast::().unwrap()); + } else { + self.global_variables.insert(k.clone(), v.clone()); + } + } + + Ok(()) + } + } diff --git a/tests/runtime_test.rs b/tests/runtime_test.rs index 7399608..55a8a99 100644 --- a/tests/runtime_test.rs +++ b/tests/runtime_test.rs @@ -145,9 +145,11 @@ fn load_save_test() -> Result<(), String> { // save the game state let save_string = story.get_state().to_json(); + println!("{}", save_string); + // recreate game and load state Story::new(&json_string).unwrap(); - story.get_state().load_json(&save_string); + story.get_state_mut().load_json(&save_string)?; story.choose_choice_index(0); From 31c899a91a0c0954cdfb24b22107d692a5ac8de6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Sun, 1 Oct 2023 18:14:19 +0000 Subject: [PATCH 48/91] load/save state finished. --- README.md | 3 ++- src/callstack.rs | 2 +- src/choice.rs | 20 ++++++++++---------- src/flow.rs | 22 ++++++++++------------ src/json_read.rs | 12 +++++++----- src/json_write_state.rs | 16 +++------------- src/state_patch.rs | 12 ++++++------ src/story.rs | 2 +- src/story_state.rs | 21 ++++++++++----------- tests/list_test.rs | 12 +++++++++++- tests/thread_test.rs | 40 ++++++++++++++++++++++------------------ 11 files changed, 83 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index 5c62d0d..494be7a 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,11 @@ Currently under development. This is the implementation status: - [x] Threads - [x] Tags - [x] Lists -- [ ] Load/Save state +- [x] Load/Save state ## TODO +- [ ] Eliminar código de unscape. - [ ] Al cargar los flows, el current flow hay que eliminarlo. - [ ] Optimize control command getname. Use static string array and address it by order. - [ ] Error handling diff --git a/src/callstack.rs b/src/callstack.rs index 39bf13d..63f7e94 100644 --- a/src/callstack.rs +++ b/src/callstack.rs @@ -133,7 +133,7 @@ impl Thread { el_map.insert("type".to_owned(), json!(el.push_pop_type as u32)); if el.temporary_variables.len() > 0 { - el_map.insert("exp".to_owned(), json_write_state::write_dictionary_values(&el.temporary_variables)); + el_map.insert("temp".to_owned(), json_write_state::write_dictionary_values(&el.temporary_variables)); } cs_array.push(serde_json::Value::Object(el_map)); diff --git a/src/choice.rs b/src/choice.rs index 1748118..bbf780e 100644 --- a/src/choice.rs +++ b/src/choice.rs @@ -9,7 +9,7 @@ pub struct Choice { pub is_invisible_default: bool, pub tags: Vec, pub index: RefCell, - pub original_thread_index: usize, + pub original_thread_index: RefCell, pub text: String, thread_at_generation: RefCell>, pub source_path: String @@ -17,16 +17,16 @@ pub struct Choice { impl Choice { pub fn new(target_path: Path, source_path: String, is_invisible_default: bool, tags: Vec, thread_at_generation: Thread, text: String, index: usize, original_thread_index: usize) -> Choice { - Choice { + Self { obj: Object::new(), - target_path: target_path, - is_invisible_default: is_invisible_default, - tags: tags, + target_path, + is_invisible_default, + tags, index: RefCell::new(index), - original_thread_index: original_thread_index, - text: text, + original_thread_index: RefCell::new(original_thread_index), + text, thread_at_generation: RefCell::new(Some(thread_at_generation)), - source_path: source_path, + source_path, } } @@ -38,10 +38,10 @@ impl Choice { is_invisible_default: false, tags: Vec::new(), index: RefCell::new(index), - original_thread_index: original_thread_index, + original_thread_index: RefCell::new(original_thread_index), text: text.to_string(), thread_at_generation: RefCell::new(None), - source_path: source_path, + source_path, } } diff --git a/src/flow.rs b/src/flow.rs index edc4af2..3fe6adc 100644 --- a/src/flow.rs +++ b/src/flow.rs @@ -31,7 +31,7 @@ impl Flow { }; flow.callstack.borrow_mut().load_json(&main_content_container, j_obj.get("callstack").ok_or("loading callstack")?.as_object().unwrap())?; - let j_choice_threads = j_obj.get("choiceThreads").ok_or("loading choice threads")?; + let j_choice_threads = j_obj.get("choiceThreads"); flow.load_flow_choice_threads(j_choice_threads, main_content_container)?; @@ -50,18 +50,14 @@ impl Flow { let mut has_choice_threads = false; let mut jct: Map = Map::new(); for c in self.current_choices.iter() { - // c.original_thread_index = c.get_thread_at_generation().unwrap().thread_index; - let original_thread_index = match c.get_thread_at_generation() { - Some(t) => Some(t.thread_index), - None => None, - }.unwrap(); + c.original_thread_index.replace(c.get_thread_at_generation().unwrap().thread_index); - if self.callstack.borrow().get_thread_with_index(original_thread_index).is_none() { + if self.callstack.borrow().get_thread_with_index(*c.original_thread_index.borrow()).is_none() { if !has_choice_threads { has_choice_threads = true; } - jct.insert(original_thread_index.to_string(), c.get_thread_at_generation().unwrap().write_json()); + jct.insert(c.original_thread_index.borrow().to_string(), c.get_thread_at_generation().unwrap().write_json()); } } @@ -79,14 +75,16 @@ impl Flow { serde_json::Value::Object(flow) } - pub fn load_flow_choice_threads(&mut self, j_choice_threads: &serde_json::Value, main_content_container: Rc) -> Result<(), String>{ + pub fn load_flow_choice_threads(&mut self, j_choice_threads: Option<&serde_json::Value>, main_content_container: Rc) -> Result<(), String>{ for choice in self.current_choices.iter_mut() { - self.callstack.borrow().get_thread_with_index(choice.original_thread_index).map(|o| choice.set_thread_at_generation(o.copy())).or_else(|| { + self.callstack.borrow().get_thread_with_index(*choice.original_thread_index.borrow()) + .map(|o| choice.set_thread_at_generation(o.copy())) + .or_else(|| { let j_saved_choice_thread = - j_choice_threads.get(&choice.original_thread_index.to_string()).ok_or("loading choice threads").unwrap(); + j_choice_threads.and_then(|c| c.get(choice.original_thread_index.borrow().to_string())).ok_or("loading choice threads").unwrap(); choice.set_thread_at_generation(Thread::from_json(&main_content_container, j_saved_choice_thread.as_object().unwrap()).unwrap()); Some(()) - }).unwrap(); + }); } Ok(()) diff --git a/src/json_read.rs b/src/json_read.rs index 32b5cae..daed234 100644 --- a/src/json_read.rs +++ b/src/json_read.rs @@ -22,8 +22,9 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) }, serde_json::Value::String(value) => { - let unscaped = unscape_string(value)?; - let str = unscaped.as_str(); + //let unscaped = unscape_string(value)?; + //let str = unscaped.as_str(); + let str = value.as_str(); // String value let first_char = str.chars().next().unwrap(); @@ -382,11 +383,12 @@ pub(crate) fn jobject_to_hashmap_rtobjects(jobj: &Map Ok(dict) } -pub(crate) fn jobject_to_usize_hashmap(jobj: &Map) -> Result, String> { - let mut dict: HashMap = HashMap::new(); +pub(crate) fn jobject_to_int_hashmap(jobj: &Map) -> Result, String> { + let mut dict: HashMap = HashMap::new(); for (k, v) in jobj.iter() { - dict.insert(k.clone(), v.as_i64().unwrap() as usize); + println!("{}", v); + dict.insert(k.clone(), v.as_i64().unwrap() as i32); } Ok(dict) diff --git a/src/json_write_state.rs b/src/json_write_state.rs index 2c6a97b..661a2d1 100644 --- a/src/json_write_state.rs +++ b/src/json_write_state.rs @@ -7,16 +7,6 @@ use crate::{ object::RTObject, value::Value, glue::Glue, choice_point::ChoicePoint, push_pop::PushPopType, divert::Divert, ink_list::InkList, control_command::ControlCommand, native_function_call::NativeFunctionCall, variable_reference::VariableReference, variable_assigment::VariableAssignment, tag::Tag, void::Void, choice::Choice, }; -pub fn write_dictionary_runtime_objs(objs: &HashMap>) -> serde_json::Value { - let mut jobjs: Map = Map::new(); - - for (k,o) in objs { - jobjs.insert(k.clone(), write_rtobject(o.clone())); - } - - serde_json::Value::Object(jobjs) -} - pub fn write_dictionary_values(objs: &HashMap>) -> serde_json::Value { let mut jobjs: Map = Map::new(); @@ -81,7 +71,7 @@ pub fn write_rtobject(o: Rc) -> serde_json::Value { let mut s = String::new(); if v.is_newline { - s.push_str("\\n"); + s.push_str("\n"); } else { s.push('^'); s.push_str(&v.string); @@ -91,7 +81,7 @@ pub fn write_rtobject(o: Rc) -> serde_json::Value { } if let Some(v) = Value::get_list_value(o.as_ref()) { - write_ink_list(v); + return write_ink_list(v); } if let Some(v) = Value::get_divert_target_value(o.as_ref()) { @@ -257,7 +247,7 @@ pub(crate) fn write_list_rt_objs(objs: &[Rc]) -> serde_json::Value serde_json::Value::Array(c_array) } -pub(crate) fn write_int_dictionary(map: &HashMap) -> serde_json::Value { +pub(crate) fn write_int_dictionary(map: &HashMap) -> serde_json::Value { let mut jobj: Map = Map::new(); for (key, val) in map { diff --git a/src/state_patch.rs b/src/state_patch.rs index a112af0..9362beb 100644 --- a/src/state_patch.rs +++ b/src/state_patch.rs @@ -8,8 +8,8 @@ use crate::{object::{RTObject, Object}, container::Container, value::Value}; pub struct StatePatch { pub globals: HashMap>, pub changed_variables: HashSet, - pub visit_counts: HashMap, - pub turn_indices: HashMap, + pub visit_counts: HashMap, + pub turn_indices: HashMap, } impl StatePatch { @@ -30,12 +30,12 @@ impl StatePatch { } } - pub fn get_visit_count(&self, container: &Rc) -> Option { + pub fn get_visit_count(&self, container: &Rc) -> Option { let key = Object::get_path(container.as_ref()).to_string(); self.visit_counts.get(&key).copied() } - pub fn set_visit_count(&mut self, container: &Rc, count: usize) { + pub fn set_visit_count(&mut self, container: &Rc, count: i32) { let key = Object::get_path(container.as_ref()).to_string(); self.visit_counts.insert(key, count); } @@ -54,10 +54,10 @@ impl StatePatch { pub(crate) fn set_turn_index(&mut self, container: &Container, index: i32) { let key = Object::get_path(container).to_string(); - self.turn_indices.insert(key, index as usize); + self.turn_indices.insert(key, index); } - pub(crate) fn get_turn_index(&self, container: &Container) -> Option<&usize> { + pub(crate) fn get_turn_index(&self, container: &Container) -> Option<&i32> { let key = Object::get_path(container).to_string(); return self.turn_indices.get(&key); } diff --git a/src/story.rs b/src/story.rs index 7f6ba2a..71467ec 100644 --- a/src/story.rs +++ b/src/story.rs @@ -802,7 +802,7 @@ impl Story { // "Tried to divert using a target from a variable that could not be found ({})", // var_name // )); - panic!(); + panic!("Tried to divert using a target from a variable that could not be found ({})", var_name.as_ref().unwrap()); } } else if current_divert.is_external { //call_external_function(¤t_divert.get_target_path_string(), current_divert.get_external_args()); diff --git a/src/story_state.rs b/src/story_state.rs index 5321129..749ad8a 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -27,8 +27,8 @@ pub struct StoryState { patch: Option, named_flows: Option>, pub diverted_pointer: Pointer, - pub visit_counts: HashMap, - pub turn_indices: HashMap, + pub visit_counts: HashMap, + pub turn_indices: HashMap, pub current_turn_index: i32, pub story_seed: i32, pub previous_random: i32, @@ -380,7 +380,7 @@ impl StoryState { } } - pub fn visit_count_for_container(&mut self, container: &Rc) -> usize { + pub fn visit_count_for_container(&mut self, container: &Rc) -> i32 { if !container.visits_should_be_counted { // TODO @@ -414,7 +414,7 @@ impl StoryState { } let container_path_str = Object::get_path(container).to_string(); - self.turn_indices.insert(container_path_str, self.current_turn_index as usize); + self.turn_indices.insert(container_path_str, self.current_turn_index); } fn try_splitting_head_tail_whitespace(text: &str) -> Option> { @@ -780,7 +780,7 @@ impl StoryState { self.patch = None; } - fn apply_count_changes(&mut self, container: &str, new_count: usize, is_visit: bool) { + fn apply_count_changes(&mut self, container: &str, new_count: i32, is_visit: bool) { let counts = if is_visit {&mut self.visit_counts} else {&mut self.turn_indices}; counts.insert(container.to_string(), new_count); @@ -1022,7 +1022,7 @@ impl StoryState { self.output_stream_dirty(); } - pub fn visit_count_at_path_string(&self, path_string: &str) -> usize { + pub fn visit_count_at_path_string(&self, path_string: &str) -> i32 { let mut visit_count_out = None; if self.patch.is_some() { @@ -1173,9 +1173,8 @@ impl StoryState { self.current_flow.current_choices = json_read::jarray_to_runtime_obj_list(¤t_choices_obj.as_array().unwrap(), false)?.iter().map(|o| o.clone().into_any().downcast::().unwrap()).collect(); } - if let Some(j_choice_threads_obj) = j_object.get("choiceThreads") { - self.current_flow.load_flow_choice_threads(j_choice_threads_obj, self.main_content_container.clone())?; - } + let j_choice_threads_obj = j_object.get("choiceThreads"); + self.current_flow.load_flow_choice_threads(j_choice_threads_obj, self.main_content_container.clone())?; } self.output_stream_dirty(); @@ -1196,11 +1195,11 @@ impl StoryState { } if let Some(visit_counts_obj) = j_object.get("visitCounts") { - self.visit_counts = json_read::jobject_to_usize_hashmap(visit_counts_obj.as_object().ok_or_else(|| "Invalid visit counts object".to_string())?)?; + self.visit_counts = json_read::jobject_to_int_hashmap(visit_counts_obj.as_object().ok_or_else(|| "Invalid visit counts object".to_string())?)?; } if let Some(turn_indices_obj) = j_object.get("turnIndices") { - self.turn_indices = json_read::jobject_to_usize_hashmap(turn_indices_obj.as_object().ok_or_else(|| "Invalid turn indices object".to_string())?)?; + self.turn_indices = json_read::jobject_to_int_hashmap(turn_indices_obj.as_object().ok_or_else(|| "Invalid turn indices object".to_string())?)?; } if let Some(current_turn_index) = j_object.get("turnIdx") { diff --git a/tests/list_test.rs b/tests/list_test.rs index 3c24961..e8b8927 100644 --- a/tests/list_test.rs +++ b/tests/list_test.rs @@ -52,7 +52,17 @@ fn list_save_load_test() -> Result<(), String> { common::get_json_string("examples/inkfiles/lists/list-save-load.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); - //TODO + assert_eq!("a, x, c\n", &story.continue_maximally()?); + + let saved_state = story.get_state().to_json(); + + let mut story = Story::new(&json_string).unwrap(); + + story.get_state_mut().load_json(&saved_state)?; + + story.choose_path_string("elsewhere", true, None)?; + + assert_eq!("z\n", &story.continue_maximally()?); Ok(()) } diff --git a/tests/thread_test.rs b/tests/thread_test.rs index b91ae34..b0d561c 100644 --- a/tests/thread_test.rs +++ b/tests/thread_test.rs @@ -28,27 +28,31 @@ fn thread_test() -> Result<(), String> { } #[test] -fn thread_test_bug() -> Result<(), String> { - //TODO - - // let json_string = - // common::get_json_string("examples/inkfiles/threads/thread-bug.ink.json").unwrap(); - // let mut story = Story::new(&json_string).unwrap(); - // println!("{}", story.build_string_of_hierarchy()); +fn thread_test_bug() -> Result<(), String> { + let json_string = + common::get_json_string("examples/inkfiles/threads/thread-bug.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + println!("{}", story.build_string_of_hierarchy()); - // assert_eq!("Here is some gold. Do you want it?\n", story.continue_maximally()?); - // assert_eq!(2, story.get_current_choices().len()); - // assert_eq!("No", story.get_current_choices()[0].text); - // assert_eq!("Yes", story.get_current_choices()[1].text); - // story.choose_choice_index(0); + assert_eq!("Here is some gold. Do you want it?\n", story.continue_maximally()?); + assert_eq!(2, story.get_current_choices().len()); + assert_eq!("No", story.get_current_choices()[0].text); + assert_eq!("Yes", story.get_current_choices()[1].text); + + let save_string = story.get_state().to_json(); + println!("{}", save_string); + let mut story = Story::new(&json_string).unwrap(); + story.get_state_mut().load_json(&save_string)?; + + story.choose_choice_index(0); - // assert_eq!("No\nTry again!\n", story.continue_maximally()?); - // assert_eq!(2, story.get_current_choices().len()); - // assert_eq!("No", story.get_current_choices()[0].text); - // assert_eq!("Yes", story.get_current_choices()[1].text); - // story.choose_choice_index(1); + assert_eq!("No\nTry again!\n", story.continue_maximally()?); + assert_eq!(2, story.get_current_choices().len()); + assert_eq!("No", story.get_current_choices()[0].text); + assert_eq!("Yes", story.get_current_choices()[1].text); + story.choose_choice_index(1); - // assert_eq!("Yes\nYou win!\n", story.continue_maximally()?); + assert_eq!("Yes\nYou win!\n", story.continue_maximally()?); Ok(()) From 7cc08a3508d68c4d30f11d6a87eb0be47a4d9369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Sun, 1 Oct 2023 18:17:25 +0000 Subject: [PATCH 49/91] delete unnecessary unscaped method --- README.md | 1 - src/json_read.rs | 45 --------------------------------------------- 2 files changed, 46 deletions(-) diff --git a/README.md b/README.md index 494be7a..929c534 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,6 @@ Currently under development. This is the implementation status: ## TODO -- [ ] Eliminar código de unscape. - [ ] Al cargar los flows, el current flow hay que eliminarlo. - [ ] Optimize control command getname. Use static string array and address it by order. - [ ] Error handling diff --git a/src/json_read.rs b/src/json_read.rs index daed234..8b19164 100644 --- a/src/json_read.rs +++ b/src/json_read.rs @@ -22,8 +22,6 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) }, serde_json::Value::String(value) => { - //let unscaped = unscape_string(value)?; - //let str = unscaped.as_str(); let str = value.as_str(); // String value @@ -238,49 +236,6 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) } -fn unscape_string(text: &str) -> Result { - let mut sb = String::new(); - let mut offset = 0; - - while offset < text.len() { - let c = text.chars().nth(offset).unwrap(); - offset += 1; - - if c == '\\' { - // Escaped character - if offset >= text.len() { - return Err("Unexpected EOF while reading string".to_string()); - } - let escaped_char = text.chars().nth(offset).unwrap(); - offset += 1; - match escaped_char { - '"' | '\\' | '/' => sb.push(escaped_char), - 'n' => sb.push('\n'), - 't' => sb.push('\t'), - 'r' | 'b' | 'f' => { /* Ignore other control characters */ } - 'u' => { - // 4-digit Unicode - if offset + 4 >= text.len() { - return Err("Unexpected EOF while reading string".to_string()); - } - let digits = &text[offset..offset + 4]; - if let Ok(uchar) = u32::from_str_radix(digits, 16) { - sb.push(char::from_u32(uchar).ok_or(format!("Invalid Unicode escape character at offset {}", offset - 1))?); - offset += 4; - } else { - return Err(format!("Invalid Unicode escape character at offset {}", offset - 1)); - } - } - _ => return Err(format!("Invalid Unicode escape character at offset {}", offset - 1)), - } - } else { - sb.push(c); - } - } - - Ok(sb) -} - fn jarray_to_container(jarray: &Vec, name: Option) -> Result, String> { // Final object in the array is always a combination of // - named content From aa9f7835efb06b725776ce26512dd69cbbb4f476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Sun, 1 Oct 2023 18:40:40 +0000 Subject: [PATCH 50/91] Multiflow tests passed --- src/json_write_state.rs | 2 +- src/story.rs | 4 ++++ src/story_state.rs | 21 +++++++++++++++++++++ tests/multi_flow_test.rs | 30 +++++++++++++++++++++++++++++- 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/json_write_state.rs b/src/json_write_state.rs index 661a2d1..58a71c8 100644 --- a/src/json_write_state.rs +++ b/src/json_write_state.rs @@ -71,7 +71,7 @@ pub fn write_rtobject(o: Rc) -> serde_json::Value { let mut s = String::new(); if v.is_newline { - s.push_str("\n"); + s.push('\n'); } else { s.push('^'); s.push_str(&v.string); diff --git a/src/story.rs b/src/story.rs index 71467ec..aea5ca0 100644 --- a/src/story.rs +++ b/src/story.rs @@ -1911,5 +1911,9 @@ impl Story { panic!("Can't {}. Story is in the middle of a ContinueAsync(). Make more continue_async() calls or a single cont() call beforehand.", activity_str); } } + + pub fn remove_flow(&mut self, flow_name: &str) { + self.get_state_mut().remove_flow_internal(flow_name); + } } diff --git a/src/story_state.rs b/src/story_state.rs index 749ad8a..bc77bbf 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -1219,4 +1219,25 @@ impl StoryState { Ok(()) } + + pub(crate) fn remove_flow_internal(&mut self, flow_name: &str) -> Result<(), String> { + if flow_name.eq(DEFAULT_FLOW_NAME) {return Err("Cannot destroy default flow".to_owned());} + + // If we're currently in the flow that's being removed, switch back to default + if self.current_flow.name.eq(flow_name) { + self.switch_to_default_flow_internal(); + } + + self.named_flows.as_mut().unwrap().remove(flow_name); + self.alive_flow_names_dirty = true; + + Ok(()) + } + + fn switch_to_default_flow_internal(&mut self) { + match self.named_flows { + Some(_) => self.switch_flow_internal(DEFAULT_FLOW_NAME), + None => (), + } + } } \ No newline at end of file diff --git a/tests/multi_flow_test.rs b/tests/multi_flow_test.rs index 42b4400..d6ec369 100644 --- a/tests/multi_flow_test.rs +++ b/tests/multi_flow_test.rs @@ -53,7 +53,35 @@ fn multiflow_save_load_threads() -> Result<(), String> { assert_eq!("Thread 1 red choice", story.get_current_choices()[0].text); // Save/load test - // let saved = story.getState().toJson(); + let saved = story.get_state().to_json(); + + // Test choice before reloading state before resetting + story.choose_choice_index(0); + assert_eq!("Thread 1 red choice\nAfter thread 1 choice (red)\n", story.continue_maximally()?); + let mut story = Story::new(&json_string).unwrap(); + + // Load to pre-choice: still red, choose second choice + story.get_state_mut().load_json(&saved)?; + + story.choose_choice_index(1); + assert_eq!("Thread 2 red choice\nAfter thread 2 choice (red)\n", story.continue_maximally()?); + + // Load: switch to blue, choose 1 + story.get_state_mut().load_json(&saved)?; + story.switch_flow("Blue Flow"); + story.choose_choice_index(0); + assert_eq!("Thread 1 blue choice\nAfter thread 1 choice (blue)\n", story.continue_maximally()?); + + // Load: switch to blue, choose 2 + story.get_state_mut().load_json(&saved)?; + story.switch_flow("Blue Flow"); + story.choose_choice_index(1); + assert_eq!("Thread 2 blue choice\nAfter thread 2 choice (blue)\n", story.continue_maximally()?); + + // Remove active blue flow, should revert back to global flow + story.remove_flow("Blue Flow"); + assert_eq!("Default line 2\n", story.cont()?); + Ok(()) } From 9cf3c7e8835c0ee6bc6e616f0277dd500a22c7e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Sun, 1 Oct 2023 18:58:50 +0000 Subject: [PATCH 51/91] remove current flow from named flows when loading state --- README.md | 1 - src/story_state.rs | 14 +++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 929c534..125f460 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,6 @@ Currently under development. This is the implementation status: ## TODO -- [ ] Al cargar los flows, el current flow hay que eliminarlo. - [ ] Optimize control command getname. Use static string array and address it by order. - [ ] Error handling - [ ] Cache components string in Path diff --git a/src/story_state.rs b/src/story_state.rs index bc77bbf..20e25fb 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -495,7 +495,7 @@ impl StoryState { let mut include_in_output = true; // New glue, so chomp away any whitespace from the end of the stream - if let Ok(_) = glue { + if glue.is_ok() { self.trim_newlines_from_output_stream(); include_in_output = true; } @@ -512,7 +512,7 @@ impl StoryState { let cs = self.get_callstack().borrow(); let curr_el = cs.get_current_element(); if curr_el.push_pop_type == PushPopType::Function { - function_trim_index = curr_el.function_start_in_output_stream as i32; + function_trim_index = curr_el.function_start_in_output_stream; } } @@ -526,7 +526,7 @@ impl StoryState { break; } - } else if let Some(_) = o.as_ref().as_any().downcast_ref::() { + } else if o.as_ref().as_any().is::() { glue_trim_index = i as i32; break; } @@ -1144,12 +1144,13 @@ impl StoryState { } } - if let Some(named_flows) = &self.named_flows { + if let Some(named_flows) = &mut self.named_flows { if named_flows.len() > 1 { if let Some(current_flow_name) = j_object.get("currentFlowName") { if let Some(curr_flow_name) = current_flow_name.as_str() { if let Some(curr_flow) = named_flows.get(curr_flow_name) { self.current_flow = curr_flow.clone(); + named_flows.remove(curr_flow_name); } } } @@ -1235,9 +1236,8 @@ impl StoryState { } fn switch_to_default_flow_internal(&mut self) { - match self.named_flows { - Some(_) => self.switch_flow_internal(DEFAULT_FLOW_NAME), - None => (), + if self.named_flows.is_some() { + self.switch_flow_internal(DEFAULT_FLOW_NAME); } } } \ No newline at end of file From abaf7019a690e8911e289178510f26a3dec3c20f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Sun, 1 Oct 2023 20:56:18 +0000 Subject: [PATCH 52/91] Added StoryError. Initial error handling. --- README.md | 1 + src/callstack.rs | 20 +++--- src/flow.rs | 12 ++-- src/json_read.rs | 23 ++++--- src/lib.rs | 1 + src/story.rs | 128 ++++++++++++++++++------------------ src/story_error.rs | 20 ++++++ src/story_state.rs | 46 ++++++------- src/variables_state.rs | 8 +-- src/void.rs | 5 +- tests/basic_text_test.rs | 6 +- tests/choice_test.rs | 34 +++++----- tests/common/mod.rs | 8 +-- tests/conditional_test.rs | 42 ++++++------ tests/divert_test.rs | 12 ++-- tests/function_test.rs | 20 +++--- tests/gather_test.rs | 14 ++-- tests/glue_test.rs | 12 ++-- tests/knot_test.rs | 20 +++--- tests/list_test.rs | 20 +++--- tests/misc_test.rs | 10 +-- tests/multi_flow_test.rs | 6 +- tests/runtime_test.rs | 14 ++-- tests/stitch_test.rs | 10 +-- tests/tag_test.rs | 10 +-- tests/thread_test.rs | 6 +- tests/tunnel_test.rs | 4 +- tests/variable_test.rs | 10 +-- tests/variable_text_test.rs | 12 ++-- 29 files changed, 275 insertions(+), 259 deletions(-) create mode 100644 src/story_error.rs diff --git a/README.md b/README.md index 125f460..cef7a44 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Currently under development. This is the implementation status: ## TODO +- [ ] Variable observers. - [ ] Optimize control command getname. Use static string array and address it by order. - [ ] Error handling - [ ] Cache components string in Path diff --git a/src/callstack.rs b/src/callstack.rs index 63f7e94..860974d 100644 --- a/src/callstack.rs +++ b/src/callstack.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, rc::Rc}; use serde_json::{Map, json}; -use crate::{pointer::{Pointer, self}, push_pop::PushPopType, container::{Container, self}, value::Value, object::Object, json_read, json_write_state, path::Path, story::Story}; +use crate::{pointer::{Pointer, self}, push_pop::PushPopType, container::{Container, self}, value::Value, object::Object, json_read, json_write_state, path::Path, story::Story, story_error::StoryError}; pub struct Element { pub current_pointer: Pointer, @@ -50,16 +50,16 @@ impl Thread { } } - pub fn from_json(main_content_container: &Rc, j_obj: &Map) -> Result { + pub fn from_json(main_content_container: &Rc, j_obj: &Map) -> Result { let mut thread = Thread::new(); - thread.thread_index = j_obj.get("threadIndex").and_then(|i| i.as_i64()).ok_or("Invalid thread index")? as usize; + thread.thread_index = j_obj.get("threadIndex").and_then(|i| i.as_i64()).ok_or(StoryError::BadJson("Invalid thread index".to_owned()))? as usize; if let Some(j_thread_callstack) = j_obj.get("callstack").and_then(|callstack| callstack.as_array()) { for j_el_tok in j_thread_callstack.iter() { if let Some(j_element_obj) = j_el_tok.as_object() { - let push_pop_type = PushPopType::from_value(j_element_obj.get("type").and_then(|t| t.as_i64()).ok_or("Invalid push/pop type")? as usize); + let push_pop_type = PushPopType::from_value(j_element_obj.get("type").and_then(|t| t.as_i64()).ok_or(StoryError::BadJson("Invalid push/pop type".to_owned()))? as usize); let mut pointer = pointer::NULL.clone(); @@ -68,7 +68,7 @@ impl Thread { let thread_pointer_result = main_content_container.content_at_path(&Path::new_with_components_string (current_container_path_str), 0, -1); pointer.container = thread_pointer_result.container(); - let pointer_index = j_element_obj.get("idx").and_then(|i| i.as_i64()).ok_or("Invalid pointer index")? as i32; + let pointer_index = j_element_obj.get("idx").and_then(|i| i.as_i64()).ok_or(StoryError::BadJson("Invalid pointer index".to_owned()))? as i32; pointer.index = pointer_index; // TODO @@ -209,12 +209,12 @@ impl CallStack { return self.threads.len() > 1 && !self.element_is_evaluate_from_game(); } - pub fn pop_thread(&mut self) -> Result<(), String> { + pub fn pop_thread(&mut self) -> Result<(), StoryError> { if self.can_pop_thread() { self.threads.remove(self.threads.len() - 1); Ok(()) } else { - Err("Can't pop thread".to_owned()) + Err(StoryError::InvalidStoryState("Can't pop thread".to_owned())) } } @@ -297,7 +297,7 @@ impl CallStack { value: Rc, declare_new: bool, mut context_index: i32, - ) -> Result<(), String> { + ) -> Result<(), StoryError> { if context_index == -1 { context_index = self.get_current_element_index() + 1; } @@ -305,7 +305,7 @@ impl CallStack { let context_element = self.get_callstack_mut().get_mut((context_index - 1) as usize).unwrap(); if !declare_new && !context_element.temporary_variables.contains_key(&name) { - return Err(format!("Could not find temporary variable to set: {}", name)); + return Err(StoryError::InvalidStoryState(format!("Could not find temporary variable to set: {}", name))); } let old_value = context_element.temporary_variables.get(&name).cloned(); @@ -380,7 +380,7 @@ impl CallStack { return None; } - pub fn load_json(&mut self, main_content_container: &Rc, j_obj: &Map) -> Result<(), String> { + pub fn load_json(&mut self, main_content_container: &Rc, j_obj: &Map) -> Result<(), StoryError> { self.threads.clear(); diff --git a/src/flow.rs b/src/flow.rs index 3fe6adc..8095349 100644 --- a/src/flow.rs +++ b/src/flow.rs @@ -2,7 +2,7 @@ use std::{rc::Rc, cell::RefCell}; use serde_json::Map; -use crate::{callstack::{CallStack, Thread}, choice::Choice, object::RTObject, container::Container, json_write_state, json_read}; +use crate::{callstack::{CallStack, Thread}, choice::Choice, object::RTObject, container::Container, json_write_state, json_read, story_error::StoryError}; #[derive(Clone)] pub struct Flow { @@ -22,15 +22,15 @@ impl Flow { } } - pub fn from_json(name: &str, main_content_container: Rc, j_obj: &Map) -> Result { + pub fn from_json(name: &str, main_content_container: Rc, j_obj: &Map) -> Result { let mut flow = Self { name: name.to_string(), callstack: Rc::new(RefCell::new(CallStack::new(main_content_container.clone()))), - output_stream: json_read::jarray_to_runtime_obj_list(j_obj.get("outputStream").ok_or("outputStream not found.")?.as_array().unwrap(), false)?, - current_choices: json_read::jarray_to_runtime_obj_list(j_obj.get("currentChoices").ok_or("currentChoices not found.")?.as_array().unwrap(), false)?.iter().map(|o| o.clone().into_any().downcast::().unwrap()).collect::>>(), + output_stream: json_read::jarray_to_runtime_obj_list(j_obj.get("outputStream").ok_or(StoryError::BadJson("outputStream not found.".to_owned()))?.as_array().unwrap(), false)?, + current_choices: json_read::jarray_to_runtime_obj_list(j_obj.get("currentChoices").ok_or(StoryError::BadJson("currentChoices not found.".to_owned()))?.as_array().unwrap(), false)?.iter().map(|o| o.clone().into_any().downcast::().unwrap()).collect::>>(), }; - flow.callstack.borrow_mut().load_json(&main_content_container, j_obj.get("callstack").ok_or("loading callstack")?.as_object().unwrap())?; + flow.callstack.borrow_mut().load_json(&main_content_container, j_obj.get("callstack").ok_or(StoryError::BadJson("loading callstack".to_owned()))?.as_object().unwrap())?; let j_choice_threads = j_obj.get("choiceThreads"); flow.load_flow_choice_threads(j_choice_threads, main_content_container)?; @@ -75,7 +75,7 @@ impl Flow { serde_json::Value::Object(flow) } - pub fn load_flow_choice_threads(&mut self, j_choice_threads: Option<&serde_json::Value>, main_content_container: Rc) -> Result<(), String>{ + pub fn load_flow_choice_threads(&mut self, j_choice_threads: Option<&serde_json::Value>, main_content_container: Rc) -> Result<(), StoryError>{ for choice in self.current_choices.iter_mut() { self.callstack.borrow().get_thread_with_index(*choice.original_thread_index.borrow()) .map(|o| choice.set_thread_at_generation(o.copy())) diff --git a/src/json_read.rs b/src/json_read.rs index 8b19164..46f7992 100644 --- a/src/json_read.rs +++ b/src/json_read.rs @@ -4,10 +4,10 @@ use serde_json::Map; use crate::{ container::Container, - object::{self, RTObject}, control_command::ControlCommand, value::Value, glue::Glue, path::Path, choice_point::ChoicePoint, choice::Choice, push_pop::PushPopType, divert::Divert, variable_assigment::VariableAssignment, void::Void, variable_reference::VariableReference, native_function_call::NativeFunctionCall, tag::Tag, ink_list::InkList, ink_list_item::InkListItem, list_definitions_origin::ListDefinitionsOrigin, list_definition::ListDefinition, + object::{self, RTObject}, control_command::ControlCommand, value::Value, glue::Glue, path::Path, choice_point::ChoicePoint, choice::Choice, push_pop::PushPopType, divert::Divert, variable_assigment::VariableAssignment, void::Void, variable_reference::VariableReference, native_function_call::NativeFunctionCall, tag::Tag, ink_list::InkList, ink_list_item::InkListItem, list_definitions_origin::ListDefinitionsOrigin, list_definition::ListDefinition, story_error::StoryError, }; -pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) -> Result, String> { +pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) -> Result, StoryError> { match token { serde_json::Value::Null => Ok(Rc::new(object::Null::new())), serde_json::Value::Bool(value) => Ok(Rc::new(Value::new_bool(value.to_owned()))), @@ -52,7 +52,7 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) if "void".eq(str) {return Ok(Rc::new(Void::new()));} - Err(format!("Failed to convert token to runtime RTObject: {}", &token.to_string())) + Err(StoryError::BadJson(format!("Failed to convert token to runtime RTObject: {}", &token.to_string()))) }, serde_json::Value::Array(value) => Ok(jarray_to_container(value, name)?), serde_json::Value::Object(obj) => { @@ -230,13 +230,13 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) return jobject_to_choice(obj); } - Err(format!("Failed to convert token to runtime RTObject: {}", &token.to_string())) + Err(StoryError::BadJson(format!("Failed to convert token to runtime RTObject: {}", &token.to_string()))) }, } } -fn jarray_to_container(jarray: &Vec, name: Option) -> Result, String> { +fn jarray_to_container(jarray: &Vec, name: Option) -> Result, StoryError> { // Final object in the array is always a combination of // - named content // - a "#f" key with the countFlags @@ -270,7 +270,7 @@ fn jarray_to_container(jarray: &Vec, name: Option) -> Ok(container) } -pub fn jarray_to_runtime_obj_list(jarray: &Vec, skip_last: bool) -> Result>, String> { +pub fn jarray_to_runtime_obj_list(jarray: &Vec, skip_last: bool) -> Result>, StoryError> { let mut count = jarray.len(); if skip_last { @@ -288,7 +288,7 @@ pub fn jarray_to_runtime_obj_list(jarray: &Vec, skip_last: bo Ok(list) } -fn jobject_to_choice(obj: &Map) -> Result, String> { +fn jobject_to_choice(obj: &Map) -> Result, StoryError> { let text = obj.get("text").unwrap().as_str().unwrap(); let index = obj.get("index").unwrap().as_u64().unwrap() as usize; let source_path = obj.get("originalChoicePath").unwrap().as_str().unwrap(); @@ -298,7 +298,7 @@ fn jobject_to_choice(obj: &Map) -> Result Result { +pub fn jtoken_to_list_definitions(def: &serde_json::Value) -> Result { let mut all_defs: Vec = Vec::with_capacity(0); @@ -317,7 +317,7 @@ pub fn jtoken_to_list_definitions(def: &serde_json::Value) -> Result) -> Result>, String> { +pub(crate) fn jobject_to_hashmap_values(jobj: &Map) -> Result>, StoryError> { let mut dict: HashMap> = HashMap::new(); for (k, v) in jobj.iter() { @@ -327,7 +327,7 @@ pub(crate) fn jobject_to_hashmap_values(jobj: &Map) - Ok(dict) } -pub(crate) fn jobject_to_hashmap_rtobjects(jobj: &Map) -> Result>, String> { +pub(crate) fn jobject_to_hashmap_rtobjects(jobj: &Map) -> Result>, StoryError> { let mut dict: HashMap> = HashMap::new(); @@ -338,11 +338,10 @@ pub(crate) fn jobject_to_hashmap_rtobjects(jobj: &Map Ok(dict) } -pub(crate) fn jobject_to_int_hashmap(jobj: &Map) -> Result, String> { +pub(crate) fn jobject_to_int_hashmap(jobj: &Map) -> Result, StoryError> { let mut dict: HashMap = HashMap::new(); for (k, v) in jobj.iter() { - println!("{}", v); dict.insert(k.clone(), v.as_i64().unwrap() as i32); } diff --git a/src/lib.rs b/src/lib.rs index 44a37c1..4f71207 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ pub mod story; pub mod value_type; +pub mod story_error; mod json_read; mod json_write_state; mod object; diff --git a/src/story.rs b/src/story.rs index aea5ca0..66bf78c 100644 --- a/src/story.rs +++ b/src/story.rs @@ -9,7 +9,7 @@ use crate::{ error::ErrorType, json_read, push_pop::PushPopType, - story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, search_result::SearchResult, variable_assigment::VariableAssignment, native_function_call::NativeFunctionCall, variable_reference::VariableReference, list_definitions_origin::ListDefinitionsOrigin, ink_list::InkList, ink_list_item::InkListItem, variables_state::VariablesState, + story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, search_result::SearchResult, variable_assigment::VariableAssignment, native_function_call::NativeFunctionCall, variable_reference::VariableReference, list_definitions_origin::ListDefinitionsOrigin, ink_list::InkList, ink_list_item::InkListItem, variables_state::VariablesState, story_error::StoryError, value_type::ValueType, }; pub const INK_VERSION_CURRENT: i32 = 21; @@ -37,27 +37,27 @@ pub struct Story { } impl Story { - pub fn new(json_string: &str) -> Result { + pub fn new(json_string: &str) -> Result { let json: serde_json::Value = match serde_json::from_str(json_string) { Ok(value) => value, - Err(_) => return Err("Story not in JSON format.".to_owned()), + Err(_) => return Err(StoryError::BadJson("Story not in JSON format.".to_owned())), }; let version_opt = json.get("inkVersion"); if version_opt.is_none() || !version_opt.unwrap().is_number() { - return Err( + return Err(StoryError::BadJson( "ink version number not found. Are you sure it's a valid .ink.json file?" - .to_string(), + .to_owned()), ); } let version: i32 = version_opt.unwrap().as_i64().unwrap().try_into().unwrap(); if version > INK_VERSION_CURRENT { - return Err("Version of ink used to build story was newer than the current version of the engine".to_owned()); + return Err(StoryError::BadJson("Version of ink used to build story was newer than the current version of the engine".to_owned())); } else if version < INK_VERSION_MINIMUM_COMPATIBLE { - return Err("Version of ink used to build story is too old to be loaded by this version of the engine".to_owned()); + return Err(StoryError::BadJson("Version of ink used to build story is too old to be loaded by this version of the engine".to_owned())); } else if version != INK_VERSION_CURRENT { log::debug!("WARNING: Version of ink used to build story doesn't match current version of engine. Non-critical, but recommend synchronising."); } @@ -65,9 +65,9 @@ impl Story { let root_token = match json.get("root") { Some(value) => value, None => { - return Err( + return Err(StoryError::BadJson( "Root node for ink not found. Are you sure it's a valid .ink.json file?" - .to_string(), + .to_owned()), ) } }; @@ -76,8 +76,8 @@ impl Story { Some(def) => Rc::new(json_read::jtoken_to_list_definitions(def)?), None => { return Err( - "List Definitions node for ink not found. Are you sure it's a valid .ink.json file?" - .to_string(), + StoryError::BadJson("List Definitions node for ink not found. Are you sure it's a valid .ink.json file?" + .to_owned()), ) } }; @@ -87,7 +87,7 @@ impl Story { let main_content_container = main_content_container.into_any().downcast::(); if main_content_container.is_err() { - return Err("Root node for ink is not a container?".to_owned()); + return Err(StoryError::BadJson("Root node for ink is not a container?".to_owned())); }; let mut story = Story { @@ -163,12 +163,12 @@ impl Story { self.get_state().can_continue() } - pub fn cont(&mut self) -> Result { + pub fn cont(&mut self) -> Result { self.continue_async(0.0)?; Ok(self.get_current_text()) } - pub fn continue_maximally(&mut self) -> Result { + pub fn continue_maximally(&mut self) -> Result { self.if_async_we_cant("continue_maximally"); let mut sb = String::new(); @@ -180,7 +180,7 @@ impl Story { Ok(sb) } - pub fn continue_async(&mut self, millisecs_limit_async: f32) -> Result<(), String> { + pub fn continue_async(&mut self, millisecs_limit_async: f32) -> Result<(), StoryError> { // TODO: if (!hasValidatedExternals) validateExternalBindings(); self.continue_internal(millisecs_limit_async)?; @@ -188,7 +188,7 @@ impl Story { Ok(()) } - fn continue_internal(&mut self, millisecs_limit_async: f32) -> Result<(), String> { + fn continue_internal(&mut self, millisecs_limit_async: f32) -> Result<(), StoryError> { let is_async_time_limited = millisecs_limit_async > 0.0; self.recursive_continue_count += 1; @@ -200,7 +200,7 @@ impl Story { self.async_continue_active = is_async_time_limited; if !self.can_continue() { return Err( - "Can't continue - should check can_continue before calling Continue".to_owned(), + StoryError::InvalidStoryState("Can't continue - should check can_continue before calling Continue".to_owned()), ); } @@ -387,7 +387,7 @@ impl Story { sb.push_str(self.get_state().get_current_warnings()[0].to_string().as_str()); } - return Err(sb); + return Err(StoryError::InvalidStoryState(sb)); } } } @@ -395,9 +395,9 @@ impl Story { Ok(()) } - fn continue_single_step(&mut self) -> Result { + fn continue_single_step(&mut self) -> Result { // Run main step function (walks through content) - self.step(); + self.step()?; // Run out of content and we have a default invisible choice that we can follow? if !self.can_continue() && !self.get_state().get_callstack().borrow().element_is_evaluate_from_game() { @@ -503,14 +503,14 @@ impl Story { todo!() } - fn step(&mut self) { + fn step(&mut self) -> Result<(), StoryError> { let mut should_add_to_stream = true; // Get current content let mut pointer = self.get_state().get_current_pointer().clone(); if pointer.is_null() { - return; + return Ok(()); } // Step directly to the first element of content in a container (if @@ -558,11 +558,11 @@ impl Story { // that was diverted to rather than called as a function) let mut current_content_obj = pointer.resolve(); - let is_logic_or_flow_control = self.perform_logic_and_flow_control(¤t_content_obj); + let is_logic_or_flow_control = self.perform_logic_and_flow_control(¤t_content_obj)?; // Has flow been forced to end by flow control above? if self.get_state().get_current_pointer().is_null() { - return; + return Ok(()); } if is_logic_or_flow_control { @@ -633,6 +633,8 @@ impl Story { } } + Ok(()) + } fn try_follow_default_invisible_choice(&mut self) { @@ -753,12 +755,12 @@ impl Story { self.get_state_mut().get_variables_state_mut() } - fn perform_logic_and_flow_control(&mut self, content_obj: &Option>) -> bool { + fn perform_logic_and_flow_control(&mut self, content_obj: &Option>) -> Result { let content_obj = match content_obj { Some(content_obj) => { content_obj.clone() }, - None => return false, + None => return Ok(false), }; // Divert @@ -766,7 +768,7 @@ impl Story { if current_divert.is_conditional { let o = self.get_state_mut().pop_evaluation_stack(); if !self.is_truthy(o) { - return true; + return Ok(true); } } @@ -777,24 +779,22 @@ impl Story { let p = Self::pointer_at_path(&self.main_content_container, target); self.get_state_mut().set_diverted_pointer(p); } else { - // TODO - // let int_content = var_contents.downcast_ref::(); - // let error_message = format!( - // "Tried to divert to a target from a variable, but the variable ({}) didn't contain a divert target, it ", - // var_name - // ); - // let error_message = if let Some(int_content) = int_content { - // if int_content.value == 0 { - // format!("{}was empty/null (the value 0).", error_message) - // } else { - // format!("{}contained '{}'.", error_message, var_contents) - // } - // } else { - // error_message - // }; - - // error(error_message); - panic!(); + let error_message = format!( + "Tried to divert to a target from a variable, but the variable ({}) didn't contain a divert target, it ", + var_name.as_ref().unwrap() + ); + + let error_message = if let ValueType::Int(int_content) = var_contents.value { + if int_content == 0 { + format!("{}was empty/null (the value 0).", error_message) + } else { + format!("{}contained '{}'.", error_message, var_contents) + } + } else { + error_message + }; + + return Err(StoryError::InvalidStoryState(error_message)); } } else { // TODO @@ -806,7 +806,7 @@ impl Story { } } else if current_divert.is_external { //call_external_function(¤t_divert.get_target_path_string(), current_divert.get_external_args()); - return true; + return Ok(true); } else { self.get_state_mut().set_diverted_pointer(current_divert.get_target_pointer()); } @@ -821,7 +821,7 @@ impl Story { // error(format!("Divert resolution failed: {:?}", current_divert)); } - return true; + return Ok(true); } if let Some(eval_command) = content_obj.as_ref().as_any().downcast_ref::() { @@ -886,7 +886,7 @@ impl Story { } if self.get_state_mut().try_exit_function_evaluation_from_game() { - return true; + return Ok(true); } else if self.get_state().get_callstack().borrow().get_current_element().push_pop_type != pop_type || !self.get_state().get_callstack().borrow().can_pop() { @@ -1290,7 +1290,7 @@ impl Story { }, } - return true; + return Ok(true); } // Variable assignment @@ -1305,7 +1305,7 @@ impl Story { let assigned_val = assigned_val.into_any().downcast::().unwrap(); self.get_state_mut().get_variables_state_mut().assign( var_ass, assigned_val); - return true; + return Ok(true); } // Variable reference @@ -1337,7 +1337,7 @@ impl Story { self.get_state_mut().push_evaluation_stack(found_value.unwrap()); - return true; + return Ok(true); } // Native function call @@ -1347,11 +1347,11 @@ impl Story { let result = func.call(func_params); self.get_state_mut().push_evaluation_stack(result); - return true; + return Ok(true); } - false + Ok(false) } fn next_content(&mut self) { @@ -1706,11 +1706,11 @@ impl Story { } // TODO: The result and the args should be an object not a String - pub fn evaluate_function(&mut self, func_name: &str, args: Option<&Vec>, text_output: &mut String) -> Result, String> { + pub fn evaluate_function(&mut self, func_name: &str, args: Option<&Vec>, text_output: &mut String) -> Result, StoryError> { self.if_async_we_cant("evaluate a function"); if func_name.trim().is_empty() { - return Err("Function is empty or white space.".to_owned()); + return Err(StoryError::InvalidStoryState("Function is empty or white space.".to_owned())); } // Get the content that we need to run @@ -1720,7 +1720,7 @@ impl Story { e.push_str(func_name); e.push('\''); - return Err(e); + return Err(StoryError::BadArgument(e)); } // Snapshot the output stream @@ -1742,9 +1742,7 @@ impl Story { self.get_state_mut().reset_output(Some(output_stream_before)); // Finish evaluation, and see whether anything was produced - let result = self.get_state_mut().complete_function_evaluation_from_game(); - - return result; + self.get_state_mut().complete_function_evaluation_from_game() } fn knot_container_with_name(&self, name: &str) -> Option> { @@ -1797,15 +1795,15 @@ impl Story { panic!("Should never reach here"); } - pub fn get_global_tags(&self) -> Result, String> { + pub fn get_global_tags(&self) -> Result, StoryError> { self.tags_at_start_of_flow_container_with_path_string("") } - pub fn tags_for_content_at_path(&self, path: &str) -> Result, String> { + pub fn tags_for_content_at_path(&self, path: &str) -> Result, StoryError> { self.tags_at_start_of_flow_container_with_path_string(path) } - fn tags_at_start_of_flow_container_with_path_string(&self, path_string: &str) -> Result, String> { + fn tags_at_start_of_flow_container_with_path_string(&self, path_string: &str) -> Result, StoryError> { let path = Path::new_with_components_string(Some(path_string)); // Expected to be global story, knot, or stitch @@ -1839,7 +1837,7 @@ impl Story { tags.push(string_value.string.clone()); } else { return Err( - "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()".to_owned(), + StoryError::InvalidStoryState("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()".to_owned()), ); } } else { @@ -1861,7 +1859,7 @@ impl Story { return self.get_state_mut().get_current_tags(); } - pub fn choose_path_string(&mut self, path: &str, reset_call_stack: bool, args: Option<&Vec>) -> Result<(), String> { + pub fn choose_path_string(&mut self, path: &str, reset_call_stack: bool, args: Option<&Vec>) -> Result<(), StoryError> { self.if_async_we_cant("call ChoosePathString right now"); if reset_call_stack { @@ -1880,7 +1878,7 @@ impl Story { // Err("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()); - return Err("Story was running a function".to_owned()); + return Err(StoryError::InvalidStoryState("Story was running a function".to_owned())); } } diff --git a/src/story_error.rs b/src/story_error.rs new file mode 100644 index 0000000..b3cad4e --- /dev/null +++ b/src/story_error.rs @@ -0,0 +1,20 @@ +use core::fmt; + +#[derive(Debug)] +pub enum StoryError { + InvalidStoryState(String), + BadJson(String), + BadArgument(String), +} + +impl std::error::Error for StoryError {} + +impl fmt::Display for StoryError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + StoryError::InvalidStoryState(desc) => write!(f, "Invalid story state: {}", desc), + StoryError::BadJson(desc) => write!(f, "Error parsing JSON: {}", desc), + StoryError::BadArgument(arg) => write!(f, "Bad argument: {}", arg), + } + } +} diff --git a/src/story_state.rs b/src/story_state.rs index 20e25fb..c0b2699 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -2,7 +2,7 @@ use std::{rc::Rc, cell::RefCell, collections::HashMap}; -use crate::{pointer::{Pointer, self}, callstack::CallStack, flow::Flow, variables_state::VariablesState, choice::Choice, object::{RTObject, Object}, value::Value, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}, container::Container, state_patch::StatePatch, story::{Story, INK_VERSION_CURRENT}, path::Path, void::Void, tag::Tag, list_definitions_origin::ListDefinitionsOrigin, value_type::ValueType, json_write_state, json_read}; +use crate::{pointer::{Pointer, self}, callstack::CallStack, flow::Flow, variables_state::VariablesState, choice::Choice, object::{RTObject, Object}, value::Value, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}, container::Container, state_patch::StatePatch, story::{Story, INK_VERSION_CURRENT}, path::Path, void::Void, tag::Tag, list_definitions_origin::ListDefinitionsOrigin, value_type::ValueType, json_write_state, json_read, story_error::StoryError}; use rand::Rng; use serde_json::{json, Map}; @@ -883,7 +883,7 @@ impl StoryState { self.evaluation_stack.last() } - pub fn start_function_evaluation_from_game(&mut self, func_container: Rc, arguments: Option<&Vec>) -> Result<(), String> { + pub fn start_function_evaluation_from_game(&mut self, func_container: Rc, arguments: Option<&Vec>) -> Result<(), StoryError> { self.get_callstack().borrow_mut().push(PushPopType::FunctionEvaluationFromGame, self.evaluation_stack.len(), 0); self.get_callstack().borrow_mut().get_current_element_mut().current_pointer = Pointer::start_of(func_container); @@ -892,7 +892,7 @@ impl StoryState { Ok(()) } - pub fn pass_arguments_to_evaluation_stack(&mut self, arguments: Option<&Vec>) -> Result<(), String> { + pub fn pass_arguments_to_evaluation_stack(&mut self, arguments: Option<&Vec>) -> Result<(), StoryError> { // Pass arguments onto the evaluation stack if let Some(arguments) = arguments { for arg in arguments { @@ -918,12 +918,12 @@ impl StoryState { Ok(()) } - pub fn complete_function_evaluation_from_game(&mut self) -> Result, String> { + pub fn complete_function_evaluation_from_game(&mut self) -> Result, StoryError> { if self.get_callstack().borrow().get_current_element().push_pop_type != PushPopType::FunctionEvaluationFromGame { // TODO // return Err(format!("Expected external function evaluation to be complete. Stack trace: {}", getCallStack().getCallStackTrace()); - return Err("Expected external function evaluation to be complete. Stack trace".to_owned()); + return Err(StoryError::InvalidStoryState("Expected external function evaluation to be complete. Stack trace".to_owned())); } let original_evaluation_stack_height = self.get_callstack().borrow().get_current_element().evaluation_stack_height_when_pushed; @@ -1043,10 +1043,10 @@ impl StoryState { self.write_json().to_string() } - pub fn load_json(&mut self, save_string: &str) -> Result<(), String> { + pub fn load_json(&mut self, save_string: &str) -> Result<(), StoryError> { match serde_json::from_str(save_string) { Ok(value) => self.load_json_obj(value), - Err(_) => Err("State not in JSON format.".to_owned()), + Err(_) => Err(StoryError::BadJson("State not in JSON format.".to_owned())), } } @@ -1092,26 +1092,26 @@ impl StoryState { serde_json::Value::Object(obj) } - fn load_json_obj(&mut self, j_object: serde_json::Value) -> Result<(), String> { + fn load_json_obj(&mut self, j_object: serde_json::Value) -> Result<(), StoryError> { let j_save_version = match j_object.get("inkSaveVersion") { Some(version) => version, - None => return Err("ink save format incorrect, can't load.".to_string()), + None => return Err(StoryError::BadJson("ink save format incorrect, can't load.".to_owned())), }; if let Some(version) = j_save_version.as_i64() { if version < MIN_COMPATIBLE_LOAD_VERSION as i64 { - return Err(format!( + return Err(StoryError::BadJson(format!( "Ink save format isn't compatible with the current version (saw '{}', but minimum is {}), so can't load.", version, MIN_COMPATIBLE_LOAD_VERSION - )); + ))); } } // Flows: Always exists in latest format (even if there's just one default) // but this dictionary doesn't exist in prev format if let Some(flows_obj) = j_object.get("flows") { - let flows_obj_dict = flows_obj.as_object().ok_or_else(|| "Invalid flows object".to_string())?; + let flows_obj_dict = flows_obj.as_object().ok_or_else(|| StoryError::BadJson("Invalid flows object".to_string()))?; // Single default flow if flows_obj_dict.len() == 1 { @@ -1129,7 +1129,7 @@ impl StoryState { // Load up each flow (there may only be one) for (named_flow_name, named_flow_obj) in flows_obj_dict.iter() { let name = named_flow_name.clone(); - let flow_obj = named_flow_obj.as_object().ok_or_else(|| "Invalid flow object".to_string())?; + let flow_obj = named_flow_obj.as_object().ok_or_else(|| StoryError::BadJson("Invalid flow object".to_string()))?; // Load up this flow using JSON data let flow = Flow::from_json(&name, self.main_content_container.clone(), flow_obj)?; @@ -1139,7 +1139,7 @@ impl StoryState { } else { self.named_flows .as_mut() - .ok_or_else(|| "Named flows should be initialized".to_string())? + .ok_or_else(|| StoryError::BadJson("Named flows should be initialized".to_string()))? .insert(name, flow); } } @@ -1164,7 +1164,7 @@ impl StoryState { self.current_flow.name = "default".to_string(); // Replace with the default flow name self.current_flow .callstack - .borrow_mut().load_json(&self.main_content_container, j_object.get("callstackThreads").and_then(|o| o.as_object()).ok_or("loading callstack threads")?); + .borrow_mut().load_json(&self.main_content_container, j_object.get("callstackThreads").and_then(|o| o.as_object()).ok_or(StoryError::BadJson("loading callstack threads".to_owned()))?); if let Some(output_stream_obj) = j_object.get("outputStream") { self.current_flow.output_stream = json_read::jarray_to_runtime_obj_list(&output_stream_obj.as_array().unwrap(), false)?; @@ -1182,7 +1182,7 @@ impl StoryState { self.alive_flow_names_dirty = true; if let Some(variables_state_obj) = j_object.get("variablesState") { - self.variables_state.load_json(variables_state_obj.as_object().ok_or_else(|| "Invalid variables state object".to_string())?)?; + self.variables_state.load_json(variables_state_obj.as_object().ok_or_else(|| StoryError::BadJson("Invalid variables state object".to_string()))?)?; self.variables_state.set_callstack(self.current_flow.callstack.clone()); } @@ -1196,24 +1196,24 @@ impl StoryState { } if let Some(visit_counts_obj) = j_object.get("visitCounts") { - self.visit_counts = json_read::jobject_to_int_hashmap(visit_counts_obj.as_object().ok_or_else(|| "Invalid visit counts object".to_string())?)?; + self.visit_counts = json_read::jobject_to_int_hashmap(visit_counts_obj.as_object().ok_or_else(|| StoryError::BadJson("Invalid visit counts object".to_string()))?)?; } if let Some(turn_indices_obj) = j_object.get("turnIndices") { - self.turn_indices = json_read::jobject_to_int_hashmap(turn_indices_obj.as_object().ok_or_else(|| "Invalid turn indices object".to_string())?)?; + self.turn_indices = json_read::jobject_to_int_hashmap(turn_indices_obj.as_object().ok_or_else(|| StoryError::BadJson("Invalid turn indices object".to_string()))?)?; } if let Some(current_turn_index) = j_object.get("turnIdx") { - self.current_turn_index = current_turn_index.as_i64().ok_or_else(|| "Invalid current turn index".to_string())? as i32; + self.current_turn_index = current_turn_index.as_i64().ok_or_else(|| StoryError::BadJson("Invalid current turn index".to_string()))? as i32; } if let Some(story_seed) = j_object.get("storySeed") { - self.story_seed = story_seed.as_i64().ok_or_else(|| "Invalid story seed".to_string())? as i32; + self.story_seed = story_seed.as_i64().ok_or_else(|| StoryError::BadJson("Invalid story seed".to_string()))? as i32; } // Not optional, but bug in inkjs means it's actually missing in inkjs saves if let Some(previous_random_obj) = j_object.get("previousRandom") { - self.previous_random = previous_random_obj.as_i64().ok_or_else(|| "Invalid previous random value".to_string())? as i32; + self.previous_random = previous_random_obj.as_i64().ok_or_else(|| StoryError::BadJson("Invalid previous random value".to_string()))? as i32; } else { self.previous_random = 0; } @@ -1221,8 +1221,8 @@ impl StoryState { Ok(()) } - pub(crate) fn remove_flow_internal(&mut self, flow_name: &str) -> Result<(), String> { - if flow_name.eq(DEFAULT_FLOW_NAME) {return Err("Cannot destroy default flow".to_owned());} + pub(crate) fn remove_flow_internal(&mut self, flow_name: &str) -> Result<(), StoryError> { + if flow_name.eq(DEFAULT_FLOW_NAME) {return Err(StoryError::BadArgument("Cannot destroy default flow".to_owned()));} // If we're currently in the flow that's being removed, switch back to default if self.current_flow.name.eq(flow_name) { diff --git a/src/variables_state.rs b/src/variables_state.rs index cd04b2c..61386f0 100644 --- a/src/variables_state.rs +++ b/src/variables_state.rs @@ -2,7 +2,7 @@ use std::{collections::{HashMap, HashSet}, rc::Rc, cell::RefCell}; use serde_json::Map; -use crate::{callstack::CallStack, state_patch::StatePatch, variable_assigment::VariableAssignment, value::Value, list_definitions_origin::ListDefinitionsOrigin, value_type::{VariablePointerValue, ValueType}, json_write_state, json_read}; +use crate::{callstack::CallStack, state_patch::StatePatch, variable_assigment::VariableAssignment, value::Value, list_definitions_origin::ListDefinitionsOrigin, value_type::{VariablePointerValue, ValueType}, json_write_state, json_read, story_error::StoryError}; #[derive(Clone)] @@ -155,10 +155,10 @@ impl VariablesState { Rc::new(Value::new_variable_pointer(&var_pointer.variable_name, context_index)) } - pub fn set(&mut self, variable_name: &str, value_type: ValueType) -> Result<(), String> { + pub fn set(&mut self, variable_name: &str, value_type: ValueType) -> Result<(), StoryError> { if !self.default_global_variables.as_ref().unwrap().contains_key(variable_name) { - return Err(format!("Cannot assign to a variable {} that hasn't been declared in the story", variable_name)); + return Err(StoryError::BadArgument(format!("Cannot assign to a variable {} that hasn't been declared in the story", variable_name))); } let val = Value::from_value_type(value_type); @@ -345,7 +345,7 @@ impl VariablesState { } } - pub(crate) fn load_json(&mut self, jobj: &Map) -> Result<(), String> { + pub(crate) fn load_json(&mut self, jobj: &Map) -> Result<(), StoryError> { self.global_variables.clear(); for (k, v) in self.default_global_variables.as_ref().unwrap().iter() { diff --git a/src/void.rs b/src/void.rs index 4e868b4..cf1114f 100644 --- a/src/void.rs +++ b/src/void.rs @@ -1,7 +1,4 @@ -use std::{ - fmt, - rc::Rc, -}; +use std::fmt; use crate::object::{Object, RTObject}; diff --git a/tests/basic_text_test.rs b/tests/basic_text_test.rs index 845e0b3..b6cad13 100644 --- a/tests/basic_text_test.rs +++ b/tests/basic_text_test.rs @@ -1,10 +1,10 @@ use std::fs; -use bladeink::story::Story; +use bladeink::{story::Story, story_error::StoryError}; mod common; #[test] -fn oneline_test() -> Result<(), String> { +fn oneline_test() -> Result<(), StoryError> { let json_string = fs::read_to_string("examples/inkfiles/basictext/oneline.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -20,7 +20,7 @@ fn oneline_test() -> Result<(), String> { } #[test] -fn twolines_test() -> Result<(), String> { +fn twolines_test() -> Result<(), StoryError> { let json_string = fs::read_to_string("examples/inkfiles/basictext/twolines.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); diff --git a/tests/choice_test.rs b/tests/choice_test.rs index 54ee219..029d9d7 100644 --- a/tests/choice_test.rs +++ b/tests/choice_test.rs @@ -1,10 +1,10 @@ -use bladeink::story::Story; +use bladeink::{story::Story, story_error::StoryError}; mod common; #[test] -fn no_choice_test() -> Result<(), String> { +fn no_choice_test() -> Result<(), StoryError> { let mut errors:Vec = Vec::new(); let text = common::run_story("examples/inkfiles/choices/no-choice-text.ink.json", None, &mut errors)?; @@ -16,7 +16,7 @@ fn no_choice_test() -> Result<(), String> { } #[test] -fn one_test() -> Result<(), String> { +fn one_test() -> Result<(), StoryError> { let mut errors:Vec = Vec::new(); let text = common::run_story("examples/inkfiles/choices/one.ink.json", None, &mut errors)?; @@ -28,7 +28,7 @@ fn one_test() -> Result<(), String> { } #[test] -fn multi_choice_test() -> Result<(), String> { +fn multi_choice_test() -> Result<(), StoryError> { let mut errors:Vec = Vec::new(); let text = common::run_story("examples/inkfiles/choices/multi-choice.ink.json", Some(vec![0]), &mut errors)?; @@ -46,7 +46,7 @@ fn multi_choice_test() -> Result<(), String> { } #[test] -fn single_choice1_test() -> Result<(), String> { +fn single_choice1_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/choices/single-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -60,7 +60,7 @@ fn single_choice1_test() -> Result<(), String> { } #[test] -fn single_choic2_test() -> Result<(), String> { +fn single_choic2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/choices/single-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -78,7 +78,7 @@ fn single_choic2_test() -> Result<(), String> { } #[test] -fn suppress_choice_test() -> Result<(), String> { +fn suppress_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/choices/suppress-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -99,7 +99,7 @@ fn suppress_choice_test() -> Result<(), String> { } #[test] -fn mixed_choice_test() -> Result<(), String> { +fn mixed_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/choices/mixed-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -121,7 +121,7 @@ fn mixed_choice_test() -> Result<(), String> { } #[test] -fn varying_choice_test() -> Result<(), String> { +fn varying_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/choices/varying-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -142,7 +142,7 @@ fn varying_choice_test() -> Result<(), String> { } #[test] -fn sticky_choice_test() -> Result<(), String> { +fn sticky_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/choices/sticky-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -161,7 +161,7 @@ fn sticky_choice_test() -> Result<(), String> { } #[test] -fn fallback_choice_test() -> Result<(), String> { +fn fallback_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/choices/fallback-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -174,7 +174,7 @@ fn fallback_choice_test() -> Result<(), String> { } #[test] -fn fallback_choice2_test() -> Result<(), String> { +fn fallback_choice2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/choices/fallback-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -197,7 +197,7 @@ fn fallback_choice2_test() -> Result<(), String> { } #[test] -fn conditional_choice_test() -> Result<(), String> { +fn conditional_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/choices/conditional-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -210,7 +210,7 @@ fn conditional_choice_test() -> Result<(), String> { } #[test] -fn label_flow_test() -> Result<(), String> { +fn label_flow_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/choices/label-flow.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -230,7 +230,7 @@ fn label_flow_test() -> Result<(), String> { } #[test] -fn label_flow2_test() -> Result<(), String> { +fn label_flow2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/choices/label-flow.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -250,7 +250,7 @@ fn label_flow2_test() -> Result<(), String> { } #[test] -fn label_scope_test() -> Result<(), String> { +fn label_scope_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/choices/label-scope.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -269,7 +269,7 @@ fn label_scope_test() -> Result<(), String> { } #[test] -fn divert_choice_test() -> Result<(), String> { +fn divert_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/choices/divert-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 29cd794..f6e2810 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,9 +1,9 @@ use std::{error::Error, path::Path, fs}; -use bladeink::story::Story; +use bladeink::{story::Story, story_error::StoryError}; use rand::Rng; -pub fn next_all(story: &mut Story, text: &mut Vec) -> Result<(), String> { +pub fn next_all(story: &mut Story, text: &mut Vec) -> Result<(), StoryError> { while story.can_continue() { let line = story.cont()?; print!("{line}"); @@ -14,7 +14,7 @@ pub fn next_all(story: &mut Story, text: &mut Vec) -> Result<(), String> } if story.has_error() { - return Err(join_text(&story.get_current_errors())); + panic!("{}", join_text(&story.get_current_errors())); } Ok(()) @@ -35,7 +35,7 @@ pub fn run_story( filename: &str, choice_list: Option>, errors: &mut Vec, -) -> Result, String> { +) -> Result, StoryError> { // 1) Load story let json = get_json_string(filename).unwrap(); diff --git a/tests/conditional_test.rs b/tests/conditional_test.rs index cc1b249..c824cb0 100644 --- a/tests/conditional_test.rs +++ b/tests/conditional_test.rs @@ -1,9 +1,9 @@ -use bladeink::story::Story; +use bladeink::{story::Story, story_error::StoryError}; mod common; #[test] -fn iftrue_test() -> Result<(), String> { +fn iftrue_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/conditional/iftrue.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -19,7 +19,7 @@ fn iftrue_test() -> Result<(), String> { } #[test] -fn iffalse_test() -> Result<(), String> { +fn iffalse_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/conditional/iffalse.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -35,7 +35,7 @@ fn iffalse_test() -> Result<(), String> { } #[test] -fn ifelse_test() -> Result<(), String> { +fn ifelse_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/conditional/ifelse.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -51,7 +51,7 @@ fn ifelse_test() -> Result<(), String> { } #[test] -fn ifelse_ext_test() -> Result<(), String> { +fn ifelse_ext_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/conditional/ifelse-ext.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -67,7 +67,7 @@ fn ifelse_ext_test() -> Result<(), String> { } #[test] -fn ifelse_ext_text1_test() -> Result<(), String> { +fn ifelse_ext_text1_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/conditional/ifelse-ext-text1.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -89,7 +89,7 @@ fn ifelse_ext_text1_test() -> Result<(), String> { } #[test] -fn ifelse_ext_text2_test() -> Result<(), String> { +fn ifelse_ext_text2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/conditional/ifelse-ext-text2.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -111,7 +111,7 @@ fn ifelse_ext_text2_test() -> Result<(), String> { } #[test] -fn ifelse_ext_text3_test() -> Result<(), String> { +fn ifelse_ext_text3_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/conditional/ifelse-ext-text3.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -133,7 +133,7 @@ fn ifelse_ext_text3_test() -> Result<(), String> { } #[test] -fn cond_text1_test() -> Result<(), String> { +fn cond_text1_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/conditional/condtext.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -152,7 +152,7 @@ fn cond_text1_test() -> Result<(), String> { } #[test] -fn cond_text2_test() -> Result<(), String> { +fn cond_text2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/conditional/condtext.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -171,7 +171,7 @@ fn cond_text2_test() -> Result<(), String> { } #[test] -fn cond_opt1_test() -> Result<(), String> { +fn cond_opt1_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/conditional/condopt.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -189,7 +189,7 @@ fn cond_opt1_test() -> Result<(), String> { } #[test] -fn cond_opt2_test() -> Result<(), String> { +fn cond_opt2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/conditional/condopt.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -207,7 +207,7 @@ fn cond_opt2_test() -> Result<(), String> { } #[test] -fn stopping_test() -> Result<(), String> { +fn stopping_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/conditional/stopping.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -241,7 +241,7 @@ fn stopping_test() -> Result<(), String> { } #[test] -fn cycle_test() -> Result<(), String> { +fn cycle_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/conditional/cycle.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -275,7 +275,7 @@ fn cycle_test() -> Result<(), String> { } #[test] -fn once_test() -> Result<(), String> { +fn once_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/conditional/once.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -306,7 +306,7 @@ fn once_test() -> Result<(), String> { } #[test] -fn shuffle_test() -> Result<(), String> { +fn shuffle_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/conditional/shuffle.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -337,7 +337,7 @@ fn shuffle_test() -> Result<(), String> { } #[test] -fn shuffle_stopping() -> Result<(), String> { +fn shuffle_stopping() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/conditional/shuffle_stopping.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -370,7 +370,7 @@ fn shuffle_stopping() -> Result<(), String> { } #[test] -fn shuffle_once() -> Result<(), String> { +fn shuffle_once() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/conditional/shuffle_once.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -401,7 +401,7 @@ fn shuffle_once() -> Result<(), String> { } #[test] -fn multiline_test() -> Result<(), String> { +fn multiline_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/conditional/multiline.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -430,7 +430,7 @@ fn multiline_test() -> Result<(), String> { } #[test] -fn multiline_divert_test() -> Result<(), String> { +fn multiline_divert_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/conditional/multiline-divert.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -459,7 +459,7 @@ fn multiline_divert_test() -> Result<(), String> { } #[test] -fn multiline_choice_test() -> Result<(), String> { +fn multiline_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/conditional/multiline-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); diff --git a/tests/divert_test.rs b/tests/divert_test.rs index c2b6738..6d62486 100644 --- a/tests/divert_test.rs +++ b/tests/divert_test.rs @@ -1,9 +1,9 @@ -use bladeink::story::Story; +use bladeink::{story::Story, story_error::StoryError}; mod common; #[test] -fn simple_divert_test() -> Result<(), String> { +fn simple_divert_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/divert/simple-divert.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -18,7 +18,7 @@ fn simple_divert_test() -> Result<(), String> { } #[test] -fn invisible_divert_test() -> Result<(), String> { +fn invisible_divert_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/divert/invisible-divert.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -32,7 +32,7 @@ fn invisible_divert_test() -> Result<(), String> { } #[test] -fn divert_on_choice_test() -> Result<(), String> { +fn divert_on_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/divert/divert-on-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -51,7 +51,7 @@ fn divert_on_choice_test() -> Result<(), String> { } #[test] -fn complex_branching1_test() -> Result<(), String> { +fn complex_branching1_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/divert/complex-branching.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -71,7 +71,7 @@ fn complex_branching1_test() -> Result<(), String> { } #[test] -fn complex_branching2_test() -> Result<(), String> { +fn complex_branching2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/divert/complex-branching.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); diff --git a/tests/function_test.rs b/tests/function_test.rs index 34b44ce..a953e68 100644 --- a/tests/function_test.rs +++ b/tests/function_test.rs @@ -1,9 +1,9 @@ -use bladeink::story::Story; +use bladeink::{story::Story, story_error::StoryError}; mod common; #[test] -fn fun_basic_test() -> Result<(), String> { +fn fun_basic_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/function/func-basic.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -17,7 +17,7 @@ fn fun_basic_test() -> Result<(), String> { } #[test] -fn fun_none_test() -> Result<(), String> { +fn fun_none_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/function/func-none.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -31,7 +31,7 @@ fn fun_none_test() -> Result<(), String> { } #[test] -fn fun_inline_test() -> Result<(), String> { +fn fun_inline_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/function/func-inline.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -45,7 +45,7 @@ fn fun_inline_test() -> Result<(), String> { } #[test] -fn setvar_test() -> Result<(), String> { +fn setvar_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/function/setvar-func.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -59,7 +59,7 @@ fn setvar_test() -> Result<(), String> { } #[test] -fn complex_func1_test() -> Result<(), String> { +fn complex_func1_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/function/complex-func1.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -73,7 +73,7 @@ fn complex_func1_test() -> Result<(), String> { } #[test] -fn complex_func2_test() -> Result<(), String> { +fn complex_func2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/function/complex-func2.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -87,7 +87,7 @@ fn complex_func2_test() -> Result<(), String> { } #[test] -fn complex_func3_test() -> Result<(), String> { +fn complex_func3_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/function/complex-func3.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -102,7 +102,7 @@ fn complex_func3_test() -> Result<(), String> { } #[test] -fn rnd() -> Result<(), String> { +fn rnd() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/function/rnd-func.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -119,7 +119,7 @@ fn rnd() -> Result<(), String> { } #[test] -fn evaluating_function_variable_state_bug_test() -> Result<(), String> { +fn evaluating_function_variable_state_bug_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/function/evaluating-function-variablestate-bug.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); diff --git a/tests/gather_test.rs b/tests/gather_test.rs index bc81af3..bb6171e 100644 --- a/tests/gather_test.rs +++ b/tests/gather_test.rs @@ -1,9 +1,9 @@ -use bladeink::story::Story; +use bladeink::{story::Story, story_error::StoryError}; mod common; #[test] -fn gather_basic_test() -> Result<(), String> { +fn gather_basic_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/gather/gather-basic.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -24,7 +24,7 @@ fn gather_basic_test() -> Result<(), String> { } #[test] -fn gather_chain_test() -> Result<(), String> { +fn gather_chain_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/gather/gather-chain.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -52,7 +52,7 @@ fn gather_chain_test() -> Result<(), String> { } #[test] -fn nested_flow_test() -> Result<(), String> { +fn nested_flow_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/gather/nested-flow.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -75,7 +75,7 @@ fn nested_flow_test() -> Result<(), String> { } #[test] -fn deep_nesting_test() -> Result<(), String> { +fn deep_nesting_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/gather/deep-nesting.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -111,7 +111,7 @@ fn deep_nesting_test() -> Result<(), String> { #[test] -fn complex_flow1_test() -> Result<(), String> { +fn complex_flow1_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/gather/complex-flow.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -129,7 +129,7 @@ fn complex_flow1_test() -> Result<(), String> { } #[test] -fn complex_flow2_test() -> Result<(), String> { +fn complex_flow2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/gather/complex-flow.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); diff --git a/tests/glue_test.rs b/tests/glue_test.rs index da42589..891d545 100644 --- a/tests/glue_test.rs +++ b/tests/glue_test.rs @@ -1,9 +1,9 @@ -use bladeink::story::Story; +use bladeink::{story::Story, story_error::StoryError}; mod common; #[test] -fn simple_glue_test() -> Result<(), String> { +fn simple_glue_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/glue/simple-glue.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -17,7 +17,7 @@ fn simple_glue_test() -> Result<(), String> { } #[test] -fn glue_with_divert_test() -> Result<(), String> { +fn glue_with_divert_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/glue/glue-with-divert.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -32,7 +32,7 @@ fn glue_with_divert_test() -> Result<(), String> { } #[test] -fn has_left_right_glue_matching_test() -> Result<(), String> { +fn has_left_right_glue_matching_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/glue/left-right-glue-matching.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -48,7 +48,7 @@ fn has_left_right_glue_matching_test() -> Result<(), String> { } #[test] -fn bugfix1_test() -> Result<(), String> { +fn bugfix1_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/glue/testbugfix1.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -64,7 +64,7 @@ fn bugfix1_test() -> Result<(), String> { } #[test] -fn bugfix2_test() -> Result<(), String> { +fn bugfix2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/glue/testbugfix2.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); diff --git a/tests/knot_test.rs b/tests/knot_test.rs index 2f863e3..c74e5c8 100644 --- a/tests/knot_test.rs +++ b/tests/knot_test.rs @@ -1,9 +1,9 @@ -use bladeink::story::Story; +use bladeink::{story::Story, story_error::StoryError}; mod common; #[test] -fn single_line_test() -> Result<(), String> { +fn single_line_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/knot/single-line.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -18,7 +18,7 @@ fn single_line_test() -> Result<(), String> { } #[test] -fn multi_line_test() -> Result<(), String> { +fn multi_line_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/knot/multi-line.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -35,7 +35,7 @@ fn multi_line_test() -> Result<(), String> { } #[test] -fn strip_empty_lines_test() -> Result<(), String> { +fn strip_empty_lines_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/knot/strip-empty-lines.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -52,7 +52,7 @@ fn strip_empty_lines_test() -> Result<(), String> { } #[test] -fn param_strings_test() -> Result<(), String> { +fn param_strings_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/knot/param-strings.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -71,7 +71,7 @@ fn param_strings_test() -> Result<(), String> { } #[test] -fn param_ints_test() -> Result<(), String> { +fn param_ints_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/knot/param-ints.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -90,7 +90,7 @@ fn param_ints_test() -> Result<(), String> { } #[test] -fn param_floats_test() -> Result<(), String> { +fn param_floats_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/knot/param-floats.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -109,7 +109,7 @@ fn param_floats_test() -> Result<(), String> { } #[test] -fn param_vars_test() -> Result<(), String> { +fn param_vars_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/knot/param-vars.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -128,7 +128,7 @@ fn param_vars_test() -> Result<(), String> { } #[test] -fn param_multi_test() -> Result<(), String> { +fn param_multi_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/knot/param-multi.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -147,7 +147,7 @@ fn param_multi_test() -> Result<(), String> { } #[test] -fn param_recurse_test() -> Result<(), String> { +fn param_recurse_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/knot/param-recurse.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); diff --git a/tests/list_test.rs b/tests/list_test.rs index e8b8927..1ad36ac 100644 --- a/tests/list_test.rs +++ b/tests/list_test.rs @@ -1,9 +1,9 @@ -use bladeink::story::Story; +use bladeink::{story::Story, story_error::StoryError}; mod common; #[test] -fn list_basic_operations_test() -> Result<(), String> { +fn list_basic_operations_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/lists/basic-operations.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -14,7 +14,7 @@ fn list_basic_operations_test() -> Result<(), String> { } #[test] -fn list_mixed_items_test() -> Result<(), String> { +fn list_mixed_items_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/lists/list-mixed-items.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -25,7 +25,7 @@ fn list_mixed_items_test() -> Result<(), String> { } #[test] -fn more_list_operations_test() -> Result<(), String> { +fn more_list_operations_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/lists/more-list-operations.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -36,7 +36,7 @@ fn more_list_operations_test() -> Result<(), String> { } #[test] -fn empty_list_origin_test() -> Result<(), String> { +fn empty_list_origin_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/lists/empty-list-origin.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -47,7 +47,7 @@ fn empty_list_origin_test() -> Result<(), String> { } #[test] -fn list_save_load_test() -> Result<(), String> { +fn list_save_load_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/lists/list-save-load.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -68,7 +68,7 @@ fn list_save_load_test() -> Result<(), String> { } #[test] -fn empty_list_origin_after_assinment_test() -> Result<(), String> { +fn empty_list_origin_after_assinment_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/lists/empty-list-origin-after-assignment.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -79,7 +79,7 @@ fn empty_list_origin_after_assinment_test() -> Result<(), String> { } #[test] -fn list_range_test() -> Result<(), String> { +fn list_range_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/lists/list-range.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -90,7 +90,7 @@ fn list_range_test() -> Result<(), String> { } #[test] -fn list_bug_adding_element_test() -> Result<(), String> { +fn list_bug_adding_element_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/lists/bug-adding-element.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -107,7 +107,7 @@ fn list_bug_adding_element_test() -> Result<(), String> { } #[test] -fn more_list_operations2_test() -> Result<(), String> { +fn more_list_operations2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/lists/more-list-operations2.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); diff --git a/tests/misc_test.rs b/tests/misc_test.rs index a5acdb2..f4158d0 100644 --- a/tests/misc_test.rs +++ b/tests/misc_test.rs @@ -1,9 +1,9 @@ -use bladeink::{story::Story, value_type::ValueType}; +use bladeink::{story::Story, value_type::ValueType, story_error::StoryError}; mod common; #[test] -fn operations_test() -> Result<(), String> { +fn operations_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/misc/operations.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -14,7 +14,7 @@ fn operations_test() -> Result<(), String> { } #[test] -fn read_counts_test() -> Result<(), String> { +fn read_counts_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/misc/read-counts.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -25,7 +25,7 @@ fn read_counts_test() -> Result<(), String> { } #[test] -fn turns_since_test() -> Result<(), String> { +fn turns_since_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/misc/turns-since.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -41,7 +41,7 @@ fn turns_since_test() -> Result<(), String> { * Issue: https://github.com/bladecoder/blade-ink/issues/15 */ #[test] -fn issue15_test() -> Result<(), String> { +fn issue15_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/misc/issue15.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); diff --git a/tests/multi_flow_test.rs b/tests/multi_flow_test.rs index d6ec369..7ee673f 100644 --- a/tests/multi_flow_test.rs +++ b/tests/multi_flow_test.rs @@ -1,9 +1,9 @@ -use bladeink::story::Story; +use bladeink::{story::Story, story_error::StoryError}; mod common; #[test] -fn basics_test() -> Result<(), String> { +fn basics_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/runtime/multiflow-basics.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -26,7 +26,7 @@ fn basics_test() -> Result<(), String> { } #[test] -fn multiflow_save_load_threads() -> Result<(), String> { +fn multiflow_save_load_threads() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/runtime/multiflow-saveloadthreads.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); diff --git a/tests/runtime_test.rs b/tests/runtime_test.rs index 55a8a99..132e575 100644 --- a/tests/runtime_test.rs +++ b/tests/runtime_test.rs @@ -1,11 +1,11 @@ -use bladeink::{story::Story, value_type::{ValueType, StringValue}}; +use bladeink::{story::Story, value_type::{ValueType, StringValue}, story_error::StoryError}; mod common; // TODO external functions + variable observers #[test] -fn set_and_get_variable_test() -> Result<(), String> { +fn set_and_get_variable_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/runtime/set-get-variables.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -31,7 +31,7 @@ fn set_and_get_variable_test() -> Result<(), String> { #[test] -fn set_non_existant_variable_test() -> Result<(), String> { +fn set_non_existant_variable_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/runtime/set-get-variables.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -60,7 +60,7 @@ fn set_non_existant_variable_test() -> Result<(), String> { } #[test] -fn jump_knot_test() -> Result<(), String> { +fn jump_knot_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/runtime/jump-knot.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -89,7 +89,7 @@ fn jump_knot_test() -> Result<(), String> { } #[test] -fn jump_stitch_test() -> Result<(), String> { +fn jump_stitch_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/runtime/jump-stitch.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -118,7 +118,7 @@ fn jump_stitch_test() -> Result<(), String> { } #[test] -fn read_visit_counts_test() -> Result<(), String> { +fn read_visit_counts_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/runtime/read-visit-counts.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -132,7 +132,7 @@ fn read_visit_counts_test() -> Result<(), String> { } #[test] -fn load_save_test() -> Result<(), String> { +fn load_save_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/runtime/load-save.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); diff --git a/tests/stitch_test.rs b/tests/stitch_test.rs index 880b66e..d25e7f7 100644 --- a/tests/stitch_test.rs +++ b/tests/stitch_test.rs @@ -1,9 +1,9 @@ -use bladeink::story::Story; +use bladeink::{story::Story, story_error::StoryError}; mod common; #[test] -fn auto_stitch_test() -> Result<(), String> { +fn auto_stitch_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/stitch/auto-stitch.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -18,7 +18,7 @@ fn auto_stitch_test() -> Result<(), String> { } #[test] -fn auto_stitch2_test() -> Result<(), String> { +fn auto_stitch2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/stitch/auto-stitch.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -36,7 +36,7 @@ fn auto_stitch2_test() -> Result<(), String> { } #[test] -fn manual_stitch_test() -> Result<(), String> { +fn manual_stitch_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/stitch/manual-stitch.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -58,7 +58,7 @@ fn manual_stitch_test() -> Result<(), String> { } #[test] -fn manual_stitch2_test() -> Result<(), String> { +fn manual_stitch2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/stitch/manual-stitch.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); diff --git a/tests/tag_test.rs b/tests/tag_test.rs index 29bffe9..0adadfd 100644 --- a/tests/tag_test.rs +++ b/tests/tag_test.rs @@ -1,9 +1,9 @@ -use bladeink::story::Story; +use bladeink::{story::Story, story_error::StoryError}; mod common; #[test] -fn tags_test() -> Result<(), String> { +fn tags_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/tags/tags.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -42,7 +42,7 @@ fn tags_test() -> Result<(), String> { } #[test] -fn tags_in_seq_test() -> Result<(), String> { +fn tags_in_seq_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/tags/tagsInSeq.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -61,7 +61,7 @@ fn tags_in_seq_test() -> Result<(), String> { } #[test] -fn tags_in_choice_test() -> Result<(), String> { +fn tags_in_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/tags/tagsInChoice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -87,7 +87,7 @@ fn tags_in_choice_test() -> Result<(), String> { } #[test] -fn tags_dynamic_content_test() -> Result<(), String> { +fn tags_dynamic_content_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/tags/tagsDynamicContent.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); diff --git a/tests/thread_test.rs b/tests/thread_test.rs index b0d561c..9bc52c2 100644 --- a/tests/thread_test.rs +++ b/tests/thread_test.rs @@ -1,9 +1,9 @@ -use bladeink::story::Story; +use bladeink::{story::Story, story_error::StoryError}; mod common; #[test] -fn thread_test() -> Result<(), String> { +fn thread_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/threads/thread-bug.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -28,7 +28,7 @@ fn thread_test() -> Result<(), String> { } #[test] -fn thread_test_bug() -> Result<(), String> { +fn thread_test_bug() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/threads/thread-bug.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); diff --git a/tests/tunnel_test.rs b/tests/tunnel_test.rs index 7930198..5afa513 100644 --- a/tests/tunnel_test.rs +++ b/tests/tunnel_test.rs @@ -1,9 +1,9 @@ -use bladeink::story::Story; +use bladeink::{story::Story, story_error::StoryError}; mod common; #[test] -fn tunnel_onwards_divert_override_test() -> Result<(), String> { +fn tunnel_onwards_divert_override_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/tunnels/tunnel-onwards-divert-override.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); diff --git a/tests/variable_test.rs b/tests/variable_test.rs index cc43f50..7f4b1bc 100644 --- a/tests/variable_test.rs +++ b/tests/variable_test.rs @@ -1,9 +1,9 @@ -use bladeink::story::Story; +use bladeink::{story::Story, story_error::StoryError}; mod common; #[test] -fn variable_declaration_test() -> Result<(), String> { +fn variable_declaration_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/variable/variable-declaration.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -17,7 +17,7 @@ fn variable_declaration_test() -> Result<(), String> { } #[test] -fn var_calc_test() -> Result<(), String> { +fn var_calc_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/variable/varcalc.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -31,7 +31,7 @@ fn var_calc_test() -> Result<(), String> { } #[test] -fn var_string_ink_bug_test() -> Result<(), String> { +fn var_string_ink_bug_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/variable/varstringinc.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -49,7 +49,7 @@ fn var_string_ink_bug_test() -> Result<(), String> { } #[test] -fn var_divert_test() -> Result<(), String> { +fn var_divert_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/variable/var-divert.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); diff --git a/tests/variable_text_test.rs b/tests/variable_text_test.rs index e35f0e5..f571571 100644 --- a/tests/variable_text_test.rs +++ b/tests/variable_text_test.rs @@ -1,9 +1,9 @@ -use bladeink::story::Story; +use bladeink::{story::Story, story_error::StoryError}; mod common; #[test] -fn sequence_test() -> Result<(), String> { +fn sequence_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/variabletext/sequence.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -46,7 +46,7 @@ fn sequence_test() -> Result<(), String> { } #[test] -fn cycle_test() -> Result<(), String> { +fn cycle_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/variabletext/cycle.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -89,7 +89,7 @@ fn cycle_test() -> Result<(), String> { } #[test] -fn once_test() -> Result<(), String> { +fn once_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/variabletext/once.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -132,7 +132,7 @@ fn once_test() -> Result<(), String> { } #[test] -fn empty_elements_test() -> Result<(), String> { +fn empty_elements_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/variabletext/empty-elements.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); @@ -161,7 +161,7 @@ fn empty_elements_test() -> Result<(), String> { } #[test] -fn list_in_choice_test() -> Result<(), String> { +fn list_in_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("examples/inkfiles/variabletext/list-in-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); From adbecfbe78c7b1ee15d63226ccf90a192cb39c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Mon, 2 Oct 2023 00:14:06 +0000 Subject: [PATCH 53/91] Get rid of all panics. --- src/callstack.rs | 25 ++- src/flow.rs | 14 +- src/ink_list.rs | 10 +- src/json_write_state.rs | 258 --------------------- src/lib.rs | 2 +- src/native_function_call.rs | 435 ++++++++++++++++++------------------ src/object.rs | 14 +- src/push_pop.rs | 12 +- src/story.rs | 131 ++++++----- src/story_state.rs | 56 +++-- src/value.rs | 96 ++++---- src/variables_state.rs | 8 +- tests/list_test.rs | 2 +- tests/multi_flow_test.rs | 2 +- tests/runtime_test.rs | 6 +- tests/thread_test.rs | 2 +- 16 files changed, 408 insertions(+), 665 deletions(-) delete mode 100644 src/json_write_state.rs diff --git a/src/callstack.rs b/src/callstack.rs index 860974d..2603377 100644 --- a/src/callstack.rs +++ b/src/callstack.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, rc::Rc}; use serde_json::{Map, json}; -use crate::{pointer::{Pointer, self}, push_pop::PushPopType, container::{Container, self}, value::Value, object::Object, json_read, json_write_state, path::Path, story::Story, story_error::StoryError}; +use crate::{pointer::{Pointer, self}, push_pop::PushPopType, container::{Container, self}, value::Value, object::Object, json_read, json_write, path::Path, story::Story, story_error::StoryError}; pub struct Element { pub current_pointer: Pointer, @@ -59,7 +59,7 @@ impl Thread { for j_el_tok in j_thread_callstack.iter() { if let Some(j_element_obj) = j_el_tok.as_object() { - let push_pop_type = PushPopType::from_value(j_element_obj.get("type").and_then(|t| t.as_i64()).ok_or(StoryError::BadJson("Invalid push/pop type".to_owned()))? as usize); + let push_pop_type = PushPopType::from_value(j_element_obj.get("type").and_then(|t| t.as_i64()).ok_or(StoryError::BadJson("Invalid push/pop type".to_owned()))? as usize)?; let mut pointer = pointer::NULL.clone(); @@ -98,7 +98,7 @@ impl Thread { if let Some(prev_content_obj_path) = j_obj.get("previousContentObject").and_then(|p| p.as_str()) { let prev_path = Path::new_with_components_string(Some(prev_content_obj_path)); - thread.previous_pointer = Story::pointer_at_path(main_content_container, &prev_path); + thread.previous_pointer = Story::pointer_at_path(main_content_container, &prev_path)?; } Ok(thread) @@ -117,7 +117,7 @@ impl Thread { copy } - pub(crate) fn write_json(&self) -> serde_json::Value { + pub(crate) fn write_json(&self) -> Result { let mut thread: Map = Map::new(); let mut cs_array: Vec = Vec::new(); @@ -133,7 +133,7 @@ impl Thread { el_map.insert("type".to_owned(), json!(el.push_pop_type as u32)); if el.temporary_variables.len() > 0 { - el_map.insert("temp".to_owned(), json_write_state::write_dictionary_values(&el.temporary_variables)); + el_map.insert("temp".to_owned(), json_write::write_dictionary_values(&el.temporary_variables)?); } cs_array.push(serde_json::Value::Object(el_map)); @@ -146,7 +146,7 @@ impl Thread { thread.insert("previousContentObject".to_owned(), json!(Object::get_path(self.previous_pointer.resolve().unwrap().as_ref()).to_string())); } - serde_json::Value::Object(thread) + Ok(serde_json::Value::Object(thread)) } } @@ -239,14 +239,15 @@ impl CallStack { self.get_current_element().push_pop_type == t.unwrap() } - pub fn pop(&mut self, t: Option) { + pub fn pop(&mut self, t: Option) -> Result<(), StoryError> { if self.can_pop_type(t) { let l = self.get_callstack().len() - 1; self.get_callstack_mut().remove(l); - return; } else { - panic!("Mismatched push/pop in Callstack"); + return Err(StoryError::InvalidStoryState("Mismatched push/pop in Callstack".to_owned())); } + + Ok(()) } pub fn element_is_evaluate_from_game(&self) -> bool { @@ -353,19 +354,19 @@ impl CallStack { self.get_callstack_mut().push(element); } - pub(crate) fn write_json(&self) -> serde_json::Value { + pub(crate) fn write_json(&self) -> Result { let mut cs: Map = Map::new(); let mut treads_array: Vec = Vec::new(); for thread in &self.threads { - treads_array.push(thread.write_json()); + treads_array.push(thread.write_json()?); } cs.insert("threads".to_owned(), serde_json::Value::Array(treads_array)); cs.insert("threadCounter".to_owned(), json!(self.thread_counter)); - serde_json::Value::Object(cs) + Ok(serde_json::Value::Object(cs)) } pub fn get_thread_with_index(&self, index: usize) -> Option<&Thread> { diff --git a/src/flow.rs b/src/flow.rs index 8095349..af8aa6a 100644 --- a/src/flow.rs +++ b/src/flow.rs @@ -2,7 +2,7 @@ use std::{rc::Rc, cell::RefCell}; use serde_json::Map; -use crate::{callstack::{CallStack, Thread}, choice::Choice, object::RTObject, container::Container, json_write_state, json_read, story_error::StoryError}; +use crate::{callstack::{CallStack, Thread}, choice::Choice, object::RTObject, container::Container, json_write, json_read, story_error::StoryError}; #[derive(Clone)] pub struct Flow { @@ -38,11 +38,11 @@ impl Flow { Ok(flow) } - pub(crate) fn write_json(&self) -> serde_json::Value { + pub(crate) fn write_json(&self) -> Result { let mut flow: Map = Map::new(); - flow.insert("callstack".to_owned(), self.callstack.borrow().write_json()); - flow.insert("outputStream".to_owned(), json_write_state::write_list_rt_objs(&self.output_stream)); + flow.insert("callstack".to_owned(), self.callstack.borrow().write_json()?); + flow.insert("outputStream".to_owned(), json_write::write_list_rt_objs(&self.output_stream)?); // choiceThreads: optional // Has to come BEFORE the choices themselves are written out @@ -57,7 +57,7 @@ impl Flow { has_choice_threads = true; } - jct.insert(c.original_thread_index.borrow().to_string(), c.get_thread_at_generation().unwrap().write_json()); + jct.insert(c.original_thread_index.borrow().to_string(), c.get_thread_at_generation().unwrap().write_json()?); } } @@ -67,12 +67,12 @@ impl Flow { let mut c_array: Vec = Vec::new(); for c in self.current_choices.iter() { - c_array.push(json_write_state::write_choice(c)); + c_array.push(json_write::write_choice(c)); } flow.insert("currentChoices".to_owned(), serde_json::Value::Array(c_array)); - serde_json::Value::Object(flow) + Ok(serde_json::Value::Object(flow)) } pub fn load_flow_choice_threads(&mut self, j_choice_threads: Option<&serde_json::Value>, main_content_container: Rc) -> Result<(), StoryError>{ diff --git a/src/ink_list.rs b/src/ink_list.rs index e6ae732..11b8575 100644 --- a/src/ink_list.rs +++ b/src/ink_list.rs @@ -1,7 +1,7 @@ use core::fmt; use std::{collections::HashMap, cell::RefCell}; -use crate::{ink_list_item::InkListItem, list_definition::ListDefinition, list_definitions_origin::ListDefinitionsOrigin, value_type::ValueType}; +use crate::{ink_list_item::InkListItem, list_definition::ListDefinition, list_definitions_origin::ListDefinitionsOrigin, value_type::ValueType, story_error::StoryError}; #[derive(Clone)] @@ -28,7 +28,7 @@ impl InkList { l } - pub fn from_single_origin(single_origin: String, list_definitions: &ListDefinitionsOrigin) -> Self { + pub fn from_single_origin(single_origin: String, list_definitions: &ListDefinitionsOrigin) -> Result { let l = Self::new(); l.initial_origin_names.borrow_mut().push(single_origin); @@ -38,13 +38,13 @@ impl InkList { if let Some(list_def) = def { l.origins.borrow_mut().push(list_def.clone()); } else { - panic!( + return Err(StoryError::InvalidStoryState(format!( "InkList origin could not be found in story when constructing new list: {}", &l.initial_origin_names.borrow()[0] - ); + ))); } - l + Ok(l) } fn from_other_list(other_list: &InkList) -> Self { diff --git a/src/json_write_state.rs b/src/json_write_state.rs deleted file mode 100644 index 58a71c8..0000000 --- a/src/json_write_state.rs +++ /dev/null @@ -1,258 +0,0 @@ -use std::{collections::HashMap, rc::Rc}; - -use serde_json::{Map, json}; - -use crate::{ - container::Container, - object::RTObject, value::Value, glue::Glue, choice_point::ChoicePoint, push_pop::PushPopType, divert::Divert, ink_list::InkList, control_command::ControlCommand, native_function_call::NativeFunctionCall, variable_reference::VariableReference, variable_assigment::VariableAssignment, tag::Tag, void::Void, choice::Choice, -}; - -pub fn write_dictionary_values(objs: &HashMap>) -> serde_json::Value { - let mut jobjs: Map = Map::new(); - - for (k,o) in objs { - jobjs.insert(k.clone(), write_rtobject(o.clone())); - } - - serde_json::Value::Object(jobjs) -} - -pub fn write_rtobject(o: Rc) -> serde_json::Value { - if let Some(c) = o.as_any().downcast_ref::() { - return write_rt_container(c, false); - } - - if let Some(divert) = o.as_any().downcast_ref::() { - let mut div_type_key = "->"; - - if divert.is_external { div_type_key = "x()"; } - else if divert.pushes_to_stack { - if divert.stack_push_type == PushPopType::Function {div_type_key = "f()";} - else if divert.stack_push_type == PushPopType::Tunnel {div_type_key = "->t->";} - } - - let target_str = - if divert.has_variable_target() {divert.variable_divert_name.clone().unwrap()} - else {divert.get_target_path_string().unwrap()}; - - let mut jobj: Map = Map::new(); - - jobj.insert(div_type_key.to_string(), json!(target_str)); - - if divert.has_variable_target() {jobj.insert("var".to_owned(), json!(true));} - - if divert.is_conditional {jobj.insert("c".to_owned(), json!(true));} - - if divert.external_args > 0 {jobj.insert("exArgs".to_owned(), json!(divert.external_args));} - - return serde_json::Value::Object(jobj); - } - - if let Ok(cp) = o.clone().into_any().downcast::() { - let mut jobj: Map = Map::new(); - jobj.insert("*".to_owned(), json!(ChoicePoint::get_path_string_on_choice(&cp))); - jobj.insert("flg".to_owned(), json!(cp.get_flags())); - return serde_json::Value::Object(jobj); - } - - if let Some(v) = Value::get_bool_value(o.as_ref()) { - return json!(v); - } - - if let Some(v) = Value::get_int_value(o.as_ref()) { - return json!(v); - } - - if let Some(v) = Value::get_float_value(o.as_ref()) { - return json!(v); - } - - if let Some(v) = Value::get_string_value(o.as_ref()) { - let mut s = String::new(); - - if v.is_newline { - s.push('\n'); - } else { - s.push('^'); - s.push_str(&v.string); - } - - return json!(s); - } - - if let Some(v) = Value::get_list_value(o.as_ref()) { - return write_ink_list(v); - } - - if let Some(v) = Value::get_divert_target_value(o.as_ref()) { - let mut jobj: Map = Map::new(); - jobj.insert("^->".to_owned(), json!(v.get_components_string())); - return serde_json::Value::Object(jobj); - } - - if let Some(v) = Value::get_variable_pointer_value(o.as_ref()) { - let mut jobj: Map = Map::new(); - jobj.insert("^var".to_owned(), json!(v.variable_name)); - jobj.insert("ci".to_owned(), json!(v.context_index)); - return serde_json::Value::Object(jobj); - } - - if o.as_any().is::() { - return json!("<>") - } - - if let Some(cc) = o.as_any().downcast_ref::() { - return json!(ControlCommand::get_name(cc.command_type)); - } - - if let Some(f) = o.as_any().downcast_ref::() { - let mut name = NativeFunctionCall::get_name(f.op); - - // Avoid collision with ^ used to indicate a string - if "^".eq(&name) {name = "L^".to_owned();} - - return json!(name); - } - - if let Ok(var_ref) = o.clone().into_any().downcast::() { - - let mut jobj: Map = Map::new(); - - let read_count_path = var_ref.get_path_string_for_count(); - if read_count_path.is_some() { - jobj.insert("CNT?".to_owned(), json!(read_count_path)); - } else { - jobj.insert("VAR?".to_owned(), json!(var_ref.name.clone())); - } - - return serde_json::Value::Object(jobj); - } - - if let Some(var_ass) = o.as_any().downcast_ref::() { - let mut jobj: Map = Map::new(); - - let key = if var_ass.is_global {"VAR=".to_owned()} else {"temp=".to_owned()}; - jobj.insert(key, json!(var_ass.variable_name)); - - // Reassignment? - if !var_ass.is_new_declaration {jobj.insert("re".to_owned(), json!(true));} - - return serde_json::Value::Object(jobj); - } - - if o.as_any().is::() { - return json!("void") - } - - if let Some(tag) = o.as_any().downcast_ref::() { - let mut jobj: Map = Map::new(); - - jobj.insert("#".to_owned(), json!(tag.get_text())); - - return serde_json::Value::Object(jobj); - } - - if let Some(choice) = o.as_any().downcast_ref::() { - return write_choice(choice); - } - - panic!("Failed to write runtime object to JSON: {}", o.to_string()); -} - -pub fn write_rt_container(container: &Container, without_name: bool) -> serde_json::Value { - let mut c_array: Vec = Vec::new(); - - for c in container.content.iter() { - c_array.push(write_rtobject(c.clone())); - } - - // 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 - let named_only_content = &container.get_named_only_content(); - let count_flags = container.get_count_flags(); - let has_name_property = container.name.is_some() && !without_name; - - let has_terminator = !named_only_content.is_empty() || count_flags > 0 || has_name_property; - - if has_terminator { - let mut t_obj: Map = Map::new(); - - for (name, c) in named_only_content { - t_obj.insert(name.clone(), write_rt_container(c.as_ref(), true)); - } - - if count_flags > 0 { - t_obj.insert("#f".to_owned(), json!(count_flags)); - } - - if has_name_property { - t_obj.insert("#n".to_owned(), json!(container.name)); - } - - c_array.push(serde_json::Value::Object(t_obj)); - } else { - c_array.push(serde_json::Value::Null); - } - - serde_json::Value::Array(c_array) -} - -pub fn write_ink_list(list: &InkList) -> serde_json::Value { - let mut jobj: Map = Map::new(); - - let mut jlist: Map = Map::new(); - for (item, v) in list.items.iter() { - - let mut name = String::new(); - - match item.get_origin_name() { - Some(n) => name.push_str(n), - None => name.push('?'), - } - - name.push('.'); - name.push_str(item.get_item_name()); - - jlist.insert(name, json!(v)); - } - - jobj.insert("list".to_owned(), serde_json::Value::Object(jlist)); - - - serde_json::Value::Object(jobj) -} - -pub fn write_choice(choice: &Choice) -> serde_json::Value { - let mut jobj: Map = Map::new(); - - jobj.insert("text".to_owned(), json!(choice.text)); - jobj.insert("index".to_owned(), json!(choice.index)); - jobj.insert("originalChoicePath".to_owned(), json!(choice.source_path)); - jobj.insert("originalThreadIndex".to_owned(), json!(choice.original_thread_index)); - jobj.insert("targetPath".to_owned(), json!(choice.target_path.to_string())); - - serde_json::Value::Object(jobj) -} - -pub(crate) fn write_list_rt_objs(objs: &[Rc]) -> serde_json::Value { - let mut c_array: Vec = Vec::new(); - - for o in objs { - c_array.push(write_rtobject(o.clone())); - } - - serde_json::Value::Array(c_array) -} - -pub(crate) fn write_int_dictionary(map: &HashMap) -> serde_json::Value { - let mut jobj: Map = Map::new(); - - for (key, val) in map { - jobj.insert(key.clone(), json!(*val)); - } - - serde_json::Value::Object(jobj) -} diff --git a/src/lib.rs b/src/lib.rs index 4f71207..9974e91 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ pub mod story; pub mod value_type; pub mod story_error; mod json_read; -mod json_write_state; +mod json_write; mod object; mod value; mod container; diff --git a/src/native_function_call.rs b/src/native_function_call.rs index a9d7c7e..f55db4a 100644 --- a/src/native_function_call.rs +++ b/src/native_function_call.rs @@ -1,6 +1,6 @@ use std::{fmt, rc::Rc}; -use crate::{object::{Object, RTObject}, value::Value, void::Void, ink_list::InkList, value_type::ValueType}; +use crate::{object::{Object, RTObject}, value::Value, void::Void, ink_list::InkList, value_type::ValueType, story_error::StoryError}; #[derive(Debug, PartialEq, Clone, Copy)] pub enum Op { @@ -165,17 +165,17 @@ impl NativeFunctionCall { } } - pub(crate) fn call(&self, params: Vec>) -> Rc { + pub(crate) fn call(&self, params: Vec>) -> Result, StoryError> { if self.get_number_of_parameters() != params.len() { - panic!("Unexpected number of parameters"); + return Err(StoryError::InvalidStoryState("Unexpected number of parameters".to_owned())); } let mut has_list = false; for p in ¶ms { if p.as_ref().as_any().is::() { - panic!("Attempting to perform operation on a void value. Did you forget to 'return' a value from a function you called here?"); + return Err(StoryError::InvalidStoryState("Attempting to perform operation on a void value. Did you forget to 'return' a value from a function you called here?".to_owned())); } if Value::get_list_value(p.as_ref()).is_some() { @@ -189,17 +189,17 @@ impl NativeFunctionCall { return self.call_binary_list_operation(¶ms); } - let coerced_params = self.coerce_values_to_single_type(params); + let coerced_params = self.coerce_values_to_single_type(params)?; self.call_type(coerced_params) } - fn call_binary_list_operation(&self, params: &Vec>) -> Rc { + fn call_binary_list_operation(&self, params: &Vec>) -> Result, StoryError> { // List-Int addition/subtraction returns a List (e.g., "alpha" + 1 = "beta") if (self.op == Op::Add || self.op == Op::Subtract) && Value::get_list_value(params[0].as_ref()).is_some() && Value::get_int_value(params[1].as_ref()).is_some() { - return self.call_list_increment_operation(params); + return Ok(self.call_list_increment_operation(params)); } let v1 = params[0].clone().into_any().downcast::().unwrap(); @@ -212,13 +212,13 @@ impl NativeFunctionCall { let result = { if self.op == Op::And { - v1.is_truthy() && v2.is_truthy() + v1.is_truthy()? && v2.is_truthy()? } else { - v1.is_truthy() || v2.is_truthy() + v1.is_truthy()? || v2.is_truthy()? } }; - return Rc::new(Value::new_bool(result)); + return Ok(Rc::new(Value::new_bool(result))); } // Normal (list • list) operation @@ -229,13 +229,12 @@ impl NativeFunctionCall { return self.call_type(p); } - // Err(StoryError::new(format!( - // "Can not call use '{}' operation on {} and {}", - // self.name, - // v1.value_type(), - // v2.value_type() - // ))) - panic!() + Err(StoryError::InvalidStoryState(format!( + "Can not call use '{}' operation on {} and {}", + Self::get_name(self.op), // TODO implement Display for op + v1, + v2 + ))) } fn call_list_increment_operation(&self, list_int_params: &Vec>) -> Rc { @@ -270,7 +269,7 @@ impl NativeFunctionCall { Rc::new(Value::new_list(result_raw_list)) } - fn call_type(&self, coerced_params: Vec>) -> Rc { + fn call_type(&self, coerced_params: Vec>) -> Result, StoryError> { match self.op { Op::Add => self.add_op(&coerced_params), Op::Subtract => self.subtract_op(&coerced_params), @@ -306,7 +305,7 @@ impl NativeFunctionCall { } } - fn coerce_values_to_single_type(&self, params: Vec>) -> Vec> { + fn coerce_values_to_single_type(&self, params: Vec>) -> Result>, StoryError> { let mut dest_type = 1; // Int let mut result: Vec> = Vec::new(); @@ -324,494 +323,494 @@ impl NativeFunctionCall { for obj in params.iter() { if let Some(v) = obj.as_ref().as_any().downcast_ref::() { - match v.cast(dest_type) { + match v.cast(dest_type)? { Some(casted_value) => result.push(Rc::new(casted_value)), None => { if let Ok(obj) = obj.clone().into_any().downcast::() { - result.push(obj); + result.push(obj); } }, } } else { - panic!("RTObject of type Value expected: {}", obj.to_string()) + return Err(StoryError::InvalidStoryState(format!("RTObject of type Value expected: {}", obj.to_string()))); } } - result + Ok(result) } - fn and_op(&self, params: &[Rc]) -> Rc { + fn and_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Bool(op1) => match params[1].value { - ValueType::Bool(op2) => Rc::new(Value::new_bool(*op1 && op2)), - _ => panic!() + ValueType::Bool(op2) => Ok(Rc::new(Value::new_bool(*op1 && op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Rc::new(Value::new_bool(*op1 != 0 && op2 != 0)), - _ => panic!() + ValueType::Int(op2) => Ok(Rc::new(Value::new_bool(*op1 != 0 && op2 != 0))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Rc::new(Value::new_bool(*op1 != 0.0 && op2 != 0.0)), - _ => panic!() + ValueType::Float(op2) => Ok(Rc::new(Value::new_bool(*op1 != 0.0 && op2 != 0.0))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Rc::new(Value::new_bool(!op1.items.is_empty() && !op2.items.is_empty())), - _ => panic!() + ValueType::List(op2) => Ok(Rc::new(Value::new_bool(!op1.items.is_empty() && !op2.items.is_empty()))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn greater_op(&self, params: &[Rc]) -> Rc { + fn greater_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Rc::new(Value::new_bool(*op1 > op2)), - _ => panic!() + ValueType::Int(op2) => Ok(Rc::new(Value::new_bool(*op1 > op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Rc::new(Value::new_bool(*op1 > op2)), - _ => panic!() + ValueType::Float(op2) => Ok(Rc::new(Value::new_bool(*op1 > op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Rc::new(Value::new_bool(op1.greater_than(op2))), - _ => panic!() + ValueType::List(op2) => Ok(Rc::new(Value::new_bool(op1.greater_than(op2)))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn less_op(&self, params: &[Rc]) -> Rc { + fn less_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Rc::new(Value::new_bool(*op1 < op2)), - _ => panic!() + ValueType::Int(op2) => Ok(Rc::new(Value::new_bool(*op1 < op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Rc::new(Value::new_bool(*op1 < op2)), - _ => panic!() + ValueType::Float(op2) => Ok(Rc::new(Value::new_bool(*op1 < op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Rc::new(Value::new_bool(op1.less_than(op2))), - _ => panic!() + ValueType::List(op2) => Ok(Rc::new(Value::new_bool(op1.less_than(op2)))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn greater_than_or_equals_op(&self, params: &[Rc]) -> Rc { + fn greater_than_or_equals_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Rc::new(Value::new_bool(*op1 >= op2)), - _ => panic!() + ValueType::Int(op2) => Ok(Rc::new(Value::new_bool(*op1 >= op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Rc::new(Value::new_bool(*op1 >= op2)), - _ => panic!() + ValueType::Float(op2) => Ok(Rc::new(Value::new_bool(*op1 >= op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Rc::new(Value::new_bool(op1.greater_than_or_equals(op2))), - _ => panic!() + ValueType::List(op2) => Ok(Rc::new(Value::new_bool(op1.greater_than_or_equals(op2)))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn less_than_or_equals_op(&self, params: &[Rc]) -> Rc { + fn less_than_or_equals_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Rc::new(Value::new_bool(*op1 <= op2)), - _ => panic!() + ValueType::Int(op2) => Ok(Rc::new(Value::new_bool(*op1 <= op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Rc::new(Value::new_bool(*op1 <= op2)), - _ => panic!() + ValueType::Float(op2) => Ok(Rc::new(Value::new_bool(*op1 <= op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Rc::new(Value::new_bool(op1.less_than_or_equals(op2))), - _ => panic!() + ValueType::List(op2) => Ok(Rc::new(Value::new_bool(op1.less_than_or_equals(op2)))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn subtract_op(&self, params: &[Rc]) -> Rc { + fn subtract_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Rc::new(Value::new_int(*op1 - op2)), - _ => panic!() + ValueType::Int(op2) => Ok(Rc::new(Value::new_int(*op1 - op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Rc::new(Value::new_float(*op1 - op2)), - _ => panic!() + ValueType::Float(op2) => Ok(Rc::new(Value::new_float(*op1 - op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Rc::new(Value::new_list(op1.without(op2))), - _ => panic!() + ValueType::List(op2) => Ok(Rc::new(Value::new_list(op1.without(op2)))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn add_op(&self, params: &[Rc]) -> Rc { + fn add_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Rc::new(Value::new_int(op1 + op2)), - _ => panic!() + ValueType::Int(op2) => Ok(Rc::new(Value::new_int(op1 + op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Rc::new(Value::new_float(op1 + op2)), - _ => panic!() + ValueType::Float(op2) => Ok(Rc::new(Value::new_float(op1 + op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::String(op1) => match ¶ms[1].value { ValueType::String(op2) => { let mut sb = String::new(); sb.push_str(&op1.string); sb.push_str(&op2.string); - Rc::new(Value::new_string(&sb)) + Ok(Rc::new(Value::new_string(&sb))) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Rc::new(Value::new_list(op1.union(op2))), - _ => panic!() + ValueType::List(op2) => Ok(Rc::new(Value::new_list(op1.union(op2)))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn divide_op(&self, params: &[Rc]) -> Rc { + fn divide_op(&self, params: &[Rc]) -> Result, StoryError> { match params[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Rc::new(Value::new_int(op1 / op2)), - _ => panic!() + ValueType::Int(op2) => Ok(Rc::new(Value::new_int(op1 / op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Rc::new(Value::new_float(op1 / op2)), - _ => panic!() + ValueType::Float(op2) => Ok(Rc::new(Value::new_float(op1 / op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn pow_op(&self, params: &[Rc]) -> Rc { + fn pow_op(&self, params: &[Rc]) -> Result, StoryError> { match params[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Rc::new(Value::new_float((op1 as f32).powf(op2 as f32))), - _ => panic!() + ValueType::Int(op2) => Ok(Rc::new(Value::new_float((op1 as f32).powf(op2 as f32)))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Rc::new(Value::new_float(op1.powf(op2))), - _ => panic!() + ValueType::Float(op2) => Ok(Rc::new(Value::new_float(op1.powf(op2)))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn multiply_op(&self, params: &[Rc]) -> Rc { + fn multiply_op(&self, params: &[Rc]) -> Result, StoryError> { match params[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Rc::new(Value::new_int(op1 * op2)), - _ => panic!() + ValueType::Int(op2) => Ok(Rc::new(Value::new_int(op1 * op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Rc::new(Value::new_float(op1 * op2)), - _ => panic!() + ValueType::Float(op2) => Ok(Rc::new(Value::new_float(op1 * op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn or_op(&self, params: &[Rc]) -> Rc { + fn or_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Bool(op1) => match params[1].value { - ValueType::Bool(op2) => Rc::new(Value::new_bool(*op1 || op2)), - _ => panic!() + ValueType::Bool(op2) => Ok(Rc::new(Value::new_bool(*op1 || op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Rc::new(Value::new_bool(*op1 != 0 || op2 != 0)), - _ => panic!() + ValueType::Int(op2) => Ok(Rc::new(Value::new_bool(*op1 != 0 || op2 != 0))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Rc::new(Value::new_bool(*op1 != 0.0 || op2 != 0.0)), - _ => panic!() + ValueType::Float(op2) => Ok(Rc::new(Value::new_bool(*op1 != 0.0 || op2 != 0.0))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Rc::new(Value::new_bool(!op1.items.is_empty() || !op2.items.is_empty())), - _ => panic!() + ValueType::List(op2) => Ok(Rc::new(Value::new_bool(!op1.items.is_empty() || !op2.items.is_empty()))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn not_op(&self, params: &[Rc]) -> Rc { + fn not_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { - ValueType::Int(op1) => Rc::new(Value::new_bool(*op1 == 0)), - ValueType::Float(op1) => Rc::new(Value::new_bool(*op1 == 0.0)), - ValueType::List(op1) => Rc::new(Value::new_int(match op1.items.is_empty() { + ValueType::Int(op1) => Ok(Rc::new(Value::new_bool(*op1 == 0))), + ValueType::Float(op1) => Ok(Rc::new(Value::new_bool(*op1 == 0.0))), + ValueType::List(op1) => Ok(Rc::new(Value::new_int(match op1.items.is_empty() { true => 1, false => 0, - } )), - _ => panic!() + } ))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn min_op(&self, params: &[Rc]) -> Rc { + fn min_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Rc::new(Value::new_int(i32::min(*op1, op2))), - _ => panic!() + ValueType::Int(op2) => Ok(Rc::new(Value::new_int(i32::min(*op1, op2)))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Rc::new(Value::new_float(f32::min(*op1, op2))), - _ => panic!() + ValueType::Float(op2) => Ok(Rc::new(Value::new_float(f32::min(*op1, op2)))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::List(l) => todo!(), - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn max_op(&self, params: &[Rc]) -> Rc { + fn max_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Rc::new(Value::new_int(i32::max(*op1, op2))), - _ => panic!() + ValueType::Int(op2) => Ok(Rc::new(Value::new_int(i32::max(*op1, op2)))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Rc::new(Value::new_float(f32::max(*op1, op2))), - _ => panic!() + ValueType::Float(op2) => Ok(Rc::new(Value::new_float(f32::max(*op1, op2)))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::List(l) => todo!(), - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn equal_op(&self, params: &[Rc]) -> Rc { + fn equal_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Bool(op1) => match params[1].value { - ValueType::Bool(op2) => Rc::new(Value::new_bool(*op1 == op2)), - _ => panic!() + ValueType::Bool(op2) => Ok(Rc::new(Value::new_bool(*op1 == op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Rc::new(Value::new_bool(*op1 == op2)), - _ => panic!() + ValueType::Int(op2) => Ok(Rc::new(Value::new_bool(*op1 == op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Rc::new(Value::new_bool(*op1 == op2)), - _ => panic!() + ValueType::Float(op2) => Ok(Rc::new(Value::new_bool(*op1 == op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::String(op1) => match ¶ms[1].value { - ValueType::String(op2) => Rc::new(Value::new_bool(op1.string.eq(&op2.string))), - _ => panic!() + ValueType::String(op2) => Ok(Rc::new(Value::new_bool(op1.string.eq(&op2.string)))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Rc::new(Value::new_bool(op1.eq(op2))), - _ => panic!() + ValueType::List(op2) => Ok(Rc::new(Value::new_bool(op1.eq(op2)))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::DivertTarget(op1) => match ¶ms[1].value { - ValueType::DivertTarget(op2) => Rc::new(Value::new_bool(op1.eq(op2))), - _ => panic!() + ValueType::DivertTarget(op2) => Ok(Rc::new(Value::new_bool(op1.eq(op2)))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn not_equals_op(&self, params: &[Rc]) -> Rc { + fn not_equals_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Bool(op1) => match params[1].value { - ValueType::Bool(op2) => Rc::new(Value::new_bool(*op1 != op2)), - _ => panic!() + ValueType::Bool(op2) => Ok(Rc::new(Value::new_bool(*op1 != op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Rc::new(Value::new_bool(*op1 != op2)), - _ => panic!() + ValueType::Int(op2) => Ok(Rc::new(Value::new_bool(*op1 != op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Rc::new(Value::new_bool(*op1 != op2)), - _ => panic!() + ValueType::Float(op2) => Ok(Rc::new(Value::new_bool(*op1 != op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::String(op1) => match ¶ms[1].value { - ValueType::String(op2) => Rc::new(Value::new_bool(!op1.string.eq(&op2.string))), - _ => panic!() + ValueType::String(op2) => Ok(Rc::new(Value::new_bool(!op1.string.eq(&op2.string)))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Rc::new(Value::new_bool(!op1.eq(op2))), - _ => panic!() + ValueType::List(op2) => Ok(Rc::new(Value::new_bool(!op1.eq(op2)))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::DivertTarget(op1) => match ¶ms[1].value { - ValueType::DivertTarget(op2) => Rc::new(Value::new_bool(!op1.eq(op2))), - _ => panic!() + ValueType::DivertTarget(op2) => Ok(Rc::new(Value::new_bool(!op1.eq(op2)))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn mod_op(&self, params: &[Rc]) -> Rc { + fn mod_op(&self, params: &[Rc]) -> Result, StoryError> { match params[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Rc::new(Value::new_int(op1 % op2)), - _ => panic!() + ValueType::Int(op2) => Ok(Rc::new(Value::new_int(op1 % op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Rc::new(Value::new_float(op1 % op2)), - _ => panic!() + ValueType::Float(op2) => Ok(Rc::new(Value::new_float(op1 % op2))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn intersect_op(&self, params: &[Rc]) -> Rc { + fn intersect_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Rc::new(Value::new_list(op1.intersect(op2))), - _ => panic!() + ValueType::List(op2) => Ok(Rc::new(Value::new_list(op1.intersect(op2)))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn has(&self, params: &[Rc]) -> Rc { + fn has(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::String(op1) => match ¶ms[1].value { - ValueType::String(op2) => Rc::new(Value::new_bool(op1.string.contains(&op2.string))), - _ => panic!() + ValueType::String(op2) => Ok(Rc::new(Value::new_bool(op1.string.contains(&op2.string)))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Rc::new(Value::new_bool(op1.contains(op2))), - _ => panic!() + ValueType::List(op2) => Ok(Rc::new(Value::new_bool(op1.contains(op2)))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn hasnt(&self, params: &[Rc]) -> Rc { + fn hasnt(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::String(op1) => match ¶ms[1].value { - ValueType::String(op2) => Rc::new(Value::new_bool(!op1.string.contains(&op2.string))), - _ => panic!() + ValueType::String(op2) => Ok(Rc::new(Value::new_bool(!op1.string.contains(&op2.string)))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Rc::new(Value::new_bool(!op1.contains(op2))), - _ => panic!() + ValueType::List(op2) => Ok(Rc::new(Value::new_bool(!op1.contains(op2)))), + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn value_of_list_op(&self, params: &[Rc]) -> Rc { + fn value_of_list_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::List(op1) => { match op1.get_max_item() { - Some(i) => Rc::new(Value::new_int(i.1)), - None => Rc::new(Value::new_int(0)), + Some(i) => Ok(Rc::new(Value::new_int(i.1))), + None => Ok(Rc::new(Value::new_int(0))), } }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn all_op(&self, params: &[Rc]) -> Rc { + fn all_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::List(op1) => { - Rc::new(Value::new_list(op1.get_all())) + Ok(Rc::new(Value::new_list(op1.get_all()))) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn inverse_op(&self, params: &[Rc]) -> Rc { + fn inverse_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::List(op1) => { - Rc::new(Value::new_list(op1.inverse())) + Ok(Rc::new(Value::new_list(op1.inverse()))) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn count_op(&self, params: &[Rc]) -> Rc { + fn count_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::List(op1) => { - Rc::new(Value::new_int(op1.items.len() as i32)) + Ok(Rc::new(Value::new_int(op1.items.len() as i32))) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn list_max_op(&self, params: &[Rc]) -> Rc { + fn list_max_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::List(op1) => { - Rc::new(Value::new_list(op1.max_as_list())) + Ok(Rc::new(Value::new_list(op1.max_as_list()))) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn list_min_op(&self, params: &[Rc]) -> Rc { + fn list_min_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::List(op1) => { - Rc::new(Value::new_list(op1.min_as_list())) + Ok(Rc::new(Value::new_list(op1.min_as_list()))) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn negate_op(&self, params: &[Rc]) -> Rc { + fn negate_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Int(op1) => { - Rc::new(Value::new_int(-op1)) + Ok(Rc::new(Value::new_int(-op1))) }, ValueType::Float(op1) => { - Rc::new(Value::new_float(-op1)) + Ok(Rc::new(Value::new_float(-op1))) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn floor_op(&self, params: &[Rc]) -> Rc { + fn floor_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Int(op1) => { - Rc::new(Value::new_int(*op1)) + Ok(Rc::new(Value::new_int(*op1))) }, ValueType::Float(op1) => { - Rc::new(Value::new_float(op1.floor())) + Ok(Rc::new(Value::new_float(op1.floor()))) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn ceiling_op(&self, params: &[Rc]) -> Rc { + fn ceiling_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Int(op1) => { - Rc::new(Value::new_int(*op1)) + Ok(Rc::new(Value::new_int(*op1))) }, ValueType::Float(op1) => { - Rc::new(Value::new_float(op1.ceil())) + Ok(Rc::new(Value::new_float(op1.ceil()))) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn int_op(&self, params: &[Rc]) -> Rc { + fn int_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Int(op1) => { - Rc::new(Value::new_int(*op1)) + Ok(Rc::new(Value::new_int(*op1))) }, ValueType::Float(op1) => { - Rc::new(Value::new_int(*op1 as i32)) + Ok(Rc::new(Value::new_int(*op1 as i32))) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } - fn float_op(&self, params: &[Rc]) -> Rc { + fn float_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Int(op1) => { - Rc::new(Value::new_float(*op1 as f32)) + Ok(Rc::new(Value::new_float(*op1 as f32))) }, ValueType::Float(op1) => { - Rc::new(Value::new_float(*op1)) + Ok(Rc::new(Value::new_float(*op1))) }, - _ => panic!() + _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } } diff --git a/src/object.rs b/src/object.rs index c9878cd..56f4d22 100644 --- a/src/object.rs +++ b/src/object.rs @@ -98,7 +98,7 @@ impl Object { p = path.get_tail(); }; - return nearest_container.unwrap().content_at_path(&p, 0, -1); + nearest_container.unwrap().content_at_path(&p, 0, -1) } else { Object::get_root_container(rtobject).content_at_path(path, 0, -1) @@ -114,7 +114,7 @@ impl Object { let mut last_shared_path_comp_index:i32 = -1; for i in 0..min_path_length { - let own_comp = &own_path.as_ref().unwrap().get_component(i as usize); + let own_comp = &own_path.as_ref().unwrap().get_component(i); let other_comp = &global_path.get_component(i); if own_comp == other_comp { @@ -172,11 +172,17 @@ impl Object { match ancestor.into_any().downcast::() { Ok(c) => c.clone(), - _ => panic!("Impossible") + _ => panic!() // Not possible } } } +impl Default for Object { + fn default() -> Self { + Self::new() + } +} + pub trait IntoAny: AsAny { fn into_any(self: Rc) -> Rc; } @@ -213,7 +219,7 @@ impl RTObject for Null { impl fmt::Display for Null { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "**Null**") + write!(f, "Null") } } diff --git a/src/push_pop.rs b/src/push_pop.rs index 92c27b1..1a6ba71 100644 --- a/src/push_pop.rs +++ b/src/push_pop.rs @@ -1,3 +1,5 @@ +use crate::story_error::StoryError; + #[derive(PartialEq, Clone, Copy, Eq, Hash, Debug)] pub enum PushPopType { Tunnel, @@ -6,12 +8,12 @@ pub enum PushPopType { } impl PushPopType { - pub(crate) fn from_value(value: usize) -> PushPopType { + pub(crate) fn from_value(value: usize) -> Result { match value { - 0 => PushPopType::Tunnel, - 1 => PushPopType::Function, - 2 => PushPopType::FunctionEvaluationFromGame, - _ => panic!("Unexpected PushPopType value") + 0 => Ok(PushPopType::Tunnel), + 1 => Ok(PushPopType::Function), + 2 => Ok(PushPopType::FunctionEvaluationFromGame), + _ => Err(StoryError::BadJson("Unexpected PushPopType value".to_owned())) } } } \ No newline at end of file diff --git a/src/story.rs b/src/story.rs index 66bf78c..32be37e 100644 --- a/src/story.rs +++ b/src/story.rs @@ -573,7 +573,7 @@ impl Story { if current_content_obj.is_some() { if let Ok(choice_point) = current_content_obj.clone().unwrap().into_any().downcast::() { - let choice = self.process_choice(&choice_point); + let choice = self.process_choice(&choice_point)?; if choice.is_some() { self.get_state_mut().get_generated_choices_mut().push(choice.unwrap()); } @@ -767,7 +767,7 @@ impl Story { if let Ok(current_divert) = content_obj.clone().into_any().downcast::() { if current_divert.is_conditional { let o = self.get_state_mut().pop_evaluation_stack(); - if !self.is_truthy(o) { + if !self.is_truthy(o)? { return Ok(true); } } @@ -776,7 +776,7 @@ impl Story { let var_name = ¤t_divert.variable_divert_name; if let Some(var_contents) = self.get_state().get_variables_state().get_variable_with_name(var_name.as_ref().unwrap(), -1) { if let Some(target) = Value::get_divert_target_value(var_contents.as_ref()) { - let p = Self::pointer_at_path(&self.main_content_container, target); + let p = Self::pointer_at_path(&self.main_content_container, target)?; self.get_state_mut().set_diverted_pointer(p); } else { let error_message = format!( @@ -797,12 +797,7 @@ impl Story { return Err(StoryError::InvalidStoryState(error_message)); } } else { - // TODO - // error(format!( - // "Tried to divert using a target from a variable that could not be found ({})", - // var_name - // )); - panic!("Tried to divert using a target from a variable that could not be found ({})", var_name.as_ref().unwrap()); + return Err(StoryError::InvalidStoryState(format!("Tried to divert using a target from a variable that could not be found ({})", var_name.as_ref().unwrap()))); } } else if current_divert.is_external { //call_external_function(¤t_divert.get_target_path_string(), current_divert.get_external_args()); @@ -899,15 +894,14 @@ impl Story { expected = Some("end of flow (-> END or choice)".to_owned()); } - panic!("Found {}, when expected {}", names.get(&pop_type).unwrap(), expected.unwrap()); - //TODO error(errorMsg); + return Err(StoryError::InvalidStoryState(format!("Found {}, when expected {}", names.get(&pop_type).unwrap(), expected.unwrap()))); } else { self.get_state_mut().pop_callstack(None); // Does tunnel onwards override by diverting to a new ->-> // target? if let Some(override_tunnel_return_target) = override_tunnel_return_target { - let p = Self::pointer_at_path(&self.main_content_container, &override_tunnel_return_target); + let p = Self::pointer_at_path(&self.main_content_container, &override_tunnel_return_target)?; self.get_state_mut().set_diverted_pointer(p); } } @@ -915,9 +909,11 @@ impl Story { CommandType::BeginString => { self.get_state_mut().push_to_output_stream(content_obj.clone()); - assert!(self.get_state().get_in_expression_evaluation(), - "Expected to be in an expression when evaluating a string"); - self.get_state().set_in_expression_evaluation(false); + if !self.get_state().get_in_expression_evaluation() { + return Err(StoryError::InvalidStoryState("Expected to be in an expression when evaluating a string".to_owned())); + } + + self.get_state().set_in_expression_evaluation(false); }, CommandType::EndString => { @@ -982,14 +978,14 @@ impl Story { CommandType::TurnsSince | CommandType::ReadCount => { let target = self.get_state_mut().pop_evaluation_stack(); if Value::get_divert_target_value(target.as_ref()).is_none() { - let extra_note = ""; + let mut extra_note = "".to_owned(); if Value::get_int_value(target.as_ref()).is_some() { - // extraNote = ". Did you accidentally pass a read count ('knot_name') instead of a target " - // + "('-> knot_name')?"; + extra_note = format!(". Did you accidentally pass a read count ('knot_name') instead of a target {}", + "('-> knot_name')?").to_owned(); } - // error("TURNS_SINCE expected a divert target (knot, stitch, label name), but saw " + target - // + extra_note); - panic!(); + + return Err(StoryError::InvalidStoryState(format!("TURNS_SINCE expected a divert target (knot, stitch, label name), but saw {} {}", target + , extra_note))); } let target = Value::get_divert_target_value(target.as_ref()).unwrap(); @@ -1005,7 +1001,7 @@ impl Story { match container { Some(container) => { if eval_command.command_type == CommandType::TurnsSince { - either_count = self.get_state().turns_since_for_container(container.as_ref()); + either_count = self.get_state().turns_since_for_container(container.as_ref())?; } else {either_count = self.get_state_mut().visit_count_for_container(&container) as i32;} }, None => { @@ -1037,11 +1033,11 @@ impl Story { } if min_int.is_none() { - panic!("Invalid value for the minimum parameter of RANDOM(min, max)"); + return Err(StoryError::InvalidStoryState("Invalid value for the minimum parameter of RANDOM(min, max)".to_owned())); } if max_int.is_none() { - panic!("Invalid value for the maximum parameter of RANDOM(min, max)"); + return Err(StoryError::InvalidStoryState("Invalid value for the maximum parameter of RANDOM(min, max)".to_owned())); } let min_value = min_int.unwrap(); @@ -1050,13 +1046,10 @@ impl Story { let random_range = max_value - min_value + 1; if random_range <= 0 { - // TODO - // panic!(format!( - // "RANDOM was called with minimum as {} and maximum as {}. The maximum must be larger", - // min_value.to_string(), max_value.to_string() - // )); - - panic!(); + return Err(StoryError::InvalidStoryState(format!( + "RANDOM was called with minimum as {} and maximum as {}. The maximum must be larger", + min_value, max_value + ))); } let result_seed = self.get_state().story_seed + self.get_state().previous_random; @@ -1078,7 +1071,7 @@ impl Story { if let Some(v) = Value::get_int_value(o.as_ref()) {seed = Some(v);} if seed.is_none() { - panic!("Invalid value passed to SEED_RANDOM"); + return Err(StoryError::InvalidStoryState("Invalid value passed to SEED_RANDOM".to_owned())); } // Story seed affects both RANDOM and shuffle behaviour @@ -1096,7 +1089,7 @@ impl Story { self.get_state_mut().push_evaluation_stack(Rc::new(Value::new_int(count as i32))); }, CommandType::SequenceShuffleIndex => { - let shuffle_index = self.next_sequence_shuffle_index(); + let shuffle_index = self.next_sequence_shuffle_index()?; let v = Rc::new(Value::new_int(shuffle_index)); self.get_state_mut().push_evaluation_stack(v); }, @@ -1137,7 +1130,7 @@ impl Story { } if int_val.is_none() { - panic!("Passed non-integer when creating a list element from a numerical value."); + return Err(StoryError::InvalidStoryState("Passed non-integer when creating a list element from a numerical value.".to_owned())); } let mut generated_list_value: Option = None; @@ -1148,8 +1141,7 @@ impl Story { generated_list_value = Some(Value::new_list(l)); } } else { - //panic!(format!("Failed to find List called {}", list_name_val.as_ref().unwrap())); - panic!(); + return Err(StoryError::InvalidStoryState(format!("Failed to find List called {}", list_name_val.as_ref().unwrap()))); } if generated_list_value.is_none() { @@ -1169,8 +1161,9 @@ impl Story { p = self.get_state_mut().pop_evaluation_stack(); let target_list = Value::get_list_value(p.as_ref()); - if target_list.is_none() || min.is_err() || max.is_err() - {panic!("Expected List, minimum and maximum for LIST_RANGE");} + if target_list.is_none() || min.is_err() || max.is_err() { + return Err(StoryError::InvalidStoryState("Expected List, minimum and maximum for LIST_RANGE".to_owned())); + } let result = target_list.unwrap().list_with_sub_range(&min.unwrap().value, &max.unwrap().value); @@ -1180,7 +1173,9 @@ impl Story { let o = self.get_state_mut().pop_evaluation_stack(); let list = Value::get_list_value(o.as_ref()); - if list.is_none() {panic!("Expected list for LIST_RANDOM");} + if list.is_none() { + return Err(StoryError::InvalidStoryState("Expected list for LIST_RANDOM".to_owned())); + } let list = list.unwrap(); @@ -1203,7 +1198,7 @@ impl Story { let random_item = sorted[list_item_index]; // Origin list is simply the origin of the one element - let mut new_list = InkList::from_single_origin(random_item.0.get_origin_name().unwrap().clone(), self.list_definitions.as_ref()); + let mut new_list = InkList::from_single_origin(random_item.0.get_origin_name().unwrap().clone(), self.list_definitions.as_ref())?; new_list.items.insert(random_item.0.clone(), *random_item.1); self.get_state_mut().previous_random = next_random as i32; @@ -1258,8 +1253,7 @@ impl Story { if command.command_type == CommandType::BeginTag { break; } else { - panic!("Unexpected ControlCommand while extracting tag from choice"); - //break; + return Err(StoryError::InvalidStoryState("Unexpected ControlCommand while extracting tag from choice".to_owned())); } } @@ -1344,7 +1338,7 @@ impl Story { if let Some(func) = content_obj.as_ref().as_any().downcast_ref::() { let func_params = self.get_state_mut().pop_evaluation_stack_multiple(func.get_number_of_parameters()); - let result = func.call(func_params); + let result = func.call(func_params)?; self.get_state_mut().push_evaluation_stack(result); return Ok(true); @@ -1510,7 +1504,7 @@ impl Story { self.visit_changed_containers_due_to_divert(); } - fn is_truthy(&self, obj: Rc) -> bool { + fn is_truthy(&self, obj: Rc) -> Result { let truthy = false; if let Some(val) = obj.as_ref().as_any().downcast_ref::() { @@ -1519,22 +1513,22 @@ impl Story { // self.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 Ok(false); } return val.is_truthy(); } - return truthy; + return Ok(truthy); } - fn process_choice(&mut self, choice_point: &Rc) -> Option> { + fn process_choice(&mut self, choice_point: &Rc) -> Result>, StoryError> { let mut show_choice = true; // Don't create choice if choice point doesn't pass conditional if choice_point.has_condition() { let condition_value = self.get_state_mut().pop_evaluation_stack(); - if !self.is_truthy(condition_value) { + if !self.is_truthy(condition_value)? { show_choice = false; } } @@ -1563,14 +1557,14 @@ impl Story { // that we consume the content for it, since otherwise it'll // be shown on the output stream. if !show_choice { - return None; + return Ok(None); } start_text.push_str(&choice_only_text); let choice = Rc::new(Choice::new(choice_point.get_path_on_choice(), Object::get_path(choice_point.as_ref()).to_string(), choice_point.is_invisible_default(), tags, self.get_state().get_callstack().borrow_mut().fork_thread(), start_text.trim().to_string(), 0, 0)); - Some(choice) + Ok(Some(choice)) } fn pop_choice_string_and_tags(&mut self, tags: &mut Vec) -> String { @@ -1585,9 +1579,9 @@ impl Story { return choice_only_str_val.string.to_string(); } - pub fn pointer_at_path(main_content_container: &Rc, path: &Path) -> Pointer { + pub fn pointer_at_path(main_content_container: &Rc, path: &Path) -> Result { if path.len() == 0 { - return pointer::NULL.clone(); + return Ok(pointer::NULL.clone()); } let mut p = Pointer::default(); @@ -1613,11 +1607,10 @@ impl Story { let main_container: Rc = main_content_container.clone(); if Rc::ptr_eq(&result.obj, &main_container) && path_length_to_use > 0 { - // self.error(format!( - // "Failed to find content at path '{}', and no approximation of it was possible.", - // path - // )); - panic!() + return Err(StoryError::InvalidStoryState(format!( + "Failed to find content at path '{}', and no approximation of it was possible.", + path + ))); } else if result.approximate { // warning(format!( // "Failed to find content at path '{}', so it was approximated to: '{}'.", @@ -1626,7 +1619,7 @@ impl Story { // )); } - p + Ok(p) } fn visit_changed_containers_due_to_divert(&mut self) { @@ -1751,12 +1744,12 @@ impl Story { named_container.cloned() } - fn next_sequence_shuffle_index(&mut self) -> i32 { + fn next_sequence_shuffle_index(&mut self) -> Result { let pop_evaluation_stack = self.get_state_mut().pop_evaluation_stack(); let num_elements = if let Some(v) = Value::get_int_value(pop_evaluation_stack.as_ref()) { v } else { - panic!("Expected number of elements in sequence for shuffle index"); + return Err(StoryError::InvalidStoryState("Expected number of elements in sequence for shuffle index".to_owned())); }; let seq_container = self.get_state().get_current_pointer().container.unwrap(); @@ -1764,7 +1757,7 @@ impl Story { let seq_count = if let Some(v) = Value::get_int_value(pop_evaluation_stack.as_ref()) { v } else { - panic!("Expected sequence count value for shuffle index"); + return Err(StoryError::InvalidStoryState("Expected sequence count value for shuffle index".to_owned())); }; let loop_index = seq_count / num_elements; @@ -1788,11 +1781,11 @@ impl Story { unpicked_indices.retain(|&x| x != chosen_index); if i == iteration_index { - return chosen_index; + return Ok(chosen_index); } } - panic!("Should never reach here"); + return Err(StoryError::InvalidStoryState("Should never reach here".to_owned())); } pub fn get_global_tags(&self) -> Result, StoryError> { @@ -1894,20 +1887,24 @@ impl Story { self.get_state_mut().force_end(); } - pub fn switch_flow(&mut self, flow_name: &str) { + pub fn switch_flow(&mut self, flow_name: &str) -> Result<(), StoryError> { self.if_async_we_cant("switch flow"); if self.async_saving { - panic!("Story is already in background saving mode, can't switch flow to {}", flow_name); + return Err(StoryError::InvalidStoryState(format!("Story is already in background saving mode, can't switch flow to {}", flow_name))); } self.get_state_mut().switch_flow_internal(flow_name); + + Ok(()) } - fn if_async_we_cant(&self, activity_str: &str) { + fn if_async_we_cant(&self, activity_str: &str) -> Result<(), StoryError> { if self.async_continue_active { - panic!("Can't {}. Story is in the middle of a ContinueAsync(). Make more continue_async() calls or a single cont() call beforehand.", activity_str); + return Err(StoryError::InvalidStoryState(format!("Can't {}. Story is in the middle of a ContinueAsync(). Make more continue_async() calls or a single cont() call beforehand.", activity_str))); } + + Ok(()) } pub fn remove_flow(&mut self, flow_name: &str) { diff --git a/src/story_state.rs b/src/story_state.rs index c0b2699..bf7ebad 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -2,7 +2,7 @@ use std::{rc::Rc, cell::RefCell, collections::HashMap}; -use crate::{pointer::{Pointer, self}, callstack::CallStack, flow::Flow, variables_state::VariablesState, choice::Choice, object::{RTObject, Object}, value::Value, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}, container::Container, state_patch::StatePatch, story::{Story, INK_VERSION_CURRENT}, path::Path, void::Void, tag::Tag, list_definitions_origin::ListDefinitionsOrigin, value_type::ValueType, json_write_state, json_read, story_error::StoryError}; +use crate::{pointer::{Pointer, self}, callstack::CallStack, flow::Flow, variables_state::VariablesState, choice::Choice, object::{RTObject, Object}, value::Value, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}, container::Container, state_patch::StatePatch, story::{Story, INK_VERSION_CURRENT}, path::Path, void::Void, tag::Tag, list_definitions_origin::ListDefinitionsOrigin, value_type::ValueType, json_write, json_read, story_error::StoryError}; use rand::Rng; use serde_json::{json, Map}; @@ -299,10 +299,6 @@ impl StoryState { } pub fn set_current_pointer(&self, pointer: Pointer) { - if !pointer.container.is_none() && pointer.index >= pointer.container.as_ref().unwrap().content.len() as i32 { - panic!() - } - self.get_callstack().as_ref().borrow_mut().get_current_element_mut().current_pointer = pointer; } @@ -816,11 +812,11 @@ impl StoryState { self.diverted_pointer = p; } - pub fn set_chosen_path(&mut self, path: &Path, incrementing_turn_index: bool) { + pub fn set_chosen_path(&mut self, path: &Path, incrementing_turn_index: bool) -> Result<(), StoryError> { // Changing direction, assume we need to clear current set of choices self.current_flow.current_choices.clear(); - let mut new_pointer = Story::pointer_at_path(&self.main_content_container, &path); + let mut new_pointer = Story::pointer_at_path(&self.main_content_container, &path)?; if !new_pointer.is_null() && new_pointer.index == -1 { new_pointer.index = 0; } @@ -830,6 +826,8 @@ impl StoryState { if incrementing_turn_index { self.current_turn_index += 1; } + + Ok(()) } pub(crate) fn force_end(&mut self) { @@ -966,27 +964,25 @@ impl StoryState { Ok(None) } - pub(crate) fn turns_since_for_container(&self, container: &Container) -> i32 { + pub(crate) fn turns_since_for_container(&self, container: &Container) -> Result { if !container.turn_index_should_be_counted { - // story.error("TURNS_SINCE() for target (" + container.getName() + " - on " + container.getDebugMetadata() - // + ") unknown."); - panic!() + return Err(StoryError::InvalidStoryState(format!("TURNS_SINCE() for target ({}) unknown.", container.name.as_ref().unwrap()))); } let mut index = 0; if self.patch.is_some() && self.patch.as_ref().unwrap().get_turn_index(container).is_some() { index = *self.patch.as_ref().unwrap().get_turn_index(container).unwrap() as i32; - return self.current_turn_index - index; + return Ok(self.current_turn_index - index); } let container_path_str = Object::get_path(container).to_string(); if self.turn_indices.contains_key(&container_path_str) { index = *self.turn_indices.get(&container_path_str).unwrap() as i32; - self.current_turn_index - index + Ok(self.current_turn_index - index) } else { - -1 + Ok(-1) } } @@ -1022,25 +1018,25 @@ impl StoryState { self.output_stream_dirty(); } - pub fn visit_count_at_path_string(&self, path_string: &str) -> i32 { + pub fn visit_count_at_path_string(&self, path_string: &str) -> Result { let mut visit_count_out = None; if self.patch.is_some() { let container = self.main_content_container.content_at_path(&Path::new_with_components_string(Some(path_string)), 0, -1).container(); - if container.is_none() { panic!("Content at path not found: {}", path_string);} + if container.is_none() { return Err(StoryError::InvalidStoryState(format!("Content at path not found: {}", path_string)));} visit_count_out = self.patch.as_ref().unwrap().get_visit_count(container.as_ref().unwrap()); - if let Some(visit_count_out) = visit_count_out {return visit_count_out;} + if let Some(visit_count_out) = visit_count_out {return Ok(visit_count_out);} } visit_count_out = self.visit_counts.get(path_string).copied(); - if let Some(visit_count_out) = visit_count_out {return visit_count_out;} + if let Some(visit_count_out) = visit_count_out {return Ok(visit_count_out);} - 0 + Ok(0) } - pub fn to_json(&self) -> String { - self.write_json().to_string() + pub fn to_json(&self) -> Result { + Ok(self.write_json()?.to_string()) } pub fn load_json(&mut self, save_string: &str) -> Result<(), StoryError> { @@ -1050,19 +1046,19 @@ impl StoryState { } } - fn write_json(&self) -> serde_json::Value { + fn write_json(&self) -> Result { let mut obj: Map = Map::new(); // Flows let mut flows: Map = Map::new(); // current flow - flows.insert(self.current_flow.name.clone(), self.current_flow.write_json()); + flows.insert(self.current_flow.name.clone(), self.current_flow.write_json()?); // named flows if let Some(named_flows) = &self.named_flows { for (k,v) in named_flows { - flows.insert(k.clone(), v.write_json()); + flows.insert(k.clone(), v.write_json()?); } } @@ -1070,15 +1066,15 @@ impl StoryState { obj.insert("currentFlowName".to_owned(), json!(self.current_flow.name)); - obj.insert("variablesState".to_owned(), self.variables_state.write_json()); - obj.insert("evalStack".to_owned(), json_write_state::write_list_rt_objs(&self.evaluation_stack)); + obj.insert("variablesState".to_owned(), self.variables_state.write_json()?); + obj.insert("evalStack".to_owned(), json_write::write_list_rt_objs(&self.evaluation_stack)?); if !self.diverted_pointer.is_null() { obj.insert("currentDivertTarget".to_owned(), json!(self.diverted_pointer.get_path().unwrap().get_components_string())); } - obj.insert("visitCounts".to_owned(), json_write_state::write_int_dictionary(&self.visit_counts)); - obj.insert("turnIndices".to_owned(), json_write_state::write_int_dictionary(&self.turn_indices)); + obj.insert("visitCounts".to_owned(), json_write::write_int_dictionary(&self.visit_counts)); + obj.insert("turnIndices".to_owned(), json_write::write_int_dictionary(&self.turn_indices)); obj.insert("turnIdx".to_owned(), json!(self.current_turn_index)); obj.insert("storySeed".to_owned(), json!(self.story_seed)); @@ -1089,7 +1085,7 @@ impl StoryState { // Not using this right now, but could do in future. obj.insert("inkFormatVersion".to_owned(), json!(INK_VERSION_CURRENT)); - serde_json::Value::Object(obj) + Ok(serde_json::Value::Object(obj)) } fn load_json_obj(&mut self, j_object: serde_json::Value) -> Result<(), StoryError> { @@ -1192,7 +1188,7 @@ impl StoryState { if let Some(current_divert_target_path) = j_object.get("currentDivertTarget") { let divert_path = Path::new_with_components_string(current_divert_target_path.as_str()); - self.diverted_pointer = Story::pointer_at_path(&self.main_content_container, &divert_path).clone(); + self.diverted_pointer = Story::pointer_at_path(&self.main_content_container, &divert_path)?.clone(); } if let Some(visit_counts_obj) = j_object.get("visitCounts") { diff --git a/src/value.rs b/src/value.rs index 5725eda..c645792 100644 --- a/src/value.rs +++ b/src/value.rs @@ -1,6 +1,6 @@ use std::fmt; -use crate::{object::{RTObject, Object}, path::Path, ink_list::InkList, value_type::{StringValue, ValueType, VariablePointerValue}}; +use crate::{object::{RTObject, Object}, path::Path, ink_list::InkList, value_type::{StringValue, ValueType, VariablePointerValue}, story_error::StoryError}; const CAST_BOOL: u8 = 0; const CAST_INT: u8 = 1; @@ -72,22 +72,22 @@ impl Value { Self { obj: Object::new(), value: value_type } } - pub fn is_truthy(&self) -> bool { + pub fn is_truthy(&self) -> Result { match &self.value { - ValueType::Bool(v) => *v, - ValueType::Int(v) => *v != 0, - ValueType::Float(v) => *v != 0.0, - ValueType::String(v) => !v.string.is_empty(), - ValueType::DivertTarget(_) => panic!(), // exception Shouldn't be checking the truthiness of a divert target?? - ValueType::VariablePointer(_) => panic!(), - ValueType::List(l) => !l.items.is_empty(), + ValueType::Bool(v) => Ok(*v), + ValueType::Int(v) => Ok(*v != 0), + ValueType::Float(v) => Ok(*v != 0.0), + ValueType::String(v) => Ok(!v.string.is_empty()), + ValueType::DivertTarget(_) => Err(StoryError::InvalidStoryState("Shouldn't be checking the truthiness of a divert target".to_owned())), + ValueType::VariablePointer(_) => Err(StoryError::InvalidStoryState("Shouldn't be checking the truthiness of a variable pointer".to_owned())), + ValueType::List(l) => Ok(!l.items.is_empty()), } } pub fn get_string_value(o: &dyn RTObject) -> Option<&StringValue> { match o.as_any().downcast_ref::() { Some(v) => match &v.value { - ValueType::String(v) => Some(&v), + ValueType::String(v) => Some(v), _ => None, }, None => None, @@ -178,7 +178,7 @@ impl Value { if let Some(old_list) = Self::get_list_value(old_value) { if let Some(new_list) = Self::get_list_value(new_value) { - if new_list.items.len() == 0 { + if new_list.items.is_empty() { new_list.set_initial_origin_names(old_list.get_origin_names()); } } @@ -195,61 +195,61 @@ impl Value { } // If None is returned means that casting is not needed - pub fn cast(&self, cast_dest_type: u8) -> Option { + pub fn cast(&self, cast_dest_type: u8) -> Result, StoryError> { match &self.value { ValueType::Bool(v) => { match cast_dest_type { - CAST_BOOL => None, + CAST_BOOL => Ok(None), CAST_INT => if *v { - Some(Self::new_int(1)) + Ok(Some(Self::new_int(1))) } else { - Some(Self::new_int(0)) + Ok(Some(Self::new_int(0))) }, CAST_FLOAT => if *v { - Some(Self::new_float(1.0)) + Ok(Some(Self::new_float(1.0))) } else { - Some(Self::new_float(0.0)) + Ok(Some(Self::new_float(0.0))) }, CAST_STRING => if *v { - Some(Self::new_string("true")) + Ok(Some(Self::new_string("true"))) } else { - Some(Self::new_string("false")) + Ok(Some(Self::new_string("false"))) }, - _ => panic!(), + _ => Err(StoryError::InvalidStoryState("Cast not allowed for bool".to_owned())), } }, ValueType::Int(v) => { match cast_dest_type { CAST_BOOL => if *v == 0 { - Some(Self::new_bool(false)) + Ok(Some(Self::new_bool(false))) } else { - Some(Self::new_bool(true)) + Ok(Some(Self::new_bool(true))) }, - CAST_INT => None, - CAST_FLOAT => Some(Self::new_float(*v as f32)), - CAST_STRING => Some(Self::new_string(&*v.to_string())), - _ => panic!(), + CAST_INT => Ok(None), + CAST_FLOAT => Ok(Some(Self::new_float(*v as f32))), + CAST_STRING => Ok(Some(Self::new_string(&v.to_string()))), + _ => Err(StoryError::InvalidStoryState("Cast not allowed for int".to_owned())), } }, ValueType::Float(v) => { match cast_dest_type { CAST_BOOL => if *v == 0.0 { - Some(Self::new_bool(false)) + Ok(Some(Self::new_bool(false))) } else { - Some(Self::new_bool(true)) + Ok(Some(Self::new_bool(true))) }, - CAST_INT => Some(Self::new_int(*v as i32)), - CAST_FLOAT => None, - CAST_STRING => Some(Self::new_string(&*v.to_string())), - _ => panic!(), + CAST_INT => Ok(Some(Self::new_int(*v as i32))), + CAST_FLOAT => Ok(None), + CAST_STRING => Ok(Some(Self::new_string(&v.to_string()))), + _ => Err(StoryError::InvalidStoryState("Cast not allowed for float".to_owned())), } }, ValueType::String(v) => { match cast_dest_type { - CAST_INT => Some(Self::new_int(v.string.parse::().unwrap())), - CAST_FLOAT => Some(Self::new_float(v.string.parse::().unwrap())), - CAST_STRING => None, - _ => panic!(), + CAST_INT => Ok(Some(Self::new_int(v.string.parse::().unwrap()))), + CAST_FLOAT => Ok(Some(Self::new_float(v.string.parse::().unwrap()))), + CAST_STRING => Ok(None), + _ => Err(StoryError::InvalidStoryState("Cast not allowed for string".to_owned())), } }, ValueType::List(l) => { @@ -257,42 +257,42 @@ impl Value { CAST_INT => { let max = l.get_max_item(); match max { - Some(i) => Some(Self::new_int(i.1)), - None => Some(Self::new_int(0)) + Some(i) => Ok(Some(Self::new_int(i.1))), + None => Ok(Some(Self::new_int(0))) } }, CAST_FLOAT => { let max = l.get_max_item(); match max { - Some(i) => Some(Self::new_float(i.1 as f32)), - None => Some(Self::new_float(0.0)) + Some(i) => Ok(Some(Self::new_float(i.1 as f32))), + None => Ok(Some(Self::new_float(0.0))) } }, - CAST_LIST => None, + CAST_LIST => Ok(None), CAST_STRING => { let max = l.get_max_item(); match max { - Some(i) => Some(Self::new_string(&i.0.to_string())), - None => Some(Self::new_string("")) + Some(i) => Ok(Some(Self::new_string(&i.0.to_string()))), + None => Ok(Some(Self::new_string(""))) } }, - _ => panic!(), + _ => Err(StoryError::InvalidStoryState("Cast not allowed for list".to_owned())), } }, ValueType::DivertTarget(_) => { match cast_dest_type { CAST_DIVERT_TARGET => { - None + Ok(None) }, - _ => panic!(), + _ => Err(StoryError::InvalidStoryState("Cast not allowed for divert".to_owned())), } }, ValueType::VariablePointer(_) => { match cast_dest_type { CAST_VARIABLE_POINTER => { - None + Ok(None) }, - _ => panic!(), + _ => Err(StoryError::InvalidStoryState("Cast not allowed for variable pointer".to_owned())), } }, } diff --git a/src/variables_state.rs b/src/variables_state.rs index 61386f0..ae81001 100644 --- a/src/variables_state.rs +++ b/src/variables_state.rs @@ -2,7 +2,7 @@ use std::{collections::{HashMap, HashSet}, rc::Rc, cell::RefCell}; use serde_json::Map; -use crate::{callstack::CallStack, state_patch::StatePatch, variable_assigment::VariableAssignment, value::Value, list_definitions_origin::ListDefinitionsOrigin, value_type::{VariablePointerValue, ValueType}, json_write_state, json_read, story_error::StoryError}; +use crate::{callstack::CallStack, state_patch::StatePatch, variable_assigment::VariableAssignment, value::Value, list_definitions_origin::ListDefinitionsOrigin, value_type::{VariablePointerValue, ValueType}, json_write, json_read, story_error::StoryError}; #[derive(Clone)] @@ -295,7 +295,7 @@ impl VariablesState { self.callstack = callstack; } - pub(crate) fn write_json(&self) -> serde_json::Value { + pub(crate) fn write_json(&self) -> Result { let mut jobj: Map = Map::new(); for (name, val) in self.global_variables.iter() { @@ -306,10 +306,10 @@ impl VariablesState { if self.val_equal(val, default_val) {continue;} } - jobj.insert(name.clone(), json_write_state::write_rtobject(val.clone())); + jobj.insert(name.clone(), json_write::write_rtobject(val.clone())?); } - serde_json::Value::Object(jobj) + Ok(serde_json::Value::Object(jobj)) } fn val_equal(&self, val: &Value, default_val: &Value) -> bool { diff --git a/tests/list_test.rs b/tests/list_test.rs index 1ad36ac..1c8f1c6 100644 --- a/tests/list_test.rs +++ b/tests/list_test.rs @@ -54,7 +54,7 @@ fn list_save_load_test() -> Result<(), StoryError> { assert_eq!("a, x, c\n", &story.continue_maximally()?); - let saved_state = story.get_state().to_json(); + let saved_state = story.get_state().to_json()?; let mut story = Story::new(&json_string).unwrap(); diff --git a/tests/multi_flow_test.rs b/tests/multi_flow_test.rs index 7ee673f..d11692d 100644 --- a/tests/multi_flow_test.rs +++ b/tests/multi_flow_test.rs @@ -53,7 +53,7 @@ fn multiflow_save_load_threads() -> Result<(), StoryError> { assert_eq!("Thread 1 red choice", story.get_current_choices()[0].text); // Save/load test - let saved = story.get_state().to_json(); + let saved = story.get_state().to_json()?; // Test choice before reloading state before resetting story.choose_choice_index(0); diff --git a/tests/runtime_test.rs b/tests/runtime_test.rs index 132e575..6ee927e 100644 --- a/tests/runtime_test.rs +++ b/tests/runtime_test.rs @@ -125,8 +125,8 @@ fn read_visit_counts_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - assert_eq!(4, story.get_state().visit_count_at_path_string("two.s2")); - assert_eq!(5, story.get_state().visit_count_at_path_string("two")); + assert_eq!(4, story.get_state().visit_count_at_path_string("two.s2")?); + assert_eq!(5, story.get_state().visit_count_at_path_string("two")?); Ok(()) } @@ -143,7 +143,7 @@ fn load_save_test() -> Result<(), StoryError> { assert_eq!("We arrived into London at 9.45pm exactly.", text.get(0).unwrap()); // save the game state - let save_string = story.get_state().to_json(); + let save_string = story.get_state().to_json()?; println!("{}", save_string); diff --git a/tests/thread_test.rs b/tests/thread_test.rs index 9bc52c2..31ce4e1 100644 --- a/tests/thread_test.rs +++ b/tests/thread_test.rs @@ -39,7 +39,7 @@ fn thread_test_bug() -> Result<(), StoryError> { assert_eq!("No", story.get_current_choices()[0].text); assert_eq!("Yes", story.get_current_choices()[1].text); - let save_string = story.get_state().to_json(); + let save_string = story.get_state().to_json()?; println!("{}", save_string); let mut story = Story::new(&json_string).unwrap(); story.get_state_mut().load_json(&save_string)?; From 0e015d88cc30630b420bc6f8acbf76c9f2152c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Mon, 2 Oct 2023 08:06:27 +0000 Subject: [PATCH 54/91] Forgot to add new file after renamig --- src/json_write.rs | 258 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 src/json_write.rs diff --git a/src/json_write.rs b/src/json_write.rs new file mode 100644 index 0000000..674a495 --- /dev/null +++ b/src/json_write.rs @@ -0,0 +1,258 @@ +use std::{collections::HashMap, rc::Rc}; + +use serde_json::{Map, json}; + +use crate::{ + container::Container, + object::RTObject, value::Value, glue::Glue, choice_point::ChoicePoint, push_pop::PushPopType, divert::Divert, ink_list::InkList, control_command::ControlCommand, native_function_call::NativeFunctionCall, variable_reference::VariableReference, variable_assigment::VariableAssignment, tag::Tag, void::Void, choice::Choice, story_error::StoryError, +}; + +pub fn write_dictionary_values(objs: &HashMap>) -> Result { + let mut jobjs: Map = Map::new(); + + for (k,o) in objs { + jobjs.insert(k.clone(), write_rtobject(o.clone())?); + } + + Ok(serde_json::Value::Object(jobjs)) +} + +pub fn write_rtobject(o: Rc) -> Result { + if let Some(c) = o.as_any().downcast_ref::() { + return Ok(write_rt_container(c, false)?); + } + + if let Some(divert) = o.as_any().downcast_ref::() { + let mut div_type_key = "->"; + + if divert.is_external { div_type_key = "x()"; } + else if divert.pushes_to_stack { + if divert.stack_push_type == PushPopType::Function {div_type_key = "f()";} + else if divert.stack_push_type == PushPopType::Tunnel {div_type_key = "->t->";} + } + + let target_str = + if divert.has_variable_target() {divert.variable_divert_name.clone().unwrap()} + else {divert.get_target_path_string().unwrap()}; + + let mut jobj: Map = Map::new(); + + jobj.insert(div_type_key.to_string(), json!(target_str)); + + if divert.has_variable_target() {jobj.insert("var".to_owned(), json!(true));} + + if divert.is_conditional {jobj.insert("c".to_owned(), json!(true));} + + if divert.external_args > 0 {jobj.insert("exArgs".to_owned(), json!(divert.external_args));} + + return Ok(serde_json::Value::Object(jobj)); + } + + if let Ok(cp) = o.clone().into_any().downcast::() { + let mut jobj: Map = Map::new(); + jobj.insert("*".to_owned(), json!(ChoicePoint::get_path_string_on_choice(&cp))); + jobj.insert("flg".to_owned(), json!(cp.get_flags())); + return Ok(serde_json::Value::Object(jobj)); + } + + if let Some(v) = Value::get_bool_value(o.as_ref()) { + return Ok(json!(v)); + } + + if let Some(v) = Value::get_int_value(o.as_ref()) { + return Ok(json!(v)); + } + + if let Some(v) = Value::get_float_value(o.as_ref()) { + return Ok(json!(v)); + } + + if let Some(v) = Value::get_string_value(o.as_ref()) { + let mut s = String::new(); + + if v.is_newline { + s.push('\n'); + } else { + s.push('^'); + s.push_str(&v.string); + } + + return Ok(json!(s)); + } + + if let Some(v) = Value::get_list_value(o.as_ref()) { + return Ok(write_ink_list(v)); + } + + if let Some(v) = Value::get_divert_target_value(o.as_ref()) { + let mut jobj: Map = Map::new(); + jobj.insert("^->".to_owned(), json!(v.get_components_string())); + return Ok(serde_json::Value::Object(jobj)); + } + + if let Some(v) = Value::get_variable_pointer_value(o.as_ref()) { + let mut jobj: Map = Map::new(); + jobj.insert("^var".to_owned(), json!(v.variable_name)); + jobj.insert("ci".to_owned(), json!(v.context_index)); + return Ok(serde_json::Value::Object(jobj)); + } + + if o.as_any().is::() { + return Ok(json!("<>")); + } + + if let Some(cc) = o.as_any().downcast_ref::() { + return Ok(json!(ControlCommand::get_name(cc.command_type))); + } + + if let Some(f) = o.as_any().downcast_ref::() { + let mut name = NativeFunctionCall::get_name(f.op); + + // Avoid collision with ^ used to indicate a string + if "^".eq(&name) {name = "L^".to_owned();} + + return Ok(json!(name)); + } + + if let Ok(var_ref) = o.clone().into_any().downcast::() { + + let mut jobj: Map = Map::new(); + + let read_count_path = var_ref.get_path_string_for_count(); + if read_count_path.is_some() { + jobj.insert("CNT?".to_owned(), json!(read_count_path)); + } else { + jobj.insert("VAR?".to_owned(), json!(var_ref.name.clone())); + } + + return Ok(serde_json::Value::Object(jobj)); + } + + if let Some(var_ass) = o.as_any().downcast_ref::() { + let mut jobj: Map = Map::new(); + + let key = if var_ass.is_global {"VAR=".to_owned()} else {"temp=".to_owned()}; + jobj.insert(key, json!(var_ass.variable_name)); + + // Reassignment? + if !var_ass.is_new_declaration {jobj.insert("re".to_owned(), json!(true));} + + return Ok(serde_json::Value::Object(jobj)); + } + + if o.as_any().is::() { + return Ok(json!("void")); + } + + if let Some(tag) = o.as_any().downcast_ref::() { + let mut jobj: Map = Map::new(); + + jobj.insert("#".to_owned(), json!(tag.get_text())); + + return Ok(serde_json::Value::Object(jobj)); + } + + if let Some(choice) = o.as_any().downcast_ref::() { + return Ok(write_choice(choice)); + } + + return Err(StoryError::BadJson(format!("Failed to write runtime object to JSON: {}", o.to_string()))); +} + +pub fn write_rt_container(container: &Container, without_name: bool) -> Result { + let mut c_array: Vec = Vec::new(); + + for c in container.content.iter() { + c_array.push(write_rtobject(c.clone())?); + } + + // 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 + let named_only_content = &container.get_named_only_content(); + let count_flags = container.get_count_flags(); + let has_name_property = container.name.is_some() && !without_name; + + let has_terminator = !named_only_content.is_empty() || count_flags > 0 || has_name_property; + + if has_terminator { + let mut t_obj: Map = Map::new(); + + for (name, c) in named_only_content { + t_obj.insert(name.clone(), write_rt_container(c.as_ref(), true)?); + } + + if count_flags > 0 { + t_obj.insert("#f".to_owned(), json!(count_flags)); + } + + if has_name_property { + t_obj.insert("#n".to_owned(), json!(container.name)); + } + + c_array.push(serde_json::Value::Object(t_obj)); + } else { + c_array.push(serde_json::Value::Null); + } + + Ok(serde_json::Value::Array(c_array)) +} + +pub fn write_ink_list(list: &InkList) -> serde_json::Value { + let mut jobj: Map = Map::new(); + + let mut jlist: Map = Map::new(); + for (item, v) in list.items.iter() { + + let mut name = String::new(); + + match item.get_origin_name() { + Some(n) => name.push_str(n), + None => name.push('?'), + } + + name.push('.'); + name.push_str(item.get_item_name()); + + jlist.insert(name, json!(v)); + } + + jobj.insert("list".to_owned(), serde_json::Value::Object(jlist)); + + + serde_json::Value::Object(jobj) +} + +pub fn write_choice(choice: &Choice) -> serde_json::Value { + let mut jobj: Map = Map::new(); + + jobj.insert("text".to_owned(), json!(choice.text)); + jobj.insert("index".to_owned(), json!(choice.index)); + jobj.insert("originalChoicePath".to_owned(), json!(choice.source_path)); + jobj.insert("originalThreadIndex".to_owned(), json!(choice.original_thread_index)); + jobj.insert("targetPath".to_owned(), json!(choice.target_path.to_string())); + + serde_json::Value::Object(jobj) +} + +pub(crate) fn write_list_rt_objs(objs: &[Rc]) -> Result { + let mut c_array: Vec = Vec::new(); + + for o in objs { + c_array.push(write_rtobject(o.clone())?); + } + + Ok(serde_json::Value::Array(c_array)) +} + +pub(crate) fn write_int_dictionary(map: &HashMap) -> serde_json::Value { + let mut jobj: Map = Map::new(); + + for (key, val) in map { + jobj.insert(key.clone(), json!(*val)); + } + + serde_json::Value::Object(jobj) +} From 75a4b311004afa3d86e2ede88625551be80fca36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Mon, 2 Oct 2023 16:19:07 +0000 Subject: [PATCH 55/91] Manage errors in vector. --- README.md | 5 ++-- src/callstack.rs | 6 +--- src/container.rs | 3 -- src/story.rs | 75 ++++++++++++++++++++++++++-------------------- src/story_error.rs | 9 ++++++ src/story_state.rs | 18 +++++++---- 6 files changed, 68 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index cef7a44..09b44e9 100644 --- a/README.md +++ b/README.md @@ -24,14 +24,13 @@ Currently under development. This is the implementation status: ## TODO +- [ ] Error handling +- [ ] story.state -> quitar el Option y que guardar/salvar sea pub(crate). - [ ] Variable observers. - [ ] Optimize control command getname. Use static string array and address it by order. -- [ ] Error handling - [ ] Cache components string in Path - [ ] Use OnceCell to lazy init the cache fields of RTObjects - [ ] Split large files. ex. Get the error handling out of the Story class. The performLogic - [ ] Story.state y VariablesState.default_global_variables shouldn't be optionals. - [ ] Review all the .unwrap() and change it by .ok_or("xxx"). We need to avoid panics! -- [ ] Multi-flow methods. - diff --git a/src/callstack.rs b/src/callstack.rs index 2603377..72aefa1 100644 --- a/src/callstack.rs +++ b/src/callstack.rs @@ -71,12 +71,8 @@ impl Thread { let pointer_index = j_element_obj.get("idx").and_then(|i| i.as_i64()).ok_or(StoryError::BadJson("Invalid pointer index".to_owned()))? as i32; pointer.index = pointer_index; - // TODO - // if thread_pointer_result.obj.is_none() { - // return Err(format!("When loading state, internal story location couldn't be found: {}. Has the story changed since this save data was created?", current_container_path_str)); - // } else - if thread_pointer_result.approximate { + // TODO // story_context.warning(format!("When loading state, exact internal story location couldn't be found: '{}', so it was approximated to '{}' to recover. Has the story changed since this save data was created?", current_container_path_str, pointer_container.get_path().to_string())); } } diff --git a/src/container.rs b/src/container.rs index e0cbda7..129c898 100644 --- a/src/container.rs +++ b/src/container.rs @@ -204,14 +204,11 @@ impl Container { current_obj = found_obj.unwrap().clone(); current_container = if let Ok(container) = current_obj.clone().into_any().downcast::() { - - Some(container) } else { None }; } - SearchResult::new(current_obj, approximate) } diff --git a/src/story.rs b/src/story.rs index 32be37e..f5bc742 100644 --- a/src/story.rs +++ b/src/story.rs @@ -104,7 +104,7 @@ impl Story { list_definitions, }; - story.reset_state(); + story.reset_state()?; Ok(story) } @@ -117,17 +117,17 @@ impl Story { self.state.as_mut().unwrap() } - fn reset_state(&mut self) { - self.if_async_we_cant("ResetState"); + fn reset_state(&mut self) -> Result<(), StoryError> { + self.if_async_we_cant("ResetState")?; self.state = Some(StoryState::new(self.main_content_container.clone(), self.list_definitions.clone())); // TODO self.get_state_mut().get_variables_state().setVariableChangedEvent(this); - self.reset_globals(); + self.reset_globals() } - fn reset_globals(&mut self) { + fn reset_globals(&mut self) -> Result<(), StoryError> { if self.main_content_container.named_content.contains_key("global decl") { let original_pointer = self.get_state().get_current_pointer().clone(); @@ -135,12 +135,14 @@ impl Story { // Continue, but without validating external bindings, // since we may be doing this reset at initialisation time. - self.continue_internal(0.0); + self.continue_internal(0.0)?; self.get_state().set_current_pointer(original_pointer); } self.get_state_mut().get_variables_state_mut().snapshot_default_globals(); + + Ok(()) } pub fn build_string_of_hierarchy(&self) -> String { @@ -169,7 +171,7 @@ impl Story { } pub fn continue_maximally(&mut self) -> Result { - self.if_async_we_cant("continue_maximally"); + self.if_async_we_cant("continue_maximally")?; let mut sb = String::new(); @@ -229,14 +231,12 @@ impl Story { loop { match self.continue_single_step() { Ok(r) => output_stream_ends_in_newline = r, - Err(s) => { - //self.add_error(s, false, e.useEndLineNumber); + Err(e) => { + self.add_error(e.get_message(), false); break; } } - //println!("{}", self.build_string_of_hierarchy()); - if output_stream_ends_in_newline { break; } @@ -276,7 +276,7 @@ impl Story { .borrow() .can_pop_thread() { - self.add_error("Thread available to pop, threads should always be flat by the end of evaluation?"); + self.add_error("Thread available to pop, threads should always be flat by the end of evaluation?", false); } if self @@ -296,7 +296,7 @@ impl Story { .borrow() .can_pop_type(Some(PushPopType::Tunnel)) { - self.add_error("unexpectedly reached end of content. Do you need a '->->' to return from a tunnel?"); + self.add_error("unexpectedly reached end of content. Do you need a '->->' to return from a tunnel?", false); } else if self .state .as_ref() @@ -306,12 +306,12 @@ impl Story { .can_pop_type(Some(PushPopType::Function)) { self.add_error( - "unexpectedly reached end of content. Do you need a '~ return'?", + "unexpectedly reached end of content. Do you need a '~ return'?", false ); } else if !self.get_state().get_callstack().borrow().can_pop() { - self.add_error("ran out of content. Do you need a '-> DONE' or '-> END'?"); + self.add_error("ran out of content. Do you need a '-> DONE' or '-> END'?", false); } else { - self.add_error("unexpectedly reached end of content for unknown reason. Please debug compiler!"); + self.add_error("unexpectedly reached end of content for unknown reason. Please debug compiler!", false); } } } @@ -495,8 +495,21 @@ impl Story { } } - fn add_error(&self, arg: &str) { - todo!() + fn add_error(&mut self, message: &str, is_warning: bool) { + let error_type_str = if is_warning {"WARNING"} else {"ERROR"}; + + let m = + if !self.get_state().get_current_pointer().is_null() { + format!( + "RUNTIME {}: ({}): {}", + error_type_str, self.get_state().get_current_pointer().get_path().unwrap(), message) + } else { + format!("RUNTIME {}: {}", error_type_str, message) + }; + + self.get_state_mut().add_error(m, is_warning); + + self.get_state_mut().force_end() } fn reset_errors(&self) { @@ -896,7 +909,7 @@ impl Story { return Err(StoryError::InvalidStoryState(format!("Found {}, when expected {}", names.get(&pop_type).unwrap(), expected.unwrap()))); } else { - self.get_state_mut().pop_callstack(None); + self.get_state_mut().pop_callstack(None)?; // Does tunnel onwards override by diverting to a new ->-> // target? @@ -1009,9 +1022,8 @@ impl Story { either_count = -1; // turn count, default to never/unknown } else { either_count = 0; } // visit count, assume 0 to default to allowing entry - // warning("Failed to find container for " + evalCommand.toString() + " lookup at " - // + divertTarget.getTargetPath().toString()); - panic!() + self.add_error(&format!("Failed to find container for {} lookup at {}", eval_command + , target), true); } } @@ -1318,12 +1330,8 @@ impl Story { found_value = self.get_state().get_variables_state().get_variable_with_name(&var_ref.name, -1); - if let None = found_value { - // TODO - // self.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."); + if found_value.is_none() { + self.add_error(&format!("Variable not found: '{}'. 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.", var_ref.name), true); found_value = Some(Rc::new(Value::new_int(0))); } @@ -1348,7 +1356,7 @@ impl Story { Ok(false) } - fn next_content(&mut self) { + fn next_content(&mut self) -> Result<(), StoryError> { // Setting previousContentObject is critical for // VisitChangedContainersDueToDivert let cp = self.get_state().get_current_pointer(); @@ -1366,7 +1374,7 @@ impl Story { // Diverted location has valid content? if !self.get_state().get_current_pointer().is_null() { - return; + return Ok(()); } // Otherwise, if diverted location doesn't have valid content, @@ -1387,7 +1395,7 @@ impl Story { if can_pop_type { // Pop from the call stack - self.get_state_mut().pop_callstack(Some(PushPopType::Function)); + self.get_state_mut().pop_callstack(Some(PushPopType::Function))?; // This pop was due to dropping off the end of a function that // didn't return anything, @@ -1410,7 +1418,9 @@ impl Story { if didPop && !self.get_state().get_current_pointer().is_null() { self.next_content(); } - } + } + + Ok(()) } fn increment_content_pointer(&self) -> bool { @@ -1612,6 +1622,7 @@ impl Story { path ))); } else if result.approximate { + // TODO // warning(format!( // "Failed to find content at path '{}', so it was approximated to: '{}'.", // path, diff --git a/src/story_error.rs b/src/story_error.rs index b3cad4e..ff1e159 100644 --- a/src/story_error.rs +++ b/src/story_error.rs @@ -6,6 +6,15 @@ pub enum StoryError { BadJson(String), BadArgument(String), } +impl StoryError { + pub(crate) fn get_message(&self) -> &str { + match self { + StoryError::InvalidStoryState(msg) | + StoryError::BadJson(msg) | + StoryError::BadArgument(msg) => msg.as_str(), + } + } +} impl std::error::Error for StoryError {} diff --git a/src/story_state.rs b/src/story_state.rs index bf7ebad..4e24609 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -173,8 +173,8 @@ impl StoryState { None => None, }; - if !in_tag && text_content.is_some() { - sb.push_str(&text_content.unwrap().string); + if let (false, Some(text_content)) = (in_tag, text_content) { + sb.push_str(&text_content.string); } else if let Some(control_command) = output_obj.as_ref().as_any().downcast_ref::() { if control_command.command_type == CommandType::BeginTag { in_tag = true; @@ -658,14 +658,14 @@ impl StoryState { return true; } - return false; + false } - pub fn pop_callstack(&mut self, t: Option) { + pub fn pop_callstack(&mut self, t: Option) -> Result<(), StoryError> { // Add the end of a function call, trim any whitespace from the end. if self.get_callstack().borrow().get_current_element().push_pop_type == PushPopType::Function {self.trim_whitespace_from_function_end();} - self.get_callstack().borrow_mut().pop(t); + self.get_callstack().borrow_mut().pop(t) } fn go_to_start(&self) { @@ -1236,4 +1236,12 @@ impl StoryState { self.switch_flow_internal(DEFAULT_FLOW_NAME); } } + + pub(crate) fn add_error(&mut self, message: String, is_warning: bool) { + if !is_warning { + self.current_errors.push(message); + } else { + self.current_warnings.push(message); + } + } } \ No newline at end of file From 18e83879237cd81528ffcbfcc1115e01be5e3954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Mon, 2 Oct 2023 17:25:55 +0000 Subject: [PATCH 56/91] Story.state as not optional --- src/story.rs | 129 +++++++++++++++++---------------------- src/story_state.rs | 28 ++++----- tests/multi_flow_test.rs | 24 ++++---- 3 files changed, 78 insertions(+), 103 deletions(-) diff --git a/src/story.rs b/src/story.rs index f5bc742..beaa2ce 100644 --- a/src/story.rs +++ b/src/story.rs @@ -24,8 +24,8 @@ enum OutputStateChange { pub struct Story { main_content_container: Rc, - state: Option, - temporaty_evaluation_container: Option>, + state: StoryState, + temporary_evaluation_container: Option>, recursive_continue_count: usize, async_continue_active: bool, async_saving: bool, @@ -90,10 +90,12 @@ impl Story { return Err(StoryError::BadJson("Root node for ink is not a container?".to_owned())); }; + let main_content_container = main_content_container.unwrap(); // unwrap: checked for err above + let mut story = Story { - main_content_container: main_content_container.unwrap(), - state: None, - temporaty_evaluation_container: None, + main_content_container: main_content_container.clone(), + state: StoryState::new(main_content_container.clone(), list_definitions.clone()), + temporary_evaluation_container: None, recursive_continue_count: 0, async_continue_active: false, async_saving: false, @@ -103,28 +105,21 @@ impl Story { prev_containers: Vec::new(), list_definitions, }; + // TODO self.get_state_mut().get_variables_state().setVariableChangedEvent(this); - story.reset_state()?; + story.reset_globals()?; Ok(story) } + #[inline] pub fn get_state(&self) -> &StoryState { - self.state.as_ref().unwrap() + &self.state } + #[inline] pub fn get_state_mut(&mut self) -> &mut StoryState { - self.state.as_mut().unwrap() - } - - fn reset_state(&mut self) -> Result<(), StoryError> { - self.if_async_we_cant("ResetState")?; - - self.state = Some(StoryState::new(self.main_content_container.clone(), self.list_definitions.clone())); - - // TODO self.get_state_mut().get_variables_state().setVariableChangedEvent(this); - - self.reset_globals() + &mut self.state } fn reset_globals(&mut self) -> Result<(), StoryError> { @@ -167,7 +162,7 @@ impl Story { pub fn cont(&mut self) -> Result { self.continue_async(0.0)?; - Ok(self.get_current_text()) + self.get_current_text() } pub fn continue_maximally(&mut self) -> Result { @@ -215,8 +210,6 @@ impl Story { // for the outermost call. if self.recursive_continue_count == 1 { self.state - .as_mut() - .unwrap() .get_variables_state_mut() .set_batch_observing_variable_changes(true); } @@ -270,8 +263,6 @@ impl Story { if !self.can_continue() { if self .state - .as_ref() - .unwrap() .get_callstack() .borrow() .can_pop_thread() @@ -281,17 +272,13 @@ impl Story { if self .state - .as_ref() - .unwrap() .get_generated_choices() .is_empty() && !self.get_state().is_did_safe_exit() - && self.temporaty_evaluation_container.is_none() + && self.temporary_evaluation_container.is_none() { if self .state - .as_ref() - .unwrap() .get_callstack() .borrow() .can_pop_type(Some(PushPopType::Tunnel)) @@ -299,8 +286,6 @@ impl Story { self.add_error("unexpectedly reached end of content. Do you need a '->->' to return from a tunnel?", false); } else if self .state - .as_ref() - .unwrap() .get_callstack() .borrow() .can_pop_type(Some(PushPopType::Function)) @@ -320,8 +305,6 @@ impl Story { if self.recursive_continue_count == 1 { self.state - .as_mut() - .unwrap() .get_variables_state_mut() .set_batch_observing_variable_changes(false); } @@ -339,13 +322,13 @@ impl Story { Some(on_err) => { if self.get_state().has_error() { for err in self.get_state().get_current_errors() { - (on_err)(&err, ErrorType::Error); + (on_err)(err, ErrorType::Error); } } if self.get_state().has_warning() { for err in self.get_state().get_current_warnings() { - (on_err)(&err, ErrorType::Warning); + (on_err)(err, ErrorType::Warning); } } @@ -415,9 +398,9 @@ impl Story { // that was previously added is definitely the end of the line. let change = Story::calculate_newline_output_state_change( &state_snapshot_at_last_new_line.get_current_text(), - &self.state.as_mut().unwrap().get_current_text(), + &self.state.get_current_text(), state_snapshot_at_last_new_line.get_current_tags().len() as i32, - self.state.as_mut().unwrap().get_current_tags().len() as i32); + self.state.get_current_tags().len() as i32); // The last time we saw a newline, it was definitely the end of the line, so we // want to rewind to that point. @@ -465,13 +448,13 @@ impl Story { Ok(false) } - pub fn get_current_text(&mut self) -> String { - self.if_async_we_cant("call currentText since it's a work in progress"); - self.get_state_mut().get_current_text() + pub fn get_current_text(&mut self) -> Result { + self.if_async_we_cant("call currentText since it's a work in progress")?; + Ok(self.get_state_mut().get_current_text()) } pub(crate) fn get_main_content_container(&self) -> Rc { - match self.temporaty_evaluation_container.as_ref() { + match self.temporary_evaluation_container.as_ref() { Some(c) => c.clone(), None => self.main_content_container.clone(), } @@ -483,9 +466,9 @@ impl Story { // 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. - self.state_snapshot_at_last_new_line.as_mut().unwrap().restore_after_patch(); + self.state_snapshot_at_last_new_line.as_mut().unwrap().restore_after_patch(); //unwrap: state_snapshot_at_last_new_line checked Some in previous fn - self.state = self.state_snapshot_at_last_new_line.take(); + self.state = self.state_snapshot_at_last_new_line.take().unwrap(); // If save completed while the above snapshot was // active, we need to apply any changes made since @@ -583,12 +566,12 @@ impl Story { } // Choice with condition? - if current_content_obj.is_some() { - if let Ok(choice_point) = current_content_obj.clone().unwrap().into_any().downcast::() { + if let Some(cco) = ¤t_content_obj { + if let Ok(choice_point) = cco.clone().into_any().downcast::() { let choice = self.process_choice(&choice_point)?; - if choice.is_some() { - self.get_state_mut().get_generated_choices_mut().push(choice.unwrap()); + if let Some(choice) = choice { + self.get_state_mut().get_generated_choices_mut().push(choice); } current_content_obj = None; @@ -632,7 +615,7 @@ impl Story { } // Increment the content pointer, following diverts if necessary - self.next_content(); + self.next_content()?; // Starting a thread should be done after the increment to the content // pointer, @@ -666,7 +649,7 @@ impl Story { } } - if invisible_choices.len() == 0 || all_choices.len() > invisible_choices.len() { + if invisible_choices.is_empty() || all_choices.len() > invisible_choices.len() { return; } @@ -696,7 +679,7 @@ impl Story { // Simple case: nothing's changed, and we still have a newline // at the end of the current content let newline_still_exists = curr_text.len() >= prev_text.len() - && prev_text.len() > 0 + && !prev_text.is_empty() && curr_text.chars().nth(prev_text.len() - 1) == Some('\n'); if prev_tag_count == curr_tag_count && prev_text.len() == curr_text.len() @@ -729,8 +712,10 @@ impl Story { } fn state_snapshot(&mut self) { - self.state_snapshot_at_last_new_line = self.state.take(); - self.state = Some(self.state_snapshot_at_last_new_line.as_ref().unwrap().copy_and_start_patching()); + // tmp_state contains the new state and current state is stored in snapshot + let mut tmp_state = self.state.copy_and_start_patching(); + std::mem::swap(&mut tmp_state, &mut self.state); + self.state_snapshot_at_last_new_line = Some(tmp_state); } fn discard_snapshot(&mut self) { @@ -841,13 +826,13 @@ impl Story { CommandType::EvalOutput => { // If the expression turned out to be empty, there may not be // anything on the stack - if self.get_state().evaluation_stack.len() > 0 { + if !self.get_state().evaluation_stack.is_empty() { let output = self.get_state_mut().pop_evaluation_stack(); // Functions may evaluate to Void, in which case we skip // output - if let None = output.as_ref().as_any().downcast_ref::() { + if !output.as_ref().as_any().is::() { // TODO: Should we really always blanket convert to // string? // It would be okay to have numbers in the output stream @@ -1015,7 +1000,7 @@ impl Story { Some(container) => { if eval_command.command_type == CommandType::TurnsSince { either_count = self.get_state().turns_since_for_container(container.as_ref())?; - } else {either_count = self.get_state_mut().visit_count_for_container(&container) as i32;} + } else {either_count = self.get_state_mut().visit_count_for_container(&container);} }, None => { if eval_command.command_type == CommandType::TurnsSince { @@ -1096,9 +1081,8 @@ impl Story { CommandType::VisitIndex => { let cpc = self.get_state().get_current_pointer().container.unwrap(); let count = self.get_state_mut().visit_count_for_container(&cpc) - 1; // index - // not - // count - self.get_state_mut().push_evaluation_stack(Rc::new(Value::new_int(count as i32))); + // not count + self.get_state_mut().push_evaluation_stack(Rc::new(Value::new_int(count))); }, CommandType::SequenceShuffleIndex => { let shuffle_index = self.next_sequence_shuffle_index()?; @@ -1113,7 +1097,7 @@ impl Story { // act of creating the thread, or in the context of // evaluating the content. if self.get_state().get_callstack().borrow().can_pop_thread() { - self.get_state().get_callstack().as_ref().borrow_mut().pop_thread(); + self.get_state().get_callstack().as_ref().borrow_mut().pop_thread()?; } // In normal flow - allow safe exit without warning @@ -1147,7 +1131,7 @@ impl Story { let mut generated_list_value: Option = None; - if let Some(found_list_def) = self.list_definitions.as_ref().get_list_definition(&list_name_val.as_ref().unwrap()) { + if let Some(found_list_def) = self.list_definitions.as_ref().get_list_definition(list_name_val.as_ref().unwrap()) { if let Some(found_item) = found_list_def.get_item_with_value(int_val.unwrap()) { let l = InkList::from_single_element((found_item.clone(), int_val.unwrap())); generated_list_value = Some(Value::new_list(l)); @@ -1279,7 +1263,7 @@ impl Story { let mut sb = String::new(); for str_val in &content_stack_for_tag { - sb.push_str(&str_val); + sb.push_str(str_val); } let choice_tag = Rc::new(Tag::new(&StoryState::clean_output_whitespace(&sb))); @@ -1322,7 +1306,7 @@ impl Story { if let Some(p) = &var_ref.path_for_count { let container = var_ref.get_container_for_count(); let count = self.get_state_mut().visit_count_for_container(container.as_ref().unwrap()); - found_value = Some(Rc::new(Value::new_int(count as i32))); + found_value = Some(Rc::new(Value::new_int(count))); } // Normal variable reference @@ -1389,7 +1373,7 @@ impl Story { // or finish evaluating the content of a thread if !successful_pointer_increment { - let mut didPop = false; + let mut did_pop = false; let can_pop_type = self.get_state().get_callstack().as_ref().borrow().can_pop_type(Some(PushPopType::Function)); if can_pop_type { @@ -1405,18 +1389,18 @@ impl Story { self.get_state_mut().push_evaluation_stack(Rc::new(Void::new())); } - didPop = true; + did_pop = true; } else if self.get_state().get_callstack().as_ref().borrow().can_pop_thread() { - self.get_state().get_callstack().as_ref().borrow_mut().pop_thread(); + self.get_state().get_callstack().as_ref().borrow_mut().pop_thread()?; - didPop = true; + did_pop = true; } else { self.get_state_mut().try_exit_function_evaluation_from_game(); } // Step past the point where we last called out - if didPop && !self.get_state().get_current_pointer().is_null() { - self.next_content(); + if did_pop && !self.get_state().get_current_pointer().is_null() { + self.next_content()?; } } @@ -1465,7 +1449,7 @@ impl Story { self.get_state().get_callstack().as_ref().borrow_mut().get_current_element_mut().current_pointer = pointer; - return successful_increment; + successful_increment } pub fn get_current_choices(&self) -> Vec> { @@ -1519,11 +1503,8 @@ impl Story { if let Some(val) = obj.as_ref().as_any().downcast_ref::() { - if let Some(_) = Value::get_divert_target_value(obj.as_ref()) { - // self.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 Ok(false); + if let Some(target_path) = Value::get_divert_target_value(obj.as_ref()) { + return Err(StoryError::InvalidStoryState(format!("Shouldn't use a divert target (to {}) as a conditional value. Did you intend a function call 'likeThis()' or a read count check 'likeThis'? (no arrows)", target_path))); } return val.is_truthy(); @@ -1597,7 +1578,7 @@ impl Story { let mut p = Pointer::default(); let mut path_length_to_use = path.len() as i32; - + let result: SearchResult = if path.get_last_component().unwrap().is_index() { path_length_to_use -= 1; diff --git a/src/story_state.rs b/src/story_state.rs index 4e24609..c3ca50b 100644 --- a/src/story_state.rs +++ b/src/story_state.rs @@ -794,11 +794,7 @@ impl StoryState { } pub fn pop_evaluation_stack(&mut self) -> Rc { - let obj = self.evaluation_stack.pop().unwrap(); - - println!("POP: {}", obj.to_string()); - - obj + self.evaluation_stack.pop().unwrap() } pub fn pop_evaluation_stack_multiple(&mut self, number_of_objects: usize) -> Vec> { @@ -816,7 +812,7 @@ impl StoryState { // Changing direction, assume we need to clear current set of choices self.current_flow.current_choices.clear(); - let mut new_pointer = Story::pointer_at_path(&self.main_content_container, &path)?; + let mut new_pointer = Story::pointer_at_path(&self.main_content_container, path)?; if !new_pointer.is_null() && new_pointer.index == -1 { new_pointer.index = 0; } @@ -860,7 +856,7 @@ impl StoryState { let mut i = self.get_output_stream().len() as isize - 1; while i >= function_start_point as isize { if let Some(obj) = self.get_output_stream().get(i as usize) { - if let Some(_) = obj.as_any().downcast_ref::() { + if obj.as_any().is::() { break; } @@ -940,11 +936,11 @@ impl StoryState { } // Finally, pop the external function evaluation - self.get_callstack().borrow_mut().pop(Some(PushPopType::FunctionEvaluationFromGame)); + self.get_callstack().borrow_mut().pop(Some(PushPopType::FunctionEvaluationFromGame))?; // What did we get back? if let Some(returned_obj) = returned_obj{ - if let Some(_) = returned_obj.as_ref().as_any().downcast_ref::() { return Ok(None); } + if returned_obj.as_ref().as_any().is::() { return Ok(None); } // Some kind of value, if not void if let Some(return_val) = returned_obj.as_ref().as_any().downcast_ref::() { @@ -969,17 +965,15 @@ impl StoryState { return Err(StoryError::InvalidStoryState(format!("TURNS_SINCE() for target ({}) unknown.", container.name.as_ref().unwrap()))); } - let mut index = 0; - if self.patch.is_some() && self.patch.as_ref().unwrap().get_turn_index(container).is_some() { - index = *self.patch.as_ref().unwrap().get_turn_index(container).unwrap() as i32; + let index = *self.patch.as_ref().unwrap().get_turn_index(container).unwrap(); return Ok(self.current_turn_index - index); } let container_path_str = Object::get_path(container).to_string(); if self.turn_indices.contains_key(&container_path_str) { - index = *self.turn_indices.get(&container_path_str).unwrap() as i32; + let index = *self.turn_indices.get(&container_path_str).unwrap(); Ok(self.current_turn_index - index) } else { Ok(-1) @@ -1157,17 +1151,17 @@ impl StoryState { // current/default flow else { self.named_flows = None; - self.current_flow.name = "default".to_string(); // Replace with the default flow name + self.current_flow.name = "default".to_owned(); // Replace with the default flow name self.current_flow .callstack - .borrow_mut().load_json(&self.main_content_container, j_object.get("callstackThreads").and_then(|o| o.as_object()).ok_or(StoryError::BadJson("loading callstack threads".to_owned()))?); + .borrow_mut().load_json(&self.main_content_container, j_object.get("callstackThreads").and_then(|o| o.as_object()).ok_or(StoryError::BadJson("loading callstack threads".to_owned()))?)?; if let Some(output_stream_obj) = j_object.get("outputStream") { - self.current_flow.output_stream = json_read::jarray_to_runtime_obj_list(&output_stream_obj.as_array().unwrap(), false)?; + self.current_flow.output_stream = json_read::jarray_to_runtime_obj_list(output_stream_obj.as_array().unwrap(), false)?; } if let Some(current_choices_obj) = j_object.get("currentChoices") { - self.current_flow.current_choices = json_read::jarray_to_runtime_obj_list(¤t_choices_obj.as_array().unwrap(), false)?.iter().map(|o| o.clone().into_any().downcast::().unwrap()).collect(); + self.current_flow.current_choices = json_read::jarray_to_runtime_obj_list(current_choices_obj.as_array().unwrap(), false)?.iter().map(|o| o.clone().into_any().downcast::().unwrap()).collect(); } let j_choice_threads_obj = j_object.get("choiceThreads"); diff --git a/tests/multi_flow_test.rs b/tests/multi_flow_test.rs index d11692d..95dda3d 100644 --- a/tests/multi_flow_test.rs +++ b/tests/multi_flow_test.rs @@ -8,18 +8,18 @@ fn basics_test() -> Result<(), StoryError> { common::get_json_string("examples/inkfiles/runtime/multiflow-basics.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); - story.switch_flow("First"); + story.switch_flow("First")?; story.choose_path_string("knot1", true, None)?; assert_eq!("knot 1 line 1\n", story.cont()?); - story.switch_flow("Second"); + story.switch_flow("Second")?; story.choose_path_string("knot2", true, None)?; assert_eq!("knot 2 line 1\n", story.cont()?); - story.switch_flow("First"); + story.switch_flow("First")?; assert_eq!("knot 1 line 2\n", story.cont()?); - story.switch_flow("Second"); + story.switch_flow("Second")?; assert_eq!("knot 2 line 2\n", story.cont()?); Ok(()) @@ -34,22 +34,22 @@ fn multiflow_save_load_threads() -> Result<(), StoryError> { // Default flow assert_eq!("Default line 1\n", story.cont()?); - story.switch_flow("Blue Flow"); + story.switch_flow("Blue Flow")?; story.choose_path_string("blue", true, None)?; assert_eq!("Hello I'm blue\n", story.cont()?); - story.switch_flow("Red Flow"); + story.switch_flow("Red Flow")?; story.choose_path_string("red", true, None)?; assert_eq!("Hello I'm red\n", story.cont()?); // Test existing state remains after switch (blue) - story.switch_flow("Blue Flow"); - assert_eq!("Hello I'm blue\n", story.get_current_text()); + story.switch_flow("Blue Flow")?; + assert_eq!("Hello I'm blue\n", story.get_current_text()?); assert_eq!("Thread 1 blue choice", story.get_current_choices()[0].text); // Test existing state remains after switch (red) - story.switch_flow("Red Flow"); - assert_eq!("Hello I'm red\n", story.get_current_text()); + story.switch_flow("Red Flow")?; + assert_eq!("Hello I'm red\n", story.get_current_text()?); assert_eq!("Thread 1 red choice", story.get_current_choices()[0].text); // Save/load test @@ -68,13 +68,13 @@ fn multiflow_save_load_threads() -> Result<(), StoryError> { // Load: switch to blue, choose 1 story.get_state_mut().load_json(&saved)?; - story.switch_flow("Blue Flow"); + story.switch_flow("Blue Flow")?; story.choose_choice_index(0); assert_eq!("Thread 1 blue choice\nAfter thread 1 choice (blue)\n", story.continue_maximally()?); // Load: switch to blue, choose 2 story.get_state_mut().load_json(&saved)?; - story.switch_flow("Blue Flow"); + story.switch_flow("Blue Flow")?; story.choose_choice_index(1); assert_eq!("Thread 2 blue choice\nAfter thread 2 choice (blue)\n", story.continue_maximally()?); From 9bcf9b2efe0d36d308265f2f585bd2dae5691a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Tue, 3 Oct 2023 12:30:38 +0000 Subject: [PATCH 57/91] Changed project structure to cargo workspace. --- .vscode/settings.json | 5 +++ Cargo.toml | 31 ++++---------- README.md | 13 +++--- cli-player/Cargo.toml | 13 ++++++ .../src/cli_player.rs | 0 clib/Cargo.toml | 12 ++++++ clib/src/lib.rs | 0 lib/Cargo.toml | 25 +++++++++++ {src => lib/src}/callstack.rs | 0 {src => lib/src}/choice.rs | 0 {src => lib/src}/choice_point.rs | 0 {src => lib/src}/container.rs | 0 {src => lib/src}/control_command.rs | 0 {src => lib/src}/divert.rs | 0 {src => lib/src}/error.rs | 0 {src => lib/src}/flow.rs | 0 {src => lib/src}/glue.rs | 0 {src => lib/src}/ink_list.rs | 0 {src => lib/src}/ink_list_item.rs | 0 {src => lib/src}/json_read.rs | 0 {src => lib/src}/json_write.rs | 0 {src => lib/src}/lib.rs | 0 {src => lib/src}/list_definition.rs | 0 {src => lib/src}/list_definitions_origin.rs | 0 {src => lib/src}/native_function_call.rs | 0 {src => lib/src}/object.rs | 0 {src => lib/src}/path.rs | 0 {src => lib/src}/pointer.rs | 0 {src => lib/src}/push_pop.rs | 0 {src => lib/src}/search_result.rs | 0 {src => lib/src}/state_patch.rs | 0 {src => lib/src}/story.rs | 0 {src => lib/src}/story_error.rs | 0 {src => lib/src}/story_state.rs | 0 {src => lib/src}/tag.rs | 0 {src => lib/src}/value.rs | 0 {src => lib/src}/value_type.rs | 0 {src => lib/src}/variable_assigment.rs | 0 {src => lib/src}/variable_reference.rs | 0 {src => lib/src}/variables_state.rs | 0 {src => lib/src}/void.rs | 0 {tests => lib/tests}/basic_text_test.rs | 6 +-- {tests => lib/tests}/choice_test.rs | 36 ++++++++-------- {tests => lib/tests}/common/mod.rs | 2 +- {tests => lib/tests}/conditional_test.rs | 42 +++++++++---------- .../tests/data}/basictext/oneline.ink | 0 .../tests/data}/basictext/oneline.ink.json | 0 .../tests/data}/basictext/twolines.ink | 0 .../tests/data}/basictext/twolines.ink.json | 0 .../data}/choices/conditional-choice.ink | 0 .../data}/choices/conditional-choice.ink.json | 0 .../tests/data}/choices/divert-choice.ink | 0 .../data}/choices/divert-choice.ink.json | 0 .../tests/data}/choices/fallback-choice.ink | 0 .../data}/choices/fallback-choice.ink.json | 0 .../tests/data}/choices/label-flow.ink | 0 .../tests/data}/choices/label-flow.ink.json | 0 .../tests/data}/choices/label-scope-error.ink | 0 .../data}/choices/label-scope-error.ink.json | 0 .../tests/data}/choices/label-scope.ink | 0 .../tests/data}/choices/label-scope.ink.json | 0 .../tests/data}/choices/mixed-choice.ink | 0 .../tests/data}/choices/mixed-choice.ink.json | 0 .../tests/data}/choices/multi-choice.ink | 0 .../tests/data}/choices/multi-choice.ink.json | 0 .../tests/data}/choices/no-choice-text.ink | 0 .../data}/choices/no-choice-text.ink.json | 0 .../tests/data}/choices/one.ink | 0 .../tests/data}/choices/one.ink.json | 0 .../tests/data}/choices/single-choice.ink | 0 .../data}/choices/single-choice.ink.json | 0 .../tests/data}/choices/sticky-choice.ink | 0 .../data}/choices/sticky-choice.ink.json | 0 .../tests/data}/choices/suppress-choice.ink | 0 .../data}/choices/suppress-choice.ink.json | 0 .../tests/data}/choices/varying-choice.ink | 0 .../data}/choices/varying-choice.ink.json | 0 .../tests/data}/conditional/condopt.ink | 0 .../tests/data}/conditional/condopt.ink.json | 0 .../tests/data}/conditional/condtext.ink | 0 .../tests/data}/conditional/condtext.ink.json | 0 .../tests/data}/conditional/cycle.ink | 0 .../tests/data}/conditional/cycle.ink.json | 0 .../data}/conditional/ifelse-ext-text1.ink | 0 .../conditional/ifelse-ext-text1.ink.json | 0 .../data}/conditional/ifelse-ext-text2.ink | 0 .../conditional/ifelse-ext-text2.ink.json | 0 .../data}/conditional/ifelse-ext-text3.ink | 0 .../conditional/ifelse-ext-text3.ink.json | 0 .../tests/data}/conditional/ifelse-ext.ink | 0 .../data}/conditional/ifelse-ext.ink.json | 0 .../tests/data}/conditional/ifelse.ink | 0 .../tests/data}/conditional/ifelse.ink.json | 0 .../tests/data}/conditional/iffalse.ink | 0 .../tests/data}/conditional/iffalse.ink.json | 0 .../tests/data}/conditional/iftrue.ink | 0 .../tests/data}/conditional/iftrue.ink.json | 0 .../data}/conditional/multiline-choice.ink | 0 .../conditional/multiline-choice.ink.json | 0 .../data}/conditional/multiline-divert.ink | 0 .../conditional/multiline-divert.ink.json | 0 .../tests/data}/conditional/multiline.ink | 0 .../data}/conditional/multiline.ink.json | 0 .../tests/data}/conditional/once.ink | 0 .../tests/data}/conditional/once.ink.json | 0 .../tests/data}/conditional/shuffle.ink | 0 .../tests/data}/conditional/shuffle.ink.json | 0 .../tests/data}/conditional/shuffle_once.ink | 0 .../data}/conditional/shuffle_once.ink.json | 0 .../data}/conditional/shuffle_stopping.ink | 0 .../conditional/shuffle_stopping.ink.json | 0 .../tests/data}/conditional/stopping.ink | 0 .../tests/data}/conditional/stopping.ink.json | 0 .../tests/data}/divert/complex-branching.ink | 0 .../data}/divert/complex-branching.ink.json | 0 .../tests/data}/divert/divert-on-choice.ink | 0 .../data}/divert/divert-on-choice.ink.json | 0 .../tests/data}/divert/invisible-divert.ink | 0 .../data}/divert/invisible-divert.ink.json | 0 .../tests/data}/divert/simple-divert.ink | 0 .../tests/data}/divert/simple-divert.ink.json | 0 .../tests/data}/function/complex-func1.ink | 0 .../data}/function/complex-func1.ink.json | 0 .../tests/data}/function/complex-func2.ink | 0 .../data}/function/complex-func2.ink.json | 0 .../tests/data}/function/complex-func3.ink | 0 .../data}/function/complex-func3.ink.json | 0 .../evaluating-function-variablestate-bug.ink | 0 ...uating-function-variablestate-bug.ink.json | 0 .../tests/data}/function/func-basic.ink | 0 .../tests/data}/function/func-basic.ink.json | 0 .../tests/data}/function/func-inline.ink | 0 .../tests/data}/function/func-inline.ink.json | 0 .../tests/data}/function/func-none.ink | 0 .../tests/data}/function/func-none.ink.json | 0 .../tests/data}/function/rnd-func.ink | 0 .../tests/data}/function/rnd-func.ink.json | 0 .../tests/data}/function/setvar-func.ink | 0 .../tests/data}/function/setvar-func.ink.json | 0 .../tests/data}/function/test-error.ink | 0 .../tests/data}/function/test-error.ink.json | 0 .../tests/data}/gather/complex-flow.ink | 0 .../tests/data}/gather/complex-flow.ink.json | 0 .../tests/data}/gather/deep-nesting.ink | 0 .../tests/data}/gather/deep-nesting.ink.json | 0 .../tests/data}/gather/gather-basic.ink | 0 .../tests/data}/gather/gather-basic.ink.json | 0 .../tests/data}/gather/gather-chain.ink | 0 .../tests/data}/gather/gather-chain.ink.json | 0 .../tests/data}/gather/nested-flow.ink | 0 .../tests/data}/gather/nested-flow.ink.json | 0 .../tests/data}/gather/nested-gather.ink | 0 .../tests/data}/gather/nested-gather.ink.json | 0 .../tests/data}/glue/glue-with-divert.ink | 0 .../data}/glue/glue-with-divert.ink.json | 0 .../data}/glue/left-right-glue-matching.ink | 0 .../glue/left-right-glue-matching.ink.json | 0 .../tests/data}/glue/simple-glue.ink | 0 .../tests/data}/glue/simple-glue.ink.json | 0 .../tests/data}/glue/testbugfix1.ink | 0 .../tests/data}/glue/testbugfix1.ink.json | 0 .../tests/data}/glue/testbugfix2.ink | 0 .../tests/data}/glue/testbugfix2.ink.json | 0 .../tests/data}/knot/multi-line.ink | 0 .../tests/data}/knot/multi-line.ink.json | 0 .../tests/data}/knot/param-floats.ink | 0 .../tests/data}/knot/param-floats.ink.json | 0 .../tests/data}/knot/param-ints.ink | 0 .../tests/data}/knot/param-ints.ink.json | 0 .../tests/data}/knot/param-multi.ink | 0 .../tests/data}/knot/param-multi.ink.json | 0 .../tests/data}/knot/param-recurse.ink | 0 .../tests/data}/knot/param-recurse.ink.json | 0 .../tests/data}/knot/param-strings.ink | 0 .../tests/data}/knot/param-strings.ink.json | 0 .../tests/data}/knot/param-vars.ink | 0 .../tests/data}/knot/param-vars.ink.json | 0 .../tests/data}/knot/single-line.ink | 0 .../tests/data}/knot/single-line.ink.json | 0 .../tests/data}/knot/strip-empty-lines.ink | 0 .../data}/knot/strip-empty-lines.ink.json | 0 .../tests/data}/lists/basic-operations.ink | 0 .../data}/lists/basic-operations.ink.json | 0 .../tests/data}/lists/bug-adding-element.ink | 0 .../data}/lists/bug-adding-element.ink.json | 0 .../empty-list-origin-after-assignment.ink | 0 ...mpty-list-origin-after-assignment.ink.json | 0 .../tests/data}/lists/empty-list-origin.ink | 0 .../data}/lists/empty-list-origin.ink.json | 0 .../tests/data}/lists/list-mixed-items.ink | 0 .../data}/lists/list-mixed-items.ink.json | 0 .../tests/data}/lists/list-range.ink | 0 .../tests/data}/lists/list-range.ink.json | 0 .../tests/data}/lists/list-save-load.ink | 0 .../tests/data}/lists/list-save-load.ink.json | 0 .../data}/lists/more-list-operations.ink | 0 .../data}/lists/more-list-operations.ink.json | 0 .../data}/lists/more-list-operations2.ink | 0 .../lists/more-list-operations2.ink.json | 0 .../tests/data}/misc/issue15.ink | 0 .../tests/data}/misc/issue15.ink.json | 0 .../tests/data}/misc/operations.ink | 0 .../tests/data}/misc/operations.ink.json | 0 .../tests/data}/misc/read-counts.ink | 0 .../tests/data}/misc/read-counts.ink.json | 0 .../tests/data}/misc/turns-since.ink | 0 .../tests/data}/misc/turns-since.ink.json | 0 .../data}/runtime/external-function-0-arg.ink | 0 .../runtime/external-function-0-arg.ink.json | 0 .../data}/runtime/external-function-1-arg.ink | 0 .../runtime/external-function-1-arg.ink.json | 0 .../data}/runtime/external-function-2-arg.ink | 0 .../runtime/external-function-2-arg.ink.json | 0 .../data}/runtime/external-function-3-arg.ink | 0 .../runtime/external-function-3-arg.ink.json | 0 .../tests/data}/runtime/jump-knot.ink | 0 .../tests/data}/runtime/jump-knot.ink.json | 0 .../tests/data}/runtime/jump-stitch.ink | 0 .../tests/data}/runtime/jump-stitch.ink.json | 0 .../tests/data}/runtime/load-save.ink | 0 .../tests/data}/runtime/load-save.ink.json | 0 .../tests/data}/runtime/multiflow-basics.ink | 0 .../data}/runtime/multiflow-basics.ink.json | 0 .../runtime/multiflow-saveloadthreads.ink | 0 .../multiflow-saveloadthreads.ink.json | 0 .../tests/data}/runtime/read-visit-counts.ink | 0 .../data}/runtime/read-visit-counts.ink.json | 0 .../tests/data}/runtime/saving-loading.ink | 0 .../data}/runtime/saving-loading.ink.json | 0 .../tests/data}/runtime/set-get-variables.ink | 0 .../data}/runtime/set-get-variables.ink.json | 0 .../data}/runtime/variable-observers.ink | 0 .../data}/runtime/variable-observers.ink.json | 0 .../tests/data}/stitch/auto-stitch.ink | 0 .../tests/data}/stitch/auto-stitch.ink.json | 0 .../tests/data}/stitch/manual-stitch.ink | 0 .../tests/data}/stitch/manual-stitch.ink.json | 0 .../inkfiles => lib/tests/data}/tags/tags.ink | 0 .../tests/data}/tags/tags.ink.json | 0 .../tests/data}/tags/tagsDynamicContent.ink | 0 .../data}/tags/tagsDynamicContent.ink.json | 0 .../tests/data}/tags/tagsInChoice.ink | 0 .../tests/data}/tags/tagsInChoice.ink.json | 0 .../tests/data}/tags/tagsInSeq.ink | 0 .../tests/data}/tags/tagsInSeq.ink.json | 0 .../tests/data}/threads/thread-bug.ink | 0 .../tests/data}/threads/thread-bug.ink.json | 0 .../tunnel-onwards-divert-override.ink | 0 .../tunnel-onwards-divert-override.ink.json | 0 .../tests/data}/variable/var-divert.ink | 0 .../tests/data}/variable/var-divert.ink.json | 0 .../tests/data}/variable/varcalc.ink | 0 .../tests/data}/variable/varcalc.ink.json | 0 .../data}/variable/variable-declaration.ink | 0 .../variable/variable-declaration.ink.json | 0 .../tests/data}/variable/varstringinc.ink | 0 .../data}/variable/varstringinc.ink.json | 0 .../tests/data}/variabletext/cycle.ink | 0 .../tests/data}/variabletext/cycle.ink.json | 0 .../data}/variabletext/empty-elements.ink | 0 .../variabletext/empty-elements.ink.json | 0 .../data}/variabletext/list-in-choice.ink | 0 .../variabletext/list-in-choice.ink.json | 0 .../tests/data}/variabletext/once.ink | 0 .../tests/data}/variabletext/once.ink.json | 0 .../tests/data}/variabletext/sequence.ink | 0 .../data}/variabletext/sequence.ink.json | 0 {tests => lib/tests}/divert_test.rs | 12 +++--- {tests => lib/tests}/function_test.rs | 20 ++++----- {tests => lib/tests}/gather_test.rs | 14 +++---- {tests => lib/tests}/glue_test.rs | 12 +++--- {tests => lib/tests}/knot_test.rs | 20 ++++----- {tests => lib/tests}/list_test.rs | 20 ++++----- {tests => lib/tests}/misc_test.rs | 10 ++--- {tests => lib/tests}/multi_flow_test.rs | 6 +-- {tests => lib/tests}/runtime_test.rs | 14 +++---- {tests => lib/tests}/stitch_test.rs | 10 ++--- {tests => lib/tests}/tag_test.rs | 10 ++--- {tests => lib/tests}/thread_test.rs | 6 +-- {tests => lib/tests}/tunnel_test.rs | 4 +- {tests => lib/tests}/variable_test.rs | 10 ++--- {tests => lib/tests}/variable_text_test.rs | 12 +++--- 282 files changed, 202 insertions(+), 163 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 cli-player/Cargo.toml rename src/bin/console-player.rs => cli-player/src/cli_player.rs (100%) create mode 100644 clib/Cargo.toml create mode 100644 clib/src/lib.rs create mode 100644 lib/Cargo.toml rename {src => lib/src}/callstack.rs (100%) rename {src => lib/src}/choice.rs (100%) rename {src => lib/src}/choice_point.rs (100%) rename {src => lib/src}/container.rs (100%) rename {src => lib/src}/control_command.rs (100%) rename {src => lib/src}/divert.rs (100%) rename {src => lib/src}/error.rs (100%) rename {src => lib/src}/flow.rs (100%) rename {src => lib/src}/glue.rs (100%) rename {src => lib/src}/ink_list.rs (100%) rename {src => lib/src}/ink_list_item.rs (100%) rename {src => lib/src}/json_read.rs (100%) rename {src => lib/src}/json_write.rs (100%) rename {src => lib/src}/lib.rs (100%) rename {src => lib/src}/list_definition.rs (100%) rename {src => lib/src}/list_definitions_origin.rs (100%) rename {src => lib/src}/native_function_call.rs (100%) rename {src => lib/src}/object.rs (100%) rename {src => lib/src}/path.rs (100%) rename {src => lib/src}/pointer.rs (100%) rename {src => lib/src}/push_pop.rs (100%) rename {src => lib/src}/search_result.rs (100%) rename {src => lib/src}/state_patch.rs (100%) rename {src => lib/src}/story.rs (100%) rename {src => lib/src}/story_error.rs (100%) rename {src => lib/src}/story_state.rs (100%) rename {src => lib/src}/tag.rs (100%) rename {src => lib/src}/value.rs (100%) rename {src => lib/src}/value_type.rs (100%) rename {src => lib/src}/variable_assigment.rs (100%) rename {src => lib/src}/variable_reference.rs (100%) rename {src => lib/src}/variables_state.rs (100%) rename {src => lib/src}/void.rs (100%) rename {tests => lib/tests}/basic_text_test.rs (77%) rename {tests => lib/tests}/choice_test.rs (80%) rename {tests => lib/tests}/common/mod.rs (98%) rename {tests => lib/tests}/conditional_test.rs (86%) rename {examples/inkfiles => lib/tests/data}/basictext/oneline.ink (100%) rename {examples/inkfiles => lib/tests/data}/basictext/oneline.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/basictext/twolines.ink (100%) rename {examples/inkfiles => lib/tests/data}/basictext/twolines.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/choices/conditional-choice.ink (100%) rename {examples/inkfiles => lib/tests/data}/choices/conditional-choice.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/choices/divert-choice.ink (100%) rename {examples/inkfiles => lib/tests/data}/choices/divert-choice.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/choices/fallback-choice.ink (100%) rename {examples/inkfiles => lib/tests/data}/choices/fallback-choice.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/choices/label-flow.ink (100%) rename {examples/inkfiles => lib/tests/data}/choices/label-flow.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/choices/label-scope-error.ink (100%) rename {examples/inkfiles => lib/tests/data}/choices/label-scope-error.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/choices/label-scope.ink (100%) rename {examples/inkfiles => lib/tests/data}/choices/label-scope.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/choices/mixed-choice.ink (100%) rename {examples/inkfiles => lib/tests/data}/choices/mixed-choice.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/choices/multi-choice.ink (100%) rename {examples/inkfiles => lib/tests/data}/choices/multi-choice.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/choices/no-choice-text.ink (100%) rename {examples/inkfiles => lib/tests/data}/choices/no-choice-text.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/choices/one.ink (100%) rename {examples/inkfiles => lib/tests/data}/choices/one.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/choices/single-choice.ink (100%) rename {examples/inkfiles => lib/tests/data}/choices/single-choice.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/choices/sticky-choice.ink (100%) rename {examples/inkfiles => lib/tests/data}/choices/sticky-choice.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/choices/suppress-choice.ink (100%) rename {examples/inkfiles => lib/tests/data}/choices/suppress-choice.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/choices/varying-choice.ink (100%) rename {examples/inkfiles => lib/tests/data}/choices/varying-choice.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/conditional/condopt.ink (100%) rename {examples/inkfiles => lib/tests/data}/conditional/condopt.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/conditional/condtext.ink (100%) rename {examples/inkfiles => lib/tests/data}/conditional/condtext.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/conditional/cycle.ink (100%) rename {examples/inkfiles => lib/tests/data}/conditional/cycle.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/conditional/ifelse-ext-text1.ink (100%) rename {examples/inkfiles => lib/tests/data}/conditional/ifelse-ext-text1.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/conditional/ifelse-ext-text2.ink (100%) rename {examples/inkfiles => lib/tests/data}/conditional/ifelse-ext-text2.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/conditional/ifelse-ext-text3.ink (100%) rename {examples/inkfiles => lib/tests/data}/conditional/ifelse-ext-text3.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/conditional/ifelse-ext.ink (100%) rename {examples/inkfiles => lib/tests/data}/conditional/ifelse-ext.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/conditional/ifelse.ink (100%) rename {examples/inkfiles => lib/tests/data}/conditional/ifelse.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/conditional/iffalse.ink (100%) rename {examples/inkfiles => lib/tests/data}/conditional/iffalse.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/conditional/iftrue.ink (100%) rename {examples/inkfiles => lib/tests/data}/conditional/iftrue.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/conditional/multiline-choice.ink (100%) rename {examples/inkfiles => lib/tests/data}/conditional/multiline-choice.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/conditional/multiline-divert.ink (100%) rename {examples/inkfiles => lib/tests/data}/conditional/multiline-divert.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/conditional/multiline.ink (100%) rename {examples/inkfiles => lib/tests/data}/conditional/multiline.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/conditional/once.ink (100%) rename {examples/inkfiles => lib/tests/data}/conditional/once.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/conditional/shuffle.ink (100%) rename {examples/inkfiles => lib/tests/data}/conditional/shuffle.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/conditional/shuffle_once.ink (100%) rename {examples/inkfiles => lib/tests/data}/conditional/shuffle_once.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/conditional/shuffle_stopping.ink (100%) rename {examples/inkfiles => lib/tests/data}/conditional/shuffle_stopping.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/conditional/stopping.ink (100%) rename {examples/inkfiles => lib/tests/data}/conditional/stopping.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/divert/complex-branching.ink (100%) rename {examples/inkfiles => lib/tests/data}/divert/complex-branching.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/divert/divert-on-choice.ink (100%) rename {examples/inkfiles => lib/tests/data}/divert/divert-on-choice.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/divert/invisible-divert.ink (100%) rename {examples/inkfiles => lib/tests/data}/divert/invisible-divert.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/divert/simple-divert.ink (100%) rename {examples/inkfiles => lib/tests/data}/divert/simple-divert.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/function/complex-func1.ink (100%) rename {examples/inkfiles => lib/tests/data}/function/complex-func1.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/function/complex-func2.ink (100%) rename {examples/inkfiles => lib/tests/data}/function/complex-func2.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/function/complex-func3.ink (100%) rename {examples/inkfiles => lib/tests/data}/function/complex-func3.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/function/evaluating-function-variablestate-bug.ink (100%) rename {examples/inkfiles => lib/tests/data}/function/evaluating-function-variablestate-bug.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/function/func-basic.ink (100%) rename {examples/inkfiles => lib/tests/data}/function/func-basic.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/function/func-inline.ink (100%) rename {examples/inkfiles => lib/tests/data}/function/func-inline.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/function/func-none.ink (100%) rename {examples/inkfiles => lib/tests/data}/function/func-none.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/function/rnd-func.ink (100%) rename {examples/inkfiles => lib/tests/data}/function/rnd-func.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/function/setvar-func.ink (100%) rename {examples/inkfiles => lib/tests/data}/function/setvar-func.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/function/test-error.ink (100%) rename {examples/inkfiles => lib/tests/data}/function/test-error.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/gather/complex-flow.ink (100%) rename {examples/inkfiles => lib/tests/data}/gather/complex-flow.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/gather/deep-nesting.ink (100%) rename {examples/inkfiles => lib/tests/data}/gather/deep-nesting.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/gather/gather-basic.ink (100%) rename {examples/inkfiles => lib/tests/data}/gather/gather-basic.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/gather/gather-chain.ink (100%) rename {examples/inkfiles => lib/tests/data}/gather/gather-chain.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/gather/nested-flow.ink (100%) rename {examples/inkfiles => lib/tests/data}/gather/nested-flow.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/gather/nested-gather.ink (100%) rename {examples/inkfiles => lib/tests/data}/gather/nested-gather.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/glue/glue-with-divert.ink (100%) rename {examples/inkfiles => lib/tests/data}/glue/glue-with-divert.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/glue/left-right-glue-matching.ink (100%) rename {examples/inkfiles => lib/tests/data}/glue/left-right-glue-matching.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/glue/simple-glue.ink (100%) rename {examples/inkfiles => lib/tests/data}/glue/simple-glue.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/glue/testbugfix1.ink (100%) rename {examples/inkfiles => lib/tests/data}/glue/testbugfix1.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/glue/testbugfix2.ink (100%) rename {examples/inkfiles => lib/tests/data}/glue/testbugfix2.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/knot/multi-line.ink (100%) rename {examples/inkfiles => lib/tests/data}/knot/multi-line.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/knot/param-floats.ink (100%) rename {examples/inkfiles => lib/tests/data}/knot/param-floats.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/knot/param-ints.ink (100%) rename {examples/inkfiles => lib/tests/data}/knot/param-ints.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/knot/param-multi.ink (100%) rename {examples/inkfiles => lib/tests/data}/knot/param-multi.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/knot/param-recurse.ink (100%) rename {examples/inkfiles => lib/tests/data}/knot/param-recurse.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/knot/param-strings.ink (100%) rename {examples/inkfiles => lib/tests/data}/knot/param-strings.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/knot/param-vars.ink (100%) rename {examples/inkfiles => lib/tests/data}/knot/param-vars.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/knot/single-line.ink (100%) rename {examples/inkfiles => lib/tests/data}/knot/single-line.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/knot/strip-empty-lines.ink (100%) rename {examples/inkfiles => lib/tests/data}/knot/strip-empty-lines.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/lists/basic-operations.ink (100%) rename {examples/inkfiles => lib/tests/data}/lists/basic-operations.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/lists/bug-adding-element.ink (100%) rename {examples/inkfiles => lib/tests/data}/lists/bug-adding-element.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/lists/empty-list-origin-after-assignment.ink (100%) rename {examples/inkfiles => lib/tests/data}/lists/empty-list-origin-after-assignment.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/lists/empty-list-origin.ink (100%) rename {examples/inkfiles => lib/tests/data}/lists/empty-list-origin.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/lists/list-mixed-items.ink (100%) rename {examples/inkfiles => lib/tests/data}/lists/list-mixed-items.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/lists/list-range.ink (100%) rename {examples/inkfiles => lib/tests/data}/lists/list-range.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/lists/list-save-load.ink (100%) rename {examples/inkfiles => lib/tests/data}/lists/list-save-load.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/lists/more-list-operations.ink (100%) rename {examples/inkfiles => lib/tests/data}/lists/more-list-operations.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/lists/more-list-operations2.ink (100%) rename {examples/inkfiles => lib/tests/data}/lists/more-list-operations2.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/misc/issue15.ink (100%) rename {examples/inkfiles => lib/tests/data}/misc/issue15.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/misc/operations.ink (100%) rename {examples/inkfiles => lib/tests/data}/misc/operations.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/misc/read-counts.ink (100%) rename {examples/inkfiles => lib/tests/data}/misc/read-counts.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/misc/turns-since.ink (100%) rename {examples/inkfiles => lib/tests/data}/misc/turns-since.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/runtime/external-function-0-arg.ink (100%) rename {examples/inkfiles => lib/tests/data}/runtime/external-function-0-arg.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/runtime/external-function-1-arg.ink (100%) rename {examples/inkfiles => lib/tests/data}/runtime/external-function-1-arg.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/runtime/external-function-2-arg.ink (100%) rename {examples/inkfiles => lib/tests/data}/runtime/external-function-2-arg.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/runtime/external-function-3-arg.ink (100%) rename {examples/inkfiles => lib/tests/data}/runtime/external-function-3-arg.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/runtime/jump-knot.ink (100%) rename {examples/inkfiles => lib/tests/data}/runtime/jump-knot.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/runtime/jump-stitch.ink (100%) rename {examples/inkfiles => lib/tests/data}/runtime/jump-stitch.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/runtime/load-save.ink (100%) rename {examples/inkfiles => lib/tests/data}/runtime/load-save.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/runtime/multiflow-basics.ink (100%) rename {examples/inkfiles => lib/tests/data}/runtime/multiflow-basics.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/runtime/multiflow-saveloadthreads.ink (100%) rename {examples/inkfiles => lib/tests/data}/runtime/multiflow-saveloadthreads.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/runtime/read-visit-counts.ink (100%) rename {examples/inkfiles => lib/tests/data}/runtime/read-visit-counts.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/runtime/saving-loading.ink (100%) rename {examples/inkfiles => lib/tests/data}/runtime/saving-loading.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/runtime/set-get-variables.ink (100%) rename {examples/inkfiles => lib/tests/data}/runtime/set-get-variables.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/runtime/variable-observers.ink (100%) rename {examples/inkfiles => lib/tests/data}/runtime/variable-observers.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/stitch/auto-stitch.ink (100%) rename {examples/inkfiles => lib/tests/data}/stitch/auto-stitch.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/stitch/manual-stitch.ink (100%) rename {examples/inkfiles => lib/tests/data}/stitch/manual-stitch.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/tags/tags.ink (100%) rename {examples/inkfiles => lib/tests/data}/tags/tags.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/tags/tagsDynamicContent.ink (100%) rename {examples/inkfiles => lib/tests/data}/tags/tagsDynamicContent.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/tags/tagsInChoice.ink (100%) rename {examples/inkfiles => lib/tests/data}/tags/tagsInChoice.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/tags/tagsInSeq.ink (100%) rename {examples/inkfiles => lib/tests/data}/tags/tagsInSeq.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/threads/thread-bug.ink (100%) rename {examples/inkfiles => lib/tests/data}/threads/thread-bug.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/tunnels/tunnel-onwards-divert-override.ink (100%) rename {examples/inkfiles => lib/tests/data}/tunnels/tunnel-onwards-divert-override.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/variable/var-divert.ink (100%) rename {examples/inkfiles => lib/tests/data}/variable/var-divert.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/variable/varcalc.ink (100%) rename {examples/inkfiles => lib/tests/data}/variable/varcalc.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/variable/variable-declaration.ink (100%) rename {examples/inkfiles => lib/tests/data}/variable/variable-declaration.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/variable/varstringinc.ink (100%) rename {examples/inkfiles => lib/tests/data}/variable/varstringinc.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/variabletext/cycle.ink (100%) rename {examples/inkfiles => lib/tests/data}/variabletext/cycle.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/variabletext/empty-elements.ink (100%) rename {examples/inkfiles => lib/tests/data}/variabletext/empty-elements.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/variabletext/list-in-choice.ink (100%) rename {examples/inkfiles => lib/tests/data}/variabletext/list-in-choice.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/variabletext/once.ink (100%) rename {examples/inkfiles => lib/tests/data}/variabletext/once.ink.json (100%) rename {examples/inkfiles => lib/tests/data}/variabletext/sequence.ink (100%) rename {examples/inkfiles => lib/tests/data}/variabletext/sequence.ink.json (100%) rename {tests => lib/tests}/divert_test.rs (82%) rename {tests => lib/tests}/function_test.rs (78%) rename {tests => lib/tests}/gather_test.rs (87%) rename {tests => lib/tests}/glue_test.rs (77%) rename {tests => lib/tests}/knot_test.rs (80%) rename {tests => lib/tests}/list_test.rs (74%) rename {tests => lib/tests}/misc_test.rs (77%) rename {tests => lib/tests}/multi_flow_test.rs (91%) rename {tests => lib/tests}/runtime_test.rs (87%) rename {tests => lib/tests}/stitch_test.rs (80%) rename {tests => lib/tests}/tag_test.rs (87%) rename {tests => lib/tests}/thread_test.rs (88%) rename {tests => lib/tests}/tunnel_test.rs (60%) rename {tests => lib/tests}/variable_test.rs (78%) rename {tests => lib/tests}/variable_text_test.rs (90%) diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..352a626 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "rust-analyzer.linkedProjects": [ + "./Cargo.toml" + ] +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 3bd0af4..7a08d80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,24 +1,7 @@ -[package] -name = "blade-ink" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[lib] -name = "bladeink" -path = "src/lib.rs" - -[[bin]] -name = "console-player" -path = "src/bin/console-player.rs" - -[dependencies] -serde = { version = "1.0.152", features = ["derive"] } -serde_json = "1.0.93" -log = "0.4.17" -strum_macros = "0.25.2" -strum = { version = "0.25.0", features = ["derive"] } -as-any = "0.3.0" -rand = "0.8.5" - +[workspace] +members = [ + "cli-player", + "lib", + "clib", +] +resolver = "2" \ No newline at end of file diff --git a/README.md b/README.md index 09b44e9..9509a93 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,14 @@ Currently under development. This is the implementation status: ## TODO -- [ ] Error handling -- [ ] story.state -> quitar el Option y que guardar/salvar sea pub(crate). -- [ ] Variable observers. -- [ ] Optimize control command getname. Use static string array and address it by order. +- [ ] VariablesState.default_global_variables shouldn't be optionals. +- [ ] Optimize control command and Ops getname. Use static string array and address it by order. - [ ] Cache components string in Path +- [ ] Variable observers. +- [ ] Doc + +- [ ] story.state -> quitar el pub de get_state()/mut y que guardar/salvar sea pub(crate). Crear fichero con pub methods?? - [ ] Use OnceCell to lazy init the cache fields of RTObjects - [ ] Split large files. ex. Get the error handling out of the Story class. The performLogic -- [ ] Story.state y VariablesState.default_global_variables shouldn't be optionals. -- [ ] Review all the .unwrap() and change it by .ok_or("xxx"). We need to avoid panics! +- [ ] Review all the use bink) and change it by .ok_or("xxx"). We need to avoid panics! diff --git a/cli-player/Cargo.toml b/cli-player/Cargo.toml new file mode 100644 index 0000000..8f1bdf3 --- /dev/null +++ b/cli-player/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "binkplayer" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "binkplayer" +path = "src/cli_player.rs" + +[dependencies] +bink = { "version" = "0.1.0", path = "../lib" } + + diff --git a/src/bin/console-player.rs b/cli-player/src/cli_player.rs similarity index 100% rename from src/bin/console-player.rs rename to cli-player/src/cli_player.rs diff --git a/clib/Cargo.toml b/clib/Cargo.toml new file mode 100644 index 0000000..7cb46c1 --- /dev/null +++ b/clib/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "binkc" +version = "0.1.0" +edition = "2021" + +[lib] +name = "binkc" +path = "src/lib.rs" +crate-type = ["cdylib"] + +[dependencies] +bink = { "version" = "0.1.0", path = "../lib" } \ No newline at end of file diff --git a/clib/src/lib.rs b/clib/src/lib.rs new file mode 100644 index 0000000..e69de29 diff --git a/lib/Cargo.toml b/lib/Cargo.toml new file mode 100644 index 0000000..60920bc --- /dev/null +++ b/lib/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "bink" +version = "0.1.0" +authors = ["Rafael Garcia "] +description = """ +This is a Rust port of inkle's ink, a scripting language for writing interactive narrative. +""" +license-file = "LICENSE" +readme = "README.md" +repository = "https://github.com/bladecoder/blade-ink-rs/" +edition = "2021" + +[lib] +name = "bink" +path = "src/lib.rs" + +[dependencies] +serde = { version = "1.0.152", features = ["derive"] } +serde_json = "1.0.93" +log = "0.4.17" +strum_macros = "0.25.2" +strum = { version = "0.25.0", features = ["derive"] } +as-any = "0.3.0" +rand = "0.8.5" + diff --git a/src/callstack.rs b/lib/src/callstack.rs similarity index 100% rename from src/callstack.rs rename to lib/src/callstack.rs diff --git a/src/choice.rs b/lib/src/choice.rs similarity index 100% rename from src/choice.rs rename to lib/src/choice.rs diff --git a/src/choice_point.rs b/lib/src/choice_point.rs similarity index 100% rename from src/choice_point.rs rename to lib/src/choice_point.rs diff --git a/src/container.rs b/lib/src/container.rs similarity index 100% rename from src/container.rs rename to lib/src/container.rs diff --git a/src/control_command.rs b/lib/src/control_command.rs similarity index 100% rename from src/control_command.rs rename to lib/src/control_command.rs diff --git a/src/divert.rs b/lib/src/divert.rs similarity index 100% rename from src/divert.rs rename to lib/src/divert.rs diff --git a/src/error.rs b/lib/src/error.rs similarity index 100% rename from src/error.rs rename to lib/src/error.rs diff --git a/src/flow.rs b/lib/src/flow.rs similarity index 100% rename from src/flow.rs rename to lib/src/flow.rs diff --git a/src/glue.rs b/lib/src/glue.rs similarity index 100% rename from src/glue.rs rename to lib/src/glue.rs diff --git a/src/ink_list.rs b/lib/src/ink_list.rs similarity index 100% rename from src/ink_list.rs rename to lib/src/ink_list.rs diff --git a/src/ink_list_item.rs b/lib/src/ink_list_item.rs similarity index 100% rename from src/ink_list_item.rs rename to lib/src/ink_list_item.rs diff --git a/src/json_read.rs b/lib/src/json_read.rs similarity index 100% rename from src/json_read.rs rename to lib/src/json_read.rs diff --git a/src/json_write.rs b/lib/src/json_write.rs similarity index 100% rename from src/json_write.rs rename to lib/src/json_write.rs diff --git a/src/lib.rs b/lib/src/lib.rs similarity index 100% rename from src/lib.rs rename to lib/src/lib.rs diff --git a/src/list_definition.rs b/lib/src/list_definition.rs similarity index 100% rename from src/list_definition.rs rename to lib/src/list_definition.rs diff --git a/src/list_definitions_origin.rs b/lib/src/list_definitions_origin.rs similarity index 100% rename from src/list_definitions_origin.rs rename to lib/src/list_definitions_origin.rs diff --git a/src/native_function_call.rs b/lib/src/native_function_call.rs similarity index 100% rename from src/native_function_call.rs rename to lib/src/native_function_call.rs diff --git a/src/object.rs b/lib/src/object.rs similarity index 100% rename from src/object.rs rename to lib/src/object.rs diff --git a/src/path.rs b/lib/src/path.rs similarity index 100% rename from src/path.rs rename to lib/src/path.rs diff --git a/src/pointer.rs b/lib/src/pointer.rs similarity index 100% rename from src/pointer.rs rename to lib/src/pointer.rs diff --git a/src/push_pop.rs b/lib/src/push_pop.rs similarity index 100% rename from src/push_pop.rs rename to lib/src/push_pop.rs diff --git a/src/search_result.rs b/lib/src/search_result.rs similarity index 100% rename from src/search_result.rs rename to lib/src/search_result.rs diff --git a/src/state_patch.rs b/lib/src/state_patch.rs similarity index 100% rename from src/state_patch.rs rename to lib/src/state_patch.rs diff --git a/src/story.rs b/lib/src/story.rs similarity index 100% rename from src/story.rs rename to lib/src/story.rs diff --git a/src/story_error.rs b/lib/src/story_error.rs similarity index 100% rename from src/story_error.rs rename to lib/src/story_error.rs diff --git a/src/story_state.rs b/lib/src/story_state.rs similarity index 100% rename from src/story_state.rs rename to lib/src/story_state.rs diff --git a/src/tag.rs b/lib/src/tag.rs similarity index 100% rename from src/tag.rs rename to lib/src/tag.rs diff --git a/src/value.rs b/lib/src/value.rs similarity index 100% rename from src/value.rs rename to lib/src/value.rs diff --git a/src/value_type.rs b/lib/src/value_type.rs similarity index 100% rename from src/value_type.rs rename to lib/src/value_type.rs diff --git a/src/variable_assigment.rs b/lib/src/variable_assigment.rs similarity index 100% rename from src/variable_assigment.rs rename to lib/src/variable_assigment.rs diff --git a/src/variable_reference.rs b/lib/src/variable_reference.rs similarity index 100% rename from src/variable_reference.rs rename to lib/src/variable_reference.rs diff --git a/src/variables_state.rs b/lib/src/variables_state.rs similarity index 100% rename from src/variables_state.rs rename to lib/src/variables_state.rs diff --git a/src/void.rs b/lib/src/void.rs similarity index 100% rename from src/void.rs rename to lib/src/void.rs diff --git a/tests/basic_text_test.rs b/lib/tests/basic_text_test.rs similarity index 77% rename from tests/basic_text_test.rs rename to lib/tests/basic_text_test.rs index b6cad13..7d44612 100644 --- a/tests/basic_text_test.rs +++ b/lib/tests/basic_text_test.rs @@ -1,12 +1,12 @@ use std::fs; -use bladeink::{story::Story, story_error::StoryError}; +use bink::{story::Story, story_error::StoryError}; mod common; #[test] fn oneline_test() -> Result<(), StoryError> { let json_string = - fs::read_to_string("examples/inkfiles/basictext/oneline.ink.json").unwrap(); + fs::read_to_string("tests/data/basictext/oneline.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); @@ -22,7 +22,7 @@ fn oneline_test() -> Result<(), StoryError> { #[test] fn twolines_test() -> Result<(), StoryError> { let json_string = - fs::read_to_string("examples/inkfiles/basictext/twolines.ink.json").unwrap(); + fs::read_to_string("tests/data/basictext/twolines.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); diff --git a/tests/choice_test.rs b/lib/tests/choice_test.rs similarity index 80% rename from tests/choice_test.rs rename to lib/tests/choice_test.rs index 029d9d7..ae329d2 100644 --- a/tests/choice_test.rs +++ b/lib/tests/choice_test.rs @@ -1,4 +1,4 @@ -use bladeink::{story::Story, story_error::StoryError}; +use bink::{story::Story, story_error::StoryError}; mod common; @@ -7,7 +7,7 @@ mod common; fn no_choice_test() -> Result<(), StoryError> { let mut errors:Vec = Vec::new(); - let text = common::run_story("examples/inkfiles/choices/no-choice-text.ink.json", None, &mut errors)?; + let text = common::run_story("tests/data/choices/no-choice-text.ink.json", None, &mut errors)?; assert_eq!(0, errors.len()); assert_eq!("Hello world!\nHello back!\n", common::join_text(&text)); @@ -19,7 +19,7 @@ fn no_choice_test() -> Result<(), StoryError> { fn one_test() -> Result<(), StoryError> { let mut errors:Vec = Vec::new(); - let text = common::run_story("examples/inkfiles/choices/one.ink.json", None, &mut errors)?; + let text = common::run_story("tests/data/choices/one.ink.json", None, &mut errors)?; assert_eq!(0, errors.len()); assert_eq!("Hello world!\nHello back!\nHello back!\n", common::join_text(&text)); @@ -31,13 +31,13 @@ fn one_test() -> Result<(), StoryError> { fn multi_choice_test() -> Result<(), StoryError> { let mut errors:Vec = Vec::new(); - let text = common::run_story("examples/inkfiles/choices/multi-choice.ink.json", Some(vec![0]), &mut errors)?; + let text = common::run_story("tests/data/choices/multi-choice.ink.json", Some(vec![0]), &mut errors)?; assert_eq!(0, errors.len()); assert_eq!("Hello, world!\nHello back!\nGoodbye\nHello back!\nNice to hear from you\n", common::join_text(&text)); // Select second choice - let text = common::run_story("examples/inkfiles/choices/multi-choice.ink.json", Some(vec![1]), &mut errors)?; + let text = common::run_story("tests/data/choices/multi-choice.ink.json", Some(vec![1]), &mut errors)?; assert_eq!(0, errors.len()); assert_eq!("Hello, world!\nHello back!\nGoodbye\nGoodbye\nSee you later\n", common::join_text(&text)); @@ -48,7 +48,7 @@ fn multi_choice_test() -> Result<(), StoryError> { #[test] fn single_choice1_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/choices/single-choice.ink.json").unwrap(); + common::get_json_string("tests/data/choices/single-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -62,7 +62,7 @@ fn single_choice1_test() -> Result<(), StoryError> { #[test] fn single_choic2_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/choices/single-choice.ink.json").unwrap(); + common::get_json_string("tests/data/choices/single-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -80,7 +80,7 @@ fn single_choic2_test() -> Result<(), StoryError> { #[test] fn suppress_choice_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/choices/suppress-choice.ink.json").unwrap(); + common::get_json_string("tests/data/choices/suppress-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -101,7 +101,7 @@ fn suppress_choice_test() -> Result<(), StoryError> { #[test] fn mixed_choice_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/choices/mixed-choice.ink.json").unwrap(); + common::get_json_string("tests/data/choices/mixed-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -123,7 +123,7 @@ fn mixed_choice_test() -> Result<(), StoryError> { #[test] fn varying_choice_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/choices/varying-choice.ink.json").unwrap(); + common::get_json_string("tests/data/choices/varying-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -144,7 +144,7 @@ fn varying_choice_test() -> Result<(), StoryError> { #[test] fn sticky_choice_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/choices/sticky-choice.ink.json").unwrap(); + common::get_json_string("tests/data/choices/sticky-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -163,7 +163,7 @@ fn sticky_choice_test() -> Result<(), StoryError> { #[test] fn fallback_choice_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/choices/fallback-choice.ink.json").unwrap(); + common::get_json_string("tests/data/choices/fallback-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -176,7 +176,7 @@ fn fallback_choice_test() -> Result<(), StoryError> { #[test] fn fallback_choice2_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/choices/fallback-choice.ink.json").unwrap(); + common::get_json_string("tests/data/choices/fallback-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -199,7 +199,7 @@ fn fallback_choice2_test() -> Result<(), StoryError> { #[test] fn conditional_choice_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/choices/conditional-choice.ink.json").unwrap(); + common::get_json_string("tests/data/choices/conditional-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -212,7 +212,7 @@ fn conditional_choice_test() -> Result<(), StoryError> { #[test] fn label_flow_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/choices/label-flow.ink.json").unwrap(); + common::get_json_string("tests/data/choices/label-flow.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -232,7 +232,7 @@ fn label_flow_test() -> Result<(), StoryError> { #[test] fn label_flow2_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/choices/label-flow.ink.json").unwrap(); + common::get_json_string("tests/data/choices/label-flow.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -252,7 +252,7 @@ fn label_flow2_test() -> Result<(), StoryError> { #[test] fn label_scope_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/choices/label-scope.ink.json").unwrap(); + common::get_json_string("tests/data/choices/label-scope.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -271,7 +271,7 @@ fn label_scope_test() -> Result<(), StoryError> { #[test] fn divert_choice_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/choices/divert-choice.ink.json").unwrap(); + common::get_json_string("tests/data/choices/divert-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); diff --git a/tests/common/mod.rs b/lib/tests/common/mod.rs similarity index 98% rename from tests/common/mod.rs rename to lib/tests/common/mod.rs index f6e2810..3c32a2d 100644 --- a/tests/common/mod.rs +++ b/lib/tests/common/mod.rs @@ -1,6 +1,6 @@ use std::{error::Error, path::Path, fs}; -use bladeink::{story::Story, story_error::StoryError}; +use bink::{story::Story, story_error::StoryError}; use rand::Rng; pub fn next_all(story: &mut Story, text: &mut Vec) -> Result<(), StoryError> { diff --git a/tests/conditional_test.rs b/lib/tests/conditional_test.rs similarity index 86% rename from tests/conditional_test.rs rename to lib/tests/conditional_test.rs index c824cb0..06ed0a4 100644 --- a/tests/conditional_test.rs +++ b/lib/tests/conditional_test.rs @@ -1,11 +1,11 @@ -use bladeink::{story::Story, story_error::StoryError}; +use bink::{story::Story, story_error::StoryError}; mod common; #[test] fn iftrue_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/conditional/iftrue.ink.json").unwrap(); + common::get_json_string("tests/data/conditional/iftrue.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); @@ -21,7 +21,7 @@ fn iftrue_test() -> Result<(), StoryError> { #[test] fn iffalse_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/conditional/iffalse.ink.json").unwrap(); + common::get_json_string("tests/data/conditional/iffalse.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); @@ -37,7 +37,7 @@ fn iffalse_test() -> Result<(), StoryError> { #[test] fn ifelse_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/conditional/ifelse.ink.json").unwrap(); + common::get_json_string("tests/data/conditional/ifelse.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); @@ -53,7 +53,7 @@ fn ifelse_test() -> Result<(), StoryError> { #[test] fn ifelse_ext_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/conditional/ifelse-ext.ink.json").unwrap(); + common::get_json_string("tests/data/conditional/ifelse-ext.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); @@ -69,7 +69,7 @@ fn ifelse_ext_test() -> Result<(), StoryError> { #[test] fn ifelse_ext_text1_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/conditional/ifelse-ext-text1.ink.json").unwrap(); + common::get_json_string("tests/data/conditional/ifelse-ext-text1.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); @@ -91,7 +91,7 @@ fn ifelse_ext_text1_test() -> Result<(), StoryError> { #[test] fn ifelse_ext_text2_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/conditional/ifelse-ext-text2.ink.json").unwrap(); + common::get_json_string("tests/data/conditional/ifelse-ext-text2.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); @@ -113,7 +113,7 @@ fn ifelse_ext_text2_test() -> Result<(), StoryError> { #[test] fn ifelse_ext_text3_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/conditional/ifelse-ext-text3.ink.json").unwrap(); + common::get_json_string("tests/data/conditional/ifelse-ext-text3.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); @@ -135,7 +135,7 @@ fn ifelse_ext_text3_test() -> Result<(), StoryError> { #[test] fn cond_text1_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/conditional/condtext.ink.json").unwrap(); + common::get_json_string("tests/data/conditional/condtext.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -154,7 +154,7 @@ fn cond_text1_test() -> Result<(), StoryError> { #[test] fn cond_text2_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/conditional/condtext.ink.json").unwrap(); + common::get_json_string("tests/data/conditional/condtext.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -173,7 +173,7 @@ fn cond_text2_test() -> Result<(), StoryError> { #[test] fn cond_opt1_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/conditional/condopt.ink.json").unwrap(); + common::get_json_string("tests/data/conditional/condopt.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -191,7 +191,7 @@ fn cond_opt1_test() -> Result<(), StoryError> { #[test] fn cond_opt2_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/conditional/condopt.ink.json").unwrap(); + common::get_json_string("tests/data/conditional/condopt.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -209,7 +209,7 @@ fn cond_opt2_test() -> Result<(), StoryError> { #[test] fn stopping_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/conditional/stopping.ink.json").unwrap(); + common::get_json_string("tests/data/conditional/stopping.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -243,7 +243,7 @@ fn stopping_test() -> Result<(), StoryError> { #[test] fn cycle_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/conditional/cycle.ink.json").unwrap(); + common::get_json_string("tests/data/conditional/cycle.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -277,7 +277,7 @@ fn cycle_test() -> Result<(), StoryError> { #[test] fn once_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/conditional/once.ink.json").unwrap(); + common::get_json_string("tests/data/conditional/once.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -308,7 +308,7 @@ fn once_test() -> Result<(), StoryError> { #[test] fn shuffle_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/conditional/shuffle.ink.json").unwrap(); + common::get_json_string("tests/data/conditional/shuffle.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -339,7 +339,7 @@ fn shuffle_test() -> Result<(), StoryError> { #[test] fn shuffle_stopping() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/conditional/shuffle_stopping.ink.json").unwrap(); + common::get_json_string("tests/data/conditional/shuffle_stopping.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -372,7 +372,7 @@ fn shuffle_stopping() -> Result<(), StoryError> { #[test] fn shuffle_once() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/conditional/shuffle_once.ink.json").unwrap(); + common::get_json_string("tests/data/conditional/shuffle_once.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -403,7 +403,7 @@ fn shuffle_once() -> Result<(), StoryError> { #[test] fn multiline_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/conditional/multiline.ink.json").unwrap(); + common::get_json_string("tests/data/conditional/multiline.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -432,7 +432,7 @@ fn multiline_test() -> Result<(), StoryError> { #[test] fn multiline_divert_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/conditional/multiline-divert.ink.json").unwrap(); + common::get_json_string("tests/data/conditional/multiline-divert.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -461,7 +461,7 @@ fn multiline_divert_test() -> Result<(), StoryError> { #[test] fn multiline_choice_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/conditional/multiline-choice.ink.json").unwrap(); + common::get_json_string("tests/data/conditional/multiline-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); diff --git a/examples/inkfiles/basictext/oneline.ink b/lib/tests/data/basictext/oneline.ink similarity index 100% rename from examples/inkfiles/basictext/oneline.ink rename to lib/tests/data/basictext/oneline.ink diff --git a/examples/inkfiles/basictext/oneline.ink.json b/lib/tests/data/basictext/oneline.ink.json similarity index 100% rename from examples/inkfiles/basictext/oneline.ink.json rename to lib/tests/data/basictext/oneline.ink.json diff --git a/examples/inkfiles/basictext/twolines.ink b/lib/tests/data/basictext/twolines.ink similarity index 100% rename from examples/inkfiles/basictext/twolines.ink rename to lib/tests/data/basictext/twolines.ink diff --git a/examples/inkfiles/basictext/twolines.ink.json b/lib/tests/data/basictext/twolines.ink.json similarity index 100% rename from examples/inkfiles/basictext/twolines.ink.json rename to lib/tests/data/basictext/twolines.ink.json diff --git a/examples/inkfiles/choices/conditional-choice.ink b/lib/tests/data/choices/conditional-choice.ink similarity index 100% rename from examples/inkfiles/choices/conditional-choice.ink rename to lib/tests/data/choices/conditional-choice.ink diff --git a/examples/inkfiles/choices/conditional-choice.ink.json b/lib/tests/data/choices/conditional-choice.ink.json similarity index 100% rename from examples/inkfiles/choices/conditional-choice.ink.json rename to lib/tests/data/choices/conditional-choice.ink.json diff --git a/examples/inkfiles/choices/divert-choice.ink b/lib/tests/data/choices/divert-choice.ink similarity index 100% rename from examples/inkfiles/choices/divert-choice.ink rename to lib/tests/data/choices/divert-choice.ink diff --git a/examples/inkfiles/choices/divert-choice.ink.json b/lib/tests/data/choices/divert-choice.ink.json similarity index 100% rename from examples/inkfiles/choices/divert-choice.ink.json rename to lib/tests/data/choices/divert-choice.ink.json diff --git a/examples/inkfiles/choices/fallback-choice.ink b/lib/tests/data/choices/fallback-choice.ink similarity index 100% rename from examples/inkfiles/choices/fallback-choice.ink rename to lib/tests/data/choices/fallback-choice.ink diff --git a/examples/inkfiles/choices/fallback-choice.ink.json b/lib/tests/data/choices/fallback-choice.ink.json similarity index 100% rename from examples/inkfiles/choices/fallback-choice.ink.json rename to lib/tests/data/choices/fallback-choice.ink.json diff --git a/examples/inkfiles/choices/label-flow.ink b/lib/tests/data/choices/label-flow.ink similarity index 100% rename from examples/inkfiles/choices/label-flow.ink rename to lib/tests/data/choices/label-flow.ink diff --git a/examples/inkfiles/choices/label-flow.ink.json b/lib/tests/data/choices/label-flow.ink.json similarity index 100% rename from examples/inkfiles/choices/label-flow.ink.json rename to lib/tests/data/choices/label-flow.ink.json diff --git a/examples/inkfiles/choices/label-scope-error.ink b/lib/tests/data/choices/label-scope-error.ink similarity index 100% rename from examples/inkfiles/choices/label-scope-error.ink rename to lib/tests/data/choices/label-scope-error.ink diff --git a/examples/inkfiles/choices/label-scope-error.ink.json b/lib/tests/data/choices/label-scope-error.ink.json similarity index 100% rename from examples/inkfiles/choices/label-scope-error.ink.json rename to lib/tests/data/choices/label-scope-error.ink.json diff --git a/examples/inkfiles/choices/label-scope.ink b/lib/tests/data/choices/label-scope.ink similarity index 100% rename from examples/inkfiles/choices/label-scope.ink rename to lib/tests/data/choices/label-scope.ink diff --git a/examples/inkfiles/choices/label-scope.ink.json b/lib/tests/data/choices/label-scope.ink.json similarity index 100% rename from examples/inkfiles/choices/label-scope.ink.json rename to lib/tests/data/choices/label-scope.ink.json diff --git a/examples/inkfiles/choices/mixed-choice.ink b/lib/tests/data/choices/mixed-choice.ink similarity index 100% rename from examples/inkfiles/choices/mixed-choice.ink rename to lib/tests/data/choices/mixed-choice.ink diff --git a/examples/inkfiles/choices/mixed-choice.ink.json b/lib/tests/data/choices/mixed-choice.ink.json similarity index 100% rename from examples/inkfiles/choices/mixed-choice.ink.json rename to lib/tests/data/choices/mixed-choice.ink.json diff --git a/examples/inkfiles/choices/multi-choice.ink b/lib/tests/data/choices/multi-choice.ink similarity index 100% rename from examples/inkfiles/choices/multi-choice.ink rename to lib/tests/data/choices/multi-choice.ink diff --git a/examples/inkfiles/choices/multi-choice.ink.json b/lib/tests/data/choices/multi-choice.ink.json similarity index 100% rename from examples/inkfiles/choices/multi-choice.ink.json rename to lib/tests/data/choices/multi-choice.ink.json diff --git a/examples/inkfiles/choices/no-choice-text.ink b/lib/tests/data/choices/no-choice-text.ink similarity index 100% rename from examples/inkfiles/choices/no-choice-text.ink rename to lib/tests/data/choices/no-choice-text.ink diff --git a/examples/inkfiles/choices/no-choice-text.ink.json b/lib/tests/data/choices/no-choice-text.ink.json similarity index 100% rename from examples/inkfiles/choices/no-choice-text.ink.json rename to lib/tests/data/choices/no-choice-text.ink.json diff --git a/examples/inkfiles/choices/one.ink b/lib/tests/data/choices/one.ink similarity index 100% rename from examples/inkfiles/choices/one.ink rename to lib/tests/data/choices/one.ink diff --git a/examples/inkfiles/choices/one.ink.json b/lib/tests/data/choices/one.ink.json similarity index 100% rename from examples/inkfiles/choices/one.ink.json rename to lib/tests/data/choices/one.ink.json diff --git a/examples/inkfiles/choices/single-choice.ink b/lib/tests/data/choices/single-choice.ink similarity index 100% rename from examples/inkfiles/choices/single-choice.ink rename to lib/tests/data/choices/single-choice.ink diff --git a/examples/inkfiles/choices/single-choice.ink.json b/lib/tests/data/choices/single-choice.ink.json similarity index 100% rename from examples/inkfiles/choices/single-choice.ink.json rename to lib/tests/data/choices/single-choice.ink.json diff --git a/examples/inkfiles/choices/sticky-choice.ink b/lib/tests/data/choices/sticky-choice.ink similarity index 100% rename from examples/inkfiles/choices/sticky-choice.ink rename to lib/tests/data/choices/sticky-choice.ink diff --git a/examples/inkfiles/choices/sticky-choice.ink.json b/lib/tests/data/choices/sticky-choice.ink.json similarity index 100% rename from examples/inkfiles/choices/sticky-choice.ink.json rename to lib/tests/data/choices/sticky-choice.ink.json diff --git a/examples/inkfiles/choices/suppress-choice.ink b/lib/tests/data/choices/suppress-choice.ink similarity index 100% rename from examples/inkfiles/choices/suppress-choice.ink rename to lib/tests/data/choices/suppress-choice.ink diff --git a/examples/inkfiles/choices/suppress-choice.ink.json b/lib/tests/data/choices/suppress-choice.ink.json similarity index 100% rename from examples/inkfiles/choices/suppress-choice.ink.json rename to lib/tests/data/choices/suppress-choice.ink.json diff --git a/examples/inkfiles/choices/varying-choice.ink b/lib/tests/data/choices/varying-choice.ink similarity index 100% rename from examples/inkfiles/choices/varying-choice.ink rename to lib/tests/data/choices/varying-choice.ink diff --git a/examples/inkfiles/choices/varying-choice.ink.json b/lib/tests/data/choices/varying-choice.ink.json similarity index 100% rename from examples/inkfiles/choices/varying-choice.ink.json rename to lib/tests/data/choices/varying-choice.ink.json diff --git a/examples/inkfiles/conditional/condopt.ink b/lib/tests/data/conditional/condopt.ink similarity index 100% rename from examples/inkfiles/conditional/condopt.ink rename to lib/tests/data/conditional/condopt.ink diff --git a/examples/inkfiles/conditional/condopt.ink.json b/lib/tests/data/conditional/condopt.ink.json similarity index 100% rename from examples/inkfiles/conditional/condopt.ink.json rename to lib/tests/data/conditional/condopt.ink.json diff --git a/examples/inkfiles/conditional/condtext.ink b/lib/tests/data/conditional/condtext.ink similarity index 100% rename from examples/inkfiles/conditional/condtext.ink rename to lib/tests/data/conditional/condtext.ink diff --git a/examples/inkfiles/conditional/condtext.ink.json b/lib/tests/data/conditional/condtext.ink.json similarity index 100% rename from examples/inkfiles/conditional/condtext.ink.json rename to lib/tests/data/conditional/condtext.ink.json diff --git a/examples/inkfiles/conditional/cycle.ink b/lib/tests/data/conditional/cycle.ink similarity index 100% rename from examples/inkfiles/conditional/cycle.ink rename to lib/tests/data/conditional/cycle.ink diff --git a/examples/inkfiles/conditional/cycle.ink.json b/lib/tests/data/conditional/cycle.ink.json similarity index 100% rename from examples/inkfiles/conditional/cycle.ink.json rename to lib/tests/data/conditional/cycle.ink.json diff --git a/examples/inkfiles/conditional/ifelse-ext-text1.ink b/lib/tests/data/conditional/ifelse-ext-text1.ink similarity index 100% rename from examples/inkfiles/conditional/ifelse-ext-text1.ink rename to lib/tests/data/conditional/ifelse-ext-text1.ink diff --git a/examples/inkfiles/conditional/ifelse-ext-text1.ink.json b/lib/tests/data/conditional/ifelse-ext-text1.ink.json similarity index 100% rename from examples/inkfiles/conditional/ifelse-ext-text1.ink.json rename to lib/tests/data/conditional/ifelse-ext-text1.ink.json diff --git a/examples/inkfiles/conditional/ifelse-ext-text2.ink b/lib/tests/data/conditional/ifelse-ext-text2.ink similarity index 100% rename from examples/inkfiles/conditional/ifelse-ext-text2.ink rename to lib/tests/data/conditional/ifelse-ext-text2.ink diff --git a/examples/inkfiles/conditional/ifelse-ext-text2.ink.json b/lib/tests/data/conditional/ifelse-ext-text2.ink.json similarity index 100% rename from examples/inkfiles/conditional/ifelse-ext-text2.ink.json rename to lib/tests/data/conditional/ifelse-ext-text2.ink.json diff --git a/examples/inkfiles/conditional/ifelse-ext-text3.ink b/lib/tests/data/conditional/ifelse-ext-text3.ink similarity index 100% rename from examples/inkfiles/conditional/ifelse-ext-text3.ink rename to lib/tests/data/conditional/ifelse-ext-text3.ink diff --git a/examples/inkfiles/conditional/ifelse-ext-text3.ink.json b/lib/tests/data/conditional/ifelse-ext-text3.ink.json similarity index 100% rename from examples/inkfiles/conditional/ifelse-ext-text3.ink.json rename to lib/tests/data/conditional/ifelse-ext-text3.ink.json diff --git a/examples/inkfiles/conditional/ifelse-ext.ink b/lib/tests/data/conditional/ifelse-ext.ink similarity index 100% rename from examples/inkfiles/conditional/ifelse-ext.ink rename to lib/tests/data/conditional/ifelse-ext.ink diff --git a/examples/inkfiles/conditional/ifelse-ext.ink.json b/lib/tests/data/conditional/ifelse-ext.ink.json similarity index 100% rename from examples/inkfiles/conditional/ifelse-ext.ink.json rename to lib/tests/data/conditional/ifelse-ext.ink.json diff --git a/examples/inkfiles/conditional/ifelse.ink b/lib/tests/data/conditional/ifelse.ink similarity index 100% rename from examples/inkfiles/conditional/ifelse.ink rename to lib/tests/data/conditional/ifelse.ink diff --git a/examples/inkfiles/conditional/ifelse.ink.json b/lib/tests/data/conditional/ifelse.ink.json similarity index 100% rename from examples/inkfiles/conditional/ifelse.ink.json rename to lib/tests/data/conditional/ifelse.ink.json diff --git a/examples/inkfiles/conditional/iffalse.ink b/lib/tests/data/conditional/iffalse.ink similarity index 100% rename from examples/inkfiles/conditional/iffalse.ink rename to lib/tests/data/conditional/iffalse.ink diff --git a/examples/inkfiles/conditional/iffalse.ink.json b/lib/tests/data/conditional/iffalse.ink.json similarity index 100% rename from examples/inkfiles/conditional/iffalse.ink.json rename to lib/tests/data/conditional/iffalse.ink.json diff --git a/examples/inkfiles/conditional/iftrue.ink b/lib/tests/data/conditional/iftrue.ink similarity index 100% rename from examples/inkfiles/conditional/iftrue.ink rename to lib/tests/data/conditional/iftrue.ink diff --git a/examples/inkfiles/conditional/iftrue.ink.json b/lib/tests/data/conditional/iftrue.ink.json similarity index 100% rename from examples/inkfiles/conditional/iftrue.ink.json rename to lib/tests/data/conditional/iftrue.ink.json diff --git a/examples/inkfiles/conditional/multiline-choice.ink b/lib/tests/data/conditional/multiline-choice.ink similarity index 100% rename from examples/inkfiles/conditional/multiline-choice.ink rename to lib/tests/data/conditional/multiline-choice.ink diff --git a/examples/inkfiles/conditional/multiline-choice.ink.json b/lib/tests/data/conditional/multiline-choice.ink.json similarity index 100% rename from examples/inkfiles/conditional/multiline-choice.ink.json rename to lib/tests/data/conditional/multiline-choice.ink.json diff --git a/examples/inkfiles/conditional/multiline-divert.ink b/lib/tests/data/conditional/multiline-divert.ink similarity index 100% rename from examples/inkfiles/conditional/multiline-divert.ink rename to lib/tests/data/conditional/multiline-divert.ink diff --git a/examples/inkfiles/conditional/multiline-divert.ink.json b/lib/tests/data/conditional/multiline-divert.ink.json similarity index 100% rename from examples/inkfiles/conditional/multiline-divert.ink.json rename to lib/tests/data/conditional/multiline-divert.ink.json diff --git a/examples/inkfiles/conditional/multiline.ink b/lib/tests/data/conditional/multiline.ink similarity index 100% rename from examples/inkfiles/conditional/multiline.ink rename to lib/tests/data/conditional/multiline.ink diff --git a/examples/inkfiles/conditional/multiline.ink.json b/lib/tests/data/conditional/multiline.ink.json similarity index 100% rename from examples/inkfiles/conditional/multiline.ink.json rename to lib/tests/data/conditional/multiline.ink.json diff --git a/examples/inkfiles/conditional/once.ink b/lib/tests/data/conditional/once.ink similarity index 100% rename from examples/inkfiles/conditional/once.ink rename to lib/tests/data/conditional/once.ink diff --git a/examples/inkfiles/conditional/once.ink.json b/lib/tests/data/conditional/once.ink.json similarity index 100% rename from examples/inkfiles/conditional/once.ink.json rename to lib/tests/data/conditional/once.ink.json diff --git a/examples/inkfiles/conditional/shuffle.ink b/lib/tests/data/conditional/shuffle.ink similarity index 100% rename from examples/inkfiles/conditional/shuffle.ink rename to lib/tests/data/conditional/shuffle.ink diff --git a/examples/inkfiles/conditional/shuffle.ink.json b/lib/tests/data/conditional/shuffle.ink.json similarity index 100% rename from examples/inkfiles/conditional/shuffle.ink.json rename to lib/tests/data/conditional/shuffle.ink.json diff --git a/examples/inkfiles/conditional/shuffle_once.ink b/lib/tests/data/conditional/shuffle_once.ink similarity index 100% rename from examples/inkfiles/conditional/shuffle_once.ink rename to lib/tests/data/conditional/shuffle_once.ink diff --git a/examples/inkfiles/conditional/shuffle_once.ink.json b/lib/tests/data/conditional/shuffle_once.ink.json similarity index 100% rename from examples/inkfiles/conditional/shuffle_once.ink.json rename to lib/tests/data/conditional/shuffle_once.ink.json diff --git a/examples/inkfiles/conditional/shuffle_stopping.ink b/lib/tests/data/conditional/shuffle_stopping.ink similarity index 100% rename from examples/inkfiles/conditional/shuffle_stopping.ink rename to lib/tests/data/conditional/shuffle_stopping.ink diff --git a/examples/inkfiles/conditional/shuffle_stopping.ink.json b/lib/tests/data/conditional/shuffle_stopping.ink.json similarity index 100% rename from examples/inkfiles/conditional/shuffle_stopping.ink.json rename to lib/tests/data/conditional/shuffle_stopping.ink.json diff --git a/examples/inkfiles/conditional/stopping.ink b/lib/tests/data/conditional/stopping.ink similarity index 100% rename from examples/inkfiles/conditional/stopping.ink rename to lib/tests/data/conditional/stopping.ink diff --git a/examples/inkfiles/conditional/stopping.ink.json b/lib/tests/data/conditional/stopping.ink.json similarity index 100% rename from examples/inkfiles/conditional/stopping.ink.json rename to lib/tests/data/conditional/stopping.ink.json diff --git a/examples/inkfiles/divert/complex-branching.ink b/lib/tests/data/divert/complex-branching.ink similarity index 100% rename from examples/inkfiles/divert/complex-branching.ink rename to lib/tests/data/divert/complex-branching.ink diff --git a/examples/inkfiles/divert/complex-branching.ink.json b/lib/tests/data/divert/complex-branching.ink.json similarity index 100% rename from examples/inkfiles/divert/complex-branching.ink.json rename to lib/tests/data/divert/complex-branching.ink.json diff --git a/examples/inkfiles/divert/divert-on-choice.ink b/lib/tests/data/divert/divert-on-choice.ink similarity index 100% rename from examples/inkfiles/divert/divert-on-choice.ink rename to lib/tests/data/divert/divert-on-choice.ink diff --git a/examples/inkfiles/divert/divert-on-choice.ink.json b/lib/tests/data/divert/divert-on-choice.ink.json similarity index 100% rename from examples/inkfiles/divert/divert-on-choice.ink.json rename to lib/tests/data/divert/divert-on-choice.ink.json diff --git a/examples/inkfiles/divert/invisible-divert.ink b/lib/tests/data/divert/invisible-divert.ink similarity index 100% rename from examples/inkfiles/divert/invisible-divert.ink rename to lib/tests/data/divert/invisible-divert.ink diff --git a/examples/inkfiles/divert/invisible-divert.ink.json b/lib/tests/data/divert/invisible-divert.ink.json similarity index 100% rename from examples/inkfiles/divert/invisible-divert.ink.json rename to lib/tests/data/divert/invisible-divert.ink.json diff --git a/examples/inkfiles/divert/simple-divert.ink b/lib/tests/data/divert/simple-divert.ink similarity index 100% rename from examples/inkfiles/divert/simple-divert.ink rename to lib/tests/data/divert/simple-divert.ink diff --git a/examples/inkfiles/divert/simple-divert.ink.json b/lib/tests/data/divert/simple-divert.ink.json similarity index 100% rename from examples/inkfiles/divert/simple-divert.ink.json rename to lib/tests/data/divert/simple-divert.ink.json diff --git a/examples/inkfiles/function/complex-func1.ink b/lib/tests/data/function/complex-func1.ink similarity index 100% rename from examples/inkfiles/function/complex-func1.ink rename to lib/tests/data/function/complex-func1.ink diff --git a/examples/inkfiles/function/complex-func1.ink.json b/lib/tests/data/function/complex-func1.ink.json similarity index 100% rename from examples/inkfiles/function/complex-func1.ink.json rename to lib/tests/data/function/complex-func1.ink.json diff --git a/examples/inkfiles/function/complex-func2.ink b/lib/tests/data/function/complex-func2.ink similarity index 100% rename from examples/inkfiles/function/complex-func2.ink rename to lib/tests/data/function/complex-func2.ink diff --git a/examples/inkfiles/function/complex-func2.ink.json b/lib/tests/data/function/complex-func2.ink.json similarity index 100% rename from examples/inkfiles/function/complex-func2.ink.json rename to lib/tests/data/function/complex-func2.ink.json diff --git a/examples/inkfiles/function/complex-func3.ink b/lib/tests/data/function/complex-func3.ink similarity index 100% rename from examples/inkfiles/function/complex-func3.ink rename to lib/tests/data/function/complex-func3.ink diff --git a/examples/inkfiles/function/complex-func3.ink.json b/lib/tests/data/function/complex-func3.ink.json similarity index 100% rename from examples/inkfiles/function/complex-func3.ink.json rename to lib/tests/data/function/complex-func3.ink.json diff --git a/examples/inkfiles/function/evaluating-function-variablestate-bug.ink b/lib/tests/data/function/evaluating-function-variablestate-bug.ink similarity index 100% rename from examples/inkfiles/function/evaluating-function-variablestate-bug.ink rename to lib/tests/data/function/evaluating-function-variablestate-bug.ink diff --git a/examples/inkfiles/function/evaluating-function-variablestate-bug.ink.json b/lib/tests/data/function/evaluating-function-variablestate-bug.ink.json similarity index 100% rename from examples/inkfiles/function/evaluating-function-variablestate-bug.ink.json rename to lib/tests/data/function/evaluating-function-variablestate-bug.ink.json diff --git a/examples/inkfiles/function/func-basic.ink b/lib/tests/data/function/func-basic.ink similarity index 100% rename from examples/inkfiles/function/func-basic.ink rename to lib/tests/data/function/func-basic.ink diff --git a/examples/inkfiles/function/func-basic.ink.json b/lib/tests/data/function/func-basic.ink.json similarity index 100% rename from examples/inkfiles/function/func-basic.ink.json rename to lib/tests/data/function/func-basic.ink.json diff --git a/examples/inkfiles/function/func-inline.ink b/lib/tests/data/function/func-inline.ink similarity index 100% rename from examples/inkfiles/function/func-inline.ink rename to lib/tests/data/function/func-inline.ink diff --git a/examples/inkfiles/function/func-inline.ink.json b/lib/tests/data/function/func-inline.ink.json similarity index 100% rename from examples/inkfiles/function/func-inline.ink.json rename to lib/tests/data/function/func-inline.ink.json diff --git a/examples/inkfiles/function/func-none.ink b/lib/tests/data/function/func-none.ink similarity index 100% rename from examples/inkfiles/function/func-none.ink rename to lib/tests/data/function/func-none.ink diff --git a/examples/inkfiles/function/func-none.ink.json b/lib/tests/data/function/func-none.ink.json similarity index 100% rename from examples/inkfiles/function/func-none.ink.json rename to lib/tests/data/function/func-none.ink.json diff --git a/examples/inkfiles/function/rnd-func.ink b/lib/tests/data/function/rnd-func.ink similarity index 100% rename from examples/inkfiles/function/rnd-func.ink rename to lib/tests/data/function/rnd-func.ink diff --git a/examples/inkfiles/function/rnd-func.ink.json b/lib/tests/data/function/rnd-func.ink.json similarity index 100% rename from examples/inkfiles/function/rnd-func.ink.json rename to lib/tests/data/function/rnd-func.ink.json diff --git a/examples/inkfiles/function/setvar-func.ink b/lib/tests/data/function/setvar-func.ink similarity index 100% rename from examples/inkfiles/function/setvar-func.ink rename to lib/tests/data/function/setvar-func.ink diff --git a/examples/inkfiles/function/setvar-func.ink.json b/lib/tests/data/function/setvar-func.ink.json similarity index 100% rename from examples/inkfiles/function/setvar-func.ink.json rename to lib/tests/data/function/setvar-func.ink.json diff --git a/examples/inkfiles/function/test-error.ink b/lib/tests/data/function/test-error.ink similarity index 100% rename from examples/inkfiles/function/test-error.ink rename to lib/tests/data/function/test-error.ink diff --git a/examples/inkfiles/function/test-error.ink.json b/lib/tests/data/function/test-error.ink.json similarity index 100% rename from examples/inkfiles/function/test-error.ink.json rename to lib/tests/data/function/test-error.ink.json diff --git a/examples/inkfiles/gather/complex-flow.ink b/lib/tests/data/gather/complex-flow.ink similarity index 100% rename from examples/inkfiles/gather/complex-flow.ink rename to lib/tests/data/gather/complex-flow.ink diff --git a/examples/inkfiles/gather/complex-flow.ink.json b/lib/tests/data/gather/complex-flow.ink.json similarity index 100% rename from examples/inkfiles/gather/complex-flow.ink.json rename to lib/tests/data/gather/complex-flow.ink.json diff --git a/examples/inkfiles/gather/deep-nesting.ink b/lib/tests/data/gather/deep-nesting.ink similarity index 100% rename from examples/inkfiles/gather/deep-nesting.ink rename to lib/tests/data/gather/deep-nesting.ink diff --git a/examples/inkfiles/gather/deep-nesting.ink.json b/lib/tests/data/gather/deep-nesting.ink.json similarity index 100% rename from examples/inkfiles/gather/deep-nesting.ink.json rename to lib/tests/data/gather/deep-nesting.ink.json diff --git a/examples/inkfiles/gather/gather-basic.ink b/lib/tests/data/gather/gather-basic.ink similarity index 100% rename from examples/inkfiles/gather/gather-basic.ink rename to lib/tests/data/gather/gather-basic.ink diff --git a/examples/inkfiles/gather/gather-basic.ink.json b/lib/tests/data/gather/gather-basic.ink.json similarity index 100% rename from examples/inkfiles/gather/gather-basic.ink.json rename to lib/tests/data/gather/gather-basic.ink.json diff --git a/examples/inkfiles/gather/gather-chain.ink b/lib/tests/data/gather/gather-chain.ink similarity index 100% rename from examples/inkfiles/gather/gather-chain.ink rename to lib/tests/data/gather/gather-chain.ink diff --git a/examples/inkfiles/gather/gather-chain.ink.json b/lib/tests/data/gather/gather-chain.ink.json similarity index 100% rename from examples/inkfiles/gather/gather-chain.ink.json rename to lib/tests/data/gather/gather-chain.ink.json diff --git a/examples/inkfiles/gather/nested-flow.ink b/lib/tests/data/gather/nested-flow.ink similarity index 100% rename from examples/inkfiles/gather/nested-flow.ink rename to lib/tests/data/gather/nested-flow.ink diff --git a/examples/inkfiles/gather/nested-flow.ink.json b/lib/tests/data/gather/nested-flow.ink.json similarity index 100% rename from examples/inkfiles/gather/nested-flow.ink.json rename to lib/tests/data/gather/nested-flow.ink.json diff --git a/examples/inkfiles/gather/nested-gather.ink b/lib/tests/data/gather/nested-gather.ink similarity index 100% rename from examples/inkfiles/gather/nested-gather.ink rename to lib/tests/data/gather/nested-gather.ink diff --git a/examples/inkfiles/gather/nested-gather.ink.json b/lib/tests/data/gather/nested-gather.ink.json similarity index 100% rename from examples/inkfiles/gather/nested-gather.ink.json rename to lib/tests/data/gather/nested-gather.ink.json diff --git a/examples/inkfiles/glue/glue-with-divert.ink b/lib/tests/data/glue/glue-with-divert.ink similarity index 100% rename from examples/inkfiles/glue/glue-with-divert.ink rename to lib/tests/data/glue/glue-with-divert.ink diff --git a/examples/inkfiles/glue/glue-with-divert.ink.json b/lib/tests/data/glue/glue-with-divert.ink.json similarity index 100% rename from examples/inkfiles/glue/glue-with-divert.ink.json rename to lib/tests/data/glue/glue-with-divert.ink.json diff --git a/examples/inkfiles/glue/left-right-glue-matching.ink b/lib/tests/data/glue/left-right-glue-matching.ink similarity index 100% rename from examples/inkfiles/glue/left-right-glue-matching.ink rename to lib/tests/data/glue/left-right-glue-matching.ink diff --git a/examples/inkfiles/glue/left-right-glue-matching.ink.json b/lib/tests/data/glue/left-right-glue-matching.ink.json similarity index 100% rename from examples/inkfiles/glue/left-right-glue-matching.ink.json rename to lib/tests/data/glue/left-right-glue-matching.ink.json diff --git a/examples/inkfiles/glue/simple-glue.ink b/lib/tests/data/glue/simple-glue.ink similarity index 100% rename from examples/inkfiles/glue/simple-glue.ink rename to lib/tests/data/glue/simple-glue.ink diff --git a/examples/inkfiles/glue/simple-glue.ink.json b/lib/tests/data/glue/simple-glue.ink.json similarity index 100% rename from examples/inkfiles/glue/simple-glue.ink.json rename to lib/tests/data/glue/simple-glue.ink.json diff --git a/examples/inkfiles/glue/testbugfix1.ink b/lib/tests/data/glue/testbugfix1.ink similarity index 100% rename from examples/inkfiles/glue/testbugfix1.ink rename to lib/tests/data/glue/testbugfix1.ink diff --git a/examples/inkfiles/glue/testbugfix1.ink.json b/lib/tests/data/glue/testbugfix1.ink.json similarity index 100% rename from examples/inkfiles/glue/testbugfix1.ink.json rename to lib/tests/data/glue/testbugfix1.ink.json diff --git a/examples/inkfiles/glue/testbugfix2.ink b/lib/tests/data/glue/testbugfix2.ink similarity index 100% rename from examples/inkfiles/glue/testbugfix2.ink rename to lib/tests/data/glue/testbugfix2.ink diff --git a/examples/inkfiles/glue/testbugfix2.ink.json b/lib/tests/data/glue/testbugfix2.ink.json similarity index 100% rename from examples/inkfiles/glue/testbugfix2.ink.json rename to lib/tests/data/glue/testbugfix2.ink.json diff --git a/examples/inkfiles/knot/multi-line.ink b/lib/tests/data/knot/multi-line.ink similarity index 100% rename from examples/inkfiles/knot/multi-line.ink rename to lib/tests/data/knot/multi-line.ink diff --git a/examples/inkfiles/knot/multi-line.ink.json b/lib/tests/data/knot/multi-line.ink.json similarity index 100% rename from examples/inkfiles/knot/multi-line.ink.json rename to lib/tests/data/knot/multi-line.ink.json diff --git a/examples/inkfiles/knot/param-floats.ink b/lib/tests/data/knot/param-floats.ink similarity index 100% rename from examples/inkfiles/knot/param-floats.ink rename to lib/tests/data/knot/param-floats.ink diff --git a/examples/inkfiles/knot/param-floats.ink.json b/lib/tests/data/knot/param-floats.ink.json similarity index 100% rename from examples/inkfiles/knot/param-floats.ink.json rename to lib/tests/data/knot/param-floats.ink.json diff --git a/examples/inkfiles/knot/param-ints.ink b/lib/tests/data/knot/param-ints.ink similarity index 100% rename from examples/inkfiles/knot/param-ints.ink rename to lib/tests/data/knot/param-ints.ink diff --git a/examples/inkfiles/knot/param-ints.ink.json b/lib/tests/data/knot/param-ints.ink.json similarity index 100% rename from examples/inkfiles/knot/param-ints.ink.json rename to lib/tests/data/knot/param-ints.ink.json diff --git a/examples/inkfiles/knot/param-multi.ink b/lib/tests/data/knot/param-multi.ink similarity index 100% rename from examples/inkfiles/knot/param-multi.ink rename to lib/tests/data/knot/param-multi.ink diff --git a/examples/inkfiles/knot/param-multi.ink.json b/lib/tests/data/knot/param-multi.ink.json similarity index 100% rename from examples/inkfiles/knot/param-multi.ink.json rename to lib/tests/data/knot/param-multi.ink.json diff --git a/examples/inkfiles/knot/param-recurse.ink b/lib/tests/data/knot/param-recurse.ink similarity index 100% rename from examples/inkfiles/knot/param-recurse.ink rename to lib/tests/data/knot/param-recurse.ink diff --git a/examples/inkfiles/knot/param-recurse.ink.json b/lib/tests/data/knot/param-recurse.ink.json similarity index 100% rename from examples/inkfiles/knot/param-recurse.ink.json rename to lib/tests/data/knot/param-recurse.ink.json diff --git a/examples/inkfiles/knot/param-strings.ink b/lib/tests/data/knot/param-strings.ink similarity index 100% rename from examples/inkfiles/knot/param-strings.ink rename to lib/tests/data/knot/param-strings.ink diff --git a/examples/inkfiles/knot/param-strings.ink.json b/lib/tests/data/knot/param-strings.ink.json similarity index 100% rename from examples/inkfiles/knot/param-strings.ink.json rename to lib/tests/data/knot/param-strings.ink.json diff --git a/examples/inkfiles/knot/param-vars.ink b/lib/tests/data/knot/param-vars.ink similarity index 100% rename from examples/inkfiles/knot/param-vars.ink rename to lib/tests/data/knot/param-vars.ink diff --git a/examples/inkfiles/knot/param-vars.ink.json b/lib/tests/data/knot/param-vars.ink.json similarity index 100% rename from examples/inkfiles/knot/param-vars.ink.json rename to lib/tests/data/knot/param-vars.ink.json diff --git a/examples/inkfiles/knot/single-line.ink b/lib/tests/data/knot/single-line.ink similarity index 100% rename from examples/inkfiles/knot/single-line.ink rename to lib/tests/data/knot/single-line.ink diff --git a/examples/inkfiles/knot/single-line.ink.json b/lib/tests/data/knot/single-line.ink.json similarity index 100% rename from examples/inkfiles/knot/single-line.ink.json rename to lib/tests/data/knot/single-line.ink.json diff --git a/examples/inkfiles/knot/strip-empty-lines.ink b/lib/tests/data/knot/strip-empty-lines.ink similarity index 100% rename from examples/inkfiles/knot/strip-empty-lines.ink rename to lib/tests/data/knot/strip-empty-lines.ink diff --git a/examples/inkfiles/knot/strip-empty-lines.ink.json b/lib/tests/data/knot/strip-empty-lines.ink.json similarity index 100% rename from examples/inkfiles/knot/strip-empty-lines.ink.json rename to lib/tests/data/knot/strip-empty-lines.ink.json diff --git a/examples/inkfiles/lists/basic-operations.ink b/lib/tests/data/lists/basic-operations.ink similarity index 100% rename from examples/inkfiles/lists/basic-operations.ink rename to lib/tests/data/lists/basic-operations.ink diff --git a/examples/inkfiles/lists/basic-operations.ink.json b/lib/tests/data/lists/basic-operations.ink.json similarity index 100% rename from examples/inkfiles/lists/basic-operations.ink.json rename to lib/tests/data/lists/basic-operations.ink.json diff --git a/examples/inkfiles/lists/bug-adding-element.ink b/lib/tests/data/lists/bug-adding-element.ink similarity index 100% rename from examples/inkfiles/lists/bug-adding-element.ink rename to lib/tests/data/lists/bug-adding-element.ink diff --git a/examples/inkfiles/lists/bug-adding-element.ink.json b/lib/tests/data/lists/bug-adding-element.ink.json similarity index 100% rename from examples/inkfiles/lists/bug-adding-element.ink.json rename to lib/tests/data/lists/bug-adding-element.ink.json diff --git a/examples/inkfiles/lists/empty-list-origin-after-assignment.ink b/lib/tests/data/lists/empty-list-origin-after-assignment.ink similarity index 100% rename from examples/inkfiles/lists/empty-list-origin-after-assignment.ink rename to lib/tests/data/lists/empty-list-origin-after-assignment.ink diff --git a/examples/inkfiles/lists/empty-list-origin-after-assignment.ink.json b/lib/tests/data/lists/empty-list-origin-after-assignment.ink.json similarity index 100% rename from examples/inkfiles/lists/empty-list-origin-after-assignment.ink.json rename to lib/tests/data/lists/empty-list-origin-after-assignment.ink.json diff --git a/examples/inkfiles/lists/empty-list-origin.ink b/lib/tests/data/lists/empty-list-origin.ink similarity index 100% rename from examples/inkfiles/lists/empty-list-origin.ink rename to lib/tests/data/lists/empty-list-origin.ink diff --git a/examples/inkfiles/lists/empty-list-origin.ink.json b/lib/tests/data/lists/empty-list-origin.ink.json similarity index 100% rename from examples/inkfiles/lists/empty-list-origin.ink.json rename to lib/tests/data/lists/empty-list-origin.ink.json diff --git a/examples/inkfiles/lists/list-mixed-items.ink b/lib/tests/data/lists/list-mixed-items.ink similarity index 100% rename from examples/inkfiles/lists/list-mixed-items.ink rename to lib/tests/data/lists/list-mixed-items.ink diff --git a/examples/inkfiles/lists/list-mixed-items.ink.json b/lib/tests/data/lists/list-mixed-items.ink.json similarity index 100% rename from examples/inkfiles/lists/list-mixed-items.ink.json rename to lib/tests/data/lists/list-mixed-items.ink.json diff --git a/examples/inkfiles/lists/list-range.ink b/lib/tests/data/lists/list-range.ink similarity index 100% rename from examples/inkfiles/lists/list-range.ink rename to lib/tests/data/lists/list-range.ink diff --git a/examples/inkfiles/lists/list-range.ink.json b/lib/tests/data/lists/list-range.ink.json similarity index 100% rename from examples/inkfiles/lists/list-range.ink.json rename to lib/tests/data/lists/list-range.ink.json diff --git a/examples/inkfiles/lists/list-save-load.ink b/lib/tests/data/lists/list-save-load.ink similarity index 100% rename from examples/inkfiles/lists/list-save-load.ink rename to lib/tests/data/lists/list-save-load.ink diff --git a/examples/inkfiles/lists/list-save-load.ink.json b/lib/tests/data/lists/list-save-load.ink.json similarity index 100% rename from examples/inkfiles/lists/list-save-load.ink.json rename to lib/tests/data/lists/list-save-load.ink.json diff --git a/examples/inkfiles/lists/more-list-operations.ink b/lib/tests/data/lists/more-list-operations.ink similarity index 100% rename from examples/inkfiles/lists/more-list-operations.ink rename to lib/tests/data/lists/more-list-operations.ink diff --git a/examples/inkfiles/lists/more-list-operations.ink.json b/lib/tests/data/lists/more-list-operations.ink.json similarity index 100% rename from examples/inkfiles/lists/more-list-operations.ink.json rename to lib/tests/data/lists/more-list-operations.ink.json diff --git a/examples/inkfiles/lists/more-list-operations2.ink b/lib/tests/data/lists/more-list-operations2.ink similarity index 100% rename from examples/inkfiles/lists/more-list-operations2.ink rename to lib/tests/data/lists/more-list-operations2.ink diff --git a/examples/inkfiles/lists/more-list-operations2.ink.json b/lib/tests/data/lists/more-list-operations2.ink.json similarity index 100% rename from examples/inkfiles/lists/more-list-operations2.ink.json rename to lib/tests/data/lists/more-list-operations2.ink.json diff --git a/examples/inkfiles/misc/issue15.ink b/lib/tests/data/misc/issue15.ink similarity index 100% rename from examples/inkfiles/misc/issue15.ink rename to lib/tests/data/misc/issue15.ink diff --git a/examples/inkfiles/misc/issue15.ink.json b/lib/tests/data/misc/issue15.ink.json similarity index 100% rename from examples/inkfiles/misc/issue15.ink.json rename to lib/tests/data/misc/issue15.ink.json diff --git a/examples/inkfiles/misc/operations.ink b/lib/tests/data/misc/operations.ink similarity index 100% rename from examples/inkfiles/misc/operations.ink rename to lib/tests/data/misc/operations.ink diff --git a/examples/inkfiles/misc/operations.ink.json b/lib/tests/data/misc/operations.ink.json similarity index 100% rename from examples/inkfiles/misc/operations.ink.json rename to lib/tests/data/misc/operations.ink.json diff --git a/examples/inkfiles/misc/read-counts.ink b/lib/tests/data/misc/read-counts.ink similarity index 100% rename from examples/inkfiles/misc/read-counts.ink rename to lib/tests/data/misc/read-counts.ink diff --git a/examples/inkfiles/misc/read-counts.ink.json b/lib/tests/data/misc/read-counts.ink.json similarity index 100% rename from examples/inkfiles/misc/read-counts.ink.json rename to lib/tests/data/misc/read-counts.ink.json diff --git a/examples/inkfiles/misc/turns-since.ink b/lib/tests/data/misc/turns-since.ink similarity index 100% rename from examples/inkfiles/misc/turns-since.ink rename to lib/tests/data/misc/turns-since.ink diff --git a/examples/inkfiles/misc/turns-since.ink.json b/lib/tests/data/misc/turns-since.ink.json similarity index 100% rename from examples/inkfiles/misc/turns-since.ink.json rename to lib/tests/data/misc/turns-since.ink.json diff --git a/examples/inkfiles/runtime/external-function-0-arg.ink b/lib/tests/data/runtime/external-function-0-arg.ink similarity index 100% rename from examples/inkfiles/runtime/external-function-0-arg.ink rename to lib/tests/data/runtime/external-function-0-arg.ink diff --git a/examples/inkfiles/runtime/external-function-0-arg.ink.json b/lib/tests/data/runtime/external-function-0-arg.ink.json similarity index 100% rename from examples/inkfiles/runtime/external-function-0-arg.ink.json rename to lib/tests/data/runtime/external-function-0-arg.ink.json diff --git a/examples/inkfiles/runtime/external-function-1-arg.ink b/lib/tests/data/runtime/external-function-1-arg.ink similarity index 100% rename from examples/inkfiles/runtime/external-function-1-arg.ink rename to lib/tests/data/runtime/external-function-1-arg.ink diff --git a/examples/inkfiles/runtime/external-function-1-arg.ink.json b/lib/tests/data/runtime/external-function-1-arg.ink.json similarity index 100% rename from examples/inkfiles/runtime/external-function-1-arg.ink.json rename to lib/tests/data/runtime/external-function-1-arg.ink.json diff --git a/examples/inkfiles/runtime/external-function-2-arg.ink b/lib/tests/data/runtime/external-function-2-arg.ink similarity index 100% rename from examples/inkfiles/runtime/external-function-2-arg.ink rename to lib/tests/data/runtime/external-function-2-arg.ink diff --git a/examples/inkfiles/runtime/external-function-2-arg.ink.json b/lib/tests/data/runtime/external-function-2-arg.ink.json similarity index 100% rename from examples/inkfiles/runtime/external-function-2-arg.ink.json rename to lib/tests/data/runtime/external-function-2-arg.ink.json diff --git a/examples/inkfiles/runtime/external-function-3-arg.ink b/lib/tests/data/runtime/external-function-3-arg.ink similarity index 100% rename from examples/inkfiles/runtime/external-function-3-arg.ink rename to lib/tests/data/runtime/external-function-3-arg.ink diff --git a/examples/inkfiles/runtime/external-function-3-arg.ink.json b/lib/tests/data/runtime/external-function-3-arg.ink.json similarity index 100% rename from examples/inkfiles/runtime/external-function-3-arg.ink.json rename to lib/tests/data/runtime/external-function-3-arg.ink.json diff --git a/examples/inkfiles/runtime/jump-knot.ink b/lib/tests/data/runtime/jump-knot.ink similarity index 100% rename from examples/inkfiles/runtime/jump-knot.ink rename to lib/tests/data/runtime/jump-knot.ink diff --git a/examples/inkfiles/runtime/jump-knot.ink.json b/lib/tests/data/runtime/jump-knot.ink.json similarity index 100% rename from examples/inkfiles/runtime/jump-knot.ink.json rename to lib/tests/data/runtime/jump-knot.ink.json diff --git a/examples/inkfiles/runtime/jump-stitch.ink b/lib/tests/data/runtime/jump-stitch.ink similarity index 100% rename from examples/inkfiles/runtime/jump-stitch.ink rename to lib/tests/data/runtime/jump-stitch.ink diff --git a/examples/inkfiles/runtime/jump-stitch.ink.json b/lib/tests/data/runtime/jump-stitch.ink.json similarity index 100% rename from examples/inkfiles/runtime/jump-stitch.ink.json rename to lib/tests/data/runtime/jump-stitch.ink.json diff --git a/examples/inkfiles/runtime/load-save.ink b/lib/tests/data/runtime/load-save.ink similarity index 100% rename from examples/inkfiles/runtime/load-save.ink rename to lib/tests/data/runtime/load-save.ink diff --git a/examples/inkfiles/runtime/load-save.ink.json b/lib/tests/data/runtime/load-save.ink.json similarity index 100% rename from examples/inkfiles/runtime/load-save.ink.json rename to lib/tests/data/runtime/load-save.ink.json diff --git a/examples/inkfiles/runtime/multiflow-basics.ink b/lib/tests/data/runtime/multiflow-basics.ink similarity index 100% rename from examples/inkfiles/runtime/multiflow-basics.ink rename to lib/tests/data/runtime/multiflow-basics.ink diff --git a/examples/inkfiles/runtime/multiflow-basics.ink.json b/lib/tests/data/runtime/multiflow-basics.ink.json similarity index 100% rename from examples/inkfiles/runtime/multiflow-basics.ink.json rename to lib/tests/data/runtime/multiflow-basics.ink.json diff --git a/examples/inkfiles/runtime/multiflow-saveloadthreads.ink b/lib/tests/data/runtime/multiflow-saveloadthreads.ink similarity index 100% rename from examples/inkfiles/runtime/multiflow-saveloadthreads.ink rename to lib/tests/data/runtime/multiflow-saveloadthreads.ink diff --git a/examples/inkfiles/runtime/multiflow-saveloadthreads.ink.json b/lib/tests/data/runtime/multiflow-saveloadthreads.ink.json similarity index 100% rename from examples/inkfiles/runtime/multiflow-saveloadthreads.ink.json rename to lib/tests/data/runtime/multiflow-saveloadthreads.ink.json diff --git a/examples/inkfiles/runtime/read-visit-counts.ink b/lib/tests/data/runtime/read-visit-counts.ink similarity index 100% rename from examples/inkfiles/runtime/read-visit-counts.ink rename to lib/tests/data/runtime/read-visit-counts.ink diff --git a/examples/inkfiles/runtime/read-visit-counts.ink.json b/lib/tests/data/runtime/read-visit-counts.ink.json similarity index 100% rename from examples/inkfiles/runtime/read-visit-counts.ink.json rename to lib/tests/data/runtime/read-visit-counts.ink.json diff --git a/examples/inkfiles/runtime/saving-loading.ink b/lib/tests/data/runtime/saving-loading.ink similarity index 100% rename from examples/inkfiles/runtime/saving-loading.ink rename to lib/tests/data/runtime/saving-loading.ink diff --git a/examples/inkfiles/runtime/saving-loading.ink.json b/lib/tests/data/runtime/saving-loading.ink.json similarity index 100% rename from examples/inkfiles/runtime/saving-loading.ink.json rename to lib/tests/data/runtime/saving-loading.ink.json diff --git a/examples/inkfiles/runtime/set-get-variables.ink b/lib/tests/data/runtime/set-get-variables.ink similarity index 100% rename from examples/inkfiles/runtime/set-get-variables.ink rename to lib/tests/data/runtime/set-get-variables.ink diff --git a/examples/inkfiles/runtime/set-get-variables.ink.json b/lib/tests/data/runtime/set-get-variables.ink.json similarity index 100% rename from examples/inkfiles/runtime/set-get-variables.ink.json rename to lib/tests/data/runtime/set-get-variables.ink.json diff --git a/examples/inkfiles/runtime/variable-observers.ink b/lib/tests/data/runtime/variable-observers.ink similarity index 100% rename from examples/inkfiles/runtime/variable-observers.ink rename to lib/tests/data/runtime/variable-observers.ink diff --git a/examples/inkfiles/runtime/variable-observers.ink.json b/lib/tests/data/runtime/variable-observers.ink.json similarity index 100% rename from examples/inkfiles/runtime/variable-observers.ink.json rename to lib/tests/data/runtime/variable-observers.ink.json diff --git a/examples/inkfiles/stitch/auto-stitch.ink b/lib/tests/data/stitch/auto-stitch.ink similarity index 100% rename from examples/inkfiles/stitch/auto-stitch.ink rename to lib/tests/data/stitch/auto-stitch.ink diff --git a/examples/inkfiles/stitch/auto-stitch.ink.json b/lib/tests/data/stitch/auto-stitch.ink.json similarity index 100% rename from examples/inkfiles/stitch/auto-stitch.ink.json rename to lib/tests/data/stitch/auto-stitch.ink.json diff --git a/examples/inkfiles/stitch/manual-stitch.ink b/lib/tests/data/stitch/manual-stitch.ink similarity index 100% rename from examples/inkfiles/stitch/manual-stitch.ink rename to lib/tests/data/stitch/manual-stitch.ink diff --git a/examples/inkfiles/stitch/manual-stitch.ink.json b/lib/tests/data/stitch/manual-stitch.ink.json similarity index 100% rename from examples/inkfiles/stitch/manual-stitch.ink.json rename to lib/tests/data/stitch/manual-stitch.ink.json diff --git a/examples/inkfiles/tags/tags.ink b/lib/tests/data/tags/tags.ink similarity index 100% rename from examples/inkfiles/tags/tags.ink rename to lib/tests/data/tags/tags.ink diff --git a/examples/inkfiles/tags/tags.ink.json b/lib/tests/data/tags/tags.ink.json similarity index 100% rename from examples/inkfiles/tags/tags.ink.json rename to lib/tests/data/tags/tags.ink.json diff --git a/examples/inkfiles/tags/tagsDynamicContent.ink b/lib/tests/data/tags/tagsDynamicContent.ink similarity index 100% rename from examples/inkfiles/tags/tagsDynamicContent.ink rename to lib/tests/data/tags/tagsDynamicContent.ink diff --git a/examples/inkfiles/tags/tagsDynamicContent.ink.json b/lib/tests/data/tags/tagsDynamicContent.ink.json similarity index 100% rename from examples/inkfiles/tags/tagsDynamicContent.ink.json rename to lib/tests/data/tags/tagsDynamicContent.ink.json diff --git a/examples/inkfiles/tags/tagsInChoice.ink b/lib/tests/data/tags/tagsInChoice.ink similarity index 100% rename from examples/inkfiles/tags/tagsInChoice.ink rename to lib/tests/data/tags/tagsInChoice.ink diff --git a/examples/inkfiles/tags/tagsInChoice.ink.json b/lib/tests/data/tags/tagsInChoice.ink.json similarity index 100% rename from examples/inkfiles/tags/tagsInChoice.ink.json rename to lib/tests/data/tags/tagsInChoice.ink.json diff --git a/examples/inkfiles/tags/tagsInSeq.ink b/lib/tests/data/tags/tagsInSeq.ink similarity index 100% rename from examples/inkfiles/tags/tagsInSeq.ink rename to lib/tests/data/tags/tagsInSeq.ink diff --git a/examples/inkfiles/tags/tagsInSeq.ink.json b/lib/tests/data/tags/tagsInSeq.ink.json similarity index 100% rename from examples/inkfiles/tags/tagsInSeq.ink.json rename to lib/tests/data/tags/tagsInSeq.ink.json diff --git a/examples/inkfiles/threads/thread-bug.ink b/lib/tests/data/threads/thread-bug.ink similarity index 100% rename from examples/inkfiles/threads/thread-bug.ink rename to lib/tests/data/threads/thread-bug.ink diff --git a/examples/inkfiles/threads/thread-bug.ink.json b/lib/tests/data/threads/thread-bug.ink.json similarity index 100% rename from examples/inkfiles/threads/thread-bug.ink.json rename to lib/tests/data/threads/thread-bug.ink.json diff --git a/examples/inkfiles/tunnels/tunnel-onwards-divert-override.ink b/lib/tests/data/tunnels/tunnel-onwards-divert-override.ink similarity index 100% rename from examples/inkfiles/tunnels/tunnel-onwards-divert-override.ink rename to lib/tests/data/tunnels/tunnel-onwards-divert-override.ink diff --git a/examples/inkfiles/tunnels/tunnel-onwards-divert-override.ink.json b/lib/tests/data/tunnels/tunnel-onwards-divert-override.ink.json similarity index 100% rename from examples/inkfiles/tunnels/tunnel-onwards-divert-override.ink.json rename to lib/tests/data/tunnels/tunnel-onwards-divert-override.ink.json diff --git a/examples/inkfiles/variable/var-divert.ink b/lib/tests/data/variable/var-divert.ink similarity index 100% rename from examples/inkfiles/variable/var-divert.ink rename to lib/tests/data/variable/var-divert.ink diff --git a/examples/inkfiles/variable/var-divert.ink.json b/lib/tests/data/variable/var-divert.ink.json similarity index 100% rename from examples/inkfiles/variable/var-divert.ink.json rename to lib/tests/data/variable/var-divert.ink.json diff --git a/examples/inkfiles/variable/varcalc.ink b/lib/tests/data/variable/varcalc.ink similarity index 100% rename from examples/inkfiles/variable/varcalc.ink rename to lib/tests/data/variable/varcalc.ink diff --git a/examples/inkfiles/variable/varcalc.ink.json b/lib/tests/data/variable/varcalc.ink.json similarity index 100% rename from examples/inkfiles/variable/varcalc.ink.json rename to lib/tests/data/variable/varcalc.ink.json diff --git a/examples/inkfiles/variable/variable-declaration.ink b/lib/tests/data/variable/variable-declaration.ink similarity index 100% rename from examples/inkfiles/variable/variable-declaration.ink rename to lib/tests/data/variable/variable-declaration.ink diff --git a/examples/inkfiles/variable/variable-declaration.ink.json b/lib/tests/data/variable/variable-declaration.ink.json similarity index 100% rename from examples/inkfiles/variable/variable-declaration.ink.json rename to lib/tests/data/variable/variable-declaration.ink.json diff --git a/examples/inkfiles/variable/varstringinc.ink b/lib/tests/data/variable/varstringinc.ink similarity index 100% rename from examples/inkfiles/variable/varstringinc.ink rename to lib/tests/data/variable/varstringinc.ink diff --git a/examples/inkfiles/variable/varstringinc.ink.json b/lib/tests/data/variable/varstringinc.ink.json similarity index 100% rename from examples/inkfiles/variable/varstringinc.ink.json rename to lib/tests/data/variable/varstringinc.ink.json diff --git a/examples/inkfiles/variabletext/cycle.ink b/lib/tests/data/variabletext/cycle.ink similarity index 100% rename from examples/inkfiles/variabletext/cycle.ink rename to lib/tests/data/variabletext/cycle.ink diff --git a/examples/inkfiles/variabletext/cycle.ink.json b/lib/tests/data/variabletext/cycle.ink.json similarity index 100% rename from examples/inkfiles/variabletext/cycle.ink.json rename to lib/tests/data/variabletext/cycle.ink.json diff --git a/examples/inkfiles/variabletext/empty-elements.ink b/lib/tests/data/variabletext/empty-elements.ink similarity index 100% rename from examples/inkfiles/variabletext/empty-elements.ink rename to lib/tests/data/variabletext/empty-elements.ink diff --git a/examples/inkfiles/variabletext/empty-elements.ink.json b/lib/tests/data/variabletext/empty-elements.ink.json similarity index 100% rename from examples/inkfiles/variabletext/empty-elements.ink.json rename to lib/tests/data/variabletext/empty-elements.ink.json diff --git a/examples/inkfiles/variabletext/list-in-choice.ink b/lib/tests/data/variabletext/list-in-choice.ink similarity index 100% rename from examples/inkfiles/variabletext/list-in-choice.ink rename to lib/tests/data/variabletext/list-in-choice.ink diff --git a/examples/inkfiles/variabletext/list-in-choice.ink.json b/lib/tests/data/variabletext/list-in-choice.ink.json similarity index 100% rename from examples/inkfiles/variabletext/list-in-choice.ink.json rename to lib/tests/data/variabletext/list-in-choice.ink.json diff --git a/examples/inkfiles/variabletext/once.ink b/lib/tests/data/variabletext/once.ink similarity index 100% rename from examples/inkfiles/variabletext/once.ink rename to lib/tests/data/variabletext/once.ink diff --git a/examples/inkfiles/variabletext/once.ink.json b/lib/tests/data/variabletext/once.ink.json similarity index 100% rename from examples/inkfiles/variabletext/once.ink.json rename to lib/tests/data/variabletext/once.ink.json diff --git a/examples/inkfiles/variabletext/sequence.ink b/lib/tests/data/variabletext/sequence.ink similarity index 100% rename from examples/inkfiles/variabletext/sequence.ink rename to lib/tests/data/variabletext/sequence.ink diff --git a/examples/inkfiles/variabletext/sequence.ink.json b/lib/tests/data/variabletext/sequence.ink.json similarity index 100% rename from examples/inkfiles/variabletext/sequence.ink.json rename to lib/tests/data/variabletext/sequence.ink.json diff --git a/tests/divert_test.rs b/lib/tests/divert_test.rs similarity index 82% rename from tests/divert_test.rs rename to lib/tests/divert_test.rs index 6d62486..ca2f82c 100644 --- a/tests/divert_test.rs +++ b/lib/tests/divert_test.rs @@ -1,11 +1,11 @@ -use bladeink::{story::Story, story_error::StoryError}; +use bink::{story::Story, story_error::StoryError}; mod common; #[test] fn simple_divert_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/divert/simple-divert.ink.json").unwrap(); + common::get_json_string("tests/data/divert/simple-divert.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -20,7 +20,7 @@ fn simple_divert_test() -> Result<(), StoryError> { #[test] fn invisible_divert_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/divert/invisible-divert.ink.json").unwrap(); + common::get_json_string("tests/data/divert/invisible-divert.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -34,7 +34,7 @@ fn invisible_divert_test() -> Result<(), StoryError> { #[test] fn divert_on_choice_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/divert/divert-on-choice.ink.json").unwrap(); + common::get_json_string("tests/data/divert/divert-on-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -53,7 +53,7 @@ fn divert_on_choice_test() -> Result<(), StoryError> { #[test] fn complex_branching1_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/divert/complex-branching.ink.json").unwrap(); + common::get_json_string("tests/data/divert/complex-branching.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -73,7 +73,7 @@ fn complex_branching1_test() -> Result<(), StoryError> { #[test] fn complex_branching2_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/divert/complex-branching.ink.json").unwrap(); + common::get_json_string("tests/data/divert/complex-branching.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); diff --git a/tests/function_test.rs b/lib/tests/function_test.rs similarity index 78% rename from tests/function_test.rs rename to lib/tests/function_test.rs index a953e68..62e430c 100644 --- a/tests/function_test.rs +++ b/lib/tests/function_test.rs @@ -1,11 +1,11 @@ -use bladeink::{story::Story, story_error::StoryError}; +use bink::{story::Story, story_error::StoryError}; mod common; #[test] fn fun_basic_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/function/func-basic.ink.json").unwrap(); + common::get_json_string("tests/data/function/func-basic.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -19,7 +19,7 @@ fn fun_basic_test() -> Result<(), StoryError> { #[test] fn fun_none_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/function/func-none.ink.json").unwrap(); + common::get_json_string("tests/data/function/func-none.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -33,7 +33,7 @@ fn fun_none_test() -> Result<(), StoryError> { #[test] fn fun_inline_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/function/func-inline.ink.json").unwrap(); + common::get_json_string("tests/data/function/func-inline.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -47,7 +47,7 @@ fn fun_inline_test() -> Result<(), StoryError> { #[test] fn setvar_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/function/setvar-func.ink.json").unwrap(); + common::get_json_string("tests/data/function/setvar-func.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -61,7 +61,7 @@ fn setvar_test() -> Result<(), StoryError> { #[test] fn complex_func1_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/function/complex-func1.ink.json").unwrap(); + common::get_json_string("tests/data/function/complex-func1.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -75,7 +75,7 @@ fn complex_func1_test() -> Result<(), StoryError> { #[test] fn complex_func2_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/function/complex-func2.ink.json").unwrap(); + common::get_json_string("tests/data/function/complex-func2.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -89,7 +89,7 @@ fn complex_func2_test() -> Result<(), StoryError> { #[test] fn complex_func3_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/function/complex-func3.ink.json").unwrap(); + common::get_json_string("tests/data/function/complex-func3.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -104,7 +104,7 @@ fn complex_func3_test() -> Result<(), StoryError> { #[test] fn rnd() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/function/rnd-func.ink.json").unwrap(); + common::get_json_string("tests/data/function/rnd-func.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -121,7 +121,7 @@ fn rnd() -> Result<(), StoryError> { #[test] fn evaluating_function_variable_state_bug_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/function/evaluating-function-variablestate-bug.ink.json").unwrap(); + common::get_json_string("tests/data/function/evaluating-function-variablestate-bug.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); assert_eq!("Start\n", story.cont()?); diff --git a/tests/gather_test.rs b/lib/tests/gather_test.rs similarity index 87% rename from tests/gather_test.rs rename to lib/tests/gather_test.rs index bb6171e..95290d0 100644 --- a/tests/gather_test.rs +++ b/lib/tests/gather_test.rs @@ -1,11 +1,11 @@ -use bladeink::{story::Story, story_error::StoryError}; +use bink::{story::Story, story_error::StoryError}; mod common; #[test] fn gather_basic_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/gather/gather-basic.ink.json").unwrap(); + common::get_json_string("tests/data/gather/gather-basic.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -26,7 +26,7 @@ fn gather_basic_test() -> Result<(), StoryError> { #[test] fn gather_chain_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/gather/gather-chain.ink.json").unwrap(); + common::get_json_string("tests/data/gather/gather-chain.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -54,7 +54,7 @@ fn gather_chain_test() -> Result<(), StoryError> { #[test] fn nested_flow_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/gather/nested-flow.ink.json").unwrap(); + common::get_json_string("tests/data/gather/nested-flow.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -77,7 +77,7 @@ fn nested_flow_test() -> Result<(), StoryError> { #[test] fn deep_nesting_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/gather/deep-nesting.ink.json").unwrap(); + common::get_json_string("tests/data/gather/deep-nesting.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -113,7 +113,7 @@ fn deep_nesting_test() -> Result<(), StoryError> { #[test] fn complex_flow1_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/gather/complex-flow.ink.json").unwrap(); + common::get_json_string("tests/data/gather/complex-flow.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -131,7 +131,7 @@ fn complex_flow1_test() -> Result<(), StoryError> { #[test] fn complex_flow2_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/gather/complex-flow.ink.json").unwrap(); + common::get_json_string("tests/data/gather/complex-flow.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); diff --git a/tests/glue_test.rs b/lib/tests/glue_test.rs similarity index 77% rename from tests/glue_test.rs rename to lib/tests/glue_test.rs index 891d545..70a14a7 100644 --- a/tests/glue_test.rs +++ b/lib/tests/glue_test.rs @@ -1,11 +1,11 @@ -use bladeink::{story::Story, story_error::StoryError}; +use bink::{story::Story, story_error::StoryError}; mod common; #[test] fn simple_glue_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/glue/simple-glue.ink.json").unwrap(); + common::get_json_string("tests/data/glue/simple-glue.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -19,7 +19,7 @@ fn simple_glue_test() -> Result<(), StoryError> { #[test] fn glue_with_divert_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/glue/glue-with-divert.ink.json").unwrap(); + common::get_json_string("tests/data/glue/glue-with-divert.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -34,7 +34,7 @@ fn glue_with_divert_test() -> Result<(), StoryError> { #[test] fn has_left_right_glue_matching_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/glue/left-right-glue-matching.ink.json").unwrap(); + common::get_json_string("tests/data/glue/left-right-glue-matching.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -50,7 +50,7 @@ fn has_left_right_glue_matching_test() -> Result<(), StoryError> { #[test] fn bugfix1_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/glue/testbugfix1.ink.json").unwrap(); + common::get_json_string("tests/data/glue/testbugfix1.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -66,7 +66,7 @@ fn bugfix1_test() -> Result<(), StoryError> { #[test] fn bugfix2_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/glue/testbugfix2.ink.json").unwrap(); + common::get_json_string("tests/data/glue/testbugfix2.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); diff --git a/tests/knot_test.rs b/lib/tests/knot_test.rs similarity index 80% rename from tests/knot_test.rs rename to lib/tests/knot_test.rs index c74e5c8..f369328 100644 --- a/tests/knot_test.rs +++ b/lib/tests/knot_test.rs @@ -1,11 +1,11 @@ -use bladeink::{story::Story, story_error::StoryError}; +use bink::{story::Story, story_error::StoryError}; mod common; #[test] fn single_line_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/knot/single-line.ink.json").unwrap(); + common::get_json_string("tests/data/knot/single-line.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -20,7 +20,7 @@ fn single_line_test() -> Result<(), StoryError> { #[test] fn multi_line_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/knot/multi-line.ink.json").unwrap(); + common::get_json_string("tests/data/knot/multi-line.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -37,7 +37,7 @@ fn multi_line_test() -> Result<(), StoryError> { #[test] fn strip_empty_lines_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/knot/strip-empty-lines.ink.json").unwrap(); + common::get_json_string("tests/data/knot/strip-empty-lines.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -54,7 +54,7 @@ fn strip_empty_lines_test() -> Result<(), StoryError> { #[test] fn param_strings_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/knot/param-strings.ink.json").unwrap(); + common::get_json_string("tests/data/knot/param-strings.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -73,7 +73,7 @@ fn param_strings_test() -> Result<(), StoryError> { #[test] fn param_ints_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/knot/param-ints.ink.json").unwrap(); + common::get_json_string("tests/data/knot/param-ints.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -92,7 +92,7 @@ fn param_ints_test() -> Result<(), StoryError> { #[test] fn param_floats_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/knot/param-floats.ink.json").unwrap(); + common::get_json_string("tests/data/knot/param-floats.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -111,7 +111,7 @@ fn param_floats_test() -> Result<(), StoryError> { #[test] fn param_vars_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/knot/param-vars.ink.json").unwrap(); + common::get_json_string("tests/data/knot/param-vars.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -130,7 +130,7 @@ fn param_vars_test() -> Result<(), StoryError> { #[test] fn param_multi_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/knot/param-multi.ink.json").unwrap(); + common::get_json_string("tests/data/knot/param-multi.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -149,7 +149,7 @@ fn param_multi_test() -> Result<(), StoryError> { #[test] fn param_recurse_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/knot/param-recurse.ink.json").unwrap(); + common::get_json_string("tests/data/knot/param-recurse.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); diff --git a/tests/list_test.rs b/lib/tests/list_test.rs similarity index 74% rename from tests/list_test.rs rename to lib/tests/list_test.rs index 1c8f1c6..caeb984 100644 --- a/tests/list_test.rs +++ b/lib/tests/list_test.rs @@ -1,11 +1,11 @@ -use bladeink::{story::Story, story_error::StoryError}; +use bink::{story::Story, story_error::StoryError}; mod common; #[test] fn list_basic_operations_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/lists/basic-operations.ink.json").unwrap(); + common::get_json_string("tests/data/lists/basic-operations.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); assert_eq!("b, d\na, b, c, e\nb, c\nfalse\ntrue\ntrue\n", &story.continue_maximally()?); @@ -16,7 +16,7 @@ fn list_basic_operations_test() -> Result<(), StoryError> { #[test] fn list_mixed_items_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/lists/list-mixed-items.ink.json").unwrap(); + common::get_json_string("tests/data/lists/list-mixed-items.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); assert_eq!("a, y, c\n", &story.continue_maximally()?); @@ -27,7 +27,7 @@ fn list_mixed_items_test() -> Result<(), StoryError> { #[test] fn more_list_operations_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/lists/more-list-operations.ink.json").unwrap(); + common::get_json_string("tests/data/lists/more-list-operations.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); assert_eq!("1\nl\nn\nl, m\nn\n", &story.continue_maximally()?); @@ -38,7 +38,7 @@ fn more_list_operations_test() -> Result<(), StoryError> { #[test] fn empty_list_origin_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/lists/empty-list-origin.ink.json").unwrap(); + common::get_json_string("tests/data/lists/empty-list-origin.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); assert_eq!("a, b\n", &story.continue_maximally()?); @@ -49,7 +49,7 @@ fn empty_list_origin_test() -> Result<(), StoryError> { #[test] fn list_save_load_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/lists/list-save-load.ink.json").unwrap(); + common::get_json_string("tests/data/lists/list-save-load.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); assert_eq!("a, x, c\n", &story.continue_maximally()?); @@ -70,7 +70,7 @@ fn list_save_load_test() -> Result<(), StoryError> { #[test] fn empty_list_origin_after_assinment_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/lists/empty-list-origin-after-assignment.ink.json").unwrap(); + common::get_json_string("tests/data/lists/empty-list-origin-after-assignment.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); assert_eq!("a, b, c\n", &story.continue_maximally()?); @@ -81,7 +81,7 @@ fn empty_list_origin_after_assinment_test() -> Result<(), StoryError> { #[test] fn list_range_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/lists/list-range.ink.json").unwrap(); + common::get_json_string("tests/data/lists/list-range.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); assert_eq!("Pound, Pizza, Euro, Pasta, Dollar, Curry, Paella\nEuro, Pasta, Dollar, Curry\nTwo, Three, Four, Five, Six\nPizza, Pasta\n", &story.continue_maximally()?); @@ -92,7 +92,7 @@ fn list_range_test() -> Result<(), StoryError> { #[test] fn list_bug_adding_element_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/lists/bug-adding-element.ink.json").unwrap(); + common::get_json_string("tests/data/lists/bug-adding-element.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); assert_eq!("", &story.continue_maximally()?); @@ -109,7 +109,7 @@ fn list_bug_adding_element_test() -> Result<(), StoryError> { #[test] fn more_list_operations2_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/lists/more-list-operations2.ink.json").unwrap(); + common::get_json_string("tests/data/lists/more-list-operations2.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); assert_eq!("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:c2\nlistinc:b1\n", &story.continue_maximally()?); diff --git a/tests/misc_test.rs b/lib/tests/misc_test.rs similarity index 77% rename from tests/misc_test.rs rename to lib/tests/misc_test.rs index f4158d0..3dcd33e 100644 --- a/tests/misc_test.rs +++ b/lib/tests/misc_test.rs @@ -1,11 +1,11 @@ -use bladeink::{story::Story, value_type::ValueType, story_error::StoryError}; +use bink::{story::Story, value_type::ValueType, story_error::StoryError}; mod common; #[test] fn operations_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/misc/operations.ink.json").unwrap(); + common::get_json_string("tests/data/misc/operations.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); assert_eq!("neg:-3\nmod:1\npow:27\nfloor:3\nceiling:4\nint:3\nfloat:1\n", &story.continue_maximally()?); @@ -16,7 +16,7 @@ fn operations_test() -> Result<(), StoryError> { #[test] fn read_counts_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/misc/read-counts.ink.json").unwrap(); + common::get_json_string("tests/data/misc/read-counts.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); assert_eq!("Count start: 0 0 0\n1\n2\n3\nCount end: 3 3 3\n", &story.continue_maximally()?); @@ -27,7 +27,7 @@ fn read_counts_test() -> Result<(), StoryError> { #[test] fn turns_since_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/misc/turns-since.ink.json").unwrap(); + common::get_json_string("tests/data/misc/turns-since.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); assert_eq!("0\n0\n", &story.continue_maximally()?); @@ -43,7 +43,7 @@ fn turns_since_test() -> Result<(), StoryError> { #[test] fn issue15_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/misc/issue15.ink.json").unwrap(); + common::get_json_string("tests/data/misc/issue15.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); assert_eq!("This is a test\n", story.cont()?); diff --git a/tests/multi_flow_test.rs b/lib/tests/multi_flow_test.rs similarity index 91% rename from tests/multi_flow_test.rs rename to lib/tests/multi_flow_test.rs index 95dda3d..25adef5 100644 --- a/tests/multi_flow_test.rs +++ b/lib/tests/multi_flow_test.rs @@ -1,11 +1,11 @@ -use bladeink::{story::Story, story_error::StoryError}; +use bink::{story::Story, story_error::StoryError}; mod common; #[test] fn basics_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/runtime/multiflow-basics.ink.json").unwrap(); + common::get_json_string("tests/data/runtime/multiflow-basics.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); story.switch_flow("First")?; @@ -28,7 +28,7 @@ fn basics_test() -> Result<(), StoryError> { #[test] fn multiflow_save_load_threads() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/runtime/multiflow-saveloadthreads.ink.json").unwrap(); + common::get_json_string("tests/data/runtime/multiflow-saveloadthreads.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); // Default flow diff --git a/tests/runtime_test.rs b/lib/tests/runtime_test.rs similarity index 87% rename from tests/runtime_test.rs rename to lib/tests/runtime_test.rs index 6ee927e..bcebbe5 100644 --- a/tests/runtime_test.rs +++ b/lib/tests/runtime_test.rs @@ -1,4 +1,4 @@ -use bladeink::{story::Story, value_type::{ValueType, StringValue}, story_error::StoryError}; +use bink::{story::Story, value_type::{ValueType, StringValue}, story_error::StoryError}; mod common; @@ -7,7 +7,7 @@ mod common; #[test] fn set_and_get_variable_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/runtime/set-get-variables.ink.json").unwrap(); + common::get_json_string("tests/data/runtime/set-get-variables.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -33,7 +33,7 @@ fn set_and_get_variable_test() -> Result<(), StoryError> { #[test] fn set_non_existant_variable_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/runtime/set-get-variables.ink.json").unwrap(); + common::get_json_string("tests/data/runtime/set-get-variables.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -62,7 +62,7 @@ fn set_non_existant_variable_test() -> Result<(), StoryError> { #[test] fn jump_knot_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/runtime/jump-knot.ink.json").unwrap(); + common::get_json_string("tests/data/runtime/jump-knot.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -91,7 +91,7 @@ fn jump_knot_test() -> Result<(), StoryError> { #[test] fn jump_stitch_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/runtime/jump-stitch.ink.json").unwrap(); + common::get_json_string("tests/data/runtime/jump-stitch.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -120,7 +120,7 @@ fn jump_stitch_test() -> Result<(), StoryError> { #[test] fn read_visit_counts_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/runtime/read-visit-counts.ink.json").unwrap(); + common::get_json_string("tests/data/runtime/read-visit-counts.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -134,7 +134,7 @@ fn read_visit_counts_test() -> Result<(), StoryError> { #[test] fn load_save_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/runtime/load-save.ink.json").unwrap(); + common::get_json_string("tests/data/runtime/load-save.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); diff --git a/tests/stitch_test.rs b/lib/tests/stitch_test.rs similarity index 80% rename from tests/stitch_test.rs rename to lib/tests/stitch_test.rs index d25e7f7..92244f6 100644 --- a/tests/stitch_test.rs +++ b/lib/tests/stitch_test.rs @@ -1,11 +1,11 @@ -use bladeink::{story::Story, story_error::StoryError}; +use bink::{story::Story, story_error::StoryError}; mod common; #[test] fn auto_stitch_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/stitch/auto-stitch.ink.json").unwrap(); + common::get_json_string("tests/data/stitch/auto-stitch.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -20,7 +20,7 @@ fn auto_stitch_test() -> Result<(), StoryError> { #[test] fn auto_stitch2_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/stitch/auto-stitch.ink.json").unwrap(); + common::get_json_string("tests/data/stitch/auto-stitch.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -38,7 +38,7 @@ fn auto_stitch2_test() -> Result<(), StoryError> { #[test] fn manual_stitch_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/stitch/manual-stitch.ink.json").unwrap(); + common::get_json_string("tests/data/stitch/manual-stitch.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -60,7 +60,7 @@ fn manual_stitch_test() -> Result<(), StoryError> { #[test] fn manual_stitch2_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/stitch/manual-stitch.ink.json").unwrap(); + common::get_json_string("tests/data/stitch/manual-stitch.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); diff --git a/tests/tag_test.rs b/lib/tests/tag_test.rs similarity index 87% rename from tests/tag_test.rs rename to lib/tests/tag_test.rs index 0adadfd..73c6455 100644 --- a/tests/tag_test.rs +++ b/lib/tests/tag_test.rs @@ -1,11 +1,11 @@ -use bladeink::{story::Story, story_error::StoryError}; +use bink::{story::Story, story_error::StoryError}; mod common; #[test] fn tags_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/tags/tags.ink.json").unwrap(); + common::get_json_string("tests/data/tags/tags.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let global_tags = story.get_global_tags()?; @@ -44,7 +44,7 @@ fn tags_test() -> Result<(), StoryError> { #[test] fn tags_in_seq_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/tags/tagsInSeq.ink.json").unwrap(); + common::get_json_string("tests/data/tags/tagsInSeq.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); assert_eq!("A red sequence.\n", story.cont()?); @@ -63,7 +63,7 @@ fn tags_in_seq_test() -> Result<(), StoryError> { #[test] fn tags_in_choice_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/tags/tagsInChoice.ink.json").unwrap(); + common::get_json_string("tests/data/tags/tagsInChoice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); story.cont()?; @@ -89,7 +89,7 @@ fn tags_in_choice_test() -> Result<(), StoryError> { #[test] fn tags_dynamic_content_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/tags/tagsDynamicContent.ink.json").unwrap(); + common::get_json_string("tests/data/tags/tagsDynamicContent.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); assert_eq!("tag\n", story.cont()?); diff --git a/tests/thread_test.rs b/lib/tests/thread_test.rs similarity index 88% rename from tests/thread_test.rs rename to lib/tests/thread_test.rs index 31ce4e1..724ab6b 100644 --- a/tests/thread_test.rs +++ b/lib/tests/thread_test.rs @@ -1,11 +1,11 @@ -use bladeink::{story::Story, story_error::StoryError}; +use bink::{story::Story, story_error::StoryError}; mod common; #[test] fn thread_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/threads/thread-bug.ink.json").unwrap(); + common::get_json_string("tests/data/threads/thread-bug.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); @@ -30,7 +30,7 @@ fn thread_test() -> Result<(), StoryError> { #[test] fn thread_test_bug() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/threads/thread-bug.ink.json").unwrap(); + common::get_json_string("tests/data/threads/thread-bug.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); println!("{}", story.build_string_of_hierarchy()); diff --git a/tests/tunnel_test.rs b/lib/tests/tunnel_test.rs similarity index 60% rename from tests/tunnel_test.rs rename to lib/tests/tunnel_test.rs index 5afa513..b271487 100644 --- a/tests/tunnel_test.rs +++ b/lib/tests/tunnel_test.rs @@ -1,11 +1,11 @@ -use bladeink::{story::Story, story_error::StoryError}; +use bink::{story::Story, story_error::StoryError}; mod common; #[test] fn tunnel_onwards_divert_override_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/tunnels/tunnel-onwards-divert-override.ink.json").unwrap(); + common::get_json_string("tests/data/tunnels/tunnel-onwards-divert-override.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); assert_eq!("This is A\nNow in B.\n", story.continue_maximally()?); diff --git a/tests/variable_test.rs b/lib/tests/variable_test.rs similarity index 78% rename from tests/variable_test.rs rename to lib/tests/variable_test.rs index 7f4b1bc..b512736 100644 --- a/tests/variable_test.rs +++ b/lib/tests/variable_test.rs @@ -1,11 +1,11 @@ -use bladeink::{story::Story, story_error::StoryError}; +use bink::{story::Story, story_error::StoryError}; mod common; #[test] fn variable_declaration_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/variable/variable-declaration.ink.json").unwrap(); + common::get_json_string("tests/data/variable/variable-declaration.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -19,7 +19,7 @@ fn variable_declaration_test() -> Result<(), StoryError> { #[test] fn var_calc_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/variable/varcalc.ink.json").unwrap(); + common::get_json_string("tests/data/variable/varcalc.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -33,7 +33,7 @@ fn var_calc_test() -> Result<(), StoryError> { #[test] fn var_string_ink_bug_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/variable/varstringinc.ink.json").unwrap(); + common::get_json_string("tests/data/variable/varstringinc.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -51,7 +51,7 @@ fn var_string_ink_bug_test() -> Result<(), StoryError> { #[test] fn var_divert_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/variable/var-divert.ink.json").unwrap(); + common::get_json_string("tests/data/variable/var-divert.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); diff --git a/tests/variable_text_test.rs b/lib/tests/variable_text_test.rs similarity index 90% rename from tests/variable_text_test.rs rename to lib/tests/variable_text_test.rs index f571571..c1709b4 100644 --- a/tests/variable_text_test.rs +++ b/lib/tests/variable_text_test.rs @@ -1,11 +1,11 @@ -use bladeink::{story::Story, story_error::StoryError}; +use bink::{story::Story, story_error::StoryError}; mod common; #[test] fn sequence_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/variabletext/sequence.ink.json").unwrap(); + common::get_json_string("tests/data/variabletext/sequence.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -48,7 +48,7 @@ fn sequence_test() -> Result<(), StoryError> { #[test] fn cycle_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/variabletext/cycle.ink.json").unwrap(); + common::get_json_string("tests/data/variabletext/cycle.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -91,7 +91,7 @@ fn cycle_test() -> Result<(), StoryError> { #[test] fn once_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/variabletext/once.ink.json").unwrap(); + common::get_json_string("tests/data/variabletext/once.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -134,7 +134,7 @@ fn once_test() -> Result<(), StoryError> { #[test] fn empty_elements_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/variabletext/empty-elements.ink.json").unwrap(); + common::get_json_string("tests/data/variabletext/empty-elements.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); @@ -163,7 +163,7 @@ fn empty_elements_test() -> Result<(), StoryError> { #[test] fn list_in_choice_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("examples/inkfiles/variabletext/list-in-choice.ink.json").unwrap(); + common::get_json_string("tests/data/variabletext/list-in-choice.ink.json").unwrap(); let mut story = Story::new(&json_string).unwrap(); let mut text: Vec = Vec::new(); From af0d0196ddfb64c987034bc02eadab619445dda2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Wed, 4 Oct 2023 11:57:53 +0000 Subject: [PATCH 58/91] Working version of the cli app. --- .vscode/settings.json | 3 +- cli-player/Cargo.toml | 12 +- cli-player/src/cli_player.rs | 5 - cli-player/src/main.rs | 146 ++ cli-player/tests/basic_tests.rs | 40 + cli-player/tests/data/TheIntercept.ink | 1686 +++++++++++++++++++ cli-player/tests/data/TheIntercept.ink.json | 1 + cli-player/tests/data/test1.ink | 12 + cli-player/tests/data/test1.ink.json | 1 + cli-player/tests/test_the_intercept.rs | 28 + lib/src/lib.rs | 2 +- lib/src/story_state.rs | 1 - lib/tests/choice_test.rs | 2 +- 13 files changed, 1928 insertions(+), 11 deletions(-) delete mode 100644 cli-player/src/cli_player.rs create mode 100644 cli-player/src/main.rs create mode 100644 cli-player/tests/basic_tests.rs create mode 100644 cli-player/tests/data/TheIntercept.ink create mode 100644 cli-player/tests/data/TheIntercept.ink.json create mode 100644 cli-player/tests/data/test1.ink create mode 100644 cli-player/tests/data/test1.ink.json create mode 100644 cli-player/tests/test_the_intercept.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index 352a626..daf1107 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "rust-analyzer.linkedProjects": [ - "./Cargo.toml" + "./Cargo.toml", + "./cli-player/Cargo.toml" ] } \ No newline at end of file diff --git a/cli-player/Cargo.toml b/cli-player/Cargo.toml index 8f1bdf3..7273a3e 100644 --- a/cli-player/Cargo.toml +++ b/cli-player/Cargo.toml @@ -1,13 +1,21 @@ [package] name = "binkplayer" version = "0.1.0" +description = """ +Console player for compiled .json Ink story files. +""" +authors = ["Rafael Garcia "] edition = "2021" [[bin]] name = "binkplayer" -path = "src/cli_player.rs" +path = "src/main.rs" [dependencies] +anyhow = "1.0.75" bink = { "version" = "0.1.0", path = "../lib" } +clap = { "version" = "4.4.6", features = ["derive"] } - +[dev-dependencies] +assert_cmd = "2.0.12" +predicates = "3.0.4" diff --git a/cli-player/src/cli_player.rs b/cli-player/src/cli_player.rs deleted file mode 100644 index 85aad77..0000000 --- a/cli-player/src/cli_player.rs +++ /dev/null @@ -1,5 +0,0 @@ -fn main() { - println!("Hello, world!"); -} - - diff --git a/cli-player/src/main.rs b/cli-player/src/main.rs new file mode 100644 index 0000000..d2a8e5c --- /dev/null +++ b/cli-player/src/main.rs @@ -0,0 +1,146 @@ +use std::{path::Path, fs, error::Error, rc::Rc, io}; +use std::io::Write; + +use anyhow::Context; +use bink::{story::Story, choice::Choice}; +use clap::Parser; + + +#[derive(Parser)] +struct Args { + pub json_filename: String, +} + +enum Command { + Choose(usize), + Exit(), + Help(), + Load(String), + Save(String), +} + +fn main() -> Result<(), Box> { + let args = Args::parse(); + let json_string = get_json_string(&args.json_filename)?; + + // REMOVE BOM if exits + let json_string_without_bom = json_string.strip_prefix("\u{feff}").unwrap_or(&json_string); + + let mut story = Story::new(json_string_without_bom)?; + let mut end = false; + + while !end { + while story.can_continue() { + let line = story.cont()?; + let trimmed = line.trim(); + + println!("{}", trimmed); + } + + let choices = story.get_current_choices(); + if !choices.is_empty() { + let command = read_input(&choices)?; + end = process_command(command, &mut story)?; + } else { + end = true; + } + } + + Ok(()) +} + + +// Returns true if the program has to stop +fn process_command(command: Command, story: &mut Story) -> Result> { + match command { + Command::Choose(c) => story.choose_choice_index(c), + Command::Exit() => return Ok(true), + Command::Load(filename) => { + let saved_string = get_json_string(&filename)?; + story.get_state_mut().load_json(&saved_string)?; + println!("Ok.") + }, + Command::Save(filename) => { + let json_string = story.get_state().to_json()?; + save_json(&filename, &json_string)?; + }, + Command::Help() => println!("Commands:\n\tload \n\tsave \n\tquit\n\t"), + } + + Ok(false) +} + +fn print_choices(choices: &[Rc]) { + for (i, c) in choices.iter().enumerate() { + println!("{}. {}", i + 1, c.text); + } +} + +fn read_input(choices: &Vec>) -> Result> { + let mut line = String::new(); + + loop { + println!(); + print_choices(choices); + println!(); + print!("?>"); + io::stdout().flush()?; + + let _b1 = std::io::stdin().read_line(&mut line)?; + + let trimmed = line.trim(); + + if trimmed.is_empty() { + continue; + } + + if trimmed.len() == 1 { + match trimmed.parse::() { + Ok(v) => { + if v < 1 || v > choices.len() as i32 { + print_error("option out of range"); + } else { + return Ok(Command::Choose((v - 1) as usize)); + } + }, + Err(_) => print_error("unrecognized option or command"), + } + } + + let words:Vec<&str> = trimmed.split_whitespace().collect(); + + match words[0].trim().to_lowercase().as_str() { + "exit" | "quit" => return Ok(Command::Exit()), + "help" => return Ok(Command::Help()), + "load" => { + if words.len() == 2 { + return Ok(Command::Load(words[1].trim().to_string())) + } + + print_error("incorrect filename"); + }, + "save" => {return Ok(Command::Save(words[1].trim().to_string()))}, + _ => print_error("unrecognized option or command"), + } + } +} + +fn print_error(error: &str) { + println!("<{error}>"); +} + +fn get_json_string(filename: &str) -> Result> { + let path = Path::new(filename); + let json = fs::read_to_string(path).with_context(|| format!("could not read file `{}`", path.to_string_lossy()))?; + + Ok(json) +} + +fn save_json(filename: &str, content: &str) -> Result<(), Box> { + let path = Path::new(filename); + fs::write(path, content).with_context(|| format!("could not write file `{}`", path.to_string_lossy()))?; + + Ok(()) +} + + diff --git a/cli-player/tests/basic_tests.rs b/cli-player/tests/basic_tests.rs new file mode 100644 index 0000000..4b199d9 --- /dev/null +++ b/cli-player/tests/basic_tests.rs @@ -0,0 +1,40 @@ +use assert_cmd::prelude::*; +use predicates::prelude::predicate; // Add methods on commands +use std::process::{Command, Stdio}; +use std::io::Write; + +#[test] +fn basic_story_test() -> Result<(), Box> { + let mut cmd = Command::cargo_bin("binkplayer")?; + + cmd.arg("tests/data/test1.ink.json"); + cmd.stdin(Stdio::piped()); + cmd.stdout(Stdio::piped()); + + let mut child = cmd.spawn().unwrap(); + let mut stdin = child.stdin.take().unwrap(); + + stdin.write_all(b"1\n").unwrap(); + + let output = child.wait_with_output()?; + let output_str = String::from_utf8_lossy(&output.stdout); + + assert!(output.status.success()); + assert!(output_str.starts_with("Test conditional choices")); + assert!(output_str.contains("1. one")); + assert!(output_str.ends_with("one\n")); + + Ok(()) +} + +#[test] +fn story_not_found_test() -> Result<(), Box> { + let mut cmd = Command::cargo_bin("binkplayer")?; + + cmd.arg("nonexistent.ink.json"); + cmd.assert() + .failure() + .stderr(predicate::str::contains("could not read file")); + + Ok(()) +} \ No newline at end of file diff --git a/cli-player/tests/data/TheIntercept.ink b/cli-player/tests/data/TheIntercept.ink new file mode 100644 index 0000000..7e710b3 --- /dev/null +++ b/cli-player/tests/data/TheIntercept.ink @@ -0,0 +1,1686 @@ +// Character variables. We track just two, using a +/- scale +VAR forceful = 0 +VAR evasive = 0 + + +// Inventory Items +VAR teacup = false +VAR gotcomponent = false + + +// Story states: these can be done using read counts of knots; or functions that collect up more complex logic; or variables +VAR drugged = false +VAR hooper_mentioned = false + +VAR losttemper = false +VAR admitblackmail = false + +// what kind of clue did we pass to Hooper? +CONST NONE = 0 +CONST STRAIGHT = 1 +CONST CHESS = 2 +CONST CROSSWORD = 3 +VAR hooperClueType = NONE + +VAR hooperConfessed = false + +CONST SHOE = 1 +CONST BUCKET = 2 +VAR smashingWindowItem = NONE + +VAR notraitor = false +VAR revealedhooperasculprit = false +VAR smashedglass = false +VAR muddyshoes = false + +VAR framedhooper = false + +// What did you do with the component? +VAR putcomponentintent = false +VAR throwncomponentaway = false +VAR piecereturned = false +VAR longgrasshooperframe = false + + +// DEBUG mode adds a few shortcuts - remember to set to false in release! +VAR DEBUG = false +{DEBUG: + IN DEBUG MODE! + * [Beginning...] -> start + * [Framing Hooper...] -> claim_hooper_took_component + * [In with Hooper...] -> inside_hoopers_hut +- else: + // First diversion: where do we begin? + -> start +} + + /*-------------------------------------------------------------------------------- + Wrap up character movement using functions, in case we want to develop this logic in future +--------------------------------------------------------------------------------*/ + + + === function lower(ref x) + ~ x = x - 1 + + === function raise(ref x) + ~ x = x + 1 + +/*-------------------------------------------------------------------------------- + + Start the story! + +--------------------------------------------------------------------------------*/ + +=== start === + +// Intro + - They are keeping me waiting. + * Hut 14[]. The door was locked after I sat down. + I don't even have a pen to do any work. There's a copy of the morning's intercept in my pocket, but staring at the jumbled letters will only drive me mad. + I am not a machine, whatever they say about me. + + - (opts) + {|I rattle my fingers on the field table.|} + * (think) [Think] + They suspect me to be a traitor. They think I stole the component from the calculating machine. They will be searching my bunk and cases. + When they don't find it, {plan:then} they'll come back and demand I talk. + -> opts + * (plan) [Plan] + {not think:What I am is|I am} a problem—solver. Good with figures, quick with crosswords, excellent at chess. + But in this scenario — in this trap — what is the winning play? + * * (cooperate) [Co—operate] + I must co—operate. My credibility is my main asset. To contradict myself, or another source, would be fatal. + I must simply hope they do not ask the questions I do not want to answer. + ~ lower(forceful) + * * [Dissemble] + Misinformation, then. Just as the war in Europe is one of plans and interceptions, not planes and bombs. + My best hope is a story they prefer to the truth. + ~ raise(forceful) + * * (delay) [Divert] + Avoidance and delay. The military machine never fights on a single front. If I move slowly enough, things will resolve themselves some other way, my reputation intact. + ~ raise(evasive) + * [Wait] + - -> waited + += waited + - Half an hour goes by before Commander Harris returns. He closes the door behind him quickly, as though afraid a loose word might slip inside. + "Well, then," he begins, awkwardly. This is an unseemly situation. + * "Commander." + He nods. <> + * (tellme) {not start.delay} "Tell me what this is about." + He shakes his head. + "Now, don't let's pretend." + * [Wait] + I say nothing. + - He has brought two cups of tea in metal mugs: he sets them down on the tabletop between us. + * {tellme} [Deny] "I'm not pretending anything." + {cooperate:I'm lying already, despite my good intentions.} + Harris looks disapproving. -> pushes_cup + * (took) [Take one] + ~ teacup = true + I take a mug and warm my hands. It's <> + * (what2) {not tellme} "What's going on?" + "You know already." + -> pushes_cup + * [Wait] + I wait for him to speak. + - - (pushes_cup) He pushes one mug halfway towards me: <> + - a small gesture of friendship. + Enough to give me hope? + * (lift_up_cup) {not teacup} [Take it] + I {took:lift the mug|take the mug,} and blow away the steam. It is too hot to drink. + Harris picks his own up and just holds it. + ~ teacup = true + ~ lower(forceful) + * {not teacup} [Don't take it] + Just a cup of insipid canteen tea. I leave it where it is. + ~ raise(forceful) + + * {teacup} [Drink] + I raise the cup to my mouth but it's too hot to drink. + + * {teacup} [Wait] + I say nothing as -> lift_up_cup + +- "Quite a difficult situation," {lift_up_cup:he|Harris} begins{forceful <= 0:, sternly}. I've seen him adopt this stiff tone of voice before, but only when talking to the brass. "I'm sure you agree." + * [Agree] + "Awkward," I reply + * (disagree) [Disagree] + "I don't see why," I reply + ~ raise(forceful) + ~ raise(evasive) + * [Lie] -> disagree + * [Evade] + "I'm sure you've handled worse," I reply casually + ~ raise(evasive) + - { teacup: + ~ drugged = true + <>, sipping at my tea as though we were old friends + } + <>. + + - + * [Watch him] + His face is telling me nothing. I've seen Harris broad and full of laughter. Today he is tight, as much part of the military machine as the device in Hut 5. + + * [Wait] + I wait to see how he'll respond. + + * {not disagree} [Smile] + I try a weak smile. It is not returned. + ~ lower(forceful) + +// Why you're here + - + "We need that component," he says. + + - //"There's no alternative, of course," he continues. + {not missing_reel: + -> missing_reel -> harris_demands_component + } + - + * [Yes] + "Of course I do," I answer. + * (no) [No] + "No I don't. And I've got work to do..." + "Work that will be rather difficult for you to do, don't you think?" Harris interrupts. + + * [Evade] + -> here_at_bletchley_diversion + * [Lie] + -> no + - -> missing_reel -> harris_demands_component + +=== missing_reel === + * [The stolen component...] + * [Shrug] + I shrug. + ->-> + - The reel went missing from the Bombe this afternoon. The four of us were in the Hut, working on the latest German intercept. The results were garbage. It was Russell who found the gap in the plugboard. + - Any of us could have taken it; and no one else would have known its worth. + + * {forceful <= 0 }[Panic] They will pin it on me. They need a scapegoat so that the work can continue. I'm a likely target. Weaker than the rest. + ~ lower(forceful) + * [Calculate] My odds, then, are one in four. Not bad; although the stakes themselves are higher than I would like. + ~ raise(evasive) + * {evasive >= 0} [Deny] But this is still a mere formality. The work will not stop. A replacement component will be made and we will all be put back to work. We are too valuable to shoot. + ~ raise(forceful) + - ->-> + + +=== here_at_bletchley_diversion + "Here at Bletchley? Of course." + ~ raise(evasive) + ~ lower(forceful) + "Here, now," Harris corrects. "We are not talking to everyone. I can imagine you might feel pretty sore about that. I can imagine you feeling picked on. { forceful < 0:You're a sensitive soul.}" + + * (fine) "I'm fine[."]," I reply. "This is all some misunderstanding and the quicker we have it cleared up the better." + ~ lower(forceful) + "I couldn't agree more." And then he comes right out with it, with an accusation. + + * {forceful < 0} "What do you mean by that?" + + * (sore) { forceful >= 0 } "Damn right[."] I'm sore. Was it one of the others who put you up to this? Was it Hooper? He's always been jealous of me. He's..." + ~ raise(forceful) + ~ hooper_mentioned = true + The Commander moustache bristles as he purses his lips. "Has he now? Of your achievements, do you think?" + It's difficult not to shake the sense that he's { evasive > 1 :mocking|simply humouring} me. + "Or of your brain? Or something else?" + * * "Of my genius.["] Hooper simply can't stand that I'm cleverer than he is. We work so closely together, cooped up in that Hut all day. It drives him to distraction. To worse." + "You're suggesting Hooper would sabotage this country's future simply to spite you?" Harris chooses his words like the military man he is, each lining up to create a ring around me. + * * * [Yes] + "{ forceful > 0:He's petty enough, certainly|I wouldn't put it past him}. He's a creep." { teacup : I set the teacup down.|I wipe a hand across my forehead.} + ~ raise(forceful) + ~ teacup = false + * * * [No] + "No, { forceful >0:of course not|I suppose not}." { teacup :I put the teacup back down on the table|I push the teacup around on its base}. + ~ lower(forceful) + ~ teacup = false + * * * [Evade] + "I don't know what I'm suggesting. I don't understand what's going on." + ~ raise(evasive) + "But of course you do." Harris narrows his eyes. + -> done + + - - - (suggest_its_a_lie) "All I can say is, ever since I arrived here, he's been looking to ways to bring me down a peg. I wouldn't be surprised if he set this whole affair up just to have me court—martialled." + "We don't court—martial civilians," Harris replies. "Traitors are simply hung at her Majesty's pleasure." + * * * "Quite right[."]," I answer smartly. + * * * (iamnotraitor) "I'm no traitor[."]," I answer{forceful > 0 :smartly|, voice quivering. "For God's sake!"} + * * * [Lie] -> iamnotraitor + - - - He stares back at me. + + * * "Of my standing.["] My reputation." { forceful > 0:I'm aware of how arrogant I must sound but I plough on all the same.|I don't like to talk of myself like this, but I carry on all the same.} "Hooper simply can't bear knowing that, once all this is over, I'll be the one receiving the knighthood and he..." + "No—one will be getting a knighthood if the Germans make landfall," Harris answers sharply. He casts a quick eye to the door of the Hut to check the latch is still down, then continues in more of a murmur: "Not you and not Hooper. Now answer me." + For the first time since the door closed, I wonder what the threat might be if I do not. + + * * [Evade] + ~ teacup = false + ~ raise(forceful) + "How should I know?" I reply, defensively. { teacup :I set the teacup back on the table.} -> suggest_its_a_lie + + + * [Be honest] -> sore + * [Lie] -> fine + +- (done) -> harris_demands_component + + +=== harris_demands_component === + "{here_at_bletchley_diversion:Please|So}. Do you have it?" Harris is {forceful > 3:sweating slightly|wasting no time}: Bletchley is his watch. "Do you know where it is?" + * [Yes] + "I do." + -> admitted_to_something + * (nope) [No] "I have no idea." + -> silence + * [Lie] -> nope + * [Evade] + "The component?" + ~ raise(evasive) + ~ lower(forceful) + "Don't play stupid," he replies. "{ not missing_reel:The component that went missing this afternoon. }Where is it?" + + - { not missing_reel: + -> missing_reel -> + } + * [Co-operate] "I know where it is." + -> admitted_to_something + * (nothing) [Delay] "I know nothing about it." My voice shakes{ forceful > 0: with anger|; I'm unaccustomed to facing off against men with holstered guns}. + + * [Lie] -> nothing + * [Evade] + + "I don't know what gives you the right to pick on me. { forceful > 0:I demand a lawyer.|I want a lawyer.}" + + "This is time of war," Harris answers. "And by God, if I have to shoot you to recover the component, I will. Understand?" He points at the mug,-> drinkit + + - (silence) There's an icy silence. { forceful > 2:I've cracked him a little.|{ evasive > 2:He's tiring of my evasiveness.}} + + // Drink tea and talk + - (drinkit) "Now drink your tea and talk." + * { teacup } [Drink] -> drinkfromcup + * { teacup } [Put the cup down] + I set the cup carefully down on the table once more. + ~ teacup = false + ~ raise(forceful) + -> whatsinit + + * { not teacup } [Take the cup] + - - (drinkfromcup) I lift the cup { teacup :to my lips }and sip. He waits for me to swallow before speaking again. + ~ drugged = true + ~ teacup = true + * { not teacup } [Don't take it] + I leave the cup where it is. + ~ raise(forceful) + - - (whatsinit) "Why?" I ask coldly. "What's in it?" + + - "Lapsang Souchong," he {drinkfromcup:remarks|replies}, placing his own cup back on the table untouched. "Such a curious flavour. It might almost not be tea at all. You might say it hides a multitude of sins. As do you. Isn't that right?" + + * (suppose_i_have) [Agree] + // Regrets + "I suppose so," I reply. "I've done things I shouldn't have done." + ~ lower(forceful) + -> harris_presses_for_details + + * (nothing_ashamed_of) { not drugged } [Disagree] + "I've done nothing that I'm ashamed of." + -> harris_asks_for_theory + + * (cant_talk_right) { drugged } [Disagree] + I open my mouth to disagree, but the words I want won't come. It is like Harris has taken a screwdriver to the sides of my jaw. + -> admitted_to_something.ive_done_things + + * {drugged} [Lie] -> cant_talk_right + * {not drugged} [Lie] -> nothing_ashamed_of + * { drugged } [Evade] -> cant_talk_right + + * { not drugged } [Evade] + "None of us are blameless, Harris. { forceful > 1:But you're not my priest and I'm not yours|But I've done nothing to deserve this treatment}. Now, please. Let me go. I'll help you find this damn component, of course I will." + // Who do you blame? + He appears to consider the offer. + -> harris_asks_for_theory + + + +=== harris_presses_for_details +// Open to Blackmail + "You mean you've left yourself open," Harris answers. "To pressure. Is that what you're saying?" + * [Yes] -> admit_open_to_pressure + * { not drugged } [No] + "I'm not saying anything of the sort," I snap back. "What is this, Harris? You're accusing me of treachery but I don't see a shred of evidence for it! Why don't you put your cards on the table?" + ~ raise(forceful) + + + * {drugged} [No] + I shake my head violently, to say no, that's not it, but whatever is wrong with tongue is wrong with neck too. I look across at the table at Harris' face and realise with a start how sympathetic he is. Such a kind, generous man. How can I hold anything back from him? + ~ lower(forceful) + I take another mouthful of the bitter, strange—tasting tea before answering. + -> admit_open_to_pressure + + + * { not drugged } [Evade] + "You're the one applying pressure here," I answer { forceful > 1:smartly|somewhat miserably}. "I'm just waiting until you tell me what is really going on." + ~ raise(evasive) + * { drugged } [Evade] + "We're all under pressure here." + He looks at me with pity. -> harris_has_seen_it_before + + - "It's simple enough," Harris says. -> harris_has_seen_it_before + += admit_open_to_pressure + "That's it," I reply. "There are some things... which a man shouldn't do." + ~ admitblackmail = true + Harris doesn't stiffen. Doesn't lean away, as though my condition might be infectious. I had thought they trained them in the army to shoot my kind on sight. + He offers no sympathy either. He nods, once. His understanding of me is a mere turning cog in his calculations, with no meaning to it. + -> harris_has_seen_it_before + + +=== admitted_to_something + // Admitting Something + { not drugged : + Harris stares back at me. { evasive == 0:He cannot have expected it to be so easy to break me.} + - else: + Harris smiles with satisfaction, as if your willingness to talk was somehow his doing. + } + "I see." + There's a long pause, like the delay between feeding a line of cypher into the Bombe and waiting for its valves to warm up enough to begin processing. + "You want to explain that?" + * [Explain] + I pause a moment, trying to choose my words. To just come out and say it, after a lifetime of hiding... that is a circle I cannot square. + * * [Explain] -> ive_done_things + * * {drugged} [Say nothing] -> say_nothing + * * {not drugged} [Lie] -> claim_hooper_took_component + + * { not drugged } [Don't explain] + "There's nothing to explain," I reply stiffly. -> i_know_where + + * { not drugged } [Lie] -> claim_hooper_took_component + * { not drugged } [Evade] + "Explain what you should be doing, do you mean, rather than bullying me? Certainly." I fold my arms. -> i_know_where + + * (say_nothing) { drugged } [Say nothing] + I fold my arms, intended firmly to say nothing. But somehow, watching Harris' face, I cannot bring myself to do it. I want to confess. I want to tell him everything I can, to explain myself to him, to earn his forgiveness. The sensation is so strong my will is powerless in the face of it. + Something is wrong with me, I am sure of it. There is a strange, bitter flavour on my tongue. I taste it as words start to form. + -> ive_done_things + += i_know_where + "I know where your component is because it's obvious where your component is. That doesn't mean I took it, just because I can figure out a simple problem, any more than it means I'm a German spy because I can crack their codes." + -> harris_asks_for_theory + + += ive_done_things + "I've done things," I begin{harris_demands_component.cant_talk_right: helplessly}. "Things I didn't want to do. I tried not to. But in the end, it felt like cutting off my own arm to resist." + -> harris_presses_for_details + + + + +=== harris_asks_for_theory +"Tell me, then," he asks. "What's your theory? You're a smart fellow — as smart as they come around here, and that's saying something. What's your opinion on the missing component? Accident, perhaps? Or do you blame one of the other men? { hooper_mentioned :Hooper?}" + * [Blame no—one] + -> an_accident + * [Blame someone] -> claim_hooper_took_component + += an_accident + "An accident, naturally." I risk a smile. "That damned machine is made from spare parts and string. Even these Huts leak when it rains. It wouldn't take more than one fellow to trip over a cable to shake out a component. Have you tried looking under the thing?" + "Do you believe we haven't?" + In a sudden moment I understand that his reply is a threat. + "Now," he continues. "Are you sure there isn't anything you want to tell me?" + + * [Co-operate] + "All right." With a sigh, your defiance collapses. "If you're searched my things then I suppose you've found { evasive > 1: what you need|my letters. Haven't you? In fact, if you haven't, don't tell me}. + ~ admitblackmail = true + Harris nods once. + <> -> harris_has_seen_it_before + + * {evasive > 0} [Evade] "Only that you're being unreasonable, and behaving like a swine." + // Loses temper + "You imbecile," Harris replies, with sudden force. He is half out of his chair. "You know the situation as well as I do. Why the fencing? The Hun are poised like rats, ready to run all over this country. They'll destroy everything. You understand that, don't you? You're not so locked up inside your crossword puzzles that you don't see that, are you? This machine we have here — you men — you are the best and only hope this country has. God help her." + ~ losttemper = true + I sit back, startled by the force of his outburst. His carefully sculpted expression has curled to angry disgust. He really does hate me, I think. He'll have my blood for the taste of it. + * * [Placate] + "Now steady on," I reply, gesturing for him to be calm. + + * * [Mock] + "I can imagine how being surrounded by clever men is pretty threatening for you, Commander," I reply with a sneer. "They don't train you to think in the Armed Forces." + ~ raise(forceful) + + * * [Dismiss] + "Then I'll be going, on and getting on with my job of saving her, shall I?" I even rise half to my feet, before he slams the tabletop. + + - - "Talk," Harris demands. "Talk now. Tell me where you've hidden it or who you passed it to. Or God help me, I'll take your wretched pansy body to pieces looking for it." + -> harris_demands_you_speak + + + + +=== harris_has_seen_it_before + "I've seen it before. A young man like you — clever, removed. The kind that doesn't go to parties. Who takes himself too seriously. Who takes things too far." + He slides his thumb between two fingers. + "Now they own you." + + * [Agree] + "What could I do?" I'm shaking now. The night is cold and the heat—lamp in the Hut has been removed. "{ forceful > 2:I won't|I don't want to} go to prison." + "Smart man," he replies. "You wouldn't last. + + * [Disagree] + "I can still fix this." + Harris shakes his head. "You'll do nothing. This is beyond you now. You may go to prison or may go to firing squad - or we can change your name and move you somewhere where your indiscretions can't hurt you. But right now, none of that matters. What happens to you doesn't matter. All that matters is where that component is. + + * { not drugged } [Lie] + "I wanted to tell you," I tell him. "I thought I could find out who they were. Lead you to them." + Harris looks at me with contempt. "You wretch. You'll pay for what you've done to this country today. If a single man loses his life because of your pride and your perversions then God help your soul. + + * {drugged} {forceful < 0} [Apologise] + "Harris, I..." + ~lower(forceful) + "Stop it," he interrupts. "There's no jury here to sway. And there's no time. + +- (tell_me_now) <> So why don't you tell me, right now. Where is it?" + -> harris_demands_you_speak + + + + +=== harris_demands_you_speak + His eyes bear down like carbonised drill—bits. + * [Confess] + { forceful > 1 : + "You want me to tell you what happened? You'll be disgusted." + -else: + "All right. I'll tell you what happened." And never mind my shame. + } + "I can imagine how it starts," he replies. + + * { not drugged } [Dissemble] -> claim_hooper_took_component + * { drugged } [Dissemble] + My plan now is to blame Hooper, but I cannot seem to tell the story. Whatever they put in my tea, it rules my tongue. { forceful >1:I fight it as hard as I can but it does no good.|I am desperate to tell him everything. I am weeping with shame.} + + ~ lower(forceful) +- -> i_met_a_young_man + + + + +=== i_met_a_young_man + // Explain Story + * [Talk] + "There was a young man. I met him in the town. A few months ago now. We got to talking. Not about work. And I used my cover story, but he seemed to know it wasn't true. That got me wondering if he might be one of us." + - Harris is not letting me off any more. + "You seriously entertained that possibility?" + * [Yes] + "Yes, I considered it. <> + * [No] + "No. Not for more than a moment, of course. Everyone here is marked out by how little we would be willing to say about it." + "Only you told this young man more than a little, didn't you?" + I nod. "<> + * [Lie] + "I was quite certain, after a while. After we'd been talking. <> +- He seemed to know all about me. He... he was quite enchanted by my achievements." + The way Harris is staring I expect him to strike me, but he does not. He replies, "I can see how that must have been attractive to you," with such plain—spokeness that I think I must have misheard. + + * [Yes] "It's a lonely life in this place," I reply. "Lonely - and still one never gets a moment to oneself." + "That's how it is in the Service," Harris answers. + * * [Argue] "I'm not in the Service." + Harris shakes his head. "Yes, you are." + * * [Agree] "Perhaps. But I didn't choose this life." + Harris shakes his head. "No. And there's plenty of others who didn't who are suffering far worse." + - - Then he waves the thought aside. + + * (nope) { not drugged } [No] "The boy was a pretty simpleton. Quite inferior. His good opinion meant nothing to be. Harris, do not misunderstand. I was simply after his body." + ~ raise(evasive) + Harris, to his credit, doesn't flinch; but I can see he will have nightmares of this moment later tonight. I'm tempted to reach out and take his hand to worsen it for him. + + * { drugged } [No] + "It wasn't," I reply. "But I doubt you'd understand." + He simply nods. + * { not drugged } [Lie] -> nope + +- "Go on with your confession." +- (paused) + { not nope: + That gives me pause. I hadn't thought of it as such. But I suppose he's right. I am about to admit what I did. + } + "There's not much else to say. I took the part from Bombe computing device. You seem to know that already. I had to. He was going to expose me if I didn't." + // So blackmail? + "This young man was blackmailing you over your affair?" + + ~ temp harris_thinks_youre_drugged = drugged + + { drugged: + ~ drugged = false + As Harris speaks I find myself suddenly sharply aware, as if waking from a long sleep. The table, the corrugated walls of the hut, everything seems suddenly more tangible than a moment before. + Whatever it was they put in my drink is wearing off. + } + + * (yes) [Yes] + "Yes. I suppose he was their agent. I should have realised but I didn't. Then he threatened to tell you. I thought you would have me locked up: I couldn't bear the thought of it. I love working here. I've never been so happy, so successful, anywhere before. I didn't want to lose it." + "So what did you do with the component?" Harris talks urgently. He grips his gloves tightly in one hand, perhaps prepared to lift them and strike if it is required. "Have you passed it to this man already? Have you left it somewhere for him to find?" + * * (still_have) [I have it] + "I still have it. Not on me, of course. -> reveal_location_of_component + + * * (dont_have) [I don't have it] -> i_dont_have_it + * * [Lie] -> dont_have + * * [Tell the truth] -> still_have + + * (notright) [No] + "No, Harris. The young man wasn't blackmailing me." I take a deep breath. "It was Hooper." + { not hooper_mentioned: + "Hooper!" Harris exclaims, in surprise. {harris_thinks_youre_drugged:He does not doubt me for a moment.} + - else: + "Now look here," Harris interrupts. "Don't start that again." + } + "It's the truth, Harris. If I'm going to jail, so be it, but I won't hang at Traitor's Gate. Hooper was the one who told the boy about our work. Hooper put the boy on to me. { forceful < 2:I should have realised, of course. These things don't happen by chance. I was a fool to think they might.} And then, once he had me compromised, he demanded I steal the part from the machine." + ~ revealedhooperasculprit = true + "Which you did." Harris leans forward. "And then what? You still have it? You've stashed it somewhere?" + * * (didnt_have_long) [Yes] + "Yes. I only had a moment. -> reveal_location_of_component + + * * (passed_on) [No] -> passed_onto_hooper + * * [Lie] -> passed_on + * * [Evade] + "I can't remember." + He draws his gun and lays it lightly on the field table. + "I'm sorry to threaten you, friend. But His Majesty needs that brain of yours, and that brain alone. There are plenty of other parts to you that our country could do better without. Now I'll ask you again. Did you hide the component?" + * * * [Yes] -> didnt_have_long + * * * (nope_didnt_hide) [No] + "Very well then." I swallow nervously, to make it look more genuine. -> passed_onto_hooper + * * * [Lie] -> nope_didnt_hide + + * * * [Evade] -> i_dont_have_it + + * [Tell the truth] -> yes + * [Lie] -> notright + += i_dont_have_it + "I don't have it any more. I passed it through the fence to my contact straight after taking it, before it was discovered to be missing. It would have been idiocy to do differently. It's long gone, I'm afraid." + "You fool, Manning," Harris curses, getting quickly to his feet. "You utter fool. Do you suppose you will be any better off living under Hitler? It's men like you who will get us all killed. Men too feeble, too weak in their hearts to stand up and take a man's responsibility for the world. You're happier to stay a child all your life and play with your little childish toys." + * [Answer back] + "Really, Commander," I reply. "It rather sounds like you want to spank me." + "For God's sake," he declares with thick disgust, then swoops away out of the room. + + * [Say nothing] + I say nothing. It's true, isn't it? I can't deny that I know there is a world out there, a complicated world of pain and suffering. And I can't deny that I don't think about it a moment longer than I have to. What use is thinking on a problem that cannot be solved? It is precisely our ability to avoid such endless spirals that makes us human and not machine. + "God have mercy on your soul," Harris says finally, as he gets to his feet and heads for the door. "I fear no—one else will." + + - -> left_alone + += passed_onto_hooper + ~ hooper_mentioned = true + "No. I passed it on to Hooper." + "I see. And what did he do with it?" + * [Evade] + "I don't know." + "You can do better than that. Remember, there's a hangman's noose waiting for traitors." + * * [Theorise] + "Well, then," I answer, nervously. "What would he do? Either get rid of it straight away — or if that wasn't possible, which it probably wouldn't be, since he'd have to arrange things with his contacts — so most likely, he'd hide it somewhere and wait, until you had the rope around my neck and he could be sure he was safe." + -> claim_hooper_took_component.harris_being_convinced + + * * [Shrug] -> claim_hooper_took_component.its_your_problem + + * [Tell the truth] + "I don't think Hooper could have planned this in advance. So he'd need to get word to whoever he's working with, and that would take time. So I think he would have hidden it somewhere, and be waiting to make sure I soundly take the fall. That way, if anything goes wrong, he can arrange for the part to be conveniently re—found." + -> claim_hooper_took_component.harris_being_convinced + + * [Lie] + "I'm sure I saw him this evening, talking to someone by the fence on the woodland side of the compound. He's probably passed it on already. You'll have to ask him." + + -> claim_hooper_took_component.harrumphs + + +/*-------------------------------------------------------------------------------- + Trying to frame Hooper +--------------------------------------------------------------------------------*/ + + +=== claim_hooper_took_component +// Blame Hooper + "I saw Hooper take it." + ~ hooper_mentioned = true + { losttemper : + "Did you?" + The worst of his rage is passing; he is now moving into a kind of contemptuous despair. I can imagine him wrapping up our interview soon, leaving the hut, locking the door, and dropping the key down the well in the yard. + And why wouldn't he? With my name tarnished they will not let me back to work on the Bombe — if there is the slightest smell of treachery about my name I would be lucky not be locked up for the remainder of the war. + - else: + "I see." He is starting to lose his patience. I have seen Harris angry a few times, with lackeys and secretaries. But never with us. With the 'brains' he has always been cautious, treating us like children. + And now I see that, like a father, he wants to smack us when we disobey him. + } + "Just get to the truth, man. Every minute matters." + * { admitblackmail } [Persist with this] + "I know what you're thinking. If I've transgressed once then I must be guilty of everything else... But I'm not. We were close to cracking the 13th's intercept. We were getting correlations in the data. Then Hooper disappeared for a moment, and next minute the machine was down." + + * [Tell the truth] + "Very well. I see there's no point in covering up. You know everything anyway." + Harris nods, and waits for me to continue. + -> i_met_a_young_man + + * { not admitblackmail } [Persist with this] + "This is the truth." + + - I have become, somehow, an accustomed liar — the words roll easily off my tongue. Perhaps I am a traitor, I think, now that I dissemble as easily as one. + "Go on," Harris says, giving me no indication of whether he believes my tale. + * [Assert] "I saw him take it," I continue. "Collins was outside having a cigarette. Peterson was at the table. But I was at the front of the machine. I saw Hooper go around the side. He leant down and pulled something free. I even challenged him. I said, 'What's that? Someone put a nail somewhere they shouldn't have?' He didn't reply." + Harris watches me for a long moment. + + * [Imply] "At the moment the machine halted, Peterson was at the bench and Collins was outside having a smoke. I was checking the dip—switches. Hooper was the only one at the back of the Bombe. No—one else could have done it." + "That's not quite the same as seeing him do it," Harris remarks. + * * [Logical] + "When you have eliminated the impossible..." I begin, but Harris cuts me off. + + * * [Persuasive] + "You have to believe me." + "We don't have to believe anyone," Harris returns. "I will only be happy with the truth, and your story doesn't tie up. We know you've been leaving yourself open to pressure. We've been watching your activities for some time. But we thought you were endangering the reputation of this site; not risking the country herself. Perhaps I put too much trust in your intellectual pride." + He pauses for a moment, considering something. Then he continues: + "It might have been Hooper. It might have been you. -> we_wont_guess + + * * [Confident] + "Ask the others," I reply, leaning back. "They'll tell you. If they haven't already, that's only because they're protecting Hooper. Hoping he'll come to his senses and stop being an idiot. I hope he does too. And if you lock him up in a freezing hut like you've done me, I'm sure he will." + "We have," Harris replies simply. + It's all I can do not to gape. + -> hoopers_hut_3 + + - "We are left with two possibilities. You, or Hooper." The Commander pauses to smooth down his moustache. <> + - (hoopers_hut_3) "Hooper's in Hut 3 with the Captain, having a similar conversation." + + * "And the other men?["] Do we have a hut each? Are there enough senior officers to go round?" + "Collins was outside when it happened, and Peterson can't get round the machine in that chair of his," Harris replies. "That leaves you and Hooper. + * "Then you know I'm right.["] You knew all along. Why did you threaten me?" + "All we know is that we have a traitor, holding the fate of the country in his hands. + - (we_wont_guess) <> We're not in the business of guessing here at Bletchley. We are military intelligence. We get answers." Harris points a finger. "And if that component has left these grounds, then every minute is critical." + * [Co-operate] + "I'd be happy to help," I answer, leaning forwards. "I'm sure there's something I could do." + "Like what, exactly?" + * * "Put me in with Hooper." + -> putmein + * * "Tell Hooper I've confessed.["] Better yet. Let him see you marching me off in handcuffs. Then let him go, and see what he does. Ten to one he'll go straight to wherever he's hidden that component and his game will be up." + Harris nods slowly, chewing over the idea. It isn't a bad plan even — except, of course, Hooper has not hidden the component, and won't lead them anywhere. But that's a problem I might be able to solve once I'm out of this place; and once they're too busy dogging Hooper's steps from hut to hut. + "Interesting," the Commander muses. "But I'm not so sure he'd be that stupid. And if he's already passed the part on, the whole thing will only be a waste of time." + * * * "Trust me. He hasn't.["] If I know that man, and I do, he'll be wanting to keep his options open as long as possible. If the component's gone then he's in it up to his neck. He'll take a week at least to make sure he's escaped suspicion. Then he'll pass it on." + "And if we keep applying pressure to him, you think the component will eventually just turn up?" + * * * * "Yes.["] Probably under my bunk." + Harris smiles wryly. "We'll know that for a fake, then. We've looked there already. + * * * * "Or be thrown into the river." + "Hmm." Harris chews his moustache thoughtfully. "Well, that would put us in a spot, seeing as how we'd never know for certain. We'd have to be ready to change our whole approach just in case the part had got through to the Germans. + - - - - <> I don't mind telling you, this is a disaster, this whole thing. What I want is to find that little bit of mechanical trickery. I don't care where. In your luncheon box or under Hooper's pillow. Just somewhere, and within the grounds of this place." + * * * * "Then let him he think he's off the hook.["] Make a show of me. And then you'll get your man." + Somehow, I think. But that's the part I need to work. + -> harris_takes_you_to_hooper + + * * * * "Then you'd better get searching[."]," I reply, tiring of his complaining. A war is a war, you have to expect an enemy. -> its_your_problem + + * * * "You're right. Let me talk to him[."], then. As a colleague. Maybe I can get something useful out of him." + -> putmein + + * * * "You're right." -> shake_head + + * [Block] -> its_your_problem + + += harris_being_convinced + "Makes sense," Harris agrees, cautiously. { evasive > 1:I can see he's still not entirely convinced by my tale, as well he might not be — I've hardly been entirely straight with him.|I can see he's still not certain whether he can trust me.} "Which means the question is, what can we do to rat him out?" + * [Offer to help] + "Maybe I can help with that." + "Oh, yes? And how, exactly?" + * * "I'll talk to him." + "What?" + "Put me in with Hooper with him. Maybe I can get something useful out of him." + -> putmein + * * "We'll fool him.["] He's waiting to be sure that I've been strung up for this, so let's give him what he wants. If he sees me taken away, clapped in irons — he'll go straight to that component and set about getting rid of it." + -> harris_takes_you_to_hooper + + * [Don't offer to help] + I lean back. -> its_your_problem + += putmein + Harris shakes his head. + "He despises you. I don't see why he'd give himself up to you." + * [Insist] "Try me. Just me and him." + -> go_in_alone + * [Give in] "You're right." + -> shake_head + + += shake_head + // Can't help + <> I shake my head. "You're right. I don't see how I can help you. So there's only one conclusion." + "Oh, yes? And what's that?" + -> its_your_problem + + += its_your_problem +// Won't Help + "It's your problem. Your security breach. So much for your careful vetting process." + I lean back in my chair and fold my arms so the way they shake will not be visible. + "You'd better get on with solving it, instead of wasting your time in here with me." + -> harrumphs + += harrumphs + Harris harrumphs. He's thinking it all over. + * { putmein } [Wait] + "All right," he declares, gruffly. "We'll try it. But if this doesn't work, I might just put the both of you in front of a firing squad and be done with these games. Worse things happen in time of war, you know." + "Alone," I add. + -> go_in_alone + + * { putmein } [Wait] + "No," Harris declares, finally. "I think you're lying about Hooper. I think you're a clever, scheming young man — that's why we hired you — and you're looking for the only reasonable out this situation has to offer. But I'm not taking it. We know you were in the room with the machine, we know you're of a perverted persuasion, we know you have compromised yourself. There's nothing more to say here. Either you tell me what you've done with that component, or we will hang you and search just as hard. It's your choice." + -> harris_threatens_lynching + + += go_in_alone + "Alone?" + "Alone." + Harris considers it. I watch his eyes, flicking backwards and forwards over mine, like a ribbon—reader loading its program. + * [Patient] "Well?" + * [Impatient] "For God's sake, man, what do you have to lose?" + ~ raise(forceful) + - "We'll be outside the door," Harris replies, seriously. "The first sign of any funny business and we'll have you both on the floor in minutes. You understand? The country needs your brain, but it's not too worried about your legs. Remember that." + Then he gets to his feet, and opens the door, and marches me out across the yard. The evening is drawing in and there's a chill in the air. My mind is racing. I have one opportunity here — a moment in which to put the fear of God into Hooper and make him do something foolish that places him in harm's way. But how to achieve it? + "You ready?" Harris demands. + * (yes) [Yes] + "Absolutely." + * [No] + "No." + "Too bad." + * [Lie] -> yes + + - -> inside_hoopers_hut + + +/*-------------------------------------------------------------------------------- + Quick visit to see Hooper +--------------------------------------------------------------------------------*/ + +=== harris_takes_you_to_hooper + // Past Hooper + Harris gets to his feet. "All right," he says. "I should no better than to trust a clever man, but we'll give it a go." + Then, he smiles, with all his teeth, like a wolf. + { claim_hooper_took_component.hoopers_hut_3: + "Especially since this is a plan that involves keeping you in handcuffs. I don't see what I have to lose." + - else: + "Hooper's in Hut 3 being debriefed by the Captain. Let's see if we can't get his attention somehow." + } + // Leading you past Hooper + He raps on the door for the guard and gives the man a quick instruction. He returns a moment later with a cool pair of iron cuffs. + "Put 'em up," Harris instructs, and I do so. The metal closes around my wrists like a trap. I stand and follow Harris willingly out through the door. + But whatever I'm doing with my body, my mind is scheming. Somehow, I'm thinking, I have to get away from these men long enough to get that component behind Hut 2 and put it somewhere Hooper will go. Or, otherwise, somehow get Hooper to go there himself... + Harris marches me over to Hut 3, and gestures for the guard to stand aside. Pushing me forward, he opens the door nice and wide. + // Hut 3 + "Captain. Manning talked. If you'd step out for a moment?" + * [Play the part, head down] + From where he's sitting, I know Hooper can see me, so I keep my head down and look guilty as sin. The bastard is probably smiling. + + + * [Look inside the hut] + I look in through the door and catch Hooper's expression. I had half expected him to be smiling be he isn't. He looks shocked, almost hurt. "Iain," he murmurs. "You couldn't..." + + * (shouted) [Call to Hooper] + I have a single moment to shout something to Hooper before the door closes. + "I'll get you Hooper, you'll see!" I cry. Then: + + * * "Queen to rook two, checkmate!"[] I call, then laugh viciously, as if I am damning him straight to hell. + ~ hooperClueType = CHESS + - - (only_catch) I only catch Hooper's reaction for a moment — his eyebrow lifts in surprise and alarm. Good. If he thinks it is a threat then he just might be careless enough to go looking for what it might mean. + + * * "Ask not for whom the bell tolls!" + He stares back at me, as if were a madman and perhaps for a split second I see him shudder. + + + * * "Two words: messy, without one missing!"[] I cry, laughing. It isn't the best clue, hardly worthy of The Times, but it will have to do. + ~ hooperClueType = CROSSWORD + -> only_catch + +- The Captain comes outside, pulling the door to. "What's this?" he asks. "A confession? Just like that?" + "No," the Commander admits, in a low voice. "I'm afraid not. Rather more a scheme. The idea is to let Hooper go and see what he does. If he believes we have Manning here in irons, he'll try to shift the component." + "If he has it." + "Indeed." + The Captain peers at me for a moment, like I was some kind of curious insect. + "Sometimes, I think you people are magicians," he remarks. "Other times you seem more like witches. Very well." + With that he opens the door to the Hut and goes back inside. The Commander uses the moment to hustle me roughly forward. + { shouted : + "And what was all that shouting about?" he hisses in my ear as we move towards the barracks. "Are you trying to pull something? Or just make me look incompetent?" + - else: + "This scheme of yours had better come off," he hisses in my ear. "Otherwise the Captain is going to start having men tailing me to see where I go on Saturdays." + } + * [Reassure] + { not shouted : + "It will. Hooper's running scared," I reply, hoping I sound more confident than I feel. + - else: + "Just adding to the drama," I tell him, confidently. "I'm sure you can understand that." + } + "I think we've had enough drama today already," Harris replies. "Let's hope for a clean kill." + + * [Dissuade] + { not shouted: + "The Captain thought it was a good scheme. You'll most likely get a promotion." + - else: + "I'm not trying to do anything except save my neck." + } + "Let's hope things work out," Harris agrees darkly. + + * [Evade] + "We're still in ear—shot if they let Hooper go. Best get us inside and then we can talk, if we must." + "I've had enough of your voice for one day," Harris replies grimly. <> + + * [Say nothing] + I let him have his rant. <> +- He hustles me up the steps of the barracks, keeping me firmly gripped as if I had any chance of giving him, a trained military man, the slip. It's all I can do not to fall into the room. + -> slam_door_shut_and_gone + + + + +=== inside_hoopers_hut + - Harris opens the door and pushes me inside. "Captain," he calls. "Could I have a moment?" + The Captain, looking puzzled, steps out. The door is closed. Hooper stares at me, open—mouthed, about to say something. I probably have less than a minute before the Captain storms back in and declares this plan to be bunkum. + * [Threaten] + "Listen to me, Hooper. We were the only men in that hut today, so we know what happened. But I want you to know this. I put the component inside a breeze—block in the foundations of Hut 2, wrapped in one of your shirts. They're going to find it eventually, and that's going to be what tips the balance. And there's nothing you can do to stop any of that from happening." + ~ hooperClueType = STRAIGHT + + His eyes bulge with terror. "What did I do, to you? What did I ever do?" + * * [Tell the truth] + "You treated me like vermin. Like something abhorrent." + "You are something abhorrent." + "I wasn't. Not when I came here. And I won't be, once you're gone." + + * * [Lie] + "Nothing," I reply. "You're just the other man in the room. One of us has to get the blame." + + * * [Evade] + "It doesn't matter. Just remember what I said. I've beaten you, Hooper. Remember that." + - - I get to my feet and open the door of the Hut. The Captain storms back inside and I'm quickly thrown out. -> hustled_out + + + * [Bargain] + "Hooper, I'll make a deal with you. We both know what happened in that hut this afternoon. I know because I did it, and you know because you know you didn't. But once this is done I'll be rich, and I'll split that with you. I'll let you have the results, too. Your name on the discovery of the Bombe. And it won't hurt the war effort — you know as well as me that the component on its own is worthless, it's the wiring of the Bombe, the usage, that's what's valuable. So how about it?" + Hooper looks back at me, appalled. "You're asking me to commit treason?" + * * [Yes] + "Yes, perhaps. But also to ensure your name goes down in the annals of mathematics. -> back_of_hut_2 + * * [No] + "No. It's not treason. It's a trade, plain and simple." + + * * (lie) [Lie] + "I'm suggesting you save your own skin. I've wrapped that component in one of your shirts, Hooper. They'll be searching this place top to bottom. They'll find it eventually, and when they do, that's the thing that will swing it against you. So take my advice now. Hut 2." + ~ hooperClueType = STRAIGHT + + * * [Evade] -> lie + - - -> no_chance + + * [Plead] + "Please, Hooper. You don't understand. They have information on me. I don't need to tell you what I've done, you know. Have a soul. And the component — it's nothing. It's not the secret of the Bombe. It's just a part. The German's think it's a weapon — a missile component. Let them have it. Please, man. Just help me." + "Help you?" Hooper stares. "Help you? You're a traitor. A snake in the grass. And you're queer." + * * [Deny] + "I'm no traitor. You know I'm not. How much work have I done here against the Germans? I've given my all. And you know as well as I do, if the Reich were to invade, I would be a dead man. Please, Hooper. I'm not doing any of this lightly." + + * * [Accept] + "I am what I am," I reply. "I'm the way I was made. But they'll hang me unless you help, Hooper. Don't let them hang me." + + * * [Evade] + "That's not important now. What matters is what you do, this evening." + + - - "Assuming I wanted to help you," he replies, carefully. "Which I don't. What would I do?" + "Nothing. Almost nothing. + -> back_of_hut_2 + += back_of_hut_2 + <> All you have to do is go to the back of Hut 2. There's a breeze—block with a cavity. That's where I've put it. I'll be locked up overnight. But you can pick it up and pass it to my contact. He'll be at the south fence around two AM." + ~ hooperClueType = STRAIGHT + -> no_chance + += no_chance + "If you think I'll do that then you're crazy," Hooper replies. + At that moment the door flies open and the Captain comes storming back inside. + -> hustled_out + += hustled_out + // To Barracks + Harris hustles me over to the barracks. "I hope that's the end of it," he mutters. + "Just be sure to let him out," I reply. "And then see where he goes." + -> slam_door_shut_and_gone + + + +/*-------------------------------------------------------------------------------- + Left alone overnight +--------------------------------------------------------------------------------*/ + + +=== slam_door_shut_and_gone + Then they slam the door shut, and it locks. + { hooperClueType == NONE : + <> How am I supposed to manage anything from in here? + * [Try the door] -> try_the_door + * [Try the windows] -> try_the_windows + + - else: + I can only hope that Hooper bites. If he thinks I'm bitter enough to have framed him, and arrogant enough to have taunted him with {hooperClueType > STRAIGHT:a clue to} where the damning evidence is hidden... + If he hates me enough, and is paranoid enough, then he might {hooperClueType > STRAIGHT:unravel my little riddle and} go searching around Hut 2. + } + + * [Wait] -> night_falls + + += try_the_door + I try the door. It's locked, of course. + -> from_outside_heard + += from_outside_heard + From outside, I hear a voice. Hooper's. He's haranguing someone. + - (opts) + * (listened) [Listen at the keyhole] + I put my ear down to the keyhole, but there's nothing now. Probably still a guard outside, of course, but they're keeping mum. + -> opts + + * { not try_the_windows } [Try the window] -> try_the_windows + * { not try_the_door } {listened} [Try the door] -> try_the_door + * { try_the_windows } [Smash the window] -> try_to_smash_the_window + * { try_the_door && try_the_windows } [Wait] + It's useless. There's nothing I can do but hope. I sit down on one corner of the bunk to wait. + -> night_falls + += try_the_windows + I go over to the window and try to jimmy it open. Not much luck, but in my struggling I notice this window only backs on the thin little brook that runs down the back of the compound. Which means, if I smashed it, I might get away with no—one seeing. + -> from_outside_heard + + += try_to_smash_the_window + The window is my only way out of here. I just need a way to smash it. + * [Punch it] + I suppose my fist would do a good enough job. But I'd cut myself to ribbons, most likely. <> + + * (use_bucket) [Find something] + ~ smashingWindowItem = BUCKET + I cast around the small room. There's a bucket in one corner for emergencies — I suppose I could use that. I pick it up but it's not very easy to heft. <> + * [Use something you've got] + I pat down my pockets but all I'm carrying is the intercept, which is no good at all. + * * [Something you're wearing?] + Ah, but of course! I slip off one shoe and heft it by the toe. The heel will make a decent enough hammer, if I give it enough wallop. + ~ smashingWindowItem = SHOE + But I'll cut my hand to ribbons doing it. <> + * * [Look around] -> use_bucket + - And the noise would be terrible. There must be a way of making this easier. I'm supposed to be a thief now. What would a burglar do? + * [Work slowly] + Work carefully? It's difficult to work carefully when all one's has is { smashingWindowItem == BUCKET :a bucket. It's rather like the sledgehammer for the proverbial nut|{ smashingWindowItem == SHOE :a shoe|nothing but brute force}}. + * * [Just do it] -> time_to_move_now + * * [Look around for something] + * [Find something to help] + - -> find_something_to_smash_window + + += time_to_move_now + Enough of this. There isn't any time to lose. Right now they'll be following Hooper as he goes to bed, and goes to sleep; and then that's it. The minute he closes his eyelids and drifts off that's the moment that this trap swings shut on me. + So I punch out the glass with my { smashingWindowItem == BUCKET :bucket|{ smashingWindowItem == SHOE :shoe|fist}} and it shatters with a terrific noise. Then I stop, and wait, to see if anyone will come in through the door. + Nothing. + * (pause) [Wait a little longer] + I pause for a moment longer. It doesn't do to be too careless... + * [Clear the frame of shards] + With my jacket wrapped round my arm, I sweep out the remaining shards of glass. It's not a big window, but I'm not a big man. If I was Harris, I'd be stuffed, but as it is... + + - Then the door locks turns. The door opens. Then Jeremy — one of the guards, rather — sticks his head through the door. "I thought I heard..." + He stops. Looks for a moment. { smashingWindowItem ==BUCKET :Sees the bucket in my hand.|Sees the broken window.} Then without a moment's further thought he blows his shrill whistles and hustles into the hut, grabbing me roughly by my arms. + { pause: + I'll never know if I hadn't have waited that extra moment — maybe I still could have got away. But, how far? + } + I'm hustled into one of the huts. Nowhere to sleep, but they're not interested in my comfort any longer. Harris comes in with the Captain. + "So," Harris remarks. "Looks like your little trap worked. Only it worked to show you out for what you are." + * [Tell the truth] + { i_met_a_young_man : + "Please, Harris. You can't understand the pressure they put me under. You can't understand what it's like, to be in love but be able to do nothing about it..." + - else: + "Harris. They were blackmailing me. They knew about... certain indiscretions. You can understand, can't you, Harris? I was in an impossible bind..." + } + * [Lie] + "I had to get out, Harris. I had to provoke Hooper into doing something that would incriminate himself fully. He's too clever, you see..." + + * [Evade] + "This proves nothing," I reply stubbornly. "You still don't have the component and without it, I don't see what you can hope to prove." + + - "Be quiet, man. We know all about your and your sordid affairs." The Captain curls his lip. "Don't you know there's a war on? Do you know the kind of place they would have sent you if it haven't had been for that brain of yours? Don't you think you owe it to your country to use it a little more?" + + Do I, I wonder? Do I owe this country anything, this country that has spurned who and what am I since the day I became a man? + * [Yes] + My anger deflates like a collapsing equation, all arguments cancelling each other out. The world, of course, owes me nothing; and I owe it everything. + + * (alone) [No] + Of course not. I am alone; that is what they wanted me to be, because of who and what I love. So I have no nation, no country. + + + * [Lie] -> alone + * [Evade] + But what is a country, after all? A country is not a concept, not an ideal. Every country falls, its borders shift and move, its language disappears to be replaced by another. Neither the Reich nor the British Empire will survive forever, so what use is my loyalty to either? + I may as well, therefore, look after myself. Something I have attempted, but failed miserably, to do. + + - // Tell us where + "I'm afraid we have only one option, Manning," Harris says. "Please, man. Tell us where the component is." + ~ notraitor = true + ~ losttemper = false + * [Tell them] + ~ revealedhooperasculprit = false + "All right." I am beaten, after all. "<>-> reveal_location_of_component + + * [Say nothing] -> my_lips_are_sealed + += find_something_to_smash_window + Let me see. There's the bunk, { not smashingWindowItem == BUCKET :a bucket,} nothing else. I have my jacket but nothing in the pockets — no handkerchief, for instance. + - (opts) + * [The bunk] + The bunk has a solid metal frame, a blanket, a pillow, nothing more. + - - (bunk_opts) + * * [The frame] + The frame is heavy and solid. I couldn't lift it or shift it without help from another man. And it wouldn't do me any good here anyway. I can reach the window perfectly well. + -> bunk_opts + * * [The blanket] + The blanket. Perfect. I scoop it up off the bed and hold it in place over the window. -> smash_the_window + * * [The pillow] + The pillow is fat and fluffy. I could put it over the window and it would muffle the sound of breaking glass, certainly; but I wouldn't be able to break any glass through it either. + -> bunk_opts + + * * {bunk_opts > 1} [Something else] -> opts + + * [The jacket] + I slip off my jacket and hold it with one hand over the glass. -> smash_the_window + * { not smashingWindowItem == BUCKET } [The bucket] + The bucket? Hardly. The bucket might do some good if I wanted to sweep up the glass afterwards, but it won't help me smash the glass quietly. + -> opts + + +=== smash_the_window + // Smashing glass + Then I heft { smashingWindowItem == BUCKET :up the bucket — this really is quite a fiddly thing to be doing in cuffs — |{ smashingWindowItem == SHOE : my shoe by its toe, |back my arm, }} and take a strong swing, trying to imagine it's Harris' face on the other side. + ~ smashedglass = true + ~ smashingWindowItem = NONE + * [Smash!] + - The sound of the impact is muffled. With my arm still covered, I sweep out the remaining glass in the frame. + - I'm ready to escape. The only trouble is — when they look in on me in the morning, there will be no question what has happened. It won't help me one jot with shifting suspicion off my back. + * [Wait] + So perhaps I should wait it out, after all. Who knows? I might have a better opportunity later. + -> night_passes + * [Slip out] + Moving quickly and quietly, I hoist myself up onto the window—frame and worm my way outside into the freezing night air. Then I am away, slipping down the paths between the Huts, sticking to the shadows, on my way to Hut 2. + // Out at night + - + * [Go the shortest way] + There's no time to lose. Throwing caution to the wind I make my way quickly to Hut 2, and around the back. I don't think I've been seen but if I have it is too late. My actions are suspicious enough for the noose. I have no choice but to follow through. + * [Take a longer route] + In case I'm being followed, I divert around the perimeter of the compound. It's a much longer path, and it takes me across some terrain that's difficult to negotiate in the dark — muddy, and thick with thistles and nestles. + ~ muddyshoes = true + Still, I can be confident no—one is behind me. I crouch down behind the rear wall of Hut 2. <> + - The component is still there, wrapped in a tea—towel and shoved into a cavity in a breeze—block at the base of the Hut wall. + * [Take it] + Quickly, I pull it free, and slip it into the pocket of my jacket. + ~ gotcomponent = true + + * [Leave it] + Still there means no—one has found it, which means it is probably well—hidden. And short of skipping the compound now, I can afford to leave it hidden there a while longer. So I leave it in place. + - Where now? + * [Back to the barracks] -> return_to_room_after_excursion + * { gotcomponent } [Go to Hooper's dorm] -> go_to_hoopers_dorm + * [Escape the compound] + Enough of this place. Time for me to get moving. I can get to the train station on foot, catch the postal train to Scotland and be somewhere else before anyone realises that I'm gone. + + Of course, then they'll be looking for me in earnest. { not framedhooper :As a confirmed traitor.|Perhaps not as a traitor — they might take the idea that Hooper was involved with the theft — but certainly as a valuable mind, one containing valuable secrets and all too easily threatened. They will think I am running away because of my indiscretions. I suppose, in fairness, that I am.} + * * [Go] -> live_on_the_run + * * [Don't go] + It's no good. That's only half a solution. I couldn't be happy with that. + * * * [Back to the barracks] -> return_to_room_after_excursion + * * * { gotcomponent && not go_to_hoopers_dorm } [To Hooper's dorm] -> go_to_hoopers_dorm + + +/*-------------------------------------------------------------------------------- + Visit Hooper's dorm overnight +--------------------------------------------------------------------------------*/ + + +=== go_to_hoopers_dorm + // Hooper's Dorm + I creep around the outside of the huts towards Hooper's dorm. Time to wrap up this little game once and for all. A few guards patrol the area at night but not many — after all, very few know this place even exists. + Our quarters are arranged away from the main house; where we sleep is of less importance than where we work. We each have our own hut, through some are less permanent than others. Hooper's is a military issue tent: quite a large canopy, with two rooms inside and a short porch area where he insists people leave their shoes. It's all zipped up for the night and no light shines from inside. + I hang back for a moment. If Harris is keeping to the terms of our deal then someone will be watching this place. But I can see no—one. + * (outer_zip) [Open the outer zip] + I creep forward to the tent, intent on lifting the zip to the front porch area just a little — enough to slip the component inside, and without the risk of the noise waking Hooper from his snoring. + The work is careful, and more than little fiddly — Hooper has tied the zips down on the inside, the fastidious little bastard! — but after a little work I manage to make a hole large enough for my hand. + * * [Slip in the component] + I slide the component into the tent, work the zip closed, and move quickly away into the shadows. It takes a few minutes for my breath to slow, and my heart to stop hammering, but I see no other movement. If anyone is watching Hooper's tent, they are asleep at their posts. + ~ putcomponentintent = true + ~ gotcomponent = false + -> return_to_room_after_excursion + * * [No, some other way] + Then pause. This is too transparent. Too blatant. If I leave it here, like this, Hooper will never be seen to go looking for it: he will stumble over it in plain sight, and the men watching will wonder why it was not there when he went to bed. + No, I must try something else — or nothing at all. + * * * [On top of the tent] -> put_component_on_tent + * * * [Throw the component into the long grass] + From inspiration — or desperation, I am not certain — a simple approach occurs to me. -> toss_component_into_bushes + * * * [Give up] + There is nothing to be gained here. I have the component now; maybe it will be of some value tomorrow. + * * * * [Return to my barrack] -> return_to_room_after_excursion + * * * * [Escape the compound] -> live_on_the_run + + * (wide_circuit) [Look for another opening] + Making a wide circuit I creep around the tent. It has plenty of other flaps and openings, tied down with Gordian complexity. But nothing afford itself to slipping the component inside. + * * [Try the porch zip] -> outer_zip + * * [Try on top of the tent] -> put_component_on_tent + * * [Give up] + It's no good. Nothing I can do will be any less than obvious — something appearing where something was not there before. The men watching Hooper will know it is a deception and Hooper's protestations will be taken at face value. + If I can't find a way for Hooper to pick the component up, as if from a hiding place of his own devising, and be caught doing it, then I have no plan at all. + * * * [Return to my barrack] -> return_to_room_after_excursion + * * * [Escape the compound] -> live_on_the_run + * * * [Toss the component into the bushes] -> toss_component_into_bushes + + * [Hide the component somewhere] + If I leave the component here somewhere it should be somewhere I can rely on Hooper finding it, but no—one before Hooper. In particular. + * * [Behind the tent] -> wide_circuit + * * [Inside the porch section] -> outer_zip + * * [On top of the canvas] -> put_component_on_tent + + += put_component_on_tent + A neat idea strikes me. If I could place it on top of the canvas, somewhere in the middle where it would bow the cloth inwards, then it would be invisible to anyone passing by. But to Hooper, it would be above him: a shadow staring him in the face as he awoke. What could be more natural than getting up, coming out, and looking to see what had fallen on him during the night? + + It's the work of a moment. I was once an excellent bowler for the second XI back at school. This time I throw underarm, of course, but I still land the vital missing component exactly where I want it to go. + ~ framedhooper = true + ~ gotcomponent = false + For a second I hold my breath, but nothing and no—one stirs. -> return_to_room_after_excursion + + += toss_component_into_bushes + I toss the component away into the bushes behind Hooper's tent and return to my barrack, wishing myself a long sleep followed by a morning, free of this business. + ~ gotcomponent = false + ~ throwncomponentaway = true + -> return_to_room_after_excursion + +/*-------------------------------------------------------------------------------- + Ending: Run away from the camp +--------------------------------------------------------------------------------*/ + + +=== live_on_the_run + Better to live on the run than die on the spit. Creeping around the edge of the compound{ gotcomponent :, the Bombe component heavy in my pocket}, I make my way to the front gate. As always, it's manned by two guards, but I slip past their box by crawling on my belly. + And then I'm on the road. Walking, not running. Silent. Free. + // End - Run Away + For the moment, at least. + -> END + +/*-------------------------------------------------------------------------------- + Return to room after slipping out +--------------------------------------------------------------------------------*/ + + +=== return_to_room_after_excursion + { gotcomponent :The weight of the Bombe component safely in my jacket|Satisfied}, I return the short way up the paths between the huts to the barrack block and the broken window. + It's a little harder getting back through — the window is higher off the ground than the floor inside — but after a decent bit of jumping and hauling I manage to get my elbows up, and then one leg, and finally I collapse inside, quite winded and out breath. + * [Wait] -> night_passes + +/*-------------------------------------------------------------------------------- + Night passes +--------------------------------------------------------------------------------*/ + + +=== night_passes +// In room smashed glass + The rest of the night passes slowly. I sleep a little, dozing mostly. Then I'm woken by the rooster in the yard. The door opens, and Harris comes in. He takes one look at the broken window and frowns with puzzlement. + { putcomponentintent: -> put_component_inside_tent } + + "What happened there?" + * [Confess] + "I broke it," I reply. There doesn't seem any use in trying to lie. "I thought I could escape. But I couldn't get myself through." + The Commander laughs. -> glad_youre_here + + * (deny) [Deny] + "I'm not sure. I was asleep: I woke up when someone broke the window. I looked out to see who it was, but they were already gone." + Harris looks at me with puzzlement. "Someone came by to break the window, and then ran off? That's absurd. That's utterly absurd. Admit it, Manning. You tried to escape and you couldn't get through." + * * [Admit it] + "All right. {forceful>1:Damn you.} That's exactly it." + -> glad_youre_here + + * * { not framedhooper } [Deny it] + "If I wanted to escape, I would have made damn sure that I could," I tell him sternly. + -> harris_certain_is_you + + * * { framedhooper } [Deny it] + "I tell you, someone broke it. Someone wanted to threaten me, I think." + Harris shakes his head. "Well, we can look into that matter later. For now, you probably want to hear the more pressing news. -> found_missing_component + + * { gotcomponent } [Show him the component] -> someone_threw_component + += put_component_inside_tent + He takes one look around, and sighs, a deep, wistful sigh. + "Things just get worse and worse for you, Manning," he remarks. "You are your own worst enemy." + * [Agree] + "I've thought so before." { admitblackmail :Certainly in the matter of getting blackmailed.} + "Let me tell you what happened this morning. <> + + * [Disagree] + "Right now, I think you take that role, Harris," I reply coolly. + - - (droll) "Very droll," he replies. "Let me tell you what happened this morning. It will take the smile off your face. <> + + * [Evade] + "I'm looking forward to having a wash and a change of clothes; which should make me a little less evil to be around." + -> droll + + - Our men watching Hooper's tent saw Hooper wake up, get dressed, clamber out of his tent and then step on something in at the entrance of his tent." + ~ piecereturned = true + * [Be interested] + "You mean he didn't even hide it? He put it in his shoe?" + - - (not_that) "No," Harris replies. "That isn't really what I mean. <> + + * [Be dismissive] + "So he's an idiot, and he hid it in his shoe." + -> not_that + + * [Say nothing] + I say quiet, listening, not sure how this will go. + "In case I'm not making myself clear," Harris continues, "<> + + - I mean, he managed to find it, by accident, somewhere where it wasn't the night before. And at the same time, you're sitting here with your window broken. So, I rather think you've played your last hand and lost. It's utterly implausible that Hooper stole that component and then left it lying around in the doorway of his tent. So I came to tell you that the game is up, for you." + He nods and gets to his feet. -> left_alone + + + += someone_threw_component + "Someone threw this in through the window over night," I reply, and open my jacket to reveal the component from the Bombe. "I couldn't see who, it was too dark. But I know what it is." + He reaches out and takes it. "Well, I'll be damned," he murmurs. "That's it all right. And you didn't have it on you when we put you in here. But it can't have been Hooper — I had men watching him all night. And there's no—one else it could have been." + He turns the component over in his hands, bemused. + ~ piecereturned = true + * [Suggest something] + "Perhaps Hooper had an accomplice. Someone else who works on site." + Harris shakes his head, distractedly. "That doesn't make sense," he says. "Why go to all the trouble of stealing it only to give it back? And why like this?" + * * [Suggest something] + "Perhaps the accomplice thought it was Hooper being kept in here. Maybe they saw the guard..." + -> all_too_farfetched + * * [Suggest nothing] + * [Suggest nothing] + - I shrug, eloquently. + - -> all_too_farfetched + + += glad_youre_here + "Shame," he remarks. "I should have left that window open and put a guard on you. Might have been interesting to see where you went. Anyway, I'm glad you're still here, even if you do smell like a dog." + + * { not framedhooper } [Be optimistic] + -> night_falls.morning_not_saved.optimism + * { not framedhooper } [Be pessimistic] + -> night_falls.morning_not_saved.pessimism + + * { framedhooper } [Be optimistic] + "I'm looking forward to having a bath." + // Framed Hooper + "Well, you should enjoy it. <> + + * { framedhooper } [Be pessimistic] + "I imagine I'll smell worse after another couple of days of this." + "That won't be necessary. <> + - -> found_missing_component + + += found_missing_component + // Framed Hooper + We found the missing component. Or rather, Hooper found it for us. He snuck out and retrieved it from on top. Of all the damnest places — you would never have known it was there. He claimed ignorance when we jumped him, of course. But it's good enough for me." + * (devil) [Approve] + "I can't tell you enough, I'm glad to hear it. I've had a devil of a night." + His gaze flicks to the broken window, but only for a moment. I think he genuinely cannot believe I could have done it. + * [Disapprove] + "You should never have hired him. A below-average intelligence can't be expected to cope with the pressure of our work." + - Harris rolls his eyes, but he might almost be smiling. "You'd better get along, { devil :and work through your devils|Mr Intelligent}. There's a 24—hour—late message to be tackled and we're a genius short. So you'd better be ready to work twice as hard." + * [Thank him] + "I'll enjoy it. Thank you for helping me clear this up." + "Don't thank me yet. There's still a war to fight. Now get a move on." + I nod, and hurry out of the door. The air outside has never tasted fresher and more invigorating. <> + + * [Argue with him] + "I'll work as hard as I work." + "Get out," Harris growls. "Before I decide to arrest you as an accessory." + I do as he says. Outside the barrack, the air has never smelt sweeter. + - -> head_for_my_dorm_free + + +=== night_falls === +// Night falls + Night falls. The clockwork of the heavens keeps turning, whatever state I might be in. No—one can steal the components that make the sun go down and the stars come out. I watch it performing its operations. I can't sleep. + { hooperClueType > NONE : + Has Hooper taken my bait? + } + * [Look of out the window] + I peer out of the window, but it looks out onto the little brook at the back of the compound, with no view of the other huts or the House. Who knows if there are men up, searching the base of Hut 2, following one another with flashlights... + {inside_hoopers_hut.back_of_hut_2: + Perhaps Hooper is there, in the dark, trying to help me after all? + } + * [Listen at the door] + I put my ear to the keyhole but can make out nothing. Are there still guards posted? { hooperClueType > NONE :Perhaps, if Hooper has managed to incriminate himself, the guards have been removed?|Perhaps the component has been found and the crisis is over.} + Perhaps the door is unlocked and they left me to sleep? + * * [Try it] I try the handle. No such luck. + * * [Leave it] I don't touch it. I don't want anyone outside thinking I'm trying to escape. + + * [Wait] + There is nothing I can do to speed up time. + + - The night moves at its own pace. I suppose by morning I will know my fate. + * { hooperClueType > NONE } [Wait] + // Hooper now arrested + Morning comes. I'm woken by a rooster calling from the yard behind the House. I must have slept after all. I pull myself up from the bunk, shivering slightly. There is condensation on the inside of the window. I have probably given myself a chill. + Without knocking, Harris comes inside. "You're up," he remarks, and then, "You smell like an animal." + * * [Be friendly] + "I suppose I do rather." I laugh, but Harris does not. + "This damn business gets worse and worse," he says, talking as he goes over to unlock and throw open the window. <> + * * [Be cold] + "So would you," I reply tartly. Harris shrugs. + "I've been through worse than this," he replies matter—of—factly. "It's hardly my fault if you sleep in your clothes." + I glare back. He goes over to the window, unlocks it and throws it open, relishing the fresh air from outside. + - - "Hooper's confessed, you know." + * * [Be eager] + "He has? I knew he would. The worm." + "Steady now. Matters aren't over yet. <> + * * [Be cautious] + "Oh, yes?" + "Yes. For what that's worth. <> + - - (hooper_didnt_give_himself_up) There's still the issue of the component. It hasn't turned up. He didn't lead us to it. I guess he figured you must have had something on him. I don't know." + + He looks quite put out by the whole affair. He is not the kind of man to deal well with probabilities. + * * [Be interested] + "You mean he confessed of his own accord? You didn't catch him?" + + * * [Be disinterested] + "Well, I'm glad his conscience finally caught up with him," I reply dismissively. + - - "The Captain went back into that hut and he confessed immediately. We were so surprised we didn't let you go." He wrinkles his nose. "I'm rather sorry about that now. I suggest you have a wash." + And with that he gestures to the doorway. + * * [Go] + * * [Wait] + I hang back a moment. Something does not seem quite right. After all, Hooper did not steal the component. He has no reason to confess to anything. Perhaps this is another trap? + "Well?" Harris asks. "What are you waiting for? Please don't tell me you want to confess now as well, I don't think my head could stand it." + * * * [Confess] + After a chance like this? A chance — however real — to save my neck? To hand it over — what, to save Hooper's worthless skin? + * * * * [Confess] + I see. Perhaps you think I bullied the man into giving himself up. Perhaps he understood my little clue far enough to know it was a threat against him, but not well enough to understand where he should look to find it. So he took the easy route out and folded. Gave me the hand. + ~ hooperConfessed = true + Hardly sporting, of course. + * * * * * [Confess] + Well, then. I suppose this must be what it feels like to have a conscience. I suppose I had always wondered. + "Harris, sir. I don't know what Hooper's playing at, sir. But I can't let him do this." + "Do what?" + "Take the rope for this. I took it, sir. + ~ revealedhooperasculprit = false + ~ losttemper = false + -> reveal_location_of_component + * * * * * [Don't confess] + * * * * [Don't confess] + * * * [Don't confess] + - - - "I certainly don't. But still, I'm surprised. I had Hooper down for a full—blown double agent, a traitor. He knows he'll face the rope, doesn't he?" + "Don't ask me to explain why he did what he did," Harris sighs. "Just be grateful that he did, and you're now off the hook." + - - Curiouser and curiouser. I nod once to Harris and slip outside into the cold morning air. + { hooperClueType == NONE : + Hooper's confession only makes sense in one fashion{ hooperConfessed :, and that is his being dim—witted and slow| — if I successfully implied to him that I had him framed, but he did not unpack my little clue well enough to go looking for the component. Well, I had figured him for a more intelligent opponent, but a resignation from the game will suffice}. Or perhaps he knew he would be followed if he went to check, and decided he would be doomed either way. + - else: + Hooper's confession only makes sense in one way — and that's that he believed me. He reasoned that he would be followed. To try and uncover the component would have got him arrested, and to confess was the same. + He simply caved, and threw in his hand. + } + // Outside, possibly free + Of course, however, there is only one way to be certain that Harris is telling the truth, and that is to check the breeze—block at the back of Hut 2. + * * [Check] -> go_to_where_component_is_hidden + * * [Don't check] + But there will time for that later. If there is nothing there, then Hooper discovered the component after all and Harris' men will have swooped on him, and the story about his confession is just a ruse to test me out. + And if the component is still there — well. It will be just as valuable to my contact in a week's time, and his deadline of the 31st is not yet upon us. + -> head_for_my_dorm_free + + * { hooperClueType == NONE } [Wait] -> morning_not_saved + += morning_not_saved + // Not saved + Morning comes with the call of a rooster from the yard of the House. I must have slept after all. I pull myself up off the bunk, shivering slightly. There is condensation on the inside of the window. I have probably given myself a chill. + It's not long after that Harris enters the hut. He closes the door behind him, careful as ever, then takes a chair across from me. + "You smell like a dog," he remarks. + * (optimism) [Be optimistic] + "I'm looking forward to a long bath," I reply. "And getting back to work." + * (pessimism) [Be pessimistic] + "So would you after the night I've had." + + - -> harris_certain_is_you + + +=== harris_certain_is_you + "Well, I'm afraid it is going to get worse for you," Harris replies soberly. "We followed Hooper, and he took himself neatly to bed and slept like a boy scout. Which puts us back to square one, and you firmly in the frame. And I'm afraid I don't have time for any more games. I want you to tell me where that component is, or we will hang you as a traitor." + ~ revealedhooperasculprit = false + ~ losttemper = false + -> harris_threatens_lynching + + + + +/*--------------------------------------------------------------- + Ending: they don't think it was you +---------------------------------------------------------------*/ + + +=== head_for_my_dorm_free +I head for my dorm, intent on a bath, breakfast, a glance at the crossword before the other men get to it, and then on with work. They should have replaced the component in the Bombe by now. We will only be a day behind. + { not framedhooper : + And then everything will proceed as before. The component will mean nothing to the Germans — this is the one fact I could never have explained to a man like Harris, even though the principle behind the Bombe is the same as the principle behind the army. The individual pieces — the men, the components — do not matter. They are identical. It is how they are arranged that counts. +} +I bump into Russell in the dorm hut. +"Did you hear?" he whispers. "Terrible news about Hooper. Absolutely terrible." + * [Yes] + "Quite terrible. I would never have guessed." + "Well." Russell harrumphs. + - - (quince) "Quince was saying this morning, apparently his grandfather was German. So perhaps it's to be expected. See you there?" + + * [No] + + "Heard what?" + - - (hooper_taken) "Hooper's been taken away. They caught him, uncovering that missing Bombe component from a hiding place somewhere, apparently about to take it to his contact." Russell harrumphs. -> quince + * [Lie] + "I don't know what you're talking about." + -> hooper_taken + * [Evade] + "If you'll excuse me, Russell. I was about to take a bath." + "Oh, of course. Worked all night, did you? Well, you'll hear soon enough. Can hardly hide the fact there'll only be three of us from now on." + +- I wave to him and move away, my thoughts turning to the young man in the village. My lover. My contact. My blackmailer. Hooper may have taken the fall for the missing component, but { not framedhooper :if he did recover it from Hut 2 then | its recovery does mean }I have nothing to sell to save my reputation{ i_met_a_young_man :, if I have any left}. + { not framedhooper : +If he didn't, of course, and Harris was telling the truth about his sudden confession, then I will be able to buy my freedom once and for all. +} + * { not framedhooper } [Get the component] -> go_to_where_component_is_hidden + * { not framedhooper } [Leave it] + I will have to leave that question for another day. To return there now, when they're probably watching my every step, would be suicide. After all, if Hooper { hooperClueType == STRAIGHT :followed|understood} my clue, he will have explained it to them to save his neck. They won't believe him — but they won't quite disbelieve him either. We're locked in a cycle now, him and me, of half—truth and probability. There's nothing either of us can do to put the other entirely into blame. + -> ending_return_to_normal + * [Act normal] + But there is nothing to be done about it. -> ending_return_to_normal + + + + +=== ending_return_to_normal +Nothing, that is, except to act as if there is no game being played. I'll have a bath, then start work as normal. I've got a week to find something to give my blackmailer{ i_met_a_young_man : — or give him nothing: it seems my superiors know about my indiscretions now already}. + * [Co-operate] + Something will turn up. It always does. An opportunity will present itself, and more easily now that Hooper is out of the way. + But for now, there's yesterday's intercept to be resolved. + + * [Dissemble] + Or perhaps I might hand my young blackmailer over my superiors instead for being the spy he is. + Perhaps that would be the moral thing to do, even, and not just the most smart. + But not today. Today, there's an intercept to resolve. + + * [Lie] + In a week's time, this whole affair will be in the past and quite forgotten. I'm quite sure of that. -> moreimportant + * (moreimportant) [Evade] I've more important problems to think about now. There's still yesterday's intercept to be resolved. +- The Bombe needs to be set up once more and set running. +It's time I tackled a problem I can solve. +// End - Scot Free +-> END + + +=== go_to_where_component_is_hidden + It won't take a moment to settle the matter. I can justify a walk past Hut 2 as part of my morning stroll. It will be obvious in a moment if the component is still there. + On my way across the paddocks, between the huts and the House, I catch sight of young Miss Lyon, arriving for work on her bicycle. She giggles as she sees me and waves. + * [Wave back] + I wave cheerily back and she giggles, almost drops her bicycle, then dashes away inside the House. Judging by the clock on the front gable, she's running a little late this morning. + * [Ignore her] + I give no reaction. She sighs to herself, as if this kind of behaviour is normal, and trots away inside the House to begin her duties. + - I turn the corner of Hut 3 and walk down the short gravel path to Hut 2. It was a good spot to choose — Hut 2 is where the electricians work, and they're generally focussed on what they're doing. They don't often come outside to smoke a cigarette so it's easy to slip past the doorway unnoticed. + * [Check inside] + I hop up the steps and put my head inside all the same. Nobody about. Still too early in the AM for sparks, I suppose. <> + * [Go around the back] + + - I head on around the back of the hut. The breeze—block with the cavity is on the left side. + * (check) [Check] + No time to waste. I drop to my knees and check the breeze—block. Sure enough, there's nothing there. Hooper took the bait. + Suddenly, there's a movement behind me. I look up to see, first a snub pistol, and then, Harris. + + * [Look around] + I pause to glance around, and catch a glimpse of movement. Someone ducking around the corner of the hut. Or a canvas sheet flapping in the light breeze. Impossible to be sure. + * * [Check the breeze—block] -> check + * * [Check around the side of the hut] + But too important to guess. I move back around the side of the hut. + Harris is there, leaning in against the wall. He holds a stub pistol in his hand. + + - { hooperClueType > STRAIGHT : + "{ hooperClueType == CHESS:Queen to rook two|Messy without one missing whatever it was}," he declares. "I wouldn't have fathomed it but Hooper did. Explained it right after we sprung him doing what you're doing now. We weren't sure what to believe but now, you seem to have resolved that for us." + - else: + "Hooper said you'd told him where to look. I didn't believe him. Or, well. I wasn't sure what to believe. Now I rather think you've settled it." + } + * [Agree] + "I have, rather." I put my hands into my pockets. "I seem to have done exactly that." + "I'm afraid my little story about Hooper confessing wasn't true. I wanted to see if you'd go to retrieve the part." Harris gestures me to start walking. "You were close, Manning, I'll give you that. I wanted to believe you. But I'm glad I didn't." + -> done + * [Lie] + "I spoke to Russell. He said he saw Hooper doing something round here. I wanted to see what it was." + + * [Evade] + "Harris, you'd better watch out. He's planted a time—bomb here." + Harris stares at me for a moment, then laughs. "Oh, goodness. That's rich." + I almost wish I had a way to make the hut explode, but of course I don't. + + - "Enough." Harris gestures for me to start walking. "This story couldn't be simpler. You took it to cover your back. You hid it. You lied to get Hooper into trouble, and when you thought you'd won, you came to scoop your prize. A good hand but ultimately, { hooperClueType <= STRAIGHT :if it hadn't have been you who hid the component, then you wouldn't be here now|you told Hooper where to look with your little riddle}." + + - (done) + // End - Caught in AM + He leads me across the yard. Back towards Hut 5 to be decoded, and taken to pieces, once again. + -> END + + +/*--------------------------------------------------------------- + Ending: they think it was you +---------------------------------------------------------------*/ + +=== harris_threatens_lynching + { harris_certain_is_you:He passes a hand across his eyes with a long look of despair.|He gets to his feet, and gathers his gloves from the table top.} + "I'm going to go outside and organise a rope. That'll take about twelve minutes. That's how long you have to decide." + * [Protest] + "You can't do this!" I cry. "It's murder! I demand a trial, a lawyer; for God's sake, man, you can't just throw me overboard, we're not barbarians...!" + - - (too_clever) "You leave me no choice," Harris snaps back, eyes cold as gun—metal. "You and your damn cyphers. Your damn clever problems. If men like you didn't exist, if we could just all be straight with one another." He gets to his feet and heads for the door. "I fear for the future of this world, with men like you in. Reich or no Reich, Mr Manning, people like you simply complicate matters." + -> left_alone + * { not gotcomponent && not throwncomponentaway } [Confess] + I nod. "I don't need twelve minutes. -> reveal_location_of_component + * [Stay silent] -> my_lips_are_sealed + * { gotcomponent } [Show him the component] + "I don't need twelve minutes. Here it is." + I open my jacket and pull the Bombe component out of my pocket. Harris takes it from me, whistling, curious. + "Well, I'll be. That's it all right." + "That's it." + "But you didn't have it on you yesterday." + * * [Explain] + "I climbed out of the window overnight," I explain. "I went and got this from where it was hidden, and brought it back here." + * * [Don't explain] + "No. I didn't." + - -> all_too_farfetched + + * { throwncomponentaway } [Confess] + "I don't need twelve minutes. The component is in the long grass behind Hooper's tent. I threw it there hoping to somehow frame him, but now I see that won't be possible. I was naive, I suppose." + ~ piecereturned = true + -> reveal_location_of_component.harris_believes + + * { throwncomponentaway } [Frame Hooper] + "Look, I know where it is. The missing piece of the Bombe is in the long grasses behind Hooper's tent. I saw him throw it there right after we finished work. He knew you'd scour the camp but I suppose he thought you'd more obvious places first. I suppose he was right about that. Look there. That proves his guilt." + ~ longgrasshooperframe = true + ~ piecereturned = true + "That doesn't prove anything," Harris returns sharply. "But we'll check what you say, all the same." He gets to his feet and heads out of the door. + -> left_alone + + + +=== reveal_location_of_component + <> The missing component of the Bombe computer is hidden in a small cavity in a breeze—block supporting the left rear post of Hut 2. I put in there anticipating a search. I intended to { revealedhooperasculprit:pass it to Hooper|dispose of it} once the fuss had died down. I suppose I was foolish to think that it might." + ~ piecereturned = true + -> harris_believes += harris_believes + { not night_falls.hooper_didnt_give_himself_up : + "Indeed. And Mr Manning: God help you if you're lying to me." + - else: + "I thought as much. I hadn't expected you to give it out so easily, however. You understand, Hooper has said nothing, of course. In fact, he went to Hut 2 directly after we released him and uncovered the component. But he told us you had instructed him where to go. Hence my little double bluff. Frankly, I'll be glad when I'm shot of the lot of you mathematicians." + } + Harris stands, and slips away smartly. -> left_alone + + + +=== my_lips_are_sealed + I say nothing, my lips tightly, firmly sealed. It's true I am a traitor, to the very laws of nature. The world has taught me that since a very early age. But not to my country — should the Reich win this war, I would hardly be treated as an honoured hero. I was doomed from the very start. + ~ notraitor = true + I explain none of this. How could a man like Harris understand? + The Commander takes one look back from the doorway as he pulls it to. + "It's been a pleasure working with you, Mr Manning," he declares. "You've done a great service to this country. If we come through, I'm sure they'll remember you name. I'm sorry it had to end this way and I'll do my best to keep it quiet. No—one need know what you did." + -> left_alone + + + + +=== all_too_farfetched + // Returned Component + "This is all too far—fetched," Harris says. "I'm glad to have this back, but I need to think." + Getting to his feet, he nods once. "You'll have to wait a little longer, I'm afraid, Manning." + Then he steps out of the door, muttering to himself. + -> make_your_peace + + + +=== left_alone + // Alone, about to die + { slam_door_shut_and_gone.time_to_move_now :The Commander holds the door for his superior, and follows him out.} Then the door closes. I am alone again, as I have been for most of my short life. + -> make_your_peace + + +=== make_your_peace + * [Make your peace] + - I am waiting again. I have no God to make my peace with. I find it difficult to believe in goodness of any kind, in a world such as this. + { not notraitor: + ~ notraitor = true + But I am no traitor. Not to my country. To my sex, perhaps. But how could I support the Reich? If the Nazis were to come to power, I would be worse off than ever. + } + { harris_threatens_lynching.too_clever: + In truth, it is men like Harris who are complex, not men like me. I live to make things ordered, systematic. I like my pencils sharpened and lined up in a row. I do not deal in difficult borders, or uncertainties, or alliances. If I could, I would reduce the world to something easier to understand, something finite. + But I cannot, not even here, in our little haven from the horrors of the war. + } + I have no place here. No way to fit. I am caught, in the middle, cryptic and understood only thinly, through my machines. + * I must seem very calm. + * Perhaps I should try to escape.[] But escape to where? I am already a prisoner. Jail would be a blessing. -> monastic + - <> I suppose I do not believe they will hang me. They will lock me up and continue to use my brain, if they can. I wonder what they will tell the world — perhaps that I have taken my own life. That would be simplest. The few who know me would believe it. + Well, then. Not a bad existence, in prison. Removed from temptation. + - (monastic) A monastic life, with plenty of problems to keep me going. + I wonder what else I might yet unravel before I'm done? + * The door is opening.[] Harris is returning. Our little calculation here is complete. { not piecereturned: I can only hope one of the others will be able to explain to him that the part I stole will mean nothing to the Germans.|We are just pieces in this machine; interchangeable and prone to wear.} + - That is the true secret of the calculating engine, and the source of its power. It is not the components that matter, they are quite repetitive. What matters is how they are wired; the diversity of the patterns and structures they can form. Much like people — it is how they connect that determines our victories and tragedies, and not their genius. + Which makes me wonder. Should I give { i_met_a_young_man :up my beautiful young man|the young man who put me in this spot} to them as well as myself? + * [Yes] + But of course I will. { forceful > 2:Perhaps I can persuade them to put him in my cell.|A little vengeance, disguised as doing something good.} + * [No] + No. What would be the use? He will be long gone, and the name he told me is no doubt hokum. No: I was alone before in guilt, and I am thus alone again. + * [Lie] + No. Why would I? He is no doubt an innocent himself, trapped by some dire circumstance. Forced to act the way he did. I have every sympathy for him. + Of course I do. + * [Evade] + It depends, perhaps, on what his name his worth. If it were to prove valuable, well; perhaps I can concoct a few more such lovers with which to ease my later days. + { hooper_mentioned: Hooper, perhaps. He wouldn't like that. } + - { not longgrasshooperframe : + Harris put the cuffs around my wrists. "I still have the intercept in my pocket," I remark. "Wherever we're going, could I have a pencil?" + - else: + "We recovered the part, just where you said it was," Harris reports, as he puts the cuffs around my wrists. "Of course, a couple of the men swear blind they searched there yesterday, so I'm afraid, what with the broken window... we've formed a perfectly good theory which doesn't bode well for you." + } + ~ piecereturned = true + { longgrasshooperframe : + "I see." It doesn't seem worth arguing any further. "I still have the intercept in my pocket," I remark. "Wherever we're going, could I have a pencil?" + } + He looks me in the eye. + { not losttemper : + "Of course. And one of your computing things, if I get my way. And when we're old, and smoking pipes together in The Rag like heroes, I'll explain to you the way that decent men have affairs. + - else: + "I'll give you a stone to chisel notches in the wall. And that's all the calculations you'll be doing. And as you sit there, pissing into a bucket and growing a beard down to your toes, you have a think about how a smart man would conduct his illicit affairs. With a bit of due decorum you could have learnt off any squaddie. + } + <> You scientists." + He drags me up to my feet. + "You think you have to re—invent everything." + With that, he hustles me out of the door and I can't help thinking that, with a little more strategy, I could still have won the day. But too late now, of course. + -> END diff --git a/cli-player/tests/data/TheIntercept.ink.json b/cli-player/tests/data/TheIntercept.ink.json new file mode 100644 index 0000000..507a739 --- /dev/null +++ b/cli-player/tests/data/TheIntercept.ink.json @@ -0,0 +1 @@ +{"inkVersion":20,"root":[["ev",{"VAR?":"DEBUG"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^IN DEBUG MODE!","\n","ev","str","^Beginning...","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Framing Hooper...","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^In with Hooper...","/str","/ev",{"*":".^.c-2","flg":20},{"->":"0.5"},{"c-0":["^\t",{"->":"start"},"\n",{"#f":5}],"c-1":["^ ",{"->":"claim_hooper_took_component"},"\n",{"#f":5}],"c-2":["^ ",{"->":"inside_hoopers_hut"},"\n",{"#f":5}]}]}],[{"->":".^.b"},{"b":["\n",{"->":"start"},{"->":"0.5"},null]}],"nop","\n",["done",{"#f":5,"#n":"g-0"}],null],"done",{"lower":[{"temp=":"x"},"ev",{"VAR?":"x"},1,"-","/ev",{"temp=":"x","re":true},{"#f":1}],"raise":[{"temp=":"x"},"ev",{"VAR?":"x"},1,"+","/ev",{"temp=":"x","re":true},{"#f":1}],"start":[[["^They are keeping me waiting.","\n",["ev",{"^->":"start.0.g-0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^Hut 14",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"start.0.g-0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"^. The door was locked after I sat down. ","\n","^I don't even have a pen to do any work. There's a copy of the morning's intercept in my pocket, but staring at the jumbled letters will only drive me mad.","\n","^I am not a machine, whatever they say about me.","\n",{"->":".^.^.^.opts"},{"#f":5}],"#f":5,"#n":"g-0"}],{"opts":[["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","^I rattle my fingers on the field table.",{"->":".^.^.23"},null],"s2":["pop",{"->":".^.^.23"},null],"#f":5}],"\n","ev","str","^Think","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Plan","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Wait","/str","/ev",{"*":".^.c-3","flg":20},{"c-1":["^ ","\n","^They suspect me to be a traitor. They think I stole the component from the calculating machine. They will be searching my bunk and cases.","\n","^When they don't find it, ","ev",{"CNT?":".^.^.c-2"},"/ev",[{"->":".^.b","c":true},{"b":["^then",{"->":".^.^.^.9"},null]}],"nop","^ they'll come back and demand I talk.","\n",{"->":".^.^"},{"->":".^.^.^.g-1"},{"#f":5}],"c-2":["\n","ev",{"CNT?":".^.^.c-1"},"!","/ev",[{"->":".^.b","c":true},{"b":["^What I am is",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^I am",{"->":".^.^.^.7"},null]}],"nop","^ a problem—solver. Good with figures, quick with crosswords, excellent at chess.","\n","^But in this scenario — in this trap — what is the winning play?","\n",["ev","str","^Co—operate","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Dissemble","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Divert","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^I must co—operate. My credibility is my main asset. To contradict myself, or another source, would be fatal.","\n","^I must simply hope they do not ask the questions I do not want to answer.","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n",{"->":"start.0.g-1"},{"#f":5}],"c-1":["^ ","\n","^Misinformation, then. Just as the war in Europe is one of plans and interceptions, not planes and bombs.","\n","^My best hope is a story they prefer to the truth.","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":"start.0.g-1"},{"#f":5}],"c-2":["^ ","\n","^Avoidance and delay. The military machine never fights on a single front. If I move slowly enough, things will resolve themselves some other way, my reputation intact.","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":"start.0.g-1"},{"#f":5}]}],{"#f":5}],"c-3":["^\t\t","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":[{"->":"start.waited"},{"#f":5}]}],{"waited":[[["^Half an hour goes by before Commander Harris returns. He closes the door behind him quickly, as though afraid a loose word might slip inside.","\n","^\"Well, then,\" he begins, awkwardly. This is an unseemly situation.","\n",["ev",{"^->":"start.waited.0.g-0.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"Commander.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"start.waited.0.g-0.5.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",{"CNT?":"start.0.opts.c-2.12.c-2"},"!","/ev",{"*":".^.^.c-1","flg":19},{"s":["^\"Tell me what this is about.\"",{"->":"$r","var":true},null]}],"ev","str","^Wait","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["ev",{"^->":"start.waited.0.g-0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.4.s"},[{"#n":"$r2"}],"\n","^He nods. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-1":["ev",{"^->":"start.waited.0.g-0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.5.s"},[{"#n":"$r2"}],"\n","^He shakes his head.","\n","^\"Now, don't let's pretend.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-2":["\n","^I say nothing.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5,"#n":"g-0"}],{"g-1":["^He has brought two cups of tea in metal mugs: he sets them down on the tabletop between us.","\n","ev","str","^Deny","/str",{"CNT?":".^.^.g-0.c-1"},"/ev",{"*":".^.c-3","flg":21},"ev","str","^Take one","/str","/ev",{"*":".^.c-4","flg":20},["ev",{"^->":"start.waited.0.g-1.15.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",{"CNT?":".^.^.^.g-0.c-1"},"!","/ev",{"*":".^.^.c-5","flg":19},{"s":["^\"What's going on?\"",{"->":"$r","var":true},null]}],"ev","str","^Wait","/str","/ev",{"*":".^.c-6","flg":20},{"c-3":["^ \"I'm not pretending anything.\"","\n","ev",{"CNT?":"start.0.opts.c-2.12.c-0"},"/ev",[{"->":".^.b","c":true},{"b":["^I'm lying already, despite my good intentions.",{"->":".^.^.^.6"},null]}],"nop","\n","^Harris looks disapproving. ",{"->":".^.^.c-6.3.pushes_cup"},"\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-4":["\n","ev",true,"/ev",{"VAR=":"teacup","re":true},"^I take a mug and warm my hands. It's ","<>","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-5":["ev",{"^->":"start.waited.0.g-1.c-5.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.15.s"},[{"#n":"$r2"}],"\n","^\"You know already.\"","\n",{"->":".^.^.c-6.3.pushes_cup"},{"->":".^.^.^.g-2"},{"#f":5}],"c-6":["\n","^I wait for him to speak.","\n",[["^He pushes one mug halfway towards me: ","<>","\n",{"->":".^.^.^.^.^.g-2"},{"#f":5,"#n":"pushes_cup"}],null],{"#f":5}],"#f":5}],"g-2":["^a small gesture of friendship.","\n","^Enough to give me hope?","\n","ev","str","^Take it","/str",{"VAR?":"teacup"},"!","/ev",{"*":".^.c-7","flg":21},"ev","str","^Don't take it","/str",{"VAR?":"teacup"},"!","/ev",{"*":".^.c-8","flg":21},"ev","str","^Drink","/str",{"VAR?":"teacup"},"/ev",{"*":".^.c-9","flg":21},"ev","str","^Wait","/str",{"VAR?":"teacup"},"/ev",{"*":".^.c-10","flg":21},{"c-7":["^ ","\n","^I ","ev",{"CNT?":".^.^.^.g-1.c-4"},"/ev",[{"->":".^.b","c":true},{"b":["^lift the mug",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["^take the mug,",{"->":".^.^.^.8"},null]}],"nop","^ and blow away the steam. It is too hot to drink.","\n","^Harris picks his own up and just holds it.","\n","ev",true,"/ev",{"VAR=":"teacup","re":true},"ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-8":["^ ","\n","^Just a cup of insipid canteen tea. I leave it where it is.","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-9":["^ ","\n","^I raise the cup to my mouth but it's too hot to drink.","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-10":["^ \t\t","\n","^I say nothing as ",{"->":".^.^.c-7"},"\n",{"->":".^.^.^.g-3"},{"#f":5}],"#f":5}],"g-3":["^\"Quite a difficult situation,\" ","ev",{"CNT?":".^.^.g-2.c-7"},"/ev",[{"->":".^.b","c":true},{"b":["^he",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["^Harris",{"->":".^.^.^.6"},null]}],"nop","^ begins","ev",{"VAR?":"forceful"},0,"<=","/ev",[{"->":".^.b","c":true},{"b":["^, sternly",{"->":".^.^.^.14"},null]}],"nop","^. I've seen him adopt this stiff tone of voice before, but only when talking to the brass. \"I'm sure you agree.\"","\n","ev","str","^Agree","/str","/ev",{"*":".^.c-11","flg":20},"ev","str","^Disagree","/str","/ev",{"*":".^.c-12","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-13","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-14","flg":20},{"c-11":["^ ","\n","^\"Awkward,\" I reply","\n",{"->":".^.^.^.g-4"},{"#f":5}],"c-12":["^ ","\n","^\"I don't see why,\" I reply","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.^.g-4"},{"#f":5}],"c-13":["^ ",{"->":".^.^.c-12"},"\n",{"->":".^.^.^.g-4"},{"#f":5}],"c-14":["^ ","\n","^\"I'm sure you've handled worse,\" I reply casually","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.^.g-4"},{"#f":5}],"#f":5}],"g-4":["ev",{"VAR?":"teacup"},"/ev",[{"->":".^.b","c":true},{"b":["\n","ev",true,"/ev",{"VAR=":"drugged","re":true},"<>","^, sipping at my tea as though we were old friends","\n",{"->":".^.^.^.4"},null]}],"nop","\n","<>","^.","\n",["ev","str","^Watch him","/str","/ev",{"*":".^.c-15","flg":20},"ev","str","^Wait","/str","/ev",{"*":".^.c-16","flg":20},"ev","str","^Smile","/str",{"CNT?":".^.^.^.g-3.c-12"},"!","/ev",{"*":".^.c-17","flg":21},{"c-15":["\n","^His face is telling me nothing. I've seen Harris broad and full of laughter. Today he is tight, as much part of the military machine as the device in Hut 5.","\n",{"->":".^.^.^.^.g-6"},{"#f":5}],"c-16":["\n","^I wait to see how he'll respond.","\n",{"->":".^.^.^.^.g-6"},{"#f":5}],"c-17":["\n","^I try a weak smile. It is not returned.","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n",{"->":".^.^.^.^.g-6"},{"#f":5}],"#f":5,"#n":"g-5"}],{"#f":5}],"g-6":["^\"We need that component,\" he says.","\n",["ev",{"CNT?":"missing_reel"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->t->":"missing_reel"},{"->":"harris_demands_component"},{"->":".^.^.^.5"},null]}],"nop","\n",["ev","str","^Yes","/str","/ev",{"*":".^.c-18","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-19","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-20","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-21","flg":20},{"c-18":["\n","^\"Of course I do,\" I answer.","\n",{"->":".^.^.^.^.^.g-9"},{"#f":5}],"c-19":["\n","^\"No I don't. And I've got work to do...\"","\n","^\"Work that will be rather difficult for you to do, don't you think?\" Harris interrupts.","\n",{"->":".^.^.^.^.^.g-9"},{"#f":5}],"c-20":["\n",{"->":"here_at_bletchley_diversion"},{"->":".^.^.^.^.^.g-9"},{"#f":5}],"c-21":["^ ","\n",{"->":".^.^.c-19"},{"->":".^.^.^.^.^.g-9"},{"#f":5}],"#f":5,"#n":"g-8"}],{"#f":5,"#n":"g-7"}],{"#f":5}],"g-9":[{"->t->":"missing_reel"},{"->":"harris_demands_component"},{"#f":5}]}],{"#f":1}],"#f":1}],"missing_reel":[["ev","str","^The stolen component...","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Shrug","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["\n","^I shrug.","\n","ev","void","/ev","->->",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^The reel went missing from the Bombe this afternoon. The four of us were in the Hut, working on the latest German intercept. The results were garbage. It was Russell who found the gap in the plugboard.","\n",["^Any of us could have taken it; and no one else would have known its worth.","\n","ev","str","^Panic","/str",{"VAR?":"forceful"},0,"<=","/ev",{"*":".^.c-2","flg":21},"ev","str","^Calculate","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Deny","/str",{"VAR?":"evasive"},0,">=","/ev",{"*":".^.c-4","flg":21},{"c-2":["^ They will pin it on me. They need a scapegoat so that the work can continue. I'm a likely target. Weaker than the rest. ","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n",{"->":".^.^.^.^.g-2"},{"#f":5}],"c-3":["^ My odds, then, are one in four. Not bad; although the stakes themselves are higher than I would like.","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.^.^.g-2"},{"#f":5}],"c-4":["^ But this is still a mere formality. The work will not stop. A replacement component will be made and we will all be put back to work. We are too valuable to shoot. ","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.^.^.g-2"},{"#f":5}],"#f":5,"#n":"g-1"}],{"#f":5}],"g-2":["ev","void","/ev","->->",{"#f":5}]}],{"#f":1}],"here_at_bletchley_diversion":[["^\"Here at Bletchley? Of course.\"","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n","^\"Here, now,\" Harris corrects. \"We are not talking to everyone. I can imagine you might feel pretty sore about that. I can imagine you feeling picked on. ","ev",{"VAR?":"forceful"},0,"<","/ev",[{"->":".^.b","c":true},{"b":["^You're a sensitive soul.",{"->":".^.^.^.21"},null]}],"nop","^\"","\n",["ev",{"^->":"here_at_bletchley_diversion.0.24.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str","/ev",{"*":".^.^.c-0","flg":22},{"s":["^\"I'm fine",{"->":"$r","var":true},null]}],["ev",{"^->":"here_at_bletchley_diversion.0.25.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",{"VAR?":"forceful"},0,"<","/ev",{"*":".^.^.c-1","flg":19},{"s":["^\"What do you mean by that?\"",{"->":"$r","var":true},null]}],["ev",{"^->":"here_at_bletchley_diversion.0.26.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str",{"VAR?":"forceful"},0,">=","/ev",{"*":".^.^.c-2","flg":23},{"s":["^\"Damn right",{"->":"$r","var":true},null]}],"ev","str","^Be honest","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-4","flg":20},{"c-0":["ev",{"^->":"here_at_bletchley_diversion.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.24.s"},[{"#n":"$r2"}],"^,\" I reply. \"This is all some misunderstanding and the quicker we have it cleared up the better.\"","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n","^\"I couldn't agree more.\" And then he comes right out with it, with an accusation.","\n",{"->":".^.^.done"},{"#f":5}],"c-1":["ev",{"^->":"here_at_bletchley_diversion.0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.25.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.done"},{"#f":5}],"c-2":["ev",{"^->":"here_at_bletchley_diversion.0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.26.s"},[{"#n":"$r2"}],"^ I'm sore. Was it one of the others who put you up to this? Was it Hooper? He's always been jealous of me. He's...\"","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n","ev",true,"/ev",{"VAR=":"hooper_mentioned","re":true},"^The Commander moustache bristles as he purses his lips. \"Has he now? Of your achievements, do you think?\"","\n","^It's difficult not to shake the sense that he's ","ev",{"VAR?":"evasive"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^mocking",{"->":".^.^.^.28"},null]}],[{"->":".^.b"},{"b":["^simply humouring",{"->":".^.^.^.28"},null]}],"nop","^ me.","\n","^\"Or of your brain? Or something else?\"","\n",[["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-0","flg":22},{"s":["^\"Of my genius.",{"->":"$r","var":true},null]}],["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-1","flg":22},{"s":["^\"Of my standing.",{"->":"$r","var":true},null]}],"ev","str","^Evade","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"^ Hooper simply can't stand that I'm cleverer than he is. We work so closely together, cooped up in that Hut all day. It drives him to distraction. To worse.\"","\n","^\"You're suggesting Hooper would sabotage this country's future simply to spite you?\" Harris chooses his words like the military man he is, each lining up to create a ring around me.","\n",["ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ \t\t\t","\n","^\"","ev",{"VAR?":"forceful"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^He's petty enough, certainly",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^I wouldn't put it past him",{"->":".^.^.^.10"},null]}],"nop","^. He's a creep.\" ","ev",{"VAR?":"teacup"},"/ev",[{"->":".^.b","c":true},{"b":["^ I set the teacup down.",{"->":".^.^.^.17"},null]}],[{"->":".^.b"},{"b":["^I wipe a hand across my forehead.",{"->":".^.^.^.17"},null]}],"nop","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n","ev",false,"/ev",{"VAR=":"teacup","re":true},{"->":".^.^.suggest_its_a_lie"},{"#f":5}],"c-1":["^ \t\t\t","\n","^\"No, ","ev",{"VAR?":"forceful"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^of course not",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^I suppose not",{"->":".^.^.^.10"},null]}],"nop","^.\" ","ev",{"VAR?":"teacup"},"/ev",[{"->":".^.b","c":true},{"b":["^I put the teacup back down on the table",{"->":".^.^.^.17"},null]}],[{"->":".^.b"},{"b":["^I push the teacup around on its base",{"->":".^.^.^.17"},null]}],"nop","^.","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n","ev",false,"/ev",{"VAR=":"teacup","re":true},{"->":".^.^.suggest_its_a_lie"},{"#f":5}],"c-2":["^ \t\t","\n","^\"I don't know what I'm suggesting. I don't understand what's going on.\"","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n","^\"But of course you do.\" Harris narrows his eyes.","\n",{"->":".^.^.^.^.^.^.done"},{"->":".^.^.suggest_its_a_lie"},{"#f":5}],"suggest_its_a_lie":["^\"All I can say is, ever since I arrived here, he's been looking to ways to bring me down a peg. I wouldn't be surprised if he set this whole affair up just to have me court—martialled.\"","\n","^\"We don't court—martial civilians,\" Harris replies. \"Traitors are simply hung at her Majesty's pleasure.\"","\n",["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.c-0.10.suggest_its_a_lie.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str","/ev",{"*":".^.^.c-3","flg":22},{"s":["^\"Quite right",{"->":"$r","var":true},null]}],["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.c-0.10.suggest_its_a_lie.5.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str","/ev",{"*":".^.^.c-4","flg":22},{"s":["^\"I'm no traitor",{"->":"$r","var":true},null]}],"ev","str","^Lie","/str","/ev",{"*":".^.c-5","flg":20},{"c-3":["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.c-0.10.suggest_its_a_lie.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.4.s"},[{"#n":"$r2"}],"^,\" I answer smartly.","\n",{"->":".^.^.^.g-0"},{"#f":5}],"c-4":["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.c-0.10.suggest_its_a_lie.c-4.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.5.s"},[{"#n":"$r2"}],"^,\" I answer","ev",{"VAR?":"forceful"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^smartly",{"->":".^.^.^.14"},null]}],[{"->":".^.b"},{"b":["^, voice quivering. \"For God's sake!\"",{"->":".^.^.^.14"},null]}],"nop","\n",{"->":".^.^.^.g-0"},{"#f":5}],"c-5":["^ ",{"->":".^.^.c-4"},"\n",{"->":".^.^.^.g-0"},{"#f":5}],"#f":5}],"g-0":["^He stares back at me.","\n",{"->":".^.^.^.^.^.^.done"},{"#f":5}]}],{"#f":5}],"c-1":["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"^ My reputation.\" ","ev",{"VAR?":"forceful"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^I'm aware of how arrogant I must sound but I plough on all the same.",{"->":".^.^.^.14"},null]}],[{"->":".^.b"},{"b":["^I don't like to talk of myself like this, but I carry on all the same.",{"->":".^.^.^.14"},null]}],"nop","^ \"Hooper simply can't bear knowing that, once all this is over, I'll be the one receiving the knighthood and he...\"","\n","^\"No—one will be getting a knighthood if the Germans make landfall,\" Harris answers sharply. He casts a quick eye to the door of the Hut to check the latch is still down, then continues in more of a murmur: \"Not you and not Hooper. Now answer me.\"","\n","^For the first time since the door closed, I wonder what the threat might be if I do not.","\n",{"->":".^.^.^.^.done"},{"#f":5}],"c-2":["^ \t\t\t\t","\n","ev",false,"/ev",{"VAR=":"teacup","re":true},"ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n","^\"How should I know?\" I reply, defensively. ","ev",{"VAR?":"teacup"},"/ev",[{"->":".^.b","c":true},{"b":["^I set the teacup back on the table.",{"->":".^.^.^.17"},null]}],"nop","^ ",{"->":".^.^.c-0.10.suggest_its_a_lie"},"\n",{"->":".^.^.^.^.done"},{"#f":5}]}],{"#f":5}],"c-3":["^ \t",{"->":".^.^.c-2"},"\n",{"->":".^.^.done"},{"#f":5}],"c-4":["^ \t\t",{"->":".^.^.c-0"},"\n",{"->":".^.^.done"},{"#f":5}],"done":[{"->":"harris_demands_component"},{"#f":5}]}],{"#f":1}],"harris_demands_component":[["^\"","ev",{"CNT?":"here_at_bletchley_diversion"},"/ev",[{"->":".^.b","c":true},{"b":["^Please",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["^So",{"->":".^.^.^.6"},null]}],"nop","^. Do you have it?\" Harris is ","ev",{"VAR?":"forceful"},3,">","/ev",[{"->":".^.b","c":true},{"b":["^sweating slightly",{"->":".^.^.^.15"},null]}],[{"->":".^.b"},{"b":["^wasting no time",{"->":".^.^.^.15"},null]}],"nop","^: Bletchley is his watch. \"Do you know where it is?\"","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["\n","^\"I do.\"","\n",{"->":"admitted_to_something"},{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ \"I have no idea.\" ","\n",{"->":".^.^.silence"},{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ \t\t",{"->":".^.^.c-1"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ \t\t","\n","^\"The component?\"","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n","^\"Don't play stupid,\" he replies. \"","ev",{"CNT?":"missing_reel"},"!","/ev",[{"->":".^.b","c":true},{"b":["^The component that went missing this afternoon. ",{"->":".^.^.^.22"},null]}],"nop","^Where is it?\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["ev",{"CNT?":"missing_reel"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->t->":"missing_reel"},{"->":".^.^.^.5"},null]}],"nop","\n","ev","str","^Co-operate","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Delay","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-7","flg":20},{"c-4":["^ \"I know where it is.\"","\n",{"->":"admitted_to_something"},{"->":".^.^.^.silence"},{"#f":5}],"c-5":["^ \"I know nothing about it.\" My voice shakes","ev",{"VAR?":"forceful"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^ with anger",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["^; I'm unaccustomed to facing off against men with holstered guns",{"->":".^.^.^.8"},null]}],"nop","^. ","\n",{"->":".^.^.^.silence"},{"#f":5}],"c-6":["^ ",{"->":".^.^.c-5"},"\n",{"->":".^.^.^.silence"},{"#f":5}],"c-7":["^ ","\n","^\"I don't know what gives you the right to pick on me. ","ev",{"VAR?":"forceful"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^I demand a lawyer.",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^I want a lawyer.",{"->":".^.^.^.10"},null]}],"nop","^\"","\n","^\"This is time of war,\" Harris answers. \"And by God, if I have to shoot you to recover the component, I will. Understand?\" He points at the mug, ",{"->":".^.^.^.silence.drinkit"},"\n",{"->":".^.^.^.silence"},{"#f":5}],"#f":5}],"silence":["^There's an icy silence. ","ev",{"VAR?":"forceful"},2,">","/ev",[{"->":".^.b","c":true},{"b":["^I've cracked him a little.",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["ev",{"VAR?":"evasive"},2,">","/ev",[{"->":".^.b","c":true},{"b":["^He's tiring of my evasiveness.",{"->":".^.^.^.6"},null]}],"nop",{"->":".^.^.^.8"},null]}],"nop","\n",["^\"Now drink your tea and talk.\"","\n","ev","str","^Drink","/str",{"VAR?":"teacup"},"/ev",{"*":".^.c-8","flg":21},"ev","str","^Put the cup down","/str",{"VAR?":"teacup"},"/ev",{"*":".^.c-9","flg":21},"ev","str","^Take the cup","/str",{"VAR?":"teacup"},"!","/ev",{"*":".^.c-10","flg":21},"ev","str","^Don't take it","/str",{"VAR?":"teacup"},"!","/ev",{"*":".^.c-11","flg":21},{"c-8":["^ \t\t\t",{"->":".^.^.c-10.2.drinkfromcup"},"\n",{"->":".^.^.^.^.g-1"},{"#f":5}],"c-9":["^ ","\n","^I set the cup carefully down on the table once more.","\n","ev",false,"/ev",{"VAR=":"teacup","re":true},"ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.c-11.10.whatsinit"},{"->":".^.^.^.^.g-1"},{"#f":5}],"c-10":["^ ","\n",[["^I lift the cup ","ev",{"VAR?":"teacup"},"/ev",[{"->":".^.b","c":true},{"b":["^to my lips ",{"->":".^.^.^.5"},null]}],"nop","^and sip. He waits for me to swallow before speaking again.","\n","ev",true,"/ev",{"VAR=":"drugged","re":true},"ev",true,"/ev",{"VAR=":"teacup","re":true},{"->":".^.^.^.^.^.^.g-1"},{"#f":5,"#n":"drinkfromcup"}],null],{"#f":5}],"c-11":["^ ","\n","^I leave the cup where it is.","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",[["^\"Why?\" I ask coldly. \"What's in it?\"","\n",{"->":".^.^.^.^.^.^.g-1"},{"#f":5,"#n":"whatsinit"}],null],{"#f":5}],"#f":5,"#n":"drinkit"}],{"#f":5}],"g-1":["^\"Lapsang Souchong,\" he ","ev",{"CNT?":".^.^.silence.drinkit.c-10.2.drinkfromcup"},"/ev",[{"->":".^.b","c":true},{"b":["^remarks",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["^replies",{"->":".^.^.^.6"},null]}],"nop","^, placing his own cup back on the table untouched. \"Such a curious flavour. It might almost not be tea at all. You might say it hides a multitude of sins. As do you. Isn't that right?\"","\n","ev","str","^Agree","/str","/ev",{"*":".^.c-12","flg":20},"ev","str","^Disagree","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-13","flg":21},"ev","str","^Disagree","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-14","flg":21},"ev","str","^Lie","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-15","flg":21},"ev","str","^Lie","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-16","flg":21},"ev","str","^Evade","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-17","flg":21},"ev","str","^Evade","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-18","flg":21},{"c-12":["^ ","\n","^\"I suppose so,\" I reply. \"I've done things I shouldn't have done.\"","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n",{"->":"harris_presses_for_details"},{"#f":5}],"c-13":["\n","^\"I've done nothing that I'm ashamed of.\"","\n",{"->":"harris_asks_for_theory"},{"#f":5}],"c-14":["^ ","\n","^I open my mouth to disagree, but the words I want won't come. It is like Harris has taken a screwdriver to the sides of my jaw.","\n",{"->":"admitted_to_something.ive_done_things"},{"#f":5}],"c-15":["^ \t",{"->":".^.^.c-14"},"\n",{"#f":5}],"c-16":["^ \t",{"->":".^.^.c-13"},"\n",{"#f":5}],"c-17":["^ ",{"->":".^.^.c-14"},"\n",{"#f":5}],"c-18":["^ ","\n","^\"None of us are blameless, Harris. ","ev",{"VAR?":"forceful"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^But you're not my priest and I'm not yours",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^But I've done nothing to deserve this treatment",{"->":".^.^.^.10"},null]}],"nop","^. Now, please. Let me go. I'll help you find this damn component, of course I will.\"","\n","^He appears to consider the offer.","\n",{"->":"harris_asks_for_theory"},{"#f":5}],"#f":5}]}],{"#f":1}],"harris_presses_for_details":[["^\"You mean you've left yourself open,\" Harris answers. \"To pressure. Is that what you're saying?\"","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-1","flg":21},"ev","str","^No","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-2","flg":21},"ev","str","^Evade","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-3","flg":21},"ev","str","^Evade","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-4","flg":21},{"c-0":["^ ",{"->":".^.^.^.admit_open_to_pressure"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"I'm not saying anything of the sort,\" I snap back. \"What is this, Harris? You're accusing me of treachery but I don't see a shred of evidence for it! Why don't you put your cards on the table?\"","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^I shake my head violently, to say no, that's not it, but whatever is wrong with tongue is wrong with neck too. I look across at the table at Harris' face and realise with a start how sympathetic he is. Such a kind, generous man. How can I hold anything back from him?","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n","^I take another mouthful of the bitter, strange—tasting tea before answering.","\n",{"->":".^.^.^.admit_open_to_pressure"},{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ ","\n","^\"You're the one applying pressure here,\" I answer ","ev",{"VAR?":"forceful"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^smartly",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^somewhat miserably",{"->":".^.^.^.10"},null]}],"nop","^. \"I'm just waiting until you tell me what is really going on.\"","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.g-0"},{"#f":5}],"c-4":["^ \t\t\t\t ","\n","^\"We're all under pressure here.\"","\n","^He looks at me with pity. ",{"->":"harris_has_seen_it_before"},"\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"It's simple enough,\" Harris says. ",{"->":"harris_has_seen_it_before"},"\n",{"#f":5}]}],{"admit_open_to_pressure":["^\"That's it,\" I reply. \"There are some things... which a man shouldn't do.\"","\n","ev",true,"/ev",{"VAR=":"admitblackmail","re":true},"^Harris doesn't stiffen. Doesn't lean away, as though my condition might be infectious. I had thought they trained them in the army to shoot my kind on sight.","\n","^He offers no sympathy either. He nods, once. His understanding of me is a mere turning cog in his calculations, with no meaning to it.","\n",{"->":"harris_has_seen_it_before"},{"#f":1}],"#f":1}],"admitted_to_something":[["ev",{"VAR?":"drugged"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^Harris stares back at me. ","ev",{"VAR?":"evasive"},0,"==","/ev",[{"->":".^.b","c":true},{"b":["^He cannot have expected it to be so easy to break me.",{"->":".^.^.^.8"},null]}],"nop","\n",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["\n","^Harris smiles with satisfaction, as if your willingness to talk was somehow his doing.","\n",{"->":".^.^.^.6"},null]}],"nop","\n","^\"I see.\"","\n","^There's a long pause, like the delay between feeding a line of cypher into the Bombe and waiting for its valves to warm up enough to begin processing.","\n","^\"You want to explain that?\"","\n","ev","str","^Explain","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't explain","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-1","flg":21},"ev","str","^Lie","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-2","flg":21},"ev","str","^Evade","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-3","flg":21},"ev","str","^Say nothing","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-4","flg":21},{"c-0":["^ ","\n","^I pause a moment, trying to choose my words. To just come out and say it, after a lifetime of hiding... that is a circle I cannot square.","\n",["ev","str","^Explain","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Say nothing","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-1","flg":21},"ev","str","^Lie","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-2","flg":21},{"c-0":["^ \t",{"->":".^.^.^.^.^.ive_done_things"},"\n",{"#f":5}],"c-1":["^ \t",{"->":".^.^.^.^.c-4"},"\n",{"#f":5}],"c-2":["^ \t",{"->":"claim_hooper_took_component"},"\n",{"#f":5}]}],{"#f":5}],"c-1":["\n","^\"There's nothing to explain,\" I reply stiffly. ",{"->":".^.^.^.i_know_where"},"\n",{"#f":5}],"c-2":["^ ",{"->":"claim_hooper_took_component"},"\n",{"#f":5}],"c-3":["\n","^\"Explain what you should be doing, do you mean, rather than bullying me? Certainly.\" I fold my arms. ",{"->":".^.^.^.i_know_where"},"\n",{"#f":5}],"c-4":["\n","^I fold my arms, intended firmly to say nothing. But somehow, watching Harris' face, I cannot bring myself to do it. I want to confess. I want to tell him everything I can, to explain myself to him, to earn his forgiveness. The sensation is so strong my will is powerless in the face of it.","\n","^Something is wrong with me, I am sure of it. There is a strange, bitter flavour on my tongue. I taste it as words start to form.","\n",{"->":".^.^.^.ive_done_things"},{"#f":5}]}],{"i_know_where":["^\"I know where your component is because it's obvious where your component is. That doesn't mean I took it, just because I can figure out a simple problem, any more than it means I'm a German spy because I can crack their codes.\"","\n",{"->":"harris_asks_for_theory"},{"#f":1}],"ive_done_things":["^\"I've done things,\" I begin","ev",{"CNT?":"harris_demands_component.0.g-1.c-14"},"/ev",[{"->":".^.b","c":true},{"b":["^ helplessly",{"->":".^.^.^.5"},null]}],"nop","^. \"Things I didn't want to do. I tried not to. But in the end, it felt like cutting off my own arm to resist.\"","\n",{"->":"harris_presses_for_details"},{"#f":1}],"#f":1}],"harris_asks_for_theory":[["^\"Tell me, then,\" he asks. \"What's your theory? You're a smart fellow — as smart as they come around here, and that's saying something. What's your opinion on the missing component? Accident, perhaps? Or do you blame one of the other men? ","ev",{"VAR?":"hooper_mentioned"},"/ev",[{"->":".^.b","c":true},{"b":["^Hooper?",{"->":".^.^.^.5"},null]}],"nop","^\"","\n","ev","str","^Blame no—one","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Blame someone","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n",{"->":".^.^.^.an_accident"},{"#f":5}],"c-1":["^ ",{"->":"claim_hooper_took_component"},"\n",{"#f":5}]}],{"an_accident":[["^\"An accident, naturally.\" I risk a smile. \"That damned machine is made from spare parts and string. Even these Huts leak when it rains. It wouldn't take more than one fellow to trip over a cable to shake out a component. Have you tried looking under the thing?\"","\n","^\"Do you believe we haven't?\"","\n","^In a sudden moment I understand that his reply is a threat.","\n","^\"Now,\" he continues. \"Are you sure there isn't anything you want to tell me?\"","\n","ev","str","^Co-operate","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Evade","/str",{"VAR?":"evasive"},0,">","/ev",{"*":".^.c-1","flg":21},{"c-0":["\n","^\"All right.\" With a sigh, your defiance collapses. \"If you're searched my things then I suppose you've found ","ev",{"VAR?":"evasive"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^ what you need",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["^my letters. Haven't you? In fact, if you haven't, don't tell me",{"->":".^.^.^.9"},null]}],"nop","^.","\n","ev",true,"/ev",{"VAR=":"admitblackmail","re":true},"^Harris nods once.","\n","<>","^ ",{"->":"harris_has_seen_it_before"},"\n",{"#f":5}],"c-1":["^ \"Only that you're being unreasonable, and behaving like a swine.\"","\n","^\"You imbecile,\" Harris replies, with sudden force. He is half out of his chair. \"You know the situation as well as I do. Why the fencing? The Hun are poised like rats, ready to run all over this country. They'll destroy everything. You understand that, don't you? You're not so locked up inside your crossword puzzles that you don't see that, are you? This machine we have here — you men — you are the best and only hope this country has. God help her.\"","\n","ev",true,"/ev",{"VAR=":"losttemper","re":true},"^I sit back, startled by the force of his outburst. His carefully sculpted expression has curled to angry disgust. He really does hate me, I think. He'll have my blood for the taste of it.","\n",["ev","str","^Placate","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Mock","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Dismiss","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","^\"Now steady on,\" I reply, gesturing for him to be calm.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"I can imagine how being surrounded by clever men is pretty threatening for you, Commander,\" I reply with a sneer. \"They don't train you to think in the Armed Forces.\"","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["\n","^\"Then I'll be going, on and getting on with my job of saving her, shall I?\" I even rise half to my feet, before he slams the tabletop.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"Talk,\" Harris demands. \"Talk now. Tell me where you've hidden it or who you passed it to. Or God help me, I'll take your wretched pansy body to pieces looking for it.\"","\n",{"->":"harris_demands_you_speak"},{"#f":5}]}],{"#f":5}]}],{"#f":1}],"#f":1}],"harris_has_seen_it_before":[["^\"I've seen it before. A young man like you — clever, removed. The kind that doesn't go to parties. Who takes himself too seriously. Who takes things too far.\"","\n","^He slides his thumb between two fingers.","\n","^\"Now they own you.\"","\n","ev","str","^Agree","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Disagree","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-2","flg":21},"ev","str","^Apologise","/str",{"VAR?":"drugged"},{"VAR?":"forceful"},0,"<","&&","/ev",{"*":".^.c-3","flg":21},{"c-0":["^ ","\n","^\"What could I do?\" I'm shaking now. The night is cold and the heat—lamp in the Hut has been removed. \"","ev",{"VAR?":"forceful"},2,">","/ev",[{"->":".^.b","c":true},{"b":["^I won't",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^I don't want to",{"->":".^.^.^.10"},null]}],"nop","^ go to prison.\"","\n","^\"Smart man,\" he replies. \"You wouldn't last.","\n",{"->":".^.^.tell_me_now"},{"#f":5}],"c-1":["^ ","\n","^\"I can still fix this.\"","\n","^Harris shakes his head. \"You'll do nothing. This is beyond you now. You may go to prison or may go to firing squad - or we can change your name and move you somewhere where your indiscretions can't hurt you. But right now, none of that matters. What happens to you doesn't matter. All that matters is where that component is.","\n",{"->":".^.^.tell_me_now"},{"#f":5}],"c-2":["^ ","\n","^\"I wanted to tell you,\" I tell him. \"I thought I could find out who they were. Lead you to them.\"","\n","^Harris looks at me with contempt. \"You wretch. You'll pay for what you've done to this country today. If a single man loses his life because of your pride and your perversions then God help your soul.","\n",{"->":".^.^.tell_me_now"},{"#f":5}],"c-3":["\n","^\"Harris, I...\"","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n","^\"Stop it,\" he interrupts. \"There's no jury here to sway. And there's no time.","\n",{"->":".^.^.tell_me_now"},{"#f":5}],"tell_me_now":["<>","^ So why don't you tell me, right now. Where is it?\"","\n",{"->":"harris_demands_you_speak"},{"#f":5}]}],{"#f":1}],"harris_demands_you_speak":[["^His eyes bear down like carbonised drill—bits.","\n","ev","str","^Confess","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Dissemble","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-1","flg":21},"ev","str","^Dissemble","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-2","flg":21},{"c-0":["^ ","\n","ev",{"VAR?":"forceful"},1,">","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"You want me to tell you what happened? You'll be disgusted.\"","\n",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["\n","^\"All right. I'll tell you what happened.\" And never mind my shame.","\n",{"->":".^.^.^.9"},null]}],"nop","\n","^\"I can imagine how it starts,\" he replies.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ",{"->":"claim_hooper_took_component"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["\n","^My plan now is to blame Hooper, but I cannot seem to tell the story. Whatever they put in my tea, it rules my tongue. ","ev",{"VAR?":"forceful"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^I fight it as hard as I can but it does no good.",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["^I am desperate to tell him everything. I am weeping with shame.",{"->":".^.^.^.9"},null]}],"nop","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"i_met_a_young_man"},{"#f":5}]}],{"#f":1}],"i_met_a_young_man":[["ev","str","^Talk","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n","^\"There was a young man. I met him in the town. A few months ago now. We got to talking. Not about work. And I used my cover story, but he seemed to know it wasn't true. That got me wondering if he might be one of us.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^Harris is not letting me off any more.","\n","^\"You seriously entertained that possibility?\"","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-3","flg":20},{"c-1":["\n","^\"Yes, I considered it. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-2":["^ ","\n","^\"No. Not for more than a moment, of course. Everyone here is marked out by how little we would be willing to say about it.\"","\n","^\"Only you told this young man more than a little, didn't you?\"","\n","^I nod. \"","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-3":["^ ","\n","^\"I was quite certain, after a while. After we'd been talking. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":["^He seemed to know all about me. He... he was quite enchanted by my achievements.\"","\n","^The way Harris is staring I expect him to strike me, but he does not. He replies, \"I can see how that must have been attractive to you,\" with such plain—spokeness that I think I must have misheard.","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^No","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-5","flg":21},"ev","str","^No","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-6","flg":21},"ev","str","^Lie","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-7","flg":21},{"c-4":["^ \"It's a lonely life in this place,\" I reply. \"Lonely - and still one never gets a moment to oneself.\"","\n","^\"That's how it is in the Service,\" Harris answers.","\n",["ev","str","^Argue","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Agree","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ \"I'm not in the Service.\"","\n","^Harris shakes his head. \"Yes, you are.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ \"Perhaps. But I didn't choose this life.\" ","\n","^Harris shakes his head. \"No. And there's plenty of others who didn't who are suffering far worse.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^Then he waves the thought aside.","\n",{"->":".^.^.^.^.^.g-2"},{"#f":5}]}],{"#f":5}],"c-5":["^ \"The boy was a pretty simpleton. Quite inferior. His good opinion meant nothing to be. Harris, do not misunderstand. I was simply after his body.\"","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n","^Harris, to his credit, doesn't flinch; but I can see he will have nightmares of this moment later tonight. I'm tempted to reach out and take his hand to worsen it for him.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-6":["^ ","\n","^\"It wasn't,\" I reply. \"But I doubt you'd understand.\"","\n","^He simply nods.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-7":["^ ",{"->":".^.^.c-5"},"\n",{"->":".^.^.^.g-2"},{"#f":5}],"#f":5}],"g-2":["^\"Go on with your confession.\"","\n",["ev",{"CNT?":".^.^.^.g-1.c-5"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^That gives me pause. I hadn't thought of it as such. But I suppose he's right. I am about to admit what I did.","\n",{"->":".^.^.^.5"},null]}],"nop","\n","^\"There's not much else to say. I took the part from Bombe computing device. You seem to know that already. I had to. He was going to expose me if I didn't.\"","\n","^\"This young man was blackmailing you over your affair?\"","\n","ev",{"VAR?":"drugged"},"/ev",{"temp=":"harris_thinks_youre_drugged"},"ev",{"VAR?":"drugged"},"/ev",[{"->":".^.b","c":true},{"b":["\n","ev",false,"/ev",{"VAR=":"drugged","re":true},"^As Harris speaks I find myself suddenly sharply aware, as if waking from a long sleep. The table, the corrugated walls of the hut, everything seems suddenly more tangible than a moment before.","\n","^Whatever it was they put in my drink is wearing off.","\n",{"->":".^.^.^.19"},null]}],"nop","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-8","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-9","flg":20},"ev","str","^Tell the truth","/str","/ev",{"*":".^.c-10","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-11","flg":20},{"c-8":["^ ","\n","^\"Yes. I suppose he was their agent. I should have realised but I didn't. Then he threatened to tell you. I thought you would have me locked up: I couldn't bear the thought of it. I love working here. I've never been so happy, so successful, anywhere before. I didn't want to lose it.\"","\n","^\"So what did you do with the component?\" Harris talks urgently. He grips his gloves tightly in one hand, perhaps prepared to lift them and strike if it is required. \"Have you passed it to this man already? Have you left it somewhere for him to find?\"","\n",["ev","str","^I have it","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^I don't have it","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Tell the truth","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["^ \t","\n","^\"I still have it. Not on me, of course. ",{"->":"reveal_location_of_component"},"\n",{"#f":5}],"c-1":["^ \t",{"->":".^.^.^.^.^.^.^.i_dont_have_it"},"\n",{"#f":5}],"c-2":["^ \t\t\t\t\t\t\t",{"->":".^.^.c-1"},"\n",{"#f":5}],"c-3":["^ \t\t\t\t",{"->":".^.^.c-0"},"\n",{"#f":5}]}],{"#f":5}],"c-9":["^ ","\n","^\"No, Harris. The young man wasn't blackmailing me.\" I take a deep breath. \"It was Hooper.\"","\n","ev",{"VAR?":"hooper_mentioned"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"Hooper!\" Harris exclaims, in surprise. ","ev",{"VAR?":"harris_thinks_youre_drugged"},"/ev",[{"->":".^.b","c":true},{"b":["^He does not doubt me for a moment.",{"->":".^.^.^.6"},null]}],"nop","\n",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["\n","^\"Now look here,\" Harris interrupts. \"Don't start that again.\"","\n",{"->":".^.^.^.10"},null]}],"nop","\n","^\"It's the truth, Harris. If I'm going to jail, so be it, but I won't hang at Traitor's Gate. Hooper was the one who told the boy about our work. Hooper put the boy on to me. ","ev",{"VAR?":"forceful"},2,"<","/ev",[{"->":".^.b","c":true},{"b":["^I should have realised, of course. These things don't happen by chance. I was a fool to think they might.",{"->":".^.^.^.19"},null]}],"nop","^ And then, once he had me compromised, he demanded I steal the part from the machine.\"","\n","ev",true,"/ev",{"VAR=":"revealedhooperasculprit","re":true},"^\"Which you did.\" Harris leans forward. \"And then what? You still have it? You've stashed it somewhere?\"","\n",["ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["^ ","\n","^\"Yes. I only had a moment. ",{"->":"reveal_location_of_component"},"\n",{"#f":5}],"c-1":["^ ",{"->":".^.^.^.^.^.^.^.passed_onto_hooper"},"\n",{"#f":5}],"c-2":["^ \t\t\t",{"->":".^.^.c-1"},"\n",{"#f":5}],"c-3":["^ \t\t","\n","^\"I can't remember.\"","\n","^He draws his gun and lays it lightly on the field table.","\n","^\"I'm sorry to threaten you, friend. But His Majesty needs that brain of yours, and that brain alone. There are plenty of other parts to you that our country could do better without. Now I'll ask you again. Did you hide the component?\"","\n",["ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["^ ",{"->":".^.^.^.^.c-0"},"\n",{"#f":5}],"c-1":["^ ","\n","^\"Very well then.\" I swallow nervously, to make it look more genuine. ",{"->":"i_met_a_young_man.passed_onto_hooper"},"\n",{"#f":5}],"c-2":["^ ",{"->":".^.^.c-1"},"\n",{"#f":5}],"c-3":["^ ",{"->":"i_met_a_young_man.i_dont_have_it"},"\n",{"#f":5}]}],{"#f":5}]}],{"#f":5}],"c-10":["^ \t",{"->":".^.^.c-8"},"\n",{"#f":5}],"c-11":["^ \t\t\t\t",{"->":".^.^.c-9"},"\n",{"#f":5}],"#f":5,"#n":"paused"}],{"#f":5}]}],{"i_dont_have_it":[["^\"I don't have it any more. I passed it through the fence to my contact straight after taking it, before it was discovered to be missing. It would have been idiocy to do differently. It's long gone, I'm afraid.\"","\n","^\"You fool, Manning,\" Harris curses, getting quickly to his feet. \"You utter fool. Do you suppose you will be any better off living under Hitler? It's men like you who will get us all killed. Men too feeble, too weak in their hearts to stand up and take a man's responsibility for the world. You're happier to stay a child all your life and play with your little childish toys.\"","\n","ev","str","^Answer back","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Say nothing","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"Really, Commander,\" I reply. \"It rather sounds like you want to spank me.\"","\n","^\"For God's sake,\" he declares with thick disgust, then swoops away out of the room.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^I say nothing. It's true, isn't it? I can't deny that I know there is a world out there, a complicated world of pain and suffering. And I can't deny that I don't think about it a moment longer than I have to. What use is thinking on a problem that cannot be solved? It is precisely our ability to avoid such endless spirals that makes us human and not machine.","\n","^\"God have mercy on your soul,\" Harris says finally, as he gets to his feet and heads for the door. \"I fear no—one else will.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"left_alone"},{"#f":5}]}],{"#f":1}],"passed_onto_hooper":[["ev",true,"/ev",{"VAR=":"hooper_mentioned","re":true},"^\"No. I passed it on to Hooper.\"","\n","^\"I see. And what did he do with it?\"","\n","ev","str","^Evade","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Tell the truth","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^\"I don't know.\"","\n","^\"You can do better than that. Remember, there's a hangman's noose waiting for traitors.\"","\n",["ev","str","^Theorise","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Shrug","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"Well, then,\" I answer, nervously. \"What would he do? Either get rid of it straight away — or if that wasn't possible, which it probably wouldn't be, since he'd have to arrange things with his contacts — so most likely, he'd hide it somewhere and wait, until you had the rope around my neck and he could be sure he was safe.\"","\n",{"->":"claim_hooper_took_component.harris_being_convinced"},{"#f":5}],"c-1":["^ ",{"->":"claim_hooper_took_component.its_your_problem"},"\n",{"#f":5}]}],{"#f":5}],"c-1":["^ ","\n","^\"I don't think Hooper could have planned this in advance. So he'd need to get word to whoever he's working with, and that would take time. So I think he would have hidden it somewhere, and be waiting to make sure I soundly take the fall. That way, if anything goes wrong, he can arrange for the part to be conveniently re—found.\"","\n",{"->":"claim_hooper_took_component.harris_being_convinced"},{"#f":5}],"c-2":["\n","^\"I'm sure I saw him this evening, talking to someone by the fence on the woodland side of the compound. He's probably passed it on already. You'll have to ask him.\"","\n",{"->":"claim_hooper_took_component.harrumphs"},{"#f":5}]}],{"#f":1}],"#f":1}],"claim_hooper_took_component":[["^\"I saw Hooper take it.\"","\n","ev",true,"/ev",{"VAR=":"hooper_mentioned","re":true},"ev",{"VAR?":"losttemper"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^\"Did you?\"","\n","^The worst of his rage is passing; he is now moving into a kind of contemptuous despair. I can imagine him wrapping up our interview soon, leaving the hut, locking the door, and dropping the key down the well in the yard.","\n","^And why wouldn't he? With my name tarnished they will not let me back to work on the Bombe — if there is the slightest smell of treachery about my name I would be lucky not be locked up for the remainder of the war.","\n",{"->":".^.^.^.11"},null]}],[{"->":".^.b"},{"b":["\n","^\"I see.\" He is starting to lose his patience. I have seen Harris angry a few times, with lackeys and secretaries. But never with us. With the 'brains' he has always been cautious, treating us like children.","\n","^And now I see that, like a father, he wants to smack us when we disobey him.","\n",{"->":".^.^.^.11"},null]}],"nop","\n","^\"Just get to the truth, man. Every minute matters.\"","\n","ev","str","^Persist with this","/str",{"VAR?":"admitblackmail"},"/ev",{"*":".^.c-0","flg":21},"ev","str","^Tell the truth","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Persist with this","/str",{"VAR?":"admitblackmail"},"!","/ev",{"*":".^.c-2","flg":21},{"c-0":["\n","^\"I know what you're thinking. If I've transgressed once then I must be guilty of everything else... But I'm not. We were close to cracking the 13th's intercept. We were getting correlations in the data. Then Hooper disappeared for a moment, and next minute the machine was down.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"Very well. I see there's no point in covering up. You know everything anyway.\"","\n","^Harris nods, and waits for me to continue.","\n",{"->":"i_met_a_young_man"},{"->":".^.^.g-0"},{"#f":5}],"c-2":["\n","^\"This is the truth.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^I have become, somehow, an accustomed liar — the words roll easily off my tongue. Perhaps I am a traitor, I think, now that I dissemble as easily as one.","\n","^\"Go on,\" Harris says, giving me no indication of whether he believes my tale.","\n","ev","str","^Assert","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Imply","/str","/ev",{"*":".^.c-4","flg":20},{"c-3":["^ \"I saw him take it,\" I continue. \"Collins was outside having a cigarette. Peterson was at the table. But I was at the front of the machine. I saw Hooper go around the side. He leant down and pulled something free. I even challenged him. I said, 'What's that? Someone put a nail somewhere they shouldn't have?' He didn't reply.\"","\n","^Harris watches me for a long moment.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-4":["^ \"At the moment the machine halted, Peterson was at the bench and Collins was outside having a smoke. I was checking the dip—switches. Hooper was the only one at the back of the Bombe. No—one else could have done it.\"","\n","^\"That's not quite the same as seeing him do it,\" Harris remarks.","\n",["ev","str","^Logical","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Persuasive","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Confident","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","^\"When you have eliminated the impossible...\" I begin, but Harris cuts me off.","\n",{"->":".^.^.^.^.^.g-1"},{"#f":5}],"c-1":["^ ","\n","^\"You have to believe me.\"","\n","^\"We don't have to believe anyone,\" Harris returns. \"I will only be happy with the truth, and your story doesn't tie up. We know you've been leaving yourself open to pressure. We've been watching your activities for some time. But we thought you were endangering the reputation of this site; not risking the country herself. Perhaps I put too much trust in your intellectual pride.\"","\n","^He pauses for a moment, considering something. Then he continues:","\n","^\"It might have been Hooper. It might have been you. ",{"->":".^.^.^.^.^.we_wont_guess"},"\n",{"->":".^.^.^.^.^.g-1"},{"#f":5}],"c-2":["^ ","\n","^\"Ask the others,\" I reply, leaning back. \"They'll tell you. If they haven't already, that's only because they're protecting Hooper. Hoping he'll come to his senses and stop being an idiot. I hope he does too. And if you lock him up in a freezing hut like you've done me, I'm sure he will.\"","\n","^\"We have,\" Harris replies simply.","\n","^It's all I can do not to gape.","\n",{"->":".^.^.^.^.^.g-1.hoopers_hut_3"},{"->":".^.^.^.^.^.g-1"},{"#f":5}]}],{"#f":5}],"#f":5}],"g-1":["^\"We are left with two possibilities. You, or Hooper.\" The Commander pauses to smooth down his moustache. ","<>","\n",["^\"Hooper's in Hut 3 with the Captain, having a similar conversation.\"","\n",["ev",{"^->":"claim_hooper_took_component.0.g-1.hoopers_hut_3.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-5","flg":22},{"s":["^\"And the other men?",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.0.g-1.hoopers_hut_3.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-6","flg":22},{"s":["^\"Then you know I'm right.",{"->":"$r","var":true},null]}],{"c-5":["ev",{"^->":"claim_hooper_took_component.0.g-1.hoopers_hut_3.c-5.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"^ Do we have a hut each? Are there enough senior officers to go round?\"","\n","^\"Collins was outside when it happened, and Peterson can't get round the machine in that chair of his,\" Harris replies. \"That leaves you and Hooper.","\n",{"->":".^.^.^.^.we_wont_guess"},{"#f":5}],"c-6":["ev",{"^->":"claim_hooper_took_component.0.g-1.hoopers_hut_3.c-6.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"^ You knew all along. Why did you threaten me?\"","\n","^\"All we know is that we have a traitor, holding the fate of the country in his hands.","\n",{"->":".^.^.^.^.we_wont_guess"},{"#f":5}],"#f":5,"#n":"hoopers_hut_3"}],{"#f":5}],"we_wont_guess":["<>","^ We're not in the business of guessing here at Bletchley. We are military intelligence. We get answers.\" Harris points a finger. \"And if that component has left these grounds, then every minute is critical.\"","\n","ev","str","^Co-operate","/str","/ev",{"*":".^.c-7","flg":20},"ev","str","^Block","/str","/ev",{"*":".^.c-8","flg":20},{"c-7":["^ ","\n","^\"I'd be happy to help,\" I answer, leaning forwards. \"I'm sure there's something I could do.\"","\n","^\"Like what, exactly?\"","\n",[["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"Put me in with Hooper.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-1","flg":22},{"s":["^\"Tell Hooper I've confessed.",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.^.^.^.^.putmein"},{"#f":5}],"c-1":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"^ Better yet. Let him see you marching me off in handcuffs. Then let him go, and see what he does. Ten to one he'll go straight to wherever he's hidden that component and his game will be up.\"","\n","^Harris nods slowly, chewing over the idea. It isn't a bad plan even — except, of course, Hooper has not hidden the component, and won't lead them anywhere. But that's a problem I might be able to solve once I'm out of this place; and once they're too busy dogging Hooper's steps from hut to hut.","\n","^\"Interesting,\" the Commander muses. \"But I'm not so sure he'd be that stupid. And if he's already passed the part on, the whole thing will only be a waste of time.\"","\n",[["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-0","flg":22},{"s":["^\"Trust me. He hasn't.",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str","/ev",{"*":".^.^.c-1","flg":22},{"s":["^\"You're right. Let me talk to him",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":18},{"s":["^\"You're right.\" ",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"^ If I know that man, and I do, he'll be wanting to keep his options open as long as possible. If the component's gone then he's in it up to his neck. He'll take a week at least to make sure he's escaped suspicion. Then he'll pass it on.\"","\n","^\"And if we keep applying pressure to him, you think the component will eventually just turn up?\"","\n",[["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-0","flg":22},{"s":["^\"Yes.",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"Or be thrown into the river.\" ",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"^ Probably under my bunk.\"","\n","^Harris smiles wryly. \"We'll know that for a fake, then. We've looked there already.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n","^\"Hmm.\" Harris chews his moustache thoughtfully. \"Well, that would put us in a spot, seeing as how we'd never know for certain. We'd have to be ready to change our whole approach just in case the part had got through to the Germans.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["<>","^ I don't mind telling you, this is a disaster, this whole thing. What I want is to find that little bit of mechanical trickery. I don't care where. In your luncheon box or under Hooper's pillow. Just somewhere, and within the grounds of this place.\"","\n",["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.g-0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-2","flg":22},{"s":["^\"Then let him he think he's off the hook.",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.g-0.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str","/ev",{"*":".^.^.c-3","flg":22},{"s":["^\"Then you'd better get searching",{"->":"$r","var":true},null]}],{"c-2":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.g-0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"^ Make a show of me. And then you'll get your man.\"","\n","^Somehow, I think. But that's the part I need to work.","\n",{"->":"harris_takes_you_to_hooper"},{"#f":5}],"c-3":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.g-0.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.4.s"},[{"#n":"$r2"}],"^,\" I reply, tiring of his complaining. A war is a war, you have to expect an enemy. ",{"->":".^.^.^.^.^.^.^.^.^.^.^.its_your_problem"},"\n",{"#f":5}],"#f":5}]}],{"#f":5}],"c-1":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"^, then. As a colleague. Maybe I can get something useful out of him.\"","\n",{"->":".^.^.^.^.^.^.^.^.putmein"},{"#f":5}],"c-2":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],{"->":".^.^.^.^.^.^.^.^.shake_head"},"\n",{"#f":5}]}],{"#f":5}]}],{"#f":5}],"c-8":["^ ",{"->":".^.^.^.^.its_your_problem"},"\n",{"#f":5}],"#f":5}]}],{"harris_being_convinced":[["^\"Makes sense,\" Harris agrees, cautiously. ","ev",{"VAR?":"evasive"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^I can see he's still not entirely convinced by my tale, as well he might not be — I've hardly been entirely straight with him.",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["^I can see he's still not certain whether he can trust me.",{"->":".^.^.^.8"},null]}],"nop","^ \"Which means the question is, what can we do to rat him out?\"","\n","ev","str","^Offer to help","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't offer to help","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"Maybe I can help with that.\"","\n","^\"Oh, yes? And how, exactly?\"","\n",[["ev",{"^->":"claim_hooper_took_component.harris_being_convinced.0.c-0.6.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"I'll talk to him.\" ",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.harris_being_convinced.0.c-0.6.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-1","flg":22},{"s":["^\"We'll fool him.",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"claim_hooper_took_component.harris_being_convinced.0.c-0.6.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n","^\"What?\"","\n","^\"Put me in with Hooper with him. Maybe I can get something useful out of him.\"","\n",{"->":".^.^.^.^.^.^.putmein"},{"#f":5}],"c-1":["ev",{"^->":"claim_hooper_took_component.harris_being_convinced.0.c-0.6.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"^ He's waiting to be sure that I've been strung up for this, so let's give him what he wants. If he sees me taken away, clapped in irons — he'll go straight to that component and set about getting rid of it.\"","\n",{"->":"harris_takes_you_to_hooper"},{"#f":5}]}],{"#f":5}],"c-1":["\n","^I lean back. ",{"->":".^.^.^.^.its_your_problem"},"\n",{"#f":5}]}],{"#f":1}],"putmein":[["^Harris shakes his head.","\n","^\"He despises you. I don't see why he'd give himself up to you.\"","\n","ev","str","^Insist","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Give in","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ \"Try me. Just me and him.\" ","\n",{"->":".^.^.^.^.go_in_alone"},{"#f":5}],"c-1":["^ \"You're right.\" ","\n",{"->":".^.^.^.^.shake_head"},{"#f":5}]}],{"#f":1}],"shake_head":["<>","^ I shake my head. \"You're right. I don't see how I can help you. So there's only one conclusion.\"","\n","^\"Oh, yes? And what's that?\"","\n",{"->":".^.^.its_your_problem"},{"#f":1}],"its_your_problem":["^\"It's your problem. Your security breach. So much for your careful vetting process.\"","\n","^I lean back in my chair and fold my arms so the way they shake will not be visible.","\n","^\"You'd better get on with solving it, instead of wasting your time in here with me.\"","\n",{"->":".^.^.harrumphs"},{"#f":1}],"harrumphs":[["^Harris harrumphs. He's thinking it all over.","\n","ev","str","^Wait","/str",{"CNT?":".^.^.^.putmein"},"/ev",{"*":".^.c-0","flg":21},"ev","str","^Wait","/str",{"CNT?":".^.^.^.putmein"},"/ev",{"*":".^.c-1","flg":21},{"c-0":["^ ","\n","^\"All right,\" he declares, gruffly. \"We'll try it. But if this doesn't work, I might just put the both of you in front of a firing squad and be done with these games. Worse things happen in time of war, you know.\"","\n","^\"Alone,\" I add.","\n",{"->":".^.^.^.^.go_in_alone"},{"#f":5}],"c-1":["^ ","\n","^\"No,\" Harris declares, finally. \"I think you're lying about Hooper. I think you're a clever, scheming young man — that's why we hired you — and you're looking for the only reasonable out this situation has to offer. But I'm not taking it. We know you were in the room with the machine, we know you're of a perverted persuasion, we know you have compromised yourself. There's nothing more to say here. Either you tell me what you've done with that component, or we will hang you and search just as hard. It's your choice.\"","\n",{"->":"harris_threatens_lynching"},{"#f":5}]}],{"#f":1}],"go_in_alone":[["^\"Alone?\"","\n","^\"Alone.\"","\n","^Harris considers it. I watch his eyes, flicking backwards and forwards over mine, like a ribbon—reader loading its program.","\n","ev","str","^Patient","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Impatient","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ \"Well?\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ \"For God's sake, man, what do you have to lose?\" ","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"We'll be outside the door,\" Harris replies, seriously. \"The first sign of any funny business and we'll have you both on the floor in minutes. You understand? The country needs your brain, but it's not too worried about your legs. Remember that.\"","\n","^Then he gets to his feet, and opens the door, and marches me out across the yard. The evening is drawing in and there's a chill in the air. My mind is racing. I have one opportunity here — a moment in which to put the fear of God into Hooper and make him do something foolish that places him in harm's way. But how to achieve it?","\n","^\"You ready?\" Harris demands.","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-4","flg":20},{"c-2":["\n","^\"Absolutely.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-3":["\n","^\"No.\"","\n","^\"Too bad.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-4":["^ ",{"->":".^.^.c-2"},"\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":[{"->":"inside_hoopers_hut"},{"#f":5}]}],{"#f":1}],"#f":1}],"harris_takes_you_to_hooper":[["^Harris gets to his feet. \"All right,\" he says. \"I should no better than to trust a clever man, but we'll give it a go.\"","\n","^Then, he smiles, with all his teeth, like a wolf.","\n","ev",{"CNT?":"claim_hooper_took_component.0.g-1.hoopers_hut_3"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^\"Especially since this is a plan that involves keeping you in handcuffs. I don't see what I have to lose.\"","\n",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["\n","^\"Hooper's in Hut 3 being debriefed by the Captain. Let's see if we can't get his attention somehow.\"","\n",{"->":".^.^.^.9"},null]}],"nop","\n","^He raps on the door for the guard and gives the man a quick instruction. He returns a moment later with a cool pair of iron cuffs.","\n","^\"Put 'em up,\" Harris instructs, and I do so. The metal closes around my wrists like a trap. I stand and follow Harris willingly out through the door.","\n","^But whatever I'm doing with my body, my mind is scheming. Somehow, I'm thinking, I have to get away from these men long enough to get that component behind Hut 2 and put it somewhere Hooper will go. Or, otherwise, somehow get Hooper to go there himself...","\n","^Harris marches me over to Hut 3, and gestures for the guard to stand aside. Pushing me forward, he opens the door nice and wide.","\n","^\"Captain. Manning talked. If you'd step out for a moment?\"","\n","ev","str","^Play the part, head down","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Look inside the hut","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Call to Hooper","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","^From where he's sitting, I know Hooper can see me, so I keep my head down and look guilty as sin. The bastard is probably smiling.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["\n","^I look in through the door and catch Hooper's expression. I had half expected him to be smiling be he isn't. He looks shocked, almost hurt. \"Iain,\" he murmurs. \"You couldn't...\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^I have a single moment to shout something to Hooper before the door closes.","\n","^\"I'll get you Hooper, you'll see!\" I cry. Then:","\n",[["ev",{"^->":"harris_takes_you_to_hooper.0.c-2.6.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"Queen to rook two, checkmate!\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"harris_takes_you_to_hooper.0.c-2.6.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"^ I call, then laugh viciously, as if I am damning him straight to hell.","\n","ev",2,"/ev",{"VAR=":"hooperClueType","re":true},{"->":".^.^.only_catch"},{"#f":5}],"only_catch":["^I only catch Hooper's reaction for a moment — his eyebrow lifts in surprise and alarm. Good. If he thinks it is a threat then he just might be careless enough to go looking for what it might mean.","\n",["ev",{"^->":"harris_takes_you_to_hooper.0.c-2.6.only_catch.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"Ask not for whom the bell tolls!\"",{"->":"$r","var":true},null]}],["ev",{"^->":"harris_takes_you_to_hooper.0.c-2.6.only_catch.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":18},{"s":["^\"Two words: messy, without one missing!\"",{"->":"$r","var":true},null]}],{"c-1":["ev",{"^->":"harris_takes_you_to_hooper.0.c-2.6.only_catch.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","^He stares back at me, as if were a madman and perhaps for a split second I see him shudder.","\n",{"->":".^.^.^.^.^.g-0"},{"#f":5}],"c-2":["ev",{"^->":"harris_takes_you_to_hooper.0.c-2.6.only_catch.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"^ I cry, laughing. It isn't the best clue, hardly worthy of The Times, but it will have to do.","\n","ev",3,"/ev",{"VAR=":"hooperClueType","re":true},{"->":".^.^"},{"->":".^.^.^.^.^.g-0"},{"#f":5}],"#f":5}]}],{"#f":5}],"g-0":["^The Captain comes outside, pulling the door to. \"What's this?\" he asks. \"A confession? Just like that?\"","\n","^\"No,\" the Commander admits, in a low voice. \"I'm afraid not. Rather more a scheme. The idea is to let Hooper go and see what he does. If he believes we have Manning here in irons, he'll try to shift the component.\"","\n","^\"If he has it.\"","\n","^\"Indeed.\"","\n","^The Captain peers at me for a moment, like I was some kind of curious insect.","\n","^\"Sometimes, I think you people are magicians,\" he remarks. \"Other times you seem more like witches. Very well.\"","\n","^With that he opens the door to the Hut and goes back inside. The Commander uses the moment to hustle me roughly forward.","\n","ev",{"CNT?":".^.^.c-2"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^\"And what was all that shouting about?\" he hisses in my ear as we move towards the barracks. \"Are you trying to pull something? Or just make me look incompetent?\"","\n",{"->":".^.^.^.19"},null]}],[{"->":".^.b"},{"b":["\n","^\"This scheme of yours had better come off,\" he hisses in my ear. \"Otherwise the Captain is going to start having men tailing me to see where I go on Saturdays.\"","\n",{"->":".^.^.^.19"},null]}],"nop","\n","ev","str","^Reassure","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Dissuade","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Say nothing","/str","/ev",{"*":".^.c-6","flg":20},{"c-3":["^ ","\n","ev",{"CNT?":".^.^.^.c-2"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"It will. Hooper's running scared,\" I reply, hoping I sound more confident than I feel.","\n",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["\n","^\"Just adding to the drama,\" I tell him, confidently. \"I'm sure you can understand that.\"","\n",{"->":".^.^.^.8"},null]}],"nop","\n","^\"I think we've had enough drama today already,\" Harris replies. \"Let's hope for a clean kill.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-4":["^ ","\n","ev",{"CNT?":".^.^.^.c-2"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"The Captain thought it was a good scheme. You'll most likely get a promotion.\"","\n",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["\n","^\"I'm not trying to do anything except save my neck.\"","\n",{"->":".^.^.^.8"},null]}],"nop","\n","^\"Let's hope things work out,\" Harris agrees darkly.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-5":["^ ","\n","^\"We're still in ear—shot if they let Hooper go. Best get us inside and then we can talk, if we must.\"","\n","^\"I've had enough of your voice for one day,\" Harris replies grimly. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-6":["\n","^I let him have his rant. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":["^He hustles me up the steps of the barracks, keeping me firmly gripped as if I had any chance of giving him, a trained military man, the slip. It's all I can do not to fall into the room.","\n",{"->":"slam_door_shut_and_gone"},{"#f":5}]}],{"#f":1}],"inside_hoopers_hut":[[["^Harris opens the door and pushes me inside. \"Captain,\" he calls. \"Could I have a moment?\"","\n","^The Captain, looking puzzled, steps out. The door is closed. Hooper stares at me, open—mouthed, about to say something. I probably have less than a minute before the Captain storms back in and declares this plan to be bunkum.","\n","ev","str","^Threaten","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Bargain","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Plead","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","^\"Listen to me, Hooper. We were the only men in that hut today, so we know what happened. But I want you to know this. I put the component inside a breeze—block in the foundations of Hut 2, wrapped in one of your shirts. They're going to find it eventually, and that's going to be what tips the balance. And there's nothing you can do to stop any of that from happening.\"","\n","ev",1,"/ev",{"VAR=":"hooperClueType","re":true},"^His eyes bulge with terror. \"What did I do, to you? What did I ever do?\"","\n",["ev","str","^Tell the truth","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^\"You treated me like vermin. Like something abhorrent.\"","\n","^\"You are something abhorrent.\"","\n","^\"I wasn't. Not when I came here. And I won't be, once you're gone.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"Nothing,\" I reply. \"You're just the other man in the room. One of us has to get the blame.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^\"It doesn't matter. Just remember what I said. I've beaten you, Hooper. Remember that.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^I get to my feet and open the door of the Hut. The Captain storms back inside and I'm quickly thrown out. ",{"->":".^.^.^.^.^.^.hustled_out"},"\n",{"#f":5}]}],{"#f":5}],"c-1":["^ ","\n","^\"Hooper, I'll make a deal with you. We both know what happened in that hut this afternoon. I know because I did it, and you know because you know you didn't. But once this is done I'll be rich, and I'll split that with you. I'll let you have the results, too. Your name on the discovery of the Bombe. And it won't hurt the war effort — you know as well as me that the component on its own is worthless, it's the wiring of the Bombe, the usage, that's what's valuable. So how about it?\"","\n","^Hooper looks back at me, appalled. \"You're asking me to commit treason?\"","\n",["ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["\n","^\"Yes, perhaps. But also to ensure your name goes down in the annals of mathematics. ",{"->":".^.^.^.^.^.^.back_of_hut_2"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"No. It's not treason. It's a trade, plain and simple.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^\"I'm suggesting you save your own skin. I've wrapped that component in one of your shirts, Hooper. They'll be searching this place top to bottom. They'll find it eventually, and when they do, that's the thing that will swing it against you. So take my advice now. Hut 2.\"","\n","ev",1,"/ev",{"VAR=":"hooperClueType","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ ",{"->":".^.^.c-2"},"\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":".^.^.^.^.^.^.no_chance"},{"#f":5}]}],{"#f":5}],"c-2":["^ ","\n","^\"Please, Hooper. You don't understand. They have information on me. I don't need to tell you what I've done, you know. Have a soul. And the component — it's nothing. It's not the secret of the Bombe. It's just a part. The German's think it's a weapon — a missile component. Let them have it. Please, man. Just help me.\"","\n","^\"Help you?\" Hooper stares. \"Help you? You're a traitor. A snake in the grass. And you're queer.\"","\n",["ev","str","^Deny","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Accept","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^\"I'm no traitor. You know I'm not. How much work have I done here against the Germans? I've given my all. And you know as well as I do, if the Reich were to invade, I would be a dead man. Please, Hooper. I'm not doing any of this lightly.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["\n","^\"I am what I am,\" I reply. \"I'm the way I was made. But they'll hang me unless you help, Hooper. Don't let them hang me.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^\"That's not important now. What matters is what you do, this evening.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"Assuming I wanted to help you,\" he replies, carefully. \"Which I don't. What would I do?\"","\n","^\"Nothing. Almost nothing.","\n",{"->":".^.^.^.^.^.^.back_of_hut_2"},{"#f":5}]}],{"#f":5}],"#f":5,"#n":"g-0"}],null],{"back_of_hut_2":["<>","^ All you have to do is go to the back of Hut 2. There's a breeze—block with a cavity. That's where I've put it. I'll be locked up overnight. But you can pick it up and pass it to my contact. He'll be at the south fence around two AM.\"","\n","ev",1,"/ev",{"VAR=":"hooperClueType","re":true},{"->":".^.^.no_chance"},{"#f":1}],"no_chance":["^\"If you think I'll do that then you're crazy,\" Hooper replies.","\n","^At that moment the door flies open and the Captain comes storming back inside.","\n",{"->":".^.^.hustled_out"},{"#f":1}],"hustled_out":["^Harris hustles me over to the barracks. \"I hope that's the end of it,\" he mutters.","\n","^\"Just be sure to let him out,\" I reply. \"And then see where he goes.\"","\n",{"->":"slam_door_shut_and_gone"},{"#f":1}],"#f":1}],"slam_door_shut_and_gone":[["^Then they slam the door shut, and it locks.","\n","ev",{"VAR?":"hooperClueType"},0,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","<>","^ How am I supposed to manage anything from in here?","\n","ev","str","^Try the door","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Try the windows","/str","/ev",{"*":".^.c-1","flg":20},{"->":".^.^.^.9"},{"c-0":["^ ",{"->":".^.^.^.^.^.try_the_door"},"\n",{"#f":5}],"c-1":["^ ",{"->":".^.^.^.^.^.try_the_windows"},"\n",{"#f":5}]}]}],[{"->":".^.b"},{"b":["\n","^I can only hope that Hooper bites. If he thinks I'm bitter enough to have framed him, and arrogant enough to have taunted him with ","ev",{"VAR?":"hooperClueType"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^a clue to",{"->":".^.^.^.8"},null]}],"nop","^ where the damning evidence is hidden...","\n","^If he hates me enough, and is paranoid enough, then he might ","ev",{"VAR?":"hooperClueType"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^unravel my little riddle and",{"->":".^.^.^.18"},null]}],"nop","^ go searching around Hut 2.","\n",{"->":".^.^.^.9"},null]}],"nop","\n","ev","str","^Wait","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["^ \t",{"->":"night_falls"},"\n",{"#f":5}]}],{"try_the_door":["^I try the door. It's locked, of course.","\n",{"->":".^.^.from_outside_heard"},{"#f":1}],"from_outside_heard":[["^From outside, I hear a voice. Hooper's. He's haranguing someone.","\n",["ev","str","^Listen at the keyhole","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Try the window","/str",{"CNT?":".^.^.^.^.try_the_windows"},"!","/ev",{"*":".^.c-1","flg":21},"ev","str","^Try the door","/str",{"CNT?":".^.^.^.^.try_the_door"},"!",{"CNT?":".^.c-0"},"&&","/ev",{"*":".^.c-2","flg":21},"ev","str","^Smash the window","/str",{"CNT?":".^.^.^.^.try_the_windows"},"/ev",{"*":".^.c-3","flg":21},"ev","str","^Wait","/str",{"CNT?":".^.^.^.^.try_the_door"},{"CNT?":".^.^.^.^.try_the_windows"},"&&","/ev",{"*":".^.c-4","flg":21},{"c-0":["^ ","\n","^I put my ear down to the keyhole, but there's nothing now. Probably still a guard outside, of course, but they're keeping mum.","\n",{"->":".^.^"},{"#f":5}],"c-1":["^ ",{"->":".^.^.^.^.^.try_the_windows"},"\n",{"#f":5}],"c-2":["^ ",{"->":".^.^.^.^.^.try_the_door"},"\n",{"#f":5}],"c-3":["^ ",{"->":".^.^.^.^.^.try_to_smash_the_window"},"\n",{"#f":5}],"c-4":["^ ","\n","^It's useless. There's nothing I can do but hope. I sit down on one corner of the bunk to wait.","\n",{"->":"night_falls"},{"#f":5}],"#f":5,"#n":"opts"}],null],{"#f":1}],"try_the_windows":["^I go over to the window and try to jimmy it open. Not much luck, but in my struggling I notice this window only backs on the thin little brook that runs down the back of the compound. Which means, if I smashed it, I might get away with no—one seeing.","\n",{"->":".^.^.from_outside_heard"},{"#f":1}],"try_to_smash_the_window":[["^The window is my only way out of here. I just need a way to smash it.","\n","ev","str","^Punch it","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Find something","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Use something you've got","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^I suppose my fist would do a good enough job. But I'd cut myself to ribbons, most likely. ","<>","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","ev",2,"/ev",{"VAR=":"smashingWindowItem","re":true},"^I cast around the small room. There's a bucket in one corner for emergencies — I suppose I could use that. I pick it up but it's not very easy to heft. ","<>","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^I pat down my pockets but all I'm carrying is the intercept, which is no good at all.","\n",["ev","str","^Something you're wearing?","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Look around","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^Ah, but of course! I slip off one shoe and heft it by the toe. The heel will make a decent enough hammer, if I give it enough wallop.","\n","ev",1,"/ev",{"VAR=":"smashingWindowItem","re":true},"^But I'll cut my hand to ribbons doing it. ","<>","\n",{"->":".^.^.^.^.g-0"},{"#f":5}],"c-1":["^ ",{"->":".^.^.^.^.c-1"},"\n",{"->":".^.^.^.^.g-0"},{"#f":5}]}],{"#f":5}],"g-0":["^And the noise would be terrible. There must be a way of making this easier. I'm supposed to be a thief now. What would a burglar do?","\n","ev","str","^Work slowly","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Find something to help","/str","/ev",{"*":".^.c-4","flg":20},{"c-3":["^ ","\n","^Work carefully? It's difficult to work carefully when all one's has is ","ev",{"VAR?":"smashingWindowItem"},2,"==","/ev",[{"->":".^.b","c":true},{"b":["^a bucket. It's rather like the sledgehammer for the proverbial nut",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["ev",{"VAR?":"smashingWindowItem"},1,"==","/ev",[{"->":".^.b","c":true},{"b":["^a shoe",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^nothing but brute force",{"->":".^.^.^.7"},null]}],"nop",{"->":".^.^.^.10"},null]}],"nop","^.","\n",["ev","str","^Just do it","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Look around for something","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ",{"->":".^.^.^.^.^.^.^.time_to_move_now"},"\n",{"->":".^.^.^.^.^.g-1"},{"#f":5}],"c-1":["^ ","\n",{"->":".^.^.^.^.^.g-1"},{"#f":5}]}],{"#f":5}],"c-4":["^ ","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":[{"->":".^.^.^.^.find_something_to_smash_window"},{"#f":5}]}],{"#f":1}],"time_to_move_now":[["^Enough of this. There isn't any time to lose. Right now they'll be following Hooper as he goes to bed, and goes to sleep; and then that's it. The minute he closes his eyelids and drifts off that's the moment that this trap swings shut on me.","\n","^So I punch out the glass with my ","ev",{"VAR?":"smashingWindowItem"},2,"==","/ev",[{"->":".^.b","c":true},{"b":["^bucket",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["ev",{"VAR?":"smashingWindowItem"},1,"==","/ev",[{"->":".^.b","c":true},{"b":["^shoe",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^fist",{"->":".^.^.^.7"},null]}],"nop",{"->":".^.^.^.10"},null]}],"nop","^ and it shatters with a terrific noise. Then I stop, and wait, to see if anyone will come in through the door.","\n","^Nothing.","\n","ev","str","^Wait a little longer","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Clear the frame of shards","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^I pause for a moment longer. It doesn't do to be too careless...","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["\n","^With my jacket wrapped round my arm, I sweep out the remaining shards of glass. It's not a big window, but I'm not a big man. If I was Harris, I'd be stuffed, but as it is...","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^Then the door locks turns. The door opens. Then Jeremy — one of the guards, rather — sticks his head through the door. \"I thought I heard...\"","\n","^He stops. Looks for a moment. ","ev",{"VAR?":"smashingWindowItem"},2,"==","/ev",[{"->":".^.b","c":true},{"b":["^Sees the bucket in my hand.",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^Sees the broken window.",{"->":".^.^.^.10"},null]}],"nop","^ Then without a moment's further thought he blows his shrill whistles and hustles into the hut, grabbing me roughly by my arms.","\n","ev",{"CNT?":".^.^.c-0"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^I'll never know if I hadn't have waited that extra moment — maybe I still could have got away. But, how far?","\n",{"->":".^.^.^.17"},null]}],"nop","\n","^I'm hustled into one of the huts. Nowhere to sleep, but they're not interested in my comfort any longer. Harris comes in with the Captain.","\n","^\"So,\" Harris remarks. \"Looks like your little trap worked. Only it worked to show you out for what you are.\"","\n","ev","str","^Tell the truth","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-4","flg":20},{"c-2":["^ ","\n","ev",{"CNT?":"i_met_a_young_man"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^\"Please, Harris. You can't understand the pressure they put me under. You can't understand what it's like, to be in love but be able to do nothing about it...\"","\n",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["\n","^\"Harris. They were blackmailing me. They knew about... certain indiscretions. You can understand, can't you, Harris? I was in an impossible bind...\"","\n",{"->":".^.^.^.7"},null]}],"nop","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-3":["\n","^\"I had to get out, Harris. I had to provoke Hooper into doing something that would incriminate himself fully. He's too clever, you see...\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-4":["^ ","\n","^\"This proves nothing,\" I reply stubbornly. \"You still don't have the component and without it, I don't see what you can hope to prove.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":["^\"Be quiet, man. We know all about your and your sordid affairs.\" The Captain curls his lip. \"Don't you know there's a war on? Do you know the kind of place they would have sent you if it haven't had been for that brain of yours? Don't you think you owe it to your country to use it a little more?\"","\n","^Do I, I wonder? Do I owe this country anything, this country that has spurned who and what am I since the day I became a man?","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-7","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-8","flg":20},{"c-5":["^ ","\n","^My anger deflates like a collapsing equation, all arguments cancelling each other out. The world, of course, owes me nothing; and I owe it everything.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-6":["^ ","\n","^Of course not. I am alone; that is what they wanted me to be, because of who and what I love. So I have no nation, no country.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-7":["^ \t",{"->":".^.^.c-6"},"\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-8":["^ \t","\n","^But what is a country, after all? A country is not a concept, not an ideal. Every country falls, its borders shift and move, its language disappears to be replaced by another. Neither the Reich nor the British Empire will survive forever, so what use is my loyalty to either? ","\n","^I may as well, therefore, look after myself. Something I have attempted, but failed miserably, to do.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"#f":5}],"g-2":["^\"I'm afraid we have only one option, Manning,\" Harris says. \"Please, man. Tell us where the component is.\"","\n","ev",true,"/ev",{"VAR=":"notraitor","re":true},"ev",false,"/ev",{"VAR=":"losttemper","re":true},"ev","str","^Tell them","/str","/ev",{"*":".^.c-9","flg":20},"ev","str","^Say nothing","/str","/ev",{"*":".^.c-10","flg":20},{"c-9":["\n","ev",false,"/ev",{"VAR=":"revealedhooperasculprit","re":true},"^\"All right.\" I am beaten, after all. \"","<>",{"->":"reveal_location_of_component"},"\n",{"#f":5}],"c-10":["^ ",{"->":"my_lips_are_sealed"},"\n",{"#f":5}],"#f":5}]}],{"#f":1}],"find_something_to_smash_window":[["^Let me see. There's the bunk, ","ev",{"VAR?":"smashingWindowItem"},"!",2,"==","/ev",[{"->":".^.b","c":true},{"b":["^a bucket,",{"->":".^.^.^.8"},null]}],"nop","^ nothing else. I have my jacket but nothing in the pockets — no handkerchief, for instance.","\n",["ev","str","^The bunk","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^The jacket","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^The bucket","/str",{"VAR?":"smashingWindowItem"},"!",2,"==","/ev",{"*":".^.c-2","flg":21},{"c-0":["^ \t","\n","^The bunk has a solid metal frame, a blanket, a pillow, nothing more.","\n",[["ev","str","^The frame","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^The blanket","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^The pillow","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Something else","/str",{"CNT?":".^"},1,">","/ev",{"*":".^.c-3","flg":21},{"c-0":["\n","^The frame is heavy and solid. I couldn't lift it or shift it without help from another man. And it wouldn't do me any good here anyway. I can reach the window perfectly well.","\n",{"->":".^.^"},{"#f":5}],"c-1":["^ ","\n","^The blanket. Perfect. I scoop it up off the bed and hold it in place over the window. ",{"->":"smash_the_window"},"\n",{"#f":5}],"c-2":["^ ","\n","^The pillow is fat and fluffy. I could put it over the window and it would muffle the sound of breaking glass, certainly; but I wouldn't be able to break any glass through it either.","\n",{"->":".^.^"},{"#f":5}],"c-3":["^ ",{"->":".^.^.^.^.^"},"\n",{"#f":5}],"#f":5,"#n":"bunk_opts"}],null],{"#f":5}],"c-1":["^ ","\n","^I slip off my jacket and hold it with one hand over the glass. ",{"->":"smash_the_window"},"\n",{"#f":5}],"c-2":["^ ","\n","^The bucket? Hardly. The bucket might do some good if I wanted to sweep up the glass afterwards, but it won't help me smash the glass quietly.","\n",{"->":".^.^"},{"#f":5}],"#f":5,"#n":"opts"}],null],{"#f":1}],"#f":1}],"smash_the_window":[["^Then I heft ","ev",{"VAR?":"smashingWindowItem"},2,"==","/ev",[{"->":".^.b","c":true},{"b":["^up the bucket — this really is quite a fiddly thing to be doing in cuffs — ",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["ev",{"VAR?":"smashingWindowItem"},1,"==","/ev",[{"->":".^.b","c":true},{"b":["^ my shoe by its toe, ",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^back my arm, ",{"->":".^.^.^.7"},null]}],"nop",{"->":".^.^.^.8"},null]}],"nop","^ and take a strong swing, trying to imagine it's Harris' face on the other side.","\n","ev",true,"/ev",{"VAR=":"smashedglass","re":true},"ev",0,"/ev",{"VAR=":"smashingWindowItem","re":true},"ev","str","^Smash!","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^The sound of the impact is muffled. With my arm still covered, I sweep out the remaining glass in the frame.","\n",["^I'm ready to escape. The only trouble is — when they look in on me in the morning, there will be no question what has happened. It won't help me one jot with shifting suspicion off my back.","\n","ev","str","^Wait","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Slip out","/str","/ev",{"*":".^.c-2","flg":20},{"c-1":["\n","^So perhaps I should wait it out, after all. Who knows? I might have a better opportunity later.","\n",{"->":"night_passes"},{"->":".^.^.^.^.g-2"},{"#f":5}],"c-2":["^ ","\n","^Moving quickly and quietly, I hoist myself up onto the window—frame and worm my way outside into the freezing night air. Then I am away, slipping down the paths between the Huts, sticking to the shadows, on my way to Hut 2.","\n",{"->":".^.^.^.^.g-2"},{"#f":5}],"#f":5,"#n":"g-1"}],{"#f":5}],"g-2":["ev","str","^Go the shortest way","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Take a longer route","/str","/ev",{"*":".^.c-4","flg":20},{"c-3":["^ ","\n","^There's no time to lose. Throwing caution to the wind I make my way quickly to Hut 2, and around the back. I don't think I've been seen but if I have it is too late. My actions are suspicious enough for the noose. I have no choice but to follow through.","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-4":["\n","^In case I'm being followed, I divert around the perimeter of the compound. It's a much longer path, and it takes me across some terrain that's difficult to negotiate in the dark — muddy, and thick with thistles and nestles.","\n","ev",true,"/ev",{"VAR=":"muddyshoes","re":true},"^Still, I can be confident no—one is behind me. I crouch down behind the rear wall of Hut 2. ","<>","\n",{"->":".^.^.^.g-3"},{"#f":5}],"#f":5}],"g-3":["^The component is still there, wrapped in a tea—towel and shoved into a cavity in a breeze—block at the base of the Hut wall.","\n","ev","str","^Take it","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Leave it","/str","/ev",{"*":".^.c-6","flg":20},{"c-5":["^ ","\n","^Quickly, I pull it free, and slip it into the pocket of my jacket.","\n","ev",true,"/ev",{"VAR=":"gotcomponent","re":true},{"->":".^.^.^.g-4"},{"#f":5}],"c-6":["^ ","\n","^Still there means no—one has found it, which means it is probably well—hidden. And short of skipping the compound now, I can afford to leave it hidden there a while longer. So I leave it in place.","\n",{"->":".^.^.^.g-4"},{"#f":5}],"#f":5}],"g-4":["^Where now?","\n","ev","str","^Back to the barracks","/str","/ev",{"*":".^.c-7","flg":20},"ev","str","^Go to Hooper's dorm","/str",{"VAR?":"gotcomponent"},"/ev",{"*":".^.c-8","flg":21},"ev","str","^Escape the compound","/str","/ev",{"*":".^.c-9","flg":20},{"c-7":["^ ",{"->":"return_to_room_after_excursion"},"\n",{"#f":5}],"c-8":["^ ",{"->":"go_to_hoopers_dorm"},"\n",{"#f":5}],"c-9":["^ ","\n","^Enough of this place. Time for me to get moving. I can get to the train station on foot, catch the postal train to Scotland and be somewhere else before anyone realises that I'm gone.","\n","^Of course, then they'll be looking for me in earnest. ","ev",{"VAR?":"framedhooper"},"!","/ev",[{"->":".^.b","c":true},{"b":["^As a confirmed traitor.",{"->":".^.^.^.11"},null]}],[{"->":".^.b"},{"b":["^Perhaps not as a traitor — they might take the idea that Hooper was involved with the theft — but certainly as a valuable mind, one containing valuable secrets and all too easily threatened. They will think I am running away because of my indiscretions. I suppose, in fairness, that I am.",{"->":".^.^.^.11"},null]}],"nop","\n",["ev","str","^Go","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't go","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ \t\t\t",{"->":"live_on_the_run"},"\n",{"#f":5}],"c-1":["^ ","\n","^It's no good. That's only half a solution. I couldn't be happy with that.","\n",["ev","str","^Back to the barracks","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^To Hooper's dorm","/str",{"VAR?":"gotcomponent"},{"CNT?":"go_to_hoopers_dorm"},"!","&&","/ev",{"*":".^.c-1","flg":21},{"c-0":["^ \t\t\t",{"->":"return_to_room_after_excursion"},"\n",{"#f":5}],"c-1":["^ ",{"->":"go_to_hoopers_dorm"},"\n",{"#f":5}]}],{"#f":5}]}],{"#f":5}],"#f":5}]}],{"#f":1}],"go_to_hoopers_dorm":[["^I creep around the outside of the huts towards Hooper's dorm. Time to wrap up this little game once and for all. A few guards patrol the area at night but not many — after all, very few know this place even exists.","\n","^Our quarters are arranged away from the main house; where we sleep is of less importance than where we work. We each have our own hut, through some are less permanent than others. Hooper's is a military issue tent: quite a large canopy, with two rooms inside and a short porch area where he insists people leave their shoes. It's all zipped up for the night and no light shines from inside.","\n","^I hang back for a moment. If Harris is keeping to the terms of our deal then someone will be watching this place. But I can see no—one.","\n","ev","str","^Open the outer zip","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Look for another opening","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Hide the component somewhere","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^I creep forward to the tent, intent on lifting the zip to the front porch area just a little — enough to slip the component inside, and without the risk of the noise waking Hooper from his snoring.","\n","^The work is careful, and more than little fiddly — Hooper has tied the zips down on the inside, the fastidious little bastard! — but after a little work I manage to make a hole large enough for my hand.","\n",["ev","str","^Slip in the component","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No, some other way","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ \t\t","\n","^I slide the component into the tent, work the zip closed, and move quickly away into the shadows. It takes a few minutes for my breath to slow, and my heart to stop hammering, but I see no other movement. If anyone is watching Hooper's tent, they are asleep at their posts.","\n","ev",true,"/ev",{"VAR=":"putcomponentintent","re":true},"ev",false,"/ev",{"VAR=":"gotcomponent","re":true},{"->":"return_to_room_after_excursion"},{"#f":5}],"c-1":["^ \t\t\t","\n","^Then pause. This is too transparent. Too blatant. If I leave it here, like this, Hooper will never be seen to go looking for it: he will stumble over it in plain sight, and the men watching will wonder why it was not there when he went to bed.","\n","^No, I must try something else — or nothing at all.","\n",["ev","str","^On top of the tent","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Throw the component into the long grass","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Give up","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ",{"->":".^.^.^.^.^.^.^.put_component_on_tent"},"\n",{"#f":5}],"c-1":["^ ","\n","^From inspiration — or desperation, I am not certain — a simple approach occurs to me. ",{"->":".^.^.^.^.^.^.^.toss_component_into_bushes"},"\n",{"#f":5}],"c-2":["^ ","\n","^There is nothing to be gained here. I have the component now; maybe it will be of some value tomorrow.","\n",["ev","str","^Return to my barrack","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Escape the compound","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ",{"->":"return_to_room_after_excursion"},"\n",{"#f":5}],"c-1":["^ ",{"->":"live_on_the_run"},"\n",{"#f":5}]}],{"#f":5}]}],{"#f":5}]}],{"#f":5}],"c-1":["^ ","\n","^Making a wide circuit I creep around the tent. It has plenty of other flaps and openings, tied down with Gordian complexity. But nothing afford itself to slipping the component inside.","\n",["ev","str","^Try the porch zip","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Try on top of the tent","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Give up","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ \t\t\t",{"->":".^.^.^.^.c-0"},"\n",{"#f":5}],"c-1":["^ \t\t",{"->":".^.^.^.^.^.put_component_on_tent"},"\n",{"#f":5}],"c-2":["^ \t\t\t\t\t\t","\n","^It's no good. Nothing I can do will be any less than obvious — something appearing where something was not there before. The men watching Hooper will know it is a deception and Hooper's protestations will be taken at face value.","\n","^If I can't find a way for Hooper to pick the component up, as if from a hiding place of his own devising, and be caught doing it, then I have no plan at all.","\n",["ev","str","^Return to my barrack","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Escape the compound","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Toss the component into the bushes","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ",{"->":"return_to_room_after_excursion"},"\n",{"#f":5}],"c-1":["^ ",{"->":"live_on_the_run"},"\n",{"#f":5}],"c-2":["^ ",{"->":".^.^.^.^.^.^.^.toss_component_into_bushes"},"\n",{"#f":5}]}],{"#f":5}]}],{"#f":5}],"c-2":["^ ","\n","^If I leave the component here somewhere it should be somewhere I can rely on Hooper finding it, but no—one before Hooper. In particular.","\n",["ev","str","^Behind the tent","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Inside the porch section","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^On top of the canvas","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^\t\t\t \t",{"->":".^.^.^.^.c-1"},"\n",{"#f":5}],"c-1":["^ \t\t",{"->":".^.^.^.^.c-0"},"\n",{"#f":5}],"c-2":["^ \t\t\t",{"->":".^.^.^.^.^.put_component_on_tent"},"\n",{"#f":5}]}],{"#f":5}]}],{"put_component_on_tent":["^A neat idea strikes me. If I could place it on top of the canvas, somewhere in the middle where it would bow the cloth inwards, then it would be invisible to anyone passing by. But to Hooper, it would be above him: a shadow staring him in the face as he awoke. What could be more natural than getting up, coming out, and looking to see what had fallen on him during the night?","\n","^It's the work of a moment. I was once an excellent bowler for the second XI back at school. This time I throw underarm, of course, but I still land the vital missing component exactly where I want it to go.","\n","ev",true,"/ev",{"VAR=":"framedhooper","re":true},"ev",false,"/ev",{"VAR=":"gotcomponent","re":true},"^For a second I hold my breath, but nothing and no—one stirs. ",{"->":"return_to_room_after_excursion"},"\n",{"#f":1}],"toss_component_into_bushes":["^I toss the component away into the bushes behind Hooper's tent and return to my barrack, wishing myself a long sleep followed by a morning, free of this business.","\n","ev",false,"/ev",{"VAR=":"gotcomponent","re":true},"ev",true,"/ev",{"VAR=":"throwncomponentaway","re":true},{"->":"return_to_room_after_excursion"},{"#f":1}],"#f":1}],"live_on_the_run":["^Better to live on the run than die on the spit. Creeping around the edge of the compound","ev",{"VAR?":"gotcomponent"},"/ev",[{"->":".^.b","c":true},{"b":["^, the Bombe component heavy in my pocket",{"->":".^.^.^.5"},null]}],"nop","^, I make my way to the front gate. As always, it's manned by two guards, but I slip past their box by crawling on my belly.","\n","^And then I'm on the road. Walking, not running. Silent. Free.","\n","^For the moment, at least.","\n","end",{"#f":1}],"return_to_room_after_excursion":[["ev",{"VAR?":"gotcomponent"},"/ev",[{"->":".^.b","c":true},{"b":["^The weight of the Bombe component safely in my jacket",{"->":".^.^.^.5"},null]}],[{"->":".^.b"},{"b":["^Satisfied",{"->":".^.^.^.5"},null]}],"nop","^, I return the short way up the paths between the huts to the barrack block and the broken window.","\n","^It's a little harder getting back through — the window is higher off the ground than the floor inside — but after a decent bit of jumping and hauling I manage to get my elbows up, and then one leg, and finally I collapse inside, quite winded and out breath.","\n","ev","str","^Wait","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["^ \t",{"->":"night_passes"},"\n",{"#f":5}]}],{"#f":1}],"night_passes":[["^The rest of the night passes slowly. I sleep a little, dozing mostly. Then I'm woken by the rooster in the yard. The door opens, and Harris comes in. He takes one look at the broken window and frowns with puzzlement.","\n","ev",{"VAR?":"putcomponentintent"},"/ev",[{"->":".^.b","c":true},{"b":["^ ",{"->":".^.^.^.^.put_component_inside_tent"},{"->":".^.^.^.6"},null]}],"nop","\n","^\"What happened there?\"","\n","ev","str","^Confess","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Deny","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Show him the component","/str",{"VAR?":"gotcomponent"},"/ev",{"*":".^.c-2","flg":21},{"c-0":["^ ","\n","^\"I broke it,\" I reply. There doesn't seem any use in trying to lie. \"I thought I could escape. But I couldn't get myself through.\"","\n","^The Commander laughs. ",{"->":".^.^.^.glad_youre_here"},"\n",{"#f":5}],"c-1":["^ ","\n","^\"I'm not sure. I was asleep: I woke up when someone broke the window. I looked out to see who it was, but they were already gone.\"","\n","^Harris looks at me with puzzlement. \"Someone came by to break the window, and then ran off? That's absurd. That's utterly absurd. Admit it, Manning. You tried to escape and you couldn't get through.\"","\n",["ev","str","^Admit it","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Deny it","/str",{"VAR?":"framedhooper"},"!","/ev",{"*":".^.c-1","flg":21},"ev","str","^Deny it","/str",{"VAR?":"framedhooper"},"/ev",{"*":".^.c-2","flg":21},{"c-0":["\n","^\"All right. ","ev",{"VAR?":"forceful"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^Damn you.",{"->":".^.^.^.8"},null]}],"nop","^ That's exactly it.\"","\n",{"->":".^.^.^.^.^.glad_youre_here"},{"#f":5}],"c-1":["\n","^\"If I wanted to escape, I would have made damn sure that I could,\" I tell him sternly.","\n",{"->":"harris_certain_is_you"},{"#f":5}],"c-2":["^ ","\n","^\"I tell you, someone broke it. Someone wanted to threaten me, I think.\"","\n","^Harris shakes his head. \"Well, we can look into that matter later. For now, you probably want to hear the more pressing news. ",{"->":".^.^.^.^.^.found_missing_component"},"\n",{"#f":5}]}],{"#f":5}],"c-2":["^ ",{"->":".^.^.^.someone_threw_component"},"\n",{"#f":5}]}],{"put_component_inside_tent":[["^He takes one look around, and sighs, a deep, wistful sigh.","\n","^\"Things just get worse and worse for you, Manning,\" he remarks. \"You are your own worst enemy.\"","\n","ev","str","^Agree","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Disagree","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^\"I've thought so before.\" ","ev",{"VAR?":"admitblackmail"},"/ev",[{"->":".^.b","c":true},{"b":["^Certainly in the matter of getting blackmailed.",{"->":".^.^.^.7"},null]}],"nop","\n","^\"Let me tell you what happened this morning. ","<>","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["\n","^\"Right now, I think you take that role, Harris,\" I reply coolly.","\n",[["^\"Very droll,\" he replies. \"Let me tell you what happened this morning. It will take the smile off your face. ","<>","\n",{"->":".^.^.^.^.g-0"},{"#f":5,"#n":"droll"}],null],{"#f":5}],"c-2":["^ ","\n","^\"I'm looking forward to having a wash and a change of clothes; which should make me a little less evil to be around.\"","\n",{"->":".^.^.c-1.3.droll"},{"->":".^.^.g-0"},{"#f":5}],"g-0":["^Our men watching Hooper's tent saw Hooper wake up, get dressed, clamber out of his tent and then step on something in at the entrance of his tent.\"","\n","ev",true,"/ev",{"VAR=":"piecereturned","re":true},"ev","str","^Be interested","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Be dismissive","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Say nothing","/str","/ev",{"*":".^.c-5","flg":20},{"c-3":["^ ","\n","^\"You mean he didn't even hide it? He put it in his shoe?\"","\n",[["^\"No,\" Harris replies. \"That isn't really what I mean. ","<>","\n",{"->":".^.^.^.^.^.g-1"},{"#f":5,"#n":"not_that"}],null],{"#f":5}],"c-4":["\n","^\"So he's an idiot, and he hid it in his shoe.\"","\n",{"->":".^.^.c-3.4.not_that"},{"->":".^.^.^.g-1"},{"#f":5}],"c-5":["^ ","\n","^I say quiet, listening, not sure how this will go.","\n","^\"In case I'm not making myself clear,\" Harris continues, \"","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":["^I mean, he managed to find it, by accident, somewhere where it wasn't the night before. And at the same time, you're sitting here with your window broken. So, I rather think you've played your last hand and lost. It's utterly implausible that Hooper stole that component and then left it lying around in the doorway of his tent. So I came to tell you that the game is up, for you.\"","\n","^He nods and gets to his feet. ",{"->":"left_alone"},"\n",{"#f":5}]}],{"#f":1}],"someone_threw_component":[["^\"Someone threw this in through the window over night,\" I reply, and open my jacket to reveal the component from the Bombe. \"I couldn't see who, it was too dark. But I know what it is.\"","\n","^He reaches out and takes it. \"Well, I'll be damned,\" he murmurs. \"That's it all right. And you didn't have it on you when we put you in here. But it can't have been Hooper — I had men watching him all night. And there's no—one else it could have been.\"","\n","^He turns the component over in his hands, bemused.","\n","ev",true,"/ev",{"VAR=":"piecereturned","re":true},"ev","str","^Suggest something","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Suggest nothing","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"Perhaps Hooper had an accomplice. Someone else who works on site.\"","\n","^Harris shakes his head, distractedly. \"That doesn't make sense,\" he says. \"Why go to all the trouble of stealing it only to give it back? And why like this?\"","\n",["ev","str","^Suggest something","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Suggest nothing","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"Perhaps the accomplice thought it was Hooper being kept in here. Maybe they saw the guard...\"","\n",{"->":"all_too_farfetched"},{"->":".^.^.^.^.g-0"},{"#f":5}],"c-1":["^ ","\n",{"->":".^.^.^.^.g-0"},{"#f":5}]}],{"#f":5}],"c-1":["^ ","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^I shrug, eloquently.","\n",[{"->":"all_too_farfetched"},{"#f":5,"#n":"g-1"}],{"#f":5}]}],{"#f":1}],"glad_youre_here":[["^\"Shame,\" he remarks. \"I should have left that window open and put a guard on you. Might have been interesting to see where you went. Anyway, I'm glad you're still here, even if you do smell like a dog.\"","\n","ev","str","^Be optimistic","/str",{"VAR?":"framedhooper"},"!","/ev",{"*":".^.c-0","flg":21},"ev","str","^Be pessimistic","/str",{"VAR?":"framedhooper"},"!","/ev",{"*":".^.c-1","flg":21},"ev","str","^Be optimistic","/str",{"VAR?":"framedhooper"},"/ev",{"*":".^.c-2","flg":21},"ev","str","^Be pessimistic","/str",{"VAR?":"framedhooper"},"/ev",{"*":".^.c-3","flg":21},{"c-0":["^ ","\n",{"->":"night_falls.morning_not_saved.0.c-0"},{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n",{"->":"night_falls.morning_not_saved.0.c-1"},{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^\"I'm looking forward to having a bath.\"","\n","^\"Well, you should enjoy it. ","<>","\n",{"->":".^.^.g-0"},{"#f":5}],"c-3":["\n","^\"I imagine I'll smell worse after another couple of days of this.\"","\n","^\"That won't be necessary. ","<>","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":".^.^.^.^.found_missing_component"},{"#f":5}]}],{"#f":1}],"found_missing_component":[["^We found the missing component. Or rather, Hooper found it for us. He snuck out and retrieved it from on top. Of all the damnest places — you would never have known it was there. He claimed ignorance when we jumped him, of course. But it's good enough for me.\"","\n","ev","str","^Approve","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Disapprove","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"I can't tell you enough, I'm glad to hear it. I've had a devil of a night.\"","\n","^His gaze flicks to the broken window, but only for a moment. I think he genuinely cannot believe I could have done it.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"You should never have hired him. A below-average intelligence can't be expected to cope with the pressure of our work.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^Harris rolls his eyes, but he might almost be smiling. \"You'd better get along, ","ev",{"CNT?":".^.^.c-0"},"/ev",[{"->":".^.b","c":true},{"b":["^and work through your devils",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["^Mr Intelligent",{"->":".^.^.^.6"},null]}],"nop","^. There's a 24—hour—late message to be tackled and we're a genius short. So you'd better be ready to work twice as hard.\"","\n","ev","str","^Thank him","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Argue with him","/str","/ev",{"*":".^.c-3","flg":20},{"c-2":["^ \t","\n","^\"I'll enjoy it. Thank you for helping me clear this up.\"","\n","^\"Don't thank me yet. There's still a war to fight. Now get a move on.\"","\n","^I nod, and hurry out of the door. The air outside has never tasted fresher and more invigorating. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-3":["^ ","\n","^\"I'll work as hard as I work.\"","\n","^\"Get out,\" Harris growls. \"Before I decide to arrest you as an accessory.\"","\n","^I do as he says. Outside the barrack, the air has never smelt sweeter.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":[{"->":"head_for_my_dorm_free"},{"#f":5}]}],{"#f":1}],"#f":1}],"night_falls":[["^Night falls. The clockwork of the heavens keeps turning, whatever state I might be in. No—one can steal the components that make the sun go down and the stars come out. I watch it performing its operations. I can't sleep.","\n","ev",{"VAR?":"hooperClueType"},0,">","/ev",[{"->":".^.b","c":true},{"b":["\n","^Has Hooper taken my bait?","\n",{"->":".^.^.^.8"},null]}],"nop","\n","ev","str","^Look of out the window","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Listen at the door","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Wait","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^I peer out of the window, but it looks out onto the little brook at the back of the compound, with no view of the other huts or the House. Who knows if there are men up, searching the base of Hut 2, following one another with flashlights...","\n","ev",{"CNT?":"inside_hoopers_hut.back_of_hut_2"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^Perhaps Hooper is there, in the dark, trying to help me after all?","\n",{"->":".^.^.^.8"},null]}],"nop","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ \t","\n","^I put my ear to the keyhole but can make out nothing. Are there still guards posted? ","ev",{"VAR?":"hooperClueType"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^Perhaps, if Hooper has managed to incriminate himself, the guards have been removed?",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^Perhaps the component has been found and the crisis is over.",{"->":".^.^.^.10"},null]}],"nop","\n","^Perhaps the door is unlocked and they left me to sleep?","\n",["ev","str","^Try it","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Leave it","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ I try the handle. No such luck.","\n",{"->":".^.^.^.^.g-0"},{"#f":5}],"c-1":["^ I don't touch it. I don't want anyone outside thinking I'm trying to escape.","\n",{"->":".^.^.^.^.g-0"},{"#f":5}]}],{"#f":5}],"c-2":["^ \t\t\t\t\t","\n","^There is nothing I can do to speed up time.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^The night moves at its own pace. I suppose by morning I will know my fate.","\n","ev","str","^Wait","/str",{"VAR?":"hooperClueType"},0,">","/ev",{"*":".^.c-3","flg":21},"ev","str","^Wait","/str",{"VAR?":"hooperClueType"},0,"==","/ev",{"*":".^.c-4","flg":21},{"c-3":["^ ","\n","^Morning comes. I'm woken by a rooster calling from the yard behind the House. I must have slept after all. I pull myself up from the bunk, shivering slightly. There is condensation on the inside of the window. I have probably given myself a chill.","\n","^Without knocking, Harris comes inside. \"You're up,\" he remarks, and then, \"You smell like an animal.\"","\n",["ev","str","^Be friendly","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Be cold","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"I suppose I do rather.\" I laugh, but Harris does not.","\n","^\"This damn business gets worse and worse,\" he says, talking as he goes over to unlock and throw open the window. ","<>","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"So would you,\" I reply tartly. Harris shrugs.","\n","^\"I've been through worse than this,\" he replies matter—of—factly. \"It's hardly my fault if you sleep in your clothes.\"","\n","^I glare back. He goes over to the window, unlocks it and throws it open, relishing the fresh air from outside.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"Hooper's confessed, you know.\"","\n","ev","str","^Be eager","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Be cautious","/str","/ev",{"*":".^.c-3","flg":20},{"c-2":["^ ","\n","^\"He has? I knew he would. The worm.\"","\n","^\"Steady now. Matters aren't over yet. ","<>","\n",{"->":".^.^.^.hooper_didnt_give_himself_up"},{"#f":5}],"c-3":["^ ","\n","^\"Oh, yes?\"","\n","^\"Yes. For what that's worth. ","<>","\n",{"->":".^.^.^.hooper_didnt_give_himself_up"},{"#f":5}],"#f":5}],"hooper_didnt_give_himself_up":["^There's still the issue of the component. It hasn't turned up. He didn't lead us to it. I guess he figured you must have had something on him. I don't know.\"","\n","^He looks quite put out by the whole affair. He is not the kind of man to deal well with probabilities.","\n","ev","str","^Be interested","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Be disinterested","/str","/ev",{"*":".^.c-5","flg":20},{"c-4":["^ ","\n","^\"You mean he confessed of his own accord? You didn't catch him?\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-5":["^ ","\n","^\"Well, I'm glad his conscience finally caught up with him,\" I reply dismissively.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":["^\"The Captain went back into that hut and he confessed immediately. We were so surprised we didn't let you go.\" He wrinkles his nose. \"I'm rather sorry about that now. I suggest you have a wash.\"","\n","^And with that he gestures to the doorway.","\n","ev","str","^Go","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Wait","/str","/ev",{"*":".^.c-7","flg":20},{"c-6":["^ ","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-7":["^ ","\n","^I hang back a moment. Something does not seem quite right. After all, Hooper did not steal the component. He has no reason to confess to anything. Perhaps this is another trap?","\n","^\"Well?\" Harris asks. \"What are you waiting for? Please don't tell me you want to confess now as well, I don't think my head could stand it.\"","\n",["ev","str","^Confess","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't confess","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^After a chance like this? A chance — however real — to save my neck? To hand it over — what, to save Hooper's worthless skin?","\n",["ev","str","^Confess","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't confess","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^I see. Perhaps you think I bullied the man into giving himself up. Perhaps he understood my little clue far enough to know it was a threat against him, but not well enough to understand where he should look to find it. So he took the easy route out and folded. Gave me the hand.","\n","ev",true,"/ev",{"VAR=":"hooperConfessed","re":true},"^Hardly sporting, of course.","\n",["ev","str","^Confess","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't confess","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^Well, then. I suppose this must be what it feels like to have a conscience. I suppose I had always wondered.","\n","^\"Harris, sir. I don't know what Hooper's playing at, sir. But I can't let him do this.\"","\n","^\"Do what?\"","\n","^\"Take the rope for this. I took it, sir.","\n","ev",false,"/ev",{"VAR=":"revealedhooperasculprit","re":true},"ev",false,"/ev",{"VAR=":"losttemper","re":true},{"->":"reveal_location_of_component"},{"->":".^.^.^.^.^.^.g-0"},{"#f":5}],"c-1":["^ ","\n",{"->":".^.^.^.^.^.^.g-0"},{"#f":5}]}],{"#f":5}],"c-1":["^ ","\n",{"->":".^.^.^.^.g-0"},{"#f":5}]}],{"#f":5}],"c-1":["^ ","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"I certainly don't. But still, I'm surprised. I had Hooper down for a full—blown double agent, a traitor. He knows he'll face the rope, doesn't he?\"","\n","^\"Don't ask me to explain why he did what he did,\" Harris sighs. \"Just be grateful that he did, and you're now off the hook.\"","\n",{"->":".^.^.^.^.^.g-2"},{"#f":5}]}],{"#f":5}],"#f":5}],"g-2":["^Curiouser and curiouser. I nod once to Harris and slip outside into the cold morning air.","\n","ev",{"VAR?":"hooperClueType"},0,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","^Hooper's confession only makes sense in one fashion","ev",{"VAR?":"hooperConfessed"},"/ev",[{"->":".^.b","c":true},{"b":["^, and that is his being dim—witted and slow",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^ — if I successfully implied to him that I had him framed, but he did not unpack my little clue well enough to go looking for the component. Well, I had figured him for a more intelligent opponent, but a resignation from the game will suffice",{"->":".^.^.^.7"},null]}],"nop","^. Or perhaps he knew he would be followed if he went to check, and decided he would be doomed either way.","\n",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["\n","^Hooper's confession only makes sense in one way — and that's that he believed me. He reasoned that he would be followed. To try and uncover the component would have got him arrested, and to confess was the same.","\n","^He simply caved, and threw in his hand.","\n",{"->":".^.^.^.9"},null]}],"nop","\n","^Of course, however, there is only one way to be certain that Harris is telling the truth, and that is to check the breeze—block at the back of Hut 2.","\n","ev","str","^Check","/str","/ev",{"*":".^.c-8","flg":20},"ev","str","^Don't check","/str","/ev",{"*":".^.c-9","flg":20},{"c-8":["^ ",{"->":"go_to_where_component_is_hidden"},"\n",{"#f":5}],"c-9":["\n","^But there will time for that later. If there is nothing there, then Hooper discovered the component after all and Harris' men will have swooped on him, and the story about his confession is just a ruse to test me out.","\n","^And if the component is still there — well. It will be just as valuable to my contact in a week's time, and his deadline of the 31st is not yet upon us.","\n",{"->":"head_for_my_dorm_free"},{"#f":5}],"#f":5}]}],{"#f":5}],"c-4":["^ ",{"->":".^.^.^.^.morning_not_saved"},"\n",{"#f":5}],"#f":5}]}],{"morning_not_saved":[["^Morning comes with the call of a rooster from the yard of the House. I must have slept after all. I pull myself up off the bunk, shivering slightly. There is condensation on the inside of the window. I have probably given myself a chill.","\n","^It's not long after that Harris enters the hut. He closes the door behind him, careful as ever, then takes a chair across from me.","\n","^\"You smell like a dog,\" he remarks.","\n","ev","str","^Be optimistic","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Be pessimistic","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"I'm looking forward to a long bath,\" I reply. \"And getting back to work.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"So would you after the night I've had.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"harris_certain_is_you"},{"#f":5}]}],{"#f":1}],"#f":1}],"harris_certain_is_you":["^\"Well, I'm afraid it is going to get worse for you,\" Harris replies soberly. \"We followed Hooper, and he took himself neatly to bed and slept like a boy scout. Which puts us back to square one, and you firmly in the frame. And I'm afraid I don't have time for any more games. I want you to tell me where that component is, or we will hang you as a traitor.\"","\n","ev",false,"/ev",{"VAR=":"revealedhooperasculprit","re":true},"ev",false,"/ev",{"VAR=":"losttemper","re":true},{"->":"harris_threatens_lynching"},{"#f":1}],"head_for_my_dorm_free":[["^I head for my dorm, intent on a bath, breakfast, a glance at the crossword before the other men get to it, and then on with work. They should have replaced the component in the Bombe by now. We will only be a day behind.","\n","ev",{"VAR?":"framedhooper"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^And then everything will proceed as before. The component will mean nothing to the Germans — this is the one fact I could never have explained to a man like Harris, even though the principle behind the Bombe is the same as the principle behind the army. The individual pieces — the men, the components — do not matter. They are identical. It is how they are arranged that counts.","\n",{"->":".^.^.^.7"},null]}],"nop","\n","^I bump into Russell in the dorm hut.","\n","^\"Did you hear?\" he whispers. \"Terrible news about Hooper. Absolutely terrible.\"","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["^ ","\n","^\"Quite terrible. I would never have guessed.\"","\n","^\"Well.\" Russell harrumphs.","\n",[["^\"Quince was saying this morning, apparently his grandfather was German. So perhaps it's to be expected. See you there?\"","\n",{"->":".^.^.^.^.g-0"},{"#f":5,"#n":"quince"}],null],{"#f":5}],"c-1":["\n","^\"Heard what?\"","\n",[["^\"Hooper's been taken away. They caught him, uncovering that missing Bombe component from a hiding place somewhere, apparently about to take it to his contact.\" Russell harrumphs. ",{"->":".^.^.^.^.c-0.6.quince"},"\n",{"->":".^.^.^.^.g-0"},{"#f":5,"#n":"hooper_taken"}],null],{"#f":5}],"c-2":["^ ","\n","^\"I don't know what you're talking about.\"","\n",{"->":".^.^.c-1.3.hooper_taken"},{"->":".^.^.g-0"},{"#f":5}],"c-3":["\n","^\"If you'll excuse me, Russell. I was about to take a bath.\"","\n","^\"Oh, of course. Worked all night, did you? Well, you'll hear soon enough. Can hardly hide the fact there'll only be three of us from now on.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^I wave to him and move away, my thoughts turning to the young man in the village. My lover. My contact. My blackmailer. Hooper may have taken the fall for the missing component, but ","ev",{"VAR?":"framedhooper"},"!","/ev",[{"->":".^.b","c":true},{"b":["^if he did recover it from Hut 2 then ",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^ its recovery does mean ",{"->":".^.^.^.7"},null]}],"nop","^I have nothing to sell to save my reputation","ev",{"CNT?":"i_met_a_young_man"},"/ev",[{"->":".^.b","c":true},{"b":["^, if I have any left",{"->":".^.^.^.13"},null]}],"nop","^.","\n","ev",{"VAR?":"framedhooper"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^If he didn't, of course, and Harris was telling the truth about his sudden confession, then I will be able to buy my freedom once and for all.","\n",{"->":".^.^.^.21"},null]}],"nop","\n","ev","str","^Get the component","/str",{"VAR?":"framedhooper"},"!","/ev",{"*":".^.c-4","flg":21},"ev","str","^Leave it","/str",{"VAR?":"framedhooper"},"!","/ev",{"*":".^.c-5","flg":21},"ev","str","^Act normal","/str","/ev",{"*":".^.c-6","flg":20},{"c-4":["^ ",{"->":"go_to_where_component_is_hidden"},"\n",{"#f":5}],"c-5":["^ ","\n","^I will have to leave that question for another day. To return there now, when they're probably watching my every step, would be suicide. After all, if Hooper ","ev",{"VAR?":"hooperClueType"},1,"==","/ev",[{"->":".^.b","c":true},{"b":["^followed",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^understood",{"->":".^.^.^.10"},null]}],"nop","^ my clue, he will have explained it to them to save his neck. They won't believe him — but they won't quite disbelieve him either. We're locked in a cycle now, him and me, of half—truth and probability. There's nothing either of us can do to put the other entirely into blame.","\n",{"->":"ending_return_to_normal"},{"#f":5}],"c-6":["^ ","\n","^But there is nothing to be done about it. ",{"->":"ending_return_to_normal"},"\n",{"#f":5}],"#f":5}]}],{"#f":1}],"ending_return_to_normal":[["^Nothing, that is, except to act as if there is no game being played. I'll have a bath, then start work as normal. I've got a week to find something to give my blackmailer","ev",{"CNT?":"i_met_a_young_man"},"/ev",[{"->":".^.b","c":true},{"b":["^ — or give him nothing: it seems my superiors know about my indiscretions now already",{"->":".^.^.^.5"},null]}],"nop","^.","\n","ev","str","^Co-operate","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Dissemble","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["^ ","\n","^Something will turn up. It always does. An opportunity will present itself, and more easily now that Hooper is out of the way.","\n","^But for now, there's yesterday's intercept to be resolved.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^Or perhaps I might hand my young blackmailer over my superiors instead for being the spy he is.","\n","^Perhaps that would be the moral thing to do, even, and not just the most smart.","\n","^But not today. Today, there's an intercept to resolve.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["\n","^In a week's time, this whole affair will be in the past and quite forgotten. I'm quite sure of that. ",{"->":".^.^.c-3"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ I've more important problems to think about now. There's still yesterday's intercept to be resolved. ","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^The Bombe needs to be set up once more and set running.","\n","^It's time I tackled a problem I can solve.","\n","end",{"#f":5}]}],{"#f":1}],"go_to_where_component_is_hidden":[["^It won't take a moment to settle the matter. I can justify a walk past Hut 2 as part of my morning stroll. It will be obvious in a moment if the component is still there.","\n","^On my way across the paddocks, between the huts and the House, I catch sight of young Miss Lyon, arriving for work on her bicycle. She giggles as she sees me and waves.","\n","ev","str","^Wave back","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Ignore her","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^I wave cheerily back and she giggles, almost drops her bicycle, then dashes away inside the House. Judging by the clock on the front gable, she's running a little late this morning.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^I give no reaction. She sighs to herself, as if this kind of behaviour is normal, and trots away inside the House to begin her duties.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^I turn the corner of Hut 3 and walk down the short gravel path to Hut 2. It was a good spot to choose — Hut 2 is where the electricians work, and they're generally focussed on what they're doing. They don't often come outside to smoke a cigarette so it's easy to slip past the doorway unnoticed.","\n","ev","str","^Check inside","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Go around the back","/str","/ev",{"*":".^.c-3","flg":20},{"c-2":["^ \t\t","\n","^I hop up the steps and put my head inside all the same. Nobody about. Still too early in the AM for sparks, I suppose. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-3":["^ ","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":["^I head on around the back of the hut. The breeze—block with the cavity is on the left side.","\n","ev","str","^Check","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Look around","/str","/ev",{"*":".^.c-5","flg":20},{"c-4":["^ \t\t","\n","^No time to waste. I drop to my knees and check the breeze—block. Sure enough, there's nothing there. Hooper took the bait.","\n","^Suddenly, there's a movement behind me. I look up to see, first a snub pistol, and then, Harris.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-5":["^ ","\n","^I pause to glance around, and catch a glimpse of movement. Someone ducking around the corner of the hut. Or a canvas sheet flapping in the light breeze. Impossible to be sure.","\n",["ev","str","^Check the breeze—block","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Check around the side of the hut","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ",{"->":".^.^.^.^.c-4"},"\n",{"->":".^.^.^.^.^.g-2"},{"#f":5}],"c-1":["^ ","\n","^But too important to guess. I move back around the side of the hut.","\n","^Harris is there, leaning in against the wall. He holds a stub pistol in his hand.","\n",{"->":".^.^.^.^.^.g-2"},{"#f":5}]}],{"#f":5}],"#f":5}],"g-2":["ev",{"VAR?":"hooperClueType"},1,">","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"","ev",{"VAR?":"hooperClueType"},2,"==","/ev",[{"->":".^.b","c":true},{"b":["^Queen to rook two",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["^Messy without one missing whatever it was",{"->":".^.^.^.9"},null]}],"nop","^,\" he declares. \"I wouldn't have fathomed it but Hooper did. Explained it right after we sprung him doing what you're doing now. We weren't sure what to believe but now, you seem to have resolved that for us.\"","\n",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["\n","^\"Hooper said you'd told him where to look. I didn't believe him. Or, well. I wasn't sure what to believe. Now I rather think you've settled it.\"","\n",{"->":".^.^.^.7"},null]}],"nop","\n","ev","str","^Agree","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-7","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-8","flg":20},{"c-6":["^ ","\n","^\"I have, rather.\" I put my hands into my pockets. \"I seem to have done exactly that.\"","\n","^\"I'm afraid my little story about Hooper confessing wasn't true. I wanted to see if you'd go to retrieve the part.\" Harris gestures me to start walking. \"You were close, Manning, I'll give you that. I wanted to believe you. But I'm glad I didn't.\"","\n",{"->":".^.^.^.g-3.done"},{"->":".^.^.^.g-3"},{"#f":5}],"c-7":["^ ","\n","^\"I spoke to Russell. He said he saw Hooper doing something round here. I wanted to see what it was.\"","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-8":["^ ","\n","^\"Harris, you'd better watch out. He's planted a time—bomb here.\"","\n","^Harris stares at me for a moment, then laughs. \"Oh, goodness. That's rich.\"","\n","^I almost wish I had a way to make the hut explode, but of course I don't.","\n",{"->":".^.^.^.g-3"},{"#f":5}],"#f":5}],"g-3":["^\"Enough.\" Harris gestures for me to start walking. \"This story couldn't be simpler. You took it to cover your back. You hid it. You lied to get Hooper into trouble, and when you thought you'd won, you came to scoop your prize. A good hand but ultimately, ","ev",{"VAR?":"hooperClueType"},1,"<=","/ev",[{"->":".^.b","c":true},{"b":["^if it hadn't have been you who hid the component, then you wouldn't be here now",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["^you told Hooper where to look with your little riddle",{"->":".^.^.^.8"},null]}],"nop","^.\"","\n",["^He leads me across the yard. Back towards Hut 5 to be decoded, and taken to pieces, once again.","\n","end",{"#f":5,"#n":"done"}],{"#f":5}]}],{"#f":1}],"harris_threatens_lynching":[["ev",{"CNT?":"harris_certain_is_you"},"/ev",[{"->":".^.b","c":true},{"b":["^He passes a hand across his eyes with a long look of despair.",{"->":".^.^.^.5"},null]}],[{"->":".^.b"},{"b":["^He gets to his feet, and gathers his gloves from the table top.",{"->":".^.^.^.5"},null]}],"nop","\n","^\"I'm going to go outside and organise a rope. That'll take about twelve minutes. That's how long you have to decide.\"","\n","ev","str","^Protest","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Confess","/str",{"VAR?":"gotcomponent"},"!",{"VAR?":"throwncomponentaway"},"!","&&","/ev",{"*":".^.c-1","flg":21},"ev","str","^Stay silent","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Show him the component","/str",{"VAR?":"gotcomponent"},"/ev",{"*":".^.c-3","flg":21},{"c-0":["^ ","\n","^\"You can't do this!\" I cry. \"It's murder! I demand a trial, a lawyer; for God's sake, man, you can't just throw me overboard, we're not barbarians...!\"","\n",[["^\"You leave me no choice,\" Harris snaps back, eyes cold as gun—metal. \"You and your damn cyphers. Your damn clever problems. If men like you didn't exist, if we could just all be straight with one another.\" He gets to his feet and heads for the door. \"I fear for the future of this world, with men like you in. Reich or no Reich, Mr Manning, people like you simply complicate matters.\"","\n",{"->":"left_alone"},{"->":".^.^.^.^.g-0"},{"#f":5,"#n":"too_clever"}],null],{"#f":5}],"c-1":["^ ","\n","^I nod. \"I don't need twelve minutes. ",{"->":"reveal_location_of_component"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ",{"->":"my_lips_are_sealed"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ ","\n","^\"I don't need twelve minutes. Here it is.\"","\n","^I open my jacket and pull the Bombe component out of my pocket. Harris takes it from me, whistling, curious.","\n","^\"Well, I'll be. That's it all right.\"","\n","^\"That's it.\"","\n","^\"But you didn't have it on you yesterday.\"","\n",["ev","str","^Explain","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't explain","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"I climbed out of the window overnight,\" I explain. \"I went and got this from where it was hidden, and brought it back here.\"","\n",{"->":".^.^.^.^.g-0"},{"#f":5}],"c-1":["\n","^\"No. I didn't.\"","\n",{"->":".^.^.^.^.g-0"},{"#f":5}]}],{"#f":5}],"g-0":[{"->":"all_too_farfetched"},"ev","str","^Confess","/str",{"VAR?":"throwncomponentaway"},"/ev",{"*":".^.c-4","flg":21},"ev","str","^Frame Hooper","/str",{"VAR?":"throwncomponentaway"},"/ev",{"*":".^.c-5","flg":21},{"c-4":["\n","^\"I don't need twelve minutes. The component is in the long grass behind Hooper's tent. I threw it there hoping to somehow frame him, but now I see that won't be possible. I was naive, I suppose.\"","\n","ev",true,"/ev",{"VAR=":"piecereturned","re":true},{"->":"reveal_location_of_component.harris_believes"},{"#f":5}],"c-5":["^ ","\n","^\"Look, I know where it is. The missing piece of the Bombe is in the long grasses behind Hooper's tent. I saw him throw it there right after we finished work. He knew you'd scour the camp but I suppose he thought you'd more obvious places first. I suppose he was right about that. Look there. That proves his guilt.\"","\n","ev",true,"/ev",{"VAR=":"longgrasshooperframe","re":true},"ev",true,"/ev",{"VAR=":"piecereturned","re":true},"^\"That doesn't prove anything,\" Harris returns sharply. \"But we'll check what you say, all the same.\" He gets to his feet and heads out of the door.","\n",{"->":"left_alone"},{"#f":5}],"#f":5}]}],{"#f":1}],"reveal_location_of_component":["<>","^ The missing component of the Bombe computer is hidden in a small cavity in a breeze—block supporting the left rear post of Hut 2. I put in there anticipating a search. I intended to ","ev",{"VAR?":"revealedhooperasculprit"},"/ev",[{"->":".^.b","c":true},{"b":["^pass it to Hooper",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^dispose of it",{"->":".^.^.^.7"},null]}],"nop","^ once the fuss had died down. I suppose I was foolish to think that it might.\"","\n","ev",true,"/ev",{"VAR=":"piecereturned","re":true},{"->":".^.harris_believes"},{"harris_believes":["ev",{"CNT?":"night_falls.0.g-0.c-3.6.hooper_didnt_give_himself_up"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"Indeed. And Mr Manning: God help you if you're lying to me.\"","\n",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["\n","^\"I thought as much. I hadn't expected you to give it out so easily, however. You understand, Hooper has said nothing, of course. In fact, he went to Hut 2 directly after we released him and uncovered the component. But he told us you had instructed him where to go. Hence my little double bluff. Frankly, I'll be glad when I'm shot of the lot of you mathematicians.\"","\n",{"->":".^.^.^.6"},null]}],"nop","\n","^Harris stands, and slips away smartly. ",{"->":"left_alone"},"\n",{"#f":1}],"#f":1}],"my_lips_are_sealed":["^I say nothing, my lips tightly, firmly sealed. It's true I am a traitor, to the very laws of nature. The world has taught me that since a very early age. But not to my country — should the Reich win this war, I would hardly be treated as an honoured hero. I was doomed from the very start.","\n","ev",true,"/ev",{"VAR=":"notraitor","re":true},"^I explain none of this. How could a man like Harris understand?","\n","^The Commander takes one look back from the doorway as he pulls it to.","\n","^\"It's been a pleasure working with you, Mr Manning,\" he declares. \"You've done a great service to this country. If we come through, I'm sure they'll remember you name. I'm sorry it had to end this way and I'll do my best to keep it quiet. No—one need know what you did.\"","\n",{"->":"left_alone"},{"#f":1}],"all_too_farfetched":["^\"This is all too far—fetched,\" Harris says. \"I'm glad to have this back, but I need to think.\"","\n","^Getting to his feet, he nods once. \"You'll have to wait a little longer, I'm afraid, Manning.\"","\n","^Then he steps out of the door, muttering to himself.","\n",{"->":"make_your_peace"},{"#f":1}],"left_alone":["ev",{"CNT?":"slam_door_shut_and_gone.time_to_move_now"},"/ev",[{"->":".^.b","c":true},{"b":["^The Commander holds the door for his superior, and follows him out.",{"->":".^.^.^.4"},null]}],"nop","^ Then the door closes. I am alone again, as I have been for most of my short life.","\n",{"->":"make_your_peace"},{"#f":1}],"make_your_peace":[["ev","str","^Make your peace","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^I am waiting again. I have no God to make my peace with. I find it difficult to believe in goodness of any kind, in a world such as this.","\n","ev",{"VAR?":"notraitor"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",true,"/ev",{"VAR=":"notraitor","re":true},"^But I am no traitor. Not to my country. To my sex, perhaps. But how could I support the Reich? If the Nazis were to come to power, I would be worse off than ever.","\n",{"->":".^.^.^.7"},null]}],"nop","\n","ev",{"CNT?":"harris_threatens_lynching.0.c-0.4.too_clever"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^In truth, it is men like Harris who are complex, not men like me. I live to make things ordered, systematic. I like my pencils sharpened and lined up in a row. I do not deal in difficult borders, or uncertainties, or alliances. If I could, I would reduce the world to something easier to understand, something finite.","\n","^But I cannot, not even here, in our little haven from the horrors of the war.","\n",{"->":".^.^.^.13"},null]}],"nop","\n","^I have no place here. No way to fit. I am caught, in the middle, cryptic and understood only thinly, through my machines.","\n",["ev",{"^->":"make_your_peace.0.g-0.17.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^I must seem very calm. \t\t\t",{"->":"$r","var":true},null]}],["ev",{"^->":"make_your_peace.0.g-0.18.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":18},{"s":["^Perhaps I should try to escape.",{"->":"$r","var":true},null]}],{"c-1":["ev",{"^->":"make_your_peace.0.g-0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.17.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-2":["ev",{"^->":"make_your_peace.0.g-0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.18.s"},[{"#n":"$r2"}],"^ But escape to where? I am already a prisoner. Jail would be a blessing. ",{"->":".^.^.^.g-1.monastic"},"\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":["<>","^ I suppose I do not believe they will hang me. They will lock me up and continue to use my brain, if they can. I wonder what they will tell the world — perhaps that I have taken my own life. That would be simplest. The few who know me would believe it.","\n","^Well, then. Not a bad existence, in prison. Removed from temptation.","\n",["^A monastic life, with plenty of problems to keep me going.","\n","^I wonder what else I might yet unravel before I'm done?","\n",["ev",{"^->":"make_your_peace.0.g-1.monastic.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-3","flg":18},{"s":["^The door is opening.",{"->":"$r","var":true},null]}],{"c-3":["ev",{"^->":"make_your_peace.0.g-1.monastic.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.4.s"},[{"#n":"$r2"}],"^ Harris is returning. Our little calculation here is complete. ","ev",{"VAR?":"piecereturned"},"!","/ev",[{"->":".^.b","c":true},{"b":["^ I can only hope one of the others will be able to explain to him that the part I stole will mean nothing to the Germans.",{"->":".^.^.^.13"},null]}],[{"->":".^.b"},{"b":["^We are just pieces in this machine; interchangeable and prone to wear.",{"->":".^.^.^.13"},null]}],"nop","\n",{"->":".^.^.^.^.g-2"},{"#f":5}],"#f":5,"#n":"monastic"}],{"#f":5}],"g-2":["^That is the true secret of the calculating engine, and the source of its power. It is not the components that matter, they are quite repetitive. What matters is how they are wired; the diversity of the patterns and structures they can form. Much like people — it is how they connect that determines our victories and tragedies, and not their genius.","\n","^Which makes me wonder. Should I give ","ev",{"CNT?":"i_met_a_young_man"},"/ev",[{"->":".^.b","c":true},{"b":["^up my beautiful young man",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["^the young man who put me in this spot",{"->":".^.^.^.8"},null]}],"nop","^ to them as well as myself?","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-7","flg":20},{"c-4":["^ ","\n","^But of course I will. ","ev",{"VAR?":"forceful"},2,">","/ev",[{"->":".^.b","c":true},{"b":["^Perhaps I can persuade them to put him in my cell.",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^A little vengeance, disguised as doing something good.",{"->":".^.^.^.10"},null]}],"nop","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-5":["^ ","\n","^No. What would be the use? He will be long gone, and the name he told me is no doubt hokum. No: I was alone before in guilt, and I am thus alone again.","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-6":["^ ","\n","^No. Why would I? He is no doubt an innocent himself, trapped by some dire circumstance. Forced to act the way he did. I have every sympathy for him.","\n","^Of course I do.","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-7":["^ ","\n","^It depends, perhaps, on what his name his worth. If it were to prove valuable, well; perhaps I can concoct a few more such lovers with which to ease my later days.","\n","ev",{"VAR?":"hooper_mentioned"},"/ev",[{"->":".^.b","c":true},{"b":["^ Hooper, perhaps. He wouldn't like that. ",{"->":".^.^.^.8"},null]}],"nop","\n",{"->":".^.^.^.g-3"},{"#f":5}],"#f":5}],"g-3":["ev",{"VAR?":"longgrasshooperframe"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^Harris put the cuffs around my wrists. \"I still have the intercept in my pocket,\" I remark. \"Wherever we're going, could I have a pencil?\"","\n",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["\n","^\"We recovered the part, just where you said it was,\" Harris reports, as he puts the cuffs around my wrists. \"Of course, a couple of the men swear blind they searched there yesterday, so I'm afraid, what with the broken window... we've formed a perfectly good theory which doesn't bode well for you.\"","\n",{"->":".^.^.^.6"},null]}],"nop","\n","ev",true,"/ev",{"VAR=":"piecereturned","re":true},"ev",{"VAR?":"longgrasshooperframe"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^\"I see.\" It doesn't seem worth arguing any further. \"I still have the intercept in my pocket,\" I remark. \"Wherever we're going, could I have a pencil?\"","\n",{"->":".^.^.^.16"},null]}],"nop","\n","^He looks me in the eye.","\n","ev",{"VAR?":"losttemper"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"Of course. And one of your computing things, if I get my way. And when we're old, and smoking pipes together in The Rag like heroes, I'll explain to you the way that decent men have affairs.","\n",{"->":".^.^.^.26"},null]}],[{"->":".^.b"},{"b":["\n","^\"I'll give you a stone to chisel notches in the wall. And that's all the calculations you'll be doing. And as you sit there, pissing into a bucket and growing a beard down to your toes, you have a think about how a smart man would conduct his illicit affairs. With a bit of due decorum you could have learnt off any squaddie.","\n",{"->":".^.^.^.26"},null]}],"nop","\n","<>","^ You scientists.\"","\n","^He drags me up to my feet.","\n","^\"You think you have to re—invent everything.\"","\n","^With that, he hustles me out of the door and I can't help thinking that, with a little more strategy, I could still have won the day. But too late now, of course.","\n","end",{"#f":5}]}],{"#f":1}],"global decl":["ev",0,{"VAR=":"forceful"},0,{"VAR=":"evasive"},false,{"VAR=":"teacup"},false,{"VAR=":"gotcomponent"},false,{"VAR=":"drugged"},false,{"VAR=":"hooper_mentioned"},false,{"VAR=":"losttemper"},false,{"VAR=":"admitblackmail"},0,{"VAR=":"hooperClueType"},false,{"VAR=":"hooperConfessed"},0,{"VAR=":"smashingWindowItem"},false,{"VAR=":"notraitor"},false,{"VAR=":"revealedhooperasculprit"},false,{"VAR=":"smashedglass"},false,{"VAR=":"muddyshoes"},false,{"VAR=":"framedhooper"},false,{"VAR=":"putcomponentintent"},false,{"VAR=":"throwncomponentaway"},false,{"VAR=":"piecereturned"},false,{"VAR=":"longgrasshooperframe"},false,{"VAR=":"DEBUG"},"/ev","end",null],"#f":1}],"listDefs":{}} \ No newline at end of file diff --git a/cli-player/tests/data/test1.ink b/cli-player/tests/data/test1.ink new file mode 100644 index 0000000..2d2349b --- /dev/null +++ b/cli-player/tests/data/test1.ink @@ -0,0 +1,12 @@ +-> choice_test + +=== choice_test === + Test conditional choices + * { true } { false } not displayed + * { true } { true } { true and true } one + * { false } not displayed + * { true } two + * { true } { true } three + * { true } four + + - -> END diff --git a/cli-player/tests/data/test1.ink.json b/cli-player/tests/data/test1.ink.json new file mode 100644 index 0000000..bdfa28f --- /dev/null +++ b/cli-player/tests/data/test1.ink.json @@ -0,0 +1 @@ +{"inkVersion":20,"root":[[{"->":"choice_test"},["done",{"#f":5,"#n":"g-0"}],null],"done",{"choice_test":[["^Test conditional choices","\n",["ev",{"^->":"choice_test.0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",true,false,"&&","/ev",{"*":".^.^.c-0","flg":19},{"s":["^not displayed",{"->":"$r","var":true},null]}],["ev",{"^->":"choice_test.0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",true,true,"&&",true,true,"&&","&&","/ev",{"*":".^.^.c-1","flg":19},{"s":["^one",{"->":"$r","var":true},null]}],["ev",{"^->":"choice_test.0.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",false,"/ev",{"*":".^.^.c-2","flg":19},{"s":["^not displayed",{"->":"$r","var":true},null]}],["ev",{"^->":"choice_test.0.5.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",true,"/ev",{"*":".^.^.c-3","flg":19},{"s":["^two",{"->":"$r","var":true},null]}],["ev",{"^->":"choice_test.0.6.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",true,true,"&&","/ev",{"*":".^.^.c-4","flg":19},{"s":["^three",{"->":"$r","var":true},null]}],["ev",{"^->":"choice_test.0.7.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",true,"/ev",{"*":".^.^.c-5","flg":19},{"s":["^four",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"choice_test.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["ev",{"^->":"choice_test.0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["ev",{"^->":"choice_test.0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.4.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.g-0"},{"#f":5}],"c-3":["ev",{"^->":"choice_test.0.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.5.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.g-0"},{"#f":5}],"c-4":["ev",{"^->":"choice_test.0.c-4.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.6.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.g-0"},{"#f":5}],"c-5":["ev",{"^->":"choice_test.0.c-5.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.7.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["end",{"#f":5}]}],{"#f":1}],"#f":1}],"listDefs":{}} \ No newline at end of file diff --git a/cli-player/tests/test_the_intercept.rs b/cli-player/tests/test_the_intercept.rs new file mode 100644 index 0000000..7893367 --- /dev/null +++ b/cli-player/tests/test_the_intercept.rs @@ -0,0 +1,28 @@ +use assert_cmd::prelude::*; +use std::process::{Command, Stdio}; +use std::io::Write; + +#[test] +fn the_intercept_test() -> Result<(), Box> { + let mut cmd = Command::cargo_bin("binkplayer")?; + + cmd.arg("tests/data/TheIntercept.ink.json"); + cmd.stdin(Stdio::piped()); + cmd.stdout(Stdio::piped()); + + let mut child = cmd.spawn().unwrap(); + let mut stdin = child.stdin.take().unwrap(); + + stdin.write_all(b"1\n2\nquit\n").unwrap(); + + let output = child.wait_with_output()?; + let output_str = String::from_utf8_lossy(&output.stdout); + + assert!(output.status.success()); + assert!(output_str.starts_with("They are keeping me waiting.")); + assert!(output_str.contains("1. Hut 14")); + assert!(output_str.contains("3. Wait")); + assert!(output_str.contains("3. Divert")); + + Ok(()) +} \ No newline at end of file diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 9974e91..9f46ce7 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,6 +1,7 @@ pub mod story; pub mod value_type; pub mod story_error; +pub mod choice; mod json_read; mod json_write; mod object; @@ -15,7 +16,6 @@ mod callstack; mod flow; mod push_pop; mod variables_state; -mod choice; mod error; mod glue; mod void; diff --git a/lib/src/story_state.rs b/lib/src/story_state.rs index c3ca50b..c98bd4b 100644 --- a/lib/src/story_state.rs +++ b/lib/src/story_state.rs @@ -325,7 +325,6 @@ impl StoryState { } } - println!("PUSH: {}", obj.as_ref()); self.evaluation_stack.push(obj); } diff --git a/lib/tests/choice_test.rs b/lib/tests/choice_test.rs index ae329d2..89737ab 100644 --- a/lib/tests/choice_test.rs +++ b/lib/tests/choice_test.rs @@ -191,7 +191,7 @@ fn fallback_choice2_test() -> Result<(), StoryError> { text.clear(); common::next_all(&mut story, &mut text)?; - assert_eq!(true, common::is_ended(&story)); + assert!(common::is_ended(&story)); Ok(()) } From 3d62aafc4db7f1b67e1fef4e4aa9136cc2035b11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Wed, 4 Oct 2023 12:58:59 +0000 Subject: [PATCH 59/91] VariablesState.default_global_variables is not optional. --- README.md | 1 - lib/src/variables_state.rs | 28 +++++++++++++--------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 9509a93..99dc241 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,6 @@ Currently under development. This is the implementation status: ## TODO -- [ ] VariablesState.default_global_variables shouldn't be optionals. - [ ] Optimize control command and Ops getname. Use static string array and address it by order. - [ ] Cache components string in Path - [ ] Variable observers. diff --git a/lib/src/variables_state.rs b/lib/src/variables_state.rs index ae81001..089a6f5 100644 --- a/lib/src/variables_state.rs +++ b/lib/src/variables_state.rs @@ -8,7 +8,7 @@ use crate::{callstack::CallStack, state_patch::StatePatch, variable_assigment::V #[derive(Clone)] pub struct VariablesState { pub global_variables: HashMap>, - pub default_global_variables: Option>>, + pub default_global_variables: HashMap>, pub batch_observing_variable_changes: bool, pub callstack: Rc>, pub changed_variables_for_batch_obs: Option>, @@ -21,7 +21,7 @@ impl VariablesState { pub fn new(callstack: Rc>, list_defs_origin: Rc) -> VariablesState { VariablesState { global_variables: HashMap::new(), - default_global_variables: None, + default_global_variables: HashMap::new(), batch_observing_variable_changes: false, callstack, changed_variables_for_batch_obs: None, @@ -52,7 +52,9 @@ impl VariablesState { } pub fn snapshot_default_globals(&mut self) { - self.default_global_variables = Some(self.global_variables.clone()); + for (k,v) in self.global_variables.iter() { + self.default_global_variables.insert(k.clone(), v.clone()); + } } pub fn apply_patch(&mut self) { @@ -123,10 +125,7 @@ impl VariablesState { fn global_variable_exists_with_name(&self, name: &str) -> bool { self.global_variables.contains_key(name) || self - .default_global_variables - .as_ref() - .map(|variables| variables.contains_key(name)) - .unwrap_or(false) + .default_global_variables.contains_key(name) } // Given a variable pointer with just the name of the target known, resolve @@ -157,7 +156,7 @@ impl VariablesState { pub fn set(&mut self, variable_name: &str, value_type: ValueType) -> Result<(), StoryError> { - if !self.default_global_variables.as_ref().unwrap().contains_key(variable_name) { + if !self.default_global_variables.contains_key(variable_name) { return Err(StoryError::BadArgument(format!("Cannot assign to a variable {} that hasn't been declared in the story", variable_name))); } @@ -182,7 +181,7 @@ impl VariablesState { // Should really warn somehow, but it's difficult to see how...! if let Some(var_contents) = self.global_variables.get(variable_name) { return Some(var_contents.value.clone()); - } else if let Some(var_contents) = self.default_global_variables.as_ref().unwrap().get(variable_name) { + } else if let Some(var_contents) = self.default_global_variables.get(variable_name) { return Some(var_contents.value.clone()); } @@ -222,10 +221,9 @@ impl VariablesState { // 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 let Some(default_globals) = &self.default_global_variables { - if let Some(default_global) = default_globals.get(name) { - return Some(default_global.clone()); - } + + if let Some(default_global) = self.default_global_variables.get(name) { + return Some(default_global.clone()); } if let Some(list_item_value) = self.list_defs_origin.find_single_item_list_with_name(name) { @@ -301,7 +299,7 @@ impl VariablesState { for (name, val) in self.global_variables.iter() { // Don't write out values that are the same as the default global values - let default_val = self.default_global_variables.as_ref().unwrap().get(name); + let default_val = self.default_global_variables.get(name); if let Some(default_val) = default_val { if self.val_equal(val, default_val) {continue;} } @@ -348,7 +346,7 @@ impl VariablesState { pub(crate) fn load_json(&mut self, jobj: &Map) -> Result<(), StoryError> { self.global_variables.clear(); - for (k, v) in self.default_global_variables.as_ref().unwrap().iter() { + for (k, v) in self.default_global_variables.iter() { let loaded_token = jobj.get(k); if let Some(loaded_token) = loaded_token { From b2132cc5b2a1875131e88be45bc0782a211a7fbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Wed, 4 Oct 2023 16:59:30 +0000 Subject: [PATCH 60/91] command and operation names as constants. --- lib/src/control_command.rs | 131 ++++++++++++++++----------- lib/src/native_function_call.rs | 156 +++++++++++++++++++------------- 2 files changed, 173 insertions(+), 114 deletions(-) diff --git a/lib/src/control_command.rs b/lib/src/control_command.rs index 6c4e077..3bcc362 100644 --- a/lib/src/control_command.rs +++ b/lib/src/control_command.rs @@ -34,6 +34,33 @@ pub enum CommandType { EndTag } +const EVAL_START_NAME: &str = "ev"; +const EVAL_OUTPUT_NAME: &str = "out"; +const EVAL_END_NAME: &str = "/ev"; +const DUPLICATE_NAME: &str = "du"; +const POP_EVALUATED_VALUE_NAME: &str = "pop"; +const POP_FUNCTION_NAME: &str = "~ret"; +const POP_TUNNEL_NAME: &str = "->->"; +const BEGIN_STRING_NAME: &str = "str"; +const END_STRING_NAME: &str = "/str"; +const NO_OP_NAME: &str = "nop"; +const CHOICE_COUNT_NAME: &str = "choiceCnt"; +const TURNS_NAME: &str = "turn"; +const TURNS_SINCE_NAME: &str = "turns"; +const READ_COUNT_NAME: &str = "readc"; +const RANDOM_NAME: &str = "rnd"; +const SEED_RANDOM_NAME: &str = "srnd"; +const VISIT_INDEX_NAME: &str = "visit"; +const SEQUENCE_SHUFFLE_INDEX_NAME: &str = "seq"; +const START_THREAD_NAME: &str = "thread"; +const DONE_NAME: &str = "done"; +const END_NAME: &str = "end"; +const LIST_FROM_INT_NAME: &str = "listInt"; +const LIST_RANGE_NAME: &str = "range"; +const LIST_RANDOM_NAME: &str = "lrnd"; +const BEGIN_TAG_NAME: &str = "#"; +const END_TAG_NAME: &str = "/#"; + pub struct ControlCommand { obj: Object, pub command_type: CommandType @@ -43,32 +70,32 @@ impl ControlCommand { pub fn new_from_name(name: &str) -> Option { match name { - "ev" => Some(Self::new(CommandType::EvalStart)), - "out" => Some(Self::new(CommandType::EvalOutput)), - "/ev" => Some(Self::new(CommandType::EvalEnd)), - "du" => Some(Self::new(CommandType::Duplicate)), - "pop" => Some(Self::new(CommandType::PopEvaluatedValue)), - "~ret" => Some(Self::new(CommandType::PopFunction)), - "->->" => Some(Self::new(CommandType::PopTunnel)), - "str" => Some(Self::new(CommandType::BeginString)), - "/str" => Some(Self::new(CommandType::EndString)), - "nop" => Some(Self::new(CommandType::NoOp)), - "choiceCnt" => Some(Self::new(CommandType::ChoiceCount)), - "turn" => Some(Self::new(CommandType::Turns)), - "turns" => Some(Self::new(CommandType::TurnsSince)), - "readc" => Some(Self::new(CommandType::ReadCount)), - "rnd" => Some(Self::new(CommandType::Random)), - "srnd" => Some(Self::new(CommandType::SeedRandom)), - "visit" => Some(Self::new(CommandType::VisitIndex)), - "seq" => Some(Self::new(CommandType::SequenceShuffleIndex)), - "thread" => Some(Self::new(CommandType::StartThread)), - "done" => Some(Self::new(CommandType::Done)), - "end" => Some(Self::new(CommandType::End)), - "listInt" => Some(Self::new(CommandType::ListFromInt)), - "range" => Some(Self::new(CommandType::ListRange)), - "lrnd" => Some(Self::new(CommandType::ListRandom,)), - "#" => Some(Self::new(CommandType::BeginTag)), - "/#" => Some(Self::new(CommandType::EndTag)), + EVAL_START_NAME => Some(Self::new(CommandType::EvalStart)), + EVAL_OUTPUT_NAME => Some(Self::new(CommandType::EvalOutput)), + EVAL_END_NAME => Some(Self::new(CommandType::EvalEnd)), + DUPLICATE_NAME => Some(Self::new(CommandType::Duplicate)), + POP_EVALUATED_VALUE_NAME => Some(Self::new(CommandType::PopEvaluatedValue)), + POP_FUNCTION_NAME => Some(Self::new(CommandType::PopFunction)), + POP_TUNNEL_NAME => Some(Self::new(CommandType::PopTunnel)), + BEGIN_STRING_NAME => Some(Self::new(CommandType::BeginString)), + END_STRING_NAME => Some(Self::new(CommandType::EndString)), + NO_OP_NAME => Some(Self::new(CommandType::NoOp)), + CHOICE_COUNT_NAME => Some(Self::new(CommandType::ChoiceCount)), + TURNS_NAME => Some(Self::new(CommandType::Turns)), + TURNS_SINCE_NAME => Some(Self::new(CommandType::TurnsSince)), + READ_COUNT_NAME => Some(Self::new(CommandType::ReadCount)), + RANDOM_NAME => Some(Self::new(CommandType::Random)), + SEED_RANDOM_NAME => Some(Self::new(CommandType::SeedRandom)), + VISIT_INDEX_NAME => Some(Self::new(CommandType::VisitIndex)), + SEQUENCE_SHUFFLE_INDEX_NAME => Some(Self::new(CommandType::SequenceShuffleIndex)), + START_THREAD_NAME => Some(Self::new(CommandType::StartThread)), + DONE_NAME => Some(Self::new(CommandType::Done)), + END_NAME => Some(Self::new(CommandType::End)), + LIST_FROM_INT_NAME => Some(Self::new(CommandType::ListFromInt)), + LIST_RANGE_NAME => Some(Self::new(CommandType::ListRange)), + LIST_RANDOM_NAME => Some(Self::new(CommandType::ListRandom,)), + BEGIN_TAG_NAME => Some(Self::new(CommandType::BeginTag)), + END_TAG_NAME => Some(Self::new(CommandType::EndTag)), _ => None, } @@ -76,32 +103,32 @@ impl ControlCommand { pub fn get_name(c: CommandType) -> String { match c { - CommandType::EvalStart => "ev".to_owned(), - CommandType::EvalOutput => "out".to_owned(), - CommandType::EvalEnd => "/ev".to_owned(), - CommandType::Duplicate => "du".to_owned(), - CommandType::PopEvaluatedValue => "pop".to_owned(), - CommandType::PopFunction => "~ret".to_owned(), - CommandType::PopTunnel => "->->".to_owned(), - CommandType::BeginString => "str".to_owned(), - CommandType::EndString => "/str".to_owned(), - CommandType::NoOp => "nop".to_owned(), - CommandType::ChoiceCount => "choiceCnt".to_owned(), - CommandType::Turns => "turn".to_owned(), - CommandType::TurnsSince => "turns".to_owned(), - CommandType::ReadCount => "readc".to_owned(), - CommandType::Random => "rnd".to_owned(), - CommandType::SeedRandom => "srnd".to_owned(), - CommandType::VisitIndex => "visit".to_owned(), - CommandType::SequenceShuffleIndex => "seq".to_owned(), - CommandType::StartThread => "thread".to_owned(), - CommandType::Done => "done".to_owned(), - CommandType::End => "end".to_owned(), - CommandType::ListFromInt => "listInt".to_owned(), - CommandType::ListRange => "range".to_owned(), - CommandType::ListRandom => "lrnd".to_owned(), - CommandType::BeginTag => "#".to_owned(), - CommandType::EndTag => "/#".to_owned(), + CommandType::EvalStart => EVAL_START_NAME.to_owned(), + CommandType::EvalOutput => EVAL_OUTPUT_NAME.to_owned(), + CommandType::EvalEnd => EVAL_END_NAME.to_owned(), + CommandType::Duplicate => DUPLICATE_NAME.to_owned(), + CommandType::PopEvaluatedValue => POP_EVALUATED_VALUE_NAME.to_owned(), + CommandType::PopFunction => POP_FUNCTION_NAME.to_owned(), + CommandType::PopTunnel => POP_TUNNEL_NAME.to_owned(), + CommandType::BeginString => BEGIN_STRING_NAME.to_owned(), + CommandType::EndString => END_STRING_NAME.to_owned(), + CommandType::NoOp => NO_OP_NAME.to_owned(), + CommandType::ChoiceCount => CHOICE_COUNT_NAME.to_owned(), + CommandType::Turns => TURNS_NAME.to_owned(), + CommandType::TurnsSince => TURNS_SINCE_NAME.to_owned(), + CommandType::ReadCount => READ_COUNT_NAME.to_owned(), + CommandType::Random => RANDOM_NAME.to_owned(), + CommandType::SeedRandom => SEED_RANDOM_NAME.to_owned(), + CommandType::VisitIndex => VISIT_INDEX_NAME.to_owned(), + CommandType::SequenceShuffleIndex => SEQUENCE_SHUFFLE_INDEX_NAME.to_owned(), + CommandType::StartThread => START_THREAD_NAME.to_owned(), + CommandType::Done => DONE_NAME.to_owned(), + CommandType::End => END_NAME.to_owned(), + CommandType::ListFromInt => LIST_FROM_INT_NAME.to_owned(), + CommandType::ListRange => LIST_RANGE_NAME.to_owned(), + CommandType::ListRandom => LIST_RANDOM_NAME.to_owned(), + CommandType::BeginTag => BEGIN_TAG_NAME.to_owned(), + CommandType::EndTag => END_TAG_NAME.to_owned(), } } diff --git a/lib/src/native_function_call.rs b/lib/src/native_function_call.rs index f55db4a..533f1ff 100644 --- a/lib/src/native_function_call.rs +++ b/lib/src/native_function_call.rs @@ -43,6 +43,38 @@ pub enum Op { Invert, } +const ADD_NAME: &str = "+"; +const SUBTRACT_NAME: &str = "-"; +const DIVIDE_NAME: &str = "/"; +const MULTIPLY_NAME: &str = "*"; +const MOD_NAME: &str = "%"; +const NEGATE_NAME: &str = "_"; +const EQUAL_NAME: &str = "=="; +const GREATER_NAME: &str = ">"; +const LESS_NAME: &str = "<"; +const GREATER_THAN_OR_EQUALS_NAME: &str = ">="; +const LESS_THAN_OR_EQUALS_NAME: &str = "<="; +const NOT_EQUALS_NAME: &str = "!="; +const NOT_NAME: &str = "!"; +const AND_NAME: &str = "&&"; +const OR_NAME: &str = "||"; +const MIN_NAME: &str = "MIN"; +const MAX_NAME: &str = "MAX"; +const POW_NAME: &str = "POW"; +const FLOOR_NAME: &str = "FLOOR"; +const CEILING_NAME: &str = "CEILING"; +const INT_NAME: &str = "INT"; +const FLOAT_NAME: &str = "FLOAT"; +const HAS_NAME: &str = "?"; +const HASNT_NAME: &str = "!?"; +const INTERSECT_NAME: &str = "^"; +const LIST_MIN_NAME: &str = "LIST_MIN"; +const LIST_MAX_NAME: &str = "LIST_MAX"; +const LIST_ALL_NAME: &str = "LIST_ALL"; +const LIST_COUNT_NAME: &str = "LIST_COUNT"; +const LIST_VALUE_NAME: &str = "LIST_VALUE"; +const LIST_INVERT_NAME: &str = "LIST_INVERT"; + pub struct NativeFunctionCall { obj: Object, pub op: Op, @@ -58,74 +90,74 @@ impl NativeFunctionCall { pub fn new_from_name(name: &str) -> Option { match name { - "+" => Some(Self::new(Op::Add)), - "-" => Some(Self::new(Op::Subtract)), - "/" => Some(Self::new(Op::Divide)), - "*" => Some(Self::new(Op::Multiply)), - "%" => Some(Self::new(Op::Mod)), - "_" => Some(Self::new(Op::Negate)), - "==" => Some(Self::new(Op::Equal)), - ">" => Some(Self::new(Op::Greater)), - "<" => Some(Self::new(Op::Less)), - ">=" => Some(Self::new(Op::GreaterThanOrEquals)), - "<=" => Some(Self::new(Op::LessThanOrEquals)), - "!=" => Some(Self::new(Op::NotEquals)), - "!" => Some(Self::new(Op::Not)), - "&&" => Some(Self::new(Op::And)), - "||" => Some(Self::new(Op::Or)), - "MIN" => Some(Self::new(Op::Min)), - "MAX" => Some(Self::new(Op::Max)), - "POW" => Some(Self::new(Op::Pow)), - "FLOOR" => Some(Self::new(Op::Floor)), - "CEILING" => Some(Self::new(Op::Ceiling)), - "INT" => Some(Self::new(Op::Int)), - "FLOAT" => Some(Self::new(Op::Float)), - "?" => Some(Self::new(Op::Has)), - "!?" => Some(Self::new(Op::Hasnt,)), - "^" => Some(Self::new(Op::Intersect)), - "LIST_MIN" => Some(Self::new(Op::ListMin)), - "LIST_MAX" => Some(Self::new(Op::ListMax)), - "LIST_ALL" => Some(Self::new(Op::All)), - "LIST_COUNT" => Some(Self::new(Op::Count)), - "LIST_VALUE" => Some(Self::new(Op::ValueOfList)), - "LIST_INVERT" => Some(Self::new(Op::Invert)), + ADD_NAME => Some(Self::new(Op::Add)), + SUBTRACT_NAME => Some(Self::new(Op::Subtract)), + DIVIDE_NAME => Some(Self::new(Op::Divide)), + MULTIPLY_NAME => Some(Self::new(Op::Multiply)), + MOD_NAME => Some(Self::new(Op::Mod)), + NEGATE_NAME => Some(Self::new(Op::Negate)), + EQUAL_NAME => Some(Self::new(Op::Equal)), + GREATER_NAME => Some(Self::new(Op::Greater)), + LESS_NAME => Some(Self::new(Op::Less)), + GREATER_THAN_OR_EQUALS_NAME => Some(Self::new(Op::GreaterThanOrEquals)), + LESS_THAN_OR_EQUALS_NAME => Some(Self::new(Op::LessThanOrEquals)), + NOT_EQUALS_NAME => Some(Self::new(Op::NotEquals)), + NOT_NAME => Some(Self::new(Op::Not)), + AND_NAME => Some(Self::new(Op::And)), + OR_NAME => Some(Self::new(Op::Or)), + MIN_NAME => Some(Self::new(Op::Min)), + MAX_NAME => Some(Self::new(Op::Max)), + POW_NAME => Some(Self::new(Op::Pow)), + FLOOR_NAME => Some(Self::new(Op::Floor)), + CEILING_NAME => Some(Self::new(Op::Ceiling)), + INT_NAME => Some(Self::new(Op::Int)), + FLOAT_NAME => Some(Self::new(Op::Float)), + HAS_NAME => Some(Self::new(Op::Has)), + HASNT_NAME => Some(Self::new(Op::Hasnt)), + INTERSECT_NAME => Some(Self::new(Op::Intersect)), + LIST_MIN_NAME => Some(Self::new(Op::ListMin)), + LIST_MAX_NAME => Some(Self::new(Op::ListMax)), + LIST_ALL_NAME => Some(Self::new(Op::All)), + LIST_COUNT_NAME => Some(Self::new(Op::Count)), + LIST_VALUE_NAME => Some(Self::new(Op::ValueOfList)), + LIST_INVERT_NAME => Some(Self::new(Op::Invert)), _ => None, } } pub fn get_name(op: Op) -> String { match op { - Op::Add => "+".to_owned(), - Op::Subtract => "-".to_owned(), - Op::Divide => "/".to_owned(), - Op::Multiply => "*".to_owned(), - Op::Mod => "%".to_owned(), - Op::Negate => "_".to_owned(), - Op::Equal => "==".to_owned(), - Op::Greater => ">".to_owned(), - Op::Less => "<".to_owned(), - Op::GreaterThanOrEquals => ">=".to_owned(), - Op::LessThanOrEquals => "<=".to_owned(), - Op::NotEquals => "!=".to_owned(), - Op::Not => "!".to_owned(), - Op::And => "&&".to_owned(), - Op::Or => "||".to_owned(), - Op::Min => "MIN".to_owned(), - Op::Max => "MAX".to_owned(), - Op::Pow => "POW".to_owned(), - Op::Floor => "FLOOR".to_owned(), - Op::Ceiling => "CEILING".to_owned(), - Op::Int => "INT".to_owned(), - Op::Float => "FLOAT".to_owned(), - Op::Has => "?".to_owned(), - Op::Hasnt => "!?".to_owned(), - Op::Intersect => "^".to_owned(), - Op::ListMin => "LIST_MIN".to_owned(), - Op::ListMax => "LIST_MAX".to_owned(), - Op::All => "LIST_ALL".to_owned(), - Op::Count => "LIST_COUNT".to_owned(), - Op::ValueOfList => "LIST_VALUE".to_owned(), - Op::Invert => "LIST_INVERT".to_owned(), + Op::Add => ADD_NAME.to_owned(), + Op::Subtract => SUBTRACT_NAME.to_owned(), + Op::Divide => DIVIDE_NAME.to_owned(), + Op::Multiply => MULTIPLY_NAME.to_owned(), + Op::Mod => MOD_NAME.to_owned(), + Op::Negate => NEGATE_NAME.to_owned(), + Op::Equal => EQUAL_NAME.to_owned(), + Op::Greater => GREATER_NAME.to_owned(), + Op::Less => LESS_NAME.to_owned(), + Op::GreaterThanOrEquals => GREATER_THAN_OR_EQUALS_NAME.to_owned(), + Op::LessThanOrEquals => LESS_THAN_OR_EQUALS_NAME.to_owned(), + Op::NotEquals => NOT_EQUALS_NAME.to_owned(), + Op::Not => NOT_NAME.to_owned(), + Op::And => AND_NAME.to_owned(), + Op::Or => OR_NAME.to_owned(), + Op::Min => MIN_NAME.to_owned(), + Op::Max => MAX_NAME.to_owned(), + Op::Pow => POW_NAME.to_owned(), + Op::Floor => FLOOR_NAME.to_owned(), + Op::Ceiling => CEILING_NAME.to_owned(), + Op::Int => INT_NAME.to_owned(), + Op::Float => FLOAT_NAME.to_owned(), + Op::Has => HAS_NAME.to_owned(), + Op::Hasnt => HASNT_NAME.to_owned(), + Op::Intersect => INTERSECT_NAME.to_owned(), + Op::ListMin => LIST_MIN_NAME.to_owned(), + Op::ListMax => LIST_MAX_NAME.to_owned(), + Op::All => LIST_ALL_NAME.to_owned(), + Op::Count => LIST_COUNT_NAME.to_owned(), + Op::ValueOfList => LIST_VALUE_NAME.to_owned(), + Op::Invert => LIST_INVERT_NAME.to_owned(), } } From c56ba97d08b1c76e0fd78c97cbd80c19af7537b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Thu, 5 Oct 2023 19:10:58 +0000 Subject: [PATCH 61/91] Implemented variable callback. --- README.md | 4 +- cli-player/src/main.rs | 4 +- lib/src/lib.rs | 1 + lib/src/story.rs | 72 +++++++++++++++++++++----------- lib/src/story_callbacks.rs | 79 ++++++++++++++++++++++++++++++++++++ lib/src/story_state.rs | 12 +----- lib/src/variables_state.rs | 69 ++++++++++++++++--------------- lib/tests/list_test.rs | 4 +- lib/tests/misc_test.rs | 2 +- lib/tests/multi_flow_test.rs | 8 ++-- lib/tests/runtime_test.rs | 66 ++++++++++++++++++++++++------ lib/tests/thread_test.rs | 4 +- 12 files changed, 232 insertions(+), 93 deletions(-) create mode 100644 lib/src/story_callbacks.rs diff --git a/README.md b/README.md index 99dc241..201babe 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,13 @@ Currently under development. This is the implementation status: ## TODO -- [ ] Optimize control command and Ops getname. Use static string array and address it by order. - [ ] Cache components string in Path - [ ] Variable observers. +- [ ] External functions. - [ ] Doc - [ ] story.state -> quitar el pub de get_state()/mut y que guardar/salvar sea pub(crate). Crear fichero con pub methods?? - [ ] Use OnceCell to lazy init the cache fields of RTObjects - [ ] Split large files. ex. Get the error handling out of the Story class. The performLogic -- [ ] Review all the use bink) and change it by .ok_or("xxx"). We need to avoid panics! +- [ ] Review all the .unwrap()s and change them by .ok_or("xxx"). We need to avoid panics! diff --git a/cli-player/src/main.rs b/cli-player/src/main.rs index d2a8e5c..1be18bb 100644 --- a/cli-player/src/main.rs +++ b/cli-player/src/main.rs @@ -57,11 +57,11 @@ fn process_command(command: Command, story: &mut Story) -> Result return Ok(true), Command::Load(filename) => { let saved_string = get_json_string(&filename)?; - story.get_state_mut().load_json(&saved_string)?; + story.load_state(&saved_string)?; println!("Ok.") }, Command::Save(filename) => { - let json_string = story.get_state().to_json()?; + let json_string = story.save_state()?; save_json(&filename, &json_string)?; }, Command::Help() => println!("Commands:\n\tload \n\tsave \n\tquit\n\t"), diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 9f46ce7..97c13ca 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,4 +1,5 @@ pub mod story; +pub mod story_callbacks; pub mod value_type; pub mod story_error; pub mod choice; diff --git a/lib/src/story.rs b/lib/src/story.rs index beaa2ce..1b66923 100644 --- a/lib/src/story.rs +++ b/lib/src/story.rs @@ -1,6 +1,6 @@ #![allow(unused_variables, dead_code)] -use std::{rc::Rc, time::Instant, collections::{VecDeque, HashMap}}; +use std::{rc::Rc, time::Instant, collections::{VecDeque, HashMap}, cell::RefCell}; use rand::{Rng, rngs::StdRng, SeedableRng}; @@ -9,7 +9,7 @@ use crate::{ error::ErrorType, json_read, push_pop::PushPopType, - story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, search_result::SearchResult, variable_assigment::VariableAssignment, native_function_call::NativeFunctionCall, variable_reference::VariableReference, list_definitions_origin::ListDefinitionsOrigin, ink_list::InkList, ink_list_item::InkListItem, variables_state::VariablesState, story_error::StoryError, value_type::ValueType, + story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, search_result::SearchResult, variable_assigment::VariableAssignment, native_function_call::NativeFunctionCall, variable_reference::VariableReference, list_definitions_origin::ListDefinitionsOrigin, ink_list::InkList, ink_list_item::InkListItem, story_error::StoryError, value_type::ValueType, story_callbacks::VariableObserver, }; pub const INK_VERSION_CURRENT: i32 = 21; @@ -34,6 +34,7 @@ pub struct Story { on_error: Option, prev_containers: Vec>, list_definitions: Rc, + pub(crate) variable_observers: HashMap>>>, } impl Story { @@ -104,6 +105,7 @@ impl Story { on_error: None, prev_containers: Vec::new(), list_definitions, + variable_observers: HashMap::with_capacity(0), }; // TODO self.get_state_mut().get_variables_state().setVariableChangedEvent(this); @@ -113,12 +115,12 @@ impl Story { } #[inline] - pub fn get_state(&self) -> &StoryState { + pub(crate) fn get_state(&self) -> &StoryState { &self.state } #[inline] - pub fn get_state_mut(&mut self) -> &mut StoryState { + pub(crate) fn get_state_mut(&mut self) -> &mut StoryState { &mut self.state } @@ -135,7 +137,7 @@ impl Story { self.get_state().set_current_pointer(original_pointer); } - self.get_state_mut().get_variables_state_mut().snapshot_default_globals(); + self.get_state_mut().variables_state.snapshot_default_globals(); Ok(()) } @@ -210,8 +212,8 @@ impl Story { // for the outermost call. if self.recursive_continue_count == 1 { self.state - .get_variables_state_mut() - .set_batch_observing_variable_changes(true); + .variables_state + .start_batch_observing_variable_changes(); } } @@ -304,9 +306,13 @@ impl Story { self.saw_lookahead_unsafe_function_after_new_line = false; if self.recursive_continue_count == 1 { - self.state - .get_variables_state_mut() - .set_batch_observing_variable_changes(false); + let changed = self.state + .variables_state + .stop_batch_observing_variable_changes(); + + for (variable_name, value) in changed { + self.notify_variable_changed(&variable_name, &value); + } } self.async_continue_active = false; @@ -745,14 +751,6 @@ impl Story { } } - pub fn get_variables_state(&self) -> &VariablesState { - self.get_state().get_variables_state() - } - - pub fn get_variables_state_mut(&mut self) -> &mut VariablesState { - self.get_state_mut().get_variables_state_mut() - } - fn perform_logic_and_flow_control(&mut self, content_obj: &Option>) -> Result { let content_obj = match content_obj { Some(content_obj) => { @@ -772,7 +770,7 @@ impl Story { if current_divert.has_variable_target() { let var_name = ¤t_divert.variable_divert_name; - if let Some(var_contents) = self.get_state().get_variables_state().get_variable_with_name(var_name.as_ref().unwrap(), -1) { + if let Some(var_contents) = self.get_state().variables_state.get_variable_with_name(var_name.as_ref().unwrap(), -1) { if let Some(target) = Value::get_divert_target_value(var_contents.as_ref()) { let p = Self::pointer_at_path(&self.main_content_container, target)?; self.get_state_mut().set_diverted_pointer(p); @@ -1293,7 +1291,7 @@ impl Story { // var prioritiseHigherInCallStack = _temporaryEvaluationContainer // != null; let assigned_val = assigned_val.into_any().downcast::().unwrap(); - self.get_state_mut().get_variables_state_mut().assign( var_ass, assigned_val); + self.get_state_mut().variables_state.assign( var_ass, assigned_val); return Ok(true); } @@ -1312,7 +1310,7 @@ impl Story { // Normal variable reference else { - found_value = self.get_state().get_variables_state().get_variable_with_name(&var_ref.name, -1); + found_value = self.get_state().variables_state.get_variable_with_name(&var_ref.name, -1); if found_value.is_none() { self.add_error(&format!("Variable not found: '{}'. 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.", var_ref.name), true); @@ -1891,7 +1889,11 @@ impl Story { Ok(()) } - fn if_async_we_cant(&self, activity_str: &str) -> Result<(), StoryError> { + pub fn remove_flow(&mut self, flow_name: &str) -> Result<(), StoryError> { + self.get_state_mut().remove_flow_internal(flow_name) + } + + pub(crate) fn if_async_we_cant(&self, activity_str: &str) -> Result<(), StoryError> { if self.async_continue_active { return Err(StoryError::InvalidStoryState(format!("Can't {}. Story is in the middle of a ContinueAsync(). Make more continue_async() calls or a single cont() call beforehand.", activity_str))); } @@ -1899,8 +1901,30 @@ impl Story { Ok(()) } - pub fn remove_flow(&mut self, flow_name: &str) { - self.get_state_mut().remove_flow_internal(flow_name); + pub fn set_variable(&mut self, variable_name: &str, value_type: &ValueType) -> Result<(), StoryError> { + let notify_observers = self.get_state_mut().variables_state.set(variable_name, value_type.clone())?; + + if notify_observers { + self.notify_variable_changed(variable_name, value_type); + } + + Ok(()) + } + + pub fn get_variable(&self, variable_name: &str) -> Option { + self.get_state().variables_state.get(variable_name) + } + + pub fn save_state(&self) -> Result { + self.get_state().to_json() + } + + pub fn load_state(&mut self, json_state: &str) -> Result<(), StoryError> { + self.get_state_mut().load_json(json_state) + } + + pub fn get_visit_count_at_path_string(&self, path_string: &str) -> Result { + self.get_state().visit_count_at_path_string(path_string) } } diff --git a/lib/src/story_callbacks.rs b/lib/src/story_callbacks.rs new file mode 100644 index 0000000..f12db78 --- /dev/null +++ b/lib/src/story_callbacks.rs @@ -0,0 +1,79 @@ +use std::{rc::Rc, cell::RefCell}; + +use crate::{story::Story, value_type::ValueType, story_error::StoryError}; + +pub trait VariableObserver { + fn changed(&mut self, variable_name: &str, value: &ValueType); +} + +impl Story { + + pub fn observe_variable(&mut self, variable_name: &str, observer: Rc>) -> Result<(), StoryError> { + self.if_async_we_cant("observe a new variable")?; + + if !self.get_state().variables_state.global_variable_exists_with_name(variable_name) { + return Err(StoryError::BadArgument( + format!("Cannot observe variable '{variable_name}' because it wasn't declared in the ink story."))); + } + + match self.variable_observers.get_mut(variable_name) { + Some(v) => { + v.push(observer); + } + None => { + let v: Vec>> = vec![observer]; + self.variable_observers.insert(variable_name.to_string(), v); + } + } + + Ok(()) + } + + pub fn remove_variable_observer(&mut self, observer: &Rc>, specific_variable_name: Option<&str>) -> Result<(), StoryError> { + self.if_async_we_cant("remove a variable observer")?; + + // Remove observer for this specific variable + match specific_variable_name { + Some(specific_variable_name) => { + if let Some(v) = self.variable_observers.get_mut(specific_variable_name) { + let index = v.iter().position(|x| Rc::ptr_eq(x, observer)).unwrap(); + v.remove(index); + + if v.is_empty() { + self.variable_observers.remove(specific_variable_name); + } + } + }, + None => { + // Remove observer for all variables + let mut keys_to_remove = Vec::new(); + + for (k,v) in self.variable_observers.iter_mut() { + let index = v.iter().position(|x| Rc::ptr_eq(x, observer)).unwrap(); + v.remove(index); + + if v.is_empty() { + keys_to_remove.push(k.to_string()); + } + } + + for key_to_remove in keys_to_remove.iter() { + self.variable_observers.remove(key_to_remove); + } + } + } + + Ok(()) + } + + pub(crate) fn notify_variable_changed(&self, variable_name: &str, value: &ValueType) { + let observers = self.variable_observers.get(variable_name); + + if let Some(observers) = observers { + for o in observers.iter() { + o.borrow_mut().changed(variable_name, value); + } + } + } +} + diff --git a/lib/src/story_state.rs b/lib/src/story_state.rs index c98bd4b..bde84f6 100644 --- a/lib/src/story_state.rs +++ b/lib/src/story_state.rs @@ -12,12 +12,12 @@ pub const MIN_COMPATIBLE_LOAD_VERSION: u32 = 8; static DEFAULT_FLOW_NAME: &str = "DEFAULT_FLOW"; -pub struct StoryState { +pub(crate) struct StoryState { pub current_flow: Flow, pub did_safe_exit: bool, output_stream_text_dirty: bool, output_stream_tags_dirty: bool, - variables_state: VariablesState, + pub variables_state: VariablesState, alive_flow_names_dirty: bool, pub evaluation_stack: Vec>, main_content_container: Rc, @@ -103,14 +103,6 @@ impl StoryState { self.output_stream_dirty(); } - pub fn get_variables_state(&self) -> &VariablesState { - &self.variables_state - } - - pub fn get_variables_state_mut(&mut self) -> &mut VariablesState { - &mut self.variables_state - } - pub fn get_generated_choices_mut(&mut self) -> &mut Vec> { &mut self.current_flow.current_choices } diff --git a/lib/src/variables_state.rs b/lib/src/variables_state.rs index 089a6f5..491ed6d 100644 --- a/lib/src/variables_state.rs +++ b/lib/src/variables_state.rs @@ -6,15 +6,14 @@ use crate::{callstack::CallStack, state_patch::StatePatch, variable_assigment::V #[derive(Clone)] -pub struct VariablesState { +pub(crate) struct VariablesState { pub global_variables: HashMap>, pub default_global_variables: HashMap>, pub batch_observing_variable_changes: bool, pub callstack: Rc>, pub changed_variables_for_batch_obs: Option>, - pub variable_changed_event: Option, - list_defs_origin: Rc, pub patch: Option, + list_defs_origin: Rc, } impl VariablesState { @@ -25,30 +24,32 @@ impl VariablesState { batch_observing_variable_changes: false, callstack, changed_variables_for_batch_obs: None, - variable_changed_event: None, patch: None, list_defs_origin, } } - pub fn set_batch_observing_variable_changes(&mut self, value: bool) { - self.batch_observing_variable_changes = value; + pub fn start_batch_observing_variable_changes(&mut self) { + self.batch_observing_variable_changes = true; + self.changed_variables_for_batch_obs = Some(HashSet::new()); + } - if value { - self.changed_variables_for_batch_obs = Some(HashSet::new()); - } else { - // Finished observing variables in a batch - now send - // notifications for changed variables all in one go. - if self.changed_variables_for_batch_obs.is_some() { - for variable_name in self.changed_variables_for_batch_obs.as_ref().unwrap() { - let current_value = self.global_variables.get(variable_name).unwrap(); + pub fn stop_batch_observing_variable_changes(&mut self) -> Vec<(String, ValueType)>{ + self.batch_observing_variable_changes = false; - (self.variable_changed_event.as_ref().unwrap())(variable_name, current_value.as_ref()); - } + let mut changed: Vec<(String, ValueType)> = Vec::with_capacity(0); + + // Finished observing variables in a batch - now send + // notifications for changed variables all in one go. + if let Some(changed_variables_for_batch_obs) = self.changed_variables_for_batch_obs.take() { + for variable_name in changed_variables_for_batch_obs { + let current_value = self.global_variables.get(&variable_name).unwrap(); + + changed.push((variable_name, current_value.value.clone())); } + } - self.changed_variables_for_batch_obs = None; - } + changed } pub fn snapshot_default_globals(&mut self) { @@ -122,7 +123,7 @@ impl VariablesState { } } - fn global_variable_exists_with_name(&self, name: &str) -> bool { + pub fn global_variable_exists_with_name(&self, name: &str) -> bool { self.global_variables.contains_key(name) || self .default_global_variables.contains_key(name) @@ -154,7 +155,8 @@ impl VariablesState { Rc::new(Value::new_variable_pointer(&var_pointer.variable_name, context_index)) } - pub fn set(&mut self, variable_name: &str, value_type: ValueType) -> Result<(), StoryError> { + // returns true if the value changed and we should notify variable observers + pub fn set(&mut self, variable_name: &str, value_type: ValueType) -> Result { if !self.default_global_variables.contains_key(variable_name) { return Err(StoryError::BadArgument(format!("Cannot assign to a variable {} that hasn't been declared in the story", variable_name))); @@ -162,9 +164,9 @@ impl VariablesState { let val = Value::from_value_type(value_type); - self.set_global(variable_name, Rc::new(val)); + let notify = self.set_global(variable_name, Rc::new(val)); - Ok(()) + Ok(notify) } pub fn get(&self, variable_name: &str) -> Option { @@ -237,7 +239,8 @@ impl VariablesState { var_value } - fn set_global(&mut self, name: &str, value: Rc) { + // Returns true if global var has changed and we need to notify observers + fn set_global(&mut self, name: &str, value: Rc) -> bool { let mut old_value: Option> = None; if let Some(patch) = &self.patch { @@ -258,19 +261,19 @@ impl VariablesState { self.global_variables.insert(name.to_string(), value.clone()); } - if let Some(variable_changed_event) = &self.variable_changed_event { - if !Rc::ptr_eq(old_value.as_ref().unwrap(), &value) { - if self.batch_observing_variable_changes { - if let Some(patch) = &mut self.patch { - patch.add_changed_variable(name); - } else if let Some(changed_variables) = &mut self.changed_variables_for_batch_obs { - changed_variables.insert(name.to_string()); - } - } else { - variable_changed_event(name, value.as_ref()); + if old_value.is_none() || !Rc::ptr_eq(old_value.as_ref().unwrap(), &value) { + if self.batch_observing_variable_changes { + if let Some(patch) = &mut self.patch { + patch.add_changed_variable(name); + } else if let Some(changed_variables) = &mut self.changed_variables_for_batch_obs { + changed_variables.insert(name.to_string()); } + } else { + return true; } } + + false } pub fn get_variable_with_name(&self, name: &str, context_index: i32) -> Option> { diff --git a/lib/tests/list_test.rs b/lib/tests/list_test.rs index caeb984..1e31a7d 100644 --- a/lib/tests/list_test.rs +++ b/lib/tests/list_test.rs @@ -54,11 +54,11 @@ fn list_save_load_test() -> Result<(), StoryError> { assert_eq!("a, x, c\n", &story.continue_maximally()?); - let saved_state = story.get_state().to_json()?; + let saved_state = story.save_state()?; let mut story = Story::new(&json_string).unwrap(); - story.get_state_mut().load_json(&saved_state)?; + story.load_state(&saved_state)?; story.choose_path_string("elsewhere", true, None)?; diff --git a/lib/tests/misc_test.rs b/lib/tests/misc_test.rs index 3dcd33e..db0f744 100644 --- a/lib/tests/misc_test.rs +++ b/lib/tests/misc_test.rs @@ -53,7 +53,7 @@ fn issue15_test() -> Result<(), StoryError> { let line = &story.cont()?; if line.starts_with("SET_X:") { - story.get_variables_state_mut().set("x", ValueType::Int(100)); + story.set_variable("x", &ValueType::Int(100)); } else { assert_eq!("X is set\n", line); } diff --git a/lib/tests/multi_flow_test.rs b/lib/tests/multi_flow_test.rs index 25adef5..350c79e 100644 --- a/lib/tests/multi_flow_test.rs +++ b/lib/tests/multi_flow_test.rs @@ -53,7 +53,7 @@ fn multiflow_save_load_threads() -> Result<(), StoryError> { assert_eq!("Thread 1 red choice", story.get_current_choices()[0].text); // Save/load test - let saved = story.get_state().to_json()?; + let saved = story.save_state()?; // Test choice before reloading state before resetting story.choose_choice_index(0); @@ -61,19 +61,19 @@ fn multiflow_save_load_threads() -> Result<(), StoryError> { let mut story = Story::new(&json_string).unwrap(); // Load to pre-choice: still red, choose second choice - story.get_state_mut().load_json(&saved)?; + story.load_state(&saved)?; story.choose_choice_index(1); assert_eq!("Thread 2 red choice\nAfter thread 2 choice (red)\n", story.continue_maximally()?); // Load: switch to blue, choose 1 - story.get_state_mut().load_json(&saved)?; + story.load_state(&saved)?; story.switch_flow("Blue Flow")?; story.choose_choice_index(0); assert_eq!("Thread 1 blue choice\nAfter thread 1 choice (blue)\n", story.continue_maximally()?); // Load: switch to blue, choose 2 - story.get_state_mut().load_json(&saved)?; + story.load_state(&saved)?; story.switch_flow("Blue Flow")?; story.choose_choice_index(1); assert_eq!("Thread 2 blue choice\nAfter thread 2 choice (blue)\n", story.continue_maximally()?); diff --git a/lib/tests/runtime_test.rs b/lib/tests/runtime_test.rs index bcebbe5..8c9e8d1 100644 --- a/lib/tests/runtime_test.rs +++ b/lib/tests/runtime_test.rs @@ -1,8 +1,48 @@ -use bink::{story::Story, value_type::{ValueType, StringValue}, story_error::StoryError}; +use core::panic; +use std::{cell::RefCell, rc::Rc}; + +use bink::{story::Story, value_type::{ValueType, StringValue}, story_error::StoryError, story_callbacks::VariableObserver}; mod common; -// TODO external functions + variable observers +// TODO external functions + +struct VObserver { + expected_value: i32, +} + +impl VariableObserver for VObserver { + fn changed(&mut self, variable_name: &str, new_value: &ValueType) { + if !"x".eq(variable_name) { + panic!(); + } + + if let ValueType::Int(v) = new_value { + assert_eq!(self.expected_value, *v); + } else { + panic!(); + } + + self.expected_value = 10; + } +} + +#[test] +fn variable_observers_test() -> Result<(), StoryError> { + let json_string = + common::get_json_string("tests/data/runtime/variable-observers.ink.json").unwrap(); + let mut story = Story::new(&json_string).unwrap(); + let mut text: Vec = Vec::new(); + + story.observe_variable("x", Rc::new(RefCell::new(VObserver { expected_value: 5}))); + + common::next_all(&mut story, &mut text)?; + story.choose_choice_index(0); + common::next_all(&mut story, &mut text)?; + + Ok(()) +} + #[test] fn set_and_get_variable_test() -> Result<(), StoryError> { @@ -12,11 +52,11 @@ fn set_and_get_variable_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - assert_eq!(10, story.get_variables_state().get("x").unwrap().get_int().unwrap()); + assert_eq!(10, story.get_variable("x").unwrap().get_int().unwrap()); - story.get_variables_state_mut().set("x", ValueType::Int(15))?; + story.set_variable("x", &ValueType::Int(15))?; - assert_eq!(15, story.get_variables_state().get("x").unwrap().get_int().unwrap()); + assert_eq!(15, story.get_variable("x").unwrap().get_int().unwrap()); story.choose_choice_index(0); @@ -39,14 +79,14 @@ fn set_non_existant_variable_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; - let result = story.get_variables_state_mut().set("y", ValueType::new_string("earth")); + let result = story.set_variable("y", &ValueType::new_string("earth")); assert!(result.is_err()); - assert_eq!(10, story.get_variables_state().get("x").unwrap().get_int().unwrap()); + assert_eq!(10, story.get_variable("x").unwrap().get_int().unwrap()); - story.get_variables_state_mut().set("x", ValueType::Int(15))?; + story.set_variable("x", &ValueType::Int(15))?; - assert_eq!(15, story.get_variables_state().get("x").unwrap().get_int().unwrap()); + assert_eq!(15, story.get_variable("x").unwrap().get_int().unwrap()); story.choose_choice_index(0); @@ -125,8 +165,8 @@ fn read_visit_counts_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - assert_eq!(4, story.get_state().visit_count_at_path_string("two.s2")?); - assert_eq!(5, story.get_state().visit_count_at_path_string("two")?); + assert_eq!(4, story.get_visit_count_at_path_string("two.s2")?); + assert_eq!(5, story.get_visit_count_at_path_string("two")?); Ok(()) } @@ -143,13 +183,13 @@ fn load_save_test() -> Result<(), StoryError> { assert_eq!("We arrived into London at 9.45pm exactly.", text.get(0).unwrap()); // save the game state - let save_string = story.get_state().to_json()?; + let save_string = story.save_state()?; println!("{}", save_string); // recreate game and load state Story::new(&json_string).unwrap(); - story.get_state_mut().load_json(&save_string)?; + story.load_state(&save_string)?; story.choose_choice_index(0); diff --git a/lib/tests/thread_test.rs b/lib/tests/thread_test.rs index 724ab6b..1f59e3a 100644 --- a/lib/tests/thread_test.rs +++ b/lib/tests/thread_test.rs @@ -39,10 +39,10 @@ fn thread_test_bug() -> Result<(), StoryError> { assert_eq!("No", story.get_current_choices()[0].text); assert_eq!("Yes", story.get_current_choices()[1].text); - let save_string = story.get_state().to_json()?; + let save_string = story.save_state()?; println!("{}", save_string); let mut story = Story::new(&json_string).unwrap(); - story.get_state_mut().load_json(&save_string)?; + story.load_state(&save_string)?; story.choose_choice_index(0); From ea376117228b23a6310b760cc84dc35e01cd333b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Fri, 6 Oct 2023 16:14:36 +0000 Subject: [PATCH 62/91] External functions working. --- .vscode/launch.json | 12 +-- .vscode/settings.json | 3 +- README.md | 7 +- lib/src/divert.rs | 21 ++-- lib/src/json_read.rs | 2 +- lib/src/json_write.rs | 2 +- lib/src/story.rs | 27 +++-- lib/src/story_callbacks.rs | 180 +++++++++++++++++++++++++++++++- lib/src/value.rs | 4 + lib/src/value_type.rs | 38 ++++++- lib/tests/basic_text_test.rs | 4 +- lib/tests/choice_test.rs | 26 ++--- lib/tests/conditional_test.rs | 40 +++---- lib/tests/divert_test.rs | 10 +- lib/tests/function_test.rs | 18 ++-- lib/tests/gather_test.rs | 12 +-- lib/tests/glue_test.rs | 10 +- lib/tests/knot_test.rs | 18 ++-- lib/tests/list_test.rs | 58 +++++----- lib/tests/misc_test.rs | 8 +- lib/tests/multi_flow_test.rs | 6 +- lib/tests/runtime_test.rs | 162 +++++++++++++++++++++++----- lib/tests/stitch_test.rs | 8 +- lib/tests/tag_test.rs | 8 +- lib/tests/thread_test.rs | 6 +- lib/tests/tunnel_test.rs | 2 +- lib/tests/variable_test.rs | 8 +- lib/tests/variable_text_test.rs | 10 +- 28 files changed, 520 insertions(+), 190 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 065edd6..4fb7c23 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,11 +11,11 @@ "cargo": { "args": [ "build", - "--bin=console-player", - "--package=blade-ink" + "--bin=binkplayer", + "--package=binkplayer" ], "filter": { - "name": "console-player", + "name": "binkplayer", "kind": "bin" } }, @@ -30,11 +30,11 @@ "args": [ "test", "--no-run", - "--bin=console-player", - "--package=blade-ink" + "--bin=binkplayer", + "--package=binkplayer" ], "filter": { - "name": "console-player", + "name": "binkplayer", "kind": "bin" } }, diff --git a/.vscode/settings.json b/.vscode/settings.json index daf1107..fdf2867 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "rust-analyzer.linkedProjects": [ "./Cargo.toml", - "./cli-player/Cargo.toml" + "./cli-player/Cargo.toml", + "./lib/Cargo.toml" ] } \ No newline at end of file diff --git a/README.md b/README.md index 201babe..0154574 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,10 @@ Currently under development. This is the implementation status: ## TODO - [ ] Cache components string in Path -- [ ] Variable observers. -- [ ] External functions. -- [ ] Doc +- [ ] Assert, revisar y lanzar error. +- [ ] Doc. +- [ ] Publicar crate. -- [ ] story.state -> quitar el pub de get_state()/mut y que guardar/salvar sea pub(crate). Crear fichero con pub methods?? - [ ] Use OnceCell to lazy init the cache fields of RTObjects - [ ] Split large files. ex. Get the error handling out of the Story class. The performLogic - [ ] Review all the .unwrap()s and change them by .ok_or("xxx"). We need to avoid panics! diff --git a/lib/src/divert.rs b/lib/src/divert.rs index c9f0279..f12c7a7 100644 --- a/lib/src/divert.rs +++ b/lib/src/divert.rs @@ -7,7 +7,7 @@ use crate::{object::{Object, RTObject}, push_pop::PushPopType, pointer::{Pointer pub struct Divert { obj: Object, - pub external_args: i32, + pub external_args: usize, pub is_conditional: bool, pub is_external: bool, pub pushes_to_stack: bool, @@ -18,7 +18,7 @@ pub struct Divert { } impl Divert { - pub fn new(pushes_to_stack: bool, stack_push_type: PushPopType, is_external: bool, external_args: i32, is_conditional: bool, var_divert_name: Option, target_path: Option<&str>) -> Self { + pub fn new(pushes_to_stack: bool, stack_push_type: PushPopType, is_external: bool, external_args: usize, is_conditional: bool, var_divert_name: Option, target_path: Option<&str>) -> Self { Divert { obj: Object::new(), is_conditional, @@ -33,18 +33,11 @@ impl Divert { } fn target_path_string(value: Option<&str>) -> Option{ - if let Some(value) = value { - Some(Path::new_with_components_string(Some(value))) - } else { - None - } + value.map(|value| Path::new_with_components_string(Some(value))) } - pub fn get_target_path_string(&self) -> Option { - match self.target_path.borrow().as_ref() { - Some(p) => Some(self.compact_path_string(p)), - None => None, - } + pub fn get_target_path_string(self: &Rc) -> Option { + self.get_target_path().as_ref().map(|p| self.compact_path_string(p)) } pub fn has_variable_target(&self) -> bool { @@ -174,8 +167,8 @@ impl fmt::Display for Divert { } } - //result.push_str(&format!(" -> {} ({})", self.get_target_path_string().unwrap_or_default(), target_str)); - result.push_str(&format!(" -> {}", self.get_target_path_string().unwrap_or_default())); + // TODO result.push_str(&format!(" -> {} ({})", self.get_target_path_string().unwrap_or_default(), target_str)); + result.push_str(&format!(" -> {} ({})", self.target_path.borrow().as_ref().unwrap(), target_str)); } write!(f, "{result}") diff --git a/lib/src/json_read.rs b/lib/src/json_read.rs index 46f7992..1e0e20e 100644 --- a/lib/src/json_read.rs +++ b/lib/src/json_read.rs @@ -134,7 +134,7 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) if external { prop_value = obj.get("exArgs"); if let Some(prop_value) = prop_value { - external_args = prop_value.as_i64().unwrap() as i32; + external_args = prop_value.as_i64().unwrap() as usize; } } diff --git a/lib/src/json_write.rs b/lib/src/json_write.rs index 674a495..0ab24d7 100644 --- a/lib/src/json_write.rs +++ b/lib/src/json_write.rs @@ -22,7 +22,7 @@ pub fn write_rtobject(o: Rc) -> Result() { + if let Some(divert) = o.clone().into_any().downcast::().ok() { let mut div_type_key = "->"; if divert.is_external { div_type_key = "x()"; } diff --git a/lib/src/story.rs b/lib/src/story.rs index 1b66923..8c68c0a 100644 --- a/lib/src/story.rs +++ b/lib/src/story.rs @@ -9,7 +9,7 @@ use crate::{ error::ErrorType, json_read, push_pop::PushPopType, - story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, search_result::SearchResult, variable_assigment::VariableAssignment, native_function_call::NativeFunctionCall, variable_reference::VariableReference, list_definitions_origin::ListDefinitionsOrigin, ink_list::InkList, ink_list_item::InkListItem, story_error::StoryError, value_type::ValueType, story_callbacks::VariableObserver, + story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, search_result::SearchResult, variable_assigment::VariableAssignment, native_function_call::NativeFunctionCall, variable_reference::VariableReference, list_definitions_origin::ListDefinitionsOrigin, ink_list::InkList, ink_list_item::InkListItem, story_error::StoryError, value_type::ValueType, story_callbacks::{VariableObserver, ExternalFunctionDef}, }; pub const INK_VERSION_CURRENT: i32 = 21; @@ -29,12 +29,15 @@ pub struct Story { recursive_continue_count: usize, async_continue_active: bool, async_saving: bool, - saw_lookahead_unsafe_function_after_new_line: bool, - state_snapshot_at_last_new_line: Option, - on_error: Option, prev_containers: Vec>, list_definitions: Rc, + on_error: Option, + pub(crate) state_snapshot_at_last_new_line: Option, pub(crate) variable_observers: HashMap>>>, + pub(crate) has_validated_externals: bool, + pub(crate) allow_external_function_fallbacks: bool, + pub(crate) saw_lookahead_unsafe_function_after_new_line: bool, + pub(crate) externals: HashMap, } impl Story { @@ -106,8 +109,10 @@ impl Story { prev_containers: Vec::new(), list_definitions, variable_observers: HashMap::with_capacity(0), + has_validated_externals: false, + allow_external_function_fallbacks: false, + externals: HashMap::with_capacity(0), }; - // TODO self.get_state_mut().get_variables_state().setVariableChangedEvent(this); story.reset_globals()?; @@ -180,7 +185,9 @@ impl Story { } pub fn continue_async(&mut self, millisecs_limit_async: f32) -> Result<(), StoryError> { - // TODO: if (!hasValidatedExternals) validateExternalBindings(); + if !self.has_validated_externals { + self.validate_external_bindings()?; + } self.continue_internal(millisecs_limit_async)?; @@ -796,7 +803,7 @@ impl Story { return Err(StoryError::InvalidStoryState(format!("Tried to divert using a target from a variable that could not be found ({})", var_name.as_ref().unwrap()))); } } else if current_divert.is_external { - //call_external_function(¤t_divert.get_target_path_string(), current_divert.get_external_args()); + self.call_external_function(¤t_divert.get_target_path_string().unwrap(), current_divert.external_args); return Ok(true); } else { self.get_state_mut().set_diverted_pointer(current_divert.get_target_pointer()); @@ -1728,7 +1735,7 @@ impl Story { self.get_state_mut().complete_function_evaluation_from_game() } - fn knot_container_with_name(&self, name: &str) -> Option> { + pub(crate) fn knot_container_with_name(&self, name: &str) -> Option> { let named_container = self.main_content_container.named_content.get(name); named_container.cloned() @@ -1926,5 +1933,9 @@ impl Story { pub fn get_visit_count_at_path_string(&self, path_string: &str) -> Result { self.get_state().visit_count_at_path_string(path_string) } + + pub fn set_allow_external_function_fallbacks(&mut self, v: bool) { + self.allow_external_function_fallbacks = v; + } } diff --git a/lib/src/story_callbacks.rs b/lib/src/story_callbacks.rs index f12db78..dac8ce7 100644 --- a/lib/src/story_callbacks.rs +++ b/lib/src/story_callbacks.rs @@ -1,11 +1,20 @@ -use std::{rc::Rc, cell::RefCell}; +use std::{rc::Rc, cell::RefCell, collections::HashSet}; -use crate::{story::Story, value_type::ValueType, story_error::StoryError}; +use crate::{story::Story, value_type::ValueType, story_error::StoryError, push_pop::PushPopType, pointer::Pointer, container::Container, value::Value, object::RTObject, void::Void, divert::Divert}; pub trait VariableObserver { fn changed(&mut self, variable_name: &str, value: &ValueType); } +pub trait ExternalFunction { + fn call(&mut self, func_name: &str, args: Vec) -> Option; +} + +pub(crate) struct ExternalFunctionDef { + function: Rc>, + lookahead_safe: bool, +} + impl Story { pub fn observe_variable(&mut self, variable_name: &str, observer: Rc>) -> Result<(), StoryError> { @@ -29,7 +38,7 @@ impl Story { Ok(()) } - pub fn remove_variable_observer(&mut self, observer: &Rc>, specific_variable_name: Option<&str>) -> Result<(), StoryError> { + pub fn remove_variable_observer(&mut self, observer: &Rc>, specific_variable_name: Option<&str>) -> Result<(), StoryError> { self.if_async_we_cant("remove a variable observer")?; // Remove observer for this specific variable @@ -75,5 +84,168 @@ impl Story { } } } -} + pub fn bind_external_function(&mut self, func_name: &str, function: Rc>, lookahead_safe: bool) -> Result<(), StoryError> { + self.if_async_we_cant("bind an external function")?; + + if self.externals.contains_key(func_name) { + return Err(StoryError::BadArgument(format!("Function '{func_name}' has already been bound."))); + } + + let external_function_def = ExternalFunctionDef {function, lookahead_safe}; + + self.externals.insert(func_name.to_string(), external_function_def); + + Ok(()) + } + + pub(crate) fn call_external_function( + &mut self, + func_name: &str, + number_of_arguments: usize, + ) -> Result<(), StoryError> { + + + // 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 let Some(func_def) = self.externals.get(func_name) { + if !func_def.lookahead_safe && self.state_snapshot_at_last_new_line.is_some() { + self.saw_lookahead_unsafe_function_after_new_line = true; + return Ok(()); + } + } else { + // Try to use fallback function? + if self.allow_external_function_fallbacks { + if let Some(fallback_function_container) = self.knot_container_with_name(func_name) { + // Divert direct into fallback function and we're done + self.get_state() + .get_callstack().borrow_mut() + .push(PushPopType::Function, 0, self.get_state().get_output_stream().len() as i32); + self.get_state_mut().set_diverted_pointer(Pointer::start_of(fallback_function_container)); + return Ok(()); + } else { + return Err(StoryError::InvalidStoryState(format!( + "Trying to call EXTERNAL function '{}' which has not been bound, and fallback ink function could not be found.", + func_name + ))); + } + } else { + return Err(StoryError::InvalidStoryState(format!( + "Trying to call EXTERNAL function '{}' which has not been bound (and ink fallbacks disabled).", + func_name + ))); + } + } + + // Pop arguments + let mut arguments: Vec = Vec::new(); + for _ in 0..number_of_arguments { + let popped_obj = self.get_state_mut().pop_evaluation_stack(); + let value_obj = popped_obj.into_any().downcast::(); + + if let Ok(value_obj) = value_obj { + arguments.push(value_obj.value.clone()); + } else { + return Err(StoryError::InvalidStoryState(format!( + "Trying to call EXTERNAL function '{}' with arguments which are not values.", + func_name + ))); + } + } + + // Reverse arguments from the order they were popped, + // so they're the right way round again. + arguments.reverse(); + + // Run the function! + let func_def = self.externals.get(func_name); + let func_result = func_def.unwrap().function.borrow_mut().call(func_name,arguments); + + // Convert return value (if any) to a type that the ink engine can use + let return_obj: Rc = match func_result { + Some(func_result) => { + Rc::new(Value::new(func_result)) + } + None => Rc::new(Void::new()), + }; + + self.get_state_mut().push_evaluation_stack(return_obj); + + Ok(()) + } + + pub(crate) fn validate_external_bindings(&mut self) -> Result<(), StoryError> { + let mut missing_externals: HashSet = HashSet::new(); + + self.validate_external_bindings_container(&self.get_main_content_container(), &mut missing_externals)?; + + if missing_externals.is_empty() { + self.has_validated_externals = true; + } else { + let join: String = missing_externals.iter().cloned().collect::>().join(", "); + let message = format!( + "ERROR: Missing function binding for external{}: '{}' {}", + if missing_externals.len() > 1 { "s" } else { "" }, + join, + if self.allow_external_function_fallbacks { + ", and no fallback ink function found." + } else { + " (ink fallbacks disabled)" + } + ); + + return Err(StoryError::InvalidStoryState(message)); + } + + Ok(()) + } + + fn validate_external_bindings_container(&self, c: &Rc, missing_externals: &mut std::collections::HashSet) -> Result<(), StoryError> { + for inner_content in c.content.iter() { + let container = inner_content.clone().into_any().downcast::().ok(); + + match &container { + Some(container) => if !container.has_valid_name(){ + self.validate_external_bindings_container(container, missing_externals)?; + }, + None => {self.validate_external_bindings_rtobject(inner_content, missing_externals)?;}, + } + + if container.is_none() || !container.as_ref().unwrap().has_valid_name() { + self.validate_external_bindings_rtobject(inner_content, missing_externals)?; + } + } + + for inner_key_value in c.named_content.values() { + self.validate_external_bindings_container(inner_key_value, missing_externals)?; + } + + Ok(()) + } + + fn validate_external_bindings_rtobject(&self, o: &Rc, missing_externals: &mut std::collections::HashSet) -> Result<(), StoryError> { + let divert = o.clone().into_any().downcast::().ok(); + + if let Some(divert) = divert { + if divert.is_external { + let name = divert.get_target_path_string().unwrap(); + + if !self.externals.contains_key(&name) { + + if self.allow_external_function_fallbacks { + let fallback_found = + self.get_main_content_container().named_content.contains_key(&name); + if !fallback_found { + missing_externals.insert(name); + } + } else { + missing_externals.insert(name); + } + } + } + } + + Ok(()) + } + +} \ No newline at end of file diff --git a/lib/src/value.rs b/lib/src/value.rs index c645792..a4db677 100644 --- a/lib/src/value.rs +++ b/lib/src/value.rs @@ -36,6 +36,10 @@ impl fmt::Display for Value { } impl Value { + pub fn new(value:ValueType) -> Self { + Self { obj: Object::new(), value } + } + pub fn new_bool(v:bool) -> Self { Self { obj: Object::new(), value: ValueType::Bool(v) } } diff --git a/lib/src/value_type.rs b/lib/src/value_type.rs index 2297164..b8d6ed8 100644 --- a/lib/src/value_type.rs +++ b/lib/src/value_type.rs @@ -1,6 +1,6 @@ use std::fmt; -use crate::{object::{RTObject, Object}, path::Path, ink_list::InkList}; +use crate::{object::{RTObject, Object}, path::Path, ink_list::InkList, story_error::StoryError}; #[repr(u8)] #[derive(Clone)] @@ -58,6 +58,42 @@ impl ValueType { _ => None, } } + + pub fn coerce_to_int(&self) -> Result { + match self { + ValueType::Bool(v) => if *v {Ok(1)} else {Ok(0)}, + ValueType::Int(v) => Ok(*v), + ValueType::Float(v) => Ok(*v as i32), + _ => Err(StoryError::BadArgument("Failed to cast to int".to_owned())), + } + } + + pub fn coerce_to_float(&self) -> Result { + match self { + ValueType::Bool(v) => if *v {Ok(1.0)} else {Ok(0.0)}, + ValueType::Int(v) => Ok(*v as f32), + ValueType::Float(v) => Ok(*v), + _ => Err(StoryError::BadArgument("Failed to cast to float".to_owned())), + } + } + + pub fn coerce_to_bool(&self) -> Result { + match self { + ValueType::Bool(v) => Ok(*v), + ValueType::Int(v) => if *v == 1 {Ok(true)} else {Ok(false)}, + _ => Err(StoryError::BadArgument("Failed to cast to boolean".to_owned())), + } + } + + pub fn coerce_to_string(&self) -> Result { + match self { + ValueType::Bool(v) => Ok(v.to_string()), + ValueType::Int(v) => Ok(v.to_string()), + ValueType::Float(v) => Ok(v.to_string()), + ValueType::String(v) => Ok(v.string.clone()), + _ => Err(StoryError::BadArgument("Failed to cast to float".to_owned())), + } + } } diff --git a/lib/tests/basic_text_test.rs b/lib/tests/basic_text_test.rs index 7d44612..c8facdd 100644 --- a/lib/tests/basic_text_test.rs +++ b/lib/tests/basic_text_test.rs @@ -7,7 +7,7 @@ mod common; fn oneline_test() -> Result<(), StoryError> { let json_string = fs::read_to_string("tests/data/basictext/oneline.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); assert!(story.can_continue()); @@ -23,7 +23,7 @@ fn oneline_test() -> Result<(), StoryError> { fn twolines_test() -> Result<(), StoryError> { let json_string = fs::read_to_string("tests/data/basictext/twolines.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); diff --git a/lib/tests/choice_test.rs b/lib/tests/choice_test.rs index 89737ab..726f090 100644 --- a/lib/tests/choice_test.rs +++ b/lib/tests/choice_test.rs @@ -49,7 +49,7 @@ fn multi_choice_test() -> Result<(), StoryError> { fn single_choice1_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/choices/single-choice.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -63,7 +63,7 @@ fn single_choice1_test() -> Result<(), StoryError> { fn single_choic2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/choices/single-choice.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; story.choose_choice_index(0); @@ -81,7 +81,7 @@ fn single_choic2_test() -> Result<(), StoryError> { fn suppress_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/choices/suppress-choice.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -102,7 +102,7 @@ fn suppress_choice_test() -> Result<(), StoryError> { fn mixed_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/choices/mixed-choice.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -124,7 +124,7 @@ fn mixed_choice_test() -> Result<(), StoryError> { fn varying_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/choices/varying-choice.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -145,7 +145,7 @@ fn varying_choice_test() -> Result<(), StoryError> { fn sticky_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/choices/sticky-choice.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -164,7 +164,7 @@ fn sticky_choice_test() -> Result<(), StoryError> { fn fallback_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/choices/fallback-choice.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -177,7 +177,7 @@ fn fallback_choice_test() -> Result<(), StoryError> { fn fallback_choice2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/choices/fallback-choice.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -200,7 +200,7 @@ fn fallback_choice2_test() -> Result<(), StoryError> { fn conditional_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/choices/conditional-choice.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -213,7 +213,7 @@ fn conditional_choice_test() -> Result<(), StoryError> { fn label_flow_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/choices/label-flow.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -233,7 +233,7 @@ fn label_flow_test() -> Result<(), StoryError> { fn label_flow2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/choices/label-flow.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -253,7 +253,7 @@ fn label_flow2_test() -> Result<(), StoryError> { fn label_scope_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/choices/label-scope.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -272,7 +272,7 @@ fn label_scope_test() -> Result<(), StoryError> { fn divert_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/choices/divert-choice.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; diff --git a/lib/tests/conditional_test.rs b/lib/tests/conditional_test.rs index 06ed0a4..6402344 100644 --- a/lib/tests/conditional_test.rs +++ b/lib/tests/conditional_test.rs @@ -6,7 +6,7 @@ mod common; fn iftrue_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/iftrue.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -22,7 +22,7 @@ fn iftrue_test() -> Result<(), StoryError> { fn iffalse_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/iffalse.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -38,7 +38,7 @@ fn iffalse_test() -> Result<(), StoryError> { fn ifelse_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/ifelse.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -54,7 +54,7 @@ fn ifelse_test() -> Result<(), StoryError> { fn ifelse_ext_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/ifelse-ext.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -70,7 +70,7 @@ fn ifelse_ext_test() -> Result<(), StoryError> { fn ifelse_ext_text1_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/ifelse-ext-text1.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -92,7 +92,7 @@ fn ifelse_ext_text1_test() -> Result<(), StoryError> { fn ifelse_ext_text2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/ifelse-ext-text2.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -114,7 +114,7 @@ fn ifelse_ext_text2_test() -> Result<(), StoryError> { fn ifelse_ext_text3_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/ifelse-ext-text3.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -136,7 +136,7 @@ fn ifelse_ext_text3_test() -> Result<(), StoryError> { fn cond_text1_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/condtext.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -155,7 +155,7 @@ fn cond_text1_test() -> Result<(), StoryError> { fn cond_text2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/condtext.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -174,7 +174,7 @@ fn cond_text2_test() -> Result<(), StoryError> { fn cond_opt1_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/condopt.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -192,7 +192,7 @@ fn cond_opt1_test() -> Result<(), StoryError> { fn cond_opt2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/condopt.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -210,7 +210,7 @@ fn cond_opt2_test() -> Result<(), StoryError> { fn stopping_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/stopping.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -244,7 +244,7 @@ fn stopping_test() -> Result<(), StoryError> { fn cycle_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/cycle.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -278,7 +278,7 @@ fn cycle_test() -> Result<(), StoryError> { fn once_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/once.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -309,7 +309,7 @@ fn once_test() -> Result<(), StoryError> { fn shuffle_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/shuffle.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -340,7 +340,7 @@ fn shuffle_test() -> Result<(), StoryError> { fn shuffle_stopping() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/shuffle_stopping.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -373,7 +373,7 @@ fn shuffle_stopping() -> Result<(), StoryError> { fn shuffle_once() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/shuffle_once.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -404,7 +404,7 @@ fn shuffle_once() -> Result<(), StoryError> { fn multiline_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/multiline.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -433,7 +433,7 @@ fn multiline_test() -> Result<(), StoryError> { fn multiline_divert_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/multiline-divert.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -462,7 +462,7 @@ fn multiline_divert_test() -> Result<(), StoryError> { fn multiline_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/multiline-choice.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); diff --git a/lib/tests/divert_test.rs b/lib/tests/divert_test.rs index ca2f82c..784be1f 100644 --- a/lib/tests/divert_test.rs +++ b/lib/tests/divert_test.rs @@ -6,7 +6,7 @@ mod common; fn simple_divert_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/divert/simple-divert.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -21,7 +21,7 @@ fn simple_divert_test() -> Result<(), StoryError> { fn invisible_divert_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/divert/invisible-divert.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -35,7 +35,7 @@ fn invisible_divert_test() -> Result<(), StoryError> { fn divert_on_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/divert/divert-on-choice.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -54,7 +54,7 @@ fn divert_on_choice_test() -> Result<(), StoryError> { fn complex_branching1_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/divert/complex-branching.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -74,7 +74,7 @@ fn complex_branching1_test() -> Result<(), StoryError> { fn complex_branching2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/divert/complex-branching.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; diff --git a/lib/tests/function_test.rs b/lib/tests/function_test.rs index 62e430c..75f0a95 100644 --- a/lib/tests/function_test.rs +++ b/lib/tests/function_test.rs @@ -6,7 +6,7 @@ mod common; fn fun_basic_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/function/func-basic.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -20,7 +20,7 @@ fn fun_basic_test() -> Result<(), StoryError> { fn fun_none_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/function/func-none.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -34,7 +34,7 @@ fn fun_none_test() -> Result<(), StoryError> { fn fun_inline_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/function/func-inline.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -48,7 +48,7 @@ fn fun_inline_test() -> Result<(), StoryError> { fn setvar_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/function/setvar-func.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -62,7 +62,7 @@ fn setvar_test() -> Result<(), StoryError> { fn complex_func1_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/function/complex-func1.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -76,7 +76,7 @@ fn complex_func1_test() -> Result<(), StoryError> { fn complex_func2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/function/complex-func2.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -90,7 +90,7 @@ fn complex_func2_test() -> Result<(), StoryError> { fn complex_func3_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/function/complex-func3.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -105,7 +105,7 @@ fn complex_func3_test() -> Result<(), StoryError> { fn rnd() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/function/rnd-func.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -122,7 +122,7 @@ fn rnd() -> Result<(), StoryError> { fn evaluating_function_variable_state_bug_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/function/evaluating-function-variablestate-bug.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; assert_eq!("Start\n", story.cont()?); assert_eq!("In tunnel.\n", story.cont()?); diff --git a/lib/tests/gather_test.rs b/lib/tests/gather_test.rs index 95290d0..b82490b 100644 --- a/lib/tests/gather_test.rs +++ b/lib/tests/gather_test.rs @@ -6,7 +6,7 @@ mod common; fn gather_basic_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/gather/gather-basic.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -27,7 +27,7 @@ fn gather_basic_test() -> Result<(), StoryError> { fn gather_chain_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/gather/gather-chain.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -55,7 +55,7 @@ fn gather_chain_test() -> Result<(), StoryError> { fn nested_flow_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/gather/nested-flow.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -78,7 +78,7 @@ fn nested_flow_test() -> Result<(), StoryError> { fn deep_nesting_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/gather/deep-nesting.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -114,7 +114,7 @@ fn deep_nesting_test() -> Result<(), StoryError> { fn complex_flow1_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/gather/complex-flow.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -132,7 +132,7 @@ fn complex_flow1_test() -> Result<(), StoryError> { fn complex_flow2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/gather/complex-flow.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; diff --git a/lib/tests/glue_test.rs b/lib/tests/glue_test.rs index 70a14a7..ebdf5dd 100644 --- a/lib/tests/glue_test.rs +++ b/lib/tests/glue_test.rs @@ -6,7 +6,7 @@ mod common; fn simple_glue_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/glue/simple-glue.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -20,7 +20,7 @@ fn simple_glue_test() -> Result<(), StoryError> { fn glue_with_divert_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/glue/glue-with-divert.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -35,7 +35,7 @@ fn glue_with_divert_test() -> Result<(), StoryError> { fn has_left_right_glue_matching_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/glue/left-right-glue-matching.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -51,7 +51,7 @@ fn has_left_right_glue_matching_test() -> Result<(), StoryError> { fn bugfix1_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/glue/testbugfix1.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -67,7 +67,7 @@ fn bugfix1_test() -> Result<(), StoryError> { fn bugfix2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/glue/testbugfix2.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; diff --git a/lib/tests/knot_test.rs b/lib/tests/knot_test.rs index f369328..f84e6af 100644 --- a/lib/tests/knot_test.rs +++ b/lib/tests/knot_test.rs @@ -6,7 +6,7 @@ mod common; fn single_line_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/knot/single-line.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -21,7 +21,7 @@ fn single_line_test() -> Result<(), StoryError> { fn multi_line_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/knot/multi-line.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -38,7 +38,7 @@ fn multi_line_test() -> Result<(), StoryError> { fn strip_empty_lines_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/knot/strip-empty-lines.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -55,7 +55,7 @@ fn strip_empty_lines_test() -> Result<(), StoryError> { fn param_strings_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/knot/param-strings.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -74,7 +74,7 @@ fn param_strings_test() -> Result<(), StoryError> { fn param_ints_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/knot/param-ints.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -93,7 +93,7 @@ fn param_ints_test() -> Result<(), StoryError> { fn param_floats_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/knot/param-floats.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -112,7 +112,7 @@ fn param_floats_test() -> Result<(), StoryError> { fn param_vars_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/knot/param-vars.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -131,7 +131,7 @@ fn param_vars_test() -> Result<(), StoryError> { fn param_multi_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/knot/param-multi.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -150,7 +150,7 @@ fn param_multi_test() -> Result<(), StoryError> { fn param_recurse_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/knot/param-recurse.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; diff --git a/lib/tests/list_test.rs b/lib/tests/list_test.rs index 1e31a7d..059ae7e 100644 --- a/lib/tests/list_test.rs +++ b/lib/tests/list_test.rs @@ -1,12 +1,14 @@ +use std::error::Error; + use bink::{story::Story, story_error::StoryError}; mod common; #[test] -fn list_basic_operations_test() -> Result<(), StoryError> { +fn list_basic_operations_test() -> Result<(), Box> { let json_string = - common::get_json_string("tests/data/lists/basic-operations.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + common::get_json_string("tests/data/lists/basic-operations.ink.json")?; + let mut story = Story::new(&json_string)?; assert_eq!("b, d\na, b, c, e\nb, c\nfalse\ntrue\ntrue\n", &story.continue_maximally()?); @@ -14,10 +16,10 @@ fn list_basic_operations_test() -> Result<(), StoryError> { } #[test] -fn list_mixed_items_test() -> Result<(), StoryError> { +fn list_mixed_items_test() -> Result<(), Box> { let json_string = - common::get_json_string("tests/data/lists/list-mixed-items.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + common::get_json_string("tests/data/lists/list-mixed-items.ink.json")?; + let mut story = Story::new(&json_string)?; assert_eq!("a, y, c\n", &story.continue_maximally()?); @@ -25,10 +27,10 @@ fn list_mixed_items_test() -> Result<(), StoryError> { } #[test] -fn more_list_operations_test() -> Result<(), StoryError> { +fn more_list_operations_test() -> Result<(), Box> { let json_string = - common::get_json_string("tests/data/lists/more-list-operations.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + common::get_json_string("tests/data/lists/more-list-operations.ink.json")?; + let mut story = Story::new(&json_string)?; assert_eq!("1\nl\nn\nl, m\nn\n", &story.continue_maximally()?); @@ -36,10 +38,10 @@ fn more_list_operations_test() -> Result<(), StoryError> { } #[test] -fn empty_list_origin_test() -> Result<(), StoryError> { +fn empty_list_origin_test() -> Result<(), Box> { let json_string = - common::get_json_string("tests/data/lists/empty-list-origin.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + common::get_json_string("tests/data/lists/empty-list-origin.ink.json")?; + let mut story = Story::new(&json_string)?; assert_eq!("a, b\n", &story.continue_maximally()?); @@ -47,16 +49,16 @@ fn empty_list_origin_test() -> Result<(), StoryError> { } #[test] -fn list_save_load_test() -> Result<(), StoryError> { +fn list_save_load_test() -> Result<(), Box> { let json_string = - common::get_json_string("tests/data/lists/list-save-load.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + common::get_json_string("tests/data/lists/list-save-load.ink.json")?; + let mut story = Story::new(&json_string)?; assert_eq!("a, x, c\n", &story.continue_maximally()?); let saved_state = story.save_state()?; - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; story.load_state(&saved_state)?; @@ -68,10 +70,10 @@ fn list_save_load_test() -> Result<(), StoryError> { } #[test] -fn empty_list_origin_after_assinment_test() -> Result<(), StoryError> { +fn empty_list_origin_after_assinment_test() -> Result<(), Box> { let json_string = - common::get_json_string("tests/data/lists/empty-list-origin-after-assignment.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + common::get_json_string("tests/data/lists/empty-list-origin-after-assignment.ink.json")?; + let mut story = Story::new(&json_string)?; assert_eq!("a, b, c\n", &story.continue_maximally()?); @@ -79,10 +81,10 @@ fn empty_list_origin_after_assinment_test() -> Result<(), StoryError> { } #[test] -fn list_range_test() -> Result<(), StoryError> { +fn list_range_test() -> Result<(), Box> { let json_string = - common::get_json_string("tests/data/lists/list-range.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + common::get_json_string("tests/data/lists/list-range.ink.json")?; + let mut story = Story::new(&json_string)?; assert_eq!("Pound, Pizza, Euro, Pasta, Dollar, Curry, Paella\nEuro, Pasta, Dollar, Curry\nTwo, Three, Four, Five, Six\nPizza, Pasta\n", &story.continue_maximally()?); @@ -90,10 +92,10 @@ fn list_range_test() -> Result<(), StoryError> { } #[test] -fn list_bug_adding_element_test() -> Result<(), StoryError> { +fn list_bug_adding_element_test() -> Result<(), Box> { let json_string = - common::get_json_string("tests/data/lists/bug-adding-element.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + common::get_json_string("tests/data/lists/bug-adding-element.ink.json")?; + let mut story = Story::new(&json_string)?; assert_eq!("", &story.continue_maximally()?); @@ -107,10 +109,10 @@ fn list_bug_adding_element_test() -> Result<(), StoryError> { } #[test] -fn more_list_operations2_test() -> Result<(), StoryError> { +fn more_list_operations2_test() -> Result<(), Box> { let json_string = - common::get_json_string("tests/data/lists/more-list-operations2.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + common::get_json_string("tests/data/lists/more-list-operations2.ink.json")?; + let mut story = Story::new(&json_string)?; assert_eq!("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:c2\nlistinc:b1\n", &story.continue_maximally()?); diff --git a/lib/tests/misc_test.rs b/lib/tests/misc_test.rs index db0f744..b2e325a 100644 --- a/lib/tests/misc_test.rs +++ b/lib/tests/misc_test.rs @@ -6,7 +6,7 @@ mod common; fn operations_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/misc/operations.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; assert_eq!("neg:-3\nmod:1\npow:27\nfloor:3\nceiling:4\nint:3\nfloat:1\n", &story.continue_maximally()?); @@ -17,7 +17,7 @@ fn operations_test() -> Result<(), StoryError> { fn read_counts_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/misc/read-counts.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; assert_eq!("Count start: 0 0 0\n1\n2\n3\nCount end: 3 3 3\n", &story.continue_maximally()?); @@ -28,7 +28,7 @@ fn read_counts_test() -> Result<(), StoryError> { fn turns_since_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/misc/turns-since.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; assert_eq!("0\n0\n", &story.continue_maximally()?); story.choose_choice_index(0); @@ -44,7 +44,7 @@ fn turns_since_test() -> Result<(), StoryError> { fn issue15_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/misc/issue15.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; assert_eq!("This is a test\n", story.cont()?); diff --git a/lib/tests/multi_flow_test.rs b/lib/tests/multi_flow_test.rs index 350c79e..f6511f7 100644 --- a/lib/tests/multi_flow_test.rs +++ b/lib/tests/multi_flow_test.rs @@ -6,7 +6,7 @@ mod common; fn basics_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/runtime/multiflow-basics.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; story.switch_flow("First")?; story.choose_path_string("knot1", true, None)?; @@ -29,7 +29,7 @@ fn basics_test() -> Result<(), StoryError> { fn multiflow_save_load_threads() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/runtime/multiflow-saveloadthreads.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; // Default flow assert_eq!("Default line 1\n", story.cont()?); @@ -58,7 +58,7 @@ fn multiflow_save_load_threads() -> Result<(), StoryError> { // Test choice before reloading state before resetting story.choose_choice_index(0); assert_eq!("Thread 1 red choice\nAfter thread 1 choice (red)\n", story.continue_maximally()?); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; // Load to pre-choice: still red, choose second choice story.load_state(&saved)?; diff --git a/lib/tests/runtime_test.rs b/lib/tests/runtime_test.rs index 8c9e8d1..47ac7d0 100644 --- a/lib/tests/runtime_test.rs +++ b/lib/tests/runtime_test.rs @@ -1,11 +1,123 @@ use core::panic; -use std::{cell::RefCell, rc::Rc}; +use std::{cell::RefCell, rc::Rc, error::Error}; -use bink::{story::Story, value_type::{ValueType, StringValue}, story_error::StoryError, story_callbacks::VariableObserver}; +use bink::{story::Story, value_type::ValueType, story_callbacks::{VariableObserver, ExternalFunction}}; mod common; -// TODO external functions +struct ExtFunc1; +struct ExtFunc2; +struct ExtFunc3; +struct ExtFunc4; + +impl ExternalFunction for ExtFunc1 { + fn call(&mut self, func_name: &str, args: Vec) -> Option { + println!("Calling {func_name}..."); + + let x = args[0].coerce_to_int().unwrap_or_default(); + let y = args[1].coerce_to_int().unwrap_or_default(); + + Some(ValueType::Int(x - y)) + } +} + +impl ExternalFunction for ExtFunc2 { + fn call(&mut self, _: &str, args: Vec) -> Option { + Some(ValueType::new_string("Hello world")) + } +} + +impl ExternalFunction for ExtFunc3 { + fn call(&mut self, _: &str, args: Vec) -> Option { + Some(ValueType::Bool(args[0].get_int().unwrap() != 1)) + } +} + +impl ExternalFunction for ExtFunc4 { + fn call(&mut self, _: &str, args: Vec) -> Option { + Some(ValueType::Bool(!args[0].coerce_to_bool().unwrap())) + } +} + +#[test] +fn external_function() -> Result<(), Box> { + let json_string = + common::get_json_string("tests/data/runtime/external-function-2-arg.ink.json")?; + let mut story = Story::new(&json_string)?; + let mut text: Vec = Vec::new(); + + story.bind_external_function("externalFunction", Rc::new(RefCell::new(ExtFunc1{})), true)?; + + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("The value is -1.", text[0]); + + Ok(()) +} + +#[test] +fn external_function_zero_arguments() -> Result<(), Box> { + let json_string = + common::get_json_string("tests/data/runtime/external-function-0-arg.ink.json")?; + let mut story = Story::new(&json_string)?; + let mut text: Vec = Vec::new(); + + story.bind_external_function("externalFunction", Rc::new(RefCell::new(ExtFunc2{})), true)?; + + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("The value is Hello world.", text[0]); + + Ok(()) +} + +#[test] +fn external_function_one_arguments() -> Result<(), Box> { + let json_string = + common::get_json_string("tests/data/runtime/external-function-1-arg.ink.json")?; + let mut story = Story::new(&json_string)?; + let mut text: Vec = Vec::new(); + + story.bind_external_function("externalFunction", Rc::new(RefCell::new(ExtFunc3{})), true)?; + + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("The value is false.", text[0]); + + Ok(()) +} + +#[test] +fn external_function_coerce_test() -> Result<(), Box> { + let json_string = + common::get_json_string("tests/data/runtime/external-function-1-arg.ink.json")?; + let mut story = Story::new(&json_string)?; + let mut text: Vec = Vec::new(); + + story.bind_external_function("externalFunction", Rc::new(RefCell::new(ExtFunc4{})), true)?; + + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("The value is false.", text[0]); + + Ok(()) +} + +#[test] +fn external_function_fallback_test() -> Result<(), Box> { + let json_string = + common::get_json_string("tests/data/runtime/external-function-2-arg.ink.json")?; + let mut story = Story::new(&json_string)?; + let mut text: Vec = Vec::new(); + + story.set_allow_external_function_fallbacks(true); + + common::next_all(&mut story, &mut text)?; + assert_eq!(1, text.len()); + assert_eq!("The value is 7.", text[0]); + + Ok(()) +} struct VObserver { expected_value: i32, @@ -28,13 +140,13 @@ impl VariableObserver for VObserver { } #[test] -fn variable_observers_test() -> Result<(), StoryError> { +fn variable_observers_test() -> Result<(), Box> { let json_string = - common::get_json_string("tests/data/runtime/variable-observers.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + common::get_json_string("tests/data/runtime/variable-observers.ink.json")?; + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - story.observe_variable("x", Rc::new(RefCell::new(VObserver { expected_value: 5}))); + story.observe_variable("x", Rc::new(RefCell::new(VObserver { expected_value: 5})))?; common::next_all(&mut story, &mut text)?; story.choose_choice_index(0); @@ -45,10 +157,10 @@ fn variable_observers_test() -> Result<(), StoryError> { #[test] -fn set_and_get_variable_test() -> Result<(), StoryError> { +fn set_and_get_variable_test() -> Result<(), Box> { let json_string = - common::get_json_string("tests/data/runtime/set-get-variables.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + common::get_json_string("tests/data/runtime/set-get-variables.ink.json")?; + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -71,10 +183,10 @@ fn set_and_get_variable_test() -> Result<(), StoryError> { #[test] -fn set_non_existant_variable_test() -> Result<(), StoryError> { +fn set_non_existant_variable_test() -> Result<(), Box> { let json_string = - common::get_json_string("tests/data/runtime/set-get-variables.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + common::get_json_string("tests/data/runtime/set-get-variables.ink.json")?; + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -100,10 +212,10 @@ fn set_non_existant_variable_test() -> Result<(), StoryError> { } #[test] -fn jump_knot_test() -> Result<(), StoryError> { +fn jump_knot_test() -> Result<(), Box> { let json_string = - common::get_json_string("tests/data/runtime/jump-knot.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + common::get_json_string("tests/data/runtime/jump-knot.ink.json")?; + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); story.choose_path_string("two", true, None)?; @@ -129,10 +241,10 @@ fn jump_knot_test() -> Result<(), StoryError> { } #[test] -fn jump_stitch_test() -> Result<(), StoryError> { +fn jump_stitch_test() -> Result<(), Box> { let json_string = - common::get_json_string("tests/data/runtime/jump-stitch.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + common::get_json_string("tests/data/runtime/jump-stitch.ink.json")?; + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); story.choose_path_string("two.sthree", true, None)?; @@ -158,10 +270,10 @@ fn jump_stitch_test() -> Result<(), StoryError> { } #[test] -fn read_visit_counts_test() -> Result<(), StoryError> { +fn read_visit_counts_test() -> Result<(), Box> { let json_string = - common::get_json_string("tests/data/runtime/read-visit-counts.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + common::get_json_string("tests/data/runtime/read-visit-counts.ink.json")?; + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -172,10 +284,10 @@ fn read_visit_counts_test() -> Result<(), StoryError> { } #[test] -fn load_save_test() -> Result<(), StoryError> { +fn load_save_test() -> Result<(), Box> { let json_string = - common::get_json_string("tests/data/runtime/load-save.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + common::get_json_string("tests/data/runtime/load-save.ink.json")?; + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; diff --git a/lib/tests/stitch_test.rs b/lib/tests/stitch_test.rs index 92244f6..df70109 100644 --- a/lib/tests/stitch_test.rs +++ b/lib/tests/stitch_test.rs @@ -6,7 +6,7 @@ mod common; fn auto_stitch_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/stitch/auto-stitch.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -21,7 +21,7 @@ fn auto_stitch_test() -> Result<(), StoryError> { fn auto_stitch2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/stitch/auto-stitch.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -39,7 +39,7 @@ fn auto_stitch2_test() -> Result<(), StoryError> { fn manual_stitch_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/stitch/manual-stitch.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -61,7 +61,7 @@ fn manual_stitch_test() -> Result<(), StoryError> { fn manual_stitch2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/stitch/manual-stitch.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; diff --git a/lib/tests/tag_test.rs b/lib/tests/tag_test.rs index 73c6455..8339fe3 100644 --- a/lib/tests/tag_test.rs +++ b/lib/tests/tag_test.rs @@ -6,7 +6,7 @@ mod common; fn tags_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/tags/tags.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let global_tags = story.get_global_tags()?; assert_eq!(2, global_tags.len()); @@ -45,7 +45,7 @@ fn tags_test() -> Result<(), StoryError> { fn tags_in_seq_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/tags/tagsInSeq.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; assert_eq!("A red sequence.\n", story.cont()?); let current_tags = story.get_current_tags(); @@ -64,7 +64,7 @@ fn tags_in_seq_test() -> Result<(), StoryError> { fn tags_in_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/tags/tagsInChoice.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; story.cont()?; let current_tags = story.get_current_tags(); @@ -90,7 +90,7 @@ fn tags_in_choice_test() -> Result<(), StoryError> { fn tags_dynamic_content_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/tags/tagsDynamicContent.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; assert_eq!("tag\n", story.cont()?); let current_tags = story.get_current_tags(); diff --git a/lib/tests/thread_test.rs b/lib/tests/thread_test.rs index 1f59e3a..ff185ba 100644 --- a/lib/tests/thread_test.rs +++ b/lib/tests/thread_test.rs @@ -6,7 +6,7 @@ mod common; fn thread_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/threads/thread-bug.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); assert_eq!("Here is some gold. Do you want it?\n", story.continue_maximally()?); @@ -31,7 +31,7 @@ fn thread_test() -> Result<(), StoryError> { fn thread_test_bug() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/threads/thread-bug.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); assert_eq!("Here is some gold. Do you want it?\n", story.continue_maximally()?); @@ -41,7 +41,7 @@ fn thread_test_bug() -> Result<(), StoryError> { let save_string = story.save_state()?; println!("{}", save_string); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; story.load_state(&save_string)?; story.choose_choice_index(0); diff --git a/lib/tests/tunnel_test.rs b/lib/tests/tunnel_test.rs index b271487..dc2892a 100644 --- a/lib/tests/tunnel_test.rs +++ b/lib/tests/tunnel_test.rs @@ -6,7 +6,7 @@ mod common; fn tunnel_onwards_divert_override_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/tunnels/tunnel-onwards-divert-override.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; assert_eq!("This is A\nNow in B.\n", story.continue_maximally()?); diff --git a/lib/tests/variable_test.rs b/lib/tests/variable_test.rs index b512736..a330f27 100644 --- a/lib/tests/variable_test.rs +++ b/lib/tests/variable_test.rs @@ -6,7 +6,7 @@ mod common; fn variable_declaration_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/variable/variable-declaration.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -20,7 +20,7 @@ fn variable_declaration_test() -> Result<(), StoryError> { fn var_calc_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/variable/varcalc.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -34,7 +34,7 @@ fn var_calc_test() -> Result<(), StoryError> { fn var_string_ink_bug_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/variable/varstringinc.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -52,7 +52,7 @@ fn var_string_ink_bug_test() -> Result<(), StoryError> { fn var_divert_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/variable/var-divert.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; diff --git a/lib/tests/variable_text_test.rs b/lib/tests/variable_text_test.rs index c1709b4..4c00ab7 100644 --- a/lib/tests/variable_text_test.rs +++ b/lib/tests/variable_text_test.rs @@ -6,7 +6,7 @@ mod common; fn sequence_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/variabletext/sequence.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -49,7 +49,7 @@ fn sequence_test() -> Result<(), StoryError> { fn cycle_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/variabletext/cycle.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -92,7 +92,7 @@ fn cycle_test() -> Result<(), StoryError> { fn once_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/variabletext/once.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -135,7 +135,7 @@ fn once_test() -> Result<(), StoryError> { fn empty_elements_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/variabletext/empty-elements.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -164,7 +164,7 @@ fn empty_elements_test() -> Result<(), StoryError> { fn list_in_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/variabletext/list-in-choice.ink.json").unwrap(); - let mut story = Story::new(&json_string).unwrap(); + let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; From 4ee82cc368e375e00952b7912eddb758abbfde02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Fri, 6 Oct 2023 19:13:01 +0000 Subject: [PATCH 63/91] Error handling + lot of cleanup --- README.md | 1 - cli-player/src/main.rs | 2 +- lib/src/callstack.rs | 24 +++------ lib/src/choice.rs | 19 +++---- lib/src/choice_point.rs | 27 +++------- lib/src/container.rs | 2 +- lib/src/control_command.rs | 2 +- lib/src/divert.rs | 4 +- lib/src/error.rs | 12 ----- lib/src/flow.rs | 2 +- lib/src/lib.rs | 1 - lib/src/path.rs | 59 ++++++++++---------- lib/src/story.rs | 108 +++++++++++++++++++------------------ lib/src/story_callbacks.rs | 11 ++++ lib/tests/tag_test.rs | 18 +++---- 15 files changed, 130 insertions(+), 162 deletions(-) delete mode 100644 lib/src/error.rs diff --git a/README.md b/README.md index 0154574..4b2544a 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,6 @@ Currently under development. This is the implementation status: ## TODO -- [ ] Cache components string in Path - [ ] Assert, revisar y lanzar error. - [ ] Doc. - [ ] Publicar crate. diff --git a/cli-player/src/main.rs b/cli-player/src/main.rs index 1be18bb..45c85ca 100644 --- a/cli-player/src/main.rs +++ b/cli-player/src/main.rs @@ -53,7 +53,7 @@ fn main() -> Result<(), Box> { // Returns true if the program has to stop fn process_command(command: Command, story: &mut Story) -> Result> { match command { - Command::Choose(c) => story.choose_choice_index(c), + Command::Choose(c) => story.choose_choice_index(c)?, Command::Exit() => return Ok(true), Command::Load(filename) => { let saved_string = get_json_string(&filename)?; diff --git a/lib/src/callstack.rs b/lib/src/callstack.rs index 72aefa1..b1c91bc 100644 --- a/lib/src/callstack.rs +++ b/lib/src/callstack.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, rc::Rc}; use serde_json::{Map, json}; -use crate::{pointer::{Pointer, self}, push_pop::PushPopType, container::{Container, self}, value::Value, object::Object, json_read, json_write, path::Path, story::Story, story_error::StoryError}; +use crate::{pointer::{Pointer, self}, push_pop::PushPopType, container::Container, value::Value, object::Object, json_read, json_write, path::Path, story::Story, story_error::StoryError}; pub struct Element { pub current_pointer: Pointer, @@ -128,7 +128,7 @@ impl Thread { el_map.insert("exp".to_owned(), json!(el.in_expression_evaluation)); el_map.insert("type".to_owned(), json!(el.push_pop_type as u32)); - if el.temporary_variables.len() > 0 { + if !el.temporary_variables.is_empty() { el_map.insert("temp".to_owned(), json_write::write_dictionary_values(&el.temporary_variables)?); } @@ -202,7 +202,7 @@ impl CallStack { } pub fn can_pop_thread(&self) -> bool { - return self.threads.len() > 1 && !self.element_is_evaluate_from_game(); + self.threads.len() > 1 && !self.element_is_evaluate_from_game() } pub fn pop_thread(&mut self) -> Result<(), StoryError> { @@ -250,10 +250,6 @@ impl CallStack { self.get_current_element().push_pop_type == PushPopType::FunctionEvaluationFromGame } - pub fn get_elements(&self) -> &Vec { - self.get_callstack() - } - pub fn get_elements_mut(&mut self) -> &mut Vec { self.get_callstack_mut() } @@ -366,15 +362,7 @@ impl CallStack { } pub fn get_thread_with_index(&self, index: usize) -> Option<&Thread> { - // return threads.Find (t => t.threadIndex == index); - - for t in self.threads.iter() { - if t.thread_index == index { - return Some(t); - } - } - - return None; + self.threads.iter().find(|&t| t.thread_index == index) } pub fn load_json(&mut self, main_content_container: &Rc, j_obj: &Map) -> Result<(), StoryError> { @@ -383,8 +371,8 @@ impl CallStack { let j_threads = j_obj.get("threads").unwrap(); - for jThreadTok in j_threads.as_array().unwrap().iter() { - let j_thread_obj = jThreadTok.as_object().unwrap(); + for j_thread_tok in j_threads.as_array().unwrap().iter() { + let j_thread_obj = j_thread_tok.as_object().unwrap(); let thread = Thread::from_json(main_content_container, j_thread_obj)?; self.threads.push(thread); } diff --git a/lib/src/choice.rs b/lib/src/choice.rs index bbf780e..8118a40 100644 --- a/lib/src/choice.rs +++ b/lib/src/choice.rs @@ -5,25 +5,25 @@ use crate::{path::Path, callstack::Thread, object::{Object, RTObject}}; pub struct Choice { obj: Object, - pub target_path: Path, + thread_at_generation: RefCell>, + pub(crate) original_thread_index: RefCell, + pub(crate) source_path: String, + pub(crate) target_path: Path, pub is_invisible_default: bool, pub tags: Vec, pub index: RefCell, - pub original_thread_index: RefCell, pub text: String, - thread_at_generation: RefCell>, - pub source_path: String } impl Choice { - pub fn new(target_path: Path, source_path: String, is_invisible_default: bool, tags: Vec, thread_at_generation: Thread, text: String, index: usize, original_thread_index: usize) -> Choice { + pub fn new(target_path: Path, source_path: String, is_invisible_default: bool, tags: Vec, thread_at_generation: Thread, text: String) -> Choice { Self { obj: Object::new(), target_path, is_invisible_default, tags, - index: RefCell::new(index), - original_thread_index: RefCell::new(original_thread_index), + index: RefCell::new(0), + original_thread_index: RefCell::new(0), text, thread_at_generation: RefCell::new(Some(thread_at_generation)), source_path, @@ -50,10 +50,7 @@ impl Choice { } pub fn get_thread_at_generation(&self) -> Option { - match self.thread_at_generation.borrow().as_ref() { - Some(t) => Some(t.copy()), - None => None, - } + self.thread_at_generation.borrow().as_ref().map(|t| t.copy()) } } diff --git a/lib/src/choice_point.rs b/lib/src/choice_point.rs index 0740956..7eb202b 100644 --- a/lib/src/choice_point.rs +++ b/lib/src/choice_point.rs @@ -10,7 +10,7 @@ pub struct ChoicePoint { is_invisible_default: bool, once_only: bool, has_condition: bool, - path_on_choice: RefCell>, + path_on_choice: RefCell, } impl ChoicePoint { @@ -22,24 +22,12 @@ impl ChoicePoint { is_invisible_default: (flags & 8) > 0, once_only: (flags & 16) > 0, has_condition: (flags & 1) > 0, - path_on_choice: RefCell::new(Some(Path::new_with_components_string(Some(path_string_on_choice)))), - } - } - - pub fn with_once_only(once_only: bool) -> Self { - Self { - obj: Object::new(), - has_choice_only_content: false, - has_start_content: false, - is_invisible_default: false, - once_only, - has_condition: false, - path_on_choice: RefCell::new(None), + path_on_choice: RefCell::new(Path::new_with_components_string(Some(path_string_on_choice))), } } pub fn get_choice_target(self: &Rc) -> Option> { - Object::resolve_path(self.clone(), &self.path_on_choice.borrow().as_ref().unwrap()).container() + Object::resolve_path(self.clone(), &self.path_on_choice.borrow()).container() } pub fn get_flags(&self) -> i32 { @@ -84,14 +72,13 @@ impl ChoicePoint { pub fn get_path_on_choice(self: &Rc) -> Path { // Resolve any relative paths to global ones as we come across them - let has_path = self.path_on_choice.borrow().is_some(); - if has_path && self.path_on_choice.borrow().as_ref().unwrap().is_relative(){ + if self.path_on_choice.borrow().is_relative(){ if let Some(choice_target_obj) = self.get_choice_target() { - self.path_on_choice.replace(Some(choice_target_obj.get_path())); + self.path_on_choice.replace(choice_target_obj.get_path()); } } - self.path_on_choice.borrow().as_ref().unwrap().clone() + self.path_on_choice.borrow().clone() } pub fn get_path_string_on_choice(self: &Rc) -> String { @@ -111,7 +98,7 @@ impl fmt::Display for ChoicePoint { // let mut target_string = self.get_path_on_choice()?.to_string(); - let target_string = self.path_on_choice.borrow().as_ref().unwrap().to_string(); + let target_string = self.path_on_choice.borrow().to_string(); // if let Some(line_num) = target_line_num { // target_string = format!(" line {}({})", line_num, target_string); diff --git a/lib/src/container.rs b/lib/src/container.rs index 129c898..aaa6371 100644 --- a/lib/src/container.rs +++ b/lib/src/container.rs @@ -301,6 +301,6 @@ impl RTObject for Container { impl fmt::Display for Container { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "**Container**") + write!(f, "Container ({})", self.name.as_ref().unwrap_or(&"".to_owned())) } } \ No newline at end of file diff --git a/lib/src/control_command.rs b/lib/src/control_command.rs index 3bcc362..599ab91 100644 --- a/lib/src/control_command.rs +++ b/lib/src/control_command.rs @@ -145,7 +145,7 @@ impl RTObject for ControlCommand { impl fmt::Display for ControlCommand { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.command_type.to_string()) + write!(f, "{}", self.command_type) } } diff --git a/lib/src/divert.rs b/lib/src/divert.rs index f12c7a7..014e4f7 100644 --- a/lib/src/divert.rs +++ b/lib/src/divert.rs @@ -7,13 +7,13 @@ use crate::{object::{Object, RTObject}, push_pop::PushPopType, pointer::{Pointer pub struct Divert { obj: Object, + target_pointer: RefCell, + target_path: RefCell>, pub external_args: usize, pub is_conditional: bool, pub is_external: bool, pub pushes_to_stack: bool, pub stack_push_type: PushPopType, - target_pointer: RefCell, - target_path: RefCell>, pub variable_divert_name: Option, } diff --git a/lib/src/error.rs b/lib/src/error.rs deleted file mode 100644 index 4b1123d..0000000 --- a/lib/src/error.rs +++ /dev/null @@ -1,12 +0,0 @@ -pub trait ErrorHandler { - fn error(message: &str, error_type: ErrorType); -} - -pub 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 -} \ No newline at end of file diff --git a/lib/src/flow.rs b/lib/src/flow.rs index af8aa6a..28319ef 100644 --- a/lib/src/flow.rs +++ b/lib/src/flow.rs @@ -5,7 +5,7 @@ use serde_json::Map; use crate::{callstack::{CallStack, Thread}, choice::Choice, object::RTObject, container::Container, json_write, json_read, story_error::StoryError}; #[derive(Clone)] -pub struct Flow { +pub(crate) struct Flow { pub name: String, pub callstack: Rc>, pub output_stream: Vec>, diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 97c13ca..f4ba511 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -17,7 +17,6 @@ mod callstack; mod flow; mod push_pop; mod variables_state; -mod error; mod glue; mod void; mod state_patch; diff --git a/lib/src/path.rs b/lib/src/path.rs index 5b97c4c..3239794 100644 --- a/lib/src/path.rs +++ b/lib/src/path.rs @@ -1,17 +1,17 @@ use std::{ fmt, - hash::{Hash, Hasher}, + hash::{Hash, Hasher}, cell::{RefCell, OnceCell}, }; const PARENT_ID: &str = "^"; /// The componentsString field from the C# impl. has been removed and it is always generated dinamically from the components field. -#[derive(Eq, Clone)] +#[derive(Eq, Clone, Default)] pub struct Path { components: Vec, is_relative: bool, - // components_string: RefCell, // TODO + components_string: OnceCell, } impl Path { @@ -33,7 +33,7 @@ impl Path { pub fn new_with_components_string(components_string: Option<&str>) -> Path { let cs = components_string; - let mut is_relative = false; + let is_relative:bool; // Empty path, empty components // (path is to root, like "/" in file system) @@ -50,7 +50,7 @@ impl Path { // is equivalent to file system style path: // ../../hello/5 - if cs.chars().next().unwrap() == '.' { + if cs.starts_with('.') { is_relative = true; cs = cs[1..].to_string(); } else { @@ -69,9 +69,13 @@ impl Path { } } + let cs_cell = OnceCell::new(); + let _ = cs_cell.set(cs); + Path { components, is_relative, + components_string: cs_cell } } @@ -87,9 +91,9 @@ impl Path { if self.components.len() >= 2 { let tail_comps = &self.components[1..]; - return Path::new(tail_comps, false); + Path::new(tail_comps, false) } else { - return Path::get_self(); + Path::get_self() } } @@ -105,8 +109,8 @@ impl Path { } pub fn get_last_component(&self) -> Option<&Component> { - if self.components.len() > 0 { - return self.components.get(self.components.len() - 1); + if !self.components.is_empty() { + return self.components.last(); } None @@ -141,22 +145,24 @@ impl Path { } pub fn get_components_string(&self) -> String { - let mut sb = String::new(); + return self.components_string.get_or_init( || { + let mut sb = String::new(); - if self.components.len() > 0 { - sb.push_str(&self.components.get(0).unwrap().to_string()); + if !self.components.is_empty() { + sb.push_str(&self.components.get(0).unwrap().to_string()); - for i in 1..self.components.len() { - sb.push('.'); - sb.push_str(&self.components.get(i).unwrap().to_string()); + for i in 1..self.components.len() { + sb.push('.'); + sb.push_str(&self.components.get(i).unwrap().to_string()); + } } - } - if self.is_relative { - return ".".to_owned() + &sb; - } + if self.is_relative { + return ".".to_owned() + &sb; + } - sb + sb + }).to_string(); } pub fn path_by_appending_component( &self, c: Component) -> Path { @@ -173,15 +179,6 @@ impl fmt::Display for Path { } } -impl Default for Path { - fn default() -> Self { - Self { - components: Default::default(), - is_relative: Default::default(), - } - } -} - impl Hash for Path { fn hash(&self, state: &mut H) { self.to_string().hash(state) @@ -209,7 +206,7 @@ impl PartialEq for Path { } } - return true; + true } } @@ -277,7 +274,7 @@ impl PartialEq for Component { impl Hash for Component { fn hash(&self, state: &mut H) { match self.index { - Some(index) => return index.hash(state), + Some(index) => index.hash(state), None => return self.name.as_ref().unwrap().hash(state), } } diff --git a/lib/src/story.rs b/lib/src/story.rs index 8c68c0a..a62b8cc 100644 --- a/lib/src/story.rs +++ b/lib/src/story.rs @@ -6,10 +6,9 @@ use rand::{Rng, rngs::StdRng, SeedableRng}; use crate::{ container::Container, - error::ErrorType, json_read, push_pop::PushPopType, - story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, search_result::SearchResult, variable_assigment::VariableAssignment, native_function_call::NativeFunctionCall, variable_reference::VariableReference, list_definitions_origin::ListDefinitionsOrigin, ink_list::InkList, ink_list_item::InkListItem, story_error::StoryError, value_type::ValueType, story_callbacks::{VariableObserver, ExternalFunctionDef}, + story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, search_result::SearchResult, variable_assigment::VariableAssignment, native_function_call::NativeFunctionCall, variable_reference::VariableReference, list_definitions_origin::ListDefinitionsOrigin, ink_list::InkList, ink_list_item::InkListItem, story_error::StoryError, value_type::ValueType, story_callbacks::{VariableObserver, ExternalFunctionDef, ErrorType, ErrorHandler}, }; pub const INK_VERSION_CURRENT: i32 = 21; @@ -31,7 +30,7 @@ pub struct Story { async_saving: bool, prev_containers: Vec>, list_definitions: Rc, - on_error: Option, + on_error: Option>>, pub(crate) state_snapshot_at_last_new_line: Option, pub(crate) variable_observers: HashMap>>>, pub(crate) has_validated_externals: bool, @@ -152,10 +151,7 @@ impl Story { let cp = self.get_state().get_current_pointer().resolve(); - let cp = match cp { - Some(_) => Some(cp.as_ref().unwrap().as_ref()), - None => None, - }; + let cp = cp.as_ref().map(|cp| cp.as_ref()); self.main_content_container .build_string_of_hierarchy(&mut sb, 0, cp); @@ -189,9 +185,7 @@ impl Story { self.validate_external_bindings()?; } - self.continue_internal(millisecs_limit_async)?; - - Ok(()) + self.continue_internal(millisecs_limit_async) } fn continue_internal(&mut self, millisecs_limit_async: f32) -> Result<(), StoryError> { @@ -331,17 +325,17 @@ impl Story { // This may either have been StoryExceptions that were thrown // and caught during evaluation, or directly added with AddError. if self.get_state().has_error() || self.get_state().has_warning() { - match self.on_error { + match &self.on_error { Some(on_err) => { if self.get_state().has_error() { for err in self.get_state().get_current_errors() { - (on_err)(err, ErrorType::Error); + on_err.borrow_mut().error(err, ErrorType::Error); } } if self.get_state().has_warning() { for err in self.get_state().get_current_warnings() { - (on_err)(err, ErrorType::Warning); + on_err.borrow_mut().error(err, ErrorType::Warning); } } @@ -580,7 +574,13 @@ impl Story { // Choice with condition? if let Some(cco) = ¤t_content_obj { - if let Ok(choice_point) = cco.clone().into_any().downcast::() { + // If the container has no content, then it will be + // the "content" itself, but we skip over it. + if cco.as_any().is::() { + should_add_to_stream = false; + } + + if let Ok(choice_point) = cco.clone().into_any().downcast::() { let choice = self.process_choice(&choice_point)?; if let Some(choice) = choice { @@ -592,12 +592,6 @@ impl Story { } } - // If the container has no content, then it will be - // the "content" itself, but we skip over it. - if current_content_obj.is_some() && current_content_obj.as_ref().unwrap().as_any().is::() { - should_add_to_stream = false; - } - // Content to add to evaluation stack or the output stream if should_add_to_stream { @@ -609,12 +603,14 @@ impl Story { let var_pointer = Value::get_variable_pointer_value(current_content_obj.as_ref().unwrap().as_ref()); - if var_pointer.is_some() && var_pointer.unwrap().context_index == -1 { + if let Some(var_pointer) = var_pointer { + if var_pointer.context_index == -1 { - // Create new Object so we're not overwriting the story's own - // data - let context_idx = self.get_state().get_callstack().borrow().context_for_variable_named(&var_pointer.unwrap().variable_name); - current_content_obj = Some(Rc::new(Value::new_variable_pointer(&var_pointer.unwrap().variable_name, context_idx as i32))); + // Create new Object so we're not overwriting the story's own + // data + let context_idx = self.get_state().get_callstack().borrow().context_for_variable_named(&var_pointer.variable_name); + current_content_obj = Some(Rc::new(Value::new_variable_pointer(&var_pointer.variable_name, context_idx as i32))); + } } // Expression evaluation content @@ -803,7 +799,7 @@ impl Story { return Err(StoryError::InvalidStoryState(format!("Tried to divert using a target from a variable that could not be found ({})", var_name.as_ref().unwrap()))); } } else if current_divert.is_external { - self.call_external_function(¤t_divert.get_target_path_string().unwrap(), current_divert.external_args); + self.call_external_function(¤t_divert.get_target_path_string().unwrap(), current_divert.external_args)?; return Ok(true); } else { self.get_state_mut().set_diverted_pointer(current_divert.get_target_pointer()); @@ -999,7 +995,7 @@ impl Story { None => None, }; - let mut either_count = 0; + let either_count:i32; match container { Some(container) => { @@ -1305,28 +1301,28 @@ impl Story { // Variable reference if let Ok(var_ref) = content_obj.clone().into_any().downcast::() { - let mut found_value: Option> = None; + let found_value: Rc; // Explicit read count value if let Some(p) = &var_ref.path_for_count { let container = var_ref.get_container_for_count(); let count = self.get_state_mut().visit_count_for_container(container.as_ref().unwrap()); - found_value = Some(Rc::new(Value::new_int(count))); + found_value = Rc::new(Value::new_int(count)); } // Normal variable reference else { - - found_value = self.get_state().variables_state.get_variable_with_name(&var_ref.name, -1); - - if found_value.is_none() { - self.add_error(&format!("Variable not found: '{}'. 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.", var_ref.name), true); - - found_value = Some(Rc::new(Value::new_int(0))); + match self.get_state().variables_state.get_variable_with_name(&var_ref.name, -1) { + Some(v) => found_value = v, + None => { + self.add_error(&format!("Variable not found: '{}'. 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.", var_ref.name), true); + + found_value = Rc::new(Value::new_int(0)); + }, } } - self.get_state_mut().push_evaluation_stack(found_value.unwrap()); + self.get_state_mut().push_evaluation_stack(found_value); return Ok(true); } @@ -1481,9 +1477,11 @@ impl Story { self.get_state().get_current_errors() } - pub fn choose_choice_index(&mut self, choice_index: usize) { + pub fn choose_choice_index(&mut self, choice_index: usize) -> Result<(), StoryError> { let choices = self.get_current_choices(); - //assert!(choice_index < choices.len(), "choice out of range"); + if choice_index >= choices.len() { + return Err(StoryError::BadArgument("choice out of range".to_owned())); + } // Replace callstack with the one from the thread at the choosing point, // so that we can jump into the right place in the flow. @@ -1493,14 +1491,18 @@ impl Story { let choice_to_choose = choices.get(choice_index).unwrap(); self.get_state().get_callstack().borrow_mut().set_current_thread(choice_to_choose.get_thread_at_generation().unwrap()); - self.choose_path(&choice_to_choose.target_path, true); + self.choose_path(&choice_to_choose.target_path, true)?; + + Ok(()) } - fn choose_path(&mut self, p: &Path, incrementing_turn_index: bool) { - self.get_state_mut().set_chosen_path( &p, incrementing_turn_index); + fn choose_path(&mut self, p: &Path, incrementing_turn_index: bool) -> Result<(), StoryError> { + self.get_state_mut().set_chosen_path( p, incrementing_turn_index)?; // Take a note of newly visited containers for read counts etc self.visit_changed_containers_due_to_divert(); + + Ok(()) } fn is_truthy(&self, obj: Rc) -> Result { @@ -1515,7 +1517,7 @@ impl Story { return val.is_truthy(); } - return Ok(truthy); + Ok(truthy) } fn process_choice(&mut self, choice_point: &Rc) -> Result>, StoryError> { @@ -1558,7 +1560,7 @@ impl Story { start_text.push_str(&choice_only_text); - let choice = Rc::new(Choice::new(choice_point.get_path_on_choice(), Object::get_path(choice_point.as_ref()).to_string(), choice_point.is_invisible_default(), tags, self.get_state().get_callstack().borrow_mut().fork_thread(), start_text.trim().to_string(), 0, 0)); + let choice = Rc::new(Choice::new(choice_point.get_path_on_choice(), Object::get_path(choice_point.as_ref()).to_string(), choice_point.is_invisible_default(), tags, self.get_state().get_callstack().borrow_mut().fork_thread(), start_text.trim().to_string())); Ok(Some(choice)) } @@ -1567,12 +1569,12 @@ impl Story { let obj = self.get_state_mut().pop_evaluation_stack(); let choice_only_str_val = Value::get_string_value(obj.as_ref()).unwrap(); - while self.get_state().evaluation_stack.len() > 0 && self.get_state().peek_evaluation_stack().unwrap().as_any().is::() { + while !self.get_state().evaluation_stack.is_empty() && self.get_state().peek_evaluation_stack().unwrap().as_any().is::() { let tag = self.get_state_mut().pop_evaluation_stack().into_any().downcast::().unwrap(); tags.insert(0, tag.get_text().clone()); // popped in reverse order } - return choice_only_str_val.string.to_string(); + choice_only_str_val.string.to_string() } pub fn pointer_at_path(main_content_container: &Rc, path: &Path) -> Result { @@ -1697,7 +1699,7 @@ impl Story { // TODO: The result and the args should be an object not a String pub fn evaluate_function(&mut self, func_name: &str, args: Option<&Vec>, text_output: &mut String) -> Result, StoryError> { - self.if_async_we_cant("evaluate a function"); + self.if_async_we_cant("evaluate a function")?; if func_name.trim().is_empty() { return Err(StoryError::InvalidStoryState("Function is empty or white space.".to_owned())); @@ -1718,7 +1720,7 @@ impl Story { self.get_state_mut().reset_output(None); // State will temporarily replace the callstack in order to evaluate - self.get_state_mut().start_function_evaluation_from_game(func_container.unwrap(), args); + self.get_state_mut().start_function_evaluation_from_game(func_container.unwrap(), args)?; // Evaluate the function, and collect the string output while self.can_continue() { @@ -1782,7 +1784,7 @@ impl Story { } } - return Err(StoryError::InvalidStoryState("Should never reach here".to_owned())); + Err(StoryError::InvalidStoryState("Should never reach here".to_owned())) } pub fn get_global_tags(&self) -> Result, StoryError> { @@ -1844,13 +1846,13 @@ impl Story { self.main_content_container.content_at_path(path, 0, -1) } - pub fn get_current_tags(&mut self) -> Vec { - self.if_async_we_cant("call currentTags since it's a work in progress"); - return self.get_state_mut().get_current_tags(); + pub fn get_current_tags(&mut self) -> Result, StoryError> { + self.if_async_we_cant("call currentTags since it's a work in progress")?; + Ok(self.get_state_mut().get_current_tags()) } pub fn choose_path_string(&mut self, path: &str, reset_call_stack: bool, args: Option<&Vec>) -> Result<(), StoryError> { - self.if_async_we_cant("call ChoosePathString right now"); + self.if_async_we_cant("call ChoosePathString right now")?; if reset_call_stack { self.reset_callstack(); diff --git a/lib/src/story_callbacks.rs b/lib/src/story_callbacks.rs index dac8ce7..0b1130c 100644 --- a/lib/src/story_callbacks.rs +++ b/lib/src/story_callbacks.rs @@ -15,6 +15,17 @@ pub(crate) struct ExternalFunctionDef { lookahead_safe: bool, } +pub trait ErrorHandler { + fn error(&mut self, message: &str, error_type: ErrorType); +} + +pub enum ErrorType { + // You should probably fix this, but it's not critical + Warning, + // Critical error that can't be recovered from + Error +} + impl Story { pub fn observe_variable(&mut self, variable_name: &str, observer: Rc>) -> Result<(), StoryError> { diff --git a/lib/tests/tag_test.rs b/lib/tests/tag_test.rs index 8339fe3..8066b87 100644 --- a/lib/tests/tag_test.rs +++ b/lib/tests/tag_test.rs @@ -15,7 +15,7 @@ fn tags_test() -> Result<(), StoryError> { assert_eq!("This is the content\n", story.cont()?); - let current_tags = story.get_current_tags(); + let current_tags = story.get_current_tags()?; assert_eq!(2, current_tags.len()); assert_eq!("author: Joe", current_tags[0]); assert_eq!("title: My Great Story", current_tags[1]); @@ -30,12 +30,12 @@ fn tags_test() -> Result<(), StoryError> { story.choose_path_string("knot", false, None)?; assert_eq!("Knot content\n", story.cont()?); - let current_tags = story.get_current_tags(); + let current_tags = story.get_current_tags()?; assert_eq!(1, current_tags.len()); assert_eq!("knot tag", current_tags[0]); assert_eq!("", story.cont()?); - let current_tags = story.get_current_tags(); + let current_tags = story.get_current_tags()?; assert_eq!("end of knot tag", current_tags[0]); Ok(()) @@ -48,12 +48,12 @@ fn tags_in_seq_test() -> Result<(), StoryError> { let mut story = Story::new(&json_string)?; assert_eq!("A red sequence.\n", story.cont()?); - let current_tags = story.get_current_tags(); + let current_tags = story.get_current_tags()?; assert_eq!(1, current_tags.len()); assert_eq!("red", current_tags[0]); assert_eq!("A white sequence.\n", story.cont()?); - let current_tags = story.get_current_tags(); + let current_tags = story.get_current_tags()?; assert_eq!(1, current_tags.len()); assert_eq!("white", current_tags[0]); @@ -67,17 +67,17 @@ fn tags_in_choice_test() -> Result<(), StoryError> { let mut story = Story::new(&json_string)?; story.cont()?; - let current_tags = story.get_current_tags(); + let current_tags = story.get_current_tags()?; assert_eq!(0, current_tags.len()); assert_eq!(1, story.get_current_choices().len()); assert_eq!(2, story.get_current_choices()[0].tags.len()); assert_eq!("one", story.get_current_choices()[0].tags[0]); assert_eq!("two", story.get_current_choices()[0].tags[1]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; assert_eq!("one three", story.cont()?); - let current_tags = story.get_current_tags(); + let current_tags = story.get_current_tags()?; assert_eq!(2, current_tags.len()); assert_eq!("one", current_tags[0]); assert_eq!("three", current_tags[1]); @@ -93,7 +93,7 @@ fn tags_dynamic_content_test() -> Result<(), StoryError> { let mut story = Story::new(&json_string)?; assert_eq!("tag\n", story.cont()?); - let current_tags = story.get_current_tags(); + let current_tags = story.get_current_tags()?; assert_eq!(1, current_tags.len()); assert_eq!("pic8red.jpg", current_tags[0]); From 60a795484198e8cd487500df002ab6b49bc00e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Fri, 6 Oct 2023 21:15:09 +0000 Subject: [PATCH 64/91] More cleanup --- lib/src/callstack.rs | 34 +++++++++++++++ lib/src/ink_list.rs | 12 ++++-- lib/src/ink_list_item.rs | 2 +- lib/src/json_read.rs | 14 +------ lib/src/json_write.rs | 6 +-- lib/src/list_definitions_origin.rs | 4 -- lib/src/native_function_call.rs | 10 ++--- lib/src/object.rs | 2 +- lib/src/path.rs | 2 +- lib/src/pointer.rs | 15 ++----- lib/src/state_patch.rs | 2 +- lib/src/story.rs | 31 +++++++------- lib/src/story_state.rs | 4 +- lib/src/value_type.rs | 3 +- lib/src/variable_reference.rs | 6 +-- lib/src/variables_state.rs | 10 +++-- lib/tests/choice_test.rs | 22 +++++----- lib/tests/conditional_test.rs | 66 +++++++++++++++--------------- lib/tests/divert_test.rs | 6 +-- lib/tests/gather_test.rs | 30 +++++++------- lib/tests/knot_test.rs | 10 ++--- lib/tests/list_test.rs | 4 +- lib/tests/misc_test.rs | 2 +- lib/tests/multi_flow_test.rs | 8 ++-- lib/tests/runtime_test.rs | 8 ++-- lib/tests/stitch_test.rs | 6 +-- lib/tests/thread_test.rs | 8 ++-- lib/tests/variable_test.rs | 4 +- lib/tests/variable_text_test.rs | 32 +++++++-------- 29 files changed, 186 insertions(+), 177 deletions(-) diff --git a/lib/src/callstack.rs b/lib/src/callstack.rs index b1c91bc..e7ede09 100644 --- a/lib/src/callstack.rs +++ b/lib/src/callstack.rs @@ -382,4 +382,38 @@ impl CallStack { Ok(()) } + + pub fn get_callstack_trace(&self) -> String { + let mut sb = String::new(); + + for (t, thread) in self.threads.iter().enumerate() { + let is_current = t == self.threads.len() - 1; + + sb.push_str(&format!( + "=== THREAD {}/{} {}===", + t + 1, + self.threads.len(), + if is_current { &"(current) "} else { &""} + )); + + for element in &thread.callstack { + if element.push_pop_type == PushPopType::Function { + sb.push_str( " [FUNCTION] "); + } else { + sb.push_str(" [TUNNEL] "); + } + + let pointer = &element.current_pointer; + + if !pointer.is_null() { + sb.push_str(&format!( + "\n", + pointer.container.as_ref().unwrap().get_path()) + ) + } + } + } + + sb + } } \ No newline at end of file diff --git a/lib/src/ink_list.rs b/lib/src/ink_list.rs index 11b8575..be0ef54 100644 --- a/lib/src/ink_list.rs +++ b/lib/src/ink_list.rs @@ -132,9 +132,9 @@ impl InkList { pub fn without(&self, other_list: &InkList) -> InkList { let mut result = InkList::from_other_list(self); - for (key, value) in &other_list.items { + other_list.items.iter().for_each(|(key, _)| { result.items.remove(key); - } + }); result } @@ -215,7 +215,7 @@ impl InkList { } } - return sub_list; + sub_list } pub fn inverse(&self) -> InkList { @@ -309,6 +309,12 @@ impl InkList { } } +impl Default for InkList { + fn default() -> Self { + Self::new() + } +} + impl PartialEq for InkList { fn eq(&self, other: &Self) -> bool { if other.items.len() != self.items.len() {return false;} diff --git a/lib/src/ink_list_item.rs b/lib/src/ink_list_item.rs index 7d7affb..6cc390b 100644 --- a/lib/src/ink_list_item.rs +++ b/lib/src/ink_list_item.rs @@ -42,7 +42,7 @@ impl InkListItem { } pub fn get_full_name(&self) -> String { - let origin = self.origin_name.as_ref().map(|s| s.as_str()).unwrap_or("?"); + let origin = self.origin_name.as_deref().unwrap_or("?"); format!("{}.{}", origin, self.item_name) } diff --git a/lib/src/json_read.rs b/lib/src/json_read.rs index 1e0e20e..2e8c12f 100644 --- a/lib/src/json_read.rs +++ b/lib/src/json_read.rs @@ -279,8 +279,7 @@ pub fn jarray_to_runtime_obj_list(jarray: &Vec, skip_last: bo let mut list: Vec> = Vec::with_capacity(jarray.len()); - for i in 0..count { - let jtok = &jarray[i]; + for jtok in jarray.iter().take(count) { let runtime_obj = jtoken_to_runtime_object(jtok, None); list.push(runtime_obj?); } @@ -327,17 +326,6 @@ pub(crate) fn jobject_to_hashmap_values(jobj: &Map) - Ok(dict) } -pub(crate) fn jobject_to_hashmap_rtobjects(jobj: &Map) -> Result>, StoryError> { - - let mut dict: HashMap> = HashMap::new(); - - for (k, v) in jobj.iter() { - dict.insert(k.clone(), jtoken_to_runtime_object(v, None)?); - } - - Ok(dict) -} - pub(crate) fn jobject_to_int_hashmap(jobj: &Map) -> Result, StoryError> { let mut dict: HashMap = HashMap::new(); diff --git a/lib/src/json_write.rs b/lib/src/json_write.rs index 0ab24d7..e17ba48 100644 --- a/lib/src/json_write.rs +++ b/lib/src/json_write.rs @@ -19,10 +19,10 @@ pub fn write_dictionary_values(objs: &HashMap>) -> Result) -> Result { if let Some(c) = o.as_any().downcast_ref::() { - return Ok(write_rt_container(c, false)?); + return write_rt_container(c, false); } - if let Some(divert) = o.clone().into_any().downcast::().ok() { + if let Ok(divert) = o.clone().into_any().downcast::() { let mut div_type_key = "->"; if divert.is_external { div_type_key = "x()"; } @@ -156,7 +156,7 @@ pub fn write_rtobject(o: Rc) -> Result Result { diff --git a/lib/src/list_definitions_origin.rs b/lib/src/list_definitions_origin.rs index bb9cd7a..c003671 100644 --- a/lib/src/list_definitions_origin.rs +++ b/lib/src/list_definitions_origin.rs @@ -40,10 +40,6 @@ impl ListDefinitionsOrigin { self.lists.get(name) } - fn get_lists(&self) -> Vec<&ListDefinition> { - self.lists.values().collect() - } - pub fn find_single_item_list_with_name(&self, name: &str) -> Option<&Rc> { self.all_unambiguous_list_value_cache.get(name) } diff --git a/lib/src/native_function_call.rs b/lib/src/native_function_call.rs index 533f1ff..f4c72ea 100644 --- a/lib/src/native_function_call.rs +++ b/lib/src/native_function_call.rs @@ -226,7 +226,7 @@ impl NativeFunctionCall { self.call_type(coerced_params) } - fn call_binary_list_operation(&self, params: &Vec>) -> Result, StoryError> { + fn call_binary_list_operation(&self, params: &[Rc]) -> Result, StoryError> { // List-Int addition/subtraction returns a List (e.g., "alpha" + 1 = "beta") if (self.op == Op::Add || self.op == Op::Subtract) && Value::get_list_value(params[0].as_ref()).is_some() && @@ -256,7 +256,7 @@ impl NativeFunctionCall { // Normal (list • list) operation if Value::get_list_value(params[0].as_ref()).is_some() && Value::get_list_value(params[1].as_ref()).is_some() { - let mut p = vec![v1.clone(), v2.clone()]; + let p = vec![v1.clone(), v2.clone()]; return self.call_type(p); } @@ -269,7 +269,7 @@ impl NativeFunctionCall { ))) } - fn call_list_increment_operation(&self, list_int_params: &Vec>) -> Rc { + fn call_list_increment_operation(&self, list_int_params: &[Rc]) -> Rc { let list_val = Value::get_list_value(list_int_params[0].as_ref()).unwrap(); let int_val = Value::get_int_value(list_int_params[1].as_ref()).unwrap(); @@ -364,7 +364,7 @@ impl NativeFunctionCall { }, } } else { - return Err(StoryError::InvalidStoryState(format!("RTObject of type Value expected: {}", obj.to_string()))); + return Err(StoryError::InvalidStoryState(format!("RTObject of type Value expected: {}", obj))); } } @@ -597,7 +597,6 @@ impl NativeFunctionCall { ValueType::Float(op2) => Ok(Rc::new(Value::new_float(f32::min(*op1, op2)))), _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, - ValueType::List(l) => todo!(), _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } @@ -612,7 +611,6 @@ impl NativeFunctionCall { ValueType::Float(op2) => Ok(Rc::new(Value::new_float(f32::max(*op1, op2)))), _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) }, - ValueType::List(l) => todo!(), _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) } } diff --git a/lib/src/object.rs b/lib/src/object.rs index 56f4d22..e1608e7 100644 --- a/lib/src/object.rs +++ b/lib/src/object.rs @@ -46,7 +46,7 @@ impl Object { let mut container = rtobject.get_object().get_parent(); let mut child = rtobject.clone(); - let mut child_rc = None; + let mut child_rc; while let Some(c) = container { let mut child_valid_name = false; diff --git a/lib/src/path.rs b/lib/src/path.rs index 3239794..2899b85 100644 --- a/lib/src/path.rs +++ b/lib/src/path.rs @@ -1,6 +1,6 @@ use std::{ fmt, - hash::{Hash, Hasher}, cell::{RefCell, OnceCell}, + hash::{Hash, Hasher}, cell::OnceCell, }; const PARENT_ID: &str = "^"; diff --git a/lib/src/pointer.rs b/lib/src/pointer.rs index d12b87d..da1663d 100644 --- a/lib/src/pointer.rs +++ b/lib/src/pointer.rs @@ -5,7 +5,7 @@ use crate::{container::Container, object::RTObject, path::{Path, Component}}; pub const NULL: Pointer = Pointer::new(None, -1); -#[derive(Clone)] +#[derive(Clone, Default)] pub struct Pointer { pub container: Option>, pub index: i32, @@ -54,24 +54,15 @@ impl Pointer { } pub fn start_of(container:Rc) -> Pointer { - return Pointer{container: Some(container), index:0}; + Pointer{container: Some(container), index:0} } } impl fmt::Display for Pointer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.container { - Some(container) => write!(f, "Ink Pointer -> {} -- index {}", container.get_path().to_string(), self.index), + Some(container) => write!(f, "Ink Pointer -> {} -- index {}", container.get_path(), self.index), None => write!(f, "Ink Pointer (null)"), } } } - -impl Default for Pointer { - fn default() -> Self { - Self { - container: Default::default(), - index: Default::default(), - } - } -} diff --git a/lib/src/state_patch.rs b/lib/src/state_patch.rs index 9362beb..fa41e4f 100644 --- a/lib/src/state_patch.rs +++ b/lib/src/state_patch.rs @@ -2,7 +2,7 @@ use std::{ rc::Rc, collections::{HashMap, HashSet}, }; -use crate::{object::{RTObject, Object}, container::Container, value::Value}; +use crate::{object::Object, container::Container, value::Value}; #[derive(Clone)] pub struct StatePatch { diff --git a/lib/src/story.rs b/lib/src/story.rs index a62b8cc..1d28ecd 100644 --- a/lib/src/story.rs +++ b/lib/src/story.rs @@ -132,7 +132,7 @@ impl Story { if self.main_content_container.named_content.contains_key("global decl") { let original_pointer = self.get_state().get_current_pointer().clone(); - self.choose_path(&Path::new_with_components_string(Some("global decl")), false); + self.choose_path(&Path::new_with_components_string(Some("global decl")), false)?; // Continue, but without validating external bindings, // since we may be doing this reset at initialisation time. @@ -642,10 +642,10 @@ impl Story { } - fn try_follow_default_invisible_choice(&mut self) { + fn try_follow_default_invisible_choice(&mut self) -> Result<(), StoryError> { let all_choices = match self.get_state().get_current_choices() { Some(c) => c, - None => return, + None => return Ok(()), }; // Is a default invisible choice the ONLY choice? @@ -659,7 +659,7 @@ impl Story { } if invisible_choices.is_empty() || all_choices.len() > invisible_choices.len() { - return; + return Ok(()); } let choice = &invisible_choices[0]; @@ -676,7 +676,7 @@ impl Story { self.get_state().get_callstack().as_ref().borrow_mut().set_current_thread(fork_thread); } - self.choose_path(&choice.target_path, false); + self.choose_path(&choice.target_path, false) } fn calculate_newline_output_state_change( @@ -1294,7 +1294,7 @@ impl Story { // var prioritiseHigherInCallStack = _temporaryEvaluationContainer // != null; let assigned_val = assigned_val.into_any().downcast::().unwrap(); - self.get_state_mut().variables_state.assign( var_ass, assigned_val); + self.get_state_mut().variables_state.assign( var_ass, assigned_val)?; return Ok(true); } @@ -1855,7 +1855,7 @@ impl Story { self.if_async_we_cant("call ChoosePathString right now")?; if reset_call_stack { - self.reset_callstack(); + self.reset_callstack()?; } else { // ChoosePathString is potentially dangerous since you can call it when the // stack is @@ -1864,30 +1864,29 @@ impl Story { let mut func_detail = "".to_owned(); let container = self.get_state().get_callstack().borrow().get_current_element().current_pointer.container.clone(); if let Some(container) = container { - func_detail = format!("({})", Object::get_path(container.as_ref()).to_string()); + func_detail = format!("({})", Object::get_path(container.as_ref())); } - // Err("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()); - return Err(StoryError::InvalidStoryState("Story was running a function".to_owned())); + return Err(StoryError::InvalidStoryState(format!("Story was running a function {func_detail} when you called ChoosePathString({}) - this is almost certainly not what you want! Full stack trace: \n{}", path, self.get_state().get_callstack().borrow().get_callstack_trace()))); } } self.get_state_mut().pass_arguments_to_evaluation_stack(args)?; - self.choose_path(&Path::new_with_components_string(Some(path)), true); + self.choose_path(&Path::new_with_components_string(Some(path)), true)?; Ok(()) } - fn reset_callstack(&mut self) { - self.if_async_we_cant("ResetCallstack"); + fn reset_callstack(&mut self) -> Result<(), StoryError> { + self.if_async_we_cant("ResetCallstack")?; self.get_state_mut().force_end(); + + Ok(()) } pub fn switch_flow(&mut self, flow_name: &str) -> Result<(), StoryError> { - self.if_async_we_cant("switch flow"); + self.if_async_we_cant("switch flow")?; if self.async_saving { return Err(StoryError::InvalidStoryState(format!("Story is already in background saving mode, can't switch flow to {}", flow_name))); diff --git a/lib/src/story_state.rs b/lib/src/story_state.rs index bde84f6..517c51b 100644 --- a/lib/src/story_state.rs +++ b/lib/src/story_state.rs @@ -519,7 +519,7 @@ impl StoryState { } } - let mut trim_index = -1; + let trim_index; if glue_trim_index != -1 && function_trim_index != -1 { trim_index = function_trim_index.min(glue_trim_index); } else if glue_trim_index != -1 { @@ -1004,7 +1004,7 @@ impl StoryState { } pub fn visit_count_at_path_string(&self, path_string: &str) -> Result { - let mut visit_count_out = None; + let mut visit_count_out; if self.patch.is_some() { let container = self.main_content_container.content_at_path(&Path::new_with_components_string(Some(path_string)), 0, -1).container(); diff --git a/lib/src/value_type.rs b/lib/src/value_type.rs index b8d6ed8..0e30ee6 100644 --- a/lib/src/value_type.rs +++ b/lib/src/value_type.rs @@ -1,6 +1,5 @@ -use std::fmt; -use crate::{object::{RTObject, Object}, path::Path, ink_list::InkList, story_error::StoryError}; +use crate::{path::Path, ink_list::InkList, story_error::StoryError}; #[repr(u8)] #[derive(Clone)] diff --git a/lib/src/variable_reference.rs b/lib/src/variable_reference.rs index 2fba21b..68ce966 100644 --- a/lib/src/variable_reference.rs +++ b/lib/src/variable_reference.rs @@ -27,11 +27,7 @@ impl VariableReference { } pub fn get_path_string_for_count(self: &Rc) -> Option { - if let Some(path_for_count) = &self.path_for_count { - Some(Object::compact_path_string(self.clone(), path_for_count)) - } else { - None - } + self.path_for_count.as_ref().map(|path_for_count| Object::compact_path_string(self.clone(), path_for_count)) } } diff --git a/lib/src/variables_state.rs b/lib/src/variables_state.rs index 491ed6d..a30658a 100644 --- a/lib/src/variables_state.rs +++ b/lib/src/variables_state.rs @@ -76,10 +76,10 @@ impl VariablesState { &mut self, var_ass: &VariableAssignment, value: Rc, - ) { + ) -> Result<(), StoryError>{ let mut name = var_ass.variable_name.to_string(); let mut context_index = -1; - let mut set_global = false; + let mut set_global; // Are we assigning to a global variable? if var_ass.is_new_declaration { @@ -119,8 +119,10 @@ impl VariablesState { if set_global { self.set_global(&name, value); } else { - self.callstack.borrow_mut().set_temporary_variable(name, value, var_ass.is_new_declaration, context_index); + self.callstack.borrow_mut().set_temporary_variable(name, value, var_ass.is_new_declaration, context_index)?; } + + Ok(()) } pub fn global_variable_exists_with_name(&self, name: &str) -> bool { @@ -147,7 +149,7 @@ impl VariablesState { // create // a chain of indirection by just returning the final target. if let Some(value_of_variable_pointed_to) = value_of_variable_pointed_to { - if let Some(double_redirection_pointer) = Value::get_variable_pointer_value(value_of_variable_pointed_to.as_ref()) { + if Value::get_variable_pointer_value(value_of_variable_pointed_to.as_ref()).is_some() { return value_of_variable_pointed_to; } } diff --git a/lib/tests/choice_test.rs b/lib/tests/choice_test.rs index 726f090..867be4d 100644 --- a/lib/tests/choice_test.rs +++ b/lib/tests/choice_test.rs @@ -66,7 +66,7 @@ fn single_choic2_test() -> Result<(), StoryError> { let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -86,7 +86,7 @@ fn suppress_choice_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!("Hello back!", story.get_current_choices().get(0).unwrap().text); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -107,7 +107,7 @@ fn mixed_choice_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!("Hello back!", story.get_current_choices().get(0).unwrap().text); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -129,7 +129,7 @@ fn varying_choice_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!(2, story.get_current_choices().len()); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -150,7 +150,7 @@ fn sticky_choice_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!(2, story.get_current_choices().len()); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -182,11 +182,11 @@ fn fallback_choice2_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!(2, story.get_current_choices().len()); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -218,7 +218,7 @@ fn label_flow_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!(2, story.get_current_choices().len()); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -238,7 +238,7 @@ fn label_flow2_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!(2, story.get_current_choices().len()); - story.choose_choice_index(1); + story.choose_choice_index(1)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -257,7 +257,7 @@ fn label_scope_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -277,7 +277,7 @@ fn divert_choice_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!(2, story.get_current_choices().len()); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; diff --git a/lib/tests/conditional_test.rs b/lib/tests/conditional_test.rs index 6402344..31ecece 100644 --- a/lib/tests/conditional_test.rs +++ b/lib/tests/conditional_test.rs @@ -79,7 +79,7 @@ fn ifelse_ext_text1_test() -> Result<(), StoryError> { assert_eq!(1, text.len()); assert_eq!("This is text 1.", text[0]); assert_eq!(1, story.get_current_choices().len()); - story.choose_choice_index(0); + story.choose_choice_index(0)?; common::next_all(&mut story, &mut text)?; assert_eq!(2, text.len()); @@ -101,7 +101,7 @@ fn ifelse_ext_text2_test() -> Result<(), StoryError> { assert_eq!(1, text.len()); assert_eq!("This is text 2.", text[0]); assert_eq!(1, story.get_current_choices().len()); - story.choose_choice_index(0); + story.choose_choice_index(0)?; common::next_all(&mut story, &mut text)?; assert_eq!(2, text.len()); @@ -123,7 +123,7 @@ fn ifelse_ext_text3_test() -> Result<(), StoryError> { assert_eq!(1, text.len()); assert_eq!("This is text 3.", text[0]); assert_eq!(1, story.get_current_choices().len()); - story.choose_choice_index(0); + story.choose_choice_index(0)?; common::next_all(&mut story, &mut text)?; assert_eq!(2, text.len()); @@ -141,7 +141,7 @@ fn cond_text1_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -160,7 +160,7 @@ fn cond_text2_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(1); + story.choose_choice_index(1)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -179,7 +179,7 @@ fn cond_opt1_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -197,7 +197,7 @@ fn cond_opt2_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(1); + story.choose_choice_index(1)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -217,25 +217,25 @@ fn stopping_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("I entered the casino.", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("I entered the casino again.", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("Once more, I went inside.", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("Once more, I went inside.", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; Ok(()) } @@ -251,25 +251,25 @@ fn cycle_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("I held my breath.", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("I waited impatiently.", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("I paused.", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("I held my breath.", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; Ok(()) } @@ -285,18 +285,18 @@ fn once_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("Would my luck hold?", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("Could I win the hand?", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(0, text.len()); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -315,17 +315,17 @@ fn shuffle_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -346,18 +346,18 @@ fn shuffle_stopping() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("final", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -379,17 +379,17 @@ fn shuffle_once() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(0, text.len()); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -411,14 +411,14 @@ fn multiline_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("At the table, I drew a card. Ace of Hearts.", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(2, text.len()); assert_eq!("I drew a card. 2 of Diamonds.", text[0]); assert_eq!("\"Should I hit you again,\" the croupier asks.", text[1]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -440,14 +440,14 @@ fn multiline_divert_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("At the table, I drew a card. Ace of Hearts.", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(2, text.len()); assert_eq!("I drew a card. 2 of Diamonds.", text[0]); assert_eq!("\"Should I hit you again,\" the croupier asks.", text[1]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -469,12 +469,12 @@ fn multiline_choice_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("At the table, I drew a card. Ace of Hearts.", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(2, story.get_current_choices().len()); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; diff --git a/lib/tests/divert_test.rs b/lib/tests/divert_test.rs index 784be1f..83f6134 100644 --- a/lib/tests/divert_test.rs +++ b/lib/tests/divert_test.rs @@ -39,7 +39,7 @@ fn divert_on_choice_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -58,7 +58,7 @@ fn complex_branching1_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -78,7 +78,7 @@ fn complex_branching2_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(1); + story.choose_choice_index(1)?; text.clear(); common::next_all(&mut story, &mut text)?; diff --git a/lib/tests/gather_test.rs b/lib/tests/gather_test.rs index b82490b..597e4d8 100644 --- a/lib/tests/gather_test.rs +++ b/lib/tests/gather_test.rs @@ -10,7 +10,7 @@ fn gather_basic_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(1); + story.choose_choice_index(1)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -32,7 +32,7 @@ fn gather_chain_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!(3, story.get_current_choices().len()); - story.choose_choice_index(1); + story.choose_choice_index(1)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -40,7 +40,7 @@ fn gather_chain_test() -> Result<(), StoryError> { assert_eq!( "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[0]); assert_eq!(2, story.get_current_choices().len()); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -59,11 +59,11 @@ fn nested_flow_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(2); + story.choose_choice_index(2)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -82,23 +82,23 @@ fn deep_nesting_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -118,7 +118,7 @@ fn complex_flow1_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(1); + story.choose_choice_index(1)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -136,22 +136,22 @@ fn complex_flow2_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(3, text.len()); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(2, text.len()); - story.choose_choice_index(1); + story.choose_choice_index(1)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(2, text.len()); - story.choose_choice_index(1); + story.choose_choice_index(1)?; text.clear(); common::next_all(&mut story, &mut text)?; diff --git a/lib/tests/knot_test.rs b/lib/tests/knot_test.rs index f84e6af..6ee94a9 100644 --- a/lib/tests/knot_test.rs +++ b/lib/tests/knot_test.rs @@ -59,7 +59,7 @@ fn param_strings_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(2); + story.choose_choice_index(2)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -78,7 +78,7 @@ fn param_ints_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(1); + story.choose_choice_index(1)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -97,7 +97,7 @@ fn param_floats_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(1); + story.choose_choice_index(1)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -116,7 +116,7 @@ fn param_vars_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(1); + story.choose_choice_index(1)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -135,7 +135,7 @@ fn param_multi_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; diff --git a/lib/tests/list_test.rs b/lib/tests/list_test.rs index 059ae7e..6e70dcb 100644 --- a/lib/tests/list_test.rs +++ b/lib/tests/list_test.rs @@ -99,10 +99,10 @@ fn list_bug_adding_element_test() -> Result<(), Box> { assert_eq!("", &story.continue_maximally()?); - story.choose_choice_index(0); + story.choose_choice_index(0)?; assert_eq!("a\n", &story.continue_maximally()?); - story.choose_choice_index(1); + story.choose_choice_index(1)?; assert_eq!("OK\n", &story.continue_maximally()?); Ok(()) diff --git a/lib/tests/misc_test.rs b/lib/tests/misc_test.rs index b2e325a..cfc40cb 100644 --- a/lib/tests/misc_test.rs +++ b/lib/tests/misc_test.rs @@ -31,7 +31,7 @@ fn turns_since_test() -> Result<(), StoryError> { let mut story = Story::new(&json_string)?; assert_eq!("0\n0\n", &story.continue_maximally()?); - story.choose_choice_index(0); + story.choose_choice_index(0)?; assert_eq!("1\n", &story.continue_maximally()?); Ok(()) diff --git a/lib/tests/multi_flow_test.rs b/lib/tests/multi_flow_test.rs index f6511f7..969c897 100644 --- a/lib/tests/multi_flow_test.rs +++ b/lib/tests/multi_flow_test.rs @@ -56,26 +56,26 @@ fn multiflow_save_load_threads() -> Result<(), StoryError> { let saved = story.save_state()?; // Test choice before reloading state before resetting - story.choose_choice_index(0); + story.choose_choice_index(0)?; assert_eq!("Thread 1 red choice\nAfter thread 1 choice (red)\n", story.continue_maximally()?); let mut story = Story::new(&json_string)?; // Load to pre-choice: still red, choose second choice story.load_state(&saved)?; - story.choose_choice_index(1); + story.choose_choice_index(1)?; assert_eq!("Thread 2 red choice\nAfter thread 2 choice (red)\n", story.continue_maximally()?); // Load: switch to blue, choose 1 story.load_state(&saved)?; story.switch_flow("Blue Flow")?; - story.choose_choice_index(0); + story.choose_choice_index(0)?; assert_eq!("Thread 1 blue choice\nAfter thread 1 choice (blue)\n", story.continue_maximally()?); // Load: switch to blue, choose 2 story.load_state(&saved)?; story.switch_flow("Blue Flow")?; - story.choose_choice_index(1); + story.choose_choice_index(1)?; assert_eq!("Thread 2 blue choice\nAfter thread 2 choice (blue)\n", story.continue_maximally()?); // Remove active blue flow, should revert back to global flow diff --git a/lib/tests/runtime_test.rs b/lib/tests/runtime_test.rs index 47ac7d0..cbcf725 100644 --- a/lib/tests/runtime_test.rs +++ b/lib/tests/runtime_test.rs @@ -149,7 +149,7 @@ fn variable_observers_test() -> Result<(), Box> { story.observe_variable("x", Rc::new(RefCell::new(VObserver { expected_value: 5})))?; common::next_all(&mut story, &mut text)?; - story.choose_choice_index(0); + story.choose_choice_index(0)?; common::next_all(&mut story, &mut text)?; Ok(()) @@ -170,7 +170,7 @@ fn set_and_get_variable_test() -> Result<(), Box> { assert_eq!(15, story.get_variable("x").unwrap().get_int().unwrap()); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -200,7 +200,7 @@ fn set_non_existant_variable_test() -> Result<(), Box> { assert_eq!(15, story.get_variable("x").unwrap().get_int().unwrap()); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -303,7 +303,7 @@ fn load_save_test() -> Result<(), Box> { Story::new(&json_string).unwrap(); story.load_state(&save_string)?; - story.choose_choice_index(0); + story.choose_choice_index(0)?; common::next_all(&mut story, &mut text)?; assert_eq!("\"There is not a moment to lose!\" I declared.", text.get(1).unwrap()); diff --git a/lib/tests/stitch_test.rs b/lib/tests/stitch_test.rs index df70109..6128d41 100644 --- a/lib/tests/stitch_test.rs +++ b/lib/tests/stitch_test.rs @@ -25,7 +25,7 @@ fn auto_stitch2_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(1); + story.choose_choice_index(1)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -47,7 +47,7 @@ fn manual_stitch_test() -> Result<(), StoryError> { assert_eq!(1, text.len()); assert_eq!("How shall we travel?", text[0]); - story.choose_choice_index(1); + story.choose_choice_index(1)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -69,7 +69,7 @@ fn manual_stitch2_test() -> Result<(), StoryError> { assert_eq!(1, text.len()); assert_eq!("How shall we travel?", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; diff --git a/lib/tests/thread_test.rs b/lib/tests/thread_test.rs index ff185ba..d69e1c7 100644 --- a/lib/tests/thread_test.rs +++ b/lib/tests/thread_test.rs @@ -13,13 +13,13 @@ fn thread_test() -> Result<(), StoryError> { assert_eq!(2, story.get_current_choices().len()); assert_eq!("No", story.get_current_choices()[0].text); assert_eq!("Yes", story.get_current_choices()[1].text); - story.choose_choice_index(0); + story.choose_choice_index(0)?; assert_eq!("No\nTry again!\n", story.continue_maximally()?); assert_eq!(2, story.get_current_choices().len()); assert_eq!("No", story.get_current_choices()[0].text); assert_eq!("Yes", story.get_current_choices()[1].text); - story.choose_choice_index(1); + story.choose_choice_index(1)?; assert_eq!("Yes\nYou win!\n", story.continue_maximally()?); @@ -44,13 +44,13 @@ fn thread_test_bug() -> Result<(), StoryError> { let mut story = Story::new(&json_string)?; story.load_state(&save_string)?; - story.choose_choice_index(0); + story.choose_choice_index(0)?; assert_eq!("No\nTry again!\n", story.continue_maximally()?); assert_eq!(2, story.get_current_choices().len()); assert_eq!("No", story.get_current_choices()[0].text); assert_eq!("Yes", story.get_current_choices()[1].text); - story.choose_choice_index(1); + story.choose_choice_index(1)?; assert_eq!("Yes\nYou win!\n", story.continue_maximally()?); diff --git a/lib/tests/variable_test.rs b/lib/tests/variable_test.rs index a330f27..1f65dea 100644 --- a/lib/tests/variable_test.rs +++ b/lib/tests/variable_test.rs @@ -38,7 +38,7 @@ fn var_string_ink_bug_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -56,7 +56,7 @@ fn var_divert_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - story.choose_choice_index(1); + story.choose_choice_index(1)?; text.clear(); common::next_all(&mut story, &mut text)?; diff --git a/lib/tests/variable_text_test.rs b/lib/tests/variable_text_test.rs index 4c00ab7..53c6a3f 100644 --- a/lib/tests/variable_text_test.rs +++ b/lib/tests/variable_text_test.rs @@ -14,28 +14,28 @@ fn sequence_test() -> Result<(), StoryError> { assert_eq!(1, text.len()); assert_eq!("The radio hissed into life. \"Three!\"", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("The radio hissed into life. \"Two!\"", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("The radio hissed into life. \"One!\"", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("The radio hissed into life. There was the white noise racket of an explosion.", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -57,28 +57,28 @@ fn cycle_test() -> Result<(), StoryError> { assert_eq!(1, text.len()); assert_eq!("The radio hissed into life. \"Three!\"", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("The radio hissed into life. \"Two!\"", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("The radio hissed into life. \"One!\"", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("The radio hissed into life. \"Three!\"", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -100,28 +100,28 @@ fn once_test() -> Result<(), StoryError> { assert_eq!(1, text.len()); assert_eq!("The radio hissed into life. \"Three!\"", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("The radio hissed into life. \"Two!\"", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("The radio hissed into life. \"One!\"", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("The radio hissed into life.", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -143,14 +143,14 @@ fn empty_elements_test() -> Result<(), StoryError> { assert_eq!(1, text.len()); assert_eq!("The radio hissed into life.", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("The radio hissed into life.", text[0]); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; @@ -171,14 +171,14 @@ fn list_in_choice_test() -> Result<(), StoryError> { assert_eq!(1, text.len()); assert_eq!("\"Hello, Master!\"", story.get_current_choices()[0].text); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("\"Hello, Monsieur!\"", story.get_current_choices()[0].text); - story.choose_choice_index(0); + story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; From ec016a89f3bb17d3dfdeb730f6ac0b094ae1a921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Sat, 7 Oct 2023 23:19:37 +0000 Subject: [PATCH 65/91] More cleanup --- README.md | 2 -- cli-player/src/main.rs | 2 +- lib/src/story.rs | 22 +++++++++++++--------- lib/src/story_callbacks.rs | 13 +++++++++++++ 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 4b2544a..c6e40f9 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,9 @@ Currently under development. This is the implementation status: ## TODO -- [ ] Assert, revisar y lanzar error. - [ ] Doc. - [ ] Publicar crate. -- [ ] Use OnceCell to lazy init the cache fields of RTObjects - [ ] Split large files. ex. Get the error handling out of the Story class. The performLogic - [ ] Review all the .unwrap()s and change them by .ok_or("xxx"). We need to avoid panics! diff --git a/cli-player/src/main.rs b/cli-player/src/main.rs index 45c85ca..04a4144 100644 --- a/cli-player/src/main.rs +++ b/cli-player/src/main.rs @@ -24,7 +24,7 @@ fn main() -> Result<(), Box> { let json_string = get_json_string(&args.json_filename)?; // REMOVE BOM if exits - let json_string_without_bom = json_string.strip_prefix("\u{feff}").unwrap_or(&json_string); + let json_string_without_bom = json_string.strip_prefix('\u{feff}').unwrap_or(&json_string); let mut story = Story::new(json_string_without_bom)?; let mut end = false; diff --git a/lib/src/story.rs b/lib/src/story.rs index 1d28ecd..5f20fcb 100644 --- a/lib/src/story.rs +++ b/lib/src/story.rs @@ -391,7 +391,7 @@ impl Story { // Run out of content and we have a default invisible choice that we can follow? if !self.can_continue() && !self.get_state().get_callstack().borrow().element_is_evaluate_from_game() { - self.try_follow_default_invisible_choice(); + self.try_follow_default_invisible_choice()?; } // Don't save/rewind during string evaluation, which is e.g. used for choices @@ -821,7 +821,10 @@ impl Story { if let Some(eval_command) = content_obj.as_ref().as_any().downcast_ref::() { match eval_command.command_type { CommandType::EvalStart => { - assert!(!self.get_state().get_in_expression_evaluation(), "Already in expression evaluation?"); + if self.get_state().get_in_expression_evaluation() { + return Err(StoryError::InvalidStoryState("Already in expression evaluation?".to_owned())); + } + self.get_state().set_in_expression_evaluation(true); }, CommandType::EvalOutput => { @@ -847,7 +850,9 @@ impl Story { } }, CommandType::EvalEnd => { - assert!(self.get_state().get_in_expression_evaluation(), "Not in expression evaluation mode"); + if !self.get_state().get_in_expression_evaluation() { + return Err(StoryError::InvalidStoryState("Not in expression evaluation mode".to_owned())); + } self.get_state().set_in_expression_evaluation(false); }, CommandType::Duplicate => { @@ -874,8 +879,8 @@ impl Story { override_tunnel_return_target = Some(v.clone()); } - if override_tunnel_return_target.is_none() { - assert!(popped.as_ref().as_any().downcast_ref::().is_some(), "Expected void if ->-> doesn't override target"); + if override_tunnel_return_target.is_none() && !popped.as_ref().as_any().is::() { + return Err(StoryError::InvalidStoryState("Expected void if ->-> doesn't override target".to_owned())); } } @@ -1636,10 +1641,9 @@ impl Story { if !previous_pointer.is_null() { let mut prev_ancestor = None; - - let resolved = previous_pointer.resolve(); - if resolved.is_some() && resolved.as_ref().unwrap().as_any().is::() { - prev_ancestor = resolved.unwrap().into_any().downcast::().ok(); + + if let Some(container) = previous_pointer.resolve().and_then(|res| res.into_any().downcast::().ok()) { + prev_ancestor = Some(container); } else if previous_pointer.container.is_some() { prev_ancestor = previous_pointer.container.clone(); } diff --git a/lib/src/story_callbacks.rs b/lib/src/story_callbacks.rs index 0b1130c..d5aa9ed 100644 --- a/lib/src/story_callbacks.rs +++ b/lib/src/story_callbacks.rs @@ -110,6 +110,19 @@ impl Story { Ok(()) } + /// Remove a binding for a named EXTERNAL ink function. + pub fn unbind_external_function(&mut self, func_name: &str) -> Result<(), StoryError> { + self.if_async_we_cant("unbind an external a function")?; + + if !self.externals.contains_key(func_name) { + return Err(StoryError::BadArgument(format!("Function '{func_name}' has not been bound."))); + } + + self.externals.remove(func_name); + + Ok(()) + } + pub(crate) fn call_external_function( &mut self, func_name: &str, From 92f7c1161da08ab08c46399721ab61c23a6be88f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Sun, 8 Oct 2023 00:04:51 +0000 Subject: [PATCH 66/91] More cleanup --- README.md | 25 +++---------------------- lib/Cargo.toml | 2 -- lib/src/divert.rs | 7 ++++++- lib/src/json_read.rs | 10 ++++------ lib/src/lib.rs | 3 +++ lib/src/object.rs | 26 -------------------------- lib/src/story.rs | 2 +- lib/tests/common/mod.rs | 12 +++++++----- lib/tests/list_test.rs | 2 +- lib/tests/misc_test.rs | 2 +- lib/tests/multi_flow_test.rs | 2 +- lib/tests/runtime_test.rs | 2 +- 12 files changed, 28 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index c6e40f9..3a3efbd 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,7 @@ -# blade-ink-rs -Inkle Ink runtime implementation in Rust +# blade-ink (bink) +This is a Rust port of inkle's [ink](https://github.com/inkle/ink), a scripting language for writing interactive narrative. -Currently under development. This is the implementation status: - -- [x] Loading .json file -- [x] Show plain lines (no logic nor choices) -- [x] Choices -- [x] Knots and Stitches -- [x] Diverts -- [x] Variable Text -- [x] Conditional Text -- [x] Game Queries and Functions -- [x] Nested flows -- [x] Variables and Logic -- [x] Conditional blocks (if/else) -- [x] Temporary Variables -- [x] Functions -- [x] Tunnels -- [x] Threads -- [x] Tags -- [x] Lists -- [x] Load/Save state +`bink` is fully compatible with the original version. ## TODO diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 60920bc..5ad8c86 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -17,8 +17,6 @@ path = "src/lib.rs" [dependencies] serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.93" -log = "0.4.17" -strum_macros = "0.25.2" strum = { version = "0.25.0", features = ["derive"] } as-any = "0.3.0" rand = "0.8.5" diff --git a/lib/src/divert.rs b/lib/src/divert.rs index 014e4f7..066943b 100644 --- a/lib/src/divert.rs +++ b/lib/src/divert.rs @@ -168,7 +168,12 @@ impl fmt::Display for Divert { } // TODO result.push_str(&format!(" -> {} ({})", self.get_target_path_string().unwrap_or_default(), target_str)); - result.push_str(&format!(" -> {} ({})", self.target_path.borrow().as_ref().unwrap(), target_str)); + let target_path = match self.target_path.borrow().as_ref() { + Some(t) => t.to_string(), + None => "".to_owned(), + }; + + result.push_str(&format!(" -> {} ({})", target_path, target_str)); } write!(f, "{result}") diff --git a/lib/src/json_read.rs b/lib/src/json_read.rs index 2e8c12f..8f5bf83 100644 --- a/lib/src/json_read.rs +++ b/lib/src/json_read.rs @@ -4,12 +4,12 @@ use serde_json::Map; use crate::{ container::Container, - object::{self, RTObject}, control_command::ControlCommand, value::Value, glue::Glue, path::Path, choice_point::ChoicePoint, choice::Choice, push_pop::PushPopType, divert::Divert, variable_assigment::VariableAssignment, void::Void, variable_reference::VariableReference, native_function_call::NativeFunctionCall, tag::Tag, ink_list::InkList, ink_list_item::InkListItem, list_definitions_origin::ListDefinitionsOrigin, list_definition::ListDefinition, story_error::StoryError, + object::RTObject, control_command::ControlCommand, value::Value, glue::Glue, path::Path, choice_point::ChoicePoint, choice::Choice, push_pop::PushPopType, divert::Divert, variable_assigment::VariableAssignment, void::Void, variable_reference::VariableReference, native_function_call::NativeFunctionCall, tag::Tag, ink_list::InkList, ink_list_item::InkListItem, list_definitions_origin::ListDefinitionsOrigin, list_definition::ListDefinition, story_error::StoryError, }; pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) -> Result, StoryError> { match token { - serde_json::Value::Null => Ok(Rc::new(object::Null::new())), + serde_json::Value::Null => Err(StoryError::BadJson(format!("Failed to convert token to runtime RTObject: {}", token))), serde_json::Value::Bool(value) => Ok(Rc::new(Value::new_bool(value.to_owned()))), serde_json::Value::Number(_) => { if token.is_i64() { @@ -52,7 +52,7 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) if "void".eq(str) {return Ok(Rc::new(Void::new()));} - Err(StoryError::BadJson(format!("Failed to convert token to runtime RTObject: {}", &token.to_string()))) + Err(StoryError::BadJson(format!("Failed to convert token to runtime RTObject: {}", token))) }, serde_json::Value::Array(value) => Ok(jarray_to_container(value, name)?), serde_json::Value::Object(obj) => { @@ -230,7 +230,7 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) return jobject_to_choice(obj); } - Err(StoryError::BadJson(format!("Failed to convert token to runtime RTObject: {}", &token.to_string()))) + Err(StoryError::BadJson(format!("Failed to convert token to runtime RTObject: {}", token))) }, } @@ -262,8 +262,6 @@ fn jarray_to_container(jarray: &Vec, name: Option) -> } } } - - // TODO container.namedOnlyContent = namedOnlyContent; } let container = Container::new(name, flags, jarray_to_runtime_obj_list(jarray, true)?, named_only_content); diff --git a/lib/src/lib.rs b/lib/src/lib.rs index f4ba511..c6a0b4f 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,3 +1,6 @@ +//! This is a Rust port of inkle's [ink](https://github.com/inkle/ink), a scripting language for writing interactive narrative. +//! `bink` is fully compatible with the original version. + pub mod story; pub mod story_callbacks; pub mod value_type; diff --git a/lib/src/object.rs b/lib/src/object.rs index e1608e7..c5d15bb 100644 --- a/lib/src/object.rs +++ b/lib/src/object.rs @@ -1,4 +1,3 @@ -use core::fmt; use std::{fmt::Display, rc::{Weak, Rc}, cell::RefCell, any::Any}; use as_any::{AsAny, Downcast}; @@ -198,31 +197,6 @@ pub trait RTObject: Display + IntoAny { fn get_object(&self) -> &Object; } -// TODO Temporal RTObject. Maybe we sould return Optional::None in null json. -pub struct Null { - obj: Object, -} - -impl Null { - pub fn new() -> Null { - Null { - obj: Object::new(), - } - } -} - -impl RTObject for Null { - fn get_object(&self) -> &Object { - &self.obj - } -} - -impl fmt::Display for Null { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Null") - } -} - #[cfg(test)] mod tests { use std::collections::HashMap; diff --git a/lib/src/story.rs b/lib/src/story.rs index 5f20fcb..8f68d15 100644 --- a/lib/src/story.rs +++ b/lib/src/story.rs @@ -62,7 +62,7 @@ impl Story { } else if version < INK_VERSION_MINIMUM_COMPATIBLE { return Err(StoryError::BadJson("Version of ink used to build story is too old to be loaded by this version of the engine".to_owned())); } else if version != INK_VERSION_CURRENT { - log::debug!("WARNING: Version of ink used to build story doesn't match current version of engine. Non-critical, but recommend synchronising."); + // TODO println!("WARNING: Version of ink used to build story doesn't match current version of engine. Non-critical, but recommend synchronising."); } let root_token = match json.get("root") { diff --git a/lib/tests/common/mod.rs b/lib/tests/common/mod.rs index 3c32a2d..c7ede4b 100644 --- a/lib/tests/common/mod.rs +++ b/lib/tests/common/mod.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use std::{error::Error, path::Path, fs}; use bink::{story::Story, story_error::StoryError}; @@ -14,7 +16,7 @@ pub fn next_all(story: &mut Story, text: &mut Vec) -> Result<(), StoryEr } if story.has_error() { - panic!("{}", join_text(&story.get_current_errors())); + panic!("{}", join_text(story.get_current_errors())); } Ok(()) @@ -77,15 +79,15 @@ pub fn run_story( if let Some(choice_list) = &choice_list { if choice_list_index < choice_list.len() { - story.choose_choice_index(choice_list[choice_list_index]); + story.choose_choice_index(choice_list[choice_list_index])?; choice_list_index += 1; } else { let random_choice_index = rng.gen_range(0..len); - story.choose_choice_index(random_choice_index); + story.choose_choice_index(random_choice_index)?; } } else { let random_choice_index = rng.gen_range(0..len); - story.choose_choice_index(random_choice_index); + story.choose_choice_index(random_choice_index)?; } } } @@ -100,5 +102,5 @@ pub fn get_json_string(filename: &str) -> Result> { } pub fn is_ended(story: &Story) -> bool { - return !story.can_continue() && story.get_current_choices().is_empty(); + !story.can_continue() && story.get_current_choices().is_empty() } diff --git a/lib/tests/list_test.rs b/lib/tests/list_test.rs index 6e70dcb..701d34a 100644 --- a/lib/tests/list_test.rs +++ b/lib/tests/list_test.rs @@ -1,6 +1,6 @@ use std::error::Error; -use bink::{story::Story, story_error::StoryError}; +use bink::story::Story; mod common; diff --git a/lib/tests/misc_test.rs b/lib/tests/misc_test.rs index cfc40cb..f33c462 100644 --- a/lib/tests/misc_test.rs +++ b/lib/tests/misc_test.rs @@ -53,7 +53,7 @@ fn issue15_test() -> Result<(), StoryError> { let line = &story.cont()?; if line.starts_with("SET_X:") { - story.set_variable("x", &ValueType::Int(100)); + story.set_variable("x", &ValueType::Int(100))?; } else { assert_eq!("X is set\n", line); } diff --git a/lib/tests/multi_flow_test.rs b/lib/tests/multi_flow_test.rs index 969c897..cd5b812 100644 --- a/lib/tests/multi_flow_test.rs +++ b/lib/tests/multi_flow_test.rs @@ -79,7 +79,7 @@ fn multiflow_save_load_threads() -> Result<(), StoryError> { assert_eq!("Thread 2 blue choice\nAfter thread 2 choice (blue)\n", story.continue_maximally()?); // Remove active blue flow, should revert back to global flow - story.remove_flow("Blue Flow"); + story.remove_flow("Blue Flow")?; assert_eq!("Default line 2\n", story.cont()?); diff --git a/lib/tests/runtime_test.rs b/lib/tests/runtime_test.rs index cbcf725..2528366 100644 --- a/lib/tests/runtime_test.rs +++ b/lib/tests/runtime_test.rs @@ -22,7 +22,7 @@ impl ExternalFunction for ExtFunc1 { } impl ExternalFunction for ExtFunc2 { - fn call(&mut self, _: &str, args: Vec) -> Option { + fn call(&mut self, _: &str, _: Vec) -> Option { Some(ValueType::new_string("Hello world")) } } From f0752f90a3a0399a6ffbe427096b5d28ba124bf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Mon, 9 Oct 2023 17:23:03 +0000 Subject: [PATCH 67/91] Initial c bindings. --- README.md | 7 --- cli-player/Cargo.toml | 2 +- clib/Cargo.toml | 2 +- clib/Makefile | 120 ++++++++++++++++++++++++++++++++++++++++ clib/VERSION | 1 + clib/binkc.h.in | 38 +++++++++++++ clib/binkc.pc.in | 11 ++++ clib/src/cstory.rs | 86 ++++++++++++++++++++++++++++ clib/src/lib.rs | 19 +++++++ clib/tests/binkc_test.c | 50 +++++++++++++++++ lib/Cargo.toml | 2 +- 11 files changed, 328 insertions(+), 10 deletions(-) create mode 100644 clib/Makefile create mode 100644 clib/VERSION create mode 100644 clib/binkc.h.in create mode 100644 clib/binkc.pc.in create mode 100644 clib/src/cstory.rs create mode 100644 clib/tests/binkc_test.c diff --git a/README.md b/README.md index 3a3efbd..7dc1dde 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,4 @@ This is a Rust port of inkle's [ink](https://github.com/inkle/ink), a scripting `bink` is fully compatible with the original version. -## TODO - -- [ ] Doc. -- [ ] Publicar crate. - -- [ ] Split large files. ex. Get the error handling out of the Story class. The performLogic -- [ ] Review all the .unwrap()s and change them by .ok_or("xxx"). We need to avoid panics! diff --git a/cli-player/Cargo.toml b/cli-player/Cargo.toml index 7273a3e..0487a57 100644 --- a/cli-player/Cargo.toml +++ b/cli-player/Cargo.toml @@ -13,7 +13,7 @@ path = "src/main.rs" [dependencies] anyhow = "1.0.75" -bink = { "version" = "0.1.0", path = "../lib" } +bink = { "version" = "0.9.0", path = "../lib" } clap = { "version" = "4.4.6", features = ["derive"] } [dev-dependencies] diff --git a/clib/Cargo.toml b/clib/Cargo.toml index 7cb46c1..548e109 100644 --- a/clib/Cargo.toml +++ b/clib/Cargo.toml @@ -9,4 +9,4 @@ path = "src/lib.rs" crate-type = ["cdylib"] [dependencies] -bink = { "version" = "0.1.0", path = "../lib" } \ No newline at end of file +bink = { "version" = "0.9.0", path = "../lib" } \ No newline at end of file diff --git a/clib/Makefile b/clib/Makefile new file mode 100644 index 0000000..db6fb66 --- /dev/null +++ b/clib/Makefile @@ -0,0 +1,120 @@ +ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) +VERSION=$(shell cat $(ROOT_DIR)/VERSION) +VERSION_MAJOR=$(shell echo $(VERSION) | cut -f1 -d.) +VERSION_MINOR=$(shell echo $(VERSION) | cut -f2 -d.) +VERSION_MICRO=$(shell echo $(VERSION) | cut -f3 -d.) +CLIB_SO_DEV=libbinkc.so +CLIB_SO_MAN=$(CLIB_SO_DEV).$(VERSION_MAJOR) +CLIB_SO_FULL=$(CLIB_SO_DEV).$(VERSION) +CLI_EXEC=binkplayer +CLIB_HEADER=binkc.h +CLIB_SO_DEV_RELEASE=../target/release/$(CLIB_SO_DEV) +CLIB_SO_DEV_DEBUG=../target/debug/$(CLIB_SO_DEV) +CLIB_PKG_CONFIG=binkc.pc +CLI_EXEC_RELEASE=../target/release/$(CLI_EXEC) +PREFIX ?= /usr/local + +#outdir is used by COPR as well: https://docs.pagure.org/copr.copr/user_documentation.html +outdir ?= $(ROOT_DIR) + +CPU_BITS = $(shell getconf LONG_BIT) +ifeq ($(CPU_BITS), 32) + LIBDIR ?= $(PREFIX)/lib +else + LIBDIR ?= $(PREFIX)/lib$(CPU_BITS) +endif + +INCLUDE_DIR ?= $(PREFIX)/include +PKG_CONFIG_LIBDIR ?= $(LIBDIR)/pkgconfig +MAN_DIR ?= $(PREFIX)/share/man + +SKIP_VENDOR_CREATION ?=0 +RELEASE ?=0 + +.PHONY: debug +debug: + cd .. && cargo build --all + ln -sfv $(CLIB_SO_DEV) ../target/debug/$(CLIB_SO_FULL) + ln -sfv $(CLIB_SO_DEV) ../target/debug/$(CLIB_SO_MAN) + +$(CLI_EXEC_RELEASE) $(CLIB_SO_DEV_RELEASE): + cd .. && cargo build --all --release + +$(CLIB_SO_DEV_DEBUG): debug + +clib: $(CLIB_HEADER) $(CLIB_SO_DEV_RELEASE) $(CLIB_PKG_CONFIG) + +.PHONY: $(CLIB_HEADER) +$(CLIB_HEADER): $(CLIB_HEADER).in + cp $(CLIB_HEADER).in $(CLIB_HEADER) + sed -i -e 's/@_VERSION_MAJOR@/$(VERSION_MAJOR)/' \ + $(CLIB_HEADER) + sed -i -e 's/@_VERSION_MINOR@/$(VERSION_MINOR)/' \ + $(CLIB_HEADER) + sed -i -e 's/@_VERSION_MICRO@/$(VERSION_MICRO)/' \ + $(CLIB_HEADER) + +.PHONY: $(CLIB_PKG_CONFIG) +$(CLIB_PKG_CONFIG): $(CLIB_PKG_CONFIG).in + cp $(CLIB_PKG_CONFIG).in $(CLIB_PKG_CONFIG) + sed -i -e 's|@VERSION@|$(VERSION)|' $(CLIB_PKG_CONFIG) + sed -i -e 's|@PREFIX@|$(PREFIX)|' $(CLIB_PKG_CONFIG) + sed -i -e 's|@LIBDIR@|$(LIBDIR)|' $(CLIB_PKG_CONFIG) + sed -i -e 's|@INCLUDE_DIR@|$(INCLUDE_DIR)|' $(CLIB_PKG_CONFIG) + +.PHONY: clib_check +clib_check: $(CLIB_SO_DEV_DEBUG) $(CLIB_HEADER) + $(eval TMPDIR := $(shell mktemp -d)) + cp $(CLIB_SO_DEV_DEBUG) $(TMPDIR)/$(CLIB_SO_FULL) + ln -sfv $(CLIB_SO_FULL) $(TMPDIR)/$(CLIB_SO_MAN) + ln -sfv $(CLIB_SO_FULL) $(TMPDIR)/$(CLIB_SO_DEV) + cp $(CLIB_HEADER) $(TMPDIR)/$(shell basename $(CLIB_HEADER)) + cc -g -Wall -Wextra -L$(TMPDIR) -I$(TMPDIR) \ + -o $(TMPDIR)/binkc_test clib/tests/binkc_test.c -lbinkc + LD_LIBRARY_PATH=$(TMPDIR) \ + valgrind --trace-children=yes --leak-check=full \ + --error-exitcode=1 \ + $(TMPDIR)/binkc_test 1>/dev/null + rm -rf $(TMPDIR) + +rust_check: + cd .. && cargo test -- --show-output; + cd clib + +check: rust_check clib_check + +run_test: $(CLIB_SO_DEV_DEBUG) $(CLIB_HEADER) + mv $(CLIB_SO_DEV_DEBUG) ../target/debug/$(CLIB_SO_FULL) + ln -sfv $(CLIB_SO_FULL) ../target/debug/$(CLIB_SO_DEV) + cp $(CLIB_HEADER) ../target/debug/$(shell basename $(CLIB_HEADER)) + cc -g -Wall -Wextra -L../target/debug -I../target/debug \ + -o ../target/debug/binkc_test ./tests/binkc_test.c -lbinkc + LD_LIBRARY_PATH=../target/debug \ + ../target/debug/binkc_test + +clean: + - cd .. && cargo clean + - rm -f target/debug/$(CLIB_SO_MAN) + - rm -f target/debug/$(CLIB_SO_FULL) + - rm -f $(CLIB_HEADER) + +install: $(CLI_EXEC_RELEASE) clib + install -p -v -D -m755 $(CLI_EXEC_RELEASE) \ + $(DESTDIR)$(PREFIX)/bin/$(CLI_EXEC) + install -p -D -m755 $(CLIB_SO_DEV_RELEASE) \ + $(DESTDIR)$(LIBDIR)/$(CLIB_SO_FULL) + ln -sfv $(CLIB_SO_FULL) $(DESTDIR)$(LIBDIR)/$(CLIB_SO_MAN) + ln -sfv $(CLIB_SO_FULL) $(DESTDIR)$(LIBDIR)/$(CLIB_SO_DEV) + install -p -v -D -m644 $(CLIB_HEADER) \ + $(DESTDIR)$(INCLUDE_DIR)/$(shell basename $(CLIB_HEADER)) + install -p -v -D -m644 $(CLIB_PKG_CONFIG) \ + $(DESTDIR)$(PKG_CONFIG_LIBDIR)/$(shell basename $(CLIB_PKG_CONFIG)) + +uninstall: + - rm -fv $(DESTDIR)$(PREFIX)/bin/$(CLI_EXEC) + - rm -fv $(DESTDIR)$(LIBDIR)/$(CLIB_SO_DEV) + - rm -fv $(DESTDIR)$(LIBDIR)/$(CLIB_SO_MAN) + - rm -fv $(DESTDIR)$(LIBDIR)/$(CLIB_SO_FULL) + - rm -fv $(DESTDIR)$(INCLUDE_DIR)/$(shell basename $(CLIB_HEADER)) + - rm -fv $(DESTDIR)$(INCLUDE_DIR)/$(shell basename $(CLIB_PKG_CONFIG)) + diff --git a/clib/VERSION b/clib/VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/clib/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/clib/binkc.h.in b/clib/binkc.h.in new file mode 100644 index 0000000..acd2b92 --- /dev/null +++ b/clib/binkc.h.in @@ -0,0 +1,38 @@ +#ifndef _LIBBINKC_H_ +#define _LIBBINKC_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#define BINKC_VERSION_MAJOR @_VERSION_MAJOR@ +#define BINKC_VERSION_MINOR @_VERSION_MINOR@ +#define BINKC_VERSION_MICRO @_VERSION_MICRO@ + +#define BINKC_VERSION \ + ((BINKC_VERSION_MAJOR * 10000) + \ + (BINKC_VERSION_MINOR * 100) + \ + BINKC_VERSION_MICRO) + +#define BINKC_OK 0 +#define BINKC_FAIL 1 +#define BINKC_FAIL_NULL_POINTER 2 + +struct binkc_story; + +int binkc_story_new(struct binkc_story **story, char *json_string, char **err_msg); +void binkc_story_free(struct binkc_story *story); +int binkc_story_can_continue(struct binkc_story *story, bool *can_continue); +int binkc_story_cont(struct binkc_story *story, char **line, char **err_msg); + + +void binkc_cstring_free(char *cstring); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* End of _LIBBINKC_H_ */ diff --git a/clib/binkc.pc.in b/clib/binkc.pc.in new file mode 100644 index 0000000..f6e5c11 --- /dev/null +++ b/clib/binkc.pc.in @@ -0,0 +1,11 @@ +prefix=@PREFIX@ +exec_prefix=${prefix} +libdir=@LIBDIR@ +includedir=@INCLUDE_DIR@/ + +Name: binkc +Version: @VERSION@ +Description: BInk C binding library +Requires: +Libs: -L${libdir} -lbinkc +Cflags: -I${includedir} diff --git a/clib/src/cstory.rs b/clib/src/cstory.rs new file mode 100644 index 0000000..214a4b7 --- /dev/null +++ b/clib/src/cstory.rs @@ -0,0 +1,86 @@ +use std::{os::raw::c_char, ffi::{CString, CStr}}; + +use bink::story::Story; + +use crate::{BINKC_FAIL_NULL_POINTER, BINKC_OK, BINKC_FAIL}; + +#[allow(clippy::not_unsafe_ptr_arg_deref)] +#[no_mangle] +pub extern "C" fn binkc_story_new( story: *mut *mut Story, json_string: *const c_char, err_msg: *mut *mut c_char) -> u32 { + if story.is_null() + || err_msg.is_null() + { + return BINKC_FAIL_NULL_POINTER; + } + + unsafe { + *story = std::ptr::null_mut(); + *err_msg = std::ptr::null_mut(); + } + + let c_str: &CStr = unsafe { CStr::from_ptr(json_string) }; + let str_slice: &str = c_str.to_str().unwrap(); + + let result = Story::new(str_slice); + + match result { + Ok(s) => unsafe { + *story = Box::into_raw(Box::new(s)); + BINKC_OK + }, + Err(e) => unsafe { + *err_msg = CString::new(e.to_string()).unwrap().into_raw(); + BINKC_FAIL + }, + } +} + +#[allow(clippy::not_unsafe_ptr_arg_deref)] +#[no_mangle] +pub extern "C" fn binkc_story_free(story: *mut Story) { + if !story.is_null() { + unsafe { + drop(Box::from_raw(story)); + } + } +} + +#[allow(clippy::not_unsafe_ptr_arg_deref)] +#[no_mangle] +pub extern "C" fn binkc_story_can_continue(story: *mut Story, can_continue: *mut bool) -> u32 { + if story.is_null() { + return BINKC_FAIL_NULL_POINTER; + } + + let story: &mut Story = unsafe { &mut *story }; + + unsafe { + *can_continue = story.can_continue(); + } + + BINKC_OK +} + +#[allow(clippy::not_unsafe_ptr_arg_deref)] +#[no_mangle] +pub extern "C" fn binkc_story_cont(story: *mut Story, line: *mut *mut c_char, err_msg: *mut *mut c_char) -> u32 { + if story.is_null() { + return BINKC_FAIL_NULL_POINTER; + } + + let story: &mut Story = unsafe { &mut *story }; + + let result = story.cont(); + + match result { + Ok(l) => unsafe { + *line = CString::new(l).unwrap().into_raw(); + BINKC_OK + }, + Err(e) => unsafe { + *err_msg = CString::new(e.to_string()).unwrap().into_raw(); + BINKC_FAIL + }, + } +} + diff --git a/clib/src/lib.rs b/clib/src/lib.rs index e69de29..7d1e62d 100644 --- a/clib/src/lib.rs +++ b/clib/src/lib.rs @@ -0,0 +1,19 @@ +//! C API for bink. + +use std::{os::raw::c_char, ffi::CString}; + +pub mod cstory; + +const BINKC_OK: u32 = 0; +const BINKC_FAIL: u32 = 1; +const BINKC_FAIL_NULL_POINTER: u32 = 2; + +#[allow(clippy::not_unsafe_ptr_arg_deref)] +#[no_mangle] +pub extern "C" fn binkc_cstring_free(cstring: *mut c_char) { + unsafe { + if !cstring.is_null() { + drop(CString::from_raw(cstring)); + } + } +} diff --git a/clib/tests/binkc_test.c b/clib/tests/binkc_test.c new file mode 100644 index 0000000..43d210c --- /dev/null +++ b/clib/tests/binkc_test.c @@ -0,0 +1,50 @@ +#include +#include +#include +#include +#include +#include + + +void finish(int rc, struct binkc_story *story, char *err_msg) { + binkc_cstring_free(err_msg); + binkc_story_free(story); + exit(rc); +} + +void check_ret(int ret, struct binkc_story *story, char *err_msg) { + if (ret != BINKC_OK) { + printf("Error: %s\n", err_msg); + finish(EXIT_FAILURE, story, err_msg); + } +} + +int main(void) { + uint32_t ret = BINKC_OK; + struct binkc_story *story = NULL; + char *err_msg = NULL; + char *line = NULL; + char *json_string = "{\"inkVersion\":21,\"root\":[[\"^Line.\",\"\\n\",[\"done\",{\"#n\":\"g-0\"}],null],\"done\",null],\"listDefs\":{}}"; + + ret = binkc_story_new(&story, json_string, &err_msg); + + check_ret(ret, story, err_msg); + + bool can_continue; + ret = binkc_story_can_continue(story, &can_continue); + check_ret(ret, story, err_msg); + + while (can_continue) { + ret = binkc_story_cont(story, &line, &err_msg); + check_ret(ret, story, err_msg); + puts(line); + binkc_cstring_free(line); + + ret = binkc_story_can_continue(story, &can_continue); + check_ret(ret, story, err_msg); + } + + printf("Ok.\n"); + + finish(EXIT_SUCCESS, story, err_msg); +} \ No newline at end of file diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 5ad8c86..570955c 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bink" -version = "0.1.0" +version = "0.9.0" authors = ["Rafael Garcia "] description = """ This is a Rust port of inkle's ink, a scripting language for writing interactive narrative. From 6df788d67ec19efb2b6d58592337b1909f94f376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Mon, 9 Oct 2023 18:04:39 +0000 Subject: [PATCH 68/91] Get rid of some TODOs comments --- lib/src/path.rs | 1 - lib/src/story.rs | 18 ++++++++--------- lib/src/story_state.rs | 45 +++++++++++++++++------------------------- 3 files changed, 26 insertions(+), 38 deletions(-) diff --git a/lib/src/path.rs b/lib/src/path.rs index 2899b85..0854f41 100644 --- a/lib/src/path.rs +++ b/lib/src/path.rs @@ -129,7 +129,6 @@ impl Path { let mut components = Vec::new(); - // TODO check that this is correct for i in 0..self.components.len() - upward_moves { components.push(self.components.get(i).unwrap().clone()); } diff --git a/lib/src/story.rs b/lib/src/story.rs index 8f68d15..1aa7101 100644 --- a/lib/src/story.rs +++ b/lib/src/story.rs @@ -61,8 +61,6 @@ impl Story { return Err(StoryError::BadJson("Version of ink used to build story was newer than the current version of the engine".to_owned())); } else if version < INK_VERSION_MINIMUM_COMPATIBLE { return Err(StoryError::BadJson("Version of ink used to build story is too old to be loaded by this version of the engine".to_owned())); - } else if version != INK_VERSION_CURRENT { - // TODO println!("WARNING: Version of ink used to build story doesn't match current version of engine. Non-critical, but recommend synchronising."); } let root_token = match json.get("root") { @@ -115,6 +113,10 @@ impl Story { story.reset_globals()?; + if version != INK_VERSION_CURRENT { + story.add_error("WARNING: Version of ink used to build story doesn't match current version of engine. Non-critical, but recommend synchronising.", true); + } + Ok(story) } @@ -1616,11 +1618,8 @@ impl Story { ))); } else if result.approximate { // TODO - // warning(format!( - // "Failed to find content at path '{}', so it was approximated to: '{}'.", - // path, - // result.obj.unwrap().get_path() - // )); + // self.add_error(&format!("Failed to find content at path '{}', so it was approximated to: '{}'.", path + // , result.obj.unwrap().get_path()), true); } Ok(p) @@ -1701,8 +1700,7 @@ impl Story { } } - // TODO: The result and the args should be an object not a String - pub fn evaluate_function(&mut self, func_name: &str, args: Option<&Vec>, text_output: &mut String) -> Result, StoryError> { + pub fn evaluate_function(&mut self, func_name: &str, args: Option<&Vec>, text_output: &mut String) -> Result, StoryError> { self.if_async_we_cant("evaluate a function")?; if func_name.trim().is_empty() { @@ -1855,7 +1853,7 @@ impl Story { Ok(self.get_state_mut().get_current_tags()) } - pub fn choose_path_string(&mut self, path: &str, reset_call_stack: bool, args: Option<&Vec>) -> Result<(), StoryError> { + pub fn choose_path_string(&mut self, path: &str, reset_call_stack: bool, args: Option<&Vec>) -> Result<(), StoryError> { self.if_async_we_cant("call ChoosePathString right now")?; if reset_call_stack { diff --git a/lib/src/story_state.rs b/lib/src/story_state.rs index 517c51b..8be4dcf 100644 --- a/lib/src/story_state.rs +++ b/lib/src/story_state.rs @@ -868,7 +868,7 @@ impl StoryState { self.evaluation_stack.last() } - pub fn start_function_evaluation_from_game(&mut self, func_container: Rc, arguments: Option<&Vec>) -> Result<(), StoryError> { + pub fn start_function_evaluation_from_game(&mut self, func_container: Rc, arguments: Option<&Vec>) -> Result<(), StoryError> { self.get_callstack().borrow_mut().push(PushPopType::FunctionEvaluationFromGame, self.evaluation_stack.len(), 0); self.get_callstack().borrow_mut().get_current_element_mut().current_pointer = Pointer::start_of(func_container); @@ -877,38 +877,30 @@ impl StoryState { Ok(()) } - pub fn pass_arguments_to_evaluation_stack(&mut self, arguments: Option<&Vec>) -> Result<(), StoryError> { + pub fn pass_arguments_to_evaluation_stack(&mut self, arguments: Option<&Vec>) -> Result<(), StoryError> { // Pass arguments onto the evaluation stack if let Some(arguments) = arguments { for arg in arguments { - // TODO - - // 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())); - // } - - self.push_evaluation_stack(Rc::new(Value::new_string(arg))); + let value = match arg { + ValueType::Bool(v) => Value::new_bool(*v), + ValueType::Int(v) => Value::new_int(*v), + ValueType::Float(v) => Value::new_float(*v), + ValueType::List(v) => Value::new_list(v.clone()), + ValueType::String(v) => Value::new_string(&v.string), + _ => {return Err(StoryError::InvalidStoryState("ink arguments when calling EvaluateFunction / ChoosePathStringWithParameters must be \ + int, float, string, bool or InkList.".to_owned()));} + }; + + self.push_evaluation_stack(Rc::new(value)); } } Ok(()) } - pub fn complete_function_evaluation_from_game(&mut self) -> Result, StoryError> { + pub fn complete_function_evaluation_from_game(&mut self) -> Result, StoryError> { if self.get_callstack().borrow().get_current_element().push_pop_type != PushPopType::FunctionEvaluationFromGame { - // TODO - // return Err(format!("Expected external function evaluation to be complete. Stack trace: {}", getCallStack().getCallStackTrace()); - - return Err(StoryError::InvalidStoryState("Expected external function evaluation to be complete. Stack trace".to_owned())); + return Err(StoryError::InvalidStoryState(format!("Expected external function evaluation to be complete. Stack trace: {}", self.get_callstack().borrow().get_callstack_trace()))); } let original_evaluation_stack_height = self.get_callstack().borrow().get_current_element().evaluation_stack_height_when_pushed; @@ -938,13 +930,12 @@ impl StoryState { // DivertTargets get returned as the string of components // (rather than a Path, which isn't public) if let ValueType::DivertTarget(p) = &return_val.value { - return Ok(Some(p.to_string())); + return Ok(Some(ValueType::new_string(&p.to_string()))); } - // Other types can ust have their exact object type: + // Other types can just have their exact object type: // int, float, string. VariablePointers get returned as strings. - // TODO - return Ok(Some(return_val.to_string())); + return Ok(Some(return_val.value.clone())); } } From d6e3e3bf339e5ca8465bfeaf2d93fb3a1865f12b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Mon, 9 Oct 2023 20:34:50 +0000 Subject: [PATCH 69/91] Added missing method for error handling. --- cli-player/src/main.rs | 30 ++++++++++++++++++++- cli-player/tests/basic_tests.rs | 2 +- cli-player/tests/data/TheIntercept.ink.json | 2 +- lib/src/story.rs | 10 ++++--- lib/src/story_callbacks.rs | 5 ++++ lib/src/story_state.rs | 4 +++ lib/tests/function_test.rs | 3 ++- 7 files changed, 48 insertions(+), 8 deletions(-) diff --git a/cli-player/src/main.rs b/cli-player/src/main.rs index 04a4144..2ccc702 100644 --- a/cli-player/src/main.rs +++ b/cli-player/src/main.rs @@ -1,7 +1,10 @@ +use std::cell::RefCell; + use std::{path::Path, fs, error::Error, rc::Rc, io}; use std::io::Write; use anyhow::Context; +use bink::story_callbacks::{ErrorHandler, ErrorType}; use bink::{story::Story, choice::Choice}; use clap::Parser; @@ -19,6 +22,26 @@ enum Command { Save(String), } +struct EHandler { + pub should_terminate: bool, +} + +impl EHandler { + pub fn new() -> Rc> { + Rc::new(RefCell::new(EHandler {should_terminate: false})) + } +} + +impl ErrorHandler for EHandler { + fn error(&mut self, message: &str, error_type: ErrorType) { + println!("{}", message); + + if error_type == ErrorType::Error { + self.should_terminate = true; + } + } +} + fn main() -> Result<(), Box> { let args = Args::parse(); let json_string = get_json_string(&args.json_filename)?; @@ -27,11 +50,16 @@ fn main() -> Result<(), Box> { let json_string_without_bom = json_string.strip_prefix('\u{feff}').unwrap_or(&json_string); let mut story = Story::new(json_string_without_bom)?; + let err_handler = EHandler::new(); + story.set_error_handler(err_handler.clone()); + + let mut end = false; - while !end { + while !end && !err_handler.borrow().should_terminate { while story.can_continue() { let line = story.cont()?; + let trimmed = line.trim(); println!("{}", trimmed); diff --git a/cli-player/tests/basic_tests.rs b/cli-player/tests/basic_tests.rs index 4b199d9..52adec5 100644 --- a/cli-player/tests/basic_tests.rs +++ b/cli-player/tests/basic_tests.rs @@ -20,7 +20,7 @@ fn basic_story_test() -> Result<(), Box> { let output_str = String::from_utf8_lossy(&output.stdout); assert!(output.status.success()); - assert!(output_str.starts_with("Test conditional choices")); + assert!(output_str.contains("Test conditional choices")); assert!(output_str.contains("1. one")); assert!(output_str.ends_with("one\n")); diff --git a/cli-player/tests/data/TheIntercept.ink.json b/cli-player/tests/data/TheIntercept.ink.json index 507a739..9db2dcf 100644 --- a/cli-player/tests/data/TheIntercept.ink.json +++ b/cli-player/tests/data/TheIntercept.ink.json @@ -1 +1 @@ -{"inkVersion":20,"root":[["ev",{"VAR?":"DEBUG"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^IN DEBUG MODE!","\n","ev","str","^Beginning...","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Framing Hooper...","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^In with Hooper...","/str","/ev",{"*":".^.c-2","flg":20},{"->":"0.5"},{"c-0":["^\t",{"->":"start"},"\n",{"#f":5}],"c-1":["^ ",{"->":"claim_hooper_took_component"},"\n",{"#f":5}],"c-2":["^ ",{"->":"inside_hoopers_hut"},"\n",{"#f":5}]}]}],[{"->":".^.b"},{"b":["\n",{"->":"start"},{"->":"0.5"},null]}],"nop","\n",["done",{"#f":5,"#n":"g-0"}],null],"done",{"lower":[{"temp=":"x"},"ev",{"VAR?":"x"},1,"-","/ev",{"temp=":"x","re":true},{"#f":1}],"raise":[{"temp=":"x"},"ev",{"VAR?":"x"},1,"+","/ev",{"temp=":"x","re":true},{"#f":1}],"start":[[["^They are keeping me waiting.","\n",["ev",{"^->":"start.0.g-0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^Hut 14",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"start.0.g-0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"^. The door was locked after I sat down. ","\n","^I don't even have a pen to do any work. There's a copy of the morning's intercept in my pocket, but staring at the jumbled letters will only drive me mad.","\n","^I am not a machine, whatever they say about me.","\n",{"->":".^.^.^.opts"},{"#f":5}],"#f":5,"#n":"g-0"}],{"opts":[["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","^I rattle my fingers on the field table.",{"->":".^.^.23"},null],"s2":["pop",{"->":".^.^.23"},null],"#f":5}],"\n","ev","str","^Think","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Plan","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Wait","/str","/ev",{"*":".^.c-3","flg":20},{"c-1":["^ ","\n","^They suspect me to be a traitor. They think I stole the component from the calculating machine. They will be searching my bunk and cases.","\n","^When they don't find it, ","ev",{"CNT?":".^.^.c-2"},"/ev",[{"->":".^.b","c":true},{"b":["^then",{"->":".^.^.^.9"},null]}],"nop","^ they'll come back and demand I talk.","\n",{"->":".^.^"},{"->":".^.^.^.g-1"},{"#f":5}],"c-2":["\n","ev",{"CNT?":".^.^.c-1"},"!","/ev",[{"->":".^.b","c":true},{"b":["^What I am is",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^I am",{"->":".^.^.^.7"},null]}],"nop","^ a problem—solver. Good with figures, quick with crosswords, excellent at chess.","\n","^But in this scenario — in this trap — what is the winning play?","\n",["ev","str","^Co—operate","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Dissemble","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Divert","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^I must co—operate. My credibility is my main asset. To contradict myself, or another source, would be fatal.","\n","^I must simply hope they do not ask the questions I do not want to answer.","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n",{"->":"start.0.g-1"},{"#f":5}],"c-1":["^ ","\n","^Misinformation, then. Just as the war in Europe is one of plans and interceptions, not planes and bombs.","\n","^My best hope is a story they prefer to the truth.","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":"start.0.g-1"},{"#f":5}],"c-2":["^ ","\n","^Avoidance and delay. The military machine never fights on a single front. If I move slowly enough, things will resolve themselves some other way, my reputation intact.","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":"start.0.g-1"},{"#f":5}]}],{"#f":5}],"c-3":["^\t\t","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":[{"->":"start.waited"},{"#f":5}]}],{"waited":[[["^Half an hour goes by before Commander Harris returns. He closes the door behind him quickly, as though afraid a loose word might slip inside.","\n","^\"Well, then,\" he begins, awkwardly. This is an unseemly situation.","\n",["ev",{"^->":"start.waited.0.g-0.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"Commander.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"start.waited.0.g-0.5.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",{"CNT?":"start.0.opts.c-2.12.c-2"},"!","/ev",{"*":".^.^.c-1","flg":19},{"s":["^\"Tell me what this is about.\"",{"->":"$r","var":true},null]}],"ev","str","^Wait","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["ev",{"^->":"start.waited.0.g-0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.4.s"},[{"#n":"$r2"}],"\n","^He nods. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-1":["ev",{"^->":"start.waited.0.g-0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.5.s"},[{"#n":"$r2"}],"\n","^He shakes his head.","\n","^\"Now, don't let's pretend.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-2":["\n","^I say nothing.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5,"#n":"g-0"}],{"g-1":["^He has brought two cups of tea in metal mugs: he sets them down on the tabletop between us.","\n","ev","str","^Deny","/str",{"CNT?":".^.^.g-0.c-1"},"/ev",{"*":".^.c-3","flg":21},"ev","str","^Take one","/str","/ev",{"*":".^.c-4","flg":20},["ev",{"^->":"start.waited.0.g-1.15.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",{"CNT?":".^.^.^.g-0.c-1"},"!","/ev",{"*":".^.^.c-5","flg":19},{"s":["^\"What's going on?\"",{"->":"$r","var":true},null]}],"ev","str","^Wait","/str","/ev",{"*":".^.c-6","flg":20},{"c-3":["^ \"I'm not pretending anything.\"","\n","ev",{"CNT?":"start.0.opts.c-2.12.c-0"},"/ev",[{"->":".^.b","c":true},{"b":["^I'm lying already, despite my good intentions.",{"->":".^.^.^.6"},null]}],"nop","\n","^Harris looks disapproving. ",{"->":".^.^.c-6.3.pushes_cup"},"\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-4":["\n","ev",true,"/ev",{"VAR=":"teacup","re":true},"^I take a mug and warm my hands. It's ","<>","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-5":["ev",{"^->":"start.waited.0.g-1.c-5.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.15.s"},[{"#n":"$r2"}],"\n","^\"You know already.\"","\n",{"->":".^.^.c-6.3.pushes_cup"},{"->":".^.^.^.g-2"},{"#f":5}],"c-6":["\n","^I wait for him to speak.","\n",[["^He pushes one mug halfway towards me: ","<>","\n",{"->":".^.^.^.^.^.g-2"},{"#f":5,"#n":"pushes_cup"}],null],{"#f":5}],"#f":5}],"g-2":["^a small gesture of friendship.","\n","^Enough to give me hope?","\n","ev","str","^Take it","/str",{"VAR?":"teacup"},"!","/ev",{"*":".^.c-7","flg":21},"ev","str","^Don't take it","/str",{"VAR?":"teacup"},"!","/ev",{"*":".^.c-8","flg":21},"ev","str","^Drink","/str",{"VAR?":"teacup"},"/ev",{"*":".^.c-9","flg":21},"ev","str","^Wait","/str",{"VAR?":"teacup"},"/ev",{"*":".^.c-10","flg":21},{"c-7":["^ ","\n","^I ","ev",{"CNT?":".^.^.^.g-1.c-4"},"/ev",[{"->":".^.b","c":true},{"b":["^lift the mug",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["^take the mug,",{"->":".^.^.^.8"},null]}],"nop","^ and blow away the steam. It is too hot to drink.","\n","^Harris picks his own up and just holds it.","\n","ev",true,"/ev",{"VAR=":"teacup","re":true},"ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-8":["^ ","\n","^Just a cup of insipid canteen tea. I leave it where it is.","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-9":["^ ","\n","^I raise the cup to my mouth but it's too hot to drink.","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-10":["^ \t\t","\n","^I say nothing as ",{"->":".^.^.c-7"},"\n",{"->":".^.^.^.g-3"},{"#f":5}],"#f":5}],"g-3":["^\"Quite a difficult situation,\" ","ev",{"CNT?":".^.^.g-2.c-7"},"/ev",[{"->":".^.b","c":true},{"b":["^he",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["^Harris",{"->":".^.^.^.6"},null]}],"nop","^ begins","ev",{"VAR?":"forceful"},0,"<=","/ev",[{"->":".^.b","c":true},{"b":["^, sternly",{"->":".^.^.^.14"},null]}],"nop","^. I've seen him adopt this stiff tone of voice before, but only when talking to the brass. \"I'm sure you agree.\"","\n","ev","str","^Agree","/str","/ev",{"*":".^.c-11","flg":20},"ev","str","^Disagree","/str","/ev",{"*":".^.c-12","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-13","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-14","flg":20},{"c-11":["^ ","\n","^\"Awkward,\" I reply","\n",{"->":".^.^.^.g-4"},{"#f":5}],"c-12":["^ ","\n","^\"I don't see why,\" I reply","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.^.g-4"},{"#f":5}],"c-13":["^ ",{"->":".^.^.c-12"},"\n",{"->":".^.^.^.g-4"},{"#f":5}],"c-14":["^ ","\n","^\"I'm sure you've handled worse,\" I reply casually","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.^.g-4"},{"#f":5}],"#f":5}],"g-4":["ev",{"VAR?":"teacup"},"/ev",[{"->":".^.b","c":true},{"b":["\n","ev",true,"/ev",{"VAR=":"drugged","re":true},"<>","^, sipping at my tea as though we were old friends","\n",{"->":".^.^.^.4"},null]}],"nop","\n","<>","^.","\n",["ev","str","^Watch him","/str","/ev",{"*":".^.c-15","flg":20},"ev","str","^Wait","/str","/ev",{"*":".^.c-16","flg":20},"ev","str","^Smile","/str",{"CNT?":".^.^.^.g-3.c-12"},"!","/ev",{"*":".^.c-17","flg":21},{"c-15":["\n","^His face is telling me nothing. I've seen Harris broad and full of laughter. Today he is tight, as much part of the military machine as the device in Hut 5.","\n",{"->":".^.^.^.^.g-6"},{"#f":5}],"c-16":["\n","^I wait to see how he'll respond.","\n",{"->":".^.^.^.^.g-6"},{"#f":5}],"c-17":["\n","^I try a weak smile. It is not returned.","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n",{"->":".^.^.^.^.g-6"},{"#f":5}],"#f":5,"#n":"g-5"}],{"#f":5}],"g-6":["^\"We need that component,\" he says.","\n",["ev",{"CNT?":"missing_reel"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->t->":"missing_reel"},{"->":"harris_demands_component"},{"->":".^.^.^.5"},null]}],"nop","\n",["ev","str","^Yes","/str","/ev",{"*":".^.c-18","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-19","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-20","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-21","flg":20},{"c-18":["\n","^\"Of course I do,\" I answer.","\n",{"->":".^.^.^.^.^.g-9"},{"#f":5}],"c-19":["\n","^\"No I don't. And I've got work to do...\"","\n","^\"Work that will be rather difficult for you to do, don't you think?\" Harris interrupts.","\n",{"->":".^.^.^.^.^.g-9"},{"#f":5}],"c-20":["\n",{"->":"here_at_bletchley_diversion"},{"->":".^.^.^.^.^.g-9"},{"#f":5}],"c-21":["^ ","\n",{"->":".^.^.c-19"},{"->":".^.^.^.^.^.g-9"},{"#f":5}],"#f":5,"#n":"g-8"}],{"#f":5,"#n":"g-7"}],{"#f":5}],"g-9":[{"->t->":"missing_reel"},{"->":"harris_demands_component"},{"#f":5}]}],{"#f":1}],"#f":1}],"missing_reel":[["ev","str","^The stolen component...","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Shrug","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["\n","^I shrug.","\n","ev","void","/ev","->->",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^The reel went missing from the Bombe this afternoon. The four of us were in the Hut, working on the latest German intercept. The results were garbage. It was Russell who found the gap in the plugboard.","\n",["^Any of us could have taken it; and no one else would have known its worth.","\n","ev","str","^Panic","/str",{"VAR?":"forceful"},0,"<=","/ev",{"*":".^.c-2","flg":21},"ev","str","^Calculate","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Deny","/str",{"VAR?":"evasive"},0,">=","/ev",{"*":".^.c-4","flg":21},{"c-2":["^ They will pin it on me. They need a scapegoat so that the work can continue. I'm a likely target. Weaker than the rest. ","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n",{"->":".^.^.^.^.g-2"},{"#f":5}],"c-3":["^ My odds, then, are one in four. Not bad; although the stakes themselves are higher than I would like.","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.^.^.g-2"},{"#f":5}],"c-4":["^ But this is still a mere formality. The work will not stop. A replacement component will be made and we will all be put back to work. We are too valuable to shoot. ","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.^.^.g-2"},{"#f":5}],"#f":5,"#n":"g-1"}],{"#f":5}],"g-2":["ev","void","/ev","->->",{"#f":5}]}],{"#f":1}],"here_at_bletchley_diversion":[["^\"Here at Bletchley? Of course.\"","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n","^\"Here, now,\" Harris corrects. \"We are not talking to everyone. I can imagine you might feel pretty sore about that. I can imagine you feeling picked on. ","ev",{"VAR?":"forceful"},0,"<","/ev",[{"->":".^.b","c":true},{"b":["^You're a sensitive soul.",{"->":".^.^.^.21"},null]}],"nop","^\"","\n",["ev",{"^->":"here_at_bletchley_diversion.0.24.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str","/ev",{"*":".^.^.c-0","flg":22},{"s":["^\"I'm fine",{"->":"$r","var":true},null]}],["ev",{"^->":"here_at_bletchley_diversion.0.25.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",{"VAR?":"forceful"},0,"<","/ev",{"*":".^.^.c-1","flg":19},{"s":["^\"What do you mean by that?\"",{"->":"$r","var":true},null]}],["ev",{"^->":"here_at_bletchley_diversion.0.26.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str",{"VAR?":"forceful"},0,">=","/ev",{"*":".^.^.c-2","flg":23},{"s":["^\"Damn right",{"->":"$r","var":true},null]}],"ev","str","^Be honest","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-4","flg":20},{"c-0":["ev",{"^->":"here_at_bletchley_diversion.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.24.s"},[{"#n":"$r2"}],"^,\" I reply. \"This is all some misunderstanding and the quicker we have it cleared up the better.\"","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n","^\"I couldn't agree more.\" And then he comes right out with it, with an accusation.","\n",{"->":".^.^.done"},{"#f":5}],"c-1":["ev",{"^->":"here_at_bletchley_diversion.0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.25.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.done"},{"#f":5}],"c-2":["ev",{"^->":"here_at_bletchley_diversion.0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.26.s"},[{"#n":"$r2"}],"^ I'm sore. Was it one of the others who put you up to this? Was it Hooper? He's always been jealous of me. He's...\"","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n","ev",true,"/ev",{"VAR=":"hooper_mentioned","re":true},"^The Commander moustache bristles as he purses his lips. \"Has he now? Of your achievements, do you think?\"","\n","^It's difficult not to shake the sense that he's ","ev",{"VAR?":"evasive"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^mocking",{"->":".^.^.^.28"},null]}],[{"->":".^.b"},{"b":["^simply humouring",{"->":".^.^.^.28"},null]}],"nop","^ me.","\n","^\"Or of your brain? Or something else?\"","\n",[["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-0","flg":22},{"s":["^\"Of my genius.",{"->":"$r","var":true},null]}],["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-1","flg":22},{"s":["^\"Of my standing.",{"->":"$r","var":true},null]}],"ev","str","^Evade","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"^ Hooper simply can't stand that I'm cleverer than he is. We work so closely together, cooped up in that Hut all day. It drives him to distraction. To worse.\"","\n","^\"You're suggesting Hooper would sabotage this country's future simply to spite you?\" Harris chooses his words like the military man he is, each lining up to create a ring around me.","\n",["ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ \t\t\t","\n","^\"","ev",{"VAR?":"forceful"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^He's petty enough, certainly",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^I wouldn't put it past him",{"->":".^.^.^.10"},null]}],"nop","^. He's a creep.\" ","ev",{"VAR?":"teacup"},"/ev",[{"->":".^.b","c":true},{"b":["^ I set the teacup down.",{"->":".^.^.^.17"},null]}],[{"->":".^.b"},{"b":["^I wipe a hand across my forehead.",{"->":".^.^.^.17"},null]}],"nop","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n","ev",false,"/ev",{"VAR=":"teacup","re":true},{"->":".^.^.suggest_its_a_lie"},{"#f":5}],"c-1":["^ \t\t\t","\n","^\"No, ","ev",{"VAR?":"forceful"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^of course not",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^I suppose not",{"->":".^.^.^.10"},null]}],"nop","^.\" ","ev",{"VAR?":"teacup"},"/ev",[{"->":".^.b","c":true},{"b":["^I put the teacup back down on the table",{"->":".^.^.^.17"},null]}],[{"->":".^.b"},{"b":["^I push the teacup around on its base",{"->":".^.^.^.17"},null]}],"nop","^.","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n","ev",false,"/ev",{"VAR=":"teacup","re":true},{"->":".^.^.suggest_its_a_lie"},{"#f":5}],"c-2":["^ \t\t","\n","^\"I don't know what I'm suggesting. I don't understand what's going on.\"","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n","^\"But of course you do.\" Harris narrows his eyes.","\n",{"->":".^.^.^.^.^.^.done"},{"->":".^.^.suggest_its_a_lie"},{"#f":5}],"suggest_its_a_lie":["^\"All I can say is, ever since I arrived here, he's been looking to ways to bring me down a peg. I wouldn't be surprised if he set this whole affair up just to have me court—martialled.\"","\n","^\"We don't court—martial civilians,\" Harris replies. \"Traitors are simply hung at her Majesty's pleasure.\"","\n",["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.c-0.10.suggest_its_a_lie.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str","/ev",{"*":".^.^.c-3","flg":22},{"s":["^\"Quite right",{"->":"$r","var":true},null]}],["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.c-0.10.suggest_its_a_lie.5.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str","/ev",{"*":".^.^.c-4","flg":22},{"s":["^\"I'm no traitor",{"->":"$r","var":true},null]}],"ev","str","^Lie","/str","/ev",{"*":".^.c-5","flg":20},{"c-3":["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.c-0.10.suggest_its_a_lie.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.4.s"},[{"#n":"$r2"}],"^,\" I answer smartly.","\n",{"->":".^.^.^.g-0"},{"#f":5}],"c-4":["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.c-0.10.suggest_its_a_lie.c-4.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.5.s"},[{"#n":"$r2"}],"^,\" I answer","ev",{"VAR?":"forceful"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^smartly",{"->":".^.^.^.14"},null]}],[{"->":".^.b"},{"b":["^, voice quivering. \"For God's sake!\"",{"->":".^.^.^.14"},null]}],"nop","\n",{"->":".^.^.^.g-0"},{"#f":5}],"c-5":["^ ",{"->":".^.^.c-4"},"\n",{"->":".^.^.^.g-0"},{"#f":5}],"#f":5}],"g-0":["^He stares back at me.","\n",{"->":".^.^.^.^.^.^.done"},{"#f":5}]}],{"#f":5}],"c-1":["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"^ My reputation.\" ","ev",{"VAR?":"forceful"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^I'm aware of how arrogant I must sound but I plough on all the same.",{"->":".^.^.^.14"},null]}],[{"->":".^.b"},{"b":["^I don't like to talk of myself like this, but I carry on all the same.",{"->":".^.^.^.14"},null]}],"nop","^ \"Hooper simply can't bear knowing that, once all this is over, I'll be the one receiving the knighthood and he...\"","\n","^\"No—one will be getting a knighthood if the Germans make landfall,\" Harris answers sharply. He casts a quick eye to the door of the Hut to check the latch is still down, then continues in more of a murmur: \"Not you and not Hooper. Now answer me.\"","\n","^For the first time since the door closed, I wonder what the threat might be if I do not.","\n",{"->":".^.^.^.^.done"},{"#f":5}],"c-2":["^ \t\t\t\t","\n","ev",false,"/ev",{"VAR=":"teacup","re":true},"ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n","^\"How should I know?\" I reply, defensively. ","ev",{"VAR?":"teacup"},"/ev",[{"->":".^.b","c":true},{"b":["^I set the teacup back on the table.",{"->":".^.^.^.17"},null]}],"nop","^ ",{"->":".^.^.c-0.10.suggest_its_a_lie"},"\n",{"->":".^.^.^.^.done"},{"#f":5}]}],{"#f":5}],"c-3":["^ \t",{"->":".^.^.c-2"},"\n",{"->":".^.^.done"},{"#f":5}],"c-4":["^ \t\t",{"->":".^.^.c-0"},"\n",{"->":".^.^.done"},{"#f":5}],"done":[{"->":"harris_demands_component"},{"#f":5}]}],{"#f":1}],"harris_demands_component":[["^\"","ev",{"CNT?":"here_at_bletchley_diversion"},"/ev",[{"->":".^.b","c":true},{"b":["^Please",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["^So",{"->":".^.^.^.6"},null]}],"nop","^. Do you have it?\" Harris is ","ev",{"VAR?":"forceful"},3,">","/ev",[{"->":".^.b","c":true},{"b":["^sweating slightly",{"->":".^.^.^.15"},null]}],[{"->":".^.b"},{"b":["^wasting no time",{"->":".^.^.^.15"},null]}],"nop","^: Bletchley is his watch. \"Do you know where it is?\"","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["\n","^\"I do.\"","\n",{"->":"admitted_to_something"},{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ \"I have no idea.\" ","\n",{"->":".^.^.silence"},{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ \t\t",{"->":".^.^.c-1"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ \t\t","\n","^\"The component?\"","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n","^\"Don't play stupid,\" he replies. \"","ev",{"CNT?":"missing_reel"},"!","/ev",[{"->":".^.b","c":true},{"b":["^The component that went missing this afternoon. ",{"->":".^.^.^.22"},null]}],"nop","^Where is it?\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["ev",{"CNT?":"missing_reel"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->t->":"missing_reel"},{"->":".^.^.^.5"},null]}],"nop","\n","ev","str","^Co-operate","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Delay","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-7","flg":20},{"c-4":["^ \"I know where it is.\"","\n",{"->":"admitted_to_something"},{"->":".^.^.^.silence"},{"#f":5}],"c-5":["^ \"I know nothing about it.\" My voice shakes","ev",{"VAR?":"forceful"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^ with anger",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["^; I'm unaccustomed to facing off against men with holstered guns",{"->":".^.^.^.8"},null]}],"nop","^. ","\n",{"->":".^.^.^.silence"},{"#f":5}],"c-6":["^ ",{"->":".^.^.c-5"},"\n",{"->":".^.^.^.silence"},{"#f":5}],"c-7":["^ ","\n","^\"I don't know what gives you the right to pick on me. ","ev",{"VAR?":"forceful"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^I demand a lawyer.",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^I want a lawyer.",{"->":".^.^.^.10"},null]}],"nop","^\"","\n","^\"This is time of war,\" Harris answers. \"And by God, if I have to shoot you to recover the component, I will. Understand?\" He points at the mug, ",{"->":".^.^.^.silence.drinkit"},"\n",{"->":".^.^.^.silence"},{"#f":5}],"#f":5}],"silence":["^There's an icy silence. ","ev",{"VAR?":"forceful"},2,">","/ev",[{"->":".^.b","c":true},{"b":["^I've cracked him a little.",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["ev",{"VAR?":"evasive"},2,">","/ev",[{"->":".^.b","c":true},{"b":["^He's tiring of my evasiveness.",{"->":".^.^.^.6"},null]}],"nop",{"->":".^.^.^.8"},null]}],"nop","\n",["^\"Now drink your tea and talk.\"","\n","ev","str","^Drink","/str",{"VAR?":"teacup"},"/ev",{"*":".^.c-8","flg":21},"ev","str","^Put the cup down","/str",{"VAR?":"teacup"},"/ev",{"*":".^.c-9","flg":21},"ev","str","^Take the cup","/str",{"VAR?":"teacup"},"!","/ev",{"*":".^.c-10","flg":21},"ev","str","^Don't take it","/str",{"VAR?":"teacup"},"!","/ev",{"*":".^.c-11","flg":21},{"c-8":["^ \t\t\t",{"->":".^.^.c-10.2.drinkfromcup"},"\n",{"->":".^.^.^.^.g-1"},{"#f":5}],"c-9":["^ ","\n","^I set the cup carefully down on the table once more.","\n","ev",false,"/ev",{"VAR=":"teacup","re":true},"ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.c-11.10.whatsinit"},{"->":".^.^.^.^.g-1"},{"#f":5}],"c-10":["^ ","\n",[["^I lift the cup ","ev",{"VAR?":"teacup"},"/ev",[{"->":".^.b","c":true},{"b":["^to my lips ",{"->":".^.^.^.5"},null]}],"nop","^and sip. He waits for me to swallow before speaking again.","\n","ev",true,"/ev",{"VAR=":"drugged","re":true},"ev",true,"/ev",{"VAR=":"teacup","re":true},{"->":".^.^.^.^.^.^.g-1"},{"#f":5,"#n":"drinkfromcup"}],null],{"#f":5}],"c-11":["^ ","\n","^I leave the cup where it is.","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",[["^\"Why?\" I ask coldly. \"What's in it?\"","\n",{"->":".^.^.^.^.^.^.g-1"},{"#f":5,"#n":"whatsinit"}],null],{"#f":5}],"#f":5,"#n":"drinkit"}],{"#f":5}],"g-1":["^\"Lapsang Souchong,\" he ","ev",{"CNT?":".^.^.silence.drinkit.c-10.2.drinkfromcup"},"/ev",[{"->":".^.b","c":true},{"b":["^remarks",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["^replies",{"->":".^.^.^.6"},null]}],"nop","^, placing his own cup back on the table untouched. \"Such a curious flavour. It might almost not be tea at all. You might say it hides a multitude of sins. As do you. Isn't that right?\"","\n","ev","str","^Agree","/str","/ev",{"*":".^.c-12","flg":20},"ev","str","^Disagree","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-13","flg":21},"ev","str","^Disagree","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-14","flg":21},"ev","str","^Lie","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-15","flg":21},"ev","str","^Lie","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-16","flg":21},"ev","str","^Evade","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-17","flg":21},"ev","str","^Evade","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-18","flg":21},{"c-12":["^ ","\n","^\"I suppose so,\" I reply. \"I've done things I shouldn't have done.\"","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n",{"->":"harris_presses_for_details"},{"#f":5}],"c-13":["\n","^\"I've done nothing that I'm ashamed of.\"","\n",{"->":"harris_asks_for_theory"},{"#f":5}],"c-14":["^ ","\n","^I open my mouth to disagree, but the words I want won't come. It is like Harris has taken a screwdriver to the sides of my jaw.","\n",{"->":"admitted_to_something.ive_done_things"},{"#f":5}],"c-15":["^ \t",{"->":".^.^.c-14"},"\n",{"#f":5}],"c-16":["^ \t",{"->":".^.^.c-13"},"\n",{"#f":5}],"c-17":["^ ",{"->":".^.^.c-14"},"\n",{"#f":5}],"c-18":["^ ","\n","^\"None of us are blameless, Harris. ","ev",{"VAR?":"forceful"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^But you're not my priest and I'm not yours",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^But I've done nothing to deserve this treatment",{"->":".^.^.^.10"},null]}],"nop","^. Now, please. Let me go. I'll help you find this damn component, of course I will.\"","\n","^He appears to consider the offer.","\n",{"->":"harris_asks_for_theory"},{"#f":5}],"#f":5}]}],{"#f":1}],"harris_presses_for_details":[["^\"You mean you've left yourself open,\" Harris answers. \"To pressure. Is that what you're saying?\"","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-1","flg":21},"ev","str","^No","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-2","flg":21},"ev","str","^Evade","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-3","flg":21},"ev","str","^Evade","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-4","flg":21},{"c-0":["^ ",{"->":".^.^.^.admit_open_to_pressure"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"I'm not saying anything of the sort,\" I snap back. \"What is this, Harris? You're accusing me of treachery but I don't see a shred of evidence for it! Why don't you put your cards on the table?\"","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^I shake my head violently, to say no, that's not it, but whatever is wrong with tongue is wrong with neck too. I look across at the table at Harris' face and realise with a start how sympathetic he is. Such a kind, generous man. How can I hold anything back from him?","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n","^I take another mouthful of the bitter, strange—tasting tea before answering.","\n",{"->":".^.^.^.admit_open_to_pressure"},{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ ","\n","^\"You're the one applying pressure here,\" I answer ","ev",{"VAR?":"forceful"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^smartly",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^somewhat miserably",{"->":".^.^.^.10"},null]}],"nop","^. \"I'm just waiting until you tell me what is really going on.\"","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.g-0"},{"#f":5}],"c-4":["^ \t\t\t\t ","\n","^\"We're all under pressure here.\"","\n","^He looks at me with pity. ",{"->":"harris_has_seen_it_before"},"\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"It's simple enough,\" Harris says. ",{"->":"harris_has_seen_it_before"},"\n",{"#f":5}]}],{"admit_open_to_pressure":["^\"That's it,\" I reply. \"There are some things... which a man shouldn't do.\"","\n","ev",true,"/ev",{"VAR=":"admitblackmail","re":true},"^Harris doesn't stiffen. Doesn't lean away, as though my condition might be infectious. I had thought they trained them in the army to shoot my kind on sight.","\n","^He offers no sympathy either. He nods, once. His understanding of me is a mere turning cog in his calculations, with no meaning to it.","\n",{"->":"harris_has_seen_it_before"},{"#f":1}],"#f":1}],"admitted_to_something":[["ev",{"VAR?":"drugged"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^Harris stares back at me. ","ev",{"VAR?":"evasive"},0,"==","/ev",[{"->":".^.b","c":true},{"b":["^He cannot have expected it to be so easy to break me.",{"->":".^.^.^.8"},null]}],"nop","\n",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["\n","^Harris smiles with satisfaction, as if your willingness to talk was somehow his doing.","\n",{"->":".^.^.^.6"},null]}],"nop","\n","^\"I see.\"","\n","^There's a long pause, like the delay between feeding a line of cypher into the Bombe and waiting for its valves to warm up enough to begin processing.","\n","^\"You want to explain that?\"","\n","ev","str","^Explain","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't explain","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-1","flg":21},"ev","str","^Lie","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-2","flg":21},"ev","str","^Evade","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-3","flg":21},"ev","str","^Say nothing","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-4","flg":21},{"c-0":["^ ","\n","^I pause a moment, trying to choose my words. To just come out and say it, after a lifetime of hiding... that is a circle I cannot square.","\n",["ev","str","^Explain","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Say nothing","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-1","flg":21},"ev","str","^Lie","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-2","flg":21},{"c-0":["^ \t",{"->":".^.^.^.^.^.ive_done_things"},"\n",{"#f":5}],"c-1":["^ \t",{"->":".^.^.^.^.c-4"},"\n",{"#f":5}],"c-2":["^ \t",{"->":"claim_hooper_took_component"},"\n",{"#f":5}]}],{"#f":5}],"c-1":["\n","^\"There's nothing to explain,\" I reply stiffly. ",{"->":".^.^.^.i_know_where"},"\n",{"#f":5}],"c-2":["^ ",{"->":"claim_hooper_took_component"},"\n",{"#f":5}],"c-3":["\n","^\"Explain what you should be doing, do you mean, rather than bullying me? Certainly.\" I fold my arms. ",{"->":".^.^.^.i_know_where"},"\n",{"#f":5}],"c-4":["\n","^I fold my arms, intended firmly to say nothing. But somehow, watching Harris' face, I cannot bring myself to do it. I want to confess. I want to tell him everything I can, to explain myself to him, to earn his forgiveness. The sensation is so strong my will is powerless in the face of it.","\n","^Something is wrong with me, I am sure of it. There is a strange, bitter flavour on my tongue. I taste it as words start to form.","\n",{"->":".^.^.^.ive_done_things"},{"#f":5}]}],{"i_know_where":["^\"I know where your component is because it's obvious where your component is. That doesn't mean I took it, just because I can figure out a simple problem, any more than it means I'm a German spy because I can crack their codes.\"","\n",{"->":"harris_asks_for_theory"},{"#f":1}],"ive_done_things":["^\"I've done things,\" I begin","ev",{"CNT?":"harris_demands_component.0.g-1.c-14"},"/ev",[{"->":".^.b","c":true},{"b":["^ helplessly",{"->":".^.^.^.5"},null]}],"nop","^. \"Things I didn't want to do. I tried not to. But in the end, it felt like cutting off my own arm to resist.\"","\n",{"->":"harris_presses_for_details"},{"#f":1}],"#f":1}],"harris_asks_for_theory":[["^\"Tell me, then,\" he asks. \"What's your theory? You're a smart fellow — as smart as they come around here, and that's saying something. What's your opinion on the missing component? Accident, perhaps? Or do you blame one of the other men? ","ev",{"VAR?":"hooper_mentioned"},"/ev",[{"->":".^.b","c":true},{"b":["^Hooper?",{"->":".^.^.^.5"},null]}],"nop","^\"","\n","ev","str","^Blame no—one","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Blame someone","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n",{"->":".^.^.^.an_accident"},{"#f":5}],"c-1":["^ ",{"->":"claim_hooper_took_component"},"\n",{"#f":5}]}],{"an_accident":[["^\"An accident, naturally.\" I risk a smile. \"That damned machine is made from spare parts and string. Even these Huts leak when it rains. It wouldn't take more than one fellow to trip over a cable to shake out a component. Have you tried looking under the thing?\"","\n","^\"Do you believe we haven't?\"","\n","^In a sudden moment I understand that his reply is a threat.","\n","^\"Now,\" he continues. \"Are you sure there isn't anything you want to tell me?\"","\n","ev","str","^Co-operate","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Evade","/str",{"VAR?":"evasive"},0,">","/ev",{"*":".^.c-1","flg":21},{"c-0":["\n","^\"All right.\" With a sigh, your defiance collapses. \"If you're searched my things then I suppose you've found ","ev",{"VAR?":"evasive"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^ what you need",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["^my letters. Haven't you? In fact, if you haven't, don't tell me",{"->":".^.^.^.9"},null]}],"nop","^.","\n","ev",true,"/ev",{"VAR=":"admitblackmail","re":true},"^Harris nods once.","\n","<>","^ ",{"->":"harris_has_seen_it_before"},"\n",{"#f":5}],"c-1":["^ \"Only that you're being unreasonable, and behaving like a swine.\"","\n","^\"You imbecile,\" Harris replies, with sudden force. He is half out of his chair. \"You know the situation as well as I do. Why the fencing? The Hun are poised like rats, ready to run all over this country. They'll destroy everything. You understand that, don't you? You're not so locked up inside your crossword puzzles that you don't see that, are you? This machine we have here — you men — you are the best and only hope this country has. God help her.\"","\n","ev",true,"/ev",{"VAR=":"losttemper","re":true},"^I sit back, startled by the force of his outburst. His carefully sculpted expression has curled to angry disgust. He really does hate me, I think. He'll have my blood for the taste of it.","\n",["ev","str","^Placate","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Mock","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Dismiss","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","^\"Now steady on,\" I reply, gesturing for him to be calm.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"I can imagine how being surrounded by clever men is pretty threatening for you, Commander,\" I reply with a sneer. \"They don't train you to think in the Armed Forces.\"","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["\n","^\"Then I'll be going, on and getting on with my job of saving her, shall I?\" I even rise half to my feet, before he slams the tabletop.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"Talk,\" Harris demands. \"Talk now. Tell me where you've hidden it or who you passed it to. Or God help me, I'll take your wretched pansy body to pieces looking for it.\"","\n",{"->":"harris_demands_you_speak"},{"#f":5}]}],{"#f":5}]}],{"#f":1}],"#f":1}],"harris_has_seen_it_before":[["^\"I've seen it before. A young man like you — clever, removed. The kind that doesn't go to parties. Who takes himself too seriously. Who takes things too far.\"","\n","^He slides his thumb between two fingers.","\n","^\"Now they own you.\"","\n","ev","str","^Agree","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Disagree","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-2","flg":21},"ev","str","^Apologise","/str",{"VAR?":"drugged"},{"VAR?":"forceful"},0,"<","&&","/ev",{"*":".^.c-3","flg":21},{"c-0":["^ ","\n","^\"What could I do?\" I'm shaking now. The night is cold and the heat—lamp in the Hut has been removed. \"","ev",{"VAR?":"forceful"},2,">","/ev",[{"->":".^.b","c":true},{"b":["^I won't",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^I don't want to",{"->":".^.^.^.10"},null]}],"nop","^ go to prison.\"","\n","^\"Smart man,\" he replies. \"You wouldn't last.","\n",{"->":".^.^.tell_me_now"},{"#f":5}],"c-1":["^ ","\n","^\"I can still fix this.\"","\n","^Harris shakes his head. \"You'll do nothing. This is beyond you now. You may go to prison or may go to firing squad - or we can change your name and move you somewhere where your indiscretions can't hurt you. But right now, none of that matters. What happens to you doesn't matter. All that matters is where that component is.","\n",{"->":".^.^.tell_me_now"},{"#f":5}],"c-2":["^ ","\n","^\"I wanted to tell you,\" I tell him. \"I thought I could find out who they were. Lead you to them.\"","\n","^Harris looks at me with contempt. \"You wretch. You'll pay for what you've done to this country today. If a single man loses his life because of your pride and your perversions then God help your soul.","\n",{"->":".^.^.tell_me_now"},{"#f":5}],"c-3":["\n","^\"Harris, I...\"","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n","^\"Stop it,\" he interrupts. \"There's no jury here to sway. And there's no time.","\n",{"->":".^.^.tell_me_now"},{"#f":5}],"tell_me_now":["<>","^ So why don't you tell me, right now. Where is it?\"","\n",{"->":"harris_demands_you_speak"},{"#f":5}]}],{"#f":1}],"harris_demands_you_speak":[["^His eyes bear down like carbonised drill—bits.","\n","ev","str","^Confess","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Dissemble","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-1","flg":21},"ev","str","^Dissemble","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-2","flg":21},{"c-0":["^ ","\n","ev",{"VAR?":"forceful"},1,">","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"You want me to tell you what happened? You'll be disgusted.\"","\n",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["\n","^\"All right. I'll tell you what happened.\" And never mind my shame.","\n",{"->":".^.^.^.9"},null]}],"nop","\n","^\"I can imagine how it starts,\" he replies.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ",{"->":"claim_hooper_took_component"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["\n","^My plan now is to blame Hooper, but I cannot seem to tell the story. Whatever they put in my tea, it rules my tongue. ","ev",{"VAR?":"forceful"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^I fight it as hard as I can but it does no good.",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["^I am desperate to tell him everything. I am weeping with shame.",{"->":".^.^.^.9"},null]}],"nop","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"i_met_a_young_man"},{"#f":5}]}],{"#f":1}],"i_met_a_young_man":[["ev","str","^Talk","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n","^\"There was a young man. I met him in the town. A few months ago now. We got to talking. Not about work. And I used my cover story, but he seemed to know it wasn't true. That got me wondering if he might be one of us.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^Harris is not letting me off any more.","\n","^\"You seriously entertained that possibility?\"","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-3","flg":20},{"c-1":["\n","^\"Yes, I considered it. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-2":["^ ","\n","^\"No. Not for more than a moment, of course. Everyone here is marked out by how little we would be willing to say about it.\"","\n","^\"Only you told this young man more than a little, didn't you?\"","\n","^I nod. \"","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-3":["^ ","\n","^\"I was quite certain, after a while. After we'd been talking. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":["^He seemed to know all about me. He... he was quite enchanted by my achievements.\"","\n","^The way Harris is staring I expect him to strike me, but he does not. He replies, \"I can see how that must have been attractive to you,\" with such plain—spokeness that I think I must have misheard.","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^No","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-5","flg":21},"ev","str","^No","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-6","flg":21},"ev","str","^Lie","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-7","flg":21},{"c-4":["^ \"It's a lonely life in this place,\" I reply. \"Lonely - and still one never gets a moment to oneself.\"","\n","^\"That's how it is in the Service,\" Harris answers.","\n",["ev","str","^Argue","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Agree","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ \"I'm not in the Service.\"","\n","^Harris shakes his head. \"Yes, you are.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ \"Perhaps. But I didn't choose this life.\" ","\n","^Harris shakes his head. \"No. And there's plenty of others who didn't who are suffering far worse.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^Then he waves the thought aside.","\n",{"->":".^.^.^.^.^.g-2"},{"#f":5}]}],{"#f":5}],"c-5":["^ \"The boy was a pretty simpleton. Quite inferior. His good opinion meant nothing to be. Harris, do not misunderstand. I was simply after his body.\"","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n","^Harris, to his credit, doesn't flinch; but I can see he will have nightmares of this moment later tonight. I'm tempted to reach out and take his hand to worsen it for him.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-6":["^ ","\n","^\"It wasn't,\" I reply. \"But I doubt you'd understand.\"","\n","^He simply nods.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-7":["^ ",{"->":".^.^.c-5"},"\n",{"->":".^.^.^.g-2"},{"#f":5}],"#f":5}],"g-2":["^\"Go on with your confession.\"","\n",["ev",{"CNT?":".^.^.^.g-1.c-5"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^That gives me pause. I hadn't thought of it as such. But I suppose he's right. I am about to admit what I did.","\n",{"->":".^.^.^.5"},null]}],"nop","\n","^\"There's not much else to say. I took the part from Bombe computing device. You seem to know that already. I had to. He was going to expose me if I didn't.\"","\n","^\"This young man was blackmailing you over your affair?\"","\n","ev",{"VAR?":"drugged"},"/ev",{"temp=":"harris_thinks_youre_drugged"},"ev",{"VAR?":"drugged"},"/ev",[{"->":".^.b","c":true},{"b":["\n","ev",false,"/ev",{"VAR=":"drugged","re":true},"^As Harris speaks I find myself suddenly sharply aware, as if waking from a long sleep. The table, the corrugated walls of the hut, everything seems suddenly more tangible than a moment before.","\n","^Whatever it was they put in my drink is wearing off.","\n",{"->":".^.^.^.19"},null]}],"nop","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-8","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-9","flg":20},"ev","str","^Tell the truth","/str","/ev",{"*":".^.c-10","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-11","flg":20},{"c-8":["^ ","\n","^\"Yes. I suppose he was their agent. I should have realised but I didn't. Then he threatened to tell you. I thought you would have me locked up: I couldn't bear the thought of it. I love working here. I've never been so happy, so successful, anywhere before. I didn't want to lose it.\"","\n","^\"So what did you do with the component?\" Harris talks urgently. He grips his gloves tightly in one hand, perhaps prepared to lift them and strike if it is required. \"Have you passed it to this man already? Have you left it somewhere for him to find?\"","\n",["ev","str","^I have it","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^I don't have it","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Tell the truth","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["^ \t","\n","^\"I still have it. Not on me, of course. ",{"->":"reveal_location_of_component"},"\n",{"#f":5}],"c-1":["^ \t",{"->":".^.^.^.^.^.^.^.i_dont_have_it"},"\n",{"#f":5}],"c-2":["^ \t\t\t\t\t\t\t",{"->":".^.^.c-1"},"\n",{"#f":5}],"c-3":["^ \t\t\t\t",{"->":".^.^.c-0"},"\n",{"#f":5}]}],{"#f":5}],"c-9":["^ ","\n","^\"No, Harris. The young man wasn't blackmailing me.\" I take a deep breath. \"It was Hooper.\"","\n","ev",{"VAR?":"hooper_mentioned"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"Hooper!\" Harris exclaims, in surprise. ","ev",{"VAR?":"harris_thinks_youre_drugged"},"/ev",[{"->":".^.b","c":true},{"b":["^He does not doubt me for a moment.",{"->":".^.^.^.6"},null]}],"nop","\n",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["\n","^\"Now look here,\" Harris interrupts. \"Don't start that again.\"","\n",{"->":".^.^.^.10"},null]}],"nop","\n","^\"It's the truth, Harris. If I'm going to jail, so be it, but I won't hang at Traitor's Gate. Hooper was the one who told the boy about our work. Hooper put the boy on to me. ","ev",{"VAR?":"forceful"},2,"<","/ev",[{"->":".^.b","c":true},{"b":["^I should have realised, of course. These things don't happen by chance. I was a fool to think they might.",{"->":".^.^.^.19"},null]}],"nop","^ And then, once he had me compromised, he demanded I steal the part from the machine.\"","\n","ev",true,"/ev",{"VAR=":"revealedhooperasculprit","re":true},"^\"Which you did.\" Harris leans forward. \"And then what? You still have it? You've stashed it somewhere?\"","\n",["ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["^ ","\n","^\"Yes. I only had a moment. ",{"->":"reveal_location_of_component"},"\n",{"#f":5}],"c-1":["^ ",{"->":".^.^.^.^.^.^.^.passed_onto_hooper"},"\n",{"#f":5}],"c-2":["^ \t\t\t",{"->":".^.^.c-1"},"\n",{"#f":5}],"c-3":["^ \t\t","\n","^\"I can't remember.\"","\n","^He draws his gun and lays it lightly on the field table.","\n","^\"I'm sorry to threaten you, friend. But His Majesty needs that brain of yours, and that brain alone. There are plenty of other parts to you that our country could do better without. Now I'll ask you again. Did you hide the component?\"","\n",["ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["^ ",{"->":".^.^.^.^.c-0"},"\n",{"#f":5}],"c-1":["^ ","\n","^\"Very well then.\" I swallow nervously, to make it look more genuine. ",{"->":"i_met_a_young_man.passed_onto_hooper"},"\n",{"#f":5}],"c-2":["^ ",{"->":".^.^.c-1"},"\n",{"#f":5}],"c-3":["^ ",{"->":"i_met_a_young_man.i_dont_have_it"},"\n",{"#f":5}]}],{"#f":5}]}],{"#f":5}],"c-10":["^ \t",{"->":".^.^.c-8"},"\n",{"#f":5}],"c-11":["^ \t\t\t\t",{"->":".^.^.c-9"},"\n",{"#f":5}],"#f":5,"#n":"paused"}],{"#f":5}]}],{"i_dont_have_it":[["^\"I don't have it any more. I passed it through the fence to my contact straight after taking it, before it was discovered to be missing. It would have been idiocy to do differently. It's long gone, I'm afraid.\"","\n","^\"You fool, Manning,\" Harris curses, getting quickly to his feet. \"You utter fool. Do you suppose you will be any better off living under Hitler? It's men like you who will get us all killed. Men too feeble, too weak in their hearts to stand up and take a man's responsibility for the world. You're happier to stay a child all your life and play with your little childish toys.\"","\n","ev","str","^Answer back","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Say nothing","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"Really, Commander,\" I reply. \"It rather sounds like you want to spank me.\"","\n","^\"For God's sake,\" he declares with thick disgust, then swoops away out of the room.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^I say nothing. It's true, isn't it? I can't deny that I know there is a world out there, a complicated world of pain and suffering. And I can't deny that I don't think about it a moment longer than I have to. What use is thinking on a problem that cannot be solved? It is precisely our ability to avoid such endless spirals that makes us human and not machine.","\n","^\"God have mercy on your soul,\" Harris says finally, as he gets to his feet and heads for the door. \"I fear no—one else will.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"left_alone"},{"#f":5}]}],{"#f":1}],"passed_onto_hooper":[["ev",true,"/ev",{"VAR=":"hooper_mentioned","re":true},"^\"No. I passed it on to Hooper.\"","\n","^\"I see. And what did he do with it?\"","\n","ev","str","^Evade","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Tell the truth","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^\"I don't know.\"","\n","^\"You can do better than that. Remember, there's a hangman's noose waiting for traitors.\"","\n",["ev","str","^Theorise","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Shrug","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"Well, then,\" I answer, nervously. \"What would he do? Either get rid of it straight away — or if that wasn't possible, which it probably wouldn't be, since he'd have to arrange things with his contacts — so most likely, he'd hide it somewhere and wait, until you had the rope around my neck and he could be sure he was safe.\"","\n",{"->":"claim_hooper_took_component.harris_being_convinced"},{"#f":5}],"c-1":["^ ",{"->":"claim_hooper_took_component.its_your_problem"},"\n",{"#f":5}]}],{"#f":5}],"c-1":["^ ","\n","^\"I don't think Hooper could have planned this in advance. So he'd need to get word to whoever he's working with, and that would take time. So I think he would have hidden it somewhere, and be waiting to make sure I soundly take the fall. That way, if anything goes wrong, he can arrange for the part to be conveniently re—found.\"","\n",{"->":"claim_hooper_took_component.harris_being_convinced"},{"#f":5}],"c-2":["\n","^\"I'm sure I saw him this evening, talking to someone by the fence on the woodland side of the compound. He's probably passed it on already. You'll have to ask him.\"","\n",{"->":"claim_hooper_took_component.harrumphs"},{"#f":5}]}],{"#f":1}],"#f":1}],"claim_hooper_took_component":[["^\"I saw Hooper take it.\"","\n","ev",true,"/ev",{"VAR=":"hooper_mentioned","re":true},"ev",{"VAR?":"losttemper"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^\"Did you?\"","\n","^The worst of his rage is passing; he is now moving into a kind of contemptuous despair. I can imagine him wrapping up our interview soon, leaving the hut, locking the door, and dropping the key down the well in the yard.","\n","^And why wouldn't he? With my name tarnished they will not let me back to work on the Bombe — if there is the slightest smell of treachery about my name I would be lucky not be locked up for the remainder of the war.","\n",{"->":".^.^.^.11"},null]}],[{"->":".^.b"},{"b":["\n","^\"I see.\" He is starting to lose his patience. I have seen Harris angry a few times, with lackeys and secretaries. But never with us. With the 'brains' he has always been cautious, treating us like children.","\n","^And now I see that, like a father, he wants to smack us when we disobey him.","\n",{"->":".^.^.^.11"},null]}],"nop","\n","^\"Just get to the truth, man. Every minute matters.\"","\n","ev","str","^Persist with this","/str",{"VAR?":"admitblackmail"},"/ev",{"*":".^.c-0","flg":21},"ev","str","^Tell the truth","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Persist with this","/str",{"VAR?":"admitblackmail"},"!","/ev",{"*":".^.c-2","flg":21},{"c-0":["\n","^\"I know what you're thinking. If I've transgressed once then I must be guilty of everything else... But I'm not. We were close to cracking the 13th's intercept. We were getting correlations in the data. Then Hooper disappeared for a moment, and next minute the machine was down.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"Very well. I see there's no point in covering up. You know everything anyway.\"","\n","^Harris nods, and waits for me to continue.","\n",{"->":"i_met_a_young_man"},{"->":".^.^.g-0"},{"#f":5}],"c-2":["\n","^\"This is the truth.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^I have become, somehow, an accustomed liar — the words roll easily off my tongue. Perhaps I am a traitor, I think, now that I dissemble as easily as one.","\n","^\"Go on,\" Harris says, giving me no indication of whether he believes my tale.","\n","ev","str","^Assert","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Imply","/str","/ev",{"*":".^.c-4","flg":20},{"c-3":["^ \"I saw him take it,\" I continue. \"Collins was outside having a cigarette. Peterson was at the table. But I was at the front of the machine. I saw Hooper go around the side. He leant down and pulled something free. I even challenged him. I said, 'What's that? Someone put a nail somewhere they shouldn't have?' He didn't reply.\"","\n","^Harris watches me for a long moment.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-4":["^ \"At the moment the machine halted, Peterson was at the bench and Collins was outside having a smoke. I was checking the dip—switches. Hooper was the only one at the back of the Bombe. No—one else could have done it.\"","\n","^\"That's not quite the same as seeing him do it,\" Harris remarks.","\n",["ev","str","^Logical","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Persuasive","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Confident","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","^\"When you have eliminated the impossible...\" I begin, but Harris cuts me off.","\n",{"->":".^.^.^.^.^.g-1"},{"#f":5}],"c-1":["^ ","\n","^\"You have to believe me.\"","\n","^\"We don't have to believe anyone,\" Harris returns. \"I will only be happy with the truth, and your story doesn't tie up. We know you've been leaving yourself open to pressure. We've been watching your activities for some time. But we thought you were endangering the reputation of this site; not risking the country herself. Perhaps I put too much trust in your intellectual pride.\"","\n","^He pauses for a moment, considering something. Then he continues:","\n","^\"It might have been Hooper. It might have been you. ",{"->":".^.^.^.^.^.we_wont_guess"},"\n",{"->":".^.^.^.^.^.g-1"},{"#f":5}],"c-2":["^ ","\n","^\"Ask the others,\" I reply, leaning back. \"They'll tell you. If they haven't already, that's only because they're protecting Hooper. Hoping he'll come to his senses and stop being an idiot. I hope he does too. And if you lock him up in a freezing hut like you've done me, I'm sure he will.\"","\n","^\"We have,\" Harris replies simply.","\n","^It's all I can do not to gape.","\n",{"->":".^.^.^.^.^.g-1.hoopers_hut_3"},{"->":".^.^.^.^.^.g-1"},{"#f":5}]}],{"#f":5}],"#f":5}],"g-1":["^\"We are left with two possibilities. You, or Hooper.\" The Commander pauses to smooth down his moustache. ","<>","\n",["^\"Hooper's in Hut 3 with the Captain, having a similar conversation.\"","\n",["ev",{"^->":"claim_hooper_took_component.0.g-1.hoopers_hut_3.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-5","flg":22},{"s":["^\"And the other men?",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.0.g-1.hoopers_hut_3.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-6","flg":22},{"s":["^\"Then you know I'm right.",{"->":"$r","var":true},null]}],{"c-5":["ev",{"^->":"claim_hooper_took_component.0.g-1.hoopers_hut_3.c-5.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"^ Do we have a hut each? Are there enough senior officers to go round?\"","\n","^\"Collins was outside when it happened, and Peterson can't get round the machine in that chair of his,\" Harris replies. \"That leaves you and Hooper.","\n",{"->":".^.^.^.^.we_wont_guess"},{"#f":5}],"c-6":["ev",{"^->":"claim_hooper_took_component.0.g-1.hoopers_hut_3.c-6.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"^ You knew all along. Why did you threaten me?\"","\n","^\"All we know is that we have a traitor, holding the fate of the country in his hands.","\n",{"->":".^.^.^.^.we_wont_guess"},{"#f":5}],"#f":5,"#n":"hoopers_hut_3"}],{"#f":5}],"we_wont_guess":["<>","^ We're not in the business of guessing here at Bletchley. We are military intelligence. We get answers.\" Harris points a finger. \"And if that component has left these grounds, then every minute is critical.\"","\n","ev","str","^Co-operate","/str","/ev",{"*":".^.c-7","flg":20},"ev","str","^Block","/str","/ev",{"*":".^.c-8","flg":20},{"c-7":["^ ","\n","^\"I'd be happy to help,\" I answer, leaning forwards. \"I'm sure there's something I could do.\"","\n","^\"Like what, exactly?\"","\n",[["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"Put me in with Hooper.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-1","flg":22},{"s":["^\"Tell Hooper I've confessed.",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.^.^.^.^.putmein"},{"#f":5}],"c-1":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"^ Better yet. Let him see you marching me off in handcuffs. Then let him go, and see what he does. Ten to one he'll go straight to wherever he's hidden that component and his game will be up.\"","\n","^Harris nods slowly, chewing over the idea. It isn't a bad plan even — except, of course, Hooper has not hidden the component, and won't lead them anywhere. But that's a problem I might be able to solve once I'm out of this place; and once they're too busy dogging Hooper's steps from hut to hut.","\n","^\"Interesting,\" the Commander muses. \"But I'm not so sure he'd be that stupid. And if he's already passed the part on, the whole thing will only be a waste of time.\"","\n",[["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-0","flg":22},{"s":["^\"Trust me. He hasn't.",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str","/ev",{"*":".^.^.c-1","flg":22},{"s":["^\"You're right. Let me talk to him",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":18},{"s":["^\"You're right.\" ",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"^ If I know that man, and I do, he'll be wanting to keep his options open as long as possible. If the component's gone then he's in it up to his neck. He'll take a week at least to make sure he's escaped suspicion. Then he'll pass it on.\"","\n","^\"And if we keep applying pressure to him, you think the component will eventually just turn up?\"","\n",[["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-0","flg":22},{"s":["^\"Yes.",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"Or be thrown into the river.\" ",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"^ Probably under my bunk.\"","\n","^Harris smiles wryly. \"We'll know that for a fake, then. We've looked there already.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n","^\"Hmm.\" Harris chews his moustache thoughtfully. \"Well, that would put us in a spot, seeing as how we'd never know for certain. We'd have to be ready to change our whole approach just in case the part had got through to the Germans.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["<>","^ I don't mind telling you, this is a disaster, this whole thing. What I want is to find that little bit of mechanical trickery. I don't care where. In your luncheon box or under Hooper's pillow. Just somewhere, and within the grounds of this place.\"","\n",["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.g-0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-2","flg":22},{"s":["^\"Then let him he think he's off the hook.",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.g-0.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str","/ev",{"*":".^.^.c-3","flg":22},{"s":["^\"Then you'd better get searching",{"->":"$r","var":true},null]}],{"c-2":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.g-0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"^ Make a show of me. And then you'll get your man.\"","\n","^Somehow, I think. But that's the part I need to work.","\n",{"->":"harris_takes_you_to_hooper"},{"#f":5}],"c-3":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.g-0.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.4.s"},[{"#n":"$r2"}],"^,\" I reply, tiring of his complaining. A war is a war, you have to expect an enemy. ",{"->":".^.^.^.^.^.^.^.^.^.^.^.its_your_problem"},"\n",{"#f":5}],"#f":5}]}],{"#f":5}],"c-1":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"^, then. As a colleague. Maybe I can get something useful out of him.\"","\n",{"->":".^.^.^.^.^.^.^.^.putmein"},{"#f":5}],"c-2":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],{"->":".^.^.^.^.^.^.^.^.shake_head"},"\n",{"#f":5}]}],{"#f":5}]}],{"#f":5}],"c-8":["^ ",{"->":".^.^.^.^.its_your_problem"},"\n",{"#f":5}],"#f":5}]}],{"harris_being_convinced":[["^\"Makes sense,\" Harris agrees, cautiously. ","ev",{"VAR?":"evasive"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^I can see he's still not entirely convinced by my tale, as well he might not be — I've hardly been entirely straight with him.",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["^I can see he's still not certain whether he can trust me.",{"->":".^.^.^.8"},null]}],"nop","^ \"Which means the question is, what can we do to rat him out?\"","\n","ev","str","^Offer to help","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't offer to help","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"Maybe I can help with that.\"","\n","^\"Oh, yes? And how, exactly?\"","\n",[["ev",{"^->":"claim_hooper_took_component.harris_being_convinced.0.c-0.6.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"I'll talk to him.\" ",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.harris_being_convinced.0.c-0.6.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-1","flg":22},{"s":["^\"We'll fool him.",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"claim_hooper_took_component.harris_being_convinced.0.c-0.6.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n","^\"What?\"","\n","^\"Put me in with Hooper with him. Maybe I can get something useful out of him.\"","\n",{"->":".^.^.^.^.^.^.putmein"},{"#f":5}],"c-1":["ev",{"^->":"claim_hooper_took_component.harris_being_convinced.0.c-0.6.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"^ He's waiting to be sure that I've been strung up for this, so let's give him what he wants. If he sees me taken away, clapped in irons — he'll go straight to that component and set about getting rid of it.\"","\n",{"->":"harris_takes_you_to_hooper"},{"#f":5}]}],{"#f":5}],"c-1":["\n","^I lean back. ",{"->":".^.^.^.^.its_your_problem"},"\n",{"#f":5}]}],{"#f":1}],"putmein":[["^Harris shakes his head.","\n","^\"He despises you. I don't see why he'd give himself up to you.\"","\n","ev","str","^Insist","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Give in","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ \"Try me. Just me and him.\" ","\n",{"->":".^.^.^.^.go_in_alone"},{"#f":5}],"c-1":["^ \"You're right.\" ","\n",{"->":".^.^.^.^.shake_head"},{"#f":5}]}],{"#f":1}],"shake_head":["<>","^ I shake my head. \"You're right. I don't see how I can help you. So there's only one conclusion.\"","\n","^\"Oh, yes? And what's that?\"","\n",{"->":".^.^.its_your_problem"},{"#f":1}],"its_your_problem":["^\"It's your problem. Your security breach. So much for your careful vetting process.\"","\n","^I lean back in my chair and fold my arms so the way they shake will not be visible.","\n","^\"You'd better get on with solving it, instead of wasting your time in here with me.\"","\n",{"->":".^.^.harrumphs"},{"#f":1}],"harrumphs":[["^Harris harrumphs. He's thinking it all over.","\n","ev","str","^Wait","/str",{"CNT?":".^.^.^.putmein"},"/ev",{"*":".^.c-0","flg":21},"ev","str","^Wait","/str",{"CNT?":".^.^.^.putmein"},"/ev",{"*":".^.c-1","flg":21},{"c-0":["^ ","\n","^\"All right,\" he declares, gruffly. \"We'll try it. But if this doesn't work, I might just put the both of you in front of a firing squad and be done with these games. Worse things happen in time of war, you know.\"","\n","^\"Alone,\" I add.","\n",{"->":".^.^.^.^.go_in_alone"},{"#f":5}],"c-1":["^ ","\n","^\"No,\" Harris declares, finally. \"I think you're lying about Hooper. I think you're a clever, scheming young man — that's why we hired you — and you're looking for the only reasonable out this situation has to offer. But I'm not taking it. We know you were in the room with the machine, we know you're of a perverted persuasion, we know you have compromised yourself. There's nothing more to say here. Either you tell me what you've done with that component, or we will hang you and search just as hard. It's your choice.\"","\n",{"->":"harris_threatens_lynching"},{"#f":5}]}],{"#f":1}],"go_in_alone":[["^\"Alone?\"","\n","^\"Alone.\"","\n","^Harris considers it. I watch his eyes, flicking backwards and forwards over mine, like a ribbon—reader loading its program.","\n","ev","str","^Patient","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Impatient","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ \"Well?\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ \"For God's sake, man, what do you have to lose?\" ","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"We'll be outside the door,\" Harris replies, seriously. \"The first sign of any funny business and we'll have you both on the floor in minutes. You understand? The country needs your brain, but it's not too worried about your legs. Remember that.\"","\n","^Then he gets to his feet, and opens the door, and marches me out across the yard. The evening is drawing in and there's a chill in the air. My mind is racing. I have one opportunity here — a moment in which to put the fear of God into Hooper and make him do something foolish that places him in harm's way. But how to achieve it?","\n","^\"You ready?\" Harris demands.","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-4","flg":20},{"c-2":["\n","^\"Absolutely.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-3":["\n","^\"No.\"","\n","^\"Too bad.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-4":["^ ",{"->":".^.^.c-2"},"\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":[{"->":"inside_hoopers_hut"},{"#f":5}]}],{"#f":1}],"#f":1}],"harris_takes_you_to_hooper":[["^Harris gets to his feet. \"All right,\" he says. \"I should no better than to trust a clever man, but we'll give it a go.\"","\n","^Then, he smiles, with all his teeth, like a wolf.","\n","ev",{"CNT?":"claim_hooper_took_component.0.g-1.hoopers_hut_3"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^\"Especially since this is a plan that involves keeping you in handcuffs. I don't see what I have to lose.\"","\n",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["\n","^\"Hooper's in Hut 3 being debriefed by the Captain. Let's see if we can't get his attention somehow.\"","\n",{"->":".^.^.^.9"},null]}],"nop","\n","^He raps on the door for the guard and gives the man a quick instruction. He returns a moment later with a cool pair of iron cuffs.","\n","^\"Put 'em up,\" Harris instructs, and I do so. The metal closes around my wrists like a trap. I stand and follow Harris willingly out through the door.","\n","^But whatever I'm doing with my body, my mind is scheming. Somehow, I'm thinking, I have to get away from these men long enough to get that component behind Hut 2 and put it somewhere Hooper will go. Or, otherwise, somehow get Hooper to go there himself...","\n","^Harris marches me over to Hut 3, and gestures for the guard to stand aside. Pushing me forward, he opens the door nice and wide.","\n","^\"Captain. Manning talked. If you'd step out for a moment?\"","\n","ev","str","^Play the part, head down","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Look inside the hut","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Call to Hooper","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","^From where he's sitting, I know Hooper can see me, so I keep my head down and look guilty as sin. The bastard is probably smiling.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["\n","^I look in through the door and catch Hooper's expression. I had half expected him to be smiling be he isn't. He looks shocked, almost hurt. \"Iain,\" he murmurs. \"You couldn't...\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^I have a single moment to shout something to Hooper before the door closes.","\n","^\"I'll get you Hooper, you'll see!\" I cry. Then:","\n",[["ev",{"^->":"harris_takes_you_to_hooper.0.c-2.6.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"Queen to rook two, checkmate!\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"harris_takes_you_to_hooper.0.c-2.6.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"^ I call, then laugh viciously, as if I am damning him straight to hell.","\n","ev",2,"/ev",{"VAR=":"hooperClueType","re":true},{"->":".^.^.only_catch"},{"#f":5}],"only_catch":["^I only catch Hooper's reaction for a moment — his eyebrow lifts in surprise and alarm. Good. If he thinks it is a threat then he just might be careless enough to go looking for what it might mean.","\n",["ev",{"^->":"harris_takes_you_to_hooper.0.c-2.6.only_catch.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"Ask not for whom the bell tolls!\"",{"->":"$r","var":true},null]}],["ev",{"^->":"harris_takes_you_to_hooper.0.c-2.6.only_catch.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":18},{"s":["^\"Two words: messy, without one missing!\"",{"->":"$r","var":true},null]}],{"c-1":["ev",{"^->":"harris_takes_you_to_hooper.0.c-2.6.only_catch.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","^He stares back at me, as if were a madman and perhaps for a split second I see him shudder.","\n",{"->":".^.^.^.^.^.g-0"},{"#f":5}],"c-2":["ev",{"^->":"harris_takes_you_to_hooper.0.c-2.6.only_catch.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"^ I cry, laughing. It isn't the best clue, hardly worthy of The Times, but it will have to do.","\n","ev",3,"/ev",{"VAR=":"hooperClueType","re":true},{"->":".^.^"},{"->":".^.^.^.^.^.g-0"},{"#f":5}],"#f":5}]}],{"#f":5}],"g-0":["^The Captain comes outside, pulling the door to. \"What's this?\" he asks. \"A confession? Just like that?\"","\n","^\"No,\" the Commander admits, in a low voice. \"I'm afraid not. Rather more a scheme. The idea is to let Hooper go and see what he does. If he believes we have Manning here in irons, he'll try to shift the component.\"","\n","^\"If he has it.\"","\n","^\"Indeed.\"","\n","^The Captain peers at me for a moment, like I was some kind of curious insect.","\n","^\"Sometimes, I think you people are magicians,\" he remarks. \"Other times you seem more like witches. Very well.\"","\n","^With that he opens the door to the Hut and goes back inside. The Commander uses the moment to hustle me roughly forward.","\n","ev",{"CNT?":".^.^.c-2"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^\"And what was all that shouting about?\" he hisses in my ear as we move towards the barracks. \"Are you trying to pull something? Or just make me look incompetent?\"","\n",{"->":".^.^.^.19"},null]}],[{"->":".^.b"},{"b":["\n","^\"This scheme of yours had better come off,\" he hisses in my ear. \"Otherwise the Captain is going to start having men tailing me to see where I go on Saturdays.\"","\n",{"->":".^.^.^.19"},null]}],"nop","\n","ev","str","^Reassure","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Dissuade","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Say nothing","/str","/ev",{"*":".^.c-6","flg":20},{"c-3":["^ ","\n","ev",{"CNT?":".^.^.^.c-2"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"It will. Hooper's running scared,\" I reply, hoping I sound more confident than I feel.","\n",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["\n","^\"Just adding to the drama,\" I tell him, confidently. \"I'm sure you can understand that.\"","\n",{"->":".^.^.^.8"},null]}],"nop","\n","^\"I think we've had enough drama today already,\" Harris replies. \"Let's hope for a clean kill.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-4":["^ ","\n","ev",{"CNT?":".^.^.^.c-2"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"The Captain thought it was a good scheme. You'll most likely get a promotion.\"","\n",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["\n","^\"I'm not trying to do anything except save my neck.\"","\n",{"->":".^.^.^.8"},null]}],"nop","\n","^\"Let's hope things work out,\" Harris agrees darkly.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-5":["^ ","\n","^\"We're still in ear—shot if they let Hooper go. Best get us inside and then we can talk, if we must.\"","\n","^\"I've had enough of your voice for one day,\" Harris replies grimly. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-6":["\n","^I let him have his rant. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":["^He hustles me up the steps of the barracks, keeping me firmly gripped as if I had any chance of giving him, a trained military man, the slip. It's all I can do not to fall into the room.","\n",{"->":"slam_door_shut_and_gone"},{"#f":5}]}],{"#f":1}],"inside_hoopers_hut":[[["^Harris opens the door and pushes me inside. \"Captain,\" he calls. \"Could I have a moment?\"","\n","^The Captain, looking puzzled, steps out. The door is closed. Hooper stares at me, open—mouthed, about to say something. I probably have less than a minute before the Captain storms back in and declares this plan to be bunkum.","\n","ev","str","^Threaten","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Bargain","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Plead","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","^\"Listen to me, Hooper. We were the only men in that hut today, so we know what happened. But I want you to know this. I put the component inside a breeze—block in the foundations of Hut 2, wrapped in one of your shirts. They're going to find it eventually, and that's going to be what tips the balance. And there's nothing you can do to stop any of that from happening.\"","\n","ev",1,"/ev",{"VAR=":"hooperClueType","re":true},"^His eyes bulge with terror. \"What did I do, to you? What did I ever do?\"","\n",["ev","str","^Tell the truth","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^\"You treated me like vermin. Like something abhorrent.\"","\n","^\"You are something abhorrent.\"","\n","^\"I wasn't. Not when I came here. And I won't be, once you're gone.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"Nothing,\" I reply. \"You're just the other man in the room. One of us has to get the blame.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^\"It doesn't matter. Just remember what I said. I've beaten you, Hooper. Remember that.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^I get to my feet and open the door of the Hut. The Captain storms back inside and I'm quickly thrown out. ",{"->":".^.^.^.^.^.^.hustled_out"},"\n",{"#f":5}]}],{"#f":5}],"c-1":["^ ","\n","^\"Hooper, I'll make a deal with you. We both know what happened in that hut this afternoon. I know because I did it, and you know because you know you didn't. But once this is done I'll be rich, and I'll split that with you. I'll let you have the results, too. Your name on the discovery of the Bombe. And it won't hurt the war effort — you know as well as me that the component on its own is worthless, it's the wiring of the Bombe, the usage, that's what's valuable. So how about it?\"","\n","^Hooper looks back at me, appalled. \"You're asking me to commit treason?\"","\n",["ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["\n","^\"Yes, perhaps. But also to ensure your name goes down in the annals of mathematics. ",{"->":".^.^.^.^.^.^.back_of_hut_2"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"No. It's not treason. It's a trade, plain and simple.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^\"I'm suggesting you save your own skin. I've wrapped that component in one of your shirts, Hooper. They'll be searching this place top to bottom. They'll find it eventually, and when they do, that's the thing that will swing it against you. So take my advice now. Hut 2.\"","\n","ev",1,"/ev",{"VAR=":"hooperClueType","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ ",{"->":".^.^.c-2"},"\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":".^.^.^.^.^.^.no_chance"},{"#f":5}]}],{"#f":5}],"c-2":["^ ","\n","^\"Please, Hooper. You don't understand. They have information on me. I don't need to tell you what I've done, you know. Have a soul. And the component — it's nothing. It's not the secret of the Bombe. It's just a part. The German's think it's a weapon — a missile component. Let them have it. Please, man. Just help me.\"","\n","^\"Help you?\" Hooper stares. \"Help you? You're a traitor. A snake in the grass. And you're queer.\"","\n",["ev","str","^Deny","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Accept","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^\"I'm no traitor. You know I'm not. How much work have I done here against the Germans? I've given my all. And you know as well as I do, if the Reich were to invade, I would be a dead man. Please, Hooper. I'm not doing any of this lightly.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["\n","^\"I am what I am,\" I reply. \"I'm the way I was made. But they'll hang me unless you help, Hooper. Don't let them hang me.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^\"That's not important now. What matters is what you do, this evening.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"Assuming I wanted to help you,\" he replies, carefully. \"Which I don't. What would I do?\"","\n","^\"Nothing. Almost nothing.","\n",{"->":".^.^.^.^.^.^.back_of_hut_2"},{"#f":5}]}],{"#f":5}],"#f":5,"#n":"g-0"}],null],{"back_of_hut_2":["<>","^ All you have to do is go to the back of Hut 2. There's a breeze—block with a cavity. That's where I've put it. I'll be locked up overnight. But you can pick it up and pass it to my contact. He'll be at the south fence around two AM.\"","\n","ev",1,"/ev",{"VAR=":"hooperClueType","re":true},{"->":".^.^.no_chance"},{"#f":1}],"no_chance":["^\"If you think I'll do that then you're crazy,\" Hooper replies.","\n","^At that moment the door flies open and the Captain comes storming back inside.","\n",{"->":".^.^.hustled_out"},{"#f":1}],"hustled_out":["^Harris hustles me over to the barracks. \"I hope that's the end of it,\" he mutters.","\n","^\"Just be sure to let him out,\" I reply. \"And then see where he goes.\"","\n",{"->":"slam_door_shut_and_gone"},{"#f":1}],"#f":1}],"slam_door_shut_and_gone":[["^Then they slam the door shut, and it locks.","\n","ev",{"VAR?":"hooperClueType"},0,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","<>","^ How am I supposed to manage anything from in here?","\n","ev","str","^Try the door","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Try the windows","/str","/ev",{"*":".^.c-1","flg":20},{"->":".^.^.^.9"},{"c-0":["^ ",{"->":".^.^.^.^.^.try_the_door"},"\n",{"#f":5}],"c-1":["^ ",{"->":".^.^.^.^.^.try_the_windows"},"\n",{"#f":5}]}]}],[{"->":".^.b"},{"b":["\n","^I can only hope that Hooper bites. If he thinks I'm bitter enough to have framed him, and arrogant enough to have taunted him with ","ev",{"VAR?":"hooperClueType"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^a clue to",{"->":".^.^.^.8"},null]}],"nop","^ where the damning evidence is hidden...","\n","^If he hates me enough, and is paranoid enough, then he might ","ev",{"VAR?":"hooperClueType"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^unravel my little riddle and",{"->":".^.^.^.18"},null]}],"nop","^ go searching around Hut 2.","\n",{"->":".^.^.^.9"},null]}],"nop","\n","ev","str","^Wait","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["^ \t",{"->":"night_falls"},"\n",{"#f":5}]}],{"try_the_door":["^I try the door. It's locked, of course.","\n",{"->":".^.^.from_outside_heard"},{"#f":1}],"from_outside_heard":[["^From outside, I hear a voice. Hooper's. He's haranguing someone.","\n",["ev","str","^Listen at the keyhole","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Try the window","/str",{"CNT?":".^.^.^.^.try_the_windows"},"!","/ev",{"*":".^.c-1","flg":21},"ev","str","^Try the door","/str",{"CNT?":".^.^.^.^.try_the_door"},"!",{"CNT?":".^.c-0"},"&&","/ev",{"*":".^.c-2","flg":21},"ev","str","^Smash the window","/str",{"CNT?":".^.^.^.^.try_the_windows"},"/ev",{"*":".^.c-3","flg":21},"ev","str","^Wait","/str",{"CNT?":".^.^.^.^.try_the_door"},{"CNT?":".^.^.^.^.try_the_windows"},"&&","/ev",{"*":".^.c-4","flg":21},{"c-0":["^ ","\n","^I put my ear down to the keyhole, but there's nothing now. Probably still a guard outside, of course, but they're keeping mum.","\n",{"->":".^.^"},{"#f":5}],"c-1":["^ ",{"->":".^.^.^.^.^.try_the_windows"},"\n",{"#f":5}],"c-2":["^ ",{"->":".^.^.^.^.^.try_the_door"},"\n",{"#f":5}],"c-3":["^ ",{"->":".^.^.^.^.^.try_to_smash_the_window"},"\n",{"#f":5}],"c-4":["^ ","\n","^It's useless. There's nothing I can do but hope. I sit down on one corner of the bunk to wait.","\n",{"->":"night_falls"},{"#f":5}],"#f":5,"#n":"opts"}],null],{"#f":1}],"try_the_windows":["^I go over to the window and try to jimmy it open. Not much luck, but in my struggling I notice this window only backs on the thin little brook that runs down the back of the compound. Which means, if I smashed it, I might get away with no—one seeing.","\n",{"->":".^.^.from_outside_heard"},{"#f":1}],"try_to_smash_the_window":[["^The window is my only way out of here. I just need a way to smash it.","\n","ev","str","^Punch it","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Find something","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Use something you've got","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^I suppose my fist would do a good enough job. But I'd cut myself to ribbons, most likely. ","<>","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","ev",2,"/ev",{"VAR=":"smashingWindowItem","re":true},"^I cast around the small room. There's a bucket in one corner for emergencies — I suppose I could use that. I pick it up but it's not very easy to heft. ","<>","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^I pat down my pockets but all I'm carrying is the intercept, which is no good at all.","\n",["ev","str","^Something you're wearing?","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Look around","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^Ah, but of course! I slip off one shoe and heft it by the toe. The heel will make a decent enough hammer, if I give it enough wallop.","\n","ev",1,"/ev",{"VAR=":"smashingWindowItem","re":true},"^But I'll cut my hand to ribbons doing it. ","<>","\n",{"->":".^.^.^.^.g-0"},{"#f":5}],"c-1":["^ ",{"->":".^.^.^.^.c-1"},"\n",{"->":".^.^.^.^.g-0"},{"#f":5}]}],{"#f":5}],"g-0":["^And the noise would be terrible. There must be a way of making this easier. I'm supposed to be a thief now. What would a burglar do?","\n","ev","str","^Work slowly","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Find something to help","/str","/ev",{"*":".^.c-4","flg":20},{"c-3":["^ ","\n","^Work carefully? It's difficult to work carefully when all one's has is ","ev",{"VAR?":"smashingWindowItem"},2,"==","/ev",[{"->":".^.b","c":true},{"b":["^a bucket. It's rather like the sledgehammer for the proverbial nut",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["ev",{"VAR?":"smashingWindowItem"},1,"==","/ev",[{"->":".^.b","c":true},{"b":["^a shoe",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^nothing but brute force",{"->":".^.^.^.7"},null]}],"nop",{"->":".^.^.^.10"},null]}],"nop","^.","\n",["ev","str","^Just do it","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Look around for something","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ",{"->":".^.^.^.^.^.^.^.time_to_move_now"},"\n",{"->":".^.^.^.^.^.g-1"},{"#f":5}],"c-1":["^ ","\n",{"->":".^.^.^.^.^.g-1"},{"#f":5}]}],{"#f":5}],"c-4":["^ ","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":[{"->":".^.^.^.^.find_something_to_smash_window"},{"#f":5}]}],{"#f":1}],"time_to_move_now":[["^Enough of this. There isn't any time to lose. Right now they'll be following Hooper as he goes to bed, and goes to sleep; and then that's it. The minute he closes his eyelids and drifts off that's the moment that this trap swings shut on me.","\n","^So I punch out the glass with my ","ev",{"VAR?":"smashingWindowItem"},2,"==","/ev",[{"->":".^.b","c":true},{"b":["^bucket",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["ev",{"VAR?":"smashingWindowItem"},1,"==","/ev",[{"->":".^.b","c":true},{"b":["^shoe",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^fist",{"->":".^.^.^.7"},null]}],"nop",{"->":".^.^.^.10"},null]}],"nop","^ and it shatters with a terrific noise. Then I stop, and wait, to see if anyone will come in through the door.","\n","^Nothing.","\n","ev","str","^Wait a little longer","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Clear the frame of shards","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^I pause for a moment longer. It doesn't do to be too careless...","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["\n","^With my jacket wrapped round my arm, I sweep out the remaining shards of glass. It's not a big window, but I'm not a big man. If I was Harris, I'd be stuffed, but as it is...","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^Then the door locks turns. The door opens. Then Jeremy — one of the guards, rather — sticks his head through the door. \"I thought I heard...\"","\n","^He stops. Looks for a moment. ","ev",{"VAR?":"smashingWindowItem"},2,"==","/ev",[{"->":".^.b","c":true},{"b":["^Sees the bucket in my hand.",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^Sees the broken window.",{"->":".^.^.^.10"},null]}],"nop","^ Then without a moment's further thought he blows his shrill whistles and hustles into the hut, grabbing me roughly by my arms.","\n","ev",{"CNT?":".^.^.c-0"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^I'll never know if I hadn't have waited that extra moment — maybe I still could have got away. But, how far?","\n",{"->":".^.^.^.17"},null]}],"nop","\n","^I'm hustled into one of the huts. Nowhere to sleep, but they're not interested in my comfort any longer. Harris comes in with the Captain.","\n","^\"So,\" Harris remarks. \"Looks like your little trap worked. Only it worked to show you out for what you are.\"","\n","ev","str","^Tell the truth","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-4","flg":20},{"c-2":["^ ","\n","ev",{"CNT?":"i_met_a_young_man"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^\"Please, Harris. You can't understand the pressure they put me under. You can't understand what it's like, to be in love but be able to do nothing about it...\"","\n",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["\n","^\"Harris. They were blackmailing me. They knew about... certain indiscretions. You can understand, can't you, Harris? I was in an impossible bind...\"","\n",{"->":".^.^.^.7"},null]}],"nop","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-3":["\n","^\"I had to get out, Harris. I had to provoke Hooper into doing something that would incriminate himself fully. He's too clever, you see...\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-4":["^ ","\n","^\"This proves nothing,\" I reply stubbornly. \"You still don't have the component and without it, I don't see what you can hope to prove.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":["^\"Be quiet, man. We know all about your and your sordid affairs.\" The Captain curls his lip. \"Don't you know there's a war on? Do you know the kind of place they would have sent you if it haven't had been for that brain of yours? Don't you think you owe it to your country to use it a little more?\"","\n","^Do I, I wonder? Do I owe this country anything, this country that has spurned who and what am I since the day I became a man?","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-7","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-8","flg":20},{"c-5":["^ ","\n","^My anger deflates like a collapsing equation, all arguments cancelling each other out. The world, of course, owes me nothing; and I owe it everything.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-6":["^ ","\n","^Of course not. I am alone; that is what they wanted me to be, because of who and what I love. So I have no nation, no country.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-7":["^ \t",{"->":".^.^.c-6"},"\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-8":["^ \t","\n","^But what is a country, after all? A country is not a concept, not an ideal. Every country falls, its borders shift and move, its language disappears to be replaced by another. Neither the Reich nor the British Empire will survive forever, so what use is my loyalty to either? ","\n","^I may as well, therefore, look after myself. Something I have attempted, but failed miserably, to do.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"#f":5}],"g-2":["^\"I'm afraid we have only one option, Manning,\" Harris says. \"Please, man. Tell us where the component is.\"","\n","ev",true,"/ev",{"VAR=":"notraitor","re":true},"ev",false,"/ev",{"VAR=":"losttemper","re":true},"ev","str","^Tell them","/str","/ev",{"*":".^.c-9","flg":20},"ev","str","^Say nothing","/str","/ev",{"*":".^.c-10","flg":20},{"c-9":["\n","ev",false,"/ev",{"VAR=":"revealedhooperasculprit","re":true},"^\"All right.\" I am beaten, after all. \"","<>",{"->":"reveal_location_of_component"},"\n",{"#f":5}],"c-10":["^ ",{"->":"my_lips_are_sealed"},"\n",{"#f":5}],"#f":5}]}],{"#f":1}],"find_something_to_smash_window":[["^Let me see. There's the bunk, ","ev",{"VAR?":"smashingWindowItem"},"!",2,"==","/ev",[{"->":".^.b","c":true},{"b":["^a bucket,",{"->":".^.^.^.8"},null]}],"nop","^ nothing else. I have my jacket but nothing in the pockets — no handkerchief, for instance.","\n",["ev","str","^The bunk","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^The jacket","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^The bucket","/str",{"VAR?":"smashingWindowItem"},"!",2,"==","/ev",{"*":".^.c-2","flg":21},{"c-0":["^ \t","\n","^The bunk has a solid metal frame, a blanket, a pillow, nothing more.","\n",[["ev","str","^The frame","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^The blanket","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^The pillow","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Something else","/str",{"CNT?":".^"},1,">","/ev",{"*":".^.c-3","flg":21},{"c-0":["\n","^The frame is heavy and solid. I couldn't lift it or shift it without help from another man. And it wouldn't do me any good here anyway. I can reach the window perfectly well.","\n",{"->":".^.^"},{"#f":5}],"c-1":["^ ","\n","^The blanket. Perfect. I scoop it up off the bed and hold it in place over the window. ",{"->":"smash_the_window"},"\n",{"#f":5}],"c-2":["^ ","\n","^The pillow is fat and fluffy. I could put it over the window and it would muffle the sound of breaking glass, certainly; but I wouldn't be able to break any glass through it either.","\n",{"->":".^.^"},{"#f":5}],"c-3":["^ ",{"->":".^.^.^.^.^"},"\n",{"#f":5}],"#f":5,"#n":"bunk_opts"}],null],{"#f":5}],"c-1":["^ ","\n","^I slip off my jacket and hold it with one hand over the glass. ",{"->":"smash_the_window"},"\n",{"#f":5}],"c-2":["^ ","\n","^The bucket? Hardly. The bucket might do some good if I wanted to sweep up the glass afterwards, but it won't help me smash the glass quietly.","\n",{"->":".^.^"},{"#f":5}],"#f":5,"#n":"opts"}],null],{"#f":1}],"#f":1}],"smash_the_window":[["^Then I heft ","ev",{"VAR?":"smashingWindowItem"},2,"==","/ev",[{"->":".^.b","c":true},{"b":["^up the bucket — this really is quite a fiddly thing to be doing in cuffs — ",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["ev",{"VAR?":"smashingWindowItem"},1,"==","/ev",[{"->":".^.b","c":true},{"b":["^ my shoe by its toe, ",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^back my arm, ",{"->":".^.^.^.7"},null]}],"nop",{"->":".^.^.^.8"},null]}],"nop","^ and take a strong swing, trying to imagine it's Harris' face on the other side.","\n","ev",true,"/ev",{"VAR=":"smashedglass","re":true},"ev",0,"/ev",{"VAR=":"smashingWindowItem","re":true},"ev","str","^Smash!","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^The sound of the impact is muffled. With my arm still covered, I sweep out the remaining glass in the frame.","\n",["^I'm ready to escape. The only trouble is — when they look in on me in the morning, there will be no question what has happened. It won't help me one jot with shifting suspicion off my back.","\n","ev","str","^Wait","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Slip out","/str","/ev",{"*":".^.c-2","flg":20},{"c-1":["\n","^So perhaps I should wait it out, after all. Who knows? I might have a better opportunity later.","\n",{"->":"night_passes"},{"->":".^.^.^.^.g-2"},{"#f":5}],"c-2":["^ ","\n","^Moving quickly and quietly, I hoist myself up onto the window—frame and worm my way outside into the freezing night air. Then I am away, slipping down the paths between the Huts, sticking to the shadows, on my way to Hut 2.","\n",{"->":".^.^.^.^.g-2"},{"#f":5}],"#f":5,"#n":"g-1"}],{"#f":5}],"g-2":["ev","str","^Go the shortest way","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Take a longer route","/str","/ev",{"*":".^.c-4","flg":20},{"c-3":["^ ","\n","^There's no time to lose. Throwing caution to the wind I make my way quickly to Hut 2, and around the back. I don't think I've been seen but if I have it is too late. My actions are suspicious enough for the noose. I have no choice but to follow through.","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-4":["\n","^In case I'm being followed, I divert around the perimeter of the compound. It's a much longer path, and it takes me across some terrain that's difficult to negotiate in the dark — muddy, and thick with thistles and nestles.","\n","ev",true,"/ev",{"VAR=":"muddyshoes","re":true},"^Still, I can be confident no—one is behind me. I crouch down behind the rear wall of Hut 2. ","<>","\n",{"->":".^.^.^.g-3"},{"#f":5}],"#f":5}],"g-3":["^The component is still there, wrapped in a tea—towel and shoved into a cavity in a breeze—block at the base of the Hut wall.","\n","ev","str","^Take it","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Leave it","/str","/ev",{"*":".^.c-6","flg":20},{"c-5":["^ ","\n","^Quickly, I pull it free, and slip it into the pocket of my jacket.","\n","ev",true,"/ev",{"VAR=":"gotcomponent","re":true},{"->":".^.^.^.g-4"},{"#f":5}],"c-6":["^ ","\n","^Still there means no—one has found it, which means it is probably well—hidden. And short of skipping the compound now, I can afford to leave it hidden there a while longer. So I leave it in place.","\n",{"->":".^.^.^.g-4"},{"#f":5}],"#f":5}],"g-4":["^Where now?","\n","ev","str","^Back to the barracks","/str","/ev",{"*":".^.c-7","flg":20},"ev","str","^Go to Hooper's dorm","/str",{"VAR?":"gotcomponent"},"/ev",{"*":".^.c-8","flg":21},"ev","str","^Escape the compound","/str","/ev",{"*":".^.c-9","flg":20},{"c-7":["^ ",{"->":"return_to_room_after_excursion"},"\n",{"#f":5}],"c-8":["^ ",{"->":"go_to_hoopers_dorm"},"\n",{"#f":5}],"c-9":["^ ","\n","^Enough of this place. Time for me to get moving. I can get to the train station on foot, catch the postal train to Scotland and be somewhere else before anyone realises that I'm gone.","\n","^Of course, then they'll be looking for me in earnest. ","ev",{"VAR?":"framedhooper"},"!","/ev",[{"->":".^.b","c":true},{"b":["^As a confirmed traitor.",{"->":".^.^.^.11"},null]}],[{"->":".^.b"},{"b":["^Perhaps not as a traitor — they might take the idea that Hooper was involved with the theft — but certainly as a valuable mind, one containing valuable secrets and all too easily threatened. They will think I am running away because of my indiscretions. I suppose, in fairness, that I am.",{"->":".^.^.^.11"},null]}],"nop","\n",["ev","str","^Go","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't go","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ \t\t\t",{"->":"live_on_the_run"},"\n",{"#f":5}],"c-1":["^ ","\n","^It's no good. That's only half a solution. I couldn't be happy with that.","\n",["ev","str","^Back to the barracks","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^To Hooper's dorm","/str",{"VAR?":"gotcomponent"},{"CNT?":"go_to_hoopers_dorm"},"!","&&","/ev",{"*":".^.c-1","flg":21},{"c-0":["^ \t\t\t",{"->":"return_to_room_after_excursion"},"\n",{"#f":5}],"c-1":["^ ",{"->":"go_to_hoopers_dorm"},"\n",{"#f":5}]}],{"#f":5}]}],{"#f":5}],"#f":5}]}],{"#f":1}],"go_to_hoopers_dorm":[["^I creep around the outside of the huts towards Hooper's dorm. Time to wrap up this little game once and for all. A few guards patrol the area at night but not many — after all, very few know this place even exists.","\n","^Our quarters are arranged away from the main house; where we sleep is of less importance than where we work. We each have our own hut, through some are less permanent than others. Hooper's is a military issue tent: quite a large canopy, with two rooms inside and a short porch area where he insists people leave their shoes. It's all zipped up for the night and no light shines from inside.","\n","^I hang back for a moment. If Harris is keeping to the terms of our deal then someone will be watching this place. But I can see no—one.","\n","ev","str","^Open the outer zip","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Look for another opening","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Hide the component somewhere","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^I creep forward to the tent, intent on lifting the zip to the front porch area just a little — enough to slip the component inside, and without the risk of the noise waking Hooper from his snoring.","\n","^The work is careful, and more than little fiddly — Hooper has tied the zips down on the inside, the fastidious little bastard! — but after a little work I manage to make a hole large enough for my hand.","\n",["ev","str","^Slip in the component","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No, some other way","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ \t\t","\n","^I slide the component into the tent, work the zip closed, and move quickly away into the shadows. It takes a few minutes for my breath to slow, and my heart to stop hammering, but I see no other movement. If anyone is watching Hooper's tent, they are asleep at their posts.","\n","ev",true,"/ev",{"VAR=":"putcomponentintent","re":true},"ev",false,"/ev",{"VAR=":"gotcomponent","re":true},{"->":"return_to_room_after_excursion"},{"#f":5}],"c-1":["^ \t\t\t","\n","^Then pause. This is too transparent. Too blatant. If I leave it here, like this, Hooper will never be seen to go looking for it: he will stumble over it in plain sight, and the men watching will wonder why it was not there when he went to bed.","\n","^No, I must try something else — or nothing at all.","\n",["ev","str","^On top of the tent","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Throw the component into the long grass","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Give up","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ",{"->":".^.^.^.^.^.^.^.put_component_on_tent"},"\n",{"#f":5}],"c-1":["^ ","\n","^From inspiration — or desperation, I am not certain — a simple approach occurs to me. ",{"->":".^.^.^.^.^.^.^.toss_component_into_bushes"},"\n",{"#f":5}],"c-2":["^ ","\n","^There is nothing to be gained here. I have the component now; maybe it will be of some value tomorrow.","\n",["ev","str","^Return to my barrack","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Escape the compound","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ",{"->":"return_to_room_after_excursion"},"\n",{"#f":5}],"c-1":["^ ",{"->":"live_on_the_run"},"\n",{"#f":5}]}],{"#f":5}]}],{"#f":5}]}],{"#f":5}],"c-1":["^ ","\n","^Making a wide circuit I creep around the tent. It has plenty of other flaps and openings, tied down with Gordian complexity. But nothing afford itself to slipping the component inside.","\n",["ev","str","^Try the porch zip","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Try on top of the tent","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Give up","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ \t\t\t",{"->":".^.^.^.^.c-0"},"\n",{"#f":5}],"c-1":["^ \t\t",{"->":".^.^.^.^.^.put_component_on_tent"},"\n",{"#f":5}],"c-2":["^ \t\t\t\t\t\t","\n","^It's no good. Nothing I can do will be any less than obvious — something appearing where something was not there before. The men watching Hooper will know it is a deception and Hooper's protestations will be taken at face value.","\n","^If I can't find a way for Hooper to pick the component up, as if from a hiding place of his own devising, and be caught doing it, then I have no plan at all.","\n",["ev","str","^Return to my barrack","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Escape the compound","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Toss the component into the bushes","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ",{"->":"return_to_room_after_excursion"},"\n",{"#f":5}],"c-1":["^ ",{"->":"live_on_the_run"},"\n",{"#f":5}],"c-2":["^ ",{"->":".^.^.^.^.^.^.^.toss_component_into_bushes"},"\n",{"#f":5}]}],{"#f":5}]}],{"#f":5}],"c-2":["^ ","\n","^If I leave the component here somewhere it should be somewhere I can rely on Hooper finding it, but no—one before Hooper. In particular.","\n",["ev","str","^Behind the tent","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Inside the porch section","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^On top of the canvas","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^\t\t\t \t",{"->":".^.^.^.^.c-1"},"\n",{"#f":5}],"c-1":["^ \t\t",{"->":".^.^.^.^.c-0"},"\n",{"#f":5}],"c-2":["^ \t\t\t",{"->":".^.^.^.^.^.put_component_on_tent"},"\n",{"#f":5}]}],{"#f":5}]}],{"put_component_on_tent":["^A neat idea strikes me. If I could place it on top of the canvas, somewhere in the middle where it would bow the cloth inwards, then it would be invisible to anyone passing by. But to Hooper, it would be above him: a shadow staring him in the face as he awoke. What could be more natural than getting up, coming out, and looking to see what had fallen on him during the night?","\n","^It's the work of a moment. I was once an excellent bowler for the second XI back at school. This time I throw underarm, of course, but I still land the vital missing component exactly where I want it to go.","\n","ev",true,"/ev",{"VAR=":"framedhooper","re":true},"ev",false,"/ev",{"VAR=":"gotcomponent","re":true},"^For a second I hold my breath, but nothing and no—one stirs. ",{"->":"return_to_room_after_excursion"},"\n",{"#f":1}],"toss_component_into_bushes":["^I toss the component away into the bushes behind Hooper's tent and return to my barrack, wishing myself a long sleep followed by a morning, free of this business.","\n","ev",false,"/ev",{"VAR=":"gotcomponent","re":true},"ev",true,"/ev",{"VAR=":"throwncomponentaway","re":true},{"->":"return_to_room_after_excursion"},{"#f":1}],"#f":1}],"live_on_the_run":["^Better to live on the run than die on the spit. Creeping around the edge of the compound","ev",{"VAR?":"gotcomponent"},"/ev",[{"->":".^.b","c":true},{"b":["^, the Bombe component heavy in my pocket",{"->":".^.^.^.5"},null]}],"nop","^, I make my way to the front gate. As always, it's manned by two guards, but I slip past their box by crawling on my belly.","\n","^And then I'm on the road. Walking, not running. Silent. Free.","\n","^For the moment, at least.","\n","end",{"#f":1}],"return_to_room_after_excursion":[["ev",{"VAR?":"gotcomponent"},"/ev",[{"->":".^.b","c":true},{"b":["^The weight of the Bombe component safely in my jacket",{"->":".^.^.^.5"},null]}],[{"->":".^.b"},{"b":["^Satisfied",{"->":".^.^.^.5"},null]}],"nop","^, I return the short way up the paths between the huts to the barrack block and the broken window.","\n","^It's a little harder getting back through — the window is higher off the ground than the floor inside — but after a decent bit of jumping and hauling I manage to get my elbows up, and then one leg, and finally I collapse inside, quite winded and out breath.","\n","ev","str","^Wait","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["^ \t",{"->":"night_passes"},"\n",{"#f":5}]}],{"#f":1}],"night_passes":[["^The rest of the night passes slowly. I sleep a little, dozing mostly. Then I'm woken by the rooster in the yard. The door opens, and Harris comes in. He takes one look at the broken window and frowns with puzzlement.","\n","ev",{"VAR?":"putcomponentintent"},"/ev",[{"->":".^.b","c":true},{"b":["^ ",{"->":".^.^.^.^.put_component_inside_tent"},{"->":".^.^.^.6"},null]}],"nop","\n","^\"What happened there?\"","\n","ev","str","^Confess","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Deny","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Show him the component","/str",{"VAR?":"gotcomponent"},"/ev",{"*":".^.c-2","flg":21},{"c-0":["^ ","\n","^\"I broke it,\" I reply. There doesn't seem any use in trying to lie. \"I thought I could escape. But I couldn't get myself through.\"","\n","^The Commander laughs. ",{"->":".^.^.^.glad_youre_here"},"\n",{"#f":5}],"c-1":["^ ","\n","^\"I'm not sure. I was asleep: I woke up when someone broke the window. I looked out to see who it was, but they were already gone.\"","\n","^Harris looks at me with puzzlement. \"Someone came by to break the window, and then ran off? That's absurd. That's utterly absurd. Admit it, Manning. You tried to escape and you couldn't get through.\"","\n",["ev","str","^Admit it","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Deny it","/str",{"VAR?":"framedhooper"},"!","/ev",{"*":".^.c-1","flg":21},"ev","str","^Deny it","/str",{"VAR?":"framedhooper"},"/ev",{"*":".^.c-2","flg":21},{"c-0":["\n","^\"All right. ","ev",{"VAR?":"forceful"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^Damn you.",{"->":".^.^.^.8"},null]}],"nop","^ That's exactly it.\"","\n",{"->":".^.^.^.^.^.glad_youre_here"},{"#f":5}],"c-1":["\n","^\"If I wanted to escape, I would have made damn sure that I could,\" I tell him sternly.","\n",{"->":"harris_certain_is_you"},{"#f":5}],"c-2":["^ ","\n","^\"I tell you, someone broke it. Someone wanted to threaten me, I think.\"","\n","^Harris shakes his head. \"Well, we can look into that matter later. For now, you probably want to hear the more pressing news. ",{"->":".^.^.^.^.^.found_missing_component"},"\n",{"#f":5}]}],{"#f":5}],"c-2":["^ ",{"->":".^.^.^.someone_threw_component"},"\n",{"#f":5}]}],{"put_component_inside_tent":[["^He takes one look around, and sighs, a deep, wistful sigh.","\n","^\"Things just get worse and worse for you, Manning,\" he remarks. \"You are your own worst enemy.\"","\n","ev","str","^Agree","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Disagree","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^\"I've thought so before.\" ","ev",{"VAR?":"admitblackmail"},"/ev",[{"->":".^.b","c":true},{"b":["^Certainly in the matter of getting blackmailed.",{"->":".^.^.^.7"},null]}],"nop","\n","^\"Let me tell you what happened this morning. ","<>","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["\n","^\"Right now, I think you take that role, Harris,\" I reply coolly.","\n",[["^\"Very droll,\" he replies. \"Let me tell you what happened this morning. It will take the smile off your face. ","<>","\n",{"->":".^.^.^.^.g-0"},{"#f":5,"#n":"droll"}],null],{"#f":5}],"c-2":["^ ","\n","^\"I'm looking forward to having a wash and a change of clothes; which should make me a little less evil to be around.\"","\n",{"->":".^.^.c-1.3.droll"},{"->":".^.^.g-0"},{"#f":5}],"g-0":["^Our men watching Hooper's tent saw Hooper wake up, get dressed, clamber out of his tent and then step on something in at the entrance of his tent.\"","\n","ev",true,"/ev",{"VAR=":"piecereturned","re":true},"ev","str","^Be interested","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Be dismissive","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Say nothing","/str","/ev",{"*":".^.c-5","flg":20},{"c-3":["^ ","\n","^\"You mean he didn't even hide it? He put it in his shoe?\"","\n",[["^\"No,\" Harris replies. \"That isn't really what I mean. ","<>","\n",{"->":".^.^.^.^.^.g-1"},{"#f":5,"#n":"not_that"}],null],{"#f":5}],"c-4":["\n","^\"So he's an idiot, and he hid it in his shoe.\"","\n",{"->":".^.^.c-3.4.not_that"},{"->":".^.^.^.g-1"},{"#f":5}],"c-5":["^ ","\n","^I say quiet, listening, not sure how this will go.","\n","^\"In case I'm not making myself clear,\" Harris continues, \"","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":["^I mean, he managed to find it, by accident, somewhere where it wasn't the night before. And at the same time, you're sitting here with your window broken. So, I rather think you've played your last hand and lost. It's utterly implausible that Hooper stole that component and then left it lying around in the doorway of his tent. So I came to tell you that the game is up, for you.\"","\n","^He nods and gets to his feet. ",{"->":"left_alone"},"\n",{"#f":5}]}],{"#f":1}],"someone_threw_component":[["^\"Someone threw this in through the window over night,\" I reply, and open my jacket to reveal the component from the Bombe. \"I couldn't see who, it was too dark. But I know what it is.\"","\n","^He reaches out and takes it. \"Well, I'll be damned,\" he murmurs. \"That's it all right. And you didn't have it on you when we put you in here. But it can't have been Hooper — I had men watching him all night. And there's no—one else it could have been.\"","\n","^He turns the component over in his hands, bemused.","\n","ev",true,"/ev",{"VAR=":"piecereturned","re":true},"ev","str","^Suggest something","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Suggest nothing","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"Perhaps Hooper had an accomplice. Someone else who works on site.\"","\n","^Harris shakes his head, distractedly. \"That doesn't make sense,\" he says. \"Why go to all the trouble of stealing it only to give it back? And why like this?\"","\n",["ev","str","^Suggest something","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Suggest nothing","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"Perhaps the accomplice thought it was Hooper being kept in here. Maybe they saw the guard...\"","\n",{"->":"all_too_farfetched"},{"->":".^.^.^.^.g-0"},{"#f":5}],"c-1":["^ ","\n",{"->":".^.^.^.^.g-0"},{"#f":5}]}],{"#f":5}],"c-1":["^ ","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^I shrug, eloquently.","\n",[{"->":"all_too_farfetched"},{"#f":5,"#n":"g-1"}],{"#f":5}]}],{"#f":1}],"glad_youre_here":[["^\"Shame,\" he remarks. \"I should have left that window open and put a guard on you. Might have been interesting to see where you went. Anyway, I'm glad you're still here, even if you do smell like a dog.\"","\n","ev","str","^Be optimistic","/str",{"VAR?":"framedhooper"},"!","/ev",{"*":".^.c-0","flg":21},"ev","str","^Be pessimistic","/str",{"VAR?":"framedhooper"},"!","/ev",{"*":".^.c-1","flg":21},"ev","str","^Be optimistic","/str",{"VAR?":"framedhooper"},"/ev",{"*":".^.c-2","flg":21},"ev","str","^Be pessimistic","/str",{"VAR?":"framedhooper"},"/ev",{"*":".^.c-3","flg":21},{"c-0":["^ ","\n",{"->":"night_falls.morning_not_saved.0.c-0"},{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n",{"->":"night_falls.morning_not_saved.0.c-1"},{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^\"I'm looking forward to having a bath.\"","\n","^\"Well, you should enjoy it. ","<>","\n",{"->":".^.^.g-0"},{"#f":5}],"c-3":["\n","^\"I imagine I'll smell worse after another couple of days of this.\"","\n","^\"That won't be necessary. ","<>","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":".^.^.^.^.found_missing_component"},{"#f":5}]}],{"#f":1}],"found_missing_component":[["^We found the missing component. Or rather, Hooper found it for us. He snuck out and retrieved it from on top. Of all the damnest places — you would never have known it was there. He claimed ignorance when we jumped him, of course. But it's good enough for me.\"","\n","ev","str","^Approve","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Disapprove","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"I can't tell you enough, I'm glad to hear it. I've had a devil of a night.\"","\n","^His gaze flicks to the broken window, but only for a moment. I think he genuinely cannot believe I could have done it.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"You should never have hired him. A below-average intelligence can't be expected to cope with the pressure of our work.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^Harris rolls his eyes, but he might almost be smiling. \"You'd better get along, ","ev",{"CNT?":".^.^.c-0"},"/ev",[{"->":".^.b","c":true},{"b":["^and work through your devils",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["^Mr Intelligent",{"->":".^.^.^.6"},null]}],"nop","^. There's a 24—hour—late message to be tackled and we're a genius short. So you'd better be ready to work twice as hard.\"","\n","ev","str","^Thank him","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Argue with him","/str","/ev",{"*":".^.c-3","flg":20},{"c-2":["^ \t","\n","^\"I'll enjoy it. Thank you for helping me clear this up.\"","\n","^\"Don't thank me yet. There's still a war to fight. Now get a move on.\"","\n","^I nod, and hurry out of the door. The air outside has never tasted fresher and more invigorating. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-3":["^ ","\n","^\"I'll work as hard as I work.\"","\n","^\"Get out,\" Harris growls. \"Before I decide to arrest you as an accessory.\"","\n","^I do as he says. Outside the barrack, the air has never smelt sweeter.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":[{"->":"head_for_my_dorm_free"},{"#f":5}]}],{"#f":1}],"#f":1}],"night_falls":[["^Night falls. The clockwork of the heavens keeps turning, whatever state I might be in. No—one can steal the components that make the sun go down and the stars come out. I watch it performing its operations. I can't sleep.","\n","ev",{"VAR?":"hooperClueType"},0,">","/ev",[{"->":".^.b","c":true},{"b":["\n","^Has Hooper taken my bait?","\n",{"->":".^.^.^.8"},null]}],"nop","\n","ev","str","^Look of out the window","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Listen at the door","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Wait","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^I peer out of the window, but it looks out onto the little brook at the back of the compound, with no view of the other huts or the House. Who knows if there are men up, searching the base of Hut 2, following one another with flashlights...","\n","ev",{"CNT?":"inside_hoopers_hut.back_of_hut_2"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^Perhaps Hooper is there, in the dark, trying to help me after all?","\n",{"->":".^.^.^.8"},null]}],"nop","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ \t","\n","^I put my ear to the keyhole but can make out nothing. Are there still guards posted? ","ev",{"VAR?":"hooperClueType"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^Perhaps, if Hooper has managed to incriminate himself, the guards have been removed?",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^Perhaps the component has been found and the crisis is over.",{"->":".^.^.^.10"},null]}],"nop","\n","^Perhaps the door is unlocked and they left me to sleep?","\n",["ev","str","^Try it","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Leave it","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ I try the handle. No such luck.","\n",{"->":".^.^.^.^.g-0"},{"#f":5}],"c-1":["^ I don't touch it. I don't want anyone outside thinking I'm trying to escape.","\n",{"->":".^.^.^.^.g-0"},{"#f":5}]}],{"#f":5}],"c-2":["^ \t\t\t\t\t","\n","^There is nothing I can do to speed up time.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^The night moves at its own pace. I suppose by morning I will know my fate.","\n","ev","str","^Wait","/str",{"VAR?":"hooperClueType"},0,">","/ev",{"*":".^.c-3","flg":21},"ev","str","^Wait","/str",{"VAR?":"hooperClueType"},0,"==","/ev",{"*":".^.c-4","flg":21},{"c-3":["^ ","\n","^Morning comes. I'm woken by a rooster calling from the yard behind the House. I must have slept after all. I pull myself up from the bunk, shivering slightly. There is condensation on the inside of the window. I have probably given myself a chill.","\n","^Without knocking, Harris comes inside. \"You're up,\" he remarks, and then, \"You smell like an animal.\"","\n",["ev","str","^Be friendly","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Be cold","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"I suppose I do rather.\" I laugh, but Harris does not.","\n","^\"This damn business gets worse and worse,\" he says, talking as he goes over to unlock and throw open the window. ","<>","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"So would you,\" I reply tartly. Harris shrugs.","\n","^\"I've been through worse than this,\" he replies matter—of—factly. \"It's hardly my fault if you sleep in your clothes.\"","\n","^I glare back. He goes over to the window, unlocks it and throws it open, relishing the fresh air from outside.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"Hooper's confessed, you know.\"","\n","ev","str","^Be eager","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Be cautious","/str","/ev",{"*":".^.c-3","flg":20},{"c-2":["^ ","\n","^\"He has? I knew he would. The worm.\"","\n","^\"Steady now. Matters aren't over yet. ","<>","\n",{"->":".^.^.^.hooper_didnt_give_himself_up"},{"#f":5}],"c-3":["^ ","\n","^\"Oh, yes?\"","\n","^\"Yes. For what that's worth. ","<>","\n",{"->":".^.^.^.hooper_didnt_give_himself_up"},{"#f":5}],"#f":5}],"hooper_didnt_give_himself_up":["^There's still the issue of the component. It hasn't turned up. He didn't lead us to it. I guess he figured you must have had something on him. I don't know.\"","\n","^He looks quite put out by the whole affair. He is not the kind of man to deal well with probabilities.","\n","ev","str","^Be interested","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Be disinterested","/str","/ev",{"*":".^.c-5","flg":20},{"c-4":["^ ","\n","^\"You mean he confessed of his own accord? You didn't catch him?\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-5":["^ ","\n","^\"Well, I'm glad his conscience finally caught up with him,\" I reply dismissively.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":["^\"The Captain went back into that hut and he confessed immediately. We were so surprised we didn't let you go.\" He wrinkles his nose. \"I'm rather sorry about that now. I suggest you have a wash.\"","\n","^And with that he gestures to the doorway.","\n","ev","str","^Go","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Wait","/str","/ev",{"*":".^.c-7","flg":20},{"c-6":["^ ","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-7":["^ ","\n","^I hang back a moment. Something does not seem quite right. After all, Hooper did not steal the component. He has no reason to confess to anything. Perhaps this is another trap?","\n","^\"Well?\" Harris asks. \"What are you waiting for? Please don't tell me you want to confess now as well, I don't think my head could stand it.\"","\n",["ev","str","^Confess","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't confess","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^After a chance like this? A chance — however real — to save my neck? To hand it over — what, to save Hooper's worthless skin?","\n",["ev","str","^Confess","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't confess","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^I see. Perhaps you think I bullied the man into giving himself up. Perhaps he understood my little clue far enough to know it was a threat against him, but not well enough to understand where he should look to find it. So he took the easy route out and folded. Gave me the hand.","\n","ev",true,"/ev",{"VAR=":"hooperConfessed","re":true},"^Hardly sporting, of course.","\n",["ev","str","^Confess","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't confess","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^Well, then. I suppose this must be what it feels like to have a conscience. I suppose I had always wondered.","\n","^\"Harris, sir. I don't know what Hooper's playing at, sir. But I can't let him do this.\"","\n","^\"Do what?\"","\n","^\"Take the rope for this. I took it, sir.","\n","ev",false,"/ev",{"VAR=":"revealedhooperasculprit","re":true},"ev",false,"/ev",{"VAR=":"losttemper","re":true},{"->":"reveal_location_of_component"},{"->":".^.^.^.^.^.^.g-0"},{"#f":5}],"c-1":["^ ","\n",{"->":".^.^.^.^.^.^.g-0"},{"#f":5}]}],{"#f":5}],"c-1":["^ ","\n",{"->":".^.^.^.^.g-0"},{"#f":5}]}],{"#f":5}],"c-1":["^ ","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"I certainly don't. But still, I'm surprised. I had Hooper down for a full—blown double agent, a traitor. He knows he'll face the rope, doesn't he?\"","\n","^\"Don't ask me to explain why he did what he did,\" Harris sighs. \"Just be grateful that he did, and you're now off the hook.\"","\n",{"->":".^.^.^.^.^.g-2"},{"#f":5}]}],{"#f":5}],"#f":5}],"g-2":["^Curiouser and curiouser. I nod once to Harris and slip outside into the cold morning air.","\n","ev",{"VAR?":"hooperClueType"},0,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","^Hooper's confession only makes sense in one fashion","ev",{"VAR?":"hooperConfessed"},"/ev",[{"->":".^.b","c":true},{"b":["^, and that is his being dim—witted and slow",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^ — if I successfully implied to him that I had him framed, but he did not unpack my little clue well enough to go looking for the component. Well, I had figured him for a more intelligent opponent, but a resignation from the game will suffice",{"->":".^.^.^.7"},null]}],"nop","^. Or perhaps he knew he would be followed if he went to check, and decided he would be doomed either way.","\n",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["\n","^Hooper's confession only makes sense in one way — and that's that he believed me. He reasoned that he would be followed. To try and uncover the component would have got him arrested, and to confess was the same.","\n","^He simply caved, and threw in his hand.","\n",{"->":".^.^.^.9"},null]}],"nop","\n","^Of course, however, there is only one way to be certain that Harris is telling the truth, and that is to check the breeze—block at the back of Hut 2.","\n","ev","str","^Check","/str","/ev",{"*":".^.c-8","flg":20},"ev","str","^Don't check","/str","/ev",{"*":".^.c-9","flg":20},{"c-8":["^ ",{"->":"go_to_where_component_is_hidden"},"\n",{"#f":5}],"c-9":["\n","^But there will time for that later. If there is nothing there, then Hooper discovered the component after all and Harris' men will have swooped on him, and the story about his confession is just a ruse to test me out.","\n","^And if the component is still there — well. It will be just as valuable to my contact in a week's time, and his deadline of the 31st is not yet upon us.","\n",{"->":"head_for_my_dorm_free"},{"#f":5}],"#f":5}]}],{"#f":5}],"c-4":["^ ",{"->":".^.^.^.^.morning_not_saved"},"\n",{"#f":5}],"#f":5}]}],{"morning_not_saved":[["^Morning comes with the call of a rooster from the yard of the House. I must have slept after all. I pull myself up off the bunk, shivering slightly. There is condensation on the inside of the window. I have probably given myself a chill.","\n","^It's not long after that Harris enters the hut. He closes the door behind him, careful as ever, then takes a chair across from me.","\n","^\"You smell like a dog,\" he remarks.","\n","ev","str","^Be optimistic","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Be pessimistic","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"I'm looking forward to a long bath,\" I reply. \"And getting back to work.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"So would you after the night I've had.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"harris_certain_is_you"},{"#f":5}]}],{"#f":1}],"#f":1}],"harris_certain_is_you":["^\"Well, I'm afraid it is going to get worse for you,\" Harris replies soberly. \"We followed Hooper, and he took himself neatly to bed and slept like a boy scout. Which puts us back to square one, and you firmly in the frame. And I'm afraid I don't have time for any more games. I want you to tell me where that component is, or we will hang you as a traitor.\"","\n","ev",false,"/ev",{"VAR=":"revealedhooperasculprit","re":true},"ev",false,"/ev",{"VAR=":"losttemper","re":true},{"->":"harris_threatens_lynching"},{"#f":1}],"head_for_my_dorm_free":[["^I head for my dorm, intent on a bath, breakfast, a glance at the crossword before the other men get to it, and then on with work. They should have replaced the component in the Bombe by now. We will only be a day behind.","\n","ev",{"VAR?":"framedhooper"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^And then everything will proceed as before. The component will mean nothing to the Germans — this is the one fact I could never have explained to a man like Harris, even though the principle behind the Bombe is the same as the principle behind the army. The individual pieces — the men, the components — do not matter. They are identical. It is how they are arranged that counts.","\n",{"->":".^.^.^.7"},null]}],"nop","\n","^I bump into Russell in the dorm hut.","\n","^\"Did you hear?\" he whispers. \"Terrible news about Hooper. Absolutely terrible.\"","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["^ ","\n","^\"Quite terrible. I would never have guessed.\"","\n","^\"Well.\" Russell harrumphs.","\n",[["^\"Quince was saying this morning, apparently his grandfather was German. So perhaps it's to be expected. See you there?\"","\n",{"->":".^.^.^.^.g-0"},{"#f":5,"#n":"quince"}],null],{"#f":5}],"c-1":["\n","^\"Heard what?\"","\n",[["^\"Hooper's been taken away. They caught him, uncovering that missing Bombe component from a hiding place somewhere, apparently about to take it to his contact.\" Russell harrumphs. ",{"->":".^.^.^.^.c-0.6.quince"},"\n",{"->":".^.^.^.^.g-0"},{"#f":5,"#n":"hooper_taken"}],null],{"#f":5}],"c-2":["^ ","\n","^\"I don't know what you're talking about.\"","\n",{"->":".^.^.c-1.3.hooper_taken"},{"->":".^.^.g-0"},{"#f":5}],"c-3":["\n","^\"If you'll excuse me, Russell. I was about to take a bath.\"","\n","^\"Oh, of course. Worked all night, did you? Well, you'll hear soon enough. Can hardly hide the fact there'll only be three of us from now on.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^I wave to him and move away, my thoughts turning to the young man in the village. My lover. My contact. My blackmailer. Hooper may have taken the fall for the missing component, but ","ev",{"VAR?":"framedhooper"},"!","/ev",[{"->":".^.b","c":true},{"b":["^if he did recover it from Hut 2 then ",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^ its recovery does mean ",{"->":".^.^.^.7"},null]}],"nop","^I have nothing to sell to save my reputation","ev",{"CNT?":"i_met_a_young_man"},"/ev",[{"->":".^.b","c":true},{"b":["^, if I have any left",{"->":".^.^.^.13"},null]}],"nop","^.","\n","ev",{"VAR?":"framedhooper"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^If he didn't, of course, and Harris was telling the truth about his sudden confession, then I will be able to buy my freedom once and for all.","\n",{"->":".^.^.^.21"},null]}],"nop","\n","ev","str","^Get the component","/str",{"VAR?":"framedhooper"},"!","/ev",{"*":".^.c-4","flg":21},"ev","str","^Leave it","/str",{"VAR?":"framedhooper"},"!","/ev",{"*":".^.c-5","flg":21},"ev","str","^Act normal","/str","/ev",{"*":".^.c-6","flg":20},{"c-4":["^ ",{"->":"go_to_where_component_is_hidden"},"\n",{"#f":5}],"c-5":["^ ","\n","^I will have to leave that question for another day. To return there now, when they're probably watching my every step, would be suicide. After all, if Hooper ","ev",{"VAR?":"hooperClueType"},1,"==","/ev",[{"->":".^.b","c":true},{"b":["^followed",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^understood",{"->":".^.^.^.10"},null]}],"nop","^ my clue, he will have explained it to them to save his neck. They won't believe him — but they won't quite disbelieve him either. We're locked in a cycle now, him and me, of half—truth and probability. There's nothing either of us can do to put the other entirely into blame.","\n",{"->":"ending_return_to_normal"},{"#f":5}],"c-6":["^ ","\n","^But there is nothing to be done about it. ",{"->":"ending_return_to_normal"},"\n",{"#f":5}],"#f":5}]}],{"#f":1}],"ending_return_to_normal":[["^Nothing, that is, except to act as if there is no game being played. I'll have a bath, then start work as normal. I've got a week to find something to give my blackmailer","ev",{"CNT?":"i_met_a_young_man"},"/ev",[{"->":".^.b","c":true},{"b":["^ — or give him nothing: it seems my superiors know about my indiscretions now already",{"->":".^.^.^.5"},null]}],"nop","^.","\n","ev","str","^Co-operate","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Dissemble","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["^ ","\n","^Something will turn up. It always does. An opportunity will present itself, and more easily now that Hooper is out of the way.","\n","^But for now, there's yesterday's intercept to be resolved.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^Or perhaps I might hand my young blackmailer over my superiors instead for being the spy he is.","\n","^Perhaps that would be the moral thing to do, even, and not just the most smart.","\n","^But not today. Today, there's an intercept to resolve.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["\n","^In a week's time, this whole affair will be in the past and quite forgotten. I'm quite sure of that. ",{"->":".^.^.c-3"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ I've more important problems to think about now. There's still yesterday's intercept to be resolved. ","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^The Bombe needs to be set up once more and set running.","\n","^It's time I tackled a problem I can solve.","\n","end",{"#f":5}]}],{"#f":1}],"go_to_where_component_is_hidden":[["^It won't take a moment to settle the matter. I can justify a walk past Hut 2 as part of my morning stroll. It will be obvious in a moment if the component is still there.","\n","^On my way across the paddocks, between the huts and the House, I catch sight of young Miss Lyon, arriving for work on her bicycle. She giggles as she sees me and waves.","\n","ev","str","^Wave back","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Ignore her","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^I wave cheerily back and she giggles, almost drops her bicycle, then dashes away inside the House. Judging by the clock on the front gable, she's running a little late this morning.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^I give no reaction. She sighs to herself, as if this kind of behaviour is normal, and trots away inside the House to begin her duties.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^I turn the corner of Hut 3 and walk down the short gravel path to Hut 2. It was a good spot to choose — Hut 2 is where the electricians work, and they're generally focussed on what they're doing. They don't often come outside to smoke a cigarette so it's easy to slip past the doorway unnoticed.","\n","ev","str","^Check inside","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Go around the back","/str","/ev",{"*":".^.c-3","flg":20},{"c-2":["^ \t\t","\n","^I hop up the steps and put my head inside all the same. Nobody about. Still too early in the AM for sparks, I suppose. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-3":["^ ","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":["^I head on around the back of the hut. The breeze—block with the cavity is on the left side.","\n","ev","str","^Check","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Look around","/str","/ev",{"*":".^.c-5","flg":20},{"c-4":["^ \t\t","\n","^No time to waste. I drop to my knees and check the breeze—block. Sure enough, there's nothing there. Hooper took the bait.","\n","^Suddenly, there's a movement behind me. I look up to see, first a snub pistol, and then, Harris.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-5":["^ ","\n","^I pause to glance around, and catch a glimpse of movement. Someone ducking around the corner of the hut. Or a canvas sheet flapping in the light breeze. Impossible to be sure.","\n",["ev","str","^Check the breeze—block","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Check around the side of the hut","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ",{"->":".^.^.^.^.c-4"},"\n",{"->":".^.^.^.^.^.g-2"},{"#f":5}],"c-1":["^ ","\n","^But too important to guess. I move back around the side of the hut.","\n","^Harris is there, leaning in against the wall. He holds a stub pistol in his hand.","\n",{"->":".^.^.^.^.^.g-2"},{"#f":5}]}],{"#f":5}],"#f":5}],"g-2":["ev",{"VAR?":"hooperClueType"},1,">","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"","ev",{"VAR?":"hooperClueType"},2,"==","/ev",[{"->":".^.b","c":true},{"b":["^Queen to rook two",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["^Messy without one missing whatever it was",{"->":".^.^.^.9"},null]}],"nop","^,\" he declares. \"I wouldn't have fathomed it but Hooper did. Explained it right after we sprung him doing what you're doing now. We weren't sure what to believe but now, you seem to have resolved that for us.\"","\n",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["\n","^\"Hooper said you'd told him where to look. I didn't believe him. Or, well. I wasn't sure what to believe. Now I rather think you've settled it.\"","\n",{"->":".^.^.^.7"},null]}],"nop","\n","ev","str","^Agree","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-7","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-8","flg":20},{"c-6":["^ ","\n","^\"I have, rather.\" I put my hands into my pockets. \"I seem to have done exactly that.\"","\n","^\"I'm afraid my little story about Hooper confessing wasn't true. I wanted to see if you'd go to retrieve the part.\" Harris gestures me to start walking. \"You were close, Manning, I'll give you that. I wanted to believe you. But I'm glad I didn't.\"","\n",{"->":".^.^.^.g-3.done"},{"->":".^.^.^.g-3"},{"#f":5}],"c-7":["^ ","\n","^\"I spoke to Russell. He said he saw Hooper doing something round here. I wanted to see what it was.\"","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-8":["^ ","\n","^\"Harris, you'd better watch out. He's planted a time—bomb here.\"","\n","^Harris stares at me for a moment, then laughs. \"Oh, goodness. That's rich.\"","\n","^I almost wish I had a way to make the hut explode, but of course I don't.","\n",{"->":".^.^.^.g-3"},{"#f":5}],"#f":5}],"g-3":["^\"Enough.\" Harris gestures for me to start walking. \"This story couldn't be simpler. You took it to cover your back. You hid it. You lied to get Hooper into trouble, and when you thought you'd won, you came to scoop your prize. A good hand but ultimately, ","ev",{"VAR?":"hooperClueType"},1,"<=","/ev",[{"->":".^.b","c":true},{"b":["^if it hadn't have been you who hid the component, then you wouldn't be here now",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["^you told Hooper where to look with your little riddle",{"->":".^.^.^.8"},null]}],"nop","^.\"","\n",["^He leads me across the yard. Back towards Hut 5 to be decoded, and taken to pieces, once again.","\n","end",{"#f":5,"#n":"done"}],{"#f":5}]}],{"#f":1}],"harris_threatens_lynching":[["ev",{"CNT?":"harris_certain_is_you"},"/ev",[{"->":".^.b","c":true},{"b":["^He passes a hand across his eyes with a long look of despair.",{"->":".^.^.^.5"},null]}],[{"->":".^.b"},{"b":["^He gets to his feet, and gathers his gloves from the table top.",{"->":".^.^.^.5"},null]}],"nop","\n","^\"I'm going to go outside and organise a rope. That'll take about twelve minutes. That's how long you have to decide.\"","\n","ev","str","^Protest","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Confess","/str",{"VAR?":"gotcomponent"},"!",{"VAR?":"throwncomponentaway"},"!","&&","/ev",{"*":".^.c-1","flg":21},"ev","str","^Stay silent","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Show him the component","/str",{"VAR?":"gotcomponent"},"/ev",{"*":".^.c-3","flg":21},{"c-0":["^ ","\n","^\"You can't do this!\" I cry. \"It's murder! I demand a trial, a lawyer; for God's sake, man, you can't just throw me overboard, we're not barbarians...!\"","\n",[["^\"You leave me no choice,\" Harris snaps back, eyes cold as gun—metal. \"You and your damn cyphers. Your damn clever problems. If men like you didn't exist, if we could just all be straight with one another.\" He gets to his feet and heads for the door. \"I fear for the future of this world, with men like you in. Reich or no Reich, Mr Manning, people like you simply complicate matters.\"","\n",{"->":"left_alone"},{"->":".^.^.^.^.g-0"},{"#f":5,"#n":"too_clever"}],null],{"#f":5}],"c-1":["^ ","\n","^I nod. \"I don't need twelve minutes. ",{"->":"reveal_location_of_component"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ",{"->":"my_lips_are_sealed"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ ","\n","^\"I don't need twelve minutes. Here it is.\"","\n","^I open my jacket and pull the Bombe component out of my pocket. Harris takes it from me, whistling, curious.","\n","^\"Well, I'll be. That's it all right.\"","\n","^\"That's it.\"","\n","^\"But you didn't have it on you yesterday.\"","\n",["ev","str","^Explain","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't explain","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"I climbed out of the window overnight,\" I explain. \"I went and got this from where it was hidden, and brought it back here.\"","\n",{"->":".^.^.^.^.g-0"},{"#f":5}],"c-1":["\n","^\"No. I didn't.\"","\n",{"->":".^.^.^.^.g-0"},{"#f":5}]}],{"#f":5}],"g-0":[{"->":"all_too_farfetched"},"ev","str","^Confess","/str",{"VAR?":"throwncomponentaway"},"/ev",{"*":".^.c-4","flg":21},"ev","str","^Frame Hooper","/str",{"VAR?":"throwncomponentaway"},"/ev",{"*":".^.c-5","flg":21},{"c-4":["\n","^\"I don't need twelve minutes. The component is in the long grass behind Hooper's tent. I threw it there hoping to somehow frame him, but now I see that won't be possible. I was naive, I suppose.\"","\n","ev",true,"/ev",{"VAR=":"piecereturned","re":true},{"->":"reveal_location_of_component.harris_believes"},{"#f":5}],"c-5":["^ ","\n","^\"Look, I know where it is. The missing piece of the Bombe is in the long grasses behind Hooper's tent. I saw him throw it there right after we finished work. He knew you'd scour the camp but I suppose he thought you'd more obvious places first. I suppose he was right about that. Look there. That proves his guilt.\"","\n","ev",true,"/ev",{"VAR=":"longgrasshooperframe","re":true},"ev",true,"/ev",{"VAR=":"piecereturned","re":true},"^\"That doesn't prove anything,\" Harris returns sharply. \"But we'll check what you say, all the same.\" He gets to his feet and heads out of the door.","\n",{"->":"left_alone"},{"#f":5}],"#f":5}]}],{"#f":1}],"reveal_location_of_component":["<>","^ The missing component of the Bombe computer is hidden in a small cavity in a breeze—block supporting the left rear post of Hut 2. I put in there anticipating a search. I intended to ","ev",{"VAR?":"revealedhooperasculprit"},"/ev",[{"->":".^.b","c":true},{"b":["^pass it to Hooper",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^dispose of it",{"->":".^.^.^.7"},null]}],"nop","^ once the fuss had died down. I suppose I was foolish to think that it might.\"","\n","ev",true,"/ev",{"VAR=":"piecereturned","re":true},{"->":".^.harris_believes"},{"harris_believes":["ev",{"CNT?":"night_falls.0.g-0.c-3.6.hooper_didnt_give_himself_up"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"Indeed. And Mr Manning: God help you if you're lying to me.\"","\n",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["\n","^\"I thought as much. I hadn't expected you to give it out so easily, however. You understand, Hooper has said nothing, of course. In fact, he went to Hut 2 directly after we released him and uncovered the component. But he told us you had instructed him where to go. Hence my little double bluff. Frankly, I'll be glad when I'm shot of the lot of you mathematicians.\"","\n",{"->":".^.^.^.6"},null]}],"nop","\n","^Harris stands, and slips away smartly. ",{"->":"left_alone"},"\n",{"#f":1}],"#f":1}],"my_lips_are_sealed":["^I say nothing, my lips tightly, firmly sealed. It's true I am a traitor, to the very laws of nature. The world has taught me that since a very early age. But not to my country — should the Reich win this war, I would hardly be treated as an honoured hero. I was doomed from the very start.","\n","ev",true,"/ev",{"VAR=":"notraitor","re":true},"^I explain none of this. How could a man like Harris understand?","\n","^The Commander takes one look back from the doorway as he pulls it to.","\n","^\"It's been a pleasure working with you, Mr Manning,\" he declares. \"You've done a great service to this country. If we come through, I'm sure they'll remember you name. I'm sorry it had to end this way and I'll do my best to keep it quiet. No—one need know what you did.\"","\n",{"->":"left_alone"},{"#f":1}],"all_too_farfetched":["^\"This is all too far—fetched,\" Harris says. \"I'm glad to have this back, but I need to think.\"","\n","^Getting to his feet, he nods once. \"You'll have to wait a little longer, I'm afraid, Manning.\"","\n","^Then he steps out of the door, muttering to himself.","\n",{"->":"make_your_peace"},{"#f":1}],"left_alone":["ev",{"CNT?":"slam_door_shut_and_gone.time_to_move_now"},"/ev",[{"->":".^.b","c":true},{"b":["^The Commander holds the door for his superior, and follows him out.",{"->":".^.^.^.4"},null]}],"nop","^ Then the door closes. I am alone again, as I have been for most of my short life.","\n",{"->":"make_your_peace"},{"#f":1}],"make_your_peace":[["ev","str","^Make your peace","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^I am waiting again. I have no God to make my peace with. I find it difficult to believe in goodness of any kind, in a world such as this.","\n","ev",{"VAR?":"notraitor"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",true,"/ev",{"VAR=":"notraitor","re":true},"^But I am no traitor. Not to my country. To my sex, perhaps. But how could I support the Reich? If the Nazis were to come to power, I would be worse off than ever.","\n",{"->":".^.^.^.7"},null]}],"nop","\n","ev",{"CNT?":"harris_threatens_lynching.0.c-0.4.too_clever"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^In truth, it is men like Harris who are complex, not men like me. I live to make things ordered, systematic. I like my pencils sharpened and lined up in a row. I do not deal in difficult borders, or uncertainties, or alliances. If I could, I would reduce the world to something easier to understand, something finite.","\n","^But I cannot, not even here, in our little haven from the horrors of the war.","\n",{"->":".^.^.^.13"},null]}],"nop","\n","^I have no place here. No way to fit. I am caught, in the middle, cryptic and understood only thinly, through my machines.","\n",["ev",{"^->":"make_your_peace.0.g-0.17.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^I must seem very calm. \t\t\t",{"->":"$r","var":true},null]}],["ev",{"^->":"make_your_peace.0.g-0.18.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":18},{"s":["^Perhaps I should try to escape.",{"->":"$r","var":true},null]}],{"c-1":["ev",{"^->":"make_your_peace.0.g-0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.17.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-2":["ev",{"^->":"make_your_peace.0.g-0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.18.s"},[{"#n":"$r2"}],"^ But escape to where? I am already a prisoner. Jail would be a blessing. ",{"->":".^.^.^.g-1.monastic"},"\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":["<>","^ I suppose I do not believe they will hang me. They will lock me up and continue to use my brain, if they can. I wonder what they will tell the world — perhaps that I have taken my own life. That would be simplest. The few who know me would believe it.","\n","^Well, then. Not a bad existence, in prison. Removed from temptation.","\n",["^A monastic life, with plenty of problems to keep me going.","\n","^I wonder what else I might yet unravel before I'm done?","\n",["ev",{"^->":"make_your_peace.0.g-1.monastic.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-3","flg":18},{"s":["^The door is opening.",{"->":"$r","var":true},null]}],{"c-3":["ev",{"^->":"make_your_peace.0.g-1.monastic.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.4.s"},[{"#n":"$r2"}],"^ Harris is returning. Our little calculation here is complete. ","ev",{"VAR?":"piecereturned"},"!","/ev",[{"->":".^.b","c":true},{"b":["^ I can only hope one of the others will be able to explain to him that the part I stole will mean nothing to the Germans.",{"->":".^.^.^.13"},null]}],[{"->":".^.b"},{"b":["^We are just pieces in this machine; interchangeable and prone to wear.",{"->":".^.^.^.13"},null]}],"nop","\n",{"->":".^.^.^.^.g-2"},{"#f":5}],"#f":5,"#n":"monastic"}],{"#f":5}],"g-2":["^That is the true secret of the calculating engine, and the source of its power. It is not the components that matter, they are quite repetitive. What matters is how they are wired; the diversity of the patterns and structures they can form. Much like people — it is how they connect that determines our victories and tragedies, and not their genius.","\n","^Which makes me wonder. Should I give ","ev",{"CNT?":"i_met_a_young_man"},"/ev",[{"->":".^.b","c":true},{"b":["^up my beautiful young man",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["^the young man who put me in this spot",{"->":".^.^.^.8"},null]}],"nop","^ to them as well as myself?","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-7","flg":20},{"c-4":["^ ","\n","^But of course I will. ","ev",{"VAR?":"forceful"},2,">","/ev",[{"->":".^.b","c":true},{"b":["^Perhaps I can persuade them to put him in my cell.",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^A little vengeance, disguised as doing something good.",{"->":".^.^.^.10"},null]}],"nop","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-5":["^ ","\n","^No. What would be the use? He will be long gone, and the name he told me is no doubt hokum. No: I was alone before in guilt, and I am thus alone again.","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-6":["^ ","\n","^No. Why would I? He is no doubt an innocent himself, trapped by some dire circumstance. Forced to act the way he did. I have every sympathy for him.","\n","^Of course I do.","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-7":["^ ","\n","^It depends, perhaps, on what his name his worth. If it were to prove valuable, well; perhaps I can concoct a few more such lovers with which to ease my later days.","\n","ev",{"VAR?":"hooper_mentioned"},"/ev",[{"->":".^.b","c":true},{"b":["^ Hooper, perhaps. He wouldn't like that. ",{"->":".^.^.^.8"},null]}],"nop","\n",{"->":".^.^.^.g-3"},{"#f":5}],"#f":5}],"g-3":["ev",{"VAR?":"longgrasshooperframe"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^Harris put the cuffs around my wrists. \"I still have the intercept in my pocket,\" I remark. \"Wherever we're going, could I have a pencil?\"","\n",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["\n","^\"We recovered the part, just where you said it was,\" Harris reports, as he puts the cuffs around my wrists. \"Of course, a couple of the men swear blind they searched there yesterday, so I'm afraid, what with the broken window... we've formed a perfectly good theory which doesn't bode well for you.\"","\n",{"->":".^.^.^.6"},null]}],"nop","\n","ev",true,"/ev",{"VAR=":"piecereturned","re":true},"ev",{"VAR?":"longgrasshooperframe"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^\"I see.\" It doesn't seem worth arguing any further. \"I still have the intercept in my pocket,\" I remark. \"Wherever we're going, could I have a pencil?\"","\n",{"->":".^.^.^.16"},null]}],"nop","\n","^He looks me in the eye.","\n","ev",{"VAR?":"losttemper"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"Of course. And one of your computing things, if I get my way. And when we're old, and smoking pipes together in The Rag like heroes, I'll explain to you the way that decent men have affairs.","\n",{"->":".^.^.^.26"},null]}],[{"->":".^.b"},{"b":["\n","^\"I'll give you a stone to chisel notches in the wall. And that's all the calculations you'll be doing. And as you sit there, pissing into a bucket and growing a beard down to your toes, you have a think about how a smart man would conduct his illicit affairs. With a bit of due decorum you could have learnt off any squaddie.","\n",{"->":".^.^.^.26"},null]}],"nop","\n","<>","^ You scientists.\"","\n","^He drags me up to my feet.","\n","^\"You think you have to re—invent everything.\"","\n","^With that, he hustles me out of the door and I can't help thinking that, with a little more strategy, I could still have won the day. But too late now, of course.","\n","end",{"#f":5}]}],{"#f":1}],"global decl":["ev",0,{"VAR=":"forceful"},0,{"VAR=":"evasive"},false,{"VAR=":"teacup"},false,{"VAR=":"gotcomponent"},false,{"VAR=":"drugged"},false,{"VAR=":"hooper_mentioned"},false,{"VAR=":"losttemper"},false,{"VAR=":"admitblackmail"},0,{"VAR=":"hooperClueType"},false,{"VAR=":"hooperConfessed"},0,{"VAR=":"smashingWindowItem"},false,{"VAR=":"notraitor"},false,{"VAR=":"revealedhooperasculprit"},false,{"VAR=":"smashedglass"},false,{"VAR=":"muddyshoes"},false,{"VAR=":"framedhooper"},false,{"VAR=":"putcomponentintent"},false,{"VAR=":"throwncomponentaway"},false,{"VAR=":"piecereturned"},false,{"VAR=":"longgrasshooperframe"},false,{"VAR=":"DEBUG"},"/ev","end",null],"#f":1}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["ev",{"VAR?":"DEBUG"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^IN DEBUG MODE!","\n","ev","str","^Beginning...","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Framing Hooper...","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^In with Hooper...","/str","/ev",{"*":".^.c-2","flg":20},{"->":"0.5"},{"c-0":["^\t",{"->":"start"},"\n",{"#f":5}],"c-1":["^ ",{"->":"claim_hooper_took_component"},"\n",{"#f":5}],"c-2":["^ ",{"->":"inside_hoopers_hut"},"\n",{"#f":5}]}]}],[{"->":".^.b"},{"b":["\n",{"->":"start"},{"->":"0.5"},null]}],"nop","\n",["done",{"#f":5,"#n":"g-0"}],null],"done",{"lower":[{"temp=":"x"},"ev",{"VAR?":"x"},1,"-","/ev",{"temp=":"x","re":true},{"#f":1}],"raise":[{"temp=":"x"},"ev",{"VAR?":"x"},1,"+","/ev",{"temp=":"x","re":true},{"#f":1}],"start":[[["^They are keeping me waiting.","\n",["ev",{"^->":"start.0.g-0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^Hut 14",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"start.0.g-0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"^. The door was locked after I sat down. ","\n","^I don't even have a pen to do any work. There's a copy of the morning's intercept in my pocket, but staring at the jumbled letters will only drive me mad.","\n","^I am not a machine, whatever they say about me.","\n",{"->":".^.^.^.opts"},{"#f":5}],"#f":5,"#n":"g-0"}],{"opts":[["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","^I rattle my fingers on the field table.",{"->":".^.^.23"},null],"s2":["pop",{"->":".^.^.23"},null],"#f":5}],"\n","ev","str","^Think","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Plan","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Wait","/str","/ev",{"*":".^.c-3","flg":20},{"c-1":["^ ","\n","^They suspect me to be a traitor. They think I stole the component from the calculating machine. They will be searching my bunk and cases.","\n","^When they don't find it, ","ev",{"CNT?":".^.^.c-2"},"/ev",[{"->":".^.b","c":true},{"b":["^then",{"->":".^.^.^.9"},null]}],"nop","^ they'll come back and demand I talk.","\n",{"->":".^.^"},{"->":".^.^.^.g-1"},{"#f":5}],"c-2":["\n","ev",{"CNT?":".^.^.c-1"},"!","/ev",[{"->":".^.b","c":true},{"b":["^What I am is",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^I am",{"->":".^.^.^.7"},null]}],"nop","^ a problem—solver. Good with figures, quick with crosswords, excellent at chess.","\n","^But in this scenario — in this trap — what is the winning play?","\n",["ev","str","^Co—operate","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Dissemble","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Divert","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^I must co—operate. My credibility is my main asset. To contradict myself, or another source, would be fatal.","\n","^I must simply hope they do not ask the questions I do not want to answer.","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n",{"->":"start.0.g-1"},{"#f":5}],"c-1":["^ ","\n","^Misinformation, then. Just as the war in Europe is one of plans and interceptions, not planes and bombs.","\n","^My best hope is a story they prefer to the truth.","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":"start.0.g-1"},{"#f":5}],"c-2":["^ ","\n","^Avoidance and delay. The military machine never fights on a single front. If I move slowly enough, things will resolve themselves some other way, my reputation intact.","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":"start.0.g-1"},{"#f":5}]}],{"#f":5}],"c-3":["^\t\t","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":[{"->":"start.waited"},{"#f":5}]}],{"waited":[[["^Half an hour goes by before Commander Harris returns. He closes the door behind him quickly, as though afraid a loose word might slip inside.","\n","^\"Well, then,\" he begins, awkwardly. This is an unseemly situation.","\n",["ev",{"^->":"start.waited.0.g-0.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"Commander.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"start.waited.0.g-0.5.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",{"CNT?":"start.0.opts.c-2.12.c-2"},"!","/ev",{"*":".^.^.c-1","flg":19},{"s":["^\"Tell me what this is about.\"",{"->":"$r","var":true},null]}],"ev","str","^Wait","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["ev",{"^->":"start.waited.0.g-0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.4.s"},[{"#n":"$r2"}],"\n","^He nods. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-1":["ev",{"^->":"start.waited.0.g-0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.5.s"},[{"#n":"$r2"}],"\n","^He shakes his head.","\n","^\"Now, don't let's pretend.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-2":["\n","^I say nothing.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5,"#n":"g-0"}],{"g-1":["^He has brought two cups of tea in metal mugs: he sets them down on the tabletop between us.","\n","ev","str","^Deny","/str",{"CNT?":".^.^.g-0.c-1"},"/ev",{"*":".^.c-3","flg":21},"ev","str","^Take one","/str","/ev",{"*":".^.c-4","flg":20},["ev",{"^->":"start.waited.0.g-1.15.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",{"CNT?":".^.^.^.g-0.c-1"},"!","/ev",{"*":".^.^.c-5","flg":19},{"s":["^\"What's going on?\"",{"->":"$r","var":true},null]}],"ev","str","^Wait","/str","/ev",{"*":".^.c-6","flg":20},{"c-3":["^ \"I'm not pretending anything.\"","\n","ev",{"CNT?":"start.0.opts.c-2.12.c-0"},"/ev",[{"->":".^.b","c":true},{"b":["^I'm lying already, despite my good intentions.",{"->":".^.^.^.6"},null]}],"nop","\n","^Harris looks disapproving. ",{"->":".^.^.c-6.3.pushes_cup"},"\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-4":["\n","ev",true,"/ev",{"VAR=":"teacup","re":true},"^I take a mug and warm my hands. It's ","<>","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-5":["ev",{"^->":"start.waited.0.g-1.c-5.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.15.s"},[{"#n":"$r2"}],"\n","^\"You know already.\"","\n",{"->":".^.^.c-6.3.pushes_cup"},{"->":".^.^.^.g-2"},{"#f":5}],"c-6":["\n","^I wait for him to speak.","\n",[["^He pushes one mug halfway towards me: ","<>","\n",{"->":".^.^.^.^.^.g-2"},{"#f":5,"#n":"pushes_cup"}],null],{"#f":5}],"#f":5}],"g-2":["^a small gesture of friendship.","\n","^Enough to give me hope?","\n","ev","str","^Take it","/str",{"VAR?":"teacup"},"!","/ev",{"*":".^.c-7","flg":21},"ev","str","^Don't take it","/str",{"VAR?":"teacup"},"!","/ev",{"*":".^.c-8","flg":21},"ev","str","^Drink","/str",{"VAR?":"teacup"},"/ev",{"*":".^.c-9","flg":21},"ev","str","^Wait","/str",{"VAR?":"teacup"},"/ev",{"*":".^.c-10","flg":21},{"c-7":["^ ","\n","^I ","ev",{"CNT?":".^.^.^.g-1.c-4"},"/ev",[{"->":".^.b","c":true},{"b":["^lift the mug",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["^take the mug,",{"->":".^.^.^.8"},null]}],"nop","^ and blow away the steam. It is too hot to drink.","\n","^Harris picks his own up and just holds it.","\n","ev",true,"/ev",{"VAR=":"teacup","re":true},"ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-8":["^ ","\n","^Just a cup of insipid canteen tea. I leave it where it is.","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-9":["^ ","\n","^I raise the cup to my mouth but it's too hot to drink.","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-10":["^ \t\t","\n","^I say nothing as ",{"->":".^.^.c-7"},"\n",{"->":".^.^.^.g-3"},{"#f":5}],"#f":5}],"g-3":["^\"Quite a difficult situation,\" ","ev",{"CNT?":".^.^.g-2.c-7"},"/ev",[{"->":".^.b","c":true},{"b":["^he",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["^Harris",{"->":".^.^.^.6"},null]}],"nop","^ begins","ev",{"VAR?":"forceful"},0,"<=","/ev",[{"->":".^.b","c":true},{"b":["^, sternly",{"->":".^.^.^.14"},null]}],"nop","^. I've seen him adopt this stiff tone of voice before, but only when talking to the brass. \"I'm sure you agree.\"","\n","ev","str","^Agree","/str","/ev",{"*":".^.c-11","flg":20},"ev","str","^Disagree","/str","/ev",{"*":".^.c-12","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-13","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-14","flg":20},{"c-11":["^ ","\n","^\"Awkward,\" I reply","\n",{"->":".^.^.^.g-4"},{"#f":5}],"c-12":["^ ","\n","^\"I don't see why,\" I reply","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.^.g-4"},{"#f":5}],"c-13":["^ ",{"->":".^.^.c-12"},"\n",{"->":".^.^.^.g-4"},{"#f":5}],"c-14":["^ ","\n","^\"I'm sure you've handled worse,\" I reply casually","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.^.g-4"},{"#f":5}],"#f":5}],"g-4":["ev",{"VAR?":"teacup"},"/ev",[{"->":".^.b","c":true},{"b":["\n","ev",true,"/ev",{"VAR=":"drugged","re":true},"<>","^, sipping at my tea as though we were old friends","\n",{"->":".^.^.^.4"},null]}],"nop","\n","<>","^.","\n",["ev","str","^Watch him","/str","/ev",{"*":".^.c-15","flg":20},"ev","str","^Wait","/str","/ev",{"*":".^.c-16","flg":20},"ev","str","^Smile","/str",{"CNT?":".^.^.^.g-3.c-12"},"!","/ev",{"*":".^.c-17","flg":21},{"c-15":["\n","^His face is telling me nothing. I've seen Harris broad and full of laughter. Today he is tight, as much part of the military machine as the device in Hut 5.","\n",{"->":".^.^.^.^.g-6"},{"#f":5}],"c-16":["\n","^I wait to see how he'll respond.","\n",{"->":".^.^.^.^.g-6"},{"#f":5}],"c-17":["\n","^I try a weak smile. It is not returned.","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n",{"->":".^.^.^.^.g-6"},{"#f":5}],"#f":5,"#n":"g-5"}],{"#f":5}],"g-6":["^\"We need that component,\" he says.","\n",["ev",{"CNT?":"missing_reel"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->t->":"missing_reel"},{"->":"harris_demands_component"},{"->":".^.^.^.5"},null]}],"nop","\n",["ev","str","^Yes","/str","/ev",{"*":".^.c-18","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-19","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-20","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-21","flg":20},{"c-18":["\n","^\"Of course I do,\" I answer.","\n",{"->":".^.^.^.^.^.g-9"},{"#f":5}],"c-19":["\n","^\"No I don't. And I've got work to do...\"","\n","^\"Work that will be rather difficult for you to do, don't you think?\" Harris interrupts.","\n",{"->":".^.^.^.^.^.g-9"},{"#f":5}],"c-20":["\n",{"->":"here_at_bletchley_diversion"},{"->":".^.^.^.^.^.g-9"},{"#f":5}],"c-21":["^ ","\n",{"->":".^.^.c-19"},{"->":".^.^.^.^.^.g-9"},{"#f":5}],"#f":5,"#n":"g-8"}],{"#f":5,"#n":"g-7"}],{"#f":5}],"g-9":[{"->t->":"missing_reel"},{"->":"harris_demands_component"},{"#f":5}]}],{"#f":1}],"#f":1}],"missing_reel":[["ev","str","^The stolen component...","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Shrug","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["\n","^I shrug.","\n","ev","void","/ev","->->",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^The reel went missing from the Bombe this afternoon. The four of us were in the Hut, working on the latest German intercept. The results were garbage. It was Russell who found the gap in the plugboard.","\n",["^Any of us could have taken it; and no one else would have known its worth.","\n","ev","str","^Panic","/str",{"VAR?":"forceful"},0,"<=","/ev",{"*":".^.c-2","flg":21},"ev","str","^Calculate","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Deny","/str",{"VAR?":"evasive"},0,">=","/ev",{"*":".^.c-4","flg":21},{"c-2":["^ They will pin it on me. They need a scapegoat so that the work can continue. I'm a likely target. Weaker than the rest. ","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n",{"->":".^.^.^.^.g-2"},{"#f":5}],"c-3":["^ My odds, then, are one in four. Not bad; although the stakes themselves are higher than I would like.","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.^.^.g-2"},{"#f":5}],"c-4":["^ But this is still a mere formality. The work will not stop. A replacement component will be made and we will all be put back to work. We are too valuable to shoot. ","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.^.^.g-2"},{"#f":5}],"#f":5,"#n":"g-1"}],{"#f":5}],"g-2":["ev","void","/ev","->->",{"#f":5}]}],{"#f":1}],"here_at_bletchley_diversion":[["^\"Here at Bletchley? Of course.\"","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n","^\"Here, now,\" Harris corrects. \"We are not talking to everyone. I can imagine you might feel pretty sore about that. I can imagine you feeling picked on. ","ev",{"VAR?":"forceful"},0,"<","/ev",[{"->":".^.b","c":true},{"b":["^You're a sensitive soul.",{"->":".^.^.^.21"},null]}],"nop","^\"","\n",["ev",{"^->":"here_at_bletchley_diversion.0.24.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str","/ev",{"*":".^.^.c-0","flg":22},{"s":["^\"I'm fine",{"->":"$r","var":true},null]}],["ev",{"^->":"here_at_bletchley_diversion.0.25.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",{"VAR?":"forceful"},0,"<","/ev",{"*":".^.^.c-1","flg":19},{"s":["^\"What do you mean by that?\"",{"->":"$r","var":true},null]}],["ev",{"^->":"here_at_bletchley_diversion.0.26.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str",{"VAR?":"forceful"},0,">=","/ev",{"*":".^.^.c-2","flg":23},{"s":["^\"Damn right",{"->":"$r","var":true},null]}],"ev","str","^Be honest","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-4","flg":20},{"c-0":["ev",{"^->":"here_at_bletchley_diversion.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.24.s"},[{"#n":"$r2"}],"^,\" I reply. \"This is all some misunderstanding and the quicker we have it cleared up the better.\"","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n","^\"I couldn't agree more.\" And then he comes right out with it, with an accusation.","\n",{"->":".^.^.done"},{"#f":5}],"c-1":["ev",{"^->":"here_at_bletchley_diversion.0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.25.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.done"},{"#f":5}],"c-2":["ev",{"^->":"here_at_bletchley_diversion.0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.26.s"},[{"#n":"$r2"}],"^ I'm sore. Was it one of the others who put you up to this? Was it Hooper? He's always been jealous of me. He's...\"","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n","ev",true,"/ev",{"VAR=":"hooper_mentioned","re":true},"^The Commander moustache bristles as he purses his lips. \"Has he now? Of your achievements, do you think?\"","\n","^It's difficult not to shake the sense that he's ","ev",{"VAR?":"evasive"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^mocking",{"->":".^.^.^.28"},null]}],[{"->":".^.b"},{"b":["^simply humouring",{"->":".^.^.^.28"},null]}],"nop","^ me.","\n","^\"Or of your brain? Or something else?\"","\n",[["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-0","flg":22},{"s":["^\"Of my genius.",{"->":"$r","var":true},null]}],["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-1","flg":22},{"s":["^\"Of my standing.",{"->":"$r","var":true},null]}],"ev","str","^Evade","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"^ Hooper simply can't stand that I'm cleverer than he is. We work so closely together, cooped up in that Hut all day. It drives him to distraction. To worse.\"","\n","^\"You're suggesting Hooper would sabotage this country's future simply to spite you?\" Harris chooses his words like the military man he is, each lining up to create a ring around me.","\n",["ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ \t\t\t","\n","^\"","ev",{"VAR?":"forceful"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^He's petty enough, certainly",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^I wouldn't put it past him",{"->":".^.^.^.10"},null]}],"nop","^. He's a creep.\" ","ev",{"VAR?":"teacup"},"/ev",[{"->":".^.b","c":true},{"b":["^ I set the teacup down.",{"->":".^.^.^.17"},null]}],[{"->":".^.b"},{"b":["^I wipe a hand across my forehead.",{"->":".^.^.^.17"},null]}],"nop","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n","ev",false,"/ev",{"VAR=":"teacup","re":true},{"->":".^.^.suggest_its_a_lie"},{"#f":5}],"c-1":["^ \t\t\t","\n","^\"No, ","ev",{"VAR?":"forceful"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^of course not",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^I suppose not",{"->":".^.^.^.10"},null]}],"nop","^.\" ","ev",{"VAR?":"teacup"},"/ev",[{"->":".^.b","c":true},{"b":["^I put the teacup back down on the table",{"->":".^.^.^.17"},null]}],[{"->":".^.b"},{"b":["^I push the teacup around on its base",{"->":".^.^.^.17"},null]}],"nop","^.","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n","ev",false,"/ev",{"VAR=":"teacup","re":true},{"->":".^.^.suggest_its_a_lie"},{"#f":5}],"c-2":["^ \t\t","\n","^\"I don't know what I'm suggesting. I don't understand what's going on.\"","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n","^\"But of course you do.\" Harris narrows his eyes.","\n",{"->":".^.^.^.^.^.^.done"},{"->":".^.^.suggest_its_a_lie"},{"#f":5}],"suggest_its_a_lie":["^\"All I can say is, ever since I arrived here, he's been looking to ways to bring me down a peg. I wouldn't be surprised if he set this whole affair up just to have me court—martialled.\"","\n","^\"We don't court—martial civilians,\" Harris replies. \"Traitors are simply hung at her Majesty's pleasure.\"","\n",["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.c-0.10.suggest_its_a_lie.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str","/ev",{"*":".^.^.c-3","flg":22},{"s":["^\"Quite right",{"->":"$r","var":true},null]}],["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.c-0.10.suggest_its_a_lie.5.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str","/ev",{"*":".^.^.c-4","flg":22},{"s":["^\"I'm no traitor",{"->":"$r","var":true},null]}],"ev","str","^Lie","/str","/ev",{"*":".^.c-5","flg":20},{"c-3":["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.c-0.10.suggest_its_a_lie.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.4.s"},[{"#n":"$r2"}],"^,\" I answer smartly.","\n",{"->":".^.^.^.g-0"},{"#f":5}],"c-4":["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.c-0.10.suggest_its_a_lie.c-4.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.5.s"},[{"#n":"$r2"}],"^,\" I answer","ev",{"VAR?":"forceful"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^smartly",{"->":".^.^.^.14"},null]}],[{"->":".^.b"},{"b":["^, voice quivering. \"For God's sake!\"",{"->":".^.^.^.14"},null]}],"nop","\n",{"->":".^.^.^.g-0"},{"#f":5}],"c-5":["^ ",{"->":".^.^.c-4"},"\n",{"->":".^.^.^.g-0"},{"#f":5}],"#f":5}],"g-0":["^He stares back at me.","\n",{"->":".^.^.^.^.^.^.done"},{"#f":5}]}],{"#f":5}],"c-1":["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"^ My reputation.\" ","ev",{"VAR?":"forceful"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^I'm aware of how arrogant I must sound but I plough on all the same.",{"->":".^.^.^.14"},null]}],[{"->":".^.b"},{"b":["^I don't like to talk of myself like this, but I carry on all the same.",{"->":".^.^.^.14"},null]}],"nop","^ \"Hooper simply can't bear knowing that, once all this is over, I'll be the one receiving the knighthood and he...\"","\n","^\"No—one will be getting a knighthood if the Germans make landfall,\" Harris answers sharply. He casts a quick eye to the door of the Hut to check the latch is still down, then continues in more of a murmur: \"Not you and not Hooper. Now answer me.\"","\n","^For the first time since the door closed, I wonder what the threat might be if I do not.","\n",{"->":".^.^.^.^.done"},{"#f":5}],"c-2":["^ \t\t\t\t","\n","ev",false,"/ev",{"VAR=":"teacup","re":true},"ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n","^\"How should I know?\" I reply, defensively. ","ev",{"VAR?":"teacup"},"/ev",[{"->":".^.b","c":true},{"b":["^I set the teacup back on the table.",{"->":".^.^.^.17"},null]}],"nop","^ ",{"->":".^.^.c-0.10.suggest_its_a_lie"},"\n",{"->":".^.^.^.^.done"},{"#f":5}]}],{"#f":5}],"c-3":["^ \t",{"->":".^.^.c-2"},"\n",{"->":".^.^.done"},{"#f":5}],"c-4":["^ \t\t",{"->":".^.^.c-0"},"\n",{"->":".^.^.done"},{"#f":5}],"done":[{"->":"harris_demands_component"},{"#f":5}]}],{"#f":1}],"harris_demands_component":[["^\"","ev",{"CNT?":"here_at_bletchley_diversion"},"/ev",[{"->":".^.b","c":true},{"b":["^Please",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["^So",{"->":".^.^.^.6"},null]}],"nop","^. Do you have it?\" Harris is ","ev",{"VAR?":"forceful"},3,">","/ev",[{"->":".^.b","c":true},{"b":["^sweating slightly",{"->":".^.^.^.15"},null]}],[{"->":".^.b"},{"b":["^wasting no time",{"->":".^.^.^.15"},null]}],"nop","^: Bletchley is his watch. \"Do you know where it is?\"","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["\n","^\"I do.\"","\n",{"->":"admitted_to_something"},{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ \"I have no idea.\" ","\n",{"->":".^.^.silence"},{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ \t\t",{"->":".^.^.c-1"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ \t\t","\n","^\"The component?\"","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n","^\"Don't play stupid,\" he replies. \"","ev",{"CNT?":"missing_reel"},"!","/ev",[{"->":".^.b","c":true},{"b":["^The component that went missing this afternoon. ",{"->":".^.^.^.22"},null]}],"nop","^Where is it?\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["ev",{"CNT?":"missing_reel"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->t->":"missing_reel"},{"->":".^.^.^.5"},null]}],"nop","\n","ev","str","^Co-operate","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Delay","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-7","flg":20},{"c-4":["^ \"I know where it is.\"","\n",{"->":"admitted_to_something"},{"->":".^.^.^.silence"},{"#f":5}],"c-5":["^ \"I know nothing about it.\" My voice shakes","ev",{"VAR?":"forceful"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^ with anger",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["^; I'm unaccustomed to facing off against men with holstered guns",{"->":".^.^.^.8"},null]}],"nop","^. ","\n",{"->":".^.^.^.silence"},{"#f":5}],"c-6":["^ ",{"->":".^.^.c-5"},"\n",{"->":".^.^.^.silence"},{"#f":5}],"c-7":["^ ","\n","^\"I don't know what gives you the right to pick on me. ","ev",{"VAR?":"forceful"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^I demand a lawyer.",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^I want a lawyer.",{"->":".^.^.^.10"},null]}],"nop","^\"","\n","^\"This is time of war,\" Harris answers. \"And by God, if I have to shoot you to recover the component, I will. Understand?\" He points at the mug, ",{"->":".^.^.^.silence.drinkit"},"\n",{"->":".^.^.^.silence"},{"#f":5}],"#f":5}],"silence":["^There's an icy silence. ","ev",{"VAR?":"forceful"},2,">","/ev",[{"->":".^.b","c":true},{"b":["^I've cracked him a little.",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["ev",{"VAR?":"evasive"},2,">","/ev",[{"->":".^.b","c":true},{"b":["^He's tiring of my evasiveness.",{"->":".^.^.^.6"},null]}],"nop",{"->":".^.^.^.8"},null]}],"nop","\n",["^\"Now drink your tea and talk.\"","\n","ev","str","^Drink","/str",{"VAR?":"teacup"},"/ev",{"*":".^.c-8","flg":21},"ev","str","^Put the cup down","/str",{"VAR?":"teacup"},"/ev",{"*":".^.c-9","flg":21},"ev","str","^Take the cup","/str",{"VAR?":"teacup"},"!","/ev",{"*":".^.c-10","flg":21},"ev","str","^Don't take it","/str",{"VAR?":"teacup"},"!","/ev",{"*":".^.c-11","flg":21},{"c-8":["^ \t\t\t",{"->":".^.^.c-10.2.drinkfromcup"},"\n",{"->":".^.^.^.^.g-1"},{"#f":5}],"c-9":["^ ","\n","^I set the cup carefully down on the table once more.","\n","ev",false,"/ev",{"VAR=":"teacup","re":true},"ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.c-11.10.whatsinit"},{"->":".^.^.^.^.g-1"},{"#f":5}],"c-10":["^ ","\n",[["^I lift the cup ","ev",{"VAR?":"teacup"},"/ev",[{"->":".^.b","c":true},{"b":["^to my lips ",{"->":".^.^.^.5"},null]}],"nop","^and sip. He waits for me to swallow before speaking again.","\n","ev",true,"/ev",{"VAR=":"drugged","re":true},"ev",true,"/ev",{"VAR=":"teacup","re":true},{"->":".^.^.^.^.^.^.g-1"},{"#f":5,"#n":"drinkfromcup"}],null],{"#f":5}],"c-11":["^ ","\n","^I leave the cup where it is.","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",[["^\"Why?\" I ask coldly. \"What's in it?\"","\n",{"->":".^.^.^.^.^.^.g-1"},{"#f":5,"#n":"whatsinit"}],null],{"#f":5}],"#f":5,"#n":"drinkit"}],{"#f":5}],"g-1":["^\"Lapsang Souchong,\" he ","ev",{"CNT?":".^.^.silence.drinkit.c-10.2.drinkfromcup"},"/ev",[{"->":".^.b","c":true},{"b":["^remarks",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["^replies",{"->":".^.^.^.6"},null]}],"nop","^, placing his own cup back on the table untouched. \"Such a curious flavour. It might almost not be tea at all. You might say it hides a multitude of sins. As do you. Isn't that right?\"","\n","ev","str","^Agree","/str","/ev",{"*":".^.c-12","flg":20},"ev","str","^Disagree","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-13","flg":21},"ev","str","^Disagree","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-14","flg":21},"ev","str","^Lie","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-15","flg":21},"ev","str","^Lie","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-16","flg":21},"ev","str","^Evade","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-17","flg":21},"ev","str","^Evade","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-18","flg":21},{"c-12":["^ ","\n","^\"I suppose so,\" I reply. \"I've done things I shouldn't have done.\"","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n",{"->":"harris_presses_for_details"},{"#f":5}],"c-13":["\n","^\"I've done nothing that I'm ashamed of.\"","\n",{"->":"harris_asks_for_theory"},{"#f":5}],"c-14":["^ ","\n","^I open my mouth to disagree, but the words I want won't come. It is like Harris has taken a screwdriver to the sides of my jaw.","\n",{"->":"admitted_to_something.ive_done_things"},{"#f":5}],"c-15":["^ \t",{"->":".^.^.c-14"},"\n",{"#f":5}],"c-16":["^ \t",{"->":".^.^.c-13"},"\n",{"#f":5}],"c-17":["^ ",{"->":".^.^.c-14"},"\n",{"#f":5}],"c-18":["^ ","\n","^\"None of us are blameless, Harris. ","ev",{"VAR?":"forceful"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^But you're not my priest and I'm not yours",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^But I've done nothing to deserve this treatment",{"->":".^.^.^.10"},null]}],"nop","^. Now, please. Let me go. I'll help you find this damn component, of course I will.\"","\n","^He appears to consider the offer.","\n",{"->":"harris_asks_for_theory"},{"#f":5}],"#f":5}]}],{"#f":1}],"harris_presses_for_details":[["^\"You mean you've left yourself open,\" Harris answers. \"To pressure. Is that what you're saying?\"","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-1","flg":21},"ev","str","^No","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-2","flg":21},"ev","str","^Evade","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-3","flg":21},"ev","str","^Evade","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-4","flg":21},{"c-0":["^ ",{"->":".^.^.^.admit_open_to_pressure"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"I'm not saying anything of the sort,\" I snap back. \"What is this, Harris? You're accusing me of treachery but I don't see a shred of evidence for it! Why don't you put your cards on the table?\"","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^I shake my head violently, to say no, that's not it, but whatever is wrong with tongue is wrong with neck too. I look across at the table at Harris' face and realise with a start how sympathetic he is. Such a kind, generous man. How can I hold anything back from him?","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n","^I take another mouthful of the bitter, strange—tasting tea before answering.","\n",{"->":".^.^.^.admit_open_to_pressure"},{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ ","\n","^\"You're the one applying pressure here,\" I answer ","ev",{"VAR?":"forceful"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^smartly",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^somewhat miserably",{"->":".^.^.^.10"},null]}],"nop","^. \"I'm just waiting until you tell me what is really going on.\"","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.g-0"},{"#f":5}],"c-4":["^ \t\t\t\t ","\n","^\"We're all under pressure here.\"","\n","^He looks at me with pity. ",{"->":"harris_has_seen_it_before"},"\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"It's simple enough,\" Harris says. ",{"->":"harris_has_seen_it_before"},"\n",{"#f":5}]}],{"admit_open_to_pressure":["^\"That's it,\" I reply. \"There are some things... which a man shouldn't do.\"","\n","ev",true,"/ev",{"VAR=":"admitblackmail","re":true},"^Harris doesn't stiffen. Doesn't lean away, as though my condition might be infectious. I had thought they trained them in the army to shoot my kind on sight.","\n","^He offers no sympathy either. He nods, once. His understanding of me is a mere turning cog in his calculations, with no meaning to it.","\n",{"->":"harris_has_seen_it_before"},{"#f":1}],"#f":1}],"admitted_to_something":[["ev",{"VAR?":"drugged"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^Harris stares back at me. ","ev",{"VAR?":"evasive"},0,"==","/ev",[{"->":".^.b","c":true},{"b":["^He cannot have expected it to be so easy to break me.",{"->":".^.^.^.8"},null]}],"nop","\n",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["\n","^Harris smiles with satisfaction, as if your willingness to talk was somehow his doing.","\n",{"->":".^.^.^.6"},null]}],"nop","\n","^\"I see.\"","\n","^There's a long pause, like the delay between feeding a line of cypher into the Bombe and waiting for its valves to warm up enough to begin processing.","\n","^\"You want to explain that?\"","\n","ev","str","^Explain","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't explain","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-1","flg":21},"ev","str","^Lie","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-2","flg":21},"ev","str","^Evade","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-3","flg":21},"ev","str","^Say nothing","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-4","flg":21},{"c-0":["^ ","\n","^I pause a moment, trying to choose my words. To just come out and say it, after a lifetime of hiding... that is a circle I cannot square.","\n",["ev","str","^Explain","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Say nothing","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-1","flg":21},"ev","str","^Lie","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-2","flg":21},{"c-0":["^ \t",{"->":".^.^.^.^.^.ive_done_things"},"\n",{"#f":5}],"c-1":["^ \t",{"->":".^.^.^.^.c-4"},"\n",{"#f":5}],"c-2":["^ \t",{"->":"claim_hooper_took_component"},"\n",{"#f":5}]}],{"#f":5}],"c-1":["\n","^\"There's nothing to explain,\" I reply stiffly. ",{"->":".^.^.^.i_know_where"},"\n",{"#f":5}],"c-2":["^ ",{"->":"claim_hooper_took_component"},"\n",{"#f":5}],"c-3":["\n","^\"Explain what you should be doing, do you mean, rather than bullying me? Certainly.\" I fold my arms. ",{"->":".^.^.^.i_know_where"},"\n",{"#f":5}],"c-4":["\n","^I fold my arms, intended firmly to say nothing. But somehow, watching Harris' face, I cannot bring myself to do it. I want to confess. I want to tell him everything I can, to explain myself to him, to earn his forgiveness. The sensation is so strong my will is powerless in the face of it.","\n","^Something is wrong with me, I am sure of it. There is a strange, bitter flavour on my tongue. I taste it as words start to form.","\n",{"->":".^.^.^.ive_done_things"},{"#f":5}]}],{"i_know_where":["^\"I know where your component is because it's obvious where your component is. That doesn't mean I took it, just because I can figure out a simple problem, any more than it means I'm a German spy because I can crack their codes.\"","\n",{"->":"harris_asks_for_theory"},{"#f":1}],"ive_done_things":["^\"I've done things,\" I begin","ev",{"CNT?":"harris_demands_component.0.g-1.c-14"},"/ev",[{"->":".^.b","c":true},{"b":["^ helplessly",{"->":".^.^.^.5"},null]}],"nop","^. \"Things I didn't want to do. I tried not to. But in the end, it felt like cutting off my own arm to resist.\"","\n",{"->":"harris_presses_for_details"},{"#f":1}],"#f":1}],"harris_asks_for_theory":[["^\"Tell me, then,\" he asks. \"What's your theory? You're a smart fellow — as smart as they come around here, and that's saying something. What's your opinion on the missing component? Accident, perhaps? Or do you blame one of the other men? ","ev",{"VAR?":"hooper_mentioned"},"/ev",[{"->":".^.b","c":true},{"b":["^Hooper?",{"->":".^.^.^.5"},null]}],"nop","^\"","\n","ev","str","^Blame no—one","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Blame someone","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n",{"->":".^.^.^.an_accident"},{"#f":5}],"c-1":["^ ",{"->":"claim_hooper_took_component"},"\n",{"#f":5}]}],{"an_accident":[["^\"An accident, naturally.\" I risk a smile. \"That damned machine is made from spare parts and string. Even these Huts leak when it rains. It wouldn't take more than one fellow to trip over a cable to shake out a component. Have you tried looking under the thing?\"","\n","^\"Do you believe we haven't?\"","\n","^In a sudden moment I understand that his reply is a threat.","\n","^\"Now,\" he continues. \"Are you sure there isn't anything you want to tell me?\"","\n","ev","str","^Co-operate","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Evade","/str",{"VAR?":"evasive"},0,">","/ev",{"*":".^.c-1","flg":21},{"c-0":["\n","^\"All right.\" With a sigh, your defiance collapses. \"If you're searched my things then I suppose you've found ","ev",{"VAR?":"evasive"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^ what you need",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["^my letters. Haven't you? In fact, if you haven't, don't tell me",{"->":".^.^.^.9"},null]}],"nop","^.","\n","ev",true,"/ev",{"VAR=":"admitblackmail","re":true},"^Harris nods once.","\n","<>","^ ",{"->":"harris_has_seen_it_before"},"\n",{"#f":5}],"c-1":["^ \"Only that you're being unreasonable, and behaving like a swine.\"","\n","^\"You imbecile,\" Harris replies, with sudden force. He is half out of his chair. \"You know the situation as well as I do. Why the fencing? The Hun are poised like rats, ready to run all over this country. They'll destroy everything. You understand that, don't you? You're not so locked up inside your crossword puzzles that you don't see that, are you? This machine we have here — you men — you are the best and only hope this country has. God help her.\"","\n","ev",true,"/ev",{"VAR=":"losttemper","re":true},"^I sit back, startled by the force of his outburst. His carefully sculpted expression has curled to angry disgust. He really does hate me, I think. He'll have my blood for the taste of it.","\n",["ev","str","^Placate","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Mock","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Dismiss","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","^\"Now steady on,\" I reply, gesturing for him to be calm.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"I can imagine how being surrounded by clever men is pretty threatening for you, Commander,\" I reply with a sneer. \"They don't train you to think in the Armed Forces.\"","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["\n","^\"Then I'll be going, on and getting on with my job of saving her, shall I?\" I even rise half to my feet, before he slams the tabletop.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"Talk,\" Harris demands. \"Talk now. Tell me where you've hidden it or who you passed it to. Or God help me, I'll take your wretched pansy body to pieces looking for it.\"","\n",{"->":"harris_demands_you_speak"},{"#f":5}]}],{"#f":5}]}],{"#f":1}],"#f":1}],"harris_has_seen_it_before":[["^\"I've seen it before. A young man like you — clever, removed. The kind that doesn't go to parties. Who takes himself too seriously. Who takes things too far.\"","\n","^He slides his thumb between two fingers.","\n","^\"Now they own you.\"","\n","ev","str","^Agree","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Disagree","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-2","flg":21},"ev","str","^Apologise","/str",{"VAR?":"drugged"},{"VAR?":"forceful"},0,"<","&&","/ev",{"*":".^.c-3","flg":21},{"c-0":["^ ","\n","^\"What could I do?\" I'm shaking now. The night is cold and the heat—lamp in the Hut has been removed. \"","ev",{"VAR?":"forceful"},2,">","/ev",[{"->":".^.b","c":true},{"b":["^I won't",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^I don't want to",{"->":".^.^.^.10"},null]}],"nop","^ go to prison.\"","\n","^\"Smart man,\" he replies. \"You wouldn't last.","\n",{"->":".^.^.tell_me_now"},{"#f":5}],"c-1":["^ ","\n","^\"I can still fix this.\"","\n","^Harris shakes his head. \"You'll do nothing. This is beyond you now. You may go to prison or may go to firing squad - or we can change your name and move you somewhere where your indiscretions can't hurt you. But right now, none of that matters. What happens to you doesn't matter. All that matters is where that component is.","\n",{"->":".^.^.tell_me_now"},{"#f":5}],"c-2":["^ ","\n","^\"I wanted to tell you,\" I tell him. \"I thought I could find out who they were. Lead you to them.\"","\n","^Harris looks at me with contempt. \"You wretch. You'll pay for what you've done to this country today. If a single man loses his life because of your pride and your perversions then God help your soul.","\n",{"->":".^.^.tell_me_now"},{"#f":5}],"c-3":["\n","^\"Harris, I...\"","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n","^\"Stop it,\" he interrupts. \"There's no jury here to sway. And there's no time.","\n",{"->":".^.^.tell_me_now"},{"#f":5}],"tell_me_now":["<>","^ So why don't you tell me, right now. Where is it?\"","\n",{"->":"harris_demands_you_speak"},{"#f":5}]}],{"#f":1}],"harris_demands_you_speak":[["^His eyes bear down like carbonised drill—bits.","\n","ev","str","^Confess","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Dissemble","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-1","flg":21},"ev","str","^Dissemble","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-2","flg":21},{"c-0":["^ ","\n","ev",{"VAR?":"forceful"},1,">","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"You want me to tell you what happened? You'll be disgusted.\"","\n",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["\n","^\"All right. I'll tell you what happened.\" And never mind my shame.","\n",{"->":".^.^.^.9"},null]}],"nop","\n","^\"I can imagine how it starts,\" he replies.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ",{"->":"claim_hooper_took_component"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["\n","^My plan now is to blame Hooper, but I cannot seem to tell the story. Whatever they put in my tea, it rules my tongue. ","ev",{"VAR?":"forceful"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^I fight it as hard as I can but it does no good.",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["^I am desperate to tell him everything. I am weeping with shame.",{"->":".^.^.^.9"},null]}],"nop","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"i_met_a_young_man"},{"#f":5}]}],{"#f":1}],"i_met_a_young_man":[["ev","str","^Talk","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n","^\"There was a young man. I met him in the town. A few months ago now. We got to talking. Not about work. And I used my cover story, but he seemed to know it wasn't true. That got me wondering if he might be one of us.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^Harris is not letting me off any more.","\n","^\"You seriously entertained that possibility?\"","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-3","flg":20},{"c-1":["\n","^\"Yes, I considered it. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-2":["^ ","\n","^\"No. Not for more than a moment, of course. Everyone here is marked out by how little we would be willing to say about it.\"","\n","^\"Only you told this young man more than a little, didn't you?\"","\n","^I nod. \"","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-3":["^ ","\n","^\"I was quite certain, after a while. After we'd been talking. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":["^He seemed to know all about me. He... he was quite enchanted by my achievements.\"","\n","^The way Harris is staring I expect him to strike me, but he does not. He replies, \"I can see how that must have been attractive to you,\" with such plain—spokeness that I think I must have misheard.","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^No","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-5","flg":21},"ev","str","^No","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-6","flg":21},"ev","str","^Lie","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-7","flg":21},{"c-4":["^ \"It's a lonely life in this place,\" I reply. \"Lonely - and still one never gets a moment to oneself.\"","\n","^\"That's how it is in the Service,\" Harris answers.","\n",["ev","str","^Argue","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Agree","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ \"I'm not in the Service.\"","\n","^Harris shakes his head. \"Yes, you are.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ \"Perhaps. But I didn't choose this life.\" ","\n","^Harris shakes his head. \"No. And there's plenty of others who didn't who are suffering far worse.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^Then he waves the thought aside.","\n",{"->":".^.^.^.^.^.g-2"},{"#f":5}]}],{"#f":5}],"c-5":["^ \"The boy was a pretty simpleton. Quite inferior. His good opinion meant nothing to be. Harris, do not misunderstand. I was simply after his body.\"","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n","^Harris, to his credit, doesn't flinch; but I can see he will have nightmares of this moment later tonight. I'm tempted to reach out and take his hand to worsen it for him.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-6":["^ ","\n","^\"It wasn't,\" I reply. \"But I doubt you'd understand.\"","\n","^He simply nods.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-7":["^ ",{"->":".^.^.c-5"},"\n",{"->":".^.^.^.g-2"},{"#f":5}],"#f":5}],"g-2":["^\"Go on with your confession.\"","\n",["ev",{"CNT?":".^.^.^.g-1.c-5"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^That gives me pause. I hadn't thought of it as such. But I suppose he's right. I am about to admit what I did.","\n",{"->":".^.^.^.5"},null]}],"nop","\n","^\"There's not much else to say. I took the part from Bombe computing device. You seem to know that already. I had to. He was going to expose me if I didn't.\"","\n","^\"This young man was blackmailing you over your affair?\"","\n","ev",{"VAR?":"drugged"},"/ev",{"temp=":"harris_thinks_youre_drugged"},"ev",{"VAR?":"drugged"},"/ev",[{"->":".^.b","c":true},{"b":["\n","ev",false,"/ev",{"VAR=":"drugged","re":true},"^As Harris speaks I find myself suddenly sharply aware, as if waking from a long sleep. The table, the corrugated walls of the hut, everything seems suddenly more tangible than a moment before.","\n","^Whatever it was they put in my drink is wearing off.","\n",{"->":".^.^.^.19"},null]}],"nop","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-8","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-9","flg":20},"ev","str","^Tell the truth","/str","/ev",{"*":".^.c-10","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-11","flg":20},{"c-8":["^ ","\n","^\"Yes. I suppose he was their agent. I should have realised but I didn't. Then he threatened to tell you. I thought you would have me locked up: I couldn't bear the thought of it. I love working here. I've never been so happy, so successful, anywhere before. I didn't want to lose it.\"","\n","^\"So what did you do with the component?\" Harris talks urgently. He grips his gloves tightly in one hand, perhaps prepared to lift them and strike if it is required. \"Have you passed it to this man already? Have you left it somewhere for him to find?\"","\n",["ev","str","^I have it","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^I don't have it","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Tell the truth","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["^ \t","\n","^\"I still have it. Not on me, of course. ",{"->":"reveal_location_of_component"},"\n",{"#f":5}],"c-1":["^ \t",{"->":".^.^.^.^.^.^.^.i_dont_have_it"},"\n",{"#f":5}],"c-2":["^ \t\t\t\t\t\t\t",{"->":".^.^.c-1"},"\n",{"#f":5}],"c-3":["^ \t\t\t\t",{"->":".^.^.c-0"},"\n",{"#f":5}]}],{"#f":5}],"c-9":["^ ","\n","^\"No, Harris. The young man wasn't blackmailing me.\" I take a deep breath. \"It was Hooper.\"","\n","ev",{"VAR?":"hooper_mentioned"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"Hooper!\" Harris exclaims, in surprise. ","ev",{"VAR?":"harris_thinks_youre_drugged"},"/ev",[{"->":".^.b","c":true},{"b":["^He does not doubt me for a moment.",{"->":".^.^.^.6"},null]}],"nop","\n",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["\n","^\"Now look here,\" Harris interrupts. \"Don't start that again.\"","\n",{"->":".^.^.^.10"},null]}],"nop","\n","^\"It's the truth, Harris. If I'm going to jail, so be it, but I won't hang at Traitor's Gate. Hooper was the one who told the boy about our work. Hooper put the boy on to me. ","ev",{"VAR?":"forceful"},2,"<","/ev",[{"->":".^.b","c":true},{"b":["^I should have realised, of course. These things don't happen by chance. I was a fool to think they might.",{"->":".^.^.^.19"},null]}],"nop","^ And then, once he had me compromised, he demanded I steal the part from the machine.\"","\n","ev",true,"/ev",{"VAR=":"revealedhooperasculprit","re":true},"^\"Which you did.\" Harris leans forward. \"And then what? You still have it? You've stashed it somewhere?\"","\n",["ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["^ ","\n","^\"Yes. I only had a moment. ",{"->":"reveal_location_of_component"},"\n",{"#f":5}],"c-1":["^ ",{"->":".^.^.^.^.^.^.^.passed_onto_hooper"},"\n",{"#f":5}],"c-2":["^ \t\t\t",{"->":".^.^.c-1"},"\n",{"#f":5}],"c-3":["^ \t\t","\n","^\"I can't remember.\"","\n","^He draws his gun and lays it lightly on the field table.","\n","^\"I'm sorry to threaten you, friend. But His Majesty needs that brain of yours, and that brain alone. There are plenty of other parts to you that our country could do better without. Now I'll ask you again. Did you hide the component?\"","\n",["ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["^ ",{"->":".^.^.^.^.c-0"},"\n",{"#f":5}],"c-1":["^ ","\n","^\"Very well then.\" I swallow nervously, to make it look more genuine. ",{"->":"i_met_a_young_man.passed_onto_hooper"},"\n",{"#f":5}],"c-2":["^ ",{"->":".^.^.c-1"},"\n",{"#f":5}],"c-3":["^ ",{"->":"i_met_a_young_man.i_dont_have_it"},"\n",{"#f":5}]}],{"#f":5}]}],{"#f":5}],"c-10":["^ \t",{"->":".^.^.c-8"},"\n",{"#f":5}],"c-11":["^ \t\t\t\t",{"->":".^.^.c-9"},"\n",{"#f":5}],"#f":5,"#n":"paused"}],{"#f":5}]}],{"i_dont_have_it":[["^\"I don't have it any more. I passed it through the fence to my contact straight after taking it, before it was discovered to be missing. It would have been idiocy to do differently. It's long gone, I'm afraid.\"","\n","^\"You fool, Manning,\" Harris curses, getting quickly to his feet. \"You utter fool. Do you suppose you will be any better off living under Hitler? It's men like you who will get us all killed. Men too feeble, too weak in their hearts to stand up and take a man's responsibility for the world. You're happier to stay a child all your life and play with your little childish toys.\"","\n","ev","str","^Answer back","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Say nothing","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"Really, Commander,\" I reply. \"It rather sounds like you want to spank me.\"","\n","^\"For God's sake,\" he declares with thick disgust, then swoops away out of the room.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^I say nothing. It's true, isn't it? I can't deny that I know there is a world out there, a complicated world of pain and suffering. And I can't deny that I don't think about it a moment longer than I have to. What use is thinking on a problem that cannot be solved? It is precisely our ability to avoid such endless spirals that makes us human and not machine.","\n","^\"God have mercy on your soul,\" Harris says finally, as he gets to his feet and heads for the door. \"I fear no—one else will.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"left_alone"},{"#f":5}]}],{"#f":1}],"passed_onto_hooper":[["ev",true,"/ev",{"VAR=":"hooper_mentioned","re":true},"^\"No. I passed it on to Hooper.\"","\n","^\"I see. And what did he do with it?\"","\n","ev","str","^Evade","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Tell the truth","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^\"I don't know.\"","\n","^\"You can do better than that. Remember, there's a hangman's noose waiting for traitors.\"","\n",["ev","str","^Theorise","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Shrug","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"Well, then,\" I answer, nervously. \"What would he do? Either get rid of it straight away — or if that wasn't possible, which it probably wouldn't be, since he'd have to arrange things with his contacts — so most likely, he'd hide it somewhere and wait, until you had the rope around my neck and he could be sure he was safe.\"","\n",{"->":"claim_hooper_took_component.harris_being_convinced"},{"#f":5}],"c-1":["^ ",{"->":"claim_hooper_took_component.its_your_problem"},"\n",{"#f":5}]}],{"#f":5}],"c-1":["^ ","\n","^\"I don't think Hooper could have planned this in advance. So he'd need to get word to whoever he's working with, and that would take time. So I think he would have hidden it somewhere, and be waiting to make sure I soundly take the fall. That way, if anything goes wrong, he can arrange for the part to be conveniently re—found.\"","\n",{"->":"claim_hooper_took_component.harris_being_convinced"},{"#f":5}],"c-2":["\n","^\"I'm sure I saw him this evening, talking to someone by the fence on the woodland side of the compound. He's probably passed it on already. You'll have to ask him.\"","\n",{"->":"claim_hooper_took_component.harrumphs"},{"#f":5}]}],{"#f":1}],"#f":1}],"claim_hooper_took_component":[["^\"I saw Hooper take it.\"","\n","ev",true,"/ev",{"VAR=":"hooper_mentioned","re":true},"ev",{"VAR?":"losttemper"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^\"Did you?\"","\n","^The worst of his rage is passing; he is now moving into a kind of contemptuous despair. I can imagine him wrapping up our interview soon, leaving the hut, locking the door, and dropping the key down the well in the yard.","\n","^And why wouldn't he? With my name tarnished they will not let me back to work on the Bombe — if there is the slightest smell of treachery about my name I would be lucky not be locked up for the remainder of the war.","\n",{"->":".^.^.^.11"},null]}],[{"->":".^.b"},{"b":["\n","^\"I see.\" He is starting to lose his patience. I have seen Harris angry a few times, with lackeys and secretaries. But never with us. With the 'brains' he has always been cautious, treating us like children.","\n","^And now I see that, like a father, he wants to smack us when we disobey him.","\n",{"->":".^.^.^.11"},null]}],"nop","\n","^\"Just get to the truth, man. Every minute matters.\"","\n","ev","str","^Persist with this","/str",{"VAR?":"admitblackmail"},"/ev",{"*":".^.c-0","flg":21},"ev","str","^Tell the truth","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Persist with this","/str",{"VAR?":"admitblackmail"},"!","/ev",{"*":".^.c-2","flg":21},{"c-0":["\n","^\"I know what you're thinking. If I've transgressed once then I must be guilty of everything else... But I'm not. We were close to cracking the 13th's intercept. We were getting correlations in the data. Then Hooper disappeared for a moment, and next minute the machine was down.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"Very well. I see there's no point in covering up. You know everything anyway.\"","\n","^Harris nods, and waits for me to continue.","\n",{"->":"i_met_a_young_man"},{"->":".^.^.g-0"},{"#f":5}],"c-2":["\n","^\"This is the truth.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^I have become, somehow, an accustomed liar — the words roll easily off my tongue. Perhaps I am a traitor, I think, now that I dissemble as easily as one.","\n","^\"Go on,\" Harris says, giving me no indication of whether he believes my tale.","\n","ev","str","^Assert","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Imply","/str","/ev",{"*":".^.c-4","flg":20},{"c-3":["^ \"I saw him take it,\" I continue. \"Collins was outside having a cigarette. Peterson was at the table. But I was at the front of the machine. I saw Hooper go around the side. He leant down and pulled something free. I even challenged him. I said, 'What's that? Someone put a nail somewhere they shouldn't have?' He didn't reply.\"","\n","^Harris watches me for a long moment.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-4":["^ \"At the moment the machine halted, Peterson was at the bench and Collins was outside having a smoke. I was checking the dip—switches. Hooper was the only one at the back of the Bombe. No—one else could have done it.\"","\n","^\"That's not quite the same as seeing him do it,\" Harris remarks.","\n",["ev","str","^Logical","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Persuasive","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Confident","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","^\"When you have eliminated the impossible...\" I begin, but Harris cuts me off.","\n",{"->":".^.^.^.^.^.g-1"},{"#f":5}],"c-1":["^ ","\n","^\"You have to believe me.\"","\n","^\"We don't have to believe anyone,\" Harris returns. \"I will only be happy with the truth, and your story doesn't tie up. We know you've been leaving yourself open to pressure. We've been watching your activities for some time. But we thought you were endangering the reputation of this site; not risking the country herself. Perhaps I put too much trust in your intellectual pride.\"","\n","^He pauses for a moment, considering something. Then he continues:","\n","^\"It might have been Hooper. It might have been you. ",{"->":".^.^.^.^.^.we_wont_guess"},"\n",{"->":".^.^.^.^.^.g-1"},{"#f":5}],"c-2":["^ ","\n","^\"Ask the others,\" I reply, leaning back. \"They'll tell you. If they haven't already, that's only because they're protecting Hooper. Hoping he'll come to his senses and stop being an idiot. I hope he does too. And if you lock him up in a freezing hut like you've done me, I'm sure he will.\"","\n","^\"We have,\" Harris replies simply.","\n","^It's all I can do not to gape.","\n",{"->":".^.^.^.^.^.g-1.hoopers_hut_3"},{"->":".^.^.^.^.^.g-1"},{"#f":5}]}],{"#f":5}],"#f":5}],"g-1":["^\"We are left with two possibilities. You, or Hooper.\" The Commander pauses to smooth down his moustache. ","<>","\n",["^\"Hooper's in Hut 3 with the Captain, having a similar conversation.\"","\n",["ev",{"^->":"claim_hooper_took_component.0.g-1.hoopers_hut_3.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-5","flg":22},{"s":["^\"And the other men?",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.0.g-1.hoopers_hut_3.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-6","flg":22},{"s":["^\"Then you know I'm right.",{"->":"$r","var":true},null]}],{"c-5":["ev",{"^->":"claim_hooper_took_component.0.g-1.hoopers_hut_3.c-5.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"^ Do we have a hut each? Are there enough senior officers to go round?\"","\n","^\"Collins was outside when it happened, and Peterson can't get round the machine in that chair of his,\" Harris replies. \"That leaves you and Hooper.","\n",{"->":".^.^.^.^.we_wont_guess"},{"#f":5}],"c-6":["ev",{"^->":"claim_hooper_took_component.0.g-1.hoopers_hut_3.c-6.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"^ You knew all along. Why did you threaten me?\"","\n","^\"All we know is that we have a traitor, holding the fate of the country in his hands.","\n",{"->":".^.^.^.^.we_wont_guess"},{"#f":5}],"#f":5,"#n":"hoopers_hut_3"}],{"#f":5}],"we_wont_guess":["<>","^ We're not in the business of guessing here at Bletchley. We are military intelligence. We get answers.\" Harris points a finger. \"And if that component has left these grounds, then every minute is critical.\"","\n","ev","str","^Co-operate","/str","/ev",{"*":".^.c-7","flg":20},"ev","str","^Block","/str","/ev",{"*":".^.c-8","flg":20},{"c-7":["^ ","\n","^\"I'd be happy to help,\" I answer, leaning forwards. \"I'm sure there's something I could do.\"","\n","^\"Like what, exactly?\"","\n",[["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"Put me in with Hooper.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-1","flg":22},{"s":["^\"Tell Hooper I've confessed.",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.^.^.^.^.putmein"},{"#f":5}],"c-1":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"^ Better yet. Let him see you marching me off in handcuffs. Then let him go, and see what he does. Ten to one he'll go straight to wherever he's hidden that component and his game will be up.\"","\n","^Harris nods slowly, chewing over the idea. It isn't a bad plan even — except, of course, Hooper has not hidden the component, and won't lead them anywhere. But that's a problem I might be able to solve once I'm out of this place; and once they're too busy dogging Hooper's steps from hut to hut.","\n","^\"Interesting,\" the Commander muses. \"But I'm not so sure he'd be that stupid. And if he's already passed the part on, the whole thing will only be a waste of time.\"","\n",[["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-0","flg":22},{"s":["^\"Trust me. He hasn't.",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str","/ev",{"*":".^.^.c-1","flg":22},{"s":["^\"You're right. Let me talk to him",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":18},{"s":["^\"You're right.\" ",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"^ If I know that man, and I do, he'll be wanting to keep his options open as long as possible. If the component's gone then he's in it up to his neck. He'll take a week at least to make sure he's escaped suspicion. Then he'll pass it on.\"","\n","^\"And if we keep applying pressure to him, you think the component will eventually just turn up?\"","\n",[["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-0","flg":22},{"s":["^\"Yes.",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"Or be thrown into the river.\" ",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"^ Probably under my bunk.\"","\n","^Harris smiles wryly. \"We'll know that for a fake, then. We've looked there already.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n","^\"Hmm.\" Harris chews his moustache thoughtfully. \"Well, that would put us in a spot, seeing as how we'd never know for certain. We'd have to be ready to change our whole approach just in case the part had got through to the Germans.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["<>","^ I don't mind telling you, this is a disaster, this whole thing. What I want is to find that little bit of mechanical trickery. I don't care where. In your luncheon box or under Hooper's pillow. Just somewhere, and within the grounds of this place.\"","\n",["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.g-0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-2","flg":22},{"s":["^\"Then let him he think he's off the hook.",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.g-0.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str","/ev",{"*":".^.^.c-3","flg":22},{"s":["^\"Then you'd better get searching",{"->":"$r","var":true},null]}],{"c-2":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.g-0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"^ Make a show of me. And then you'll get your man.\"","\n","^Somehow, I think. But that's the part I need to work.","\n",{"->":"harris_takes_you_to_hooper"},{"#f":5}],"c-3":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.g-0.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.4.s"},[{"#n":"$r2"}],"^,\" I reply, tiring of his complaining. A war is a war, you have to expect an enemy. ",{"->":".^.^.^.^.^.^.^.^.^.^.^.its_your_problem"},"\n",{"#f":5}],"#f":5}]}],{"#f":5}],"c-1":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"^, then. As a colleague. Maybe I can get something useful out of him.\"","\n",{"->":".^.^.^.^.^.^.^.^.putmein"},{"#f":5}],"c-2":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],{"->":".^.^.^.^.^.^.^.^.shake_head"},"\n",{"#f":5}]}],{"#f":5}]}],{"#f":5}],"c-8":["^ ",{"->":".^.^.^.^.its_your_problem"},"\n",{"#f":5}],"#f":5}]}],{"harris_being_convinced":[["^\"Makes sense,\" Harris agrees, cautiously. ","ev",{"VAR?":"evasive"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^I can see he's still not entirely convinced by my tale, as well he might not be — I've hardly been entirely straight with him.",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["^I can see he's still not certain whether he can trust me.",{"->":".^.^.^.8"},null]}],"nop","^ \"Which means the question is, what can we do to rat him out?\"","\n","ev","str","^Offer to help","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't offer to help","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"Maybe I can help with that.\"","\n","^\"Oh, yes? And how, exactly?\"","\n",[["ev",{"^->":"claim_hooper_took_component.harris_being_convinced.0.c-0.6.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"I'll talk to him.\" ",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.harris_being_convinced.0.c-0.6.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-1","flg":22},{"s":["^\"We'll fool him.",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"claim_hooper_took_component.harris_being_convinced.0.c-0.6.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n","^\"What?\"","\n","^\"Put me in with Hooper with him. Maybe I can get something useful out of him.\"","\n",{"->":".^.^.^.^.^.^.putmein"},{"#f":5}],"c-1":["ev",{"^->":"claim_hooper_took_component.harris_being_convinced.0.c-0.6.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"^ He's waiting to be sure that I've been strung up for this, so let's give him what he wants. If he sees me taken away, clapped in irons — he'll go straight to that component and set about getting rid of it.\"","\n",{"->":"harris_takes_you_to_hooper"},{"#f":5}]}],{"#f":5}],"c-1":["\n","^I lean back. ",{"->":".^.^.^.^.its_your_problem"},"\n",{"#f":5}]}],{"#f":1}],"putmein":[["^Harris shakes his head.","\n","^\"He despises you. I don't see why he'd give himself up to you.\"","\n","ev","str","^Insist","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Give in","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ \"Try me. Just me and him.\" ","\n",{"->":".^.^.^.^.go_in_alone"},{"#f":5}],"c-1":["^ \"You're right.\" ","\n",{"->":".^.^.^.^.shake_head"},{"#f":5}]}],{"#f":1}],"shake_head":["<>","^ I shake my head. \"You're right. I don't see how I can help you. So there's only one conclusion.\"","\n","^\"Oh, yes? And what's that?\"","\n",{"->":".^.^.its_your_problem"},{"#f":1}],"its_your_problem":["^\"It's your problem. Your security breach. So much for your careful vetting process.\"","\n","^I lean back in my chair and fold my arms so the way they shake will not be visible.","\n","^\"You'd better get on with solving it, instead of wasting your time in here with me.\"","\n",{"->":".^.^.harrumphs"},{"#f":1}],"harrumphs":[["^Harris harrumphs. He's thinking it all over.","\n","ev","str","^Wait","/str",{"CNT?":".^.^.^.putmein"},"/ev",{"*":".^.c-0","flg":21},"ev","str","^Wait","/str",{"CNT?":".^.^.^.putmein"},"/ev",{"*":".^.c-1","flg":21},{"c-0":["^ ","\n","^\"All right,\" he declares, gruffly. \"We'll try it. But if this doesn't work, I might just put the both of you in front of a firing squad and be done with these games. Worse things happen in time of war, you know.\"","\n","^\"Alone,\" I add.","\n",{"->":".^.^.^.^.go_in_alone"},{"#f":5}],"c-1":["^ ","\n","^\"No,\" Harris declares, finally. \"I think you're lying about Hooper. I think you're a clever, scheming young man — that's why we hired you — and you're looking for the only reasonable out this situation has to offer. But I'm not taking it. We know you were in the room with the machine, we know you're of a perverted persuasion, we know you have compromised yourself. There's nothing more to say here. Either you tell me what you've done with that component, or we will hang you and search just as hard. It's your choice.\"","\n",{"->":"harris_threatens_lynching"},{"#f":5}]}],{"#f":1}],"go_in_alone":[["^\"Alone?\"","\n","^\"Alone.\"","\n","^Harris considers it. I watch his eyes, flicking backwards and forwards over mine, like a ribbon—reader loading its program.","\n","ev","str","^Patient","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Impatient","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ \"Well?\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ \"For God's sake, man, what do you have to lose?\" ","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"We'll be outside the door,\" Harris replies, seriously. \"The first sign of any funny business and we'll have you both on the floor in minutes. You understand? The country needs your brain, but it's not too worried about your legs. Remember that.\"","\n","^Then he gets to his feet, and opens the door, and marches me out across the yard. The evening is drawing in and there's a chill in the air. My mind is racing. I have one opportunity here — a moment in which to put the fear of God into Hooper and make him do something foolish that places him in harm's way. But how to achieve it?","\n","^\"You ready?\" Harris demands.","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-4","flg":20},{"c-2":["\n","^\"Absolutely.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-3":["\n","^\"No.\"","\n","^\"Too bad.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-4":["^ ",{"->":".^.^.c-2"},"\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":[{"->":"inside_hoopers_hut"},{"#f":5}]}],{"#f":1}],"#f":1}],"harris_takes_you_to_hooper":[["^Harris gets to his feet. \"All right,\" he says. \"I should no better than to trust a clever man, but we'll give it a go.\"","\n","^Then, he smiles, with all his teeth, like a wolf.","\n","ev",{"CNT?":"claim_hooper_took_component.0.g-1.hoopers_hut_3"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^\"Especially since this is a plan that involves keeping you in handcuffs. I don't see what I have to lose.\"","\n",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["\n","^\"Hooper's in Hut 3 being debriefed by the Captain. Let's see if we can't get his attention somehow.\"","\n",{"->":".^.^.^.9"},null]}],"nop","\n","^He raps on the door for the guard and gives the man a quick instruction. He returns a moment later with a cool pair of iron cuffs.","\n","^\"Put 'em up,\" Harris instructs, and I do so. The metal closes around my wrists like a trap. I stand and follow Harris willingly out through the door.","\n","^But whatever I'm doing with my body, my mind is scheming. Somehow, I'm thinking, I have to get away from these men long enough to get that component behind Hut 2 and put it somewhere Hooper will go. Or, otherwise, somehow get Hooper to go there himself...","\n","^Harris marches me over to Hut 3, and gestures for the guard to stand aside. Pushing me forward, he opens the door nice and wide.","\n","^\"Captain. Manning talked. If you'd step out for a moment?\"","\n","ev","str","^Play the part, head down","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Look inside the hut","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Call to Hooper","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","^From where he's sitting, I know Hooper can see me, so I keep my head down and look guilty as sin. The bastard is probably smiling.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["\n","^I look in through the door and catch Hooper's expression. I had half expected him to be smiling be he isn't. He looks shocked, almost hurt. \"Iain,\" he murmurs. \"You couldn't...\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^I have a single moment to shout something to Hooper before the door closes.","\n","^\"I'll get you Hooper, you'll see!\" I cry. Then:","\n",[["ev",{"^->":"harris_takes_you_to_hooper.0.c-2.6.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"Queen to rook two, checkmate!\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"harris_takes_you_to_hooper.0.c-2.6.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"^ I call, then laugh viciously, as if I am damning him straight to hell.","\n","ev",2,"/ev",{"VAR=":"hooperClueType","re":true},{"->":".^.^.only_catch"},{"#f":5}],"only_catch":["^I only catch Hooper's reaction for a moment — his eyebrow lifts in surprise and alarm. Good. If he thinks it is a threat then he just might be careless enough to go looking for what it might mean.","\n",["ev",{"^->":"harris_takes_you_to_hooper.0.c-2.6.only_catch.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"Ask not for whom the bell tolls!\"",{"->":"$r","var":true},null]}],["ev",{"^->":"harris_takes_you_to_hooper.0.c-2.6.only_catch.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":18},{"s":["^\"Two words: messy, without one missing!\"",{"->":"$r","var":true},null]}],{"c-1":["ev",{"^->":"harris_takes_you_to_hooper.0.c-2.6.only_catch.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","^He stares back at me, as if were a madman and perhaps for a split second I see him shudder.","\n",{"->":".^.^.^.^.^.g-0"},{"#f":5}],"c-2":["ev",{"^->":"harris_takes_you_to_hooper.0.c-2.6.only_catch.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"^ I cry, laughing. It isn't the best clue, hardly worthy of The Times, but it will have to do.","\n","ev",3,"/ev",{"VAR=":"hooperClueType","re":true},{"->":".^.^"},{"->":".^.^.^.^.^.g-0"},{"#f":5}],"#f":5}]}],{"#f":5}],"g-0":["^The Captain comes outside, pulling the door to. \"What's this?\" he asks. \"A confession? Just like that?\"","\n","^\"No,\" the Commander admits, in a low voice. \"I'm afraid not. Rather more a scheme. The idea is to let Hooper go and see what he does. If he believes we have Manning here in irons, he'll try to shift the component.\"","\n","^\"If he has it.\"","\n","^\"Indeed.\"","\n","^The Captain peers at me for a moment, like I was some kind of curious insect.","\n","^\"Sometimes, I think you people are magicians,\" he remarks. \"Other times you seem more like witches. Very well.\"","\n","^With that he opens the door to the Hut and goes back inside. The Commander uses the moment to hustle me roughly forward.","\n","ev",{"CNT?":".^.^.c-2"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^\"And what was all that shouting about?\" he hisses in my ear as we move towards the barracks. \"Are you trying to pull something? Or just make me look incompetent?\"","\n",{"->":".^.^.^.19"},null]}],[{"->":".^.b"},{"b":["\n","^\"This scheme of yours had better come off,\" he hisses in my ear. \"Otherwise the Captain is going to start having men tailing me to see where I go on Saturdays.\"","\n",{"->":".^.^.^.19"},null]}],"nop","\n","ev","str","^Reassure","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Dissuade","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Say nothing","/str","/ev",{"*":".^.c-6","flg":20},{"c-3":["^ ","\n","ev",{"CNT?":".^.^.^.c-2"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"It will. Hooper's running scared,\" I reply, hoping I sound more confident than I feel.","\n",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["\n","^\"Just adding to the drama,\" I tell him, confidently. \"I'm sure you can understand that.\"","\n",{"->":".^.^.^.8"},null]}],"nop","\n","^\"I think we've had enough drama today already,\" Harris replies. \"Let's hope for a clean kill.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-4":["^ ","\n","ev",{"CNT?":".^.^.^.c-2"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"The Captain thought it was a good scheme. You'll most likely get a promotion.\"","\n",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["\n","^\"I'm not trying to do anything except save my neck.\"","\n",{"->":".^.^.^.8"},null]}],"nop","\n","^\"Let's hope things work out,\" Harris agrees darkly.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-5":["^ ","\n","^\"We're still in ear—shot if they let Hooper go. Best get us inside and then we can talk, if we must.\"","\n","^\"I've had enough of your voice for one day,\" Harris replies grimly. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-6":["\n","^I let him have his rant. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":["^He hustles me up the steps of the barracks, keeping me firmly gripped as if I had any chance of giving him, a trained military man, the slip. It's all I can do not to fall into the room.","\n",{"->":"slam_door_shut_and_gone"},{"#f":5}]}],{"#f":1}],"inside_hoopers_hut":[[["^Harris opens the door and pushes me inside. \"Captain,\" he calls. \"Could I have a moment?\"","\n","^The Captain, looking puzzled, steps out. The door is closed. Hooper stares at me, open—mouthed, about to say something. I probably have less than a minute before the Captain storms back in and declares this plan to be bunkum.","\n","ev","str","^Threaten","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Bargain","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Plead","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","^\"Listen to me, Hooper. We were the only men in that hut today, so we know what happened. But I want you to know this. I put the component inside a breeze—block in the foundations of Hut 2, wrapped in one of your shirts. They're going to find it eventually, and that's going to be what tips the balance. And there's nothing you can do to stop any of that from happening.\"","\n","ev",1,"/ev",{"VAR=":"hooperClueType","re":true},"^His eyes bulge with terror. \"What did I do, to you? What did I ever do?\"","\n",["ev","str","^Tell the truth","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^\"You treated me like vermin. Like something abhorrent.\"","\n","^\"You are something abhorrent.\"","\n","^\"I wasn't. Not when I came here. And I won't be, once you're gone.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"Nothing,\" I reply. \"You're just the other man in the room. One of us has to get the blame.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^\"It doesn't matter. Just remember what I said. I've beaten you, Hooper. Remember that.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^I get to my feet and open the door of the Hut. The Captain storms back inside and I'm quickly thrown out. ",{"->":".^.^.^.^.^.^.hustled_out"},"\n",{"#f":5}]}],{"#f":5}],"c-1":["^ ","\n","^\"Hooper, I'll make a deal with you. We both know what happened in that hut this afternoon. I know because I did it, and you know because you know you didn't. But once this is done I'll be rich, and I'll split that with you. I'll let you have the results, too. Your name on the discovery of the Bombe. And it won't hurt the war effort — you know as well as me that the component on its own is worthless, it's the wiring of the Bombe, the usage, that's what's valuable. So how about it?\"","\n","^Hooper looks back at me, appalled. \"You're asking me to commit treason?\"","\n",["ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["\n","^\"Yes, perhaps. But also to ensure your name goes down in the annals of mathematics. ",{"->":".^.^.^.^.^.^.back_of_hut_2"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"No. It's not treason. It's a trade, plain and simple.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^\"I'm suggesting you save your own skin. I've wrapped that component in one of your shirts, Hooper. They'll be searching this place top to bottom. They'll find it eventually, and when they do, that's the thing that will swing it against you. So take my advice now. Hut 2.\"","\n","ev",1,"/ev",{"VAR=":"hooperClueType","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ ",{"->":".^.^.c-2"},"\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":".^.^.^.^.^.^.no_chance"},{"#f":5}]}],{"#f":5}],"c-2":["^ ","\n","^\"Please, Hooper. You don't understand. They have information on me. I don't need to tell you what I've done, you know. Have a soul. And the component — it's nothing. It's not the secret of the Bombe. It's just a part. The German's think it's a weapon — a missile component. Let them have it. Please, man. Just help me.\"","\n","^\"Help you?\" Hooper stares. \"Help you? You're a traitor. A snake in the grass. And you're queer.\"","\n",["ev","str","^Deny","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Accept","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^\"I'm no traitor. You know I'm not. How much work have I done here against the Germans? I've given my all. And you know as well as I do, if the Reich were to invade, I would be a dead man. Please, Hooper. I'm not doing any of this lightly.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["\n","^\"I am what I am,\" I reply. \"I'm the way I was made. But they'll hang me unless you help, Hooper. Don't let them hang me.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^\"That's not important now. What matters is what you do, this evening.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"Assuming I wanted to help you,\" he replies, carefully. \"Which I don't. What would I do?\"","\n","^\"Nothing. Almost nothing.","\n",{"->":".^.^.^.^.^.^.back_of_hut_2"},{"#f":5}]}],{"#f":5}],"#f":5,"#n":"g-0"}],null],{"back_of_hut_2":["<>","^ All you have to do is go to the back of Hut 2. There's a breeze—block with a cavity. That's where I've put it. I'll be locked up overnight. But you can pick it up and pass it to my contact. He'll be at the south fence around two AM.\"","\n","ev",1,"/ev",{"VAR=":"hooperClueType","re":true},{"->":".^.^.no_chance"},{"#f":1}],"no_chance":["^\"If you think I'll do that then you're crazy,\" Hooper replies.","\n","^At that moment the door flies open and the Captain comes storming back inside.","\n",{"->":".^.^.hustled_out"},{"#f":1}],"hustled_out":["^Harris hustles me over to the barracks. \"I hope that's the end of it,\" he mutters.","\n","^\"Just be sure to let him out,\" I reply. \"And then see where he goes.\"","\n",{"->":"slam_door_shut_and_gone"},{"#f":1}],"#f":1}],"slam_door_shut_and_gone":[["^Then they slam the door shut, and it locks.","\n","ev",{"VAR?":"hooperClueType"},0,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","<>","^ How am I supposed to manage anything from in here?","\n","ev","str","^Try the door","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Try the windows","/str","/ev",{"*":".^.c-1","flg":20},{"->":".^.^.^.9"},{"c-0":["^ ",{"->":".^.^.^.^.^.try_the_door"},"\n",{"#f":5}],"c-1":["^ ",{"->":".^.^.^.^.^.try_the_windows"},"\n",{"#f":5}]}]}],[{"->":".^.b"},{"b":["\n","^I can only hope that Hooper bites. If he thinks I'm bitter enough to have framed him, and arrogant enough to have taunted him with ","ev",{"VAR?":"hooperClueType"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^a clue to",{"->":".^.^.^.8"},null]}],"nop","^ where the damning evidence is hidden...","\n","^If he hates me enough, and is paranoid enough, then he might ","ev",{"VAR?":"hooperClueType"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^unravel my little riddle and",{"->":".^.^.^.18"},null]}],"nop","^ go searching around Hut 2.","\n",{"->":".^.^.^.9"},null]}],"nop","\n","ev","str","^Wait","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["^ \t",{"->":"night_falls"},"\n",{"#f":5}]}],{"try_the_door":["^I try the door. It's locked, of course.","\n",{"->":".^.^.from_outside_heard"},{"#f":1}],"from_outside_heard":[["^From outside, I hear a voice. Hooper's. He's haranguing someone.","\n",["ev","str","^Listen at the keyhole","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Try the window","/str",{"CNT?":".^.^.^.^.try_the_windows"},"!","/ev",{"*":".^.c-1","flg":21},"ev","str","^Try the door","/str",{"CNT?":".^.^.^.^.try_the_door"},"!",{"CNT?":".^.c-0"},"&&","/ev",{"*":".^.c-2","flg":21},"ev","str","^Smash the window","/str",{"CNT?":".^.^.^.^.try_the_windows"},"/ev",{"*":".^.c-3","flg":21},"ev","str","^Wait","/str",{"CNT?":".^.^.^.^.try_the_door"},{"CNT?":".^.^.^.^.try_the_windows"},"&&","/ev",{"*":".^.c-4","flg":21},{"c-0":["^ ","\n","^I put my ear down to the keyhole, but there's nothing now. Probably still a guard outside, of course, but they're keeping mum.","\n",{"->":".^.^"},{"#f":5}],"c-1":["^ ",{"->":".^.^.^.^.^.try_the_windows"},"\n",{"#f":5}],"c-2":["^ ",{"->":".^.^.^.^.^.try_the_door"},"\n",{"#f":5}],"c-3":["^ ",{"->":".^.^.^.^.^.try_to_smash_the_window"},"\n",{"#f":5}],"c-4":["^ ","\n","^It's useless. There's nothing I can do but hope. I sit down on one corner of the bunk to wait.","\n",{"->":"night_falls"},{"#f":5}],"#f":5,"#n":"opts"}],null],{"#f":1}],"try_the_windows":["^I go over to the window and try to jimmy it open. Not much luck, but in my struggling I notice this window only backs on the thin little brook that runs down the back of the compound. Which means, if I smashed it, I might get away with no—one seeing.","\n",{"->":".^.^.from_outside_heard"},{"#f":1}],"try_to_smash_the_window":[["^The window is my only way out of here. I just need a way to smash it.","\n","ev","str","^Punch it","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Find something","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Use something you've got","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^I suppose my fist would do a good enough job. But I'd cut myself to ribbons, most likely. ","<>","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","ev",2,"/ev",{"VAR=":"smashingWindowItem","re":true},"^I cast around the small room. There's a bucket in one corner for emergencies — I suppose I could use that. I pick it up but it's not very easy to heft. ","<>","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^I pat down my pockets but all I'm carrying is the intercept, which is no good at all.","\n",["ev","str","^Something you're wearing?","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Look around","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^Ah, but of course! I slip off one shoe and heft it by the toe. The heel will make a decent enough hammer, if I give it enough wallop.","\n","ev",1,"/ev",{"VAR=":"smashingWindowItem","re":true},"^But I'll cut my hand to ribbons doing it. ","<>","\n",{"->":".^.^.^.^.g-0"},{"#f":5}],"c-1":["^ ",{"->":".^.^.^.^.c-1"},"\n",{"->":".^.^.^.^.g-0"},{"#f":5}]}],{"#f":5}],"g-0":["^And the noise would be terrible. There must be a way of making this easier. I'm supposed to be a thief now. What would a burglar do?","\n","ev","str","^Work slowly","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Find something to help","/str","/ev",{"*":".^.c-4","flg":20},{"c-3":["^ ","\n","^Work carefully? It's difficult to work carefully when all one's has is ","ev",{"VAR?":"smashingWindowItem"},2,"==","/ev",[{"->":".^.b","c":true},{"b":["^a bucket. It's rather like the sledgehammer for the proverbial nut",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["ev",{"VAR?":"smashingWindowItem"},1,"==","/ev",[{"->":".^.b","c":true},{"b":["^a shoe",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^nothing but brute force",{"->":".^.^.^.7"},null]}],"nop",{"->":".^.^.^.10"},null]}],"nop","^.","\n",["ev","str","^Just do it","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Look around for something","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ",{"->":".^.^.^.^.^.^.^.time_to_move_now"},"\n",{"->":".^.^.^.^.^.g-1"},{"#f":5}],"c-1":["^ ","\n",{"->":".^.^.^.^.^.g-1"},{"#f":5}]}],{"#f":5}],"c-4":["^ ","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":[{"->":".^.^.^.^.find_something_to_smash_window"},{"#f":5}]}],{"#f":1}],"time_to_move_now":[["^Enough of this. There isn't any time to lose. Right now they'll be following Hooper as he goes to bed, and goes to sleep; and then that's it. The minute he closes his eyelids and drifts off that's the moment that this trap swings shut on me.","\n","^So I punch out the glass with my ","ev",{"VAR?":"smashingWindowItem"},2,"==","/ev",[{"->":".^.b","c":true},{"b":["^bucket",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["ev",{"VAR?":"smashingWindowItem"},1,"==","/ev",[{"->":".^.b","c":true},{"b":["^shoe",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^fist",{"->":".^.^.^.7"},null]}],"nop",{"->":".^.^.^.10"},null]}],"nop","^ and it shatters with a terrific noise. Then I stop, and wait, to see if anyone will come in through the door.","\n","^Nothing.","\n","ev","str","^Wait a little longer","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Clear the frame of shards","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^I pause for a moment longer. It doesn't do to be too careless...","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["\n","^With my jacket wrapped round my arm, I sweep out the remaining shards of glass. It's not a big window, but I'm not a big man. If I was Harris, I'd be stuffed, but as it is...","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^Then the door locks turns. The door opens. Then Jeremy — one of the guards, rather — sticks his head through the door. \"I thought I heard...\"","\n","^He stops. Looks for a moment. ","ev",{"VAR?":"smashingWindowItem"},2,"==","/ev",[{"->":".^.b","c":true},{"b":["^Sees the bucket in my hand.",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^Sees the broken window.",{"->":".^.^.^.10"},null]}],"nop","^ Then without a moment's further thought he blows his shrill whistles and hustles into the hut, grabbing me roughly by my arms.","\n","ev",{"CNT?":".^.^.c-0"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^I'll never know if I hadn't have waited that extra moment — maybe I still could have got away. But, how far?","\n",{"->":".^.^.^.17"},null]}],"nop","\n","^I'm hustled into one of the huts. Nowhere to sleep, but they're not interested in my comfort any longer. Harris comes in with the Captain.","\n","^\"So,\" Harris remarks. \"Looks like your little trap worked. Only it worked to show you out for what you are.\"","\n","ev","str","^Tell the truth","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-4","flg":20},{"c-2":["^ ","\n","ev",{"CNT?":"i_met_a_young_man"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^\"Please, Harris. You can't understand the pressure they put me under. You can't understand what it's like, to be in love but be able to do nothing about it...\"","\n",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["\n","^\"Harris. They were blackmailing me. They knew about... certain indiscretions. You can understand, can't you, Harris? I was in an impossible bind...\"","\n",{"->":".^.^.^.7"},null]}],"nop","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-3":["\n","^\"I had to get out, Harris. I had to provoke Hooper into doing something that would incriminate himself fully. He's too clever, you see...\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-4":["^ ","\n","^\"This proves nothing,\" I reply stubbornly. \"You still don't have the component and without it, I don't see what you can hope to prove.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":["^\"Be quiet, man. We know all about your and your sordid affairs.\" The Captain curls his lip. \"Don't you know there's a war on? Do you know the kind of place they would have sent you if it haven't had been for that brain of yours? Don't you think you owe it to your country to use it a little more?\"","\n","^Do I, I wonder? Do I owe this country anything, this country that has spurned who and what am I since the day I became a man?","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-7","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-8","flg":20},{"c-5":["^ ","\n","^My anger deflates like a collapsing equation, all arguments cancelling each other out. The world, of course, owes me nothing; and I owe it everything.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-6":["^ ","\n","^Of course not. I am alone; that is what they wanted me to be, because of who and what I love. So I have no nation, no country.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-7":["^ \t",{"->":".^.^.c-6"},"\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-8":["^ \t","\n","^But what is a country, after all? A country is not a concept, not an ideal. Every country falls, its borders shift and move, its language disappears to be replaced by another. Neither the Reich nor the British Empire will survive forever, so what use is my loyalty to either? ","\n","^I may as well, therefore, look after myself. Something I have attempted, but failed miserably, to do.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"#f":5}],"g-2":["^\"I'm afraid we have only one option, Manning,\" Harris says. \"Please, man. Tell us where the component is.\"","\n","ev",true,"/ev",{"VAR=":"notraitor","re":true},"ev",false,"/ev",{"VAR=":"losttemper","re":true},"ev","str","^Tell them","/str","/ev",{"*":".^.c-9","flg":20},"ev","str","^Say nothing","/str","/ev",{"*":".^.c-10","flg":20},{"c-9":["\n","ev",false,"/ev",{"VAR=":"revealedhooperasculprit","re":true},"^\"All right.\" I am beaten, after all. \"","<>",{"->":"reveal_location_of_component"},"\n",{"#f":5}],"c-10":["^ ",{"->":"my_lips_are_sealed"},"\n",{"#f":5}],"#f":5}]}],{"#f":1}],"find_something_to_smash_window":[["^Let me see. There's the bunk, ","ev",{"VAR?":"smashingWindowItem"},"!",2,"==","/ev",[{"->":".^.b","c":true},{"b":["^a bucket,",{"->":".^.^.^.8"},null]}],"nop","^ nothing else. I have my jacket but nothing in the pockets — no handkerchief, for instance.","\n",["ev","str","^The bunk","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^The jacket","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^The bucket","/str",{"VAR?":"smashingWindowItem"},"!",2,"==","/ev",{"*":".^.c-2","flg":21},{"c-0":["^ \t","\n","^The bunk has a solid metal frame, a blanket, a pillow, nothing more.","\n",[["ev","str","^The frame","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^The blanket","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^The pillow","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Something else","/str",{"CNT?":".^"},1,">","/ev",{"*":".^.c-3","flg":21},{"c-0":["\n","^The frame is heavy and solid. I couldn't lift it or shift it without help from another man. And it wouldn't do me any good here anyway. I can reach the window perfectly well.","\n",{"->":".^.^"},{"#f":5}],"c-1":["^ ","\n","^The blanket. Perfect. I scoop it up off the bed and hold it in place over the window. ",{"->":"smash_the_window"},"\n",{"#f":5}],"c-2":["^ ","\n","^The pillow is fat and fluffy. I could put it over the window and it would muffle the sound of breaking glass, certainly; but I wouldn't be able to break any glass through it either.","\n",{"->":".^.^"},{"#f":5}],"c-3":["^ ",{"->":".^.^.^.^.^"},"\n",{"#f":5}],"#f":5,"#n":"bunk_opts"}],null],{"#f":5}],"c-1":["^ ","\n","^I slip off my jacket and hold it with one hand over the glass. ",{"->":"smash_the_window"},"\n",{"#f":5}],"c-2":["^ ","\n","^The bucket? Hardly. The bucket might do some good if I wanted to sweep up the glass afterwards, but it won't help me smash the glass quietly.","\n",{"->":".^.^"},{"#f":5}],"#f":5,"#n":"opts"}],null],{"#f":1}],"#f":1}],"smash_the_window":[["^Then I heft ","ev",{"VAR?":"smashingWindowItem"},2,"==","/ev",[{"->":".^.b","c":true},{"b":["^up the bucket — this really is quite a fiddly thing to be doing in cuffs — ",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["ev",{"VAR?":"smashingWindowItem"},1,"==","/ev",[{"->":".^.b","c":true},{"b":["^ my shoe by its toe, ",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^back my arm, ",{"->":".^.^.^.7"},null]}],"nop",{"->":".^.^.^.8"},null]}],"nop","^ and take a strong swing, trying to imagine it's Harris' face on the other side.","\n","ev",true,"/ev",{"VAR=":"smashedglass","re":true},"ev",0,"/ev",{"VAR=":"smashingWindowItem","re":true},"ev","str","^Smash!","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^The sound of the impact is muffled. With my arm still covered, I sweep out the remaining glass in the frame.","\n",["^I'm ready to escape. The only trouble is — when they look in on me in the morning, there will be no question what has happened. It won't help me one jot with shifting suspicion off my back.","\n","ev","str","^Wait","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Slip out","/str","/ev",{"*":".^.c-2","flg":20},{"c-1":["\n","^So perhaps I should wait it out, after all. Who knows? I might have a better opportunity later.","\n",{"->":"night_passes"},{"->":".^.^.^.^.g-2"},{"#f":5}],"c-2":["^ ","\n","^Moving quickly and quietly, I hoist myself up onto the window—frame and worm my way outside into the freezing night air. Then I am away, slipping down the paths between the Huts, sticking to the shadows, on my way to Hut 2.","\n",{"->":".^.^.^.^.g-2"},{"#f":5}],"#f":5,"#n":"g-1"}],{"#f":5}],"g-2":["ev","str","^Go the shortest way","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Take a longer route","/str","/ev",{"*":".^.c-4","flg":20},{"c-3":["^ ","\n","^There's no time to lose. Throwing caution to the wind I make my way quickly to Hut 2, and around the back. I don't think I've been seen but if I have it is too late. My actions are suspicious enough for the noose. I have no choice but to follow through.","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-4":["\n","^In case I'm being followed, I divert around the perimeter of the compound. It's a much longer path, and it takes me across some terrain that's difficult to negotiate in the dark — muddy, and thick with thistles and nestles.","\n","ev",true,"/ev",{"VAR=":"muddyshoes","re":true},"^Still, I can be confident no—one is behind me. I crouch down behind the rear wall of Hut 2. ","<>","\n",{"->":".^.^.^.g-3"},{"#f":5}],"#f":5}],"g-3":["^The component is still there, wrapped in a tea—towel and shoved into a cavity in a breeze—block at the base of the Hut wall.","\n","ev","str","^Take it","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Leave it","/str","/ev",{"*":".^.c-6","flg":20},{"c-5":["^ ","\n","^Quickly, I pull it free, and slip it into the pocket of my jacket.","\n","ev",true,"/ev",{"VAR=":"gotcomponent","re":true},{"->":".^.^.^.g-4"},{"#f":5}],"c-6":["^ ","\n","^Still there means no—one has found it, which means it is probably well—hidden. And short of skipping the compound now, I can afford to leave it hidden there a while longer. So I leave it in place.","\n",{"->":".^.^.^.g-4"},{"#f":5}],"#f":5}],"g-4":["^Where now?","\n","ev","str","^Back to the barracks","/str","/ev",{"*":".^.c-7","flg":20},"ev","str","^Go to Hooper's dorm","/str",{"VAR?":"gotcomponent"},"/ev",{"*":".^.c-8","flg":21},"ev","str","^Escape the compound","/str","/ev",{"*":".^.c-9","flg":20},{"c-7":["^ ",{"->":"return_to_room_after_excursion"},"\n",{"#f":5}],"c-8":["^ ",{"->":"go_to_hoopers_dorm"},"\n",{"#f":5}],"c-9":["^ ","\n","^Enough of this place. Time for me to get moving. I can get to the train station on foot, catch the postal train to Scotland and be somewhere else before anyone realises that I'm gone.","\n","^Of course, then they'll be looking for me in earnest. ","ev",{"VAR?":"framedhooper"},"!","/ev",[{"->":".^.b","c":true},{"b":["^As a confirmed traitor.",{"->":".^.^.^.11"},null]}],[{"->":".^.b"},{"b":["^Perhaps not as a traitor — they might take the idea that Hooper was involved with the theft — but certainly as a valuable mind, one containing valuable secrets and all too easily threatened. They will think I am running away because of my indiscretions. I suppose, in fairness, that I am.",{"->":".^.^.^.11"},null]}],"nop","\n",["ev","str","^Go","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't go","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ \t\t\t",{"->":"live_on_the_run"},"\n",{"#f":5}],"c-1":["^ ","\n","^It's no good. That's only half a solution. I couldn't be happy with that.","\n",["ev","str","^Back to the barracks","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^To Hooper's dorm","/str",{"VAR?":"gotcomponent"},{"CNT?":"go_to_hoopers_dorm"},"!","&&","/ev",{"*":".^.c-1","flg":21},{"c-0":["^ \t\t\t",{"->":"return_to_room_after_excursion"},"\n",{"#f":5}],"c-1":["^ ",{"->":"go_to_hoopers_dorm"},"\n",{"#f":5}]}],{"#f":5}]}],{"#f":5}],"#f":5}]}],{"#f":1}],"go_to_hoopers_dorm":[["^I creep around the outside of the huts towards Hooper's dorm. Time to wrap up this little game once and for all. A few guards patrol the area at night but not many — after all, very few know this place even exists.","\n","^Our quarters are arranged away from the main house; where we sleep is of less importance than where we work. We each have our own hut, through some are less permanent than others. Hooper's is a military issue tent: quite a large canopy, with two rooms inside and a short porch area where he insists people leave their shoes. It's all zipped up for the night and no light shines from inside.","\n","^I hang back for a moment. If Harris is keeping to the terms of our deal then someone will be watching this place. But I can see no—one.","\n","ev","str","^Open the outer zip","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Look for another opening","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Hide the component somewhere","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^I creep forward to the tent, intent on lifting the zip to the front porch area just a little — enough to slip the component inside, and without the risk of the noise waking Hooper from his snoring.","\n","^The work is careful, and more than little fiddly — Hooper has tied the zips down on the inside, the fastidious little bastard! — but after a little work I manage to make a hole large enough for my hand.","\n",["ev","str","^Slip in the component","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No, some other way","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ \t\t","\n","^I slide the component into the tent, work the zip closed, and move quickly away into the shadows. It takes a few minutes for my breath to slow, and my heart to stop hammering, but I see no other movement. If anyone is watching Hooper's tent, they are asleep at their posts.","\n","ev",true,"/ev",{"VAR=":"putcomponentintent","re":true},"ev",false,"/ev",{"VAR=":"gotcomponent","re":true},{"->":"return_to_room_after_excursion"},{"#f":5}],"c-1":["^ \t\t\t","\n","^Then pause. This is too transparent. Too blatant. If I leave it here, like this, Hooper will never be seen to go looking for it: he will stumble over it in plain sight, and the men watching will wonder why it was not there when he went to bed.","\n","^No, I must try something else — or nothing at all.","\n",["ev","str","^On top of the tent","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Throw the component into the long grass","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Give up","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ",{"->":".^.^.^.^.^.^.^.put_component_on_tent"},"\n",{"#f":5}],"c-1":["^ ","\n","^From inspiration — or desperation, I am not certain — a simple approach occurs to me. ",{"->":".^.^.^.^.^.^.^.toss_component_into_bushes"},"\n",{"#f":5}],"c-2":["^ ","\n","^There is nothing to be gained here. I have the component now; maybe it will be of some value tomorrow.","\n",["ev","str","^Return to my barrack","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Escape the compound","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ",{"->":"return_to_room_after_excursion"},"\n",{"#f":5}],"c-1":["^ ",{"->":"live_on_the_run"},"\n",{"#f":5}]}],{"#f":5}]}],{"#f":5}]}],{"#f":5}],"c-1":["^ ","\n","^Making a wide circuit I creep around the tent. It has plenty of other flaps and openings, tied down with Gordian complexity. But nothing afford itself to slipping the component inside.","\n",["ev","str","^Try the porch zip","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Try on top of the tent","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Give up","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ \t\t\t",{"->":".^.^.^.^.c-0"},"\n",{"#f":5}],"c-1":["^ \t\t",{"->":".^.^.^.^.^.put_component_on_tent"},"\n",{"#f":5}],"c-2":["^ \t\t\t\t\t\t","\n","^It's no good. Nothing I can do will be any less than obvious — something appearing where something was not there before. The men watching Hooper will know it is a deception and Hooper's protestations will be taken at face value.","\n","^If I can't find a way for Hooper to pick the component up, as if from a hiding place of his own devising, and be caught doing it, then I have no plan at all.","\n",["ev","str","^Return to my barrack","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Escape the compound","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Toss the component into the bushes","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ",{"->":"return_to_room_after_excursion"},"\n",{"#f":5}],"c-1":["^ ",{"->":"live_on_the_run"},"\n",{"#f":5}],"c-2":["^ ",{"->":".^.^.^.^.^.^.^.toss_component_into_bushes"},"\n",{"#f":5}]}],{"#f":5}]}],{"#f":5}],"c-2":["^ ","\n","^If I leave the component here somewhere it should be somewhere I can rely on Hooper finding it, but no—one before Hooper. In particular.","\n",["ev","str","^Behind the tent","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Inside the porch section","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^On top of the canvas","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^\t\t\t \t",{"->":".^.^.^.^.c-1"},"\n",{"#f":5}],"c-1":["^ \t\t",{"->":".^.^.^.^.c-0"},"\n",{"#f":5}],"c-2":["^ \t\t\t",{"->":".^.^.^.^.^.put_component_on_tent"},"\n",{"#f":5}]}],{"#f":5}]}],{"put_component_on_tent":["^A neat idea strikes me. If I could place it on top of the canvas, somewhere in the middle where it would bow the cloth inwards, then it would be invisible to anyone passing by. But to Hooper, it would be above him: a shadow staring him in the face as he awoke. What could be more natural than getting up, coming out, and looking to see what had fallen on him during the night?","\n","^It's the work of a moment. I was once an excellent bowler for the second XI back at school. This time I throw underarm, of course, but I still land the vital missing component exactly where I want it to go.","\n","ev",true,"/ev",{"VAR=":"framedhooper","re":true},"ev",false,"/ev",{"VAR=":"gotcomponent","re":true},"^For a second I hold my breath, but nothing and no—one stirs. ",{"->":"return_to_room_after_excursion"},"\n",{"#f":1}],"toss_component_into_bushes":["^I toss the component away into the bushes behind Hooper's tent and return to my barrack, wishing myself a long sleep followed by a morning, free of this business.","\n","ev",false,"/ev",{"VAR=":"gotcomponent","re":true},"ev",true,"/ev",{"VAR=":"throwncomponentaway","re":true},{"->":"return_to_room_after_excursion"},{"#f":1}],"#f":1}],"live_on_the_run":["^Better to live on the run than die on the spit. Creeping around the edge of the compound","ev",{"VAR?":"gotcomponent"},"/ev",[{"->":".^.b","c":true},{"b":["^, the Bombe component heavy in my pocket",{"->":".^.^.^.5"},null]}],"nop","^, I make my way to the front gate. As always, it's manned by two guards, but I slip past their box by crawling on my belly.","\n","^And then I'm on the road. Walking, not running. Silent. Free.","\n","^For the moment, at least.","\n","end",{"#f":1}],"return_to_room_after_excursion":[["ev",{"VAR?":"gotcomponent"},"/ev",[{"->":".^.b","c":true},{"b":["^The weight of the Bombe component safely in my jacket",{"->":".^.^.^.5"},null]}],[{"->":".^.b"},{"b":["^Satisfied",{"->":".^.^.^.5"},null]}],"nop","^, I return the short way up the paths between the huts to the barrack block and the broken window.","\n","^It's a little harder getting back through — the window is higher off the ground than the floor inside — but after a decent bit of jumping and hauling I manage to get my elbows up, and then one leg, and finally I collapse inside, quite winded and out breath.","\n","ev","str","^Wait","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["^ \t",{"->":"night_passes"},"\n",{"#f":5}]}],{"#f":1}],"night_passes":[["^The rest of the night passes slowly. I sleep a little, dozing mostly. Then I'm woken by the rooster in the yard. The door opens, and Harris comes in. He takes one look at the broken window and frowns with puzzlement.","\n","ev",{"VAR?":"putcomponentintent"},"/ev",[{"->":".^.b","c":true},{"b":["^ ",{"->":".^.^.^.^.put_component_inside_tent"},{"->":".^.^.^.6"},null]}],"nop","\n","^\"What happened there?\"","\n","ev","str","^Confess","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Deny","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Show him the component","/str",{"VAR?":"gotcomponent"},"/ev",{"*":".^.c-2","flg":21},{"c-0":["^ ","\n","^\"I broke it,\" I reply. There doesn't seem any use in trying to lie. \"I thought I could escape. But I couldn't get myself through.\"","\n","^The Commander laughs. ",{"->":".^.^.^.glad_youre_here"},"\n",{"#f":5}],"c-1":["^ ","\n","^\"I'm not sure. I was asleep: I woke up when someone broke the window. I looked out to see who it was, but they were already gone.\"","\n","^Harris looks at me with puzzlement. \"Someone came by to break the window, and then ran off? That's absurd. That's utterly absurd. Admit it, Manning. You tried to escape and you couldn't get through.\"","\n",["ev","str","^Admit it","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Deny it","/str",{"VAR?":"framedhooper"},"!","/ev",{"*":".^.c-1","flg":21},"ev","str","^Deny it","/str",{"VAR?":"framedhooper"},"/ev",{"*":".^.c-2","flg":21},{"c-0":["\n","^\"All right. ","ev",{"VAR?":"forceful"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^Damn you.",{"->":".^.^.^.8"},null]}],"nop","^ That's exactly it.\"","\n",{"->":".^.^.^.^.^.glad_youre_here"},{"#f":5}],"c-1":["\n","^\"If I wanted to escape, I would have made damn sure that I could,\" I tell him sternly.","\n",{"->":"harris_certain_is_you"},{"#f":5}],"c-2":["^ ","\n","^\"I tell you, someone broke it. Someone wanted to threaten me, I think.\"","\n","^Harris shakes his head. \"Well, we can look into that matter later. For now, you probably want to hear the more pressing news. ",{"->":".^.^.^.^.^.found_missing_component"},"\n",{"#f":5}]}],{"#f":5}],"c-2":["^ ",{"->":".^.^.^.someone_threw_component"},"\n",{"#f":5}]}],{"put_component_inside_tent":[["^He takes one look around, and sighs, a deep, wistful sigh.","\n","^\"Things just get worse and worse for you, Manning,\" he remarks. \"You are your own worst enemy.\"","\n","ev","str","^Agree","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Disagree","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^\"I've thought so before.\" ","ev",{"VAR?":"admitblackmail"},"/ev",[{"->":".^.b","c":true},{"b":["^Certainly in the matter of getting blackmailed.",{"->":".^.^.^.7"},null]}],"nop","\n","^\"Let me tell you what happened this morning. ","<>","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["\n","^\"Right now, I think you take that role, Harris,\" I reply coolly.","\n",[["^\"Very droll,\" he replies. \"Let me tell you what happened this morning. It will take the smile off your face. ","<>","\n",{"->":".^.^.^.^.g-0"},{"#f":5,"#n":"droll"}],null],{"#f":5}],"c-2":["^ ","\n","^\"I'm looking forward to having a wash and a change of clothes; which should make me a little less evil to be around.\"","\n",{"->":".^.^.c-1.3.droll"},{"->":".^.^.g-0"},{"#f":5}],"g-0":["^Our men watching Hooper's tent saw Hooper wake up, get dressed, clamber out of his tent and then step on something in at the entrance of his tent.\"","\n","ev",true,"/ev",{"VAR=":"piecereturned","re":true},"ev","str","^Be interested","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Be dismissive","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Say nothing","/str","/ev",{"*":".^.c-5","flg":20},{"c-3":["^ ","\n","^\"You mean he didn't even hide it? He put it in his shoe?\"","\n",[["^\"No,\" Harris replies. \"That isn't really what I mean. ","<>","\n",{"->":".^.^.^.^.^.g-1"},{"#f":5,"#n":"not_that"}],null],{"#f":5}],"c-4":["\n","^\"So he's an idiot, and he hid it in his shoe.\"","\n",{"->":".^.^.c-3.4.not_that"},{"->":".^.^.^.g-1"},{"#f":5}],"c-5":["^ ","\n","^I say quiet, listening, not sure how this will go.","\n","^\"In case I'm not making myself clear,\" Harris continues, \"","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":["^I mean, he managed to find it, by accident, somewhere where it wasn't the night before. And at the same time, you're sitting here with your window broken. So, I rather think you've played your last hand and lost. It's utterly implausible that Hooper stole that component and then left it lying around in the doorway of his tent. So I came to tell you that the game is up, for you.\"","\n","^He nods and gets to his feet. ",{"->":"left_alone"},"\n",{"#f":5}]}],{"#f":1}],"someone_threw_component":[["^\"Someone threw this in through the window over night,\" I reply, and open my jacket to reveal the component from the Bombe. \"I couldn't see who, it was too dark. But I know what it is.\"","\n","^He reaches out and takes it. \"Well, I'll be damned,\" he murmurs. \"That's it all right. And you didn't have it on you when we put you in here. But it can't have been Hooper — I had men watching him all night. And there's no—one else it could have been.\"","\n","^He turns the component over in his hands, bemused.","\n","ev",true,"/ev",{"VAR=":"piecereturned","re":true},"ev","str","^Suggest something","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Suggest nothing","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"Perhaps Hooper had an accomplice. Someone else who works on site.\"","\n","^Harris shakes his head, distractedly. \"That doesn't make sense,\" he says. \"Why go to all the trouble of stealing it only to give it back? And why like this?\"","\n",["ev","str","^Suggest something","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Suggest nothing","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"Perhaps the accomplice thought it was Hooper being kept in here. Maybe they saw the guard...\"","\n",{"->":"all_too_farfetched"},{"->":".^.^.^.^.g-0"},{"#f":5}],"c-1":["^ ","\n",{"->":".^.^.^.^.g-0"},{"#f":5}]}],{"#f":5}],"c-1":["^ ","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^I shrug, eloquently.","\n",[{"->":"all_too_farfetched"},{"#f":5,"#n":"g-1"}],{"#f":5}]}],{"#f":1}],"glad_youre_here":[["^\"Shame,\" he remarks. \"I should have left that window open and put a guard on you. Might have been interesting to see where you went. Anyway, I'm glad you're still here, even if you do smell like a dog.\"","\n","ev","str","^Be optimistic","/str",{"VAR?":"framedhooper"},"!","/ev",{"*":".^.c-0","flg":21},"ev","str","^Be pessimistic","/str",{"VAR?":"framedhooper"},"!","/ev",{"*":".^.c-1","flg":21},"ev","str","^Be optimistic","/str",{"VAR?":"framedhooper"},"/ev",{"*":".^.c-2","flg":21},"ev","str","^Be pessimistic","/str",{"VAR?":"framedhooper"},"/ev",{"*":".^.c-3","flg":21},{"c-0":["^ ","\n",{"->":"night_falls.morning_not_saved.0.c-0"},{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n",{"->":"night_falls.morning_not_saved.0.c-1"},{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^\"I'm looking forward to having a bath.\"","\n","^\"Well, you should enjoy it. ","<>","\n",{"->":".^.^.g-0"},{"#f":5}],"c-3":["\n","^\"I imagine I'll smell worse after another couple of days of this.\"","\n","^\"That won't be necessary. ","<>","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":".^.^.^.^.found_missing_component"},{"#f":5}]}],{"#f":1}],"found_missing_component":[["^We found the missing component. Or rather, Hooper found it for us. He snuck out and retrieved it from on top. Of all the damnest places — you would never have known it was there. He claimed ignorance when we jumped him, of course. But it's good enough for me.\"","\n","ev","str","^Approve","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Disapprove","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"I can't tell you enough, I'm glad to hear it. I've had a devil of a night.\"","\n","^His gaze flicks to the broken window, but only for a moment. I think he genuinely cannot believe I could have done it.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"You should never have hired him. A below-average intelligence can't be expected to cope with the pressure of our work.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^Harris rolls his eyes, but he might almost be smiling. \"You'd better get along, ","ev",{"CNT?":".^.^.c-0"},"/ev",[{"->":".^.b","c":true},{"b":["^and work through your devils",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["^Mr Intelligent",{"->":".^.^.^.6"},null]}],"nop","^. There's a 24—hour—late message to be tackled and we're a genius short. So you'd better be ready to work twice as hard.\"","\n","ev","str","^Thank him","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Argue with him","/str","/ev",{"*":".^.c-3","flg":20},{"c-2":["^ \t","\n","^\"I'll enjoy it. Thank you for helping me clear this up.\"","\n","^\"Don't thank me yet. There's still a war to fight. Now get a move on.\"","\n","^I nod, and hurry out of the door. The air outside has never tasted fresher and more invigorating. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-3":["^ ","\n","^\"I'll work as hard as I work.\"","\n","^\"Get out,\" Harris growls. \"Before I decide to arrest you as an accessory.\"","\n","^I do as he says. Outside the barrack, the air has never smelt sweeter.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":[{"->":"head_for_my_dorm_free"},{"#f":5}]}],{"#f":1}],"#f":1}],"night_falls":[["^Night falls. The clockwork of the heavens keeps turning, whatever state I might be in. No—one can steal the components that make the sun go down and the stars come out. I watch it performing its operations. I can't sleep.","\n","ev",{"VAR?":"hooperClueType"},0,">","/ev",[{"->":".^.b","c":true},{"b":["\n","^Has Hooper taken my bait?","\n",{"->":".^.^.^.8"},null]}],"nop","\n","ev","str","^Look of out the window","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Listen at the door","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Wait","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^I peer out of the window, but it looks out onto the little brook at the back of the compound, with no view of the other huts or the House. Who knows if there are men up, searching the base of Hut 2, following one another with flashlights...","\n","ev",{"CNT?":"inside_hoopers_hut.back_of_hut_2"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^Perhaps Hooper is there, in the dark, trying to help me after all?","\n",{"->":".^.^.^.8"},null]}],"nop","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ \t","\n","^I put my ear to the keyhole but can make out nothing. Are there still guards posted? ","ev",{"VAR?":"hooperClueType"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^Perhaps, if Hooper has managed to incriminate himself, the guards have been removed?",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^Perhaps the component has been found and the crisis is over.",{"->":".^.^.^.10"},null]}],"nop","\n","^Perhaps the door is unlocked and they left me to sleep?","\n",["ev","str","^Try it","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Leave it","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ I try the handle. No such luck.","\n",{"->":".^.^.^.^.g-0"},{"#f":5}],"c-1":["^ I don't touch it. I don't want anyone outside thinking I'm trying to escape.","\n",{"->":".^.^.^.^.g-0"},{"#f":5}]}],{"#f":5}],"c-2":["^ \t\t\t\t\t","\n","^There is nothing I can do to speed up time.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^The night moves at its own pace. I suppose by morning I will know my fate.","\n","ev","str","^Wait","/str",{"VAR?":"hooperClueType"},0,">","/ev",{"*":".^.c-3","flg":21},"ev","str","^Wait","/str",{"VAR?":"hooperClueType"},0,"==","/ev",{"*":".^.c-4","flg":21},{"c-3":["^ ","\n","^Morning comes. I'm woken by a rooster calling from the yard behind the House. I must have slept after all. I pull myself up from the bunk, shivering slightly. There is condensation on the inside of the window. I have probably given myself a chill.","\n","^Without knocking, Harris comes inside. \"You're up,\" he remarks, and then, \"You smell like an animal.\"","\n",["ev","str","^Be friendly","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Be cold","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"I suppose I do rather.\" I laugh, but Harris does not.","\n","^\"This damn business gets worse and worse,\" he says, talking as he goes over to unlock and throw open the window. ","<>","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"So would you,\" I reply tartly. Harris shrugs.","\n","^\"I've been through worse than this,\" he replies matter—of—factly. \"It's hardly my fault if you sleep in your clothes.\"","\n","^I glare back. He goes over to the window, unlocks it and throws it open, relishing the fresh air from outside.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"Hooper's confessed, you know.\"","\n","ev","str","^Be eager","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Be cautious","/str","/ev",{"*":".^.c-3","flg":20},{"c-2":["^ ","\n","^\"He has? I knew he would. The worm.\"","\n","^\"Steady now. Matters aren't over yet. ","<>","\n",{"->":".^.^.^.hooper_didnt_give_himself_up"},{"#f":5}],"c-3":["^ ","\n","^\"Oh, yes?\"","\n","^\"Yes. For what that's worth. ","<>","\n",{"->":".^.^.^.hooper_didnt_give_himself_up"},{"#f":5}],"#f":5}],"hooper_didnt_give_himself_up":["^There's still the issue of the component. It hasn't turned up. He didn't lead us to it. I guess he figured you must have had something on him. I don't know.\"","\n","^He looks quite put out by the whole affair. He is not the kind of man to deal well with probabilities.","\n","ev","str","^Be interested","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Be disinterested","/str","/ev",{"*":".^.c-5","flg":20},{"c-4":["^ ","\n","^\"You mean he confessed of his own accord? You didn't catch him?\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-5":["^ ","\n","^\"Well, I'm glad his conscience finally caught up with him,\" I reply dismissively.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":["^\"The Captain went back into that hut and he confessed immediately. We were so surprised we didn't let you go.\" He wrinkles his nose. \"I'm rather sorry about that now. I suggest you have a wash.\"","\n","^And with that he gestures to the doorway.","\n","ev","str","^Go","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Wait","/str","/ev",{"*":".^.c-7","flg":20},{"c-6":["^ ","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-7":["^ ","\n","^I hang back a moment. Something does not seem quite right. After all, Hooper did not steal the component. He has no reason to confess to anything. Perhaps this is another trap?","\n","^\"Well?\" Harris asks. \"What are you waiting for? Please don't tell me you want to confess now as well, I don't think my head could stand it.\"","\n",["ev","str","^Confess","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't confess","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^After a chance like this? A chance — however real — to save my neck? To hand it over — what, to save Hooper's worthless skin?","\n",["ev","str","^Confess","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't confess","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^I see. Perhaps you think I bullied the man into giving himself up. Perhaps he understood my little clue far enough to know it was a threat against him, but not well enough to understand where he should look to find it. So he took the easy route out and folded. Gave me the hand.","\n","ev",true,"/ev",{"VAR=":"hooperConfessed","re":true},"^Hardly sporting, of course.","\n",["ev","str","^Confess","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't confess","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^Well, then. I suppose this must be what it feels like to have a conscience. I suppose I had always wondered.","\n","^\"Harris, sir. I don't know what Hooper's playing at, sir. But I can't let him do this.\"","\n","^\"Do what?\"","\n","^\"Take the rope for this. I took it, sir.","\n","ev",false,"/ev",{"VAR=":"revealedhooperasculprit","re":true},"ev",false,"/ev",{"VAR=":"losttemper","re":true},{"->":"reveal_location_of_component"},{"->":".^.^.^.^.^.^.g-0"},{"#f":5}],"c-1":["^ ","\n",{"->":".^.^.^.^.^.^.g-0"},{"#f":5}]}],{"#f":5}],"c-1":["^ ","\n",{"->":".^.^.^.^.g-0"},{"#f":5}]}],{"#f":5}],"c-1":["^ ","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"I certainly don't. But still, I'm surprised. I had Hooper down for a full—blown double agent, a traitor. He knows he'll face the rope, doesn't he?\"","\n","^\"Don't ask me to explain why he did what he did,\" Harris sighs. \"Just be grateful that he did, and you're now off the hook.\"","\n",{"->":".^.^.^.^.^.g-2"},{"#f":5}]}],{"#f":5}],"#f":5}],"g-2":["^Curiouser and curiouser. I nod once to Harris and slip outside into the cold morning air.","\n","ev",{"VAR?":"hooperClueType"},0,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","^Hooper's confession only makes sense in one fashion","ev",{"VAR?":"hooperConfessed"},"/ev",[{"->":".^.b","c":true},{"b":["^, and that is his being dim—witted and slow",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^ — if I successfully implied to him that I had him framed, but he did not unpack my little clue well enough to go looking for the component. Well, I had figured him for a more intelligent opponent, but a resignation from the game will suffice",{"->":".^.^.^.7"},null]}],"nop","^. Or perhaps he knew he would be followed if he went to check, and decided he would be doomed either way.","\n",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["\n","^Hooper's confession only makes sense in one way — and that's that he believed me. He reasoned that he would be followed. To try and uncover the component would have got him arrested, and to confess was the same.","\n","^He simply caved, and threw in his hand.","\n",{"->":".^.^.^.9"},null]}],"nop","\n","^Of course, however, there is only one way to be certain that Harris is telling the truth, and that is to check the breeze—block at the back of Hut 2.","\n","ev","str","^Check","/str","/ev",{"*":".^.c-8","flg":20},"ev","str","^Don't check","/str","/ev",{"*":".^.c-9","flg":20},{"c-8":["^ ",{"->":"go_to_where_component_is_hidden"},"\n",{"#f":5}],"c-9":["\n","^But there will time for that later. If there is nothing there, then Hooper discovered the component after all and Harris' men will have swooped on him, and the story about his confession is just a ruse to test me out.","\n","^And if the component is still there — well. It will be just as valuable to my contact in a week's time, and his deadline of the 31st is not yet upon us.","\n",{"->":"head_for_my_dorm_free"},{"#f":5}],"#f":5}]}],{"#f":5}],"c-4":["^ ",{"->":".^.^.^.^.morning_not_saved"},"\n",{"#f":5}],"#f":5}]}],{"morning_not_saved":[["^Morning comes with the call of a rooster from the yard of the House. I must have slept after all. I pull myself up off the bunk, shivering slightly. There is condensation on the inside of the window. I have probably given myself a chill.","\n","^It's not long after that Harris enters the hut. He closes the door behind him, careful as ever, then takes a chair across from me.","\n","^\"You smell like a dog,\" he remarks.","\n","ev","str","^Be optimistic","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Be pessimistic","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"I'm looking forward to a long bath,\" I reply. \"And getting back to work.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"So would you after the night I've had.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"harris_certain_is_you"},{"#f":5}]}],{"#f":1}],"#f":1}],"harris_certain_is_you":["^\"Well, I'm afraid it is going to get worse for you,\" Harris replies soberly. \"We followed Hooper, and he took himself neatly to bed and slept like a boy scout. Which puts us back to square one, and you firmly in the frame. And I'm afraid I don't have time for any more games. I want you to tell me where that component is, or we will hang you as a traitor.\"","\n","ev",false,"/ev",{"VAR=":"revealedhooperasculprit","re":true},"ev",false,"/ev",{"VAR=":"losttemper","re":true},{"->":"harris_threatens_lynching"},{"#f":1}],"head_for_my_dorm_free":[["^I head for my dorm, intent on a bath, breakfast, a glance at the crossword before the other men get to it, and then on with work. They should have replaced the component in the Bombe by now. We will only be a day behind.","\n","ev",{"VAR?":"framedhooper"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^And then everything will proceed as before. The component will mean nothing to the Germans — this is the one fact I could never have explained to a man like Harris, even though the principle behind the Bombe is the same as the principle behind the army. The individual pieces — the men, the components — do not matter. They are identical. It is how they are arranged that counts.","\n",{"->":".^.^.^.7"},null]}],"nop","\n","^I bump into Russell in the dorm hut.","\n","^\"Did you hear?\" he whispers. \"Terrible news about Hooper. Absolutely terrible.\"","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["^ ","\n","^\"Quite terrible. I would never have guessed.\"","\n","^\"Well.\" Russell harrumphs.","\n",[["^\"Quince was saying this morning, apparently his grandfather was German. So perhaps it's to be expected. See you there?\"","\n",{"->":".^.^.^.^.g-0"},{"#f":5,"#n":"quince"}],null],{"#f":5}],"c-1":["\n","^\"Heard what?\"","\n",[["^\"Hooper's been taken away. They caught him, uncovering that missing Bombe component from a hiding place somewhere, apparently about to take it to his contact.\" Russell harrumphs. ",{"->":".^.^.^.^.c-0.6.quince"},"\n",{"->":".^.^.^.^.g-0"},{"#f":5,"#n":"hooper_taken"}],null],{"#f":5}],"c-2":["^ ","\n","^\"I don't know what you're talking about.\"","\n",{"->":".^.^.c-1.3.hooper_taken"},{"->":".^.^.g-0"},{"#f":5}],"c-3":["\n","^\"If you'll excuse me, Russell. I was about to take a bath.\"","\n","^\"Oh, of course. Worked all night, did you? Well, you'll hear soon enough. Can hardly hide the fact there'll only be three of us from now on.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^I wave to him and move away, my thoughts turning to the young man in the village. My lover. My contact. My blackmailer. Hooper may have taken the fall for the missing component, but ","ev",{"VAR?":"framedhooper"},"!","/ev",[{"->":".^.b","c":true},{"b":["^if he did recover it from Hut 2 then ",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^ its recovery does mean ",{"->":".^.^.^.7"},null]}],"nop","^I have nothing to sell to save my reputation","ev",{"CNT?":"i_met_a_young_man"},"/ev",[{"->":".^.b","c":true},{"b":["^, if I have any left",{"->":".^.^.^.13"},null]}],"nop","^.","\n","ev",{"VAR?":"framedhooper"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^If he didn't, of course, and Harris was telling the truth about his sudden confession, then I will be able to buy my freedom once and for all.","\n",{"->":".^.^.^.21"},null]}],"nop","\n","ev","str","^Get the component","/str",{"VAR?":"framedhooper"},"!","/ev",{"*":".^.c-4","flg":21},"ev","str","^Leave it","/str",{"VAR?":"framedhooper"},"!","/ev",{"*":".^.c-5","flg":21},"ev","str","^Act normal","/str","/ev",{"*":".^.c-6","flg":20},{"c-4":["^ ",{"->":"go_to_where_component_is_hidden"},"\n",{"#f":5}],"c-5":["^ ","\n","^I will have to leave that question for another day. To return there now, when they're probably watching my every step, would be suicide. After all, if Hooper ","ev",{"VAR?":"hooperClueType"},1,"==","/ev",[{"->":".^.b","c":true},{"b":["^followed",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^understood",{"->":".^.^.^.10"},null]}],"nop","^ my clue, he will have explained it to them to save his neck. They won't believe him — but they won't quite disbelieve him either. We're locked in a cycle now, him and me, of half—truth and probability. There's nothing either of us can do to put the other entirely into blame.","\n",{"->":"ending_return_to_normal"},{"#f":5}],"c-6":["^ ","\n","^But there is nothing to be done about it. ",{"->":"ending_return_to_normal"},"\n",{"#f":5}],"#f":5}]}],{"#f":1}],"ending_return_to_normal":[["^Nothing, that is, except to act as if there is no game being played. I'll have a bath, then start work as normal. I've got a week to find something to give my blackmailer","ev",{"CNT?":"i_met_a_young_man"},"/ev",[{"->":".^.b","c":true},{"b":["^ — or give him nothing: it seems my superiors know about my indiscretions now already",{"->":".^.^.^.5"},null]}],"nop","^.","\n","ev","str","^Co-operate","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Dissemble","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["^ ","\n","^Something will turn up. It always does. An opportunity will present itself, and more easily now that Hooper is out of the way.","\n","^But for now, there's yesterday's intercept to be resolved.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^Or perhaps I might hand my young blackmailer over my superiors instead for being the spy he is.","\n","^Perhaps that would be the moral thing to do, even, and not just the most smart.","\n","^But not today. Today, there's an intercept to resolve.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["\n","^In a week's time, this whole affair will be in the past and quite forgotten. I'm quite sure of that. ",{"->":".^.^.c-3"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ I've more important problems to think about now. There's still yesterday's intercept to be resolved. ","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^The Bombe needs to be set up once more and set running.","\n","^It's time I tackled a problem I can solve.","\n","end",{"#f":5}]}],{"#f":1}],"go_to_where_component_is_hidden":[["^It won't take a moment to settle the matter. I can justify a walk past Hut 2 as part of my morning stroll. It will be obvious in a moment if the component is still there.","\n","^On my way across the paddocks, between the huts and the House, I catch sight of young Miss Lyon, arriving for work on her bicycle. She giggles as she sees me and waves.","\n","ev","str","^Wave back","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Ignore her","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^I wave cheerily back and she giggles, almost drops her bicycle, then dashes away inside the House. Judging by the clock on the front gable, she's running a little late this morning.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^I give no reaction. She sighs to herself, as if this kind of behaviour is normal, and trots away inside the House to begin her duties.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^I turn the corner of Hut 3 and walk down the short gravel path to Hut 2. It was a good spot to choose — Hut 2 is where the electricians work, and they're generally focussed on what they're doing. They don't often come outside to smoke a cigarette so it's easy to slip past the doorway unnoticed.","\n","ev","str","^Check inside","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Go around the back","/str","/ev",{"*":".^.c-3","flg":20},{"c-2":["^ \t\t","\n","^I hop up the steps and put my head inside all the same. Nobody about. Still too early in the AM for sparks, I suppose. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-3":["^ ","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":["^I head on around the back of the hut. The breeze—block with the cavity is on the left side.","\n","ev","str","^Check","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Look around","/str","/ev",{"*":".^.c-5","flg":20},{"c-4":["^ \t\t","\n","^No time to waste. I drop to my knees and check the breeze—block. Sure enough, there's nothing there. Hooper took the bait.","\n","^Suddenly, there's a movement behind me. I look up to see, first a snub pistol, and then, Harris.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-5":["^ ","\n","^I pause to glance around, and catch a glimpse of movement. Someone ducking around the corner of the hut. Or a canvas sheet flapping in the light breeze. Impossible to be sure.","\n",["ev","str","^Check the breeze—block","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Check around the side of the hut","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ",{"->":".^.^.^.^.c-4"},"\n",{"->":".^.^.^.^.^.g-2"},{"#f":5}],"c-1":["^ ","\n","^But too important to guess. I move back around the side of the hut.","\n","^Harris is there, leaning in against the wall. He holds a stub pistol in his hand.","\n",{"->":".^.^.^.^.^.g-2"},{"#f":5}]}],{"#f":5}],"#f":5}],"g-2":["ev",{"VAR?":"hooperClueType"},1,">","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"","ev",{"VAR?":"hooperClueType"},2,"==","/ev",[{"->":".^.b","c":true},{"b":["^Queen to rook two",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["^Messy without one missing whatever it was",{"->":".^.^.^.9"},null]}],"nop","^,\" he declares. \"I wouldn't have fathomed it but Hooper did. Explained it right after we sprung him doing what you're doing now. We weren't sure what to believe but now, you seem to have resolved that for us.\"","\n",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["\n","^\"Hooper said you'd told him where to look. I didn't believe him. Or, well. I wasn't sure what to believe. Now I rather think you've settled it.\"","\n",{"->":".^.^.^.7"},null]}],"nop","\n","ev","str","^Agree","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-7","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-8","flg":20},{"c-6":["^ ","\n","^\"I have, rather.\" I put my hands into my pockets. \"I seem to have done exactly that.\"","\n","^\"I'm afraid my little story about Hooper confessing wasn't true. I wanted to see if you'd go to retrieve the part.\" Harris gestures me to start walking. \"You were close, Manning, I'll give you that. I wanted to believe you. But I'm glad I didn't.\"","\n",{"->":".^.^.^.g-3.done"},{"->":".^.^.^.g-3"},{"#f":5}],"c-7":["^ ","\n","^\"I spoke to Russell. He said he saw Hooper doing something round here. I wanted to see what it was.\"","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-8":["^ ","\n","^\"Harris, you'd better watch out. He's planted a time—bomb here.\"","\n","^Harris stares at me for a moment, then laughs. \"Oh, goodness. That's rich.\"","\n","^I almost wish I had a way to make the hut explode, but of course I don't.","\n",{"->":".^.^.^.g-3"},{"#f":5}],"#f":5}],"g-3":["^\"Enough.\" Harris gestures for me to start walking. \"This story couldn't be simpler. You took it to cover your back. You hid it. You lied to get Hooper into trouble, and when you thought you'd won, you came to scoop your prize. A good hand but ultimately, ","ev",{"VAR?":"hooperClueType"},1,"<=","/ev",[{"->":".^.b","c":true},{"b":["^if it hadn't have been you who hid the component, then you wouldn't be here now",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["^you told Hooper where to look with your little riddle",{"->":".^.^.^.8"},null]}],"nop","^.\"","\n",["^He leads me across the yard. Back towards Hut 5 to be decoded, and taken to pieces, once again.","\n","end",{"#f":5,"#n":"done"}],{"#f":5}]}],{"#f":1}],"harris_threatens_lynching":[["ev",{"CNT?":"harris_certain_is_you"},"/ev",[{"->":".^.b","c":true},{"b":["^He passes a hand across his eyes with a long look of despair.",{"->":".^.^.^.5"},null]}],[{"->":".^.b"},{"b":["^He gets to his feet, and gathers his gloves from the table top.",{"->":".^.^.^.5"},null]}],"nop","\n","^\"I'm going to go outside and organise a rope. That'll take about twelve minutes. That's how long you have to decide.\"","\n","ev","str","^Protest","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Confess","/str",{"VAR?":"gotcomponent"},"!",{"VAR?":"throwncomponentaway"},"!","&&","/ev",{"*":".^.c-1","flg":21},"ev","str","^Stay silent","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Show him the component","/str",{"VAR?":"gotcomponent"},"/ev",{"*":".^.c-3","flg":21},{"c-0":["^ ","\n","^\"You can't do this!\" I cry. \"It's murder! I demand a trial, a lawyer; for God's sake, man, you can't just throw me overboard, we're not barbarians...!\"","\n",[["^\"You leave me no choice,\" Harris snaps back, eyes cold as gun—metal. \"You and your damn cyphers. Your damn clever problems. If men like you didn't exist, if we could just all be straight with one another.\" He gets to his feet and heads for the door. \"I fear for the future of this world, with men like you in. Reich or no Reich, Mr Manning, people like you simply complicate matters.\"","\n",{"->":"left_alone"},{"->":".^.^.^.^.g-0"},{"#f":5,"#n":"too_clever"}],null],{"#f":5}],"c-1":["^ ","\n","^I nod. \"I don't need twelve minutes. ",{"->":"reveal_location_of_component"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ",{"->":"my_lips_are_sealed"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ ","\n","^\"I don't need twelve minutes. Here it is.\"","\n","^I open my jacket and pull the Bombe component out of my pocket. Harris takes it from me, whistling, curious.","\n","^\"Well, I'll be. That's it all right.\"","\n","^\"That's it.\"","\n","^\"But you didn't have it on you yesterday.\"","\n",["ev","str","^Explain","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't explain","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"I climbed out of the window overnight,\" I explain. \"I went and got this from where it was hidden, and brought it back here.\"","\n",{"->":".^.^.^.^.g-0"},{"#f":5}],"c-1":["\n","^\"No. I didn't.\"","\n",{"->":".^.^.^.^.g-0"},{"#f":5}]}],{"#f":5}],"g-0":[{"->":"all_too_farfetched"},"ev","str","^Confess","/str",{"VAR?":"throwncomponentaway"},"/ev",{"*":".^.c-4","flg":21},"ev","str","^Frame Hooper","/str",{"VAR?":"throwncomponentaway"},"/ev",{"*":".^.c-5","flg":21},{"c-4":["\n","^\"I don't need twelve minutes. The component is in the long grass behind Hooper's tent. I threw it there hoping to somehow frame him, but now I see that won't be possible. I was naive, I suppose.\"","\n","ev",true,"/ev",{"VAR=":"piecereturned","re":true},{"->":"reveal_location_of_component.harris_believes"},{"#f":5}],"c-5":["^ ","\n","^\"Look, I know where it is. The missing piece of the Bombe is in the long grasses behind Hooper's tent. I saw him throw it there right after we finished work. He knew you'd scour the camp but I suppose he thought you'd more obvious places first. I suppose he was right about that. Look there. That proves his guilt.\"","\n","ev",true,"/ev",{"VAR=":"longgrasshooperframe","re":true},"ev",true,"/ev",{"VAR=":"piecereturned","re":true},"^\"That doesn't prove anything,\" Harris returns sharply. \"But we'll check what you say, all the same.\" He gets to his feet and heads out of the door.","\n",{"->":"left_alone"},{"#f":5}],"#f":5}]}],{"#f":1}],"reveal_location_of_component":["<>","^ The missing component of the Bombe computer is hidden in a small cavity in a breeze—block supporting the left rear post of Hut 2. I put in there anticipating a search. I intended to ","ev",{"VAR?":"revealedhooperasculprit"},"/ev",[{"->":".^.b","c":true},{"b":["^pass it to Hooper",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^dispose of it",{"->":".^.^.^.7"},null]}],"nop","^ once the fuss had died down. I suppose I was foolish to think that it might.\"","\n","ev",true,"/ev",{"VAR=":"piecereturned","re":true},{"->":".^.harris_believes"},{"harris_believes":["ev",{"CNT?":"night_falls.0.g-0.c-3.6.hooper_didnt_give_himself_up"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"Indeed. And Mr Manning: God help you if you're lying to me.\"","\n",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["\n","^\"I thought as much. I hadn't expected you to give it out so easily, however. You understand, Hooper has said nothing, of course. In fact, he went to Hut 2 directly after we released him and uncovered the component. But he told us you had instructed him where to go. Hence my little double bluff. Frankly, I'll be glad when I'm shot of the lot of you mathematicians.\"","\n",{"->":".^.^.^.6"},null]}],"nop","\n","^Harris stands, and slips away smartly. ",{"->":"left_alone"},"\n",{"#f":1}],"#f":1}],"my_lips_are_sealed":["^I say nothing, my lips tightly, firmly sealed. It's true I am a traitor, to the very laws of nature. The world has taught me that since a very early age. But not to my country — should the Reich win this war, I would hardly be treated as an honoured hero. I was doomed from the very start.","\n","ev",true,"/ev",{"VAR=":"notraitor","re":true},"^I explain none of this. How could a man like Harris understand?","\n","^The Commander takes one look back from the doorway as he pulls it to.","\n","^\"It's been a pleasure working with you, Mr Manning,\" he declares. \"You've done a great service to this country. If we come through, I'm sure they'll remember you name. I'm sorry it had to end this way and I'll do my best to keep it quiet. No—one need know what you did.\"","\n",{"->":"left_alone"},{"#f":1}],"all_too_farfetched":["^\"This is all too far—fetched,\" Harris says. \"I'm glad to have this back, but I need to think.\"","\n","^Getting to his feet, he nods once. \"You'll have to wait a little longer, I'm afraid, Manning.\"","\n","^Then he steps out of the door, muttering to himself.","\n",{"->":"make_your_peace"},{"#f":1}],"left_alone":["ev",{"CNT?":"slam_door_shut_and_gone.time_to_move_now"},"/ev",[{"->":".^.b","c":true},{"b":["^The Commander holds the door for his superior, and follows him out.",{"->":".^.^.^.4"},null]}],"nop","^ Then the door closes. I am alone again, as I have been for most of my short life.","\n",{"->":"make_your_peace"},{"#f":1}],"make_your_peace":[["ev","str","^Make your peace","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^I am waiting again. I have no God to make my peace with. I find it difficult to believe in goodness of any kind, in a world such as this.","\n","ev",{"VAR?":"notraitor"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",true,"/ev",{"VAR=":"notraitor","re":true},"^But I am no traitor. Not to my country. To my sex, perhaps. But how could I support the Reich? If the Nazis were to come to power, I would be worse off than ever.","\n",{"->":".^.^.^.7"},null]}],"nop","\n","ev",{"CNT?":"harris_threatens_lynching.0.c-0.4.too_clever"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^In truth, it is men like Harris who are complex, not men like me. I live to make things ordered, systematic. I like my pencils sharpened and lined up in a row. I do not deal in difficult borders, or uncertainties, or alliances. If I could, I would reduce the world to something easier to understand, something finite.","\n","^But I cannot, not even here, in our little haven from the horrors of the war.","\n",{"->":".^.^.^.13"},null]}],"nop","\n","^I have no place here. No way to fit. I am caught, in the middle, cryptic and understood only thinly, through my machines.","\n",["ev",{"^->":"make_your_peace.0.g-0.17.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^I must seem very calm. \t\t\t",{"->":"$r","var":true},null]}],["ev",{"^->":"make_your_peace.0.g-0.18.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":18},{"s":["^Perhaps I should try to escape.",{"->":"$r","var":true},null]}],{"c-1":["ev",{"^->":"make_your_peace.0.g-0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.17.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-2":["ev",{"^->":"make_your_peace.0.g-0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.18.s"},[{"#n":"$r2"}],"^ But escape to where? I am already a prisoner. Jail would be a blessing. ",{"->":".^.^.^.g-1.monastic"},"\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":["<>","^ I suppose I do not believe they will hang me. They will lock me up and continue to use my brain, if they can. I wonder what they will tell the world — perhaps that I have taken my own life. That would be simplest. The few who know me would believe it.","\n","^Well, then. Not a bad existence, in prison. Removed from temptation.","\n",["^A monastic life, with plenty of problems to keep me going.","\n","^I wonder what else I might yet unravel before I'm done?","\n",["ev",{"^->":"make_your_peace.0.g-1.monastic.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-3","flg":18},{"s":["^The door is opening.",{"->":"$r","var":true},null]}],{"c-3":["ev",{"^->":"make_your_peace.0.g-1.monastic.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.4.s"},[{"#n":"$r2"}],"^ Harris is returning. Our little calculation here is complete. ","ev",{"VAR?":"piecereturned"},"!","/ev",[{"->":".^.b","c":true},{"b":["^ I can only hope one of the others will be able to explain to him that the part I stole will mean nothing to the Germans.",{"->":".^.^.^.13"},null]}],[{"->":".^.b"},{"b":["^We are just pieces in this machine; interchangeable and prone to wear.",{"->":".^.^.^.13"},null]}],"nop","\n",{"->":".^.^.^.^.g-2"},{"#f":5}],"#f":5,"#n":"monastic"}],{"#f":5}],"g-2":["^That is the true secret of the calculating engine, and the source of its power. It is not the components that matter, they are quite repetitive. What matters is how they are wired; the diversity of the patterns and structures they can form. Much like people — it is how they connect that determines our victories and tragedies, and not their genius.","\n","^Which makes me wonder. Should I give ","ev",{"CNT?":"i_met_a_young_man"},"/ev",[{"->":".^.b","c":true},{"b":["^up my beautiful young man",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["^the young man who put me in this spot",{"->":".^.^.^.8"},null]}],"nop","^ to them as well as myself?","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-7","flg":20},{"c-4":["^ ","\n","^But of course I will. ","ev",{"VAR?":"forceful"},2,">","/ev",[{"->":".^.b","c":true},{"b":["^Perhaps I can persuade them to put him in my cell.",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^A little vengeance, disguised as doing something good.",{"->":".^.^.^.10"},null]}],"nop","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-5":["^ ","\n","^No. What would be the use? He will be long gone, and the name he told me is no doubt hokum. No: I was alone before in guilt, and I am thus alone again.","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-6":["^ ","\n","^No. Why would I? He is no doubt an innocent himself, trapped by some dire circumstance. Forced to act the way he did. I have every sympathy for him.","\n","^Of course I do.","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-7":["^ ","\n","^It depends, perhaps, on what his name his worth. If it were to prove valuable, well; perhaps I can concoct a few more such lovers with which to ease my later days.","\n","ev",{"VAR?":"hooper_mentioned"},"/ev",[{"->":".^.b","c":true},{"b":["^ Hooper, perhaps. He wouldn't like that. ",{"->":".^.^.^.8"},null]}],"nop","\n",{"->":".^.^.^.g-3"},{"#f":5}],"#f":5}],"g-3":["ev",{"VAR?":"longgrasshooperframe"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^Harris put the cuffs around my wrists. \"I still have the intercept in my pocket,\" I remark. \"Wherever we're going, could I have a pencil?\"","\n",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["\n","^\"We recovered the part, just where you said it was,\" Harris reports, as he puts the cuffs around my wrists. \"Of course, a couple of the men swear blind they searched there yesterday, so I'm afraid, what with the broken window... we've formed a perfectly good theory which doesn't bode well for you.\"","\n",{"->":".^.^.^.6"},null]}],"nop","\n","ev",true,"/ev",{"VAR=":"piecereturned","re":true},"ev",{"VAR?":"longgrasshooperframe"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^\"I see.\" It doesn't seem worth arguing any further. \"I still have the intercept in my pocket,\" I remark. \"Wherever we're going, could I have a pencil?\"","\n",{"->":".^.^.^.16"},null]}],"nop","\n","^He looks me in the eye.","\n","ev",{"VAR?":"losttemper"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"Of course. And one of your computing things, if I get my way. And when we're old, and smoking pipes together in The Rag like heroes, I'll explain to you the way that decent men have affairs.","\n",{"->":".^.^.^.26"},null]}],[{"->":".^.b"},{"b":["\n","^\"I'll give you a stone to chisel notches in the wall. And that's all the calculations you'll be doing. And as you sit there, pissing into a bucket and growing a beard down to your toes, you have a think about how a smart man would conduct his illicit affairs. With a bit of due decorum you could have learnt off any squaddie.","\n",{"->":".^.^.^.26"},null]}],"nop","\n","<>","^ You scientists.\"","\n","^He drags me up to my feet.","\n","^\"You think you have to re—invent everything.\"","\n","^With that, he hustles me out of the door and I can't help thinking that, with a little more strategy, I could still have won the day. But too late now, of course.","\n","end",{"#f":5}]}],{"#f":1}],"global decl":["ev",0,{"VAR=":"forceful"},0,{"VAR=":"evasive"},false,{"VAR=":"teacup"},false,{"VAR=":"gotcomponent"},false,{"VAR=":"drugged"},false,{"VAR=":"hooper_mentioned"},false,{"VAR=":"losttemper"},false,{"VAR=":"admitblackmail"},0,{"VAR=":"hooperClueType"},false,{"VAR=":"hooperConfessed"},0,{"VAR=":"smashingWindowItem"},false,{"VAR=":"notraitor"},false,{"VAR=":"revealedhooperasculprit"},false,{"VAR=":"smashedglass"},false,{"VAR=":"muddyshoes"},false,{"VAR=":"framedhooper"},false,{"VAR=":"putcomponentintent"},false,{"VAR=":"throwncomponentaway"},false,{"VAR=":"piecereturned"},false,{"VAR=":"longgrasshooperframe"},false,{"VAR=":"DEBUG"},"/ev","end",null],"#f":1}],"listDefs":{}} \ No newline at end of file diff --git a/lib/src/story.rs b/lib/src/story.rs index 1aa7101..a32e277 100644 --- a/lib/src/story.rs +++ b/lib/src/story.rs @@ -30,7 +30,7 @@ pub struct Story { async_saving: bool, prev_containers: Vec>, list_definitions: Rc, - on_error: Option>>, + pub(crate) on_error: Option>>, pub(crate) state_snapshot_at_last_new_line: Option, pub(crate) variable_observers: HashMap>>>, pub(crate) has_validated_externals: bool, @@ -501,11 +501,13 @@ impl Story { self.get_state_mut().add_error(m, is_warning); - self.get_state_mut().force_end() + if !is_warning { + self.get_state_mut().force_end(); + } } - fn reset_errors(&self) { - todo!() + fn reset_errors(&mut self) { + self.get_state_mut().reset_errors(); } fn step(&mut self) -> Result<(), StoryError> { diff --git a/lib/src/story_callbacks.rs b/lib/src/story_callbacks.rs index d5aa9ed..c5f44c0 100644 --- a/lib/src/story_callbacks.rs +++ b/lib/src/story_callbacks.rs @@ -19,6 +19,7 @@ pub trait ErrorHandler { fn error(&mut self, message: &str, error_type: ErrorType); } +#[derive(PartialEq, Clone, Copy)] pub enum ErrorType { // You should probably fix this, but it's not critical Warning, @@ -28,6 +29,10 @@ pub enum ErrorType { impl Story { + pub fn set_error_handler(&mut self, err_handler: Rc>) { + self.on_error = Some(err_handler); + } + pub fn observe_variable(&mut self, variable_name: &str, observer: Rc>) -> Result<(), StoryError> { self.if_async_we_cant("observe a new variable")?; diff --git a/lib/src/story_state.rs b/lib/src/story_state.rs index 8be4dcf..6d9d18e 100644 --- a/lib/src/story_state.rs +++ b/lib/src/story_state.rs @@ -1219,5 +1219,9 @@ impl StoryState { } else { self.current_warnings.push(message); } + } + + pub(crate) fn reset_errors(&mut self) { + self.current_errors.clear(); } } \ No newline at end of file diff --git a/lib/tests/function_test.rs b/lib/tests/function_test.rs index 75f0a95..f7c0eb8 100644 --- a/lib/tests/function_test.rs +++ b/lib/tests/function_test.rs @@ -130,7 +130,8 @@ fn evaluating_function_variable_state_bug_test() -> Result<(), StoryError> { let mut output = String::new(); let result = story.evaluate_function("function_to_evaluate", None, &mut output); - assert_eq!("RIGHT", result?.unwrap()); + assert_eq!("RIGHT", result?.unwrap().get_str().unwrap()); + assert_eq!("End\n", story.cont()?); Ok(()) From 745fdb9793254407d2dd4bc9cf54bd3991b9f915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Mon, 9 Oct 2023 21:10:29 +0000 Subject: [PATCH 70/91] Cargo fmt --- cli-player/src/main.rs | 41 +- cli-player/tests/basic_tests.rs | 6 +- cli-player/tests/test_the_intercept.rs | 6 +- clib/src/cstory.rs | 24 +- clib/src/lib.rs | 2 +- lib/src/callstack.rs | 197 ++++-- lib/src/choice.rs | 31 +- lib/src/choice_point.rs | 19 +- lib/src/container.rs | 119 ++-- lib/src/control_command.rs | 19 +- lib/src/divert.rs | 117 ++- lib/src/flow.rs | 123 +++- lib/src/glue.rs | 5 +- lib/src/ink_list.rs | 88 ++- lib/src/json_read.rs | 167 +++-- lib/src/json_write.rs | 103 ++- lib/src/lib.rs | 46 +- lib/src/list_definition.rs | 4 +- lib/src/list_definitions_origin.rs | 6 +- lib/src/native_function_call.rs | 537 +++++++++----- lib/src/object.rs | 83 ++- lib/src/path.rs | 45 +- lib/src/pointer.rs | 26 +- lib/src/push_pop.rs | 8 +- lib/src/search_result.rs | 10 +- lib/src/state_patch.rs | 13 +- lib/src/story.rs | 945 +++++++++++++++++-------- lib/src/story_callbacks.rs | 181 +++-- lib/src/story_error.rs | 24 +- lib/src/story_state.rs | 555 +++++++++++---- lib/src/tag.rs | 7 +- lib/src/value.rs | 233 +++--- lib/src/value_type.rs | 51 +- lib/src/variable_assigment.rs | 2 +- lib/src/variable_reference.rs | 31 +- lib/src/variables_state.rs | 134 ++-- lib/src/void.rs | 4 +- lib/tests/basic_text_test.rs | 12 +- lib/tests/choice_test.rs | 153 ++-- lib/tests/common/mod.rs | 12 +- lib/tests/conditional_test.rs | 172 +++-- lib/tests/divert_test.rs | 47 +- lib/tests/function_test.rs | 41 +- lib/tests/gather_test.rs | 48 +- lib/tests/glue_test.rs | 43 +- lib/tests/knot_test.rs | 44 +- lib/tests/list_test.rs | 31 +- lib/tests/misc_test.rs | 36 +- lib/tests/multi_flow_test.rs | 25 +- lib/tests/runtime_test.rs | 83 +-- lib/tests/stitch_test.rs | 22 +- lib/tests/tag_test.rs | 19 +- lib/tests/thread_test.rs | 32 +- lib/tests/tunnel_test.rs | 7 +- lib/tests/variable_test.rs | 30 +- lib/tests/variable_text_test.rs | 31 +- 56 files changed, 3146 insertions(+), 1754 deletions(-) diff --git a/cli-player/src/main.rs b/cli-player/src/main.rs index 2ccc702..775a5ac 100644 --- a/cli-player/src/main.rs +++ b/cli-player/src/main.rs @@ -1,14 +1,13 @@ use std::cell::RefCell; -use std::{path::Path, fs, error::Error, rc::Rc, io}; -use std::io::Write; +use std::io::Write; +use std::{error::Error, fs, io, path::Path, rc::Rc}; use anyhow::Context; use bink::story_callbacks::{ErrorHandler, ErrorType}; -use bink::{story::Story, choice::Choice}; +use bink::{choice::Choice, story::Story}; use clap::Parser; - #[derive(Parser)] struct Args { pub json_filename: String, @@ -28,8 +27,10 @@ struct EHandler { impl EHandler { pub fn new() -> Rc> { - Rc::new(RefCell::new(EHandler {should_terminate: false})) - } + Rc::new(RefCell::new(EHandler { + should_terminate: false, + })) + } } impl ErrorHandler for EHandler { @@ -53,9 +54,8 @@ fn main() -> Result<(), Box> { let err_handler = EHandler::new(); story.set_error_handler(err_handler.clone()); - let mut end = false; - + while !end && !err_handler.borrow().should_terminate { while story.can_continue() { let line = story.cont()?; @@ -77,7 +77,6 @@ fn main() -> Result<(), Box> { Ok(()) } - // Returns true if the program has to stop fn process_command(command: Command, story: &mut Story) -> Result> { match command { @@ -87,11 +86,11 @@ fn process_command(command: Command, story: &mut Story) -> Result { let json_string = story.save_state()?; save_json(&filename, &json_string)?; - }, + } Command::Help() => println!("Commands:\n\tload \n\tsave \n\tquit\n\t"), } @@ -130,25 +129,25 @@ fn read_input(choices: &Vec>) -> Result> { } else { return Ok(Command::Choose((v - 1) as usize)); } - }, + } Err(_) => print_error("unrecognized option or command"), } } - let words:Vec<&str> = trimmed.split_whitespace().collect(); + let words: Vec<&str> = trimmed.split_whitespace().collect(); match words[0].trim().to_lowercase().as_str() { "exit" | "quit" => return Ok(Command::Exit()), "help" => return Ok(Command::Help()), "load" => { if words.len() == 2 { - return Ok(Command::Load(words[1].trim().to_string())) + return Ok(Command::Load(words[1].trim().to_string())); } print_error("incorrect filename"); - }, - "save" => {return Ok(Command::Save(words[1].trim().to_string()))}, - _ => print_error("unrecognized option or command"), + } + "save" => return Ok(Command::Save(words[1].trim().to_string())), + _ => print_error("unrecognized option or command"), } } } @@ -159,16 +158,16 @@ fn print_error(error: &str) { fn get_json_string(filename: &str) -> Result> { let path = Path::new(filename); - let json = fs::read_to_string(path).with_context(|| format!("could not read file `{}`", path.to_string_lossy()))?; + let json = fs::read_to_string(path) + .with_context(|| format!("could not read file `{}`", path.to_string_lossy()))?; Ok(json) } fn save_json(filename: &str, content: &str) -> Result<(), Box> { let path = Path::new(filename); - fs::write(path, content).with_context(|| format!("could not write file `{}`", path.to_string_lossy()))?; + fs::write(path, content) + .with_context(|| format!("could not write file `{}`", path.to_string_lossy()))?; Ok(()) } - - diff --git a/cli-player/tests/basic_tests.rs b/cli-player/tests/basic_tests.rs index 52adec5..62fcc26 100644 --- a/cli-player/tests/basic_tests.rs +++ b/cli-player/tests/basic_tests.rs @@ -1,7 +1,7 @@ use assert_cmd::prelude::*; use predicates::prelude::predicate; // Add methods on commands -use std::process::{Command, Stdio}; use std::io::Write; +use std::process::{Command, Stdio}; #[test] fn basic_story_test() -> Result<(), Box> { @@ -15,7 +15,7 @@ fn basic_story_test() -> Result<(), Box> { let mut stdin = child.stdin.take().unwrap(); stdin.write_all(b"1\n").unwrap(); - + let output = child.wait_with_output()?; let output_str = String::from_utf8_lossy(&output.stdout); @@ -37,4 +37,4 @@ fn story_not_found_test() -> Result<(), Box> { .stderr(predicate::str::contains("could not read file")); Ok(()) -} \ No newline at end of file +} diff --git a/cli-player/tests/test_the_intercept.rs b/cli-player/tests/test_the_intercept.rs index 7893367..3c4a16c 100644 --- a/cli-player/tests/test_the_intercept.rs +++ b/cli-player/tests/test_the_intercept.rs @@ -1,6 +1,6 @@ use assert_cmd::prelude::*; -use std::process::{Command, Stdio}; use std::io::Write; +use std::process::{Command, Stdio}; #[test] fn the_intercept_test() -> Result<(), Box> { @@ -14,7 +14,7 @@ fn the_intercept_test() -> Result<(), Box> { let mut stdin = child.stdin.take().unwrap(); stdin.write_all(b"1\n2\nquit\n").unwrap(); - + let output = child.wait_with_output()?; let output_str = String::from_utf8_lossy(&output.stdout); @@ -25,4 +25,4 @@ fn the_intercept_test() -> Result<(), Box> { assert!(output_str.contains("3. Divert")); Ok(()) -} \ No newline at end of file +} diff --git a/clib/src/cstory.rs b/clib/src/cstory.rs index 214a4b7..8b2755a 100644 --- a/clib/src/cstory.rs +++ b/clib/src/cstory.rs @@ -1,15 +1,20 @@ -use std::{os::raw::c_char, ffi::{CString, CStr}}; +use std::{ + ffi::{CStr, CString}, + os::raw::c_char, +}; use bink::story::Story; -use crate::{BINKC_FAIL_NULL_POINTER, BINKC_OK, BINKC_FAIL}; +use crate::{BINKC_FAIL, BINKC_FAIL_NULL_POINTER, BINKC_OK}; #[allow(clippy::not_unsafe_ptr_arg_deref)] #[no_mangle] -pub extern "C" fn binkc_story_new( story: *mut *mut Story, json_string: *const c_char, err_msg: *mut *mut c_char) -> u32 { - if story.is_null() - || err_msg.is_null() - { +pub extern "C" fn binkc_story_new( + story: *mut *mut Story, + json_string: *const c_char, + err_msg: *mut *mut c_char, +) -> u32 { + if story.is_null() || err_msg.is_null() { return BINKC_FAIL_NULL_POINTER; } @@ -63,7 +68,11 @@ pub extern "C" fn binkc_story_can_continue(story: *mut Story, can_continue: *mut #[allow(clippy::not_unsafe_ptr_arg_deref)] #[no_mangle] -pub extern "C" fn binkc_story_cont(story: *mut Story, line: *mut *mut c_char, err_msg: *mut *mut c_char) -> u32 { +pub extern "C" fn binkc_story_cont( + story: *mut Story, + line: *mut *mut c_char, + err_msg: *mut *mut c_char, +) -> u32 { if story.is_null() { return BINKC_FAIL_NULL_POINTER; } @@ -83,4 +92,3 @@ pub extern "C" fn binkc_story_cont(story: *mut Story, line: *mut *mut c_char, er }, } } - diff --git a/clib/src/lib.rs b/clib/src/lib.rs index 7d1e62d..67eef34 100644 --- a/clib/src/lib.rs +++ b/clib/src/lib.rs @@ -1,6 +1,6 @@ //! C API for bink. -use std::{os::raw::c_char, ffi::CString}; +use std::{ffi::CString, os::raw::c_char}; pub mod cstory; diff --git a/lib/src/callstack.rs b/lib/src/callstack.rs index e7ede09..39049a3 100644 --- a/lib/src/callstack.rs +++ b/lib/src/callstack.rs @@ -1,8 +1,18 @@ use std::{collections::HashMap, rc::Rc}; -use serde_json::{Map, json}; - -use crate::{pointer::{Pointer, self}, push_pop::PushPopType, container::Container, value::Value, object::Object, json_read, json_write, path::Path, story::Story, story_error::StoryError}; +use serde_json::{json, Map}; + +use crate::{ + container::Container, + json_read, json_write, + object::Object, + path::Path, + pointer::{self, Pointer}, + push_pop::PushPopType, + story::Story, + story_error::StoryError, + value::Value, +}; pub struct Element { pub current_pointer: Pointer, @@ -14,23 +24,31 @@ pub struct Element { } impl Element { - fn new(push_pop_type: PushPopType, pointer: Pointer, in_expression_evaluation: bool) -> Element { + fn new( + push_pop_type: PushPopType, + pointer: Pointer, + in_expression_evaluation: bool, + ) -> Element { Element { current_pointer: pointer, in_expression_evaluation, temporary_variables: HashMap::new(), push_pop_type, - evaluation_stack_height_when_pushed:0, - function_start_in_output_stream: 0 + evaluation_stack_height_when_pushed: 0, + function_start_in_output_stream: 0, } } fn copy(&self) -> Element { - let mut copy = Element::new(self.push_pop_type, self.current_pointer.clone(), self.in_expression_evaluation); + let mut copy = Element::new( + self.push_pop_type, + self.current_pointer.clone(), + self.in_expression_evaluation, + ); copy.temporary_variables = self.temporary_variables.clone(); copy.evaluation_stack_height_when_pushed = self.evaluation_stack_height_when_pushed; copy.function_start_in_output_stream = self.function_start_in_output_stream; - + copy } } @@ -38,7 +56,7 @@ impl Element { pub struct Thread { pub callstack: Vec, pub previous_pointer: Pointer, - pub thread_index: usize + pub thread_index: usize, } impl Thread { @@ -50,25 +68,49 @@ impl Thread { } } - pub fn from_json(main_content_container: &Rc, j_obj: &Map) -> Result { + pub fn from_json( + main_content_container: &Rc, + j_obj: &Map, + ) -> Result { let mut thread = Thread::new(); - thread.thread_index = j_obj.get("threadIndex").and_then(|i| i.as_i64()).ok_or(StoryError::BadJson("Invalid thread index".to_owned()))? as usize; - - if let Some(j_thread_callstack) = j_obj.get("callstack").and_then(|callstack| callstack.as_array()) { + thread.thread_index = j_obj + .get("threadIndex") + .and_then(|i| i.as_i64()) + .ok_or(StoryError::BadJson("Invalid thread index".to_owned()))? + as usize; + if let Some(j_thread_callstack) = j_obj + .get("callstack") + .and_then(|callstack| callstack.as_array()) + { for j_el_tok in j_thread_callstack.iter() { if let Some(j_element_obj) = j_el_tok.as_object() { - let push_pop_type = PushPopType::from_value(j_element_obj.get("type").and_then(|t| t.as_i64()).ok_or(StoryError::BadJson("Invalid push/pop type".to_owned()))? as usize)?; + let push_pop_type = PushPopType::from_value( + j_element_obj + .get("type") + .and_then(|t| t.as_i64()) + .ok_or(StoryError::BadJson("Invalid push/pop type".to_owned()))? + as usize, + )?; let mut pointer = pointer::NULL.clone(); - let current_container_path_str = j_element_obj.get("cPath").and_then(|c| c.as_str()); + let current_container_path_str = + j_element_obj.get("cPath").and_then(|c| c.as_str()); if current_container_path_str.is_some() { - let thread_pointer_result = main_content_container.content_at_path(&Path::new_with_components_string (current_container_path_str), 0, -1); + let thread_pointer_result = main_content_container.content_at_path( + &Path::new_with_components_string(current_container_path_str), + 0, + -1, + ); pointer.container = thread_pointer_result.container(); - let pointer_index = j_element_obj.get("idx").and_then(|i| i.as_i64()).ok_or(StoryError::BadJson("Invalid pointer index".to_owned()))? as i32; + let pointer_index = j_element_obj + .get("idx") + .and_then(|i| i.as_i64()) + .ok_or(StoryError::BadJson("Invalid pointer index".to_owned()))? + as i32; pointer.index = pointer_index; if thread_pointer_result.approximate { @@ -77,11 +119,15 @@ impl Thread { } } - let in_expression_evaluation = j_element_obj.get("exp").and_then(|exp| exp.as_bool()).unwrap_or(false); + let in_expression_evaluation = j_element_obj + .get("exp") + .and_then(|exp| exp.as_bool()) + .unwrap_or(false); let mut el = Element::new(push_pop_type, pointer, in_expression_evaluation); - if let Some(temps) = j_element_obj.get("temp").and_then(|temp| temp.as_object()) { + if let Some(temps) = j_element_obj.get("temp").and_then(|temp| temp.as_object()) + { el.temporary_variables = json_read::jobject_to_hashmap_values(temps)?; } else { el.temporary_variables.clear(); @@ -92,7 +138,9 @@ impl Thread { } } - if let Some(prev_content_obj_path) = j_obj.get("previousContentObject").and_then(|p| p.as_str()) { + if let Some(prev_content_obj_path) = + j_obj.get("previousContentObject").and_then(|p| p.as_str()) + { let prev_path = Path::new_with_components_string(Some(prev_content_obj_path)); thread.previous_pointer = Story::pointer_at_path(main_content_container, &prev_path)?; } @@ -103,13 +151,13 @@ impl Thread { pub fn copy(&self) -> Thread { let mut copy = Thread::new(); copy.thread_index = self.thread_index; - + for e in self.callstack.iter() { copy.callstack.push(e.copy()); } copy.previous_pointer = self.previous_pointer.clone(); - + copy } @@ -120,16 +168,25 @@ impl Thread { for el in self.callstack.iter() { let mut el_map: Map = Map::new(); - + if !el.current_pointer.is_null() { - el_map.insert("cPath".to_owned(), json!(Object::get_path(el.current_pointer.container.as_ref().unwrap().as_ref()).get_components_string())); + el_map.insert( + "cPath".to_owned(), + json!(Object::get_path( + el.current_pointer.container.as_ref().unwrap().as_ref() + ) + .get_components_string()), + ); el_map.insert("idx".to_owned(), json!(el.current_pointer.index)); } el_map.insert("exp".to_owned(), json!(el.in_expression_evaluation)); el_map.insert("type".to_owned(), json!(el.push_pop_type as u32)); if !el.temporary_variables.is_empty() { - el_map.insert("temp".to_owned(), json_write::write_dictionary_values(&el.temporary_variables)?); + el_map.insert( + "temp".to_owned(), + json_write::write_dictionary_values(&el.temporary_variables)?, + ); } cs_array.push(serde_json::Value::Object(el_map)); @@ -139,7 +196,12 @@ impl Thread { thread.insert("threadIndex".to_owned(), json!(self.thread_index)); if !self.previous_pointer.is_null() { - thread.insert("previousContentObject".to_owned(), json!(Object::get_path(self.previous_pointer.resolve().unwrap().as_ref()).to_string())); + thread.insert( + "previousContentObject".to_owned(), + json!( + Object::get_path(self.previous_pointer.resolve().unwrap().as_ref()).to_string() + ), + ); } Ok(serde_json::Value::Object(thread)) @@ -149,7 +211,7 @@ impl Thread { pub struct CallStack { thread_counter: usize, start_of_root: Pointer, - threads: Vec + threads: Vec, } impl CallStack { @@ -172,7 +234,7 @@ impl CallStack { threads: Vec::new(), }; - for other_thread in &to_copy.threads { + for other_thread in &to_copy.threads { cs.threads.push(other_thread.copy()); } @@ -198,7 +260,11 @@ impl CallStack { pub fn reset(&mut self) { self.threads.clear(); self.threads.push(Thread::new()); - self.threads[0].callstack.push(Element::new(PushPopType::Tunnel, self.start_of_root.clone(), false)); + self.threads[0].callstack.push(Element::new( + PushPopType::Tunnel, + self.start_of_root.clone(), + false, + )); } pub fn can_pop_thread(&self) -> bool { @@ -230,7 +296,9 @@ impl CallStack { return false; } - if t.is_none() { return true; } + if t.is_none() { + return true; + } self.get_current_element().push_pop_type == t.unwrap() } @@ -240,7 +308,9 @@ impl CallStack { let l = self.get_callstack().len() - 1; self.get_callstack_mut().remove(l); } else { - return Err(StoryError::InvalidStoryState("Mismatched push/pop in Callstack".to_owned())); + return Err(StoryError::InvalidStoryState( + "Mismatched push/pop in Callstack".to_owned(), + )); } Ok(()) @@ -295,10 +365,16 @@ impl CallStack { context_index = self.get_current_element_index() + 1; } - let context_element = self.get_callstack_mut().get_mut((context_index - 1) as usize).unwrap(); + let context_element = self + .get_callstack_mut() + .get_mut((context_index - 1) as usize) + .unwrap(); if !declare_new && !context_element.temporary_variables.contains_key(&name) { - return Err(StoryError::InvalidStoryState(format!("Could not find temporary variable to set: {}", name))); + return Err(StoryError::InvalidStoryState(format!( + "Could not find temporary variable to set: {}", + name + ))); } let old_value = context_element.temporary_variables.get(&name).cloned(); @@ -314,31 +390,45 @@ impl CallStack { pub fn context_for_variable_named(&self, name: &str) -> usize { // Check if the current temporary context contains the variable. - if self.get_current_element().temporary_variables.contains_key(name) { + if self + .get_current_element() + .temporary_variables + .contains_key(name) + { return (self.get_current_element_index() + 1) as usize; } - + // Otherwise, it's a global variable. 0 } - pub fn get_temporary_variable_with_name(&self, name: &str, context_index: i32) -> Option> { + pub fn get_temporary_variable_with_name( + &self, + name: &str, + context_index: i32, + ) -> Option> { let mut context_index = context_index; if context_index == -1 { context_index = self.get_current_element_index() + 1; } - let context_element = self.get_callstack().get((context_index - 1)as usize); + let context_element = self.get_callstack().get((context_index - 1) as usize); let var_value = context_element.unwrap().temporary_variables.get(name); var_value.cloned() } - pub fn push( &mut self, t: PushPopType, external_evaluation_stack_height: usize, output_stream_length_with_pushed: i32) { + pub fn push( + &mut self, + t: PushPopType, + external_evaluation_stack_height: usize, + output_stream_length_with_pushed: i32, + ) { // When pushing to callstack, maintain the current content path, but // jump // out of expressions by default - let mut element = Element::new(t, self.get_current_element().current_pointer.clone(), false); + let mut element = + Element::new(t, self.get_current_element().current_pointer.clone(), false); element.evaluation_stack_height_when_pushed = external_evaluation_stack_height; element.function_start_in_output_stream = output_stream_length_with_pushed; @@ -365,13 +455,16 @@ impl CallStack { self.threads.iter().find(|&t| t.thread_index == index) } - pub fn load_json(&mut self, main_content_container: &Rc, j_obj: &Map) -> Result<(), StoryError> { - + pub fn load_json( + &mut self, + main_content_container: &Rc, + j_obj: &Map, + ) -> Result<(), StoryError> { self.threads.clear(); let j_threads = j_obj.get("threads").unwrap(); - for j_thread_tok in j_threads.as_array().unwrap().iter() { + for j_thread_tok in j_threads.as_array().unwrap().iter() { let j_thread_obj = j_thread_tok.as_object().unwrap(); let thread = Thread::from_json(main_content_container, j_thread_obj)?; self.threads.push(thread); @@ -385,7 +478,7 @@ impl CallStack { pub fn get_callstack_trace(&self) -> String { let mut sb = String::new(); - + for (t, thread) in self.threads.iter().enumerate() { let is_current = t == self.threads.len() - 1; @@ -393,27 +486,27 @@ impl CallStack { "=== THREAD {}/{} {}===", t + 1, self.threads.len(), - if is_current { &"(current) "} else { &""} + if is_current { &"(current) " } else { &"" } )); - + for element in &thread.callstack { if element.push_pop_type == PushPopType::Function { - sb.push_str( " [FUNCTION] "); + sb.push_str(" [FUNCTION] "); } else { sb.push_str(" [TUNNEL] "); } - + let pointer = &element.current_pointer; - + if !pointer.is_null() { sb.push_str(&format!( "\n", - pointer.container.as_ref().unwrap().get_path()) - ) + pointer.container.as_ref().unwrap().get_path() + )) } } } - + sb } -} \ No newline at end of file +} diff --git a/lib/src/choice.rs b/lib/src/choice.rs index 8118a40..ca68801 100644 --- a/lib/src/choice.rs +++ b/lib/src/choice.rs @@ -1,7 +1,11 @@ use core::fmt; use std::cell::RefCell; -use crate::{path::Path, callstack::Thread, object::{Object, RTObject}}; +use crate::{ + callstack::Thread, + object::{Object, RTObject}, + path::Path, +}; pub struct Choice { obj: Object, @@ -16,7 +20,14 @@ pub struct Choice { } impl Choice { - pub fn new(target_path: Path, source_path: String, is_invisible_default: bool, tags: Vec, thread_at_generation: Thread, text: String) -> Choice { + pub fn new( + target_path: Path, + source_path: String, + is_invisible_default: bool, + tags: Vec, + thread_at_generation: Thread, + text: String, + ) -> Choice { Self { obj: Object::new(), target_path, @@ -30,8 +41,13 @@ impl Choice { } } - pub fn new_from_json(path_string_on_choice: &str, source_path: String, text: &str, index: usize, original_thread_index: usize) -> Choice { - + pub fn new_from_json( + path_string_on_choice: &str, + source_path: String, + text: &str, + index: usize, + original_thread_index: usize, + ) -> Choice { Choice { obj: Object::new(), target_path: Path::new_with_components_string(Some(path_string_on_choice)), @@ -50,7 +66,10 @@ impl Choice { } pub fn get_thread_at_generation(&self) -> Option { - self.thread_at_generation.borrow().as_ref().map(|t| t.copy()) + self.thread_at_generation + .borrow() + .as_ref() + .map(|t| t.copy()) } } @@ -64,4 +83,4 @@ impl fmt::Display for Choice { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "**Choice**") } -} \ No newline at end of file +} diff --git a/lib/src/choice_point.rs b/lib/src/choice_point.rs index 7eb202b..a0d467d 100644 --- a/lib/src/choice_point.rs +++ b/lib/src/choice_point.rs @@ -1,7 +1,11 @@ use core::fmt; -use std::{rc::Rc, cell::RefCell}; +use std::{cell::RefCell, rc::Rc}; -use crate::{path::Path, object::{Object, RTObject}, container::Container}; +use crate::{ + container::Container, + object::{Object, RTObject}, + path::Path, +}; pub struct ChoicePoint { obj: Object, @@ -22,7 +26,9 @@ impl ChoicePoint { is_invisible_default: (flags & 8) > 0, once_only: (flags & 16) > 0, has_condition: (flags & 1) > 0, - path_on_choice: RefCell::new(Path::new_with_components_string(Some(path_string_on_choice))), + path_on_choice: RefCell::new(Path::new_with_components_string(Some( + path_string_on_choice, + ))), } } @@ -72,7 +78,7 @@ impl ChoicePoint { pub fn get_path_on_choice(self: &Rc) -> Path { // Resolve any relative paths to global ones as we come across them - if self.path_on_choice.borrow().is_relative(){ + if self.path_on_choice.borrow().is_relative() { if let Some(choice_target_obj) = self.get_choice_target() { self.path_on_choice.replace(choice_target_obj.get_path()); } @@ -104,7 +110,6 @@ impl fmt::Display for ChoicePoint { // target_string = format!(" line {}({})", line_num, target_string); // } - - write!(f,"Choice: -> {}", target_string) + write!(f, "Choice: -> {}", target_string) } -} \ No newline at end of file +} diff --git a/lib/src/container.rs b/lib/src/container.rs index aaa6371..6d3db5d 100644 --- a/lib/src/container.rs +++ b/lib/src/container.rs @@ -1,13 +1,13 @@ -use std::{ - fmt, - rc::Rc, collections::HashMap, -}; +use std::{collections::HashMap, fmt, rc::Rc}; use as_any::Downcast; use crate::{ object::{Object, RTObject}, - value::Value, path::{Path, Component}, search_result::SearchResult, value_type::ValueType, + path::{Component, Path}, + search_result::SearchResult, + value::Value, + value_type::ValueType, }; const COUNTFLAGS_VISITS: i32 = 1; @@ -25,10 +25,14 @@ pub struct Container { } impl Container { - pub fn new(name: Option, count_flags: i32, content: Vec>, named_content: HashMap>) -> Rc { - + pub fn new( + name: Option, + count_flags: i32, + content: Vec>, + named_content: HashMap>, + ) -> Rc { let mut named_content = named_content; - + content.iter().for_each(|o| { if let Ok(c) = o.clone().into_any().downcast::() { if c.has_valid_name() { @@ -37,7 +41,8 @@ impl Container { } }); - let (visits_should_be_counted, turn_index_should_be_counted, counting_at_start_only) = Container::split_count_flags(count_flags); + let (visits_should_be_counted, turn_index_should_be_counted, counting_at_start_only) = + Container::split_count_flags(count_flags); let c = Rc::new(Container { obj: Object::new(), @@ -50,7 +55,9 @@ impl Container { }); c.content.iter().for_each(|o| o.get_object().set_parent(&c)); - c.named_content.values().for_each(|o| o.get_object().set_parent(&c)); + c.named_content + .values() + .for_each(|o| o.get_object().set_parent(&c)); c } @@ -124,9 +131,9 @@ impl Container { sb.push('\n'); } - let mut only_named: HashMap> = HashMap::new(); + let mut only_named: HashMap> = HashMap::new(); - for (k, v) in self.named_content.iter() { + for (k, v) in self.named_content.iter() { let o: Rc = v.clone(); if self.content.iter().any(|e| Rc::ptr_eq(e, &o)) { continue; @@ -135,8 +142,6 @@ impl Container { } } - - if !only_named.is_empty() { Container::append_indentation(sb, indentation); @@ -173,29 +178,28 @@ impl Container { partial_path_start: usize, mut partial_path_length: i32, ) -> SearchResult { - if partial_path_length == -1 { partial_path_length = path.len() as i32; - } - + } + let mut approximate = false; - + let mut current_container = Some(self.clone()); - let mut current_obj:Rc = self.clone(); - + let mut current_obj: Rc = self.clone(); + for i in partial_path_start..partial_path_length as usize { let comp = path.get_component(i); - + // Path component was wrong type if current_container.is_none() { approximate = true; break; } - + let found_obj = current_container .unwrap() .content_with_path_component(comp.unwrap()); - + // Couldn't resolve entire path? if found_obj.is_none() { approximate = true; @@ -203,32 +207,32 @@ impl Container { } current_obj = found_obj.unwrap().clone(); - current_container = if let Ok(container) = current_obj.clone().into_any().downcast::() { - Some(container) - } else { - None - }; + current_container = + if let Ok(container) = current_obj.clone().into_any().downcast::() { + Some(container) + } else { + None + }; } - + SearchResult::new(current_obj, approximate) } - pub fn get_count_flags(&self) -> i32 { let mut flags: i32 = 0; - + if self.visits_should_be_counted { - flags |= COUNTFLAGS_VISITS + flags |= COUNTFLAGS_VISITS } - + if self.turn_index_should_be_counted { - flags |= COUNTFLAGS_TURNS; + flags |= COUNTFLAGS_TURNS; } - + if self.counting_at_start_only { 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 @@ -237,19 +241,22 @@ impl Container { if flags == COUNTFLAGS_COUNTSTARTONLY { flags = 0; } - + flags } fn split_count_flags(value: i32) -> (bool, bool, bool) { + let visits_should_be_counted = (value & COUNTFLAGS_VISITS) > 0; + + let turn_index_should_be_counted = (value & COUNTFLAGS_TURNS) > 0; - let visits_should_be_counted = (value & COUNTFLAGS_VISITS) > 0 ; - - let turn_index_should_be_counted = (value & COUNTFLAGS_TURNS) > 0 ; - - let counting_at_start_only = (value & COUNTFLAGS_COUNTSTARTONLY) > 0 ; - - (visits_should_be_counted, turn_index_should_be_counted, counting_at_start_only) + let counting_at_start_only = (value & COUNTFLAGS_COUNTSTARTONLY) > 0; + + ( + visits_should_be_counted, + turn_index_should_be_counted, + counting_at_start_only, + ) } fn content_with_path_component(&self, component: &Component) -> Option> { @@ -265,21 +272,22 @@ impl Container { return match self.get_object().get_parent() { Some(o) => Some(o as Rc), None => None, - } - } else if let Some(found_content) = self.named_content.get(component.name.as_ref().unwrap()) { + }; + } else if let Some(found_content) = self.named_content.get(component.name.as_ref().unwrap()) + { return Some(found_content.clone()); } - None + None } pub fn get_named_only_content(&self) -> HashMap> { let mut named_only_content_dict = HashMap::new(); - + for (key, value) in self.named_content.iter() { named_only_content_dict.insert(key.clone(), value.clone()); } - + for c in &self.content { if let Some(named) = c.as_any().downcast_ref::() { if named.has_valid_name() { @@ -287,10 +295,9 @@ impl Container { } } } - + named_only_content_dict - } - + } } impl RTObject for Container { @@ -301,6 +308,10 @@ impl RTObject for Container { impl fmt::Display for Container { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Container ({})", self.name.as_ref().unwrap_or(&"".to_owned())) + write!( + f, + "Container ({})", + self.name.as_ref().unwrap_or(&"".to_owned()) + ) } -} \ No newline at end of file +} diff --git a/lib/src/control_command.rs b/lib/src/control_command.rs index 599ab91..c269896 100644 --- a/lib/src/control_command.rs +++ b/lib/src/control_command.rs @@ -2,7 +2,7 @@ use std::fmt; use strum::Display; -use crate::object::{RTObject, Object}; +use crate::object::{Object, RTObject}; #[derive(PartialEq, Display, Clone, Copy)] pub enum CommandType { @@ -31,7 +31,7 @@ pub enum CommandType { ListRange, ListRandom, BeginTag, - EndTag + EndTag, } const EVAL_START_NAME: &str = "ev"; @@ -63,11 +63,10 @@ const END_TAG_NAME: &str = "/#"; pub struct ControlCommand { obj: Object, - pub command_type: CommandType + pub command_type: CommandType, } impl ControlCommand { - pub fn new_from_name(name: &str) -> Option { match name { EVAL_START_NAME => Some(Self::new(CommandType::EvalStart)), @@ -93,12 +92,11 @@ impl ControlCommand { END_NAME => Some(Self::new(CommandType::End)), LIST_FROM_INT_NAME => Some(Self::new(CommandType::ListFromInt)), LIST_RANGE_NAME => Some(Self::new(CommandType::ListRange)), - LIST_RANDOM_NAME => Some(Self::new(CommandType::ListRandom,)), + LIST_RANDOM_NAME => Some(Self::new(CommandType::ListRandom)), BEGIN_TAG_NAME => Some(Self::new(CommandType::BeginTag)), END_TAG_NAME => Some(Self::new(CommandType::EndTag)), _ => None, } - } pub fn get_name(c: CommandType) -> String { @@ -133,14 +131,17 @@ impl ControlCommand { } pub fn new(command_type: CommandType) -> Self { - Self {obj: Object::new(), command_type} + Self { + obj: Object::new(), + command_type, + } } } impl RTObject for ControlCommand { fn get_object(&self) -> &Object { &self.obj - } + } } impl fmt::Display for ControlCommand { @@ -148,5 +149,3 @@ impl fmt::Display for ControlCommand { write!(f, "{}", self.command_type) } } - - diff --git a/lib/src/divert.rs b/lib/src/divert.rs index 066943b..e74b9e1 100644 --- a/lib/src/divert.rs +++ b/lib/src/divert.rs @@ -1,10 +1,13 @@ -use std::{ - fmt, rc::Rc, cell::RefCell, +use std::{cell::RefCell, fmt, rc::Rc}; + +use crate::{ + container::Container, + object::{Object, RTObject}, + path::{Component, Path}, + pointer::{self, Pointer}, + push_pop::PushPopType, }; -use crate::{object::{Object, RTObject}, push_pop::PushPopType, pointer::{Pointer, self}, path::{Path, Component}, container::Container}; - - pub struct Divert { obj: Object, target_pointer: RefCell, @@ -14,11 +17,19 @@ pub struct Divert { pub is_external: bool, pub pushes_to_stack: bool, pub stack_push_type: PushPopType, - pub variable_divert_name: Option, + pub variable_divert_name: Option, } impl Divert { - pub fn new(pushes_to_stack: bool, stack_push_type: PushPopType, is_external: bool, external_args: usize, is_conditional: bool, var_divert_name: Option, target_path: Option<&str>) -> Self { + pub fn new( + pushes_to_stack: bool, + stack_push_type: PushPopType, + is_external: bool, + external_args: usize, + is_conditional: bool, + var_divert_name: Option, + target_path: Option<&str>, + ) -> Self { Divert { obj: Object::new(), is_conditional, @@ -32,12 +43,14 @@ impl Divert { } } - fn target_path_string(value: Option<&str>) -> Option{ + fn target_path_string(value: Option<&str>) -> Option { value.map(|value| Path::new_with_components_string(Some(value))) } pub fn get_target_path_string(self: &Rc) -> Option { - self.get_target_path().as_ref().map(|p| self.compact_path_string(p)) + self.get_target_path() + .as_ref() + .map(|p| self.compact_path_string(p)) } pub fn has_variable_target(&self) -> bool { @@ -47,16 +60,18 @@ impl Divert { fn compact_path_string(&self, other_path: &Path) -> String { let global_path_str; let relative_path_str; - + if other_path.is_relative() { relative_path_str = other_path.get_components_string(); - global_path_str = Object::get_path(self).path_by_appending_path(other_path).get_components_string(); + global_path_str = Object::get_path(self) + .path_by_appending_path(other_path) + .get_components_string(); } else { let relative_path = self.convert_path_to_relative(other_path); relative_path_str = relative_path.get_components_string(); global_path_str = other_path.get_components_string(); } - + if relative_path_str.len() < global_path_str.len() { relative_path_str.clone() } else { @@ -67,71 +82,90 @@ impl Divert { pub fn get_target_pointer(self: &Rc) -> Pointer { let target_pointer_null = self.target_pointer.borrow().is_null(); if target_pointer_null { - let target_obj = Object::resolve_path(self.clone(), self.target_path.borrow().as_ref().unwrap()).obj.clone(); - - if self.target_path.borrow().as_ref().unwrap().get_last_component().unwrap().is_index() { + let target_obj = + Object::resolve_path(self.clone(), self.target_path.borrow().as_ref().unwrap()) + .obj + .clone(); + + if self + .target_path + .borrow() + .as_ref() + .unwrap() + .get_last_component() + .unwrap() + .is_index() + { self.target_pointer.borrow_mut().container = target_obj.get_object().get_parent(); - self.target_pointer.borrow_mut().index = self.target_path.borrow().as_ref().unwrap().get_last_component().unwrap().index.unwrap() as i32; + self.target_pointer.borrow_mut().index = self + .target_path + .borrow() + .as_ref() + .unwrap() + .get_last_component() + .unwrap() + .index + .unwrap() as i32; } else { let c = target_obj.into_any().downcast::(); self.target_pointer.replace(Pointer::start_of(c.unwrap())); } } - + self.target_pointer.borrow().clone() } - + pub fn get_target_path(self: &Rc) -> Option { // Resolve any relative paths to global ones as we come across them let target_path = self.target_path.borrow(); - + match target_path.as_ref() { Some(target_path) => { - if target_path.is_relative() { - let target_obj = self.get_target_pointer().resolve(); - - if let Some(target_obj) = target_obj { - self.target_path.replace(Some(Object::get_path(target_obj.as_ref()))); - } + if target_path.is_relative() { + let target_obj = self.get_target_pointer().resolve(); + + if let Some(target_obj) = target_obj { + self.target_path + .replace(Some(Object::get_path(target_obj.as_ref()))); } - Some(self.target_path.borrow().as_ref().unwrap().clone()) - }, + } + Some(self.target_path.borrow().as_ref().unwrap().clone()) + } None => None, - } + } } - fn convert_path_to_relative(&self, global_path: &Path) -> Path { let own_path = Object::get_path(self); let min_path_length = std::cmp::min(global_path.len(), own_path.len()); let mut last_shared_path_comp_index: i32 = -1; - + for i in 0..min_path_length { let own_comp = own_path.get_component(i); let other_comp = global_path.get_component(i); - + if own_comp.eq(&other_comp) { last_shared_path_comp_index = i as i32; } else { break; } } - + if last_shared_path_comp_index == -1 { return global_path.clone(); } - + let num_upwards_moves = (own_path.len() - 1) - last_shared_path_comp_index as usize; let mut new_path_comps = Vec::new(); - + for _ in 0..num_upwards_moves { new_path_comps.push(Component::to_parent()); } - - for down in (last_shared_path_comp_index as usize + 1)..global_path.len() { + + for down in (last_shared_path_comp_index as usize + 1)..global_path.len() { new_path_comps.push(global_path.get_component(down).unwrap().clone()); } - + Path::new(&new_path_comps, true) } } @@ -151,7 +185,12 @@ impl fmt::Display for Divert { } else if self.target_path.borrow().is_none() { result.push_str("Divert(null)"); } else { - let target_str = self.target_path.borrow().as_ref().unwrap().get_components_string(); + let target_str = self + .target_path + .borrow() + .as_ref() + .unwrap() + .get_components_string(); result.push_str("Divert"); @@ -178,4 +217,4 @@ impl fmt::Display for Divert { write!(f, "{result}") } -} \ No newline at end of file +} diff --git a/lib/src/flow.rs b/lib/src/flow.rs index 28319ef..0d01521 100644 --- a/lib/src/flow.rs +++ b/lib/src/flow.rs @@ -1,36 +1,71 @@ -use std::{rc::Rc, cell::RefCell}; +use std::{cell::RefCell, rc::Rc}; use serde_json::Map; -use crate::{callstack::{CallStack, Thread}, choice::Choice, object::RTObject, container::Container, json_write, json_read, story_error::StoryError}; +use crate::{ + callstack::{CallStack, Thread}, + choice::Choice, + container::Container, + json_read, json_write, + object::RTObject, + story_error::StoryError, +}; #[derive(Clone)] pub(crate) struct Flow { pub name: String, pub callstack: Rc>, pub output_stream: Vec>, - pub current_choices: Vec> + pub current_choices: Vec>, } impl Flow { pub fn new(name: &str, main_content_container: Rc) -> Flow { - Flow { + Flow { name: name.to_string(), callstack: Rc::new(RefCell::new(CallStack::new(main_content_container))), output_stream: Vec::new(), - current_choices: Vec::new() + current_choices: Vec::new(), } } - pub fn from_json(name: &str, main_content_container: Rc, j_obj: &Map) -> Result { - let mut flow = Self { + pub fn from_json( + name: &str, + main_content_container: Rc, + j_obj: &Map, + ) -> Result { + let mut flow = Self { name: name.to_string(), callstack: Rc::new(RefCell::new(CallStack::new(main_content_container.clone()))), - output_stream: json_read::jarray_to_runtime_obj_list(j_obj.get("outputStream").ok_or(StoryError::BadJson("outputStream not found.".to_owned()))?.as_array().unwrap(), false)?, - current_choices: json_read::jarray_to_runtime_obj_list(j_obj.get("currentChoices").ok_or(StoryError::BadJson("currentChoices not found.".to_owned()))?.as_array().unwrap(), false)?.iter().map(|o| o.clone().into_any().downcast::().unwrap()).collect::>>(), + output_stream: json_read::jarray_to_runtime_obj_list( + j_obj + .get("outputStream") + .ok_or(StoryError::BadJson("outputStream not found.".to_owned()))? + .as_array() + .unwrap(), + false, + )?, + current_choices: json_read::jarray_to_runtime_obj_list( + j_obj + .get("currentChoices") + .ok_or(StoryError::BadJson("currentChoices not found.".to_owned()))? + .as_array() + .unwrap(), + false, + )? + .iter() + .map(|o| o.clone().into_any().downcast::().unwrap()) + .collect::>>(), }; - flow.callstack.borrow_mut().load_json(&main_content_container, j_obj.get("callstack").ok_or(StoryError::BadJson("loading callstack".to_owned()))?.as_object().unwrap())?; + flow.callstack.borrow_mut().load_json( + &main_content_container, + j_obj + .get("callstack") + .ok_or(StoryError::BadJson("loading callstack".to_owned()))? + .as_object() + .unwrap(), + )?; let j_choice_threads = j_obj.get("choiceThreads"); flow.load_flow_choice_threads(j_choice_threads, main_content_container)?; @@ -41,23 +76,38 @@ impl Flow { pub(crate) fn write_json(&self) -> Result { let mut flow: Map = Map::new(); - flow.insert("callstack".to_owned(), self.callstack.borrow().write_json()?); - flow.insert("outputStream".to_owned(), json_write::write_list_rt_objs(&self.output_stream)?); - + flow.insert( + "callstack".to_owned(), + self.callstack.borrow().write_json()?, + ); + flow.insert( + "outputStream".to_owned(), + json_write::write_list_rt_objs(&self.output_stream)?, + ); + // choiceThreads: optional // Has to come BEFORE the choices themselves are written out // since the originalThreadIndex of each choice needs to be set let mut has_choice_threads = false; let mut jct: Map = Map::new(); for c in self.current_choices.iter() { - c.original_thread_index.replace(c.get_thread_at_generation().unwrap().thread_index); - - if self.callstack.borrow().get_thread_with_index(*c.original_thread_index.borrow()).is_none() { + c.original_thread_index + .replace(c.get_thread_at_generation().unwrap().thread_index); + + if self + .callstack + .borrow() + .get_thread_with_index(*c.original_thread_index.borrow()) + .is_none() + { if !has_choice_threads { has_choice_threads = true; } - jct.insert(c.original_thread_index.borrow().to_string(), c.get_thread_at_generation().unwrap().write_json()?); + jct.insert( + c.original_thread_index.borrow().to_string(), + c.get_thread_at_generation().unwrap().write_json()?, + ); } } @@ -70,23 +120,40 @@ impl Flow { c_array.push(json_write::write_choice(c)); } - flow.insert("currentChoices".to_owned(), serde_json::Value::Array(c_array)); + flow.insert( + "currentChoices".to_owned(), + serde_json::Value::Array(c_array), + ); Ok(serde_json::Value::Object(flow)) } - pub fn load_flow_choice_threads(&mut self, j_choice_threads: Option<&serde_json::Value>, main_content_container: Rc) -> Result<(), StoryError>{ + pub fn load_flow_choice_threads( + &mut self, + j_choice_threads: Option<&serde_json::Value>, + main_content_container: Rc, + ) -> Result<(), StoryError> { for choice in self.current_choices.iter_mut() { - self.callstack.borrow().get_thread_with_index(*choice.original_thread_index.borrow()) - .map(|o| choice.set_thread_at_generation(o.copy())) - .or_else(|| { - let j_saved_choice_thread = - j_choice_threads.and_then(|c| c.get(choice.original_thread_index.borrow().to_string())).ok_or("loading choice threads").unwrap(); - choice.set_thread_at_generation(Thread::from_json(&main_content_container, j_saved_choice_thread.as_object().unwrap()).unwrap()); - Some(()) - }); + self.callstack + .borrow() + .get_thread_with_index(*choice.original_thread_index.borrow()) + .map(|o| choice.set_thread_at_generation(o.copy())) + .or_else(|| { + let j_saved_choice_thread = j_choice_threads + .and_then(|c| c.get(choice.original_thread_index.borrow().to_string())) + .ok_or("loading choice threads") + .unwrap(); + choice.set_thread_at_generation( + Thread::from_json( + &main_content_container, + j_saved_choice_thread.as_object().unwrap(), + ) + .unwrap(), + ); + Some(()) + }); } Ok(()) } -} \ No newline at end of file +} diff --git a/lib/src/glue.rs b/lib/src/glue.rs index 0d561a2..4ba1370 100644 --- a/lib/src/glue.rs +++ b/lib/src/glue.rs @@ -2,14 +2,13 @@ use std::fmt; use crate::object::{Object, RTObject}; - pub struct Glue { obj: Object, } impl Glue { pub fn new() -> Self { - Glue {obj: Object::new()} + Glue { obj: Object::new() } } } @@ -23,4 +22,4 @@ impl fmt::Display for Glue { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Glue") } -} \ No newline at end of file +} diff --git a/lib/src/ink_list.rs b/lib/src/ink_list.rs index be0ef54..14e521c 100644 --- a/lib/src/ink_list.rs +++ b/lib/src/ink_list.rs @@ -1,8 +1,10 @@ use core::fmt; -use std::{collections::HashMap, cell::RefCell}; - -use crate::{ink_list_item::InkListItem, list_definition::ListDefinition, list_definitions_origin::ListDefinitionsOrigin, value_type::ValueType, story_error::StoryError}; +use std::{cell::RefCell, collections::HashMap}; +use crate::{ + ink_list_item::InkListItem, list_definition::ListDefinition, + list_definitions_origin::ListDefinitionsOrigin, story_error::StoryError, value_type::ValueType, +}; #[derive(Clone)] pub struct InkList { @@ -21,14 +23,17 @@ impl InkList { } } - pub fn from_single_element(single_element: (InkListItem, i32)) -> Self { + pub fn from_single_element(single_element: (InkListItem, i32)) -> Self { let mut l = Self::new(); l.items.insert(single_element.0, single_element.1); l } - pub fn from_single_origin(single_origin: String, list_definitions: &ListDefinitionsOrigin) -> Result { + pub fn from_single_origin( + single_origin: String, + list_definitions: &ListDefinitionsOrigin, + ) -> Result { let l = Self::new(); l.initial_origin_names.borrow_mut().push(single_origin); @@ -65,8 +70,7 @@ impl InkList { let mut ordered: Vec<_> = self.items.iter().collect(); ordered.sort_by(|a, b| { if a.1 == b.1 { - a.0.get_origin_name() - .cmp(&b.0.get_origin_name()) + a.0.get_origin_name().cmp(&b.0.get_origin_name()) } else { a.1.cmp(b.1) } @@ -77,11 +81,10 @@ impl InkList { pub fn get_max_item(&self) -> Option<(&InkListItem, i32)> { let mut max: Option<(&InkListItem, i32)> = None; - for (k,v) in &self.items { + for (k, v) in &self.items { if max.is_none() || *v > max.as_ref().unwrap().1 { max = Some((k, *v)); } - } max @@ -90,11 +93,10 @@ impl InkList { pub fn get_min_item(&self) -> Option<(&InkListItem, i32)> { let mut min: Option<(&InkListItem, i32)> = None; - for (k,v) in &self.items { + for (k, v) in &self.items { if min.is_none() || *v < min.as_ref().unwrap().1 { min = Some((k, *v)); } - } min @@ -106,7 +108,6 @@ impl InkList { pub fn get_origin_names(&self) -> Vec { if !self.items.is_empty() { - let mut names = Vec::new(); for k in self.items.keys() { @@ -121,8 +122,8 @@ impl InkList { pub fn union(&self, other_list: &InkList) -> InkList { let mut union = InkList::from_other_list(self); - - for (key, value) in &other_list.items { + + for (key, value) in &other_list.items { union.items.insert(key.clone(), *value); } @@ -131,7 +132,7 @@ impl InkList { pub fn without(&self, other_list: &InkList) -> InkList { let mut result = InkList::from_other_list(self); - + other_list.items.iter().for_each(|(key, _)| { result.items.remove(key); }); @@ -141,7 +142,7 @@ impl InkList { pub fn intersect(&self, other_list: &InkList) -> InkList { let mut intersection = InkList::new(); - + for (k, v) in &self.items { if other_list.items.contains_key(k) { intersection.items.insert(k.clone(), *v); @@ -153,7 +154,7 @@ impl InkList { pub fn has(&self, other_list: &InkList) -> InkList { let mut result = InkList::new(); - + for (k, v) in &self.items { if other_list.items.contains_key(k) { result.items.insert(k.clone(), *v); @@ -164,13 +165,17 @@ impl InkList { } pub fn contains(&self, other_list: &InkList) -> bool { - if other_list.items.is_empty() || self.items.is_empty() { return false; } + if other_list.items.is_empty() || self.items.is_empty() { + return false; + } for k in other_list.items.keys() { - if !self.items.contains_key(k) { return false; } + if !self.items.contains_key(k) { + return false; + } } - true + true } pub(crate) fn get_all(&self) -> InkList { @@ -183,8 +188,14 @@ impl InkList { list } - pub(crate) fn list_with_sub_range(&self, min_bound: &ValueType, max_bound: &ValueType) -> InkList { - if self.items.is_empty() {return InkList::new();} + pub(crate) fn list_with_sub_range( + &self, + min_bound: &ValueType, + max_bound: &ValueType, + ) -> InkList { + if self.items.is_empty() { + return InkList::new(); + } let ordered = self.get_ordered_items(); let mut min_value = 0; @@ -237,8 +248,11 @@ impl InkList { true => InkList::new(), false => { let item = self.get_max_item(); - InkList::from_single_element((item.as_ref().unwrap().0.clone(), item.as_ref().unwrap().1)) - }, + InkList::from_single_element(( + item.as_ref().unwrap().0.clone(), + item.as_ref().unwrap().1, + )) + } } } @@ -247,8 +261,11 @@ impl InkList { true => InkList::new(), false => { let item = self.get_min_item(); - InkList::from_single_element((item.as_ref().unwrap().0.clone(), item.as_ref().unwrap().1)) - }, + InkList::from_single_element(( + item.as_ref().unwrap().0.clone(), + item.as_ref().unwrap().1, + )) + } } } @@ -261,7 +278,7 @@ impl InkList { if other_list.items.is_empty() { return true; } - + // All greater self.get_min_item().unwrap().1 > other_list.get_max_item().unwrap().1 } @@ -275,7 +292,7 @@ impl InkList { if other_list.items.is_empty() { return true; } - + // All greater self.get_min_item().unwrap().1 >= other_list.get_min_item().unwrap().1 && self.get_max_item().unwrap().1 >= other_list.get_max_item().unwrap().1 @@ -290,7 +307,7 @@ impl InkList { if self.items.is_empty() { return true; } - + self.get_max_item().unwrap().1 < other_list.get_min_item().unwrap().1 } @@ -303,7 +320,7 @@ impl InkList { if self.items.is_empty() { return true; } - + self.get_max_item().unwrap().1 <= other_list.get_max_item().unwrap().1 && self.get_min_item().unwrap().1 <= other_list.get_min_item().unwrap().1 } @@ -317,10 +334,14 @@ impl Default for InkList { impl PartialEq for InkList { fn eq(&self, other: &Self) -> bool { - if other.items.len() != self.items.len() {return false;} + if other.items.len() != self.items.len() { + return false; + } for key in self.items.keys() { - if !other.items.contains_key(key) {return false;} + if !other.items.contains_key(key) { + return false; + } } true @@ -329,7 +350,6 @@ impl PartialEq for InkList { impl fmt::Display for InkList { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let ordered = self.get_ordered_items(); let mut result = String::new(); @@ -342,4 +362,4 @@ impl fmt::Display for InkList { write!(f, "{}", result) } -} \ No newline at end of file +} diff --git a/lib/src/json_read.rs b/lib/src/json_read.rs index 8f5bf83..a40e899 100644 --- a/lib/src/json_read.rs +++ b/lib/src/json_read.rs @@ -3,35 +3,49 @@ use std::{collections::HashMap, rc::Rc}; use serde_json::Map; use crate::{ - container::Container, - object::RTObject, control_command::ControlCommand, value::Value, glue::Glue, path::Path, choice_point::ChoicePoint, choice::Choice, push_pop::PushPopType, divert::Divert, variable_assigment::VariableAssignment, void::Void, variable_reference::VariableReference, native_function_call::NativeFunctionCall, tag::Tag, ink_list::InkList, ink_list_item::InkListItem, list_definitions_origin::ListDefinitionsOrigin, list_definition::ListDefinition, story_error::StoryError, + choice::Choice, choice_point::ChoicePoint, container::Container, + control_command::ControlCommand, divert::Divert, glue::Glue, ink_list::InkList, + ink_list_item::InkListItem, list_definition::ListDefinition, + list_definitions_origin::ListDefinitionsOrigin, native_function_call::NativeFunctionCall, + object::RTObject, path::Path, push_pop::PushPopType, story_error::StoryError, tag::Tag, + value::Value, variable_assigment::VariableAssignment, variable_reference::VariableReference, + void::Void, }; -pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) -> Result, StoryError> { +pub fn jtoken_to_runtime_object( + token: &serde_json::Value, + name: Option, +) -> Result, StoryError> { match token { - serde_json::Value::Null => Err(StoryError::BadJson(format!("Failed to convert token to runtime RTObject: {}", token))), + serde_json::Value::Null => Err(StoryError::BadJson(format!( + "Failed to convert token to runtime RTObject: {}", + token + ))), serde_json::Value::Bool(value) => Ok(Rc::new(Value::new_bool(value.to_owned()))), serde_json::Value::Number(_) => { if token.is_i64() { - let val:i32 = token.as_i64().unwrap().try_into().unwrap(); + let val: i32 = token.as_i64().unwrap().try_into().unwrap(); Ok(Rc::new(Value::new_int(val))) } else { let val: f32 = token.as_f64().unwrap() as f32; Ok(Rc::new(Value::new_float(val))) } - }, + } serde_json::Value::String(value) => { let str = value.as_str(); - + // String value let first_char = str.chars().next().unwrap(); - if first_char == '^' {return Ok(Rc::new(Value::new_string(&str[1..])));} - else if first_char == '\n' && str.len() == 1 {return Ok(Rc::new(Value::new_string("\n")));} + if first_char == '^' { + return Ok(Rc::new(Value::new_string(&str[1..]))); + } else if first_char == '\n' && str.len() == 1 { + return Ok(Rc::new(Value::new_string("\n"))); + } // Glue if "<>".eq(str) { - return Ok(Rc::new(Glue::new())); + return Ok(Rc::new(Glue::new())); } if let Some(control_command) = ControlCommand::new_from_name(str) { @@ -43,24 +57,32 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) // we know it's not a string, we can convert back to the proper // symbol for the operator. let mut call_str = str; - if "L^".eq(str) {call_str = "^";} + if "L^".eq(str) { + call_str = "^"; + } if let Some(native_function_call) = NativeFunctionCall::new_from_name(call_str) { return Ok(Rc::new(native_function_call)); } - - // Void - if "void".eq(str) {return Ok(Rc::new(Void::new()));} + // Void + if "void".eq(str) { + return Ok(Rc::new(Void::new())); + } - Err(StoryError::BadJson(format!("Failed to convert token to runtime RTObject: {}", token))) - }, + Err(StoryError::BadJson(format!( + "Failed to convert token to runtime RTObject: {}", + token + ))) + } serde_json::Value::Array(value) => Ok(jarray_to_container(value, name)?), serde_json::Value::Object(obj) => { // Divert target value to path let prop_value = obj.get("^->"); if let Some(prop_value) = prop_value { - return Ok(Rc::new(Value::new_divert_target(Path::new_with_components_string(prop_value.as_str())))); + return Ok(Rc::new(Value::new_divert_target( + Path::new_with_components_string(prop_value.as_str()), + ))); } // // VariablePointerValue @@ -75,8 +97,8 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) contex_index = v.as_i64().unwrap() as i32; } - let var_ptr = Rc::new(Value::new_variable_pointer( variable_name, contex_index)); - + let var_ptr = Rc::new(Value::new_variable_pointer(variable_name, contex_index)); + return Ok(var_ptr); } @@ -138,7 +160,15 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) } } - return Ok(Rc::new(Divert::new(pushes_to_stack, div_push_type, external, external_args, conditional, var_divert_name, target_path.as_deref()))); + return Ok(Rc::new(Divert::new( + pushes_to_stack, + div_push_type, + external, + external_args, + conditional, + var_divert_name, + target_path.as_deref(), + ))); } // Choice @@ -151,7 +181,10 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) flags = f.as_u64().unwrap(); } - return Ok(Rc::new(ChoicePoint::new(flags as i32, path_string_on_choice))); + return Ok(Rc::new(ChoicePoint::new( + flags as i32, + path_string_on_choice, + ))); } // // Variable reference @@ -162,10 +195,11 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) let prop_value = obj.get("CNT?"); if let Some(v) = prop_value { - return Ok(Rc::new(VariableReference::from_path_for_count(v.as_str().unwrap()))); + return Ok(Rc::new(VariableReference::from_path_for_count( + v.as_str().unwrap(), + ))); } - // // Variable assignment let mut is_var_ass = false; let mut is_global_var = false; @@ -175,7 +209,7 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) Some(_) => { is_var_ass = true; is_global_var = true; - }, + } None => { prop_value = obj.get("temp="); if prop_value.is_some() { @@ -190,7 +224,11 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) let prop_value = obj.get("re"); let is_new_decl = prop_value.is_none(); - let var_ass = Rc::new(VariableAssignment::new(var_name, is_new_decl, is_global_var)); + let var_ass = Rc::new(VariableAssignment::new( + var_name, + is_new_decl, + is_global_var, + )); return Ok(var_ass); } @@ -212,12 +250,15 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) if let Some(o) = prop_value { let names_as_objs = o.as_array().unwrap(); - let names = names_as_objs.iter().map(|e| e.as_str().unwrap().to_string()).collect(); + let names = names_as_objs + .iter() + .map(|e| e.as_str().unwrap().to_string()) + .collect(); raw_list.set_initial_origin_names(names); } - for (k,v) in list_content { + for (k, v) in list_content { let item = InkListItem::from_full_name(k); raw_list.items.insert(item, v.as_i64().unwrap() as i32); } @@ -230,13 +271,18 @@ pub fn jtoken_to_runtime_object(token: &serde_json::Value, name: Option) return jobject_to_choice(obj); } - Err(StoryError::BadJson(format!("Failed to convert token to runtime RTObject: {}", token))) - }, + Err(StoryError::BadJson(format!( + "Failed to convert token to runtime RTObject: {}", + token + ))) + } } - } -fn jarray_to_container(jarray: &Vec, name: Option) -> Result, StoryError> { +fn jarray_to_container( + jarray: &Vec, + name: Option, +) -> Result, StoryError> { // Final object in the array is always a combination of // - named content // - a "#f" key with the countFlags @@ -245,8 +291,7 @@ fn jarray_to_container(jarray: &Vec, name: Option) -> let mut name: Option = name; let mut flags = 0; - let mut named_only_content: HashMap> = - HashMap::new(); + let mut named_only_content: HashMap> = HashMap::new(); if let Some(terminating_obj) = terminating_obj { for (k, v) in terminating_obj { @@ -254,9 +299,13 @@ fn jarray_to_container(jarray: &Vec, name: Option) -> "#f" => flags = v.as_i64().unwrap().try_into().unwrap(), "#n" => name = Some(v.as_str().unwrap().to_string()), k => { - let named_content_item = jtoken_to_runtime_object(v, Some(k.to_string())).unwrap(); - - let named_sub_container = named_content_item.into_any().downcast::().unwrap(); + let named_content_item = + jtoken_to_runtime_object(v, Some(k.to_string())).unwrap(); + + let named_sub_container = named_content_item + .into_any() + .downcast::() + .unwrap(); named_only_content.insert(k.to_string(), named_sub_container); } @@ -264,11 +313,19 @@ fn jarray_to_container(jarray: &Vec, name: Option) -> } } - let container = Container::new(name, flags, jarray_to_runtime_obj_list(jarray, true)?, named_only_content); + let container = Container::new( + name, + flags, + jarray_to_runtime_obj_list(jarray, true)?, + named_only_content, + ); Ok(container) } -pub fn jarray_to_runtime_obj_list(jarray: &Vec, skip_last: bool) -> Result>, StoryError> { +pub fn jarray_to_runtime_obj_list( + jarray: &Vec, + skip_last: bool, +) -> Result>, StoryError> { let mut count = jarray.len(); if skip_last { @@ -285,22 +342,28 @@ pub fn jarray_to_runtime_obj_list(jarray: &Vec, skip_last: bo Ok(list) } -fn jobject_to_choice(obj: &Map) -> Result, StoryError> { +fn jobject_to_choice(obj: &Map) -> Result, StoryError> { let text = obj.get("text").unwrap().as_str().unwrap(); let index = obj.get("index").unwrap().as_u64().unwrap() as usize; let source_path = obj.get("originalChoicePath").unwrap().as_str().unwrap(); let original_thread_index = obj.get("originalThreadIndex").unwrap().as_i64().unwrap() as usize; let path_string_on_choice = obj.get("targetPath").unwrap().as_str().unwrap(); - Ok(Rc::new(Choice::new_from_json(path_string_on_choice, source_path.to_string(), text, index, original_thread_index))) + Ok(Rc::new(Choice::new_from_json( + path_string_on_choice, + source_path.to_string(), + text, + index, + original_thread_index, + ))) } -pub fn jtoken_to_list_definitions(def: &serde_json::Value) -> Result { - - let mut all_defs: Vec = Vec::with_capacity(0); +pub fn jtoken_to_list_definitions( + def: &serde_json::Value, +) -> Result { + let mut all_defs: Vec = Vec::with_capacity(0); for (name, list_def_json) in def.as_object().unwrap() { - // Cast (string, object) to (string, int) for items let mut items: HashMap = HashMap::new(); for (k, v) in list_def_json.as_object().unwrap() { @@ -314,17 +377,27 @@ pub fn jtoken_to_list_definitions(def: &serde_json::Value) -> Result) -> Result>, StoryError> { +pub(crate) fn jobject_to_hashmap_values( + jobj: &Map, +) -> Result>, StoryError> { let mut dict: HashMap> = HashMap::new(); for (k, v) in jobj.iter() { - dict.insert(k.clone(), jtoken_to_runtime_object(v, None)?.into_any().downcast::().unwrap()); + dict.insert( + k.clone(), + jtoken_to_runtime_object(v, None)? + .into_any() + .downcast::() + .unwrap(), + ); } Ok(dict) } -pub(crate) fn jobject_to_int_hashmap(jobj: &Map) -> Result, StoryError> { +pub(crate) fn jobject_to_int_hashmap( + jobj: &Map, +) -> Result, StoryError> { let mut dict: HashMap = HashMap::new(); for (k, v) in jobj.iter() { diff --git a/lib/src/json_write.rs b/lib/src/json_write.rs index e17ba48..81bef20 100644 --- a/lib/src/json_write.rs +++ b/lib/src/json_write.rs @@ -1,16 +1,21 @@ use std::{collections::HashMap, rc::Rc}; -use serde_json::{Map, json}; +use serde_json::{json, Map}; use crate::{ - container::Container, - object::RTObject, value::Value, glue::Glue, choice_point::ChoicePoint, push_pop::PushPopType, divert::Divert, ink_list::InkList, control_command::ControlCommand, native_function_call::NativeFunctionCall, variable_reference::VariableReference, variable_assigment::VariableAssignment, tag::Tag, void::Void, choice::Choice, story_error::StoryError, + choice::Choice, choice_point::ChoicePoint, container::Container, + control_command::ControlCommand, divert::Divert, glue::Glue, ink_list::InkList, + native_function_call::NativeFunctionCall, object::RTObject, push_pop::PushPopType, + story_error::StoryError, tag::Tag, value::Value, variable_assigment::VariableAssignment, + variable_reference::VariableReference, void::Void, }; -pub fn write_dictionary_values(objs: &HashMap>) -> Result { +pub fn write_dictionary_values( + objs: &HashMap>, +) -> Result { let mut jobjs: Map = Map::new(); - for (k,o) in objs { + for (k, o) in objs { jobjs.insert(k.clone(), write_rtobject(o.clone())?); } @@ -25,32 +30,47 @@ pub fn write_rtobject(o: Rc) -> Result() { let mut div_type_key = "->"; - if divert.is_external { div_type_key = "x()"; } - else if divert.pushes_to_stack { - if divert.stack_push_type == PushPopType::Function {div_type_key = "f()";} - else if divert.stack_push_type == PushPopType::Tunnel {div_type_key = "->t->";} + if divert.is_external { + div_type_key = "x()"; + } else if divert.pushes_to_stack { + if divert.stack_push_type == PushPopType::Function { + div_type_key = "f()"; + } else if divert.stack_push_type == PushPopType::Tunnel { + div_type_key = "->t->"; + } } - let target_str = - if divert.has_variable_target() {divert.variable_divert_name.clone().unwrap()} - else {divert.get_target_path_string().unwrap()}; + let target_str = if divert.has_variable_target() { + divert.variable_divert_name.clone().unwrap() + } else { + divert.get_target_path_string().unwrap() + }; let mut jobj: Map = Map::new(); jobj.insert(div_type_key.to_string(), json!(target_str)); - if divert.has_variable_target() {jobj.insert("var".to_owned(), json!(true));} + if divert.has_variable_target() { + jobj.insert("var".to_owned(), json!(true)); + } - if divert.is_conditional {jobj.insert("c".to_owned(), json!(true));} + if divert.is_conditional { + jobj.insert("c".to_owned(), json!(true)); + } - if divert.external_args > 0 {jobj.insert("exArgs".to_owned(), json!(divert.external_args));} + if divert.external_args > 0 { + jobj.insert("exArgs".to_owned(), json!(divert.external_args)); + } return Ok(serde_json::Value::Object(jobj)); } if let Ok(cp) = o.clone().into_any().downcast::() { let mut jobj: Map = Map::new(); - jobj.insert("*".to_owned(), json!(ChoicePoint::get_path_string_on_choice(&cp))); + jobj.insert( + "*".to_owned(), + json!(ChoicePoint::get_path_string_on_choice(&cp)), + ); jobj.insert("flg".to_owned(), json!(cp.get_flags())); return Ok(serde_json::Value::Object(jobj)); } @@ -109,13 +129,14 @@ pub fn write_rtobject(o: Rc) -> Result() { - let mut jobj: Map = Map::new(); let read_count_path = var_ref.get_path_string_for_count(); @@ -130,13 +151,19 @@ pub fn write_rtobject(o: Rc) -> Result() { let mut jobj: Map = Map::new(); - - let key = if var_ass.is_global {"VAR=".to_owned()} else {"temp=".to_owned()}; + + let key = if var_ass.is_global { + "VAR=".to_owned() + } else { + "temp=".to_owned() + }; jobj.insert(key, json!(var_ass.variable_name)); // Reassignment? - if !var_ass.is_new_declaration {jobj.insert("re".to_owned(), json!(true));} - + if !var_ass.is_new_declaration { + jobj.insert("re".to_owned(), json!(true)); + } + return Ok(serde_json::Value::Object(jobj)); } @@ -148,7 +175,7 @@ pub fn write_rtobject(o: Rc) -> Result = Map::new(); jobj.insert("#".to_owned(), json!(tag.get_text())); - + return Ok(serde_json::Value::Object(jobj)); } @@ -156,10 +183,16 @@ pub fn write_rtobject(o: Rc) -> Result Result { +pub fn write_rt_container( + container: &Container, + without_name: bool, +) -> Result { let mut c_array: Vec = Vec::new(); for c in container.content.iter() { @@ -187,7 +220,7 @@ pub fn write_rt_container(container: &Container, without_name: bool) -> Result 0 { t_obj.insert("#f".to_owned(), json!(count_flags)); } - + if has_name_property { t_obj.insert("#n".to_owned(), json!(container.name)); } @@ -205,7 +238,6 @@ pub fn write_ink_list(list: &InkList) -> serde_json::Value { let mut jlist: Map = Map::new(); for (item, v) in list.items.iter() { - let mut name = String::new(); match item.get_origin_name() { @@ -221,7 +253,6 @@ pub fn write_ink_list(list: &InkList) -> serde_json::Value { jobj.insert("list".to_owned(), serde_json::Value::Object(jlist)); - serde_json::Value::Object(jobj) } @@ -231,15 +262,23 @@ pub fn write_choice(choice: &Choice) -> serde_json::Value { jobj.insert("text".to_owned(), json!(choice.text)); jobj.insert("index".to_owned(), json!(choice.index)); jobj.insert("originalChoicePath".to_owned(), json!(choice.source_path)); - jobj.insert("originalThreadIndex".to_owned(), json!(choice.original_thread_index)); - jobj.insert("targetPath".to_owned(), json!(choice.target_path.to_string())); + jobj.insert( + "originalThreadIndex".to_owned(), + json!(choice.original_thread_index), + ); + jobj.insert( + "targetPath".to_owned(), + json!(choice.target_path.to_string()), + ); serde_json::Value::Object(jobj) } -pub(crate) fn write_list_rt_objs(objs: &[Rc]) -> Result { +pub(crate) fn write_list_rt_objs( + objs: &[Rc], +) -> Result { let mut c_array: Vec = Vec::new(); - + for o in objs { c_array.push(write_rtobject(o.clone())?); } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index c6a0b4f..0d21f04 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,37 +1,35 @@ //! This is a Rust port of inkle's [ink](https://github.com/inkle/ink), a scripting language for writing interactive narrative. //! `bink` is fully compatible with the original version. -pub mod story; -pub mod story_callbacks; -pub mod value_type; -pub mod story_error; +mod callstack; pub mod choice; +mod choice_point; +mod container; +mod control_command; +mod divert; +mod flow; +mod glue; +mod ink_list; +mod ink_list_item; mod json_read; mod json_write; +mod list_definition; +mod list_definitions_origin; +mod native_function_call; mod object; -mod value; -mod container; -mod control_command; -mod story_state; -mod pointer; mod path; -mod search_result; -mod callstack; -mod flow; +mod pointer; mod push_pop; -mod variables_state; -mod glue; -mod void; +mod search_result; mod state_patch; -mod choice_point; +pub mod story; +pub mod story_callbacks; +pub mod story_error; +mod story_state; mod tag; -mod divert; +mod value; +pub mod value_type; mod variable_assigment; mod variable_reference; -mod native_function_call; -mod ink_list; -mod ink_list_item; -mod list_definition; -mod list_definitions_origin; - - +mod variables_state; +mod void; diff --git a/lib/src/list_definition.rs b/lib/src/list_definition.rs index c4ac4cd..e057912 100644 --- a/lib/src/list_definition.rs +++ b/lib/src/list_definition.rs @@ -2,7 +2,6 @@ use std::collections::HashMap; use crate::ink_list_item::InkListItem; - #[derive(Clone)] pub struct ListDefinition { name: String, @@ -41,7 +40,8 @@ impl ListDefinition { } pub fn contains_item(&self, item: &InkListItem) -> bool { - item.get_origin_name() == Some(&self.name) && self.item_name_to_values.contains_key(item.get_item_name()) + item.get_origin_name() == Some(&self.name) + && self.item_name_to_values.contains_key(item.get_item_name()) } pub fn contains_item_with_name(&self, item_name: &str) -> bool { diff --git a/lib/src/list_definitions_origin.rs b/lib/src/list_definitions_origin.rs index c003671..382b503 100644 --- a/lib/src/list_definitions_origin.rs +++ b/lib/src/list_definitions_origin.rs @@ -1,6 +1,6 @@ use std::{collections::HashMap, rc::Rc}; -use crate::{list_definition::ListDefinition, value::Value, ink_list::InkList}; +use crate::{ink_list::InkList, list_definition::ListDefinition, value::Value}; #[derive(Clone)] pub struct ListDefinitionsOrigin { @@ -16,7 +16,9 @@ impl ListDefinitionsOrigin { }; for list in lists { - list_definitions_origin.lists.insert(list.get_name().to_string(), list.clone()); + list_definitions_origin + .lists + .insert(list.get_name().to_string(), list.clone()); for (key, val) in list.get_items() { let mut l = InkList::new(); diff --git a/lib/src/native_function_call.rs b/lib/src/native_function_call.rs index f4c72ea..ee22396 100644 --- a/lib/src/native_function_call.rs +++ b/lib/src/native_function_call.rs @@ -1,6 +1,13 @@ use std::{fmt, rc::Rc}; -use crate::{object::{Object, RTObject}, value::Value, void::Void, ink_list::InkList, value_type::ValueType, story_error::StoryError}; +use crate::{ + ink_list::InkList, + object::{Object, RTObject}, + story_error::StoryError, + value::Value, + value_type::ValueType, + void::Void, +}; #[derive(Debug, PartialEq, Clone, Copy)] pub enum Op { @@ -10,7 +17,7 @@ pub enum Op { Multiply, Mod, Negate, - + Equal, Greater, Less, @@ -18,7 +25,7 @@ pub enum Op { LessThanOrEquals, NotEquals, Not, - + And, Or, @@ -197,10 +204,14 @@ impl NativeFunctionCall { } } - pub(crate) fn call(&self, params: Vec>) -> Result, StoryError> { - + pub(crate) fn call( + &self, + params: Vec>, + ) -> Result, StoryError> { if self.get_number_of_parameters() != params.len() { - return Err(StoryError::InvalidStoryState("Unexpected number of parameters".to_owned())); + return Err(StoryError::InvalidStoryState( + "Unexpected number of parameters".to_owned(), + )); } let mut has_list = false; @@ -226,11 +237,15 @@ impl NativeFunctionCall { self.call_type(coerced_params) } - fn call_binary_list_operation(&self, params: &[Rc]) -> Result, StoryError> { + fn call_binary_list_operation( + &self, + params: &[Rc], + ) -> Result, StoryError> { // List-Int addition/subtraction returns a List (e.g., "alpha" + 1 = "beta") - if (self.op == Op::Add || self.op == Op::Subtract) && - Value::get_list_value(params[0].as_ref()).is_some() && - Value::get_int_value(params[1].as_ref()).is_some() { + if (self.op == Op::Add || self.op == Op::Subtract) + && Value::get_list_value(params[0].as_ref()).is_some() + && Value::get_int_value(params[1].as_ref()).is_some() + { return Ok(self.call_list_increment_operation(params)); } @@ -238,10 +253,10 @@ impl NativeFunctionCall { let v2 = params[1].clone().into_any().downcast::().unwrap(); // And/or with any other type requires coercion to bool - if (self.op == Op::And || self.op == Op::Or) && - ( Value::get_list_value(params[0].as_ref()).is_none() || - Value::get_list_value(params[1].as_ref()).is_none()) { - + if (self.op == Op::And || self.op == Op::Or) + && (Value::get_list_value(params[0].as_ref()).is_none() + || Value::get_list_value(params[1].as_ref()).is_none()) + { let result = { if self.op == Op::And { v1.is_truthy()? && v2.is_truthy()? @@ -254,10 +269,11 @@ impl NativeFunctionCall { } // Normal (list • list) operation - if Value::get_list_value(params[0].as_ref()).is_some() && - Value::get_list_value(params[1].as_ref()).is_some() { + if Value::get_list_value(params[0].as_ref()).is_some() + && Value::get_list_value(params[1].as_ref()).is_some() + { let p = vec![v1.clone(), v2.clone()]; - + return self.call_type(p); } @@ -272,11 +288,10 @@ impl NativeFunctionCall { fn call_list_increment_operation(&self, list_int_params: &[Rc]) -> Rc { let list_val = Value::get_list_value(list_int_params[0].as_ref()).unwrap(); let int_val = Value::get_int_value(list_int_params[1].as_ref()).unwrap(); - + let mut result_raw_list = InkList::new(); - + for (list_item, list_item_value) in list_val.items.iter() { - let target_int = { if self.op == Op::Add { list_item_value + int_val @@ -286,18 +301,20 @@ impl NativeFunctionCall { }; let origins = list_val.origins.borrow(); - + let item_origin = origins.iter().find(|origin| { origin.get_name() == list_item.get_origin_name().unwrap_or(&"".to_owned()) }); - + if let Some(item_origin) = item_origin { if let Some(incremented_item) = item_origin.get_item_with_value(target_int) { - result_raw_list.items.insert(incremented_item.clone(), target_int); + result_raw_list + .items + .insert(incremented_item.clone(), target_int); } } } - + Rc::new(Value::new_list(result_raw_list)) } @@ -337,7 +354,10 @@ impl NativeFunctionCall { } } - fn coerce_values_to_single_type(&self, params: Vec>) -> Result>, StoryError> { + fn coerce_values_to_single_type( + &self, + params: Vec>, + ) -> Result>, StoryError> { let mut dest_type = 1; // Int let mut result: Vec> = Vec::new(); @@ -354,17 +374,20 @@ impl NativeFunctionCall { } for obj in params.iter() { - if let Some(v) = obj.as_ref().as_any().downcast_ref::() { - match v.cast(dest_type)? { + if let Some(v) = obj.as_ref().as_any().downcast_ref::() { + match v.cast(dest_type)? { Some(casted_value) => result.push(Rc::new(casted_value)), None => { if let Ok(obj) = obj.clone().into_any().downcast::() { - result.push(obj); + result.push(obj); } - }, + } } } else { - return Err(StoryError::InvalidStoryState(format!("RTObject of type Value expected: {}", obj))); + return Err(StoryError::InvalidStoryState(format!( + "RTObject of type Value expected: {}", + obj + ))); } } @@ -375,21 +398,33 @@ impl NativeFunctionCall { match ¶ms[0].value { ValueType::Bool(op1) => match params[1].value { ValueType::Bool(op2) => Ok(Rc::new(Value::new_bool(*op1 && op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Ok(Rc::new(Value::new_bool(*op1 != 0 && op2 != 0))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::Float(op1) => match params[1].value { ValueType::Float(op2) => Ok(Rc::new(Value::new_bool(*op1 != 0.0 && op2 != 0.0))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Ok(Rc::new(Value::new_bool(!op1.items.is_empty() && !op2.items.is_empty()))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) - }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + ValueType::List(op2) => Ok(Rc::new(Value::new_bool( + !op1.items.is_empty() && !op2.items.is_empty(), + ))), + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), + }, + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } @@ -397,54 +432,82 @@ impl NativeFunctionCall { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Ok(Rc::new(Value::new_bool(*op1 > op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::Float(op1) => match params[1].value { ValueType::Float(op2) => Ok(Rc::new(Value::new_bool(*op1 > op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::List(op1) => match ¶ms[1].value { ValueType::List(op2) => Ok(Rc::new(Value::new_bool(op1.greater_than(op2)))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } - fn less_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Ok(Rc::new(Value::new_bool(*op1 < op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::Float(op1) => match params[1].value { ValueType::Float(op2) => Ok(Rc::new(Value::new_bool(*op1 < op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::List(op1) => match ¶ms[1].value { ValueType::List(op2) => Ok(Rc::new(Value::new_bool(op1.less_than(op2)))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } - fn greater_than_or_equals_op(&self, params: &[Rc]) -> Result, StoryError> { + fn greater_than_or_equals_op( + &self, + params: &[Rc], + ) -> Result, StoryError> { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Ok(Rc::new(Value::new_bool(*op1 >= op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::Float(op1) => match params[1].value { ValueType::Float(op2) => Ok(Rc::new(Value::new_bool(*op1 >= op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Ok(Rc::new(Value::new_bool(op1.greater_than_or_equals(op2)))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + ValueType::List(op2) => { + Ok(Rc::new(Value::new_bool(op1.greater_than_or_equals(op2)))) + } + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } @@ -452,17 +515,25 @@ impl NativeFunctionCall { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Ok(Rc::new(Value::new_bool(*op1 <= op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::Float(op1) => match params[1].value { ValueType::Float(op2) => Ok(Rc::new(Value::new_bool(*op1 <= op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::List(op1) => match ¶ms[1].value { ValueType::List(op2) => Ok(Rc::new(Value::new_bool(op1.less_than_or_equals(op2)))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } @@ -470,17 +541,25 @@ impl NativeFunctionCall { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Ok(Rc::new(Value::new_int(*op1 - op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::Float(op1) => match params[1].value { ValueType::Float(op2) => Ok(Rc::new(Value::new_float(*op1 - op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::List(op1) => match ¶ms[1].value { ValueType::List(op2) => Ok(Rc::new(Value::new_list(op1.without(op2)))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } @@ -488,11 +567,15 @@ impl NativeFunctionCall { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Ok(Rc::new(Value::new_int(op1 + op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::Float(op1) => match params[1].value { ValueType::Float(op2) => Ok(Rc::new(Value::new_float(op1 + op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::String(op1) => match ¶ms[1].value { ValueType::String(op2) => { @@ -500,14 +583,20 @@ impl NativeFunctionCall { sb.push_str(&op1.string); sb.push_str(&op2.string); Ok(Rc::new(Value::new_string(&sb))) - }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + } + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::List(op1) => match ¶ms[1].value { ValueType::List(op2) => Ok(Rc::new(Value::new_list(op1.union(op2)))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } @@ -515,13 +604,19 @@ impl NativeFunctionCall { match params[0].value { ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Ok(Rc::new(Value::new_int(op1 / op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::Float(op1) => match params[1].value { ValueType::Float(op2) => Ok(Rc::new(Value::new_float(op1 / op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } @@ -529,13 +624,19 @@ impl NativeFunctionCall { match params[0].value { ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Ok(Rc::new(Value::new_float((op1 as f32).powf(op2 as f32)))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::Float(op1) => match params[1].value { ValueType::Float(op2) => Ok(Rc::new(Value::new_float(op1.powf(op2)))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } @@ -543,13 +644,19 @@ impl NativeFunctionCall { match params[0].value { ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Ok(Rc::new(Value::new_int(op1 * op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::Float(op1) => match params[1].value { ValueType::Float(op2) => Ok(Rc::new(Value::new_float(op1 * op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } @@ -557,21 +664,33 @@ impl NativeFunctionCall { match ¶ms[0].value { ValueType::Bool(op1) => match params[1].value { ValueType::Bool(op2) => Ok(Rc::new(Value::new_bool(*op1 || op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Ok(Rc::new(Value::new_bool(*op1 != 0 || op2 != 0))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::Float(op1) => match params[1].value { ValueType::Float(op2) => Ok(Rc::new(Value::new_bool(*op1 != 0.0 || op2 != 0.0))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Ok(Rc::new(Value::new_bool(!op1.items.is_empty() || !op2.items.is_empty()))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) - }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + ValueType::List(op2) => Ok(Rc::new(Value::new_bool( + !op1.items.is_empty() || !op2.items.is_empty(), + ))), + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), + }, + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } @@ -579,11 +698,13 @@ impl NativeFunctionCall { match ¶ms[0].value { ValueType::Int(op1) => Ok(Rc::new(Value::new_bool(*op1 == 0))), ValueType::Float(op1) => Ok(Rc::new(Value::new_bool(*op1 == 0.0))), - ValueType::List(op1) => Ok(Rc::new(Value::new_int(match op1.items.is_empty() { + ValueType::List(op1) => Ok(Rc::new(Value::new_int(match op1.items.is_empty() { true => 1, false => 0, - } ))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + }))), + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } @@ -591,13 +712,19 @@ impl NativeFunctionCall { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Ok(Rc::new(Value::new_int(i32::min(*op1, op2)))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::Float(op1) => match params[1].value { ValueType::Float(op2) => Ok(Rc::new(Value::new_float(f32::min(*op1, op2)))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } @@ -605,13 +732,19 @@ impl NativeFunctionCall { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Ok(Rc::new(Value::new_int(i32::max(*op1, op2)))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::Float(op1) => match params[1].value { ValueType::Float(op2) => Ok(Rc::new(Value::new_float(f32::max(*op1, op2)))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } @@ -619,29 +752,43 @@ impl NativeFunctionCall { match ¶ms[0].value { ValueType::Bool(op1) => match params[1].value { ValueType::Bool(op2) => Ok(Rc::new(Value::new_bool(*op1 == op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Ok(Rc::new(Value::new_bool(*op1 == op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::Float(op1) => match params[1].value { ValueType::Float(op2) => Ok(Rc::new(Value::new_bool(*op1 == op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::String(op1) => match ¶ms[1].value { ValueType::String(op2) => Ok(Rc::new(Value::new_bool(op1.string.eq(&op2.string)))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::List(op1) => match ¶ms[1].value { ValueType::List(op2) => Ok(Rc::new(Value::new_bool(op1.eq(op2)))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::DivertTarget(op1) => match ¶ms[1].value { ValueType::DivertTarget(op2) => Ok(Rc::new(Value::new_bool(op1.eq(op2)))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } @@ -649,29 +796,43 @@ impl NativeFunctionCall { match ¶ms[0].value { ValueType::Bool(op1) => match params[1].value { ValueType::Bool(op2) => Ok(Rc::new(Value::new_bool(*op1 != op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Ok(Rc::new(Value::new_bool(*op1 != op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::Float(op1) => match params[1].value { ValueType::Float(op2) => Ok(Rc::new(Value::new_bool(*op1 != op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::String(op1) => match ¶ms[1].value { ValueType::String(op2) => Ok(Rc::new(Value::new_bool(!op1.string.eq(&op2.string)))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::List(op1) => match ¶ms[1].value { ValueType::List(op2) => Ok(Rc::new(Value::new_bool(!op1.eq(op2)))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::DivertTarget(op1) => match ¶ms[1].value { ValueType::DivertTarget(op2) => Ok(Rc::new(Value::new_bool(!op1.eq(op2)))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } @@ -679,13 +840,19 @@ impl NativeFunctionCall { match params[0].value { ValueType::Int(op1) => match params[1].value { ValueType::Int(op2) => Ok(Rc::new(Value::new_int(op1 % op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::Float(op1) => match params[1].value { ValueType::Float(op2) => Ok(Rc::new(Value::new_float(op1 % op2))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } @@ -693,154 +860,164 @@ impl NativeFunctionCall { match ¶ms[0].value { ValueType::List(op1) => match ¶ms[1].value { ValueType::List(op2) => Ok(Rc::new(Value::new_list(op1.intersect(op2)))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } fn has(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::String(op1) => match ¶ms[1].value { - ValueType::String(op2) => Ok(Rc::new(Value::new_bool(op1.string.contains(&op2.string)))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + ValueType::String(op2) => { + Ok(Rc::new(Value::new_bool(op1.string.contains(&op2.string)))) + } + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::List(op1) => match ¶ms[1].value { ValueType::List(op2) => Ok(Rc::new(Value::new_bool(op1.contains(op2)))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } fn hasnt(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::String(op1) => match ¶ms[1].value { - ValueType::String(op2) => Ok(Rc::new(Value::new_bool(!op1.string.contains(&op2.string)))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + ValueType::String(op2) => { + Ok(Rc::new(Value::new_bool(!op1.string.contains(&op2.string)))) + } + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, ValueType::List(op1) => match ¶ms[1].value { ValueType::List(op2) => Ok(Rc::new(Value::new_bool(!op1.contains(op2)))), - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } fn value_of_list_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { - ValueType::List(op1) => { - match op1.get_max_item() { - Some(i) => Ok(Rc::new(Value::new_int(i.1))), - None => Ok(Rc::new(Value::new_int(0))), - } + ValueType::List(op1) => match op1.get_max_item() { + Some(i) => Ok(Rc::new(Value::new_int(i.1))), + None => Ok(Rc::new(Value::new_int(0))), }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } fn all_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { - ValueType::List(op1) => { - Ok(Rc::new(Value::new_list(op1.get_all()))) - }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + ValueType::List(op1) => Ok(Rc::new(Value::new_list(op1.get_all()))), + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } fn inverse_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { - ValueType::List(op1) => { - Ok(Rc::new(Value::new_list(op1.inverse()))) - }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + ValueType::List(op1) => Ok(Rc::new(Value::new_list(op1.inverse()))), + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } fn count_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { - ValueType::List(op1) => { - Ok(Rc::new(Value::new_int(op1.items.len() as i32))) - }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + ValueType::List(op1) => Ok(Rc::new(Value::new_int(op1.items.len() as i32))), + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } fn list_max_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { - ValueType::List(op1) => { - Ok(Rc::new(Value::new_list(op1.max_as_list()))) - }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + ValueType::List(op1) => Ok(Rc::new(Value::new_list(op1.max_as_list()))), + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } fn list_min_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { - ValueType::List(op1) => { - Ok(Rc::new(Value::new_list(op1.min_as_list()))) - }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + ValueType::List(op1) => Ok(Rc::new(Value::new_list(op1.min_as_list()))), + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } fn negate_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { - ValueType::Int(op1) => { - Ok(Rc::new(Value::new_int(-op1))) - }, - ValueType::Float(op1) => { - Ok(Rc::new(Value::new_float(-op1))) - }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + ValueType::Int(op1) => Ok(Rc::new(Value::new_int(-op1))), + ValueType::Float(op1) => Ok(Rc::new(Value::new_float(-op1))), + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } fn floor_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { - ValueType::Int(op1) => { - Ok(Rc::new(Value::new_int(*op1))) - }, - ValueType::Float(op1) => { - Ok(Rc::new(Value::new_float(op1.floor()))) - }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + ValueType::Int(op1) => Ok(Rc::new(Value::new_int(*op1))), + ValueType::Float(op1) => Ok(Rc::new(Value::new_float(op1.floor()))), + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } fn ceiling_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { - ValueType::Int(op1) => { - Ok(Rc::new(Value::new_int(*op1))) - }, - ValueType::Float(op1) => { - Ok(Rc::new(Value::new_float(op1.ceil()))) - }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + ValueType::Int(op1) => Ok(Rc::new(Value::new_int(*op1))), + ValueType::Float(op1) => Ok(Rc::new(Value::new_float(op1.ceil()))), + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } fn int_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { - ValueType::Int(op1) => { - Ok(Rc::new(Value::new_int(*op1))) - }, - ValueType::Float(op1) => { - Ok(Rc::new(Value::new_int(*op1 as i32))) - }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + ValueType::Int(op1) => Ok(Rc::new(Value::new_int(*op1))), + ValueType::Float(op1) => Ok(Rc::new(Value::new_int(*op1 as i32))), + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } fn float_op(&self, params: &[Rc]) -> Result, StoryError> { match ¶ms[0].value { - ValueType::Int(op1) => { - Ok(Rc::new(Value::new_float(*op1 as f32))) - }, - ValueType::Float(op1) => { - Ok(Rc::new(Value::new_float(*op1))) - }, - _ => Err(StoryError::InvalidStoryState("Operation not available for type.".to_owned())) + ValueType::Int(op1) => Ok(Rc::new(Value::new_float(*op1 as f32))), + ValueType::Float(op1) => Ok(Rc::new(Value::new_float(*op1))), + _ => Err(StoryError::InvalidStoryState( + "Operation not available for type.".to_owned(), + )), } } } @@ -855,4 +1032,4 @@ impl fmt::Display for NativeFunctionCall { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Native '{:?}'", self.op) } -} \ No newline at end of file +} diff --git a/lib/src/object.rs b/lib/src/object.rs index c5d15bb..c17fa86 100644 --- a/lib/src/object.rs +++ b/lib/src/object.rs @@ -1,11 +1,16 @@ -use std::{fmt::Display, rc::{Weak, Rc}, cell::RefCell, any::Any}; +use std::{ + any::Any, + cell::RefCell, + fmt::Display, + rc::{Rc, Weak}, +}; use as_any::{AsAny, Downcast}; use crate::{ container::Container, path::{Component, Path}, - search_result::SearchResult + search_result::SearchResult, }; pub struct Object { @@ -42,7 +47,7 @@ impl Object { match rtobject.get_object().get_parent() { Some(_) => { let mut comps: Vec = Vec::new(); - + let mut container = rtobject.get_object().get_parent(); let mut child = rtobject.clone(); let mut child_rc; @@ -65,7 +70,8 @@ impl Object { let a = r.as_ref() as *const _ as *const (); let b = child as *const _ as *const (); std::ptr::eq(a, b) - }).unwrap(), + }) + .unwrap(), )); } @@ -77,28 +83,39 @@ impl Object { // Reverse list because components are searched in reverse order. comps.reverse(); - rtobject.get_object().path.replace(Some(Path::new(&comps, Path::default().is_relative()))); - }, + rtobject + .get_object() + .path + .replace(Some(Path::new(&comps, Path::default().is_relative()))); + } None => { - rtobject.get_object().path.replace(Some(Path::new_with_defaults())); - }, + rtobject + .get_object() + .path + .replace(Some(Path::new_with_defaults())); + } } - rtobject.get_object().path.borrow().as_ref().unwrap().clone() + rtobject + .get_object() + .path + .borrow() + .as_ref() + .unwrap() + .clone() } pub fn resolve_path(rtobject: Rc, path: &Path) -> SearchResult { if path.is_relative() { let mut p = path.clone(); let mut nearest_container = rtobject.clone().into_any().downcast::().ok(); - + if nearest_container.is_none() { nearest_container = rtobject.get_object().get_parent(); p = path.get_tail(); }; nearest_container.unwrap().content_at_path(&p, 0, -1) - } else { Object::get_root_container(rtobject).content_at_path(path, 0, -1) } @@ -110,7 +127,7 @@ impl Object { // 3. Re-build downward chain from common ancestor let own_path = rtobject.get_object().path.borrow(); let min_path_length = std::cmp::min(global_path.len(), own_path.as_ref().unwrap().len()); - let mut last_shared_path_comp_index:i32 = -1; + let mut last_shared_path_comp_index: i32 = -1; for i in 0..min_path_length { let own_comp = &own_path.as_ref().unwrap().get_component(i); @@ -128,7 +145,8 @@ impl Object { return global_path.clone(); } - let num_upwards_moves = (own_path.as_ref().unwrap().len() - 1) - last_shared_path_comp_index as usize; + let num_upwards_moves = + (own_path.as_ref().unwrap().len() - 1) - last_shared_path_comp_index as usize; let mut new_path_comps = Vec::new(); for _ in 0..num_upwards_moves { @@ -145,16 +163,18 @@ impl Object { pub fn compact_path_string(rtobject: Rc, other_path: &Path) -> String { let global_path_str: String; let relative_path_str: String; - + if other_path.is_relative() { relative_path_str = other_path.get_components_string(); - global_path_str = Object::get_path(rtobject.as_ref()).path_by_appending_path(other_path).get_components_string(); + global_path_str = Object::get_path(rtobject.as_ref()) + .path_by_appending_path(other_path) + .get_components_string(); } else { let relative_path = Object::convert_path_to_relative(&rtobject, other_path); relative_path_str = relative_path.get_components_string(); global_path_str = other_path.get_components_string(); } - + if relative_path_str.len() < global_path_str.len() { relative_path_str } else { @@ -166,12 +186,12 @@ impl Object { let mut ancestor = rtobject; while let Some(p) = ancestor.get_object().get_parent() { - ancestor = p; + ancestor = p; } match ancestor.into_any().downcast::() { Ok(c) => c.clone(), - _ => panic!() // Not possible + _ => panic!(), // Not possible } } } @@ -208,16 +228,33 @@ mod tests { let container1 = Container::new(None, 0, Vec::new(), HashMap::new()); let container21 = Container::new(None, 0, Vec::new(), HashMap::new()); let container2 = Container::new(None, 0, vec![container21.clone()], HashMap::new()); - let root = Container::new(None, 0, vec![container1.clone(), container2.clone()], HashMap::new()); + let root = Container::new( + None, + 0, + vec![container1.clone(), container2.clone()], + HashMap::new(), + ); let mut sb = String::new(); - root.build_string_of_hierarchy(&mut sb, 0, None); + root.build_string_of_hierarchy(&mut sb, 0, None); println!("root c:{:p}", &*root); - println!("container1 p:{:p} c:{:p}", &*(container1.get_object().get_parent().unwrap()), &*container1); - println!("container2 p:{:p} c:{:p}", &*(container2.get_object().get_parent().unwrap()), &*container2); - println!("container21 p:{:p} c:{:p}", &*(container21.get_object().get_parent().unwrap()), &*container21); + println!( + "container1 p:{:p} c:{:p}", + &*(container1.get_object().get_parent().unwrap()), + &*container1 + ); + println!( + "container2 p:{:p} c:{:p}", + &*(container2.get_object().get_parent().unwrap()), + &*container2 + ); + println!( + "container21 p:{:p} c:{:p}", + &*(container21.get_object().get_parent().unwrap()), + &*container21 + ); println!("root: {}", sb); diff --git a/lib/src/path.rs b/lib/src/path.rs index 0854f41..d2bf360 100644 --- a/lib/src/path.rs +++ b/lib/src/path.rs @@ -1,11 +1,11 @@ use std::{ + cell::OnceCell, fmt, - hash::{Hash, Hasher}, cell::OnceCell, + hash::{Hash, Hasher}, }; const PARENT_ID: &str = "^"; - /// The componentsString field from the C# impl. has been removed and it is always generated dinamically from the components field. #[derive(Eq, Clone, Default)] pub struct Path { @@ -33,7 +33,7 @@ impl Path { pub fn new_with_components_string(components_string: Option<&str>) -> Path { let cs = components_string; - let is_relative:bool; + let is_relative: bool; // Empty path, empty components // (path is to root, like "/" in file system) @@ -75,7 +75,7 @@ impl Path { Path { components, is_relative, - components_string: cs_cell + components_string: cs_cell, } } @@ -144,31 +144,34 @@ impl Path { } pub fn get_components_string(&self) -> String { - return self.components_string.get_or_init( || { - let mut sb = String::new(); - - if !self.components.is_empty() { - sb.push_str(&self.components.get(0).unwrap().to_string()); - - for i in 1..self.components.len() { - sb.push('.'); - sb.push_str(&self.components.get(i).unwrap().to_string()); + return self + .components_string + .get_or_init(|| { + let mut sb = String::new(); + + if !self.components.is_empty() { + sb.push_str(&self.components.get(0).unwrap().to_string()); + + for i in 1..self.components.len() { + sb.push('.'); + sb.push_str(&self.components.get(i).unwrap().to_string()); + } } - } - if self.is_relative { - return ".".to_owned() + &sb; - } + if self.is_relative { + return ".".to_owned() + &sb; + } - sb - }).to_string(); + sb + }) + .to_string(); } - pub fn path_by_appending_component( &self, c: Component) -> Path { + pub fn path_by_appending_component(&self, c: Component) -> Path { let mut p = Path::new(self.components.as_ref(), false); p.components.push(c); - p + p } } diff --git a/lib/src/pointer.rs b/lib/src/pointer.rs index da1663d..6d40a25 100644 --- a/lib/src/pointer.rs +++ b/lib/src/pointer.rs @@ -1,10 +1,13 @@ -use std::{rc::Rc, fmt}; +use std::{fmt, rc::Rc}; -use crate::{container::Container, object::RTObject, path::{Path, Component}}; +use crate::{ + container::Container, + object::RTObject, + path::{Component, Path}, +}; pub const NULL: Pointer = Pointer::new(None, -1); - #[derive(Clone, Default)] pub struct Pointer { pub container: Option>, @@ -46,22 +49,29 @@ impl Pointer { if self.index >= 0 { let c = Component::new_i(self.index as usize); - return Some(container.get_path() - .path_by_appending_component(c)); + return Some(container.get_path().path_by_appending_component(c)); } Some(container.get_path()) } - pub fn start_of(container:Rc) -> Pointer { - Pointer{container: Some(container), index:0} + pub fn start_of(container: Rc) -> Pointer { + Pointer { + container: Some(container), + index: 0, + } } } impl fmt::Display for Pointer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.container { - Some(container) => write!(f, "Ink Pointer -> {} -- index {}", container.get_path(), self.index), + Some(container) => write!( + f, + "Ink Pointer -> {} -- index {}", + container.get_path(), + self.index + ), None => write!(f, "Ink Pointer (null)"), } } diff --git a/lib/src/push_pop.rs b/lib/src/push_pop.rs index 1a6ba71..00c70e0 100644 --- a/lib/src/push_pop.rs +++ b/lib/src/push_pop.rs @@ -4,7 +4,7 @@ use crate::story_error::StoryError; pub enum PushPopType { Tunnel, Function, - FunctionEvaluationFromGame + FunctionEvaluationFromGame, } impl PushPopType { @@ -13,7 +13,9 @@ impl PushPopType { 0 => Ok(PushPopType::Tunnel), 1 => Ok(PushPopType::Function), 2 => Ok(PushPopType::FunctionEvaluationFromGame), - _ => Err(StoryError::BadJson("Unexpected PushPopType value".to_owned())) + _ => Err(StoryError::BadJson( + "Unexpected PushPopType value".to_owned(), + )), } } -} \ No newline at end of file +} diff --git a/lib/src/search_result.rs b/lib/src/search_result.rs index dc5be91..9887e7e 100644 --- a/lib/src/search_result.rs +++ b/lib/src/search_result.rs @@ -1,7 +1,6 @@ use std::rc::Rc; -use crate::{object::RTObject, container::Container}; - +use crate::{container::Container, object::RTObject}; #[derive(Clone)] pub struct SearchResult { @@ -11,10 +10,7 @@ pub struct SearchResult { impl SearchResult { pub fn new(obj: Rc, approximate: bool) -> Self { - SearchResult { - obj, - approximate, - } + SearchResult { obj, approximate } } pub fn from_search_result(sr: &SearchResult) -> Self { @@ -40,4 +36,4 @@ impl SearchResult { Err(_) => None, } } -} \ No newline at end of file +} diff --git a/lib/src/state_patch.rs b/lib/src/state_patch.rs index fa41e4f..36f5671 100644 --- a/lib/src/state_patch.rs +++ b/lib/src/state_patch.rs @@ -1,8 +1,9 @@ use std::{ - rc::Rc, collections::{HashMap, HashSet}, + collections::{HashMap, HashSet}, + rc::Rc, }; -use crate::{object::Object, container::Container, value::Value}; +use crate::{container::Container, object::Object, value::Value}; #[derive(Clone)] pub struct StatePatch { @@ -24,8 +25,8 @@ impl StatePatch { None => StatePatch { globals: HashMap::new(), changed_variables: HashSet::new(), - visit_counts: HashMap::new(), - turn_indices: HashMap::new(), + visit_counts: HashMap::new(), + turn_indices: HashMap::new(), }, } } @@ -40,7 +41,7 @@ impl StatePatch { self.visit_counts.insert(key, count); } - pub fn get_global(&self, name: &str) -> Option>{ + pub fn get_global(&self, name: &str) -> Option> { self.globals.get(name).cloned() } @@ -61,4 +62,4 @@ impl StatePatch { let key = Object::get_path(container).to_string(); return self.turn_indices.get(&key); } -} \ No newline at end of file +} diff --git a/lib/src/story.rs b/lib/src/story.rs index a32e277..f02071b 100644 --- a/lib/src/story.rs +++ b/lib/src/story.rs @@ -1,14 +1,39 @@ #![allow(unused_variables, dead_code)] -use std::{rc::Rc, time::Instant, collections::{VecDeque, HashMap}, cell::RefCell}; +use std::{ + cell::RefCell, + collections::{HashMap, VecDeque}, + rc::Rc, + time::Instant, +}; -use rand::{Rng, rngs::StdRng, SeedableRng}; +use rand::{rngs::StdRng, Rng, SeedableRng}; use crate::{ + choice::Choice, + choice_point::ChoicePoint, container::Container, + control_command::{CommandType, ControlCommand}, + divert::Divert, + ink_list::InkList, + ink_list_item::InkListItem, json_read, + list_definitions_origin::ListDefinitionsOrigin, + native_function_call::NativeFunctionCall, + object::{Object, RTObject}, + path::Path, + pointer::{self, Pointer}, push_pop::PushPopType, - story_state::StoryState, pointer::{Pointer, self}, object::{RTObject, Object}, void::Void, path::Path, control_command::{ControlCommand, CommandType}, choice::Choice, value::Value, tag::Tag, divert::Divert, choice_point::ChoicePoint, search_result::SearchResult, variable_assigment::VariableAssignment, native_function_call::NativeFunctionCall, variable_reference::VariableReference, list_definitions_origin::ListDefinitionsOrigin, ink_list::InkList, ink_list_item::InkListItem, story_error::StoryError, value_type::ValueType, story_callbacks::{VariableObserver, ExternalFunctionDef, ErrorType, ErrorHandler}, + search_result::SearchResult, + story_callbacks::{ErrorHandler, ErrorType, ExternalFunctionDef, VariableObserver}, + story_error::StoryError, + story_state::StoryState, + tag::Tag, + value::Value, + value_type::ValueType, + variable_assigment::VariableAssignment, + variable_reference::VariableReference, + void::Void, }; pub const INK_VERSION_CURRENT: i32 = 21; @@ -18,7 +43,7 @@ const INK_VERSION_MINIMUM_COMPATIBLE: i32 = 18; enum OutputStateChange { NoChange, ExtendedBeyondNewline, - NewlineRemoved + NewlineRemoved, } pub struct Story { @@ -51,8 +76,8 @@ impl Story { if version_opt.is_none() || !version_opt.unwrap().is_number() { return Err(StoryError::BadJson( "ink version number not found. Are you sure it's a valid .ink.json file?" - .to_owned()), - ); + .to_owned(), + )); } let version: i32 = version_opt.unwrap().as_i64().unwrap().try_into().unwrap(); @@ -68,8 +93,8 @@ impl Story { None => { return Err(StoryError::BadJson( "Root node for ink not found. Are you sure it's a valid .ink.json file?" - .to_owned()), - ) + .to_owned(), + )) } }; @@ -88,7 +113,9 @@ impl Story { let main_content_container = main_content_container.into_any().downcast::(); if main_content_container.is_err() { - return Err(StoryError::BadJson("Root node for ink is not a container?".to_owned())); + return Err(StoryError::BadJson( + "Root node for ink is not a container?".to_owned(), + )); }; let main_content_container = main_content_container.unwrap(); // unwrap: checked for err above @@ -130,11 +157,18 @@ impl Story { &mut self.state } - fn reset_globals(&mut self) -> Result<(), StoryError> { - if self.main_content_container.named_content.contains_key("global decl") { + fn reset_globals(&mut self) -> Result<(), StoryError> { + if self + .main_content_container + .named_content + .contains_key("global decl") + { let original_pointer = self.get_state().get_current_pointer().clone(); - self.choose_path(&Path::new_with_components_string(Some("global decl")), false)?; + self.choose_path( + &Path::new_with_components_string(Some("global decl")), + false, + )?; // Continue, but without validating external bindings, // since we may be doing this reset at initialisation time. @@ -143,7 +177,9 @@ impl Story { self.get_state().set_current_pointer(original_pointer); } - self.get_state_mut().variables_state.snapshot_default_globals(); + self.get_state_mut() + .variables_state + .snapshot_default_globals(); Ok(()) } @@ -201,9 +237,9 @@ impl Story { if !self.async_continue_active { self.async_continue_active = is_async_time_limited; if !self.can_continue() { - return Err( - StoryError::InvalidStoryState("Can't continue - should check can_continue before calling Continue".to_owned()), - ); + return Err(StoryError::InvalidStoryState( + "Can't continue - should check can_continue before calling Continue".to_owned(), + )); } self.get_state_mut().set_did_safe_exit(false); @@ -266,19 +302,11 @@ impl Story { // Finished a section of content / reached a choice point? if !self.can_continue() { - if self - .state - .get_callstack() - .borrow() - .can_pop_thread() - { + if self.state.get_callstack().borrow().can_pop_thread() { self.add_error("Thread available to pop, threads should always be flat by the end of evaluation?", false); } - if self - .state - .get_generated_choices() - .is_empty() + if self.state.get_generated_choices().is_empty() && !self.get_state().is_did_safe_exit() && self.temporary_evaluation_container.is_none() { @@ -296,10 +324,14 @@ impl Story { .can_pop_type(Some(PushPopType::Function)) { self.add_error( - "unexpectedly reached end of content. Do you need a '~ return'?", false + "unexpectedly reached end of content. Do you need a '~ return'?", + false, ); } else if !self.get_state().get_callstack().borrow().can_pop() { - self.add_error("ran out of content. Do you need a '-> DONE' or '-> END'?", false); + self.add_error( + "ran out of content. Do you need a '-> DONE' or '-> END'?", + false, + ); } else { self.add_error("unexpectedly reached end of content for unknown reason. Please debug compiler!", false); } @@ -309,7 +341,8 @@ impl Story { self.saw_lookahead_unsafe_function_after_new_line = false; if self.recursive_continue_count == 1 { - let changed = self.state + let changed = self + .state .variables_state .stop_batch_observing_variable_changes(); @@ -363,7 +396,13 @@ impl Story { } if self.get_state().has_warning() { - sb.push_str(self.get_state().get_current_warnings().len().to_string().as_str()); + sb.push_str( + self.get_state() + .get_current_warnings() + .len() + .to_string() + .as_str(), + ); if self.get_state().get_current_errors().len() == 1 { sb.push_str(" warning"); } else { @@ -376,7 +415,11 @@ impl Story { if self.get_state().has_error() { sb.push_str(self.get_state().get_current_errors()[0].as_str()); } else { - sb.push_str(self.get_state().get_current_warnings()[0].to_string().as_str()); + sb.push_str( + self.get_state().get_current_warnings()[0] + .to_string() + .as_str(), + ); } return Err(StoryError::InvalidStoryState(sb)); @@ -392,34 +435,42 @@ impl Story { self.step()?; // Run out of content and we have a default invisible choice that we can follow? - if !self.can_continue() && !self.get_state().get_callstack().borrow().element_is_evaluate_from_game() { + if !self.can_continue() + && !self + .get_state() + .get_callstack() + .borrow() + .element_is_evaluate_from_game() + { self.try_follow_default_invisible_choice()?; } // Don't save/rewind during string evaluation, which is e.g. used for choices if !self.get_state().in_string_evaluation() { - // We previously found a newline, but were we just double checking that // it wouldn't immediately be removed by glue? - if let Some(state_snapshot_at_last_new_line) = self.state_snapshot_at_last_new_line.as_mut() { - + if let Some(state_snapshot_at_last_new_line) = + self.state_snapshot_at_last_new_line.as_mut() + { // 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. let change = Story::calculate_newline_output_state_change( - &state_snapshot_at_last_new_line.get_current_text(), - &self.state.get_current_text(), - state_snapshot_at_last_new_line.get_current_tags().len() as i32, - self.state.get_current_tags().len() as i32); + &state_snapshot_at_last_new_line.get_current_text(), + &self.state.get_current_text(), + state_snapshot_at_last_new_line.get_current_tags().len() as i32, + self.state.get_current_tags().len() as i32, + ); // 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 || self.saw_lookahead_unsafe_function_after_new_line { + if change == OutputStateChange::ExtendedBeyondNewline + || self.saw_lookahead_unsafe_function_after_new_line + { self.restore_state_snapshot(); // Hit a newline for sure, we're done return Ok(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 { @@ -430,13 +481,11 @@ impl Story { // Current content ends in a newline - approaching end of our evaluation if self.get_state().output_stream_ends_in_newline() { - // 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 self.can_continue() { - // Don't bother to record the state beyond the current newline. // e.g.: // Hello world\n // record state at the end of here @@ -445,7 +494,6 @@ impl Story { self.state_snapshot(); } } - // Can't continue, so we're about to exit - make sure we // don't have an old state hanging around. else { @@ -475,7 +523,10 @@ impl Story { // 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. - self.state_snapshot_at_last_new_line.as_mut().unwrap().restore_after_patch(); //unwrap: state_snapshot_at_last_new_line checked Some in previous fn + self.state_snapshot_at_last_new_line + .as_mut() + .unwrap() + .restore_after_patch(); //unwrap: state_snapshot_at_last_new_line checked Some in previous fn self.state = self.state_snapshot_at_last_new_line.take().unwrap(); @@ -488,16 +539,18 @@ impl Story { } fn add_error(&mut self, message: &str, is_warning: bool) { - let error_type_str = if is_warning {"WARNING"} else {"ERROR"}; - - let m = - if !self.get_state().get_current_pointer().is_null() { - format!( - "RUNTIME {}: ({}): {}", - error_type_str, self.get_state().get_current_pointer().get_path().unwrap(), message) - } else { - format!("RUNTIME {}: {}", error_type_str, message) - }; + let error_type_str = if is_warning { "WARNING" } else { "ERROR" }; + + let m = if !self.get_state().get_current_pointer().is_null() { + format!( + "RUNTIME {}: ({}): {}", + error_type_str, + self.get_state().get_current_pointer().get_path().unwrap(), + message + ) + } else { + format!("RUNTIME {}: {}", error_type_str, message) + }; self.get_state_mut().add_error(m, is_warning); @@ -533,7 +586,6 @@ impl Story { }; while let Some(cte) = container_to_enter.as_ref() { - // Mark container as being entered self.visit_container(cte, true); @@ -584,11 +636,12 @@ impl Story { should_add_to_stream = false; } - if let Ok(choice_point) = cco.clone().into_any().downcast::() { - + if let Ok(choice_point) = cco.clone().into_any().downcast::() { let choice = self.process_choice(&choice_point)?; if let Some(choice) = choice { - self.get_state_mut().get_generated_choices_mut().push(choice); + self.get_state_mut() + .get_generated_choices_mut() + .push(choice); } current_content_obj = None; @@ -598,7 +651,6 @@ impl Story { // Content to add to evaluation stack or the output stream if should_add_to_stream { - // 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 @@ -607,23 +659,31 @@ impl Story { let var_pointer = Value::get_variable_pointer_value(current_content_obj.as_ref().unwrap().as_ref()); - if let Some(var_pointer) = var_pointer { + if let Some(var_pointer) = var_pointer { if var_pointer.context_index == -1 { - // Create new Object so we're not overwriting the story's own // data - let context_idx = self.get_state().get_callstack().borrow().context_for_variable_named(&var_pointer.variable_name); - current_content_obj = Some(Rc::new(Value::new_variable_pointer(&var_pointer.variable_name, context_idx as i32))); + let context_idx = self + .get_state() + .get_callstack() + .borrow() + .context_for_variable_named(&var_pointer.variable_name); + current_content_obj = Some(Rc::new(Value::new_variable_pointer( + &var_pointer.variable_name, + context_idx as i32, + ))); } } // Expression evaluation content if self.get_state().get_in_expression_evaluation() { - self.get_state_mut().push_evaluation_stack(current_content_obj.as_ref().unwrap().clone()); + self.get_state_mut() + .push_evaluation_stack(current_content_obj.as_ref().unwrap().clone()); } // Output stream content (i.e. not expression evaluation) else { - self.get_state_mut().push_to_output_stream(current_content_obj.as_ref().unwrap().clone()); + self.get_state_mut() + .push_to_output_stream(current_content_obj.as_ref().unwrap().clone()); } } @@ -635,7 +695,12 @@ impl Story { // so that when returning from the thread, it returns to the content // after this instruction. if current_content_obj.is_some() { - if let Some(control_cmd) = current_content_obj.as_ref().unwrap().as_any().downcast_ref::() { + if let Some(control_cmd) = current_content_obj + .as_ref() + .unwrap() + .as_any() + .downcast_ref::() + { if control_cmd.command_type == CommandType::StartThread { self.get_state().get_callstack().borrow_mut().push_thread(); } @@ -643,7 +708,6 @@ impl Story { } Ok(()) - } fn try_follow_default_invisible_choice(&mut self) -> Result<(), StoryError> { @@ -655,7 +719,7 @@ impl Story { // Is a default invisible choice the ONLY choice? // var invisibleChoices = allChoices.Where (c => // c.choicePoint.isInvisibleDefault).ToList(); - let mut invisible_choices:Vec> = Vec::new(); + let mut invisible_choices: Vec> = Vec::new(); for c in all_choices { if c.is_invisible_default { invisible_choices.push(c.clone()); @@ -670,14 +734,27 @@ impl Story { // Invisible choice may have been generated on a different thread, // in which case we need to restore it before we continue - self.get_state().get_callstack().as_ref().borrow_mut().set_current_thread(choice.get_thread_at_generation().unwrap().copy()); + self.get_state() + .get_callstack() + .as_ref() + .borrow_mut() + .set_current_thread(choice.get_thread_at_generation().unwrap().copy()); // 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 self.state_snapshot_at_last_new_line.is_some() { - let fork_thread = self.get_state().get_callstack().as_ref().borrow_mut().fork_thread(); - self.get_state().get_callstack().as_ref().borrow_mut().set_current_thread(fork_thread); + let fork_thread = self + .get_state() + .get_callstack() + .as_ref() + .borrow_mut() + .fork_thread(); + self.get_state() + .get_callstack() + .as_ref() + .borrow_mut() + .set_current_thread(fork_thread); } self.choose_path(&choice.target_path, false) @@ -700,24 +777,24 @@ impl Story { { return OutputStateChange::NoChange; } - + // Old newline has been removed, it wasn't the end of the line after all if !newline_still_exists { return OutputStateChange::NewlineRemoved; } - + // Tag added - definitely the start of a new line if curr_tag_count > prev_tag_count { return OutputStateChange::ExtendedBeyondNewline; } - + // There must be new content - check whether it's just whitespace for c in curr_text.chars().skip(prev_text.len()) { 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. @@ -737,32 +814,35 @@ impl Story { // However, if we're in the middle of async // saving, we simply stay in a "patching" state, // albeit with the newer cloned patch. - + if !self.async_saving { self.get_state_mut().apply_any_patch(); } // No longer need the snapshot. - self.state_snapshot_at_last_new_line = None; + self.state_snapshot_at_last_new_line = None; } fn visit_container(&mut self, container: &Rc, at_start: bool) { if !container.counting_at_start_only || at_start { if container.visits_should_be_counted { - self.get_state_mut().increment_visit_count_for_container(container); + self.get_state_mut() + .increment_visit_count_for_container(container); } if container.turn_index_should_be_counted { - self.get_state_mut().record_turn_index_visit_to_container(container); + self.get_state_mut() + .record_turn_index_visit_to_container(container); } } } - fn perform_logic_and_flow_control(&mut self, content_obj: &Option>) -> Result { + fn perform_logic_and_flow_control( + &mut self, + content_obj: &Option>, + ) -> Result { let content_obj = match content_obj { - Some(content_obj) => { - content_obj.clone() - }, + Some(content_obj) => content_obj.clone(), None => return Ok(false), }; @@ -777,7 +857,11 @@ impl Story { if current_divert.has_variable_target() { let var_name = ¤t_divert.variable_divert_name; - if let Some(var_contents) = self.get_state().variables_state.get_variable_with_name(var_name.as_ref().unwrap(), -1) { + if let Some(var_contents) = self + .get_state() + .variables_state + .get_variable_with_name(var_name.as_ref().unwrap(), -1) + { if let Some(target) = Value::get_divert_target_value(var_contents.as_ref()) { let p = Self::pointer_at_path(&self.main_content_container, target)?; self.get_state_mut().set_diverted_pointer(p); @@ -787,7 +871,8 @@ impl Story { var_name.as_ref().unwrap() ); - let error_message = if let ValueType::Int(int_content) = var_contents.value { + let error_message = if let ValueType::Int(int_content) = var_contents.value + { if int_content == 0 { format!("{}was empty/null (the value 0).", error_message) } else { @@ -797,22 +882,28 @@ impl Story { error_message }; - return Err(StoryError::InvalidStoryState(error_message)); + return Err(StoryError::InvalidStoryState(error_message)); } } else { return Err(StoryError::InvalidStoryState(format!("Tried to divert using a target from a variable that could not be found ({})", var_name.as_ref().unwrap()))); } } else if current_divert.is_external { - self.call_external_function(¤t_divert.get_target_path_string().unwrap(), current_divert.external_args)?; + self.call_external_function( + ¤t_divert.get_target_path_string().unwrap(), + current_divert.external_args, + )?; return Ok(true); } else { - self.get_state_mut().set_diverted_pointer(current_divert.get_target_pointer()); + self.get_state_mut() + .set_diverted_pointer(current_divert.get_target_pointer()); } if current_divert.pushes_to_stack { - self.get_state() - .get_callstack().borrow_mut() - .push(current_divert.stack_push_type, 0, self.get_state().get_output_stream().len() as i32); + self.get_state().get_callstack().borrow_mut().push( + current_divert.stack_push_type, + 0, + self.get_state().get_output_stream().len() as i32, + ); } if self.get_state().diverted_pointer.is_null() && !current_divert.is_external { @@ -822,20 +913,25 @@ impl Story { return Ok(true); } - if let Some(eval_command) = content_obj.as_ref().as_any().downcast_ref::() { + if let Some(eval_command) = content_obj + .as_ref() + .as_any() + .downcast_ref::() + { match eval_command.command_type { CommandType::EvalStart => { if self.get_state().get_in_expression_evaluation() { - return Err(StoryError::InvalidStoryState("Already in expression evaluation?".to_owned())); + return Err(StoryError::InvalidStoryState( + "Already in expression evaluation?".to_owned(), + )); } self.get_state().set_in_expression_evaluation(true); - }, + } CommandType::EvalOutput => { // If the expression turned out to be empty, there may not be // anything on the stack if !self.get_state().evaluation_stack.is_empty() { - let output = self.get_state_mut().pop_evaluation_stack(); // Functions may evaluate to Void, in which case we skip @@ -847,27 +943,30 @@ impl Story { // the // only problem is when exporting text for viewing, it // skips over numbers etc. - let text:Rc = Rc::new(Value::new_string(&output.to_string())); + let text: Rc = + Rc::new(Value::new_string(&output.to_string())); self.get_state_mut().push_to_output_stream(text); } } - }, + } CommandType::EvalEnd => { if !self.get_state().get_in_expression_evaluation() { - return Err(StoryError::InvalidStoryState("Not in expression evaluation mode".to_owned())); + return Err(StoryError::InvalidStoryState( + "Not in expression evaluation mode".to_owned(), + )); } self.get_state().set_in_expression_evaluation(false); - }, + } CommandType::Duplicate => { let obj = self.get_state().peek_evaluation_stack().unwrap().clone(); self.get_state_mut().push_evaluation_stack(obj); - }, + } CommandType::PopEvaluatedValue => { self.get_state_mut().pop_evaluation_stack(); - }, - CommandType::PopFunction | CommandType::PopTunnel=> { - let pop_type = if CommandType::PopFunction == eval_command.command_type { + } + CommandType::PopFunction | CommandType::PopTunnel => { + let pop_type = if CommandType::PopFunction == eval_command.command_type { PushPopType::Function } else { PushPopType::Tunnel @@ -883,48 +982,85 @@ impl Story { override_tunnel_return_target = Some(v.clone()); } - if override_tunnel_return_target.is_none() && !popped.as_ref().as_any().is::() { - return Err(StoryError::InvalidStoryState("Expected void if ->-> doesn't override target".to_owned())); + if override_tunnel_return_target.is_none() + && !popped.as_ref().as_any().is::() + { + return Err(StoryError::InvalidStoryState( + "Expected void if ->-> doesn't override target".to_owned(), + )); } } - if self.get_state_mut().try_exit_function_evaluation_from_game() { + if self + .get_state_mut() + .try_exit_function_evaluation_from_game() + { return Ok(true); - } else if self.get_state().get_callstack().borrow().get_current_element().push_pop_type != pop_type - || !self.get_state().get_callstack().borrow().can_pop() { - - let mut names: HashMap = HashMap::new(); - names.insert(PushPopType::Function, "function return statement (~ return)".to_owned()); - names.insert(PushPopType::Tunnel, "tunnel onwards statement (->->)".to_owned()); + } else if self + .get_state() + .get_callstack() + .borrow() + .get_current_element() + .push_pop_type + != pop_type + || !self.get_state().get_callstack().borrow().can_pop() + { + let mut names: HashMap = HashMap::new(); + names.insert( + PushPopType::Function, + "function return statement (~ return)".to_owned(), + ); + names.insert( + PushPopType::Tunnel, + "tunnel onwards statement (->->)".to_owned(), + ); - let mut expected = names.get(&self.get_state().get_callstack().borrow().get_current_element().push_pop_type).cloned(); + let mut expected = names + .get( + &self + .get_state() + .get_callstack() + .borrow() + .get_current_element() + .push_pop_type, + ) + .cloned(); if !self.get_state().get_callstack().borrow().can_pop() { expected = Some("end of flow (-> END or choice)".to_owned()); } - return Err(StoryError::InvalidStoryState(format!("Found {}, when expected {}", names.get(&pop_type).unwrap(), expected.unwrap()))); + return Err(StoryError::InvalidStoryState(format!( + "Found {}, when expected {}", + names.get(&pop_type).unwrap(), + expected.unwrap() + ))); } else { self.get_state_mut().pop_callstack(None)?; // Does tunnel onwards override by diverting to a new ->-> // target? if let Some(override_tunnel_return_target) = override_tunnel_return_target { - let p = Self::pointer_at_path(&self.main_content_container, &override_tunnel_return_target)?; + let p = Self::pointer_at_path( + &self.main_content_container, + &override_tunnel_return_target, + )?; self.get_state_mut().set_diverted_pointer(p); } - } - }, + } + } CommandType::BeginString => { - self.get_state_mut().push_to_output_stream(content_obj.clone()); + self.get_state_mut() + .push_to_output_stream(content_obj.clone()); if !self.get_state().get_in_expression_evaluation() { - return Err(StoryError::InvalidStoryState("Expected to be in an expression when evaluating a string".to_owned())); + return Err(StoryError::InvalidStoryState( + "Expected to be in an expression when evaluating a string".to_owned(), + )); } - + self.get_state().set_in_expression_evaluation(false); - }, + } CommandType::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 @@ -937,7 +1073,9 @@ impl Story { let obj = &self.get_state().get_output_stream()[i]; output_count_consumed += 1; - if let Some(command) = obj.as_ref().as_any().downcast_ref::() { + if let Some(command) = + obj.as_ref().as_any().downcast_ref::() + { if command.command_type == CommandType::BeginString { break; } @@ -953,14 +1091,16 @@ impl Story { } // Consume the content that was produced for this string - self.get_state_mut().pop_from_output_stream(output_count_consumed); + self.get_state_mut() + .pop_from_output_stream(output_count_consumed); // 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 rescued_tag in content_to_retain.iter() { - self.get_state_mut().push_to_output_stream(rescued_tag.clone()); + self.get_state_mut() + .push_to_output_stream(rescued_tag.clone()); } // Build string out of the content we collected @@ -972,18 +1112,21 @@ impl Story { // Return to expression evaluation (from content mode) self.get_state().set_in_expression_evaluation(true); - self.get_state_mut().push_evaluation_stack(Rc::new(Value::new_string(&sb))); - }, - CommandType::NoOp => {}, + self.get_state_mut() + .push_evaluation_stack(Rc::new(Value::new_string(&sb))); + } + CommandType::NoOp => {} CommandType::ChoiceCount => { let choice_count = self.get_state().get_generated_choices().len(); - self.get_state_mut().push_evaluation_stack(Rc::new(Value::new_int(choice_count as i32))); - }, + self.get_state_mut() + .push_evaluation_stack(Rc::new(Value::new_int(choice_count as i32))); + } CommandType::Turns => { let current_turn = self.get_state().current_turn_index; - self.get_state_mut().push_evaluation_stack(Rc::new(Value::new_int( current_turn + 1))); - }, - CommandType::TurnsSince | CommandType::ReadCount => { + self.get_state_mut() + .push_evaluation_stack(Rc::new(Value::new_int(current_turn + 1))); + } + CommandType::TurnsSince | CommandType::ReadCount => { let target = self.get_state_mut().pop_evaluation_stack(); if Value::get_divert_target_value(target.as_ref()).is_none() { let mut extra_note = "".to_owned(); @@ -1004,35 +1147,48 @@ impl Story { None => None, }; - let either_count:i32; + let either_count: i32; match container { Some(container) => { if eval_command.command_type == CommandType::TurnsSince { - either_count = self.get_state().turns_since_for_container(container.as_ref())?; - } else {either_count = self.get_state_mut().visit_count_for_container(&container);} - }, + either_count = self + .get_state() + .turns_since_for_container(container.as_ref())?; + } else { + either_count = + self.get_state_mut().visit_count_for_container(&container); + } + } None => { if eval_command.command_type == CommandType::TurnsSince { either_count = -1; // turn count, default to never/unknown - } else { either_count = 0; } // visit count, assume 0 to default to allowing entry - - self.add_error(&format!("Failed to find container for {} lookup at {}", eval_command - , target), true); + } else { + either_count = 0; + } // visit count, assume 0 to default to allowing entry + + self.add_error( + &format!( + "Failed to find container for {} lookup at {}", + eval_command, target + ), + true, + ); } } - self.get_state_mut().push_evaluation_stack(Rc::new(Value::new_int(either_count))); - }, + self.get_state_mut() + .push_evaluation_stack(Rc::new(Value::new_int(either_count))); + } CommandType::Random => { let mut max_int = None; - let o = self.get_state_mut().pop_evaluation_stack(); + let o = self.get_state_mut().pop_evaluation_stack(); if let Some(v) = Value::get_int_value(o.as_ref()) { max_int = Some(v); } - let o = self.get_state_mut().pop_evaluation_stack(); + let o = self.get_state_mut().pop_evaluation_stack(); let mut min_int = None; if let Some(v) = Value::get_int_value(o.as_ref()) { @@ -1040,11 +1196,17 @@ impl Story { } if min_int.is_none() { - return Err(StoryError::InvalidStoryState("Invalid value for the minimum parameter of RANDOM(min, max)".to_owned())); + return Err(StoryError::InvalidStoryState( + "Invalid value for the minimum parameter of RANDOM(min, max)" + .to_owned(), + )); } if max_int.is_none() { - return Err(StoryError::InvalidStoryState("Invalid value for the maximum parameter of RANDOM(min, max)".to_owned())); + return Err(StoryError::InvalidStoryState( + "Invalid value for the maximum parameter of RANDOM(min, max)" + .to_owned(), + )); } let min_value = min_int.unwrap(); @@ -1059,26 +1221,32 @@ impl Story { ))); } - let result_seed = self.get_state().story_seed + self.get_state().previous_random; + let result_seed = + self.get_state().story_seed + self.get_state().previous_random; let mut rng = StdRng::seed_from_u64(result_seed as u64); let next_random = rng.gen::(); let chosen_value = (next_random % random_range as u32) as i32 + min_value; - - self.get_state_mut().push_evaluation_stack(Rc::new(Value::new_int(chosen_value))); + + self.get_state_mut() + .push_evaluation_stack(Rc::new(Value::new_int(chosen_value))); self.get_state_mut().previous_random = self.get_state().previous_random + 1; - }, + } CommandType::SeedRandom => { let mut seed: Option = None; let o = self.get_state_mut().pop_evaluation_stack(); - if let Some(v) = Value::get_int_value(o.as_ref()) {seed = Some(v);} + if let Some(v) = Value::get_int_value(o.as_ref()) { + seed = Some(v); + } if seed.is_none() { - return Err(StoryError::InvalidStoryState("Invalid value passed to SEED_RANDOM".to_owned())); + return Err(StoryError::InvalidStoryState( + "Invalid value passed to SEED_RANDOM".to_owned(), + )); } // Story seed affects both RANDOM and shuffle behaviour @@ -1086,77 +1254,94 @@ impl Story { self.get_state_mut().previous_random = 0; // SEED_RANDOM returns nothing. - self.get_state_mut().push_evaluation_stack(Rc::new(Void::new())); - }, + self.get_state_mut() + .push_evaluation_stack(Rc::new(Void::new())); + } CommandType::VisitIndex => { let cpc = self.get_state().get_current_pointer().container.unwrap(); let count = self.get_state_mut().visit_count_for_container(&cpc) - 1; // index - // not count - self.get_state_mut().push_evaluation_stack(Rc::new(Value::new_int(count))); - }, + // not count + self.get_state_mut() + .push_evaluation_stack(Rc::new(Value::new_int(count))); + } CommandType::SequenceShuffleIndex => { let shuffle_index = self.next_sequence_shuffle_index()?; let v = Rc::new(Value::new_int(shuffle_index)); self.get_state_mut().push_evaluation_stack(v); - }, + } CommandType::StartThread => { // Handled in main step function - }, + } CommandType::Done => { - // We may exist in the context of the initial + // We may exist in the context of the initial // act of creating the thread, or in the context of // evaluating the content. if self.get_state().get_callstack().borrow().can_pop_thread() { - self.get_state().get_callstack().as_ref().borrow_mut().pop_thread()?; + self.get_state() + .get_callstack() + .as_ref() + .borrow_mut() + .pop_thread()?; } - // In normal flow - allow safe exit without warning else { self.get_state_mut().set_did_safe_exit(true); // Stop flow in current thread self.get_state().set_current_pointer(pointer::NULL.clone()); - } - }, + } + } CommandType::End => self.get_state_mut().force_end(), CommandType::ListFromInt => { let mut int_val: Option = None; let mut list_name_val: Option<&String> = None; let o = self.get_state_mut().pop_evaluation_stack(); - + if let Some(v) = Value::get_int_value(o.as_ref()) { int_val = Some(v); } let o = self.get_state_mut().pop_evaluation_stack(); - + if let Some(s) = Value::get_string_value(o.as_ref()) { list_name_val = Some(&s.string); } - + if int_val.is_none() { return Err(StoryError::InvalidStoryState("Passed non-integer when creating a list element from a numerical value.".to_owned())); } - + let mut generated_list_value: Option = None; - - if let Some(found_list_def) = self.list_definitions.as_ref().get_list_definition(list_name_val.as_ref().unwrap()) { - if let Some(found_item) = found_list_def.get_item_with_value(int_val.unwrap()) { - let l = InkList::from_single_element((found_item.clone(), int_val.unwrap())); + + if let Some(found_list_def) = self + .list_definitions + .as_ref() + .get_list_definition(list_name_val.as_ref().unwrap()) + { + if let Some(found_item) = + found_list_def.get_item_with_value(int_val.unwrap()) + { + let l = InkList::from_single_element(( + found_item.clone(), + int_val.unwrap(), + )); generated_list_value = Some(Value::new_list(l)); } } else { - return Err(StoryError::InvalidStoryState(format!("Failed to find List called {}", list_name_val.as_ref().unwrap()))); + return Err(StoryError::InvalidStoryState(format!( + "Failed to find List called {}", + list_name_val.as_ref().unwrap() + ))); } - + if generated_list_value.is_none() { generated_list_value = Some(Value::new_list(InkList::new())); } - - self.get_state_mut().push_evaluation_stack(Rc::new(generated_list_value.unwrap())); - - }, + + self.get_state_mut() + .push_evaluation_stack(Rc::new(generated_list_value.unwrap())); + } CommandType::ListRange => { let mut p = self.get_state_mut().pop_evaluation_stack(); let max = p.into_any().downcast::(); @@ -1168,19 +1353,26 @@ impl Story { let target_list = Value::get_list_value(p.as_ref()); if target_list.is_none() || min.is_err() || max.is_err() { - return Err(StoryError::InvalidStoryState("Expected List, minimum and maximum for LIST_RANGE".to_owned())); + return Err(StoryError::InvalidStoryState( + "Expected List, minimum and maximum for LIST_RANGE".to_owned(), + )); } - let result = target_list.unwrap().list_with_sub_range(&min.unwrap().value, &max.unwrap().value); + let result = target_list + .unwrap() + .list_with_sub_range(&min.unwrap().value, &max.unwrap().value); - self.get_state_mut().push_evaluation_stack(Rc::new(Value::new_list(result))); - }, + self.get_state_mut() + .push_evaluation_stack(Rc::new(Value::new_list(result))); + } CommandType::ListRandom => { let o = self.get_state_mut().pop_evaluation_stack(); let list = Value::get_list_value(o.as_ref()); if list.is_none() { - return Err(StoryError::InvalidStoryState("Expected list for LIST_RANDOM".to_owned())); + return Err(StoryError::InvalidStoryState( + "Expected list for LIST_RANDOM".to_owned(), + )); } let list = list.unwrap(); @@ -1193,7 +1385,8 @@ impl Story { // Non-empty source list else { // Generate a random index for the element to take - let result_seed = self.get_state().story_seed + self.get_state().previous_random; + let result_seed = + self.get_state().story_seed + self.get_state().previous_random; let mut rng = StdRng::seed_from_u64(result_seed as u64); let next_random = rng.gen::(); let list_item_index = (next_random as usize) % list.items.len(); @@ -1204,20 +1397,25 @@ impl Story { let random_item = sorted[list_item_index]; // Origin list is simply the origin of the one element - let mut new_list = InkList::from_single_origin(random_item.0.get_origin_name().unwrap().clone(), self.list_definitions.as_ref())?; + let mut new_list = InkList::from_single_origin( + random_item.0.get_origin_name().unwrap().clone(), + self.list_definitions.as_ref(), + )?; new_list.items.insert(random_item.0.clone(), *random_item.1); - + self.get_state_mut().previous_random = next_random as i32; new_list } }; - self.get_state_mut().push_evaluation_stack(Rc::new(Value::new_list(new_list))); - }, - CommandType::BeginTag => self.get_state_mut().push_to_output_stream(content_obj.clone()), - CommandType::EndTag => { - + self.get_state_mut() + .push_evaluation_stack(Rc::new(Value::new_list(new_list))); + } + CommandType::BeginTag => self + .get_state_mut() + .push_to_output_stream(content_obj.clone()), + CommandType::EndTag => { // EndTag has 2 modes: // - When in string evaluation (for choices) // - Normal @@ -1246,7 +1444,6 @@ impl Story { // we push it onto the evaluation stack in the exact same way // as the string for the choice content. if self.get_state().in_string_evaluation() { - let mut content_stack_for_tag: Vec = Vec::new(); let mut output_count_consumed = 0; @@ -1255,7 +1452,9 @@ impl Story { output_count_consumed += 1; - if let Some(command) = obj.as_ref().as_any().downcast_ref::() { + if let Some(command) = + obj.as_ref().as_any().downcast_ref::() + { if command.command_type == CommandType::BeginTag { break; } else { @@ -1269,32 +1468,38 @@ impl Story { } // Consume the content that was produced for this string - self.get_state_mut().pop_from_output_stream(output_count_consumed); + self.get_state_mut() + .pop_from_output_stream(output_count_consumed); let mut sb = String::new(); for str_val in &content_stack_for_tag { sb.push_str(str_val); } - let choice_tag = Rc::new(Tag::new(&StoryState::clean_output_whitespace(&sb))); + let choice_tag = + Rc::new(Tag::new(&StoryState::clean_output_whitespace(&sb))); // Pushing to the evaluation stack means it gets picked up // when a Choice is generated from the next Choice Point. self.get_state_mut().push_evaluation_stack(choice_tag); } - // Otherwise! Simply push EndTag, so that in the output stream we // have a structure of: [BeginTag, "the tag content", EndTag] else { - self.get_state_mut().push_to_output_stream(content_obj.clone()); + self.get_state_mut() + .push_to_output_stream(content_obj.clone()); } - }, + } } return Ok(true); } // Variable assignment - if let Some(var_ass) = content_obj.as_ref().as_any().downcast_ref::() { + if let Some(var_ass) = content_obj + .as_ref() + .as_any() + .downcast_ref::() + { let assigned_val = self.get_state_mut().pop_evaluation_stack(); // When in temporary evaluation, don't create new variables purely @@ -1303,31 +1508,42 @@ impl Story { // var prioritiseHigherInCallStack = _temporaryEvaluationContainer // != null; let assigned_val = assigned_val.into_any().downcast::().unwrap(); - self.get_state_mut().variables_state.assign( var_ass, assigned_val)?; + self.get_state_mut() + .variables_state + .assign(var_ass, assigned_val)?; return Ok(true); } // Variable reference - if let Ok(var_ref) = content_obj.clone().into_any().downcast::() { + if let Ok(var_ref) = content_obj + .clone() + .into_any() + .downcast::() + { let found_value: Rc; // Explicit read count value if let Some(p) = &var_ref.path_for_count { let container = var_ref.get_container_for_count(); - let count = self.get_state_mut().visit_count_for_container(container.as_ref().unwrap()); + let count = self + .get_state_mut() + .visit_count_for_container(container.as_ref().unwrap()); found_value = Rc::new(Value::new_int(count)); } - // Normal variable reference else { - match self.get_state().variables_state.get_variable_with_name(&var_ref.name, -1) { + match self + .get_state() + .variables_state + .get_variable_with_name(&var_ref.name, -1) + { Some(v) => found_value = v, None => { self.add_error(&format!("Variable not found: '{}'. 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.", var_ref.name), true); - + found_value = Rc::new(Value::new_int(0)); - }, + } } } @@ -1337,15 +1553,20 @@ impl Story { } // Native function call - if let Some(func) = content_obj.as_ref().as_any().downcast_ref::() { - let func_params = self.get_state_mut().pop_evaluation_stack_multiple(func.get_number_of_parameters()); + if let Some(func) = content_obj + .as_ref() + .as_any() + .downcast_ref::() + { + let func_params = self + .get_state_mut() + .pop_evaluation_stack_multiple(func.get_number_of_parameters()); let result = func.call(func_params)?; self.get_state_mut().push_evaluation_stack(result); return Ok(true); } - Ok(false) } @@ -1360,7 +1581,8 @@ impl Story { if !self.get_state().diverted_pointer.is_null() { let dp = self.get_state().diverted_pointer.clone(); self.get_state_mut().set_current_pointer(dp); - self.get_state_mut().set_diverted_pointer(pointer::NULL.clone()); + self.get_state_mut() + .set_diverted_pointer(pointer::NULL.clone()); // Internally uses state.previousContentObject and // state.currentContentObject @@ -1382,30 +1604,46 @@ impl Story { // Ran out of content? Try to auto-exit from a function, // or finish evaluating the content of a thread if !successful_pointer_increment { - let mut did_pop = false; - let can_pop_type = self.get_state().get_callstack().as_ref().borrow().can_pop_type(Some(PushPopType::Function)); + let can_pop_type = self + .get_state() + .get_callstack() + .as_ref() + .borrow() + .can_pop_type(Some(PushPopType::Function)); if can_pop_type { - // Pop from the call stack - self.get_state_mut().pop_callstack(Some(PushPopType::Function))?; + self.get_state_mut() + .pop_callstack(Some(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 self.get_state().get_in_expression_evaluation() { - self.get_state_mut().push_evaluation_stack(Rc::new(Void::new())); + self.get_state_mut() + .push_evaluation_stack(Rc::new(Void::new())); } did_pop = true; - } else if self.get_state().get_callstack().as_ref().borrow().can_pop_thread() { - self.get_state().get_callstack().as_ref().borrow_mut().pop_thread()?; + } else if self + .get_state() + .get_callstack() + .as_ref() + .borrow() + .can_pop_thread() + { + self.get_state() + .get_callstack() + .as_ref() + .borrow_mut() + .pop_thread()?; did_pop = true; } else { - self.get_state_mut().try_exit_function_evaluation_from_game(); + self.get_state_mut() + .try_exit_function_evaluation_from_game(); } // Step past the point where we last called out @@ -1420,16 +1658,22 @@ impl Story { fn increment_content_pointer(&self) -> bool { let mut successful_increment = true; - let mut pointer = self.get_state().get_callstack().as_ref().borrow().get_current_element().current_pointer.clone(); + let mut pointer = self + .get_state() + .get_callstack() + .as_ref() + .borrow() + .get_current_element() + .current_pointer + .clone(); pointer.index += 1; - let mut container= pointer.container.as_ref().unwrap().clone(); + let mut container = pointer.container.as_ref().unwrap().clone(); // 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 >= container.content.len() as i32 { - successful_increment = false; let next_ancestor = container.get_object().get_parent(); @@ -1439,13 +1683,18 @@ impl Story { } let rto: Rc = container; - let index_in_ancestor = next_ancestor.as_ref().unwrap().content.iter().position(|s| Rc::ptr_eq(s, &rto)); + let index_in_ancestor = next_ancestor + .as_ref() + .unwrap() + .content + .iter() + .position(|s| Rc::ptr_eq(s, &rto)); if index_in_ancestor.is_none() { break; } pointer = Pointer::new(next_ancestor, index_in_ancestor.unwrap() as i32); - container= pointer.container.as_ref().unwrap().clone(); + container = pointer.container.as_ref().unwrap().clone(); // Increment to next content in outer container pointer.index += 1; @@ -1457,7 +1706,12 @@ impl Story { pointer = pointer::NULL.clone(); } - self.get_state().get_callstack().as_ref().borrow_mut().get_current_element_mut().current_pointer = pointer; + self.get_state() + .get_callstack() + .as_ref() + .borrow_mut() + .get_current_element_mut() + .current_pointer = pointer; successful_increment } @@ -1498,15 +1752,19 @@ impl Story { // can create multiple leading edges for the story, each of // which has its own context. let choice_to_choose = choices.get(choice_index).unwrap(); - self.get_state().get_callstack().borrow_mut().set_current_thread(choice_to_choose.get_thread_at_generation().unwrap()); + self.get_state() + .get_callstack() + .borrow_mut() + .set_current_thread(choice_to_choose.get_thread_at_generation().unwrap()); self.choose_path(&choice_to_choose.target_path, true)?; Ok(()) } - fn choose_path(&mut self, p: &Path, incrementing_turn_index: bool) -> Result<(), StoryError> { - self.get_state_mut().set_chosen_path( p, incrementing_turn_index)?; + fn choose_path(&mut self, p: &Path, incrementing_turn_index: bool) -> Result<(), StoryError> { + self.get_state_mut() + .set_chosen_path(p, incrementing_turn_index)?; // Take a note of newly visited containers for read counts etc self.visit_changed_containers_due_to_divert(); @@ -1518,7 +1776,6 @@ impl Story { let truthy = false; if let Some(val) = obj.as_ref().as_any().downcast_ref::() { - if let Some(target_path) = Value::get_divert_target_value(obj.as_ref()) { return Err(StoryError::InvalidStoryState(format!("Shouldn't use a divert target (to {}) as a conditional value. Did you intend a function call 'likeThis()' or a read count check 'likeThis'? (no arrows)", target_path))); } @@ -1529,7 +1786,10 @@ impl Story { Ok(truthy) } - fn process_choice(&mut self, choice_point: &Rc) -> Result>, StoryError> { + fn process_choice( + &mut self, + choice_point: &Rc, + ) -> Result>, StoryError> { let mut show_choice = true; // Don't create choice if choice point doesn't pass conditional @@ -1554,7 +1814,9 @@ impl Story { // Don't create choice if player has already read this content if choice_point.once_only() { - let visit_count = self.get_state_mut().visit_count_for_container(choice_point.get_choice_target().as_ref().unwrap()); + let visit_count = self + .get_state_mut() + .visit_count_for_container(choice_point.get_choice_target().as_ref().unwrap()); if visit_count > 0 { show_choice = false; } @@ -1569,7 +1831,14 @@ impl Story { start_text.push_str(&choice_only_text); - let choice = Rc::new(Choice::new(choice_point.get_path_on_choice(), Object::get_path(choice_point.as_ref()).to_string(), choice_point.is_invisible_default(), tags, self.get_state().get_callstack().borrow_mut().fork_thread(), start_text.trim().to_string())); + let choice = Rc::new(Choice::new( + choice_point.get_path_on_choice(), + Object::get_path(choice_point.as_ref()).to_string(), + choice_point.is_invisible_default(), + tags, + self.get_state().get_callstack().borrow_mut().fork_thread(), + start_text.trim().to_string(), + )); Ok(Some(choice)) } @@ -1578,33 +1847,51 @@ impl Story { let obj = self.get_state_mut().pop_evaluation_stack(); let choice_only_str_val = Value::get_string_value(obj.as_ref()).unwrap(); - while !self.get_state().evaluation_stack.is_empty() && self.get_state().peek_evaluation_stack().unwrap().as_any().is::() { - let tag = self.get_state_mut().pop_evaluation_stack().into_any().downcast::().unwrap(); + while !self.get_state().evaluation_stack.is_empty() + && self + .get_state() + .peek_evaluation_stack() + .unwrap() + .as_any() + .is::() + { + let tag = self + .get_state_mut() + .pop_evaluation_stack() + .into_any() + .downcast::() + .unwrap(); tags.insert(0, tag.get_text().clone()); // popped in reverse order } choice_only_str_val.string.to_string() } - pub fn pointer_at_path(main_content_container: &Rc, path: &Path) -> Result { + pub fn pointer_at_path( + main_content_container: &Rc, + path: &Path, + ) -> Result { if path.len() == 0 { return Ok(pointer::NULL.clone()); } - + let mut p = Pointer::default(); let mut path_length_to_use = path.len() as i32; - - - let result: SearchResult = + + let result: SearchResult = if path.get_last_component().unwrap().is_index() { path_length_to_use -= 1; - let result = SearchResult::from_search_result(&main_content_container.content_at_path(path, 0, path_length_to_use)); + let result = SearchResult::from_search_result( + &main_content_container.content_at_path(path, 0, path_length_to_use), + ); p.container = result.container(); p.index = path.get_last_component().unwrap().index.unwrap() as i32; result } else { - let result = SearchResult::from_search_result(&main_content_container.content_at_path(path, 0, -1)); + let result = SearchResult::from_search_result( + &main_content_container.content_at_path(path, 0, -1), + ); p.container = result.container(); p.index = -1; @@ -1612,7 +1899,7 @@ impl Story { }; let main_container: Rc = main_content_container.clone(); - + if Rc::ptr_eq(&result.obj, &main_container) && path_length_to_use > 0 { return Err(StoryError::InvalidStoryState(format!( "Failed to find content at path '{}', and no approximation of it was possible.", @@ -1623,56 +1910,61 @@ impl Story { // self.add_error(&format!("Failed to find content at path '{}', so it was approximated to: '{}'.", path // , result.obj.unwrap().get_path()), true); } - + Ok(p) } fn visit_changed_containers_due_to_divert(&mut self) { let previous_pointer = self.get_state().get_previous_pointer(); let pointer = self.get_state().get_current_pointer(); - + // 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.is_null() || pointer.index == -1 { return; } - + // First, find the previously open set of containers self.prev_containers.clear(); - + if !previous_pointer.is_null() { let mut prev_ancestor = None; - if let Some(container) = previous_pointer.resolve().and_then(|res| res.into_any().downcast::().ok()) { + if let Some(container) = previous_pointer + .resolve() + .and_then(|res| res.into_any().downcast::().ok()) + { prev_ancestor = Some(container); } else if previous_pointer.container.is_some() { prev_ancestor = previous_pointer.container.clone(); } - + while let Some(prev_anc) = prev_ancestor { self.prev_containers.push(prev_anc.clone()); prev_ancestor = prev_anc.get_object().get_parent(); } } - + // 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 let current_child_of_container = pointer.resolve(); - + if current_child_of_container.is_none() { return; } let mut current_child_of_container = current_child_of_container.unwrap(); - - let mut current_container_ancestor = current_child_of_container - .get_object().get_parent(); - + + let mut current_container_ancestor = current_child_of_container.get_object().get_parent(); + let mut all_children_entered_at_start = true; - + while let Some(current_container) = current_container_ancestor { - if !self.prev_containers.iter().any(|e| Rc::ptr_eq(e, ¤t_container)) + if !self + .prev_containers + .iter() + .any(|e| Rc::ptr_eq(e, ¤t_container)) || current_container.counting_at_start_only { // Check whether this ancestor container is being entered at the start, @@ -1680,9 +1972,12 @@ impl Story { let entering_at_start = current_container .content .first() - .map(|first_child| Rc::ptr_eq(first_child, ¤t_child_of_container) && all_children_entered_at_start) + .map(|first_child| { + Rc::ptr_eq(first_child, ¤t_child_of_container) + && all_children_entered_at_start + }) .unwrap_or(false); - + // Don't count it as entering at start if we're entering randomly somewhere within // a container B that happens to be nested at index 0 of container A. It only // counts @@ -1690,10 +1985,10 @@ impl Story { if !entering_at_start { all_children_entered_at_start = false; } - + // Mark a visit to this container self.visit_container(¤t_container, entering_at_start); - + current_child_of_container = current_container.clone(); current_container_ancestor = current_container.get_object().get_parent(); } else { @@ -1702,11 +1997,18 @@ impl Story { } } - pub fn evaluate_function(&mut self, func_name: &str, args: Option<&Vec>, text_output: &mut String) -> Result, StoryError> { + pub fn evaluate_function( + &mut self, + func_name: &str, + args: Option<&Vec>, + text_output: &mut String, + ) -> Result, StoryError> { self.if_async_we_cant("evaluate a function")?; if func_name.trim().is_empty() { - return Err(StoryError::InvalidStoryState("Function is empty or white space.".to_owned())); + return Err(StoryError::InvalidStoryState( + "Function is empty or white space.".to_owned(), + )); } // Get the content that we need to run @@ -1724,7 +2026,8 @@ impl Story { self.get_state_mut().reset_output(None); // State will temporarily replace the callstack in order to evaluate - self.get_state_mut().start_function_evaluation_from_game(func_container.unwrap(), args)?; + self.get_state_mut() + .start_function_evaluation_from_game(func_container.unwrap(), args)?; // Evaluate the function, and collect the string output while self.can_continue() { @@ -1735,10 +2038,12 @@ impl Story { // Restore the output stream in case this was called // during main story evaluation. - self.get_state_mut().reset_output(Some(output_stream_before)); + self.get_state_mut() + .reset_output(Some(output_stream_before)); // Finish evaluation, and see whether anything was produced - self.get_state_mut().complete_function_evaluation_from_game() + self.get_state_mut() + .complete_function_evaluation_from_game() } pub(crate) fn knot_container_with_name(&self, name: &str) -> Option> { @@ -1752,20 +2057,24 @@ impl Story { let num_elements = if let Some(v) = Value::get_int_value(pop_evaluation_stack.as_ref()) { v } else { - return Err(StoryError::InvalidStoryState("Expected number of elements in sequence for shuffle index".to_owned())); + return Err(StoryError::InvalidStoryState( + "Expected number of elements in sequence for shuffle index".to_owned(), + )); }; - + let seq_container = self.get_state().get_current_pointer().container.unwrap(); - + let seq_count = if let Some(v) = Value::get_int_value(pop_evaluation_stack.as_ref()) { v } else { - return Err(StoryError::InvalidStoryState("Expected sequence count value for shuffle index".to_owned())); + return Err(StoryError::InvalidStoryState( + "Expected sequence count value for shuffle index".to_owned(), + )); }; - + let loop_index = seq_count / num_elements; let iteration_index = seq_count % num_elements; - + // 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 @@ -1773,22 +2082,24 @@ impl Story { let seq_path_str = Object::get_path(seq_container.as_ref()).to_string(); let sequence_hash: i32 = seq_path_str.chars().map(|c| c as i32).sum(); let random_seed = sequence_hash + loop_index + self.get_state().story_seed; - + let mut rng = StdRng::seed_from_u64(random_seed as u64); - + let mut unpicked_indices: Vec = (0..num_elements).collect(); - + for i in 0..=iteration_index { let chosen = rng.gen::().rem_euclid(unpicked_indices.len() as i32); let chosen_index = unpicked_indices[chosen as usize]; unpicked_indices.retain(|&x| x != chosen_index); - + if i == iteration_index { return Ok(chosen_index); } } - - Err(StoryError::InvalidStoryState("Should never reach here".to_owned())) + + Err(StoryError::InvalidStoryState( + "Should never reach here".to_owned(), + )) } pub fn get_global_tags(&self) -> Result, StoryError> { @@ -1799,12 +2110,15 @@ impl Story { self.tags_at_start_of_flow_container_with_path_string(path) } - fn tags_at_start_of_flow_container_with_path_string(&self, path_string: &str) -> Result, StoryError> { + fn tags_at_start_of_flow_container_with_path_string( + &self, + path_string: &str, + ) -> Result, StoryError> { let path = Path::new_with_components_string(Some(path_string)); // Expected to be global story, knot, or stitch let mut flow_container = self.content_at_path(&path).container().unwrap(); - + while let Some(first_content) = flow_container.content.get(0) { if let Ok(container) = first_content.clone().into_any().downcast::() { flow_container = container; @@ -1812,21 +2126,19 @@ impl Story { break; } } - + // Any initial tag objects count as the "main tags" associated with that // story/knot/stitch let mut in_tag = false; let mut tags = Vec::new(); - - for content in &flow_container.content { + + for content in &flow_container.content { match content.as_ref().as_any().downcast_ref::() { - Some(command) => { - match command.command_type { - CommandType::BeginTag => in_tag = true, - CommandType::EndTag => in_tag = false, - _ => {} - } - } + Some(command) => match command.command_type { + CommandType::BeginTag => in_tag = true, + CommandType::EndTag => in_tag = false, + _ => {} + }, _ => { if in_tag { if let Some(string_value) = Value::get_string_value(content.as_ref()) { @@ -1842,8 +2154,8 @@ impl Story { } } } - - Ok(tags) + + Ok(tags) } fn content_at_path(&self, path: &Path) -> SearchResult { @@ -1855,7 +2167,12 @@ impl Story { Ok(self.get_state_mut().get_current_tags()) } - pub fn choose_path_string(&mut self, path: &str, reset_call_stack: bool, args: Option<&Vec>) -> Result<(), StoryError> { + pub fn choose_path_string( + &mut self, + path: &str, + reset_call_stack: bool, + args: Option<&Vec>, + ) -> Result<(), StoryError> { self.if_async_we_cant("call ChoosePathString right now")?; if reset_call_stack { @@ -1864,18 +2181,33 @@ impl Story { // 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 self.get_state().get_callstack().borrow().get_current_element().push_pop_type == PushPopType::Function { + if self + .get_state() + .get_callstack() + .borrow() + .get_current_element() + .push_pop_type + == PushPopType::Function + { let mut func_detail = "".to_owned(); - let container = self.get_state().get_callstack().borrow().get_current_element().current_pointer.container.clone(); + let container = self + .get_state() + .get_callstack() + .borrow() + .get_current_element() + .current_pointer + .container + .clone(); if let Some(container) = container { func_detail = format!("({})", Object::get_path(container.as_ref())); } - + return Err(StoryError::InvalidStoryState(format!("Story was running a function {func_detail} when you called ChoosePathString({}) - this is almost certainly not what you want! Full stack trace: \n{}", path, self.get_state().get_callstack().borrow().get_callstack_trace()))); } } - self.get_state_mut().pass_arguments_to_evaluation_stack(args)?; + self.get_state_mut() + .pass_arguments_to_evaluation_stack(args)?; self.choose_path(&Path::new_with_components_string(Some(path)), true)?; Ok(()) @@ -1893,7 +2225,10 @@ impl Story { self.if_async_we_cant("switch flow")?; if self.async_saving { - return Err(StoryError::InvalidStoryState(format!("Story is already in background saving mode, can't switch flow to {}", flow_name))); + return Err(StoryError::InvalidStoryState(format!( + "Story is already in background saving mode, can't switch flow to {}", + flow_name + ))); } self.get_state_mut().switch_flow_internal(flow_name); @@ -1913,8 +2248,15 @@ impl Story { Ok(()) } - pub fn set_variable(&mut self, variable_name: &str, value_type: &ValueType) -> Result<(), StoryError> { - let notify_observers = self.get_state_mut().variables_state.set(variable_name, value_type.clone())?; + pub fn set_variable( + &mut self, + variable_name: &str, + value_type: &ValueType, + ) -> Result<(), StoryError> { + let notify_observers = self + .get_state_mut() + .variables_state + .set(variable_name, value_type.clone())?; if notify_observers { self.notify_variable_changed(variable_name, value_type); @@ -1935,7 +2277,7 @@ impl Story { self.get_state_mut().load_json(json_state) } - pub fn get_visit_count_at_path_string(&self, path_string: &str) -> Result { + pub fn get_visit_count_at_path_string(&self, path_string: &str) -> Result { self.get_state().visit_count_at_path_string(path_string) } @@ -1943,4 +2285,3 @@ impl Story { self.allow_external_function_fallbacks = v; } } - diff --git a/lib/src/story_callbacks.rs b/lib/src/story_callbacks.rs index c5f44c0..5d64210 100644 --- a/lib/src/story_callbacks.rs +++ b/lib/src/story_callbacks.rs @@ -1,12 +1,16 @@ -use std::{rc::Rc, cell::RefCell, collections::HashSet}; +use std::{cell::RefCell, collections::HashSet, rc::Rc}; -use crate::{story::Story, value_type::ValueType, story_error::StoryError, push_pop::PushPopType, pointer::Pointer, container::Container, value::Value, object::RTObject, void::Void, divert::Divert}; +use crate::{ + container::Container, divert::Divert, object::RTObject, pointer::Pointer, + push_pop::PushPopType, story::Story, story_error::StoryError, value::Value, + value_type::ValueType, void::Void, +}; -pub trait VariableObserver { +pub trait VariableObserver { fn changed(&mut self, variable_name: &str, value: &ValueType); } -pub trait ExternalFunction { +pub trait ExternalFunction { fn call(&mut self, func_name: &str, args: Vec) -> Option; } @@ -21,22 +25,29 @@ pub trait ErrorHandler { #[derive(PartialEq, Clone, Copy)] pub enum ErrorType { - // You should probably fix this, but it's not critical - Warning, - // Critical error that can't be recovered from - Error + // You should probably fix this, but it's not critical + Warning, + // Critical error that can't be recovered from + Error, } impl Story { - pub fn set_error_handler(&mut self, err_handler: Rc>) { self.on_error = Some(err_handler); } - pub fn observe_variable(&mut self, variable_name: &str, observer: Rc>) -> Result<(), StoryError> { + pub fn observe_variable( + &mut self, + variable_name: &str, + observer: Rc>, + ) -> Result<(), StoryError> { self.if_async_we_cant("observe a new variable")?; - if !self.get_state().variables_state.global_variable_exists_with_name(variable_name) { + if !self + .get_state() + .variables_state + .global_variable_exists_with_name(variable_name) + { return Err(StoryError::BadArgument( format!("Cannot observe variable '{variable_name}' because it wasn't declared in the ink story."))); } @@ -46,15 +57,19 @@ impl Story { v.push(observer); } None => { - let v: Vec>> = vec![observer]; - self.variable_observers.insert(variable_name.to_string(), v); - } + let v: Vec>> = vec![observer]; + self.variable_observers.insert(variable_name.to_string(), v); + } } Ok(()) } - pub fn remove_variable_observer(&mut self, observer: &Rc>, specific_variable_name: Option<&str>) -> Result<(), StoryError> { + pub fn remove_variable_observer( + &mut self, + observer: &Rc>, + specific_variable_name: Option<&str>, + ) -> Result<(), StoryError> { self.if_async_we_cant("remove a variable observer")?; // Remove observer for this specific variable @@ -63,17 +78,17 @@ impl Story { if let Some(v) = self.variable_observers.get_mut(specific_variable_name) { let index = v.iter().position(|x| Rc::ptr_eq(x, observer)).unwrap(); v.remove(index); - + if v.is_empty() { self.variable_observers.remove(specific_variable_name); } - } - }, + } + } None => { // Remove observer for all variables let mut keys_to_remove = Vec::new(); - - for (k,v) in self.variable_observers.iter_mut() { + + for (k, v) in self.variable_observers.iter_mut() { let index = v.iter().position(|x| Rc::ptr_eq(x, observer)).unwrap(); v.remove(index); @@ -84,7 +99,7 @@ impl Story { for key_to_remove in keys_to_remove.iter() { self.variable_observers.remove(key_to_remove); - } + } } } @@ -101,28 +116,41 @@ impl Story { } } - pub fn bind_external_function(&mut self, func_name: &str, function: Rc>, lookahead_safe: bool) -> Result<(), StoryError> { + pub fn bind_external_function( + &mut self, + func_name: &str, + function: Rc>, + lookahead_safe: bool, + ) -> Result<(), StoryError> { self.if_async_we_cant("bind an external function")?; if self.externals.contains_key(func_name) { - return Err(StoryError::BadArgument(format!("Function '{func_name}' has already been bound."))); + return Err(StoryError::BadArgument(format!( + "Function '{func_name}' has already been bound." + ))); } - let external_function_def = ExternalFunctionDef {function, lookahead_safe}; + let external_function_def = ExternalFunctionDef { + function, + lookahead_safe, + }; - self.externals.insert(func_name.to_string(), external_function_def); + self.externals + .insert(func_name.to_string(), external_function_def); Ok(()) } - /// Remove a binding for a named EXTERNAL ink function. + /// Remove a binding for a named EXTERNAL ink function. pub fn unbind_external_function(&mut self, func_name: &str) -> Result<(), StoryError> { self.if_async_we_cant("unbind an external a function")?; - + if !self.externals.contains_key(func_name) { - return Err(StoryError::BadArgument(format!("Function '{func_name}' has not been bound."))); + return Err(StoryError::BadArgument(format!( + "Function '{func_name}' has not been bound." + ))); } - + self.externals.remove(func_name); Ok(()) @@ -133,8 +161,6 @@ impl Story { func_name: &str, number_of_arguments: usize, ) -> Result<(), StoryError> { - - // 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 let Some(func_def) = self.externals.get(func_name) { @@ -142,15 +168,19 @@ impl Story { self.saw_lookahead_unsafe_function_after_new_line = true; return Ok(()); } - } else { - // Try to use fallback function? + } else { + // Try to use fallback function? if self.allow_external_function_fallbacks { - if let Some(fallback_function_container) = self.knot_container_with_name(func_name) { + if let Some(fallback_function_container) = self.knot_container_with_name(func_name) + { // Divert direct into fallback function and we're done - self.get_state() - .get_callstack().borrow_mut() - .push(PushPopType::Function, 0, self.get_state().get_output_stream().len() as i32); - self.get_state_mut().set_diverted_pointer(Pointer::start_of(fallback_function_container)); + self.get_state().get_callstack().borrow_mut().push( + PushPopType::Function, + 0, + self.get_state().get_output_stream().len() as i32, + ); + self.get_state_mut() + .set_diverted_pointer(Pointer::start_of(fallback_function_container)); return Ok(()); } else { return Err(StoryError::InvalidStoryState(format!( @@ -165,7 +195,7 @@ impl Story { ))); } } - + // Pop arguments let mut arguments: Vec = Vec::new(); for _ in 0..number_of_arguments { @@ -181,23 +211,25 @@ impl Story { ))); } } - + // Reverse arguments from the order they were popped, // so they're the right way round again. arguments.reverse(); - + // Run the function! let func_def = self.externals.get(func_name); - let func_result = func_def.unwrap().function.borrow_mut().call(func_name,arguments); - + let func_result = func_def + .unwrap() + .function + .borrow_mut() + .call(func_name, arguments); + // Convert return value (if any) to a type that the ink engine can use let return_obj: Rc = match func_result { - Some(func_result) => { - Rc::new(Value::new(func_result)) - } + Some(func_result) => Rc::new(Value::new(func_result)), None => Rc::new(Void::new()), }; - + self.get_state_mut().push_evaluation_stack(return_obj); Ok(()) @@ -206,12 +238,19 @@ impl Story { pub(crate) fn validate_external_bindings(&mut self) -> Result<(), StoryError> { let mut missing_externals: HashSet = HashSet::new(); - self.validate_external_bindings_container(&self.get_main_content_container(), &mut missing_externals)?; + self.validate_external_bindings_container( + &self.get_main_content_container(), + &mut missing_externals, + )?; if missing_externals.is_empty() { self.has_validated_externals = true; } else { - let join: String = missing_externals.iter().cloned().collect::>().join(", "); + let join: String = missing_externals + .iter() + .cloned() + .collect::>() + .join(", "); let message = format!( "ERROR: Missing function binding for external{}: '{}' {}", if missing_externals.len() > 1 { "s" } else { "" }, @@ -229,17 +268,29 @@ impl Story { Ok(()) } - fn validate_external_bindings_container(&self, c: &Rc, missing_externals: &mut std::collections::HashSet) -> Result<(), StoryError> { - for inner_content in c.content.iter() { - let container = inner_content.clone().into_any().downcast::().ok(); + fn validate_external_bindings_container( + &self, + c: &Rc, + missing_externals: &mut std::collections::HashSet, + ) -> Result<(), StoryError> { + for inner_content in c.content.iter() { + let container = inner_content + .clone() + .into_any() + .downcast::() + .ok(); match &container { - Some(container) => if !container.has_valid_name(){ - self.validate_external_bindings_container(container, missing_externals)?; - }, - None => {self.validate_external_bindings_rtobject(inner_content, missing_externals)?;}, + Some(container) => { + if !container.has_valid_name() { + self.validate_external_bindings_container(container, missing_externals)?; + } + } + None => { + self.validate_external_bindings_rtobject(inner_content, missing_externals)?; + } } - + if container.is_none() || !container.as_ref().unwrap().has_valid_name() { self.validate_external_bindings_rtobject(inner_content, missing_externals)?; } @@ -252,7 +303,11 @@ impl Story { Ok(()) } - fn validate_external_bindings_rtobject(&self, o: &Rc, missing_externals: &mut std::collections::HashSet) -> Result<(), StoryError> { + fn validate_external_bindings_rtobject( + &self, + o: &Rc, + missing_externals: &mut std::collections::HashSet, + ) -> Result<(), StoryError> { let divert = o.clone().into_any().downcast::().ok(); if let Some(divert) = divert { @@ -260,10 +315,11 @@ impl Story { let name = divert.get_target_path_string().unwrap(); if !self.externals.contains_key(&name) { - if self.allow_external_function_fallbacks { - let fallback_found = - self.get_main_content_container().named_content.contains_key(&name); + let fallback_found = self + .get_main_content_container() + .named_content + .contains_key(&name); if !fallback_found { missing_externals.insert(name); } @@ -276,5 +332,4 @@ impl Story { Ok(()) } - -} \ No newline at end of file +} diff --git a/lib/src/story_error.rs b/lib/src/story_error.rs index ff1e159..6064a55 100644 --- a/lib/src/story_error.rs +++ b/lib/src/story_error.rs @@ -2,16 +2,16 @@ use core::fmt; #[derive(Debug)] pub enum StoryError { - InvalidStoryState(String), - BadJson(String), - BadArgument(String), + InvalidStoryState(String), + BadJson(String), + BadArgument(String), } impl StoryError { pub(crate) fn get_message(&self) -> &str { match self { - StoryError::InvalidStoryState(msg) | - StoryError::BadJson(msg) | - StoryError::BadArgument(msg) => msg.as_str(), + StoryError::InvalidStoryState(msg) + | StoryError::BadJson(msg) + | StoryError::BadArgument(msg) => msg.as_str(), } } } @@ -19,11 +19,11 @@ impl StoryError { impl std::error::Error for StoryError {} impl fmt::Display for StoryError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - StoryError::InvalidStoryState(desc) => write!(f, "Invalid story state: {}", desc), - StoryError::BadJson(desc) => write!(f, "Error parsing JSON: {}", desc), - StoryError::BadArgument(arg) => write!(f, "Bad argument: {}", arg), + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + StoryError::InvalidStoryState(desc) => write!(f, "Invalid story state: {}", desc), + StoryError::BadJson(desc) => write!(f, "Error parsing JSON: {}", desc), + StoryError::BadArgument(arg) => write!(f, "Bad argument: {}", arg), + } } - } } diff --git a/lib/src/story_state.rs b/lib/src/story_state.rs index 6d9d18e..8e18a90 100644 --- a/lib/src/story_state.rs +++ b/lib/src/story_state.rs @@ -1,8 +1,29 @@ #![allow(unused_variables, dead_code)] -use std::{rc::Rc, cell::RefCell, collections::HashMap}; - -use crate::{pointer::{Pointer, self}, callstack::CallStack, flow::Flow, variables_state::VariablesState, choice::Choice, object::{RTObject, Object}, value::Value, glue::Glue, push_pop::PushPopType, control_command::{CommandType, ControlCommand}, container::Container, state_patch::StatePatch, story::{Story, INK_VERSION_CURRENT}, path::Path, void::Void, tag::Tag, list_definitions_origin::ListDefinitionsOrigin, value_type::ValueType, json_write, json_read, story_error::StoryError}; +use std::{cell::RefCell, collections::HashMap, rc::Rc}; + +use crate::{ + callstack::CallStack, + choice::Choice, + container::Container, + control_command::{CommandType, ControlCommand}, + flow::Flow, + glue::Glue, + json_read, json_write, + list_definitions_origin::ListDefinitionsOrigin, + object::{Object, RTObject}, + path::Path, + pointer::{self, Pointer}, + push_pop::PushPopType, + state_patch::StatePatch, + story::{Story, INK_VERSION_CURRENT}, + story_error::StoryError, + tag::Tag, + value::Value, + value_type::ValueType, + variables_state::VariablesState, + void::Void, +}; use rand::Rng; use serde_json::{json, Map}; @@ -37,14 +58,17 @@ pub(crate) struct StoryState { } impl StoryState { - pub fn new(main_content_container: Rc, list_definitions: Rc) -> StoryState { + pub fn new( + main_content_container: Rc, + list_definitions: Rc, + ) -> StoryState { let current_flow = Flow::new(DEFAULT_FLOW_NAME, main_content_container.clone()); let callstack = current_flow.callstack.clone(); let mut rng = rand::thread_rng(); let story_seed = rng.gen_range(0..100); - let state = StoryState { + let state = StoryState { current_flow, did_safe_exit: false, output_stream_text_dirty: true, @@ -82,7 +106,11 @@ impl StoryState { } pub fn get_current_pointer(&self) -> Pointer { - self.get_callstack().borrow().get_current_element().current_pointer.clone() + self.get_callstack() + .borrow() + .get_current_element() + .current_pointer + .clone() } pub fn get_callstack(&self) -> &Rc> { @@ -167,7 +195,11 @@ impl StoryState { if let (false, Some(text_content)) = (in_tag, text_content) { sb.push_str(&text_content.string); - } else if let Some(control_command) = output_obj.as_ref().as_any().downcast_ref::() { + } else if let Some(control_command) = output_obj + .as_ref() + .as_any() + .downcast_ref::() + { if control_command.command_type == CommandType::BeginTag { in_tag = true; } else if control_command.command_type == CommandType::EndTag { @@ -187,12 +219,16 @@ impl StoryState { pub fn get_current_tags(&mut self) -> Vec { if self.output_stream_tags_dirty { self.current_tags.clear(); - + let mut in_tag = false; let mut sb = String::new(); - + for output_obj in self.get_output_stream().clone() { - if let Some(control_command) = output_obj.as_ref().as_any().downcast_ref::() { + if let Some(control_command) = output_obj + .as_ref() + .as_any() + .downcast_ref::() + { match control_command.command_type { CommandType::BeginTag => { if in_tag && !sb.is_empty() { @@ -201,7 +237,7 @@ impl StoryState { sb.clear(); } in_tag = true; - }, + } CommandType::EndTag => { if !sb.is_empty() { let txt = Self::clean_output_whitespace(&sb); @@ -209,8 +245,8 @@ impl StoryState { sb.clear(); } in_tag = false; - }, - _ => {}, + } + _ => {} } } else if in_tag { if let Some(string_value) = Value::get_string_value(output_obj.as_ref()) { @@ -223,16 +259,16 @@ impl StoryState { } } } - + if !sb.is_empty() { let txt = Self::clean_output_whitespace(&sb); self.current_tags.push(txt); sb.clear(); } - + self.output_stream_tags_dirty = false; } - + self.current_tags.clone() } @@ -240,33 +276,35 @@ impl StoryState { let mut sb = String::with_capacity(input_str.len()); let mut current_whitespace_start = -1; let mut start_of_line = 0; - + for (i, c) in input_str.chars().enumerate() { let is_inline_whitespace = c == ' ' || c == '\t'; - + if is_inline_whitespace && current_whitespace_start == -1 { current_whitespace_start = i as i32; } - + if !is_inline_whitespace { - if c != '\n' && current_whitespace_start > 0 && current_whitespace_start != start_of_line { + if c != '\n' + && current_whitespace_start > 0 + && current_whitespace_start != start_of_line + { sb.push(' '); } current_whitespace_start = -1; } - + if c == '\n' { start_of_line = i as i32 + 1; } - + if !is_inline_whitespace { sb.push(c); } } - + sb } - pub fn output_stream_ends_in_newline(&self) -> bool { if !self.get_output_stream().is_empty() { @@ -274,7 +312,7 @@ impl StoryState { if let Some(cmd) = e.as_any().downcast_ref::() { break; } - + if let Some(val) = e.as_any().downcast_ref::() { if let ValueType::String(text) = &val.value { if text.is_newline { @@ -286,24 +324,33 @@ impl StoryState { } } } - + false } pub fn set_current_pointer(&self, pointer: Pointer) { - self.get_callstack().as_ref().borrow_mut().get_current_element_mut().current_pointer = pointer; + self.get_callstack() + .as_ref() + .borrow_mut() + .get_current_element_mut() + .current_pointer = pointer; } pub fn get_in_expression_evaluation(&self) -> bool { - self.get_callstack().borrow().get_current_element().in_expression_evaluation + self.get_callstack() + .borrow() + .get_current_element() + .in_expression_evaluation } pub fn set_in_expression_evaluation(&self, value: bool) { - self.get_callstack().borrow_mut().get_current_element_mut().in_expression_evaluation = value; + self.get_callstack() + .borrow_mut() + .get_current_element_mut() + .in_expression_evaluation = value; } pub fn push_evaluation_stack(&mut self, obj: Rc) { - if let Some(list) = Value::get_list_value(obj.as_ref()) { let origin_names = list.get_origin_names(); @@ -311,25 +358,25 @@ impl StoryState { for name in &origin_names { let def = self.list_definitions.get_list_definition(name).unwrap(); - if !list.origins.borrow().iter().any(|e| std::ptr::eq(e, def)){ + if !list.origins.borrow().iter().any(|e| std::ptr::eq(e, def)) { list.origins.borrow_mut().push(def.clone()); } } } - + self.evaluation_stack.push(obj); } pub fn push_to_output_stream(&mut self, obj: Rc) { let text = { - let obj = obj.clone(); - match obj.into_any().downcast::() { - Ok(v) => match &v.value { - ValueType::String(s) => Some(s.clone()), - _ => None, - }, - Err(_) => None, - } + let obj = obj.clone(); + match obj.into_any().downcast::() { + Ok(v) => match &v.value { + ValueType::String(s) => Some(s.clone()), + _ => None, + }, + Err(_) => None, + } }; if let Some(s) = text { @@ -353,15 +400,18 @@ impl StoryState { if has_patch { let curr_count = self.visit_count_for_container(container); let new_count = curr_count + 1; - self.patch.as_mut().unwrap().set_visit_count(container, new_count); + self.patch + .as_mut() + .unwrap() + .set_visit_count(container, new_count); } else { let mut count = 0; let container_path_str = container.get_path().to_string(); - + if let Some(&existing_count) = self.visit_counts.get(&container_path_str) { count = existing_count; } - + count += 1; self.visit_counts.insert(container_path_str, count); } @@ -378,19 +428,19 @@ impl StoryState { // )); return 0; } - + if let Some(patch) = &self.patch { if let Some(visit_count) = patch.get_visit_count(container) { return visit_count; } } - + let container_path_str = container.get_path().to_string(); - + if let Some(&count) = self.visit_counts.get(&container_path_str) { return count; } - + 0 } @@ -401,7 +451,8 @@ impl StoryState { } let container_path_str = Object::get_path(container).to_string(); - self.turn_indices.insert(container_path_str, self.current_turn_index); + self.turn_indices + .insert(container_path_str, self.current_turn_index); } fn try_splitting_head_tail_whitespace(text: &str) -> Option> { @@ -467,7 +518,8 @@ impl StoryState { if tail_last_newline_idx < text.len() as i32 - 1 { let num_spaces = (text.len() as i32 - tail_last_newline_idx) - 1; let trailing_spaces = Value::new_string( - &text[(tail_last_newline_idx + 1) as usize..(num_spaces + tail_last_newline_idx + 1) as usize], + &text[(tail_last_newline_idx + 1) as usize + ..(num_spaces + tail_last_newline_idx + 1) as usize], ); list_texts.push(trailing_spaces); } @@ -480,13 +532,12 @@ impl StoryState { let glue = obj.clone().into_any().downcast::(); let text = Value::get_string_value(obj.as_ref()); let mut include_in_output = true; - + // New glue, so chomp away any whitespace from the end of the stream if glue.is_ok() { self.trim_newlines_from_output_stream(); include_in_output = 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 @@ -495,14 +546,15 @@ impl StoryState { else if let Some(text) = text { let mut function_trim_index = -1; - { // block to release cs borrow + { + // block to release cs borrow let cs = self.get_callstack().borrow(); let curr_el = cs.get_current_element(); if curr_el.push_pop_type == PushPopType::Function { function_trim_index = curr_el.function_start_in_output_stream; } } - + let mut glue_trim_index = -1; for (i, o) in self.get_output_stream().iter().rev().enumerate() { if let Some(c) = o.as_ref().as_any().downcast_ref::() { @@ -518,7 +570,7 @@ impl StoryState { break; } } - + let trim_index; if glue_trim_index != -1 && function_trim_index != -1 { trim_index = function_trim_index.min(glue_trim_index); @@ -527,8 +579,8 @@ impl StoryState { } else { trim_index = function_trim_index; } - - if trim_index != -1 { + + if trim_index != -1 { if text.is_newline { include_in_output = false; } else if text.is_non_whitespace() { @@ -550,11 +602,13 @@ impl StoryState { } } } - } else if text.is_newline && (self.output_stream_ends_in_newline() || !self.output_stream_contains_content()) { + } else if text.is_newline + && (self.output_stream_ends_in_newline() || !self.output_stream_contains_content()) + { include_in_output = false; } } - + if include_in_output { self.get_output_stream_mut().push(obj); self.output_stream_dirty(); @@ -578,7 +632,6 @@ impl StoryState { if obj.as_ref().as_any().is::() { break; } else if let Some(sv) = Value::get_string_value(obj.as_ref()) { - if sv.is_non_whitespace() { break; } else if sv.is_newline { @@ -593,7 +646,7 @@ impl StoryState { if remove_whitespace_from >= 0 { i = remove_whitespace_from; while i < output_stream.len() as i32 { - if let Some(text) = Value::get_string_value(output_stream[i as usize].as_ref()) { + if let Some(text) = Value::get_string_value(output_stream[i as usize].as_ref()) { output_stream.remove(i as usize); } else { i += 1; @@ -630,20 +683,35 @@ impl StoryState { } } } - + false } pub fn set_previous_pointer(&self, p: Pointer) { - self.get_callstack().as_ref().borrow_mut().get_current_thread_mut().previous_pointer = p.clone(); + self.get_callstack() + .as_ref() + .borrow_mut() + .get_current_thread_mut() + .previous_pointer = p.clone(); } pub fn get_previous_pointer(&self) -> Pointer { - self.get_callstack().as_ref().borrow_mut().get_current_thread_mut().previous_pointer.clone() + self.get_callstack() + .as_ref() + .borrow_mut() + .get_current_thread_mut() + .previous_pointer + .clone() } pub fn try_exit_function_evaluation_from_game(&mut self) -> bool { - if self.get_callstack().borrow().get_current_element().push_pop_type == PushPopType::FunctionEvaluationFromGame { + if self + .get_callstack() + .borrow() + .get_current_element() + .push_pop_type + == PushPopType::FunctionEvaluationFromGame + { self.set_current_pointer(pointer::NULL.clone()); self.did_safe_exit = true; return true; @@ -654,13 +722,25 @@ impl StoryState { pub fn pop_callstack(&mut self, t: Option) -> Result<(), StoryError> { // Add the end of a function call, trim any whitespace from the end. - if self.get_callstack().borrow().get_current_element().push_pop_type == PushPopType::Function {self.trim_whitespace_from_function_end();} + if self + .get_callstack() + .borrow() + .get_current_element() + .push_pop_type + == PushPopType::Function + { + self.trim_whitespace_from_function_end(); + } self.get_callstack().borrow_mut().pop(t) } fn go_to_start(&self) { - self.get_callstack().as_ref().borrow_mut().get_current_element_mut().current_pointer = Pointer::start_of(self.main_content_container.clone()) + self.get_callstack() + .as_ref() + .borrow_mut() + .get_current_element_mut() + .current_pointer = Pointer::start_of(self.main_content_container.clone()) } pub fn get_current_choices(&self) -> Option<&Vec>> { @@ -675,7 +755,10 @@ impl StoryState { } pub fn copy_and_start_patching(&self) -> StoryState { - let mut copy = StoryState::new(self.main_content_container.clone(), self.list_definitions.clone()); + let mut copy = StoryState::new( + self.main_content_container.clone(), + self.list_definitions.clone(), + ); copy.patch = Some(StatePatch::new(self.patch.as_ref())); @@ -683,7 +766,9 @@ impl StoryState { // If the patch is applied, then this new flow will replace the old one in // _namedFlows copy.current_flow.name = self.current_flow.name.clone(); - copy.current_flow.callstack = Rc::new(RefCell::new(CallStack::new_from(&self.current_flow.callstack.as_ref().borrow()))); + copy.current_flow.callstack = Rc::new(RefCell::new(CallStack::new_from( + &self.current_flow.callstack.as_ref().borrow(), + ))); copy.current_flow.current_choices = self.current_flow.current_choices.clone(); copy.current_flow.output_stream = self.current_flow.output_stream.clone(); copy.output_stream_dirty(); @@ -694,7 +779,10 @@ impl StoryState { // the above copy is simply the default flow copy and we're done) if let Some(named_flows) = &self.named_flows { let mut nf = self.named_flows.clone(); - nf.as_mut().unwrap().insert(copy.current_flow.name.to_string(), copy.current_flow.clone()); + nf.as_mut().unwrap().insert( + copy.current_flow.name.to_string(), + copy.current_flow.clone(), + ); copy.alive_flow_names_dirty = true; copy.named_flows = nf; @@ -706,14 +794,14 @@ impl StoryState { if self.has_warning() { copy.current_warnings = self.current_warnings.clone(); - } // 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) + // (though the callstack will be modified) copy.variables_state = self.variables_state.clone(); - copy.variables_state.set_callstack(copy.get_callstack().clone()); + copy.variables_state + .set_callstack(copy.get_callstack().clone()); copy.variables_state.patch = copy.patch.clone(); copy.evaluation_stack = self.evaluation_stack.clone(); @@ -751,24 +839,28 @@ impl StoryState { if self.patch.is_none() { return; } - + self.variables_state.apply_patch(); - + if self.patch.is_some() { for (path, count) in self.patch.as_ref().unwrap().visit_counts.clone().iter() { self.apply_count_changes(path, *count, true); } - + for (path, index) in self.patch.as_ref().unwrap().turn_indices.clone().iter() { self.apply_count_changes(path, *index, false); } } - + self.patch = None; } fn apply_count_changes(&mut self, container: &str, new_count: i32, is_visit: bool) { - let counts = if is_visit {&mut self.visit_counts} else {&mut self.turn_indices}; + let counts = if is_visit { + &mut self.visit_counts + } else { + &mut self.turn_indices + }; counts.insert(container.to_string(), new_count); } @@ -788,10 +880,13 @@ impl StoryState { self.evaluation_stack.pop().unwrap() } - pub fn pop_evaluation_stack_multiple(&mut self, number_of_objects: usize) -> Vec> { + pub fn pop_evaluation_stack_multiple( + &mut self, + number_of_objects: usize, + ) -> Vec> { let start = self.evaluation_stack.len() - number_of_objects; let obj: Vec> = self.evaluation_stack.drain(start..).collect(); - + obj } @@ -799,7 +894,11 @@ impl StoryState { self.diverted_pointer = p; } - pub fn set_chosen_path(&mut self, path: &Path, incrementing_turn_index: bool) -> Result<(), StoryError> { + pub fn set_chosen_path( + &mut self, + path: &Path, + incrementing_turn_index: bool, + ) -> Result<(), StoryError> { // Changing direction, assume we need to clear current set of choices self.current_flow.current_choices.clear(); @@ -834,22 +933,30 @@ impl StoryState { // whitespace is trimmed in one go here when we pop the function. fn trim_whitespace_from_function_end(&mut self) { assert_eq!( - self.get_callstack().borrow().get_current_element().push_pop_type, + self.get_callstack() + .borrow() + .get_current_element() + .push_pop_type, PushPopType::Function ); - - let function_start_point = match self.get_callstack().borrow().get_current_element().function_start_in_output_stream { + + let function_start_point = match self + .get_callstack() + .borrow() + .get_current_element() + .function_start_in_output_stream + { -1 => 0, start_point => start_point, }; - + // Trim whitespace from END of function call let mut i = self.get_output_stream().len() as isize - 1; while i >= function_start_point as isize { if let Some(obj) = self.get_output_stream().get(i as usize) { if obj.as_any().is::() { break; - } + } if let Some(txt) = Value::get_string_value(obj.as_ref()) { if txt.is_newline || txt.is_inline_whitespace { @@ -868,16 +975,30 @@ impl StoryState { self.evaluation_stack.last() } - pub fn start_function_evaluation_from_game(&mut self, func_container: Rc, arguments: Option<&Vec>) -> Result<(), StoryError> { - self.get_callstack().borrow_mut().push(PushPopType::FunctionEvaluationFromGame, self.evaluation_stack.len(), 0); - self.get_callstack().borrow_mut().get_current_element_mut().current_pointer = Pointer::start_of(func_container); + pub fn start_function_evaluation_from_game( + &mut self, + func_container: Rc, + arguments: Option<&Vec>, + ) -> Result<(), StoryError> { + self.get_callstack().borrow_mut().push( + PushPopType::FunctionEvaluationFromGame, + self.evaluation_stack.len(), + 0, + ); + self.get_callstack() + .borrow_mut() + .get_current_element_mut() + .current_pointer = Pointer::start_of(func_container); self.pass_arguments_to_evaluation_stack(arguments)?; Ok(()) } - pub fn pass_arguments_to_evaluation_stack(&mut self, arguments: Option<&Vec>) -> Result<(), StoryError> { + pub fn pass_arguments_to_evaluation_stack( + &mut self, + arguments: Option<&Vec>, + ) -> Result<(), StoryError> { // Pass arguments onto the evaluation stack if let Some(arguments) = arguments { for arg in arguments { @@ -887,23 +1008,40 @@ impl StoryState { ValueType::Float(v) => Value::new_float(*v), ValueType::List(v) => Value::new_list(v.clone()), ValueType::String(v) => Value::new_string(&v.string), - _ => {return Err(StoryError::InvalidStoryState("ink arguments when calling EvaluateFunction / ChoosePathStringWithParameters must be \ - int, float, string, bool or InkList.".to_owned()));} + _ => { + return Err(StoryError::InvalidStoryState("ink arguments when calling EvaluateFunction / ChoosePathStringWithParameters must be \ + int, float, string, bool or InkList.".to_owned())); + } }; self.push_evaluation_stack(Rc::new(value)); } } - + Ok(()) } - pub fn complete_function_evaluation_from_game(&mut self) -> Result, StoryError> { - if self.get_callstack().borrow().get_current_element().push_pop_type != PushPopType::FunctionEvaluationFromGame { - return Err(StoryError::InvalidStoryState(format!("Expected external function evaluation to be complete. Stack trace: {}", self.get_callstack().borrow().get_callstack_trace()))); - } - - let original_evaluation_stack_height = self.get_callstack().borrow().get_current_element().evaluation_stack_height_when_pushed; + pub fn complete_function_evaluation_from_game( + &mut self, + ) -> Result, StoryError> { + if self + .get_callstack() + .borrow() + .get_current_element() + .push_pop_type + != PushPopType::FunctionEvaluationFromGame + { + return Err(StoryError::InvalidStoryState(format!( + "Expected external function evaluation to be complete. Stack trace: {}", + self.get_callstack().borrow().get_callstack_trace() + ))); + } + + let original_evaluation_stack_height = self + .get_callstack() + .borrow() + .get_current_element() + .evaluation_stack_height_when_pushed; // Do we have a returned value? // Potentially pop multiple values off the stack, in case we need @@ -919,11 +1057,15 @@ impl StoryState { } // Finally, pop the external function evaluation - self.get_callstack().borrow_mut().pop(Some(PushPopType::FunctionEvaluationFromGame))?; + self.get_callstack() + .borrow_mut() + .pop(Some(PushPopType::FunctionEvaluationFromGame))?; // What did we get back? - if let Some(returned_obj) = returned_obj{ - if returned_obj.as_ref().as_any().is::() { return Ok(None); } + if let Some(returned_obj) = returned_obj { + if returned_obj.as_ref().as_any().is::() { + return Ok(None); + } // Some kind of value, if not void if let Some(return_val) = returned_obj.as_ref().as_any().downcast_ref::() { @@ -937,18 +1079,36 @@ impl StoryState { // int, float, string. VariablePointers get returned as strings. return Ok(Some(return_val.value.clone())); } - } + } - Ok(None) + Ok(None) } - pub(crate) fn turns_since_for_container(&self, container: &Container) -> Result { + pub(crate) fn turns_since_for_container( + &self, + container: &Container, + ) -> Result { if !container.turn_index_should_be_counted { - return Err(StoryError::InvalidStoryState(format!("TURNS_SINCE() for target ({}) unknown.", container.name.as_ref().unwrap()))); - } - - if self.patch.is_some() && self.patch.as_ref().unwrap().get_turn_index(container).is_some() { - let index = *self.patch.as_ref().unwrap().get_turn_index(container).unwrap(); + return Err(StoryError::InvalidStoryState(format!( + "TURNS_SINCE() for target ({}) unknown.", + container.name.as_ref().unwrap() + ))); + } + + if self.patch.is_some() + && self + .patch + .as_ref() + .unwrap() + .get_turn_index(container) + .is_some() + { + let index = *self + .patch + .as_ref() + .unwrap() + .get_turn_index(container) + .unwrap(); return Ok(self.current_turn_index - index); } @@ -959,11 +1119,10 @@ impl StoryState { Ok(self.current_turn_index - index) } else { Ok(-1) - } + } } pub(crate) fn switch_flow_internal(&mut self, flow_name: &str) { - if flow_name.eq(&self.current_flow.name) { return; } @@ -988,7 +1147,8 @@ impl StoryState { std::mem::swap(&mut self.current_flow, &mut next_flow); named_flows.insert(next_flow.name.clone(), next_flow); - self.variables_state.set_callstack(self.current_flow.callstack.clone()); + self.variables_state + .set_callstack(self.current_flow.callstack.clone()); // Cause text to be regenerated from output stream if necessary self.output_stream_dirty(); @@ -998,15 +1158,31 @@ impl StoryState { let mut visit_count_out; if self.patch.is_some() { - let container = self.main_content_container.content_at_path(&Path::new_with_components_string(Some(path_string)), 0, -1).container(); - if container.is_none() { return Err(StoryError::InvalidStoryState(format!("Content at path not found: {}", path_string)));} + let container = self + .main_content_container + .content_at_path(&Path::new_with_components_string(Some(path_string)), 0, -1) + .container(); + if container.is_none() { + return Err(StoryError::InvalidStoryState(format!( + "Content at path not found: {}", + path_string + ))); + } - visit_count_out = self.patch.as_ref().unwrap().get_visit_count(container.as_ref().unwrap()); - if let Some(visit_count_out) = visit_count_out {return Ok(visit_count_out);} + visit_count_out = self + .patch + .as_ref() + .unwrap() + .get_visit_count(container.as_ref().unwrap()); + if let Some(visit_count_out) = visit_count_out { + return Ok(visit_count_out); + } } visit_count_out = self.visit_counts.get(path_string).copied(); - if let Some(visit_count_out) = visit_count_out {return Ok(visit_count_out);} + if let Some(visit_count_out) = visit_count_out { + return Ok(visit_count_out); + } Ok(0) } @@ -1029,33 +1205,54 @@ impl StoryState { let mut flows: Map = Map::new(); // current flow - flows.insert(self.current_flow.name.clone(), self.current_flow.write_json()?); + flows.insert( + self.current_flow.name.clone(), + self.current_flow.write_json()?, + ); // named flows if let Some(named_flows) = &self.named_flows { - for (k,v) in named_flows { + for (k, v) in named_flows { flows.insert(k.clone(), v.write_json()?); } } obj.insert("flows".to_owned(), serde_json::Value::Object(flows)); - obj.insert("currentFlowName".to_owned(), json!(self.current_flow.name)); - obj.insert("variablesState".to_owned(), self.variables_state.write_json()?); - obj.insert("evalStack".to_owned(), json_write::write_list_rt_objs(&self.evaluation_stack)?); + obj.insert( + "variablesState".to_owned(), + self.variables_state.write_json()?, + ); + obj.insert( + "evalStack".to_owned(), + json_write::write_list_rt_objs(&self.evaluation_stack)?, + ); if !self.diverted_pointer.is_null() { - obj.insert("currentDivertTarget".to_owned(), json!(self.diverted_pointer.get_path().unwrap().get_components_string())); - } - - obj.insert("visitCounts".to_owned(), json_write::write_int_dictionary(&self.visit_counts)); - obj.insert("turnIndices".to_owned(), json_write::write_int_dictionary(&self.turn_indices)); + obj.insert( + "currentDivertTarget".to_owned(), + json!(self + .diverted_pointer + .get_path() + .unwrap() + .get_components_string()), + ); + } + + obj.insert( + "visitCounts".to_owned(), + json_write::write_int_dictionary(&self.visit_counts), + ); + obj.insert( + "turnIndices".to_owned(), + json_write::write_int_dictionary(&self.turn_indices), + ); obj.insert("turnIdx".to_owned(), json!(self.current_turn_index)); obj.insert("storySeed".to_owned(), json!(self.story_seed)); obj.insert("previousRandom".to_owned(), json!(self.previous_random)); - + obj.insert("inkSaveVersion".to_owned(), json!(INK_SAVE_STATE_VERSION)); // Not using this right now, but could do in future. @@ -1067,7 +1264,11 @@ impl StoryState { fn load_json_obj(&mut self, j_object: serde_json::Value) -> Result<(), StoryError> { let j_save_version = match j_object.get("inkSaveVersion") { Some(version) => version, - None => return Err(StoryError::BadJson("ink save format incorrect, can't load.".to_owned())), + None => { + return Err(StoryError::BadJson( + "ink save format incorrect, can't load.".to_owned(), + )) + } }; if let Some(version) = j_save_version.as_i64() { @@ -1083,7 +1284,9 @@ impl StoryState { // Flows: Always exists in latest format (even if there's just one default) // but this dictionary doesn't exist in prev format if let Some(flows_obj) = j_object.get("flows") { - let flows_obj_dict = flows_obj.as_object().ok_or_else(|| StoryError::BadJson("Invalid flows object".to_string()))?; + let flows_obj_dict = flows_obj + .as_object() + .ok_or_else(|| StoryError::BadJson("Invalid flows object".to_string()))?; // Single default flow if flows_obj_dict.len() == 1 { @@ -1101,17 +1304,22 @@ impl StoryState { // Load up each flow (there may only be one) for (named_flow_name, named_flow_obj) in flows_obj_dict.iter() { let name = named_flow_name.clone(); - let flow_obj = named_flow_obj.as_object().ok_or_else(|| StoryError::BadJson("Invalid flow object".to_string()))?; + let flow_obj = named_flow_obj + .as_object() + .ok_or_else(|| StoryError::BadJson("Invalid flow object".to_string()))?; // Load up this flow using JSON data let flow = Flow::from_json(&name, self.main_content_container.clone(), flow_obj)?; if flows_obj_dict.len() == 1 { - self.current_flow = Flow::from_json(&name, self.main_content_container.clone(), flow_obj)?; + self.current_flow = + Flow::from_json(&name, self.main_content_container.clone(), flow_obj)?; } else { self.named_flows .as_mut() - .ok_or_else(|| StoryError::BadJson("Named flows should be initialized".to_string()))? + .ok_or_else(|| { + StoryError::BadJson("Named flows should be initialized".to_string()) + })? .insert(name, flow); } } @@ -1134,58 +1342,95 @@ impl StoryState { else { self.named_flows = None; self.current_flow.name = "default".to_owned(); // Replace with the default flow name - self.current_flow - .callstack - .borrow_mut().load_json(&self.main_content_container, j_object.get("callstackThreads").and_then(|o| o.as_object()).ok_or(StoryError::BadJson("loading callstack threads".to_owned()))?)?; + self.current_flow.callstack.borrow_mut().load_json( + &self.main_content_container, + j_object + .get("callstackThreads") + .and_then(|o| o.as_object()) + .ok_or(StoryError::BadJson("loading callstack threads".to_owned()))?, + )?; if let Some(output_stream_obj) = j_object.get("outputStream") { - self.current_flow.output_stream = json_read::jarray_to_runtime_obj_list(output_stream_obj.as_array().unwrap(), false)?; + self.current_flow.output_stream = json_read::jarray_to_runtime_obj_list( + output_stream_obj.as_array().unwrap(), + false, + )?; } if let Some(current_choices_obj) = j_object.get("currentChoices") { - self.current_flow.current_choices = json_read::jarray_to_runtime_obj_list(current_choices_obj.as_array().unwrap(), false)?.iter().map(|o| o.clone().into_any().downcast::().unwrap()).collect(); + self.current_flow.current_choices = json_read::jarray_to_runtime_obj_list( + current_choices_obj.as_array().unwrap(), + false, + )? + .iter() + .map(|o| o.clone().into_any().downcast::().unwrap()) + .collect(); } let j_choice_threads_obj = j_object.get("choiceThreads"); - self.current_flow.load_flow_choice_threads(j_choice_threads_obj, self.main_content_container.clone())?; + self.current_flow.load_flow_choice_threads( + j_choice_threads_obj, + self.main_content_container.clone(), + )?; } self.output_stream_dirty(); self.alive_flow_names_dirty = true; if let Some(variables_state_obj) = j_object.get("variablesState") { - self.variables_state.load_json(variables_state_obj.as_object().ok_or_else(|| StoryError::BadJson("Invalid variables state object".to_string()))?)?; - self.variables_state.set_callstack(self.current_flow.callstack.clone()); + self.variables_state + .load_json(variables_state_obj.as_object().ok_or_else(|| { + StoryError::BadJson("Invalid variables state object".to_string()) + })?)?; + self.variables_state + .set_callstack(self.current_flow.callstack.clone()); } if let Some(eval_stack_obj) = j_object.get("evalStack") { - self.evaluation_stack = json_read::jarray_to_runtime_obj_list(eval_stack_obj.as_array().unwrap(), false)?; + self.evaluation_stack = + json_read::jarray_to_runtime_obj_list(eval_stack_obj.as_array().unwrap(), false)?; } if let Some(current_divert_target_path) = j_object.get("currentDivertTarget") { let divert_path = Path::new_with_components_string(current_divert_target_path.as_str()); - self.diverted_pointer = Story::pointer_at_path(&self.main_content_container, &divert_path)?.clone(); + self.diverted_pointer = + Story::pointer_at_path(&self.main_content_container, &divert_path)?.clone(); } if let Some(visit_counts_obj) = j_object.get("visitCounts") { - self.visit_counts = json_read::jobject_to_int_hashmap(visit_counts_obj.as_object().ok_or_else(|| StoryError::BadJson("Invalid visit counts object".to_string()))?)?; + self.visit_counts = + json_read::jobject_to_int_hashmap(visit_counts_obj.as_object().ok_or_else( + || StoryError::BadJson("Invalid visit counts object".to_string()), + )?)?; } if let Some(turn_indices_obj) = j_object.get("turnIndices") { - self.turn_indices = json_read::jobject_to_int_hashmap(turn_indices_obj.as_object().ok_or_else(|| StoryError::BadJson("Invalid turn indices object".to_string()))?)?; + self.turn_indices = + json_read::jobject_to_int_hashmap(turn_indices_obj.as_object().ok_or_else( + || StoryError::BadJson("Invalid turn indices object".to_string()), + )?)?; } if let Some(current_turn_index) = j_object.get("turnIdx") { - self.current_turn_index = current_turn_index.as_i64().ok_or_else(|| StoryError::BadJson("Invalid current turn index".to_string()))? as i32; + self.current_turn_index = current_turn_index + .as_i64() + .ok_or_else(|| StoryError::BadJson("Invalid current turn index".to_string()))? + as i32; } if let Some(story_seed) = j_object.get("storySeed") { - self.story_seed = story_seed.as_i64().ok_or_else(|| StoryError::BadJson("Invalid story seed".to_string()))? as i32; + self.story_seed = story_seed + .as_i64() + .ok_or_else(|| StoryError::BadJson("Invalid story seed".to_string()))? + as i32; } // Not optional, but bug in inkjs means it's actually missing in inkjs saves if let Some(previous_random_obj) = j_object.get("previousRandom") { - self.previous_random = previous_random_obj.as_i64().ok_or_else(|| StoryError::BadJson("Invalid previous random value".to_string()))? as i32; + self.previous_random = previous_random_obj + .as_i64() + .ok_or_else(|| StoryError::BadJson("Invalid previous random value".to_string()))? + as i32; } else { self.previous_random = 0; } @@ -1194,7 +1439,11 @@ impl StoryState { } pub(crate) fn remove_flow_internal(&mut self, flow_name: &str) -> Result<(), StoryError> { - if flow_name.eq(DEFAULT_FLOW_NAME) {return Err(StoryError::BadArgument("Cannot destroy default flow".to_owned()));} + if flow_name.eq(DEFAULT_FLOW_NAME) { + return Err(StoryError::BadArgument( + "Cannot destroy default flow".to_owned(), + )); + } // If we're currently in the flow that's being removed, switch back to default if self.current_flow.name.eq(flow_name) { @@ -1202,8 +1451,8 @@ impl StoryState { } self.named_flows.as_mut().unwrap().remove(flow_name); - self.alive_flow_names_dirty = true; - + self.alive_flow_names_dirty = true; + Ok(()) } @@ -1223,5 +1472,5 @@ impl StoryState { pub(crate) fn reset_errors(&mut self) { self.current_errors.clear(); - } -} \ No newline at end of file + } +} diff --git a/lib/src/tag.rs b/lib/src/tag.rs index b933cb9..5797b87 100644 --- a/lib/src/tag.rs +++ b/lib/src/tag.rs @@ -9,7 +9,10 @@ pub struct Tag { impl Tag { pub fn new(text: &str) -> Self { - Tag {obj: Object::new(), text: text.to_string()} + Tag { + obj: Object::new(), + text: text.to_string(), + } } pub fn get_text(&self) -> &String { @@ -28,4 +31,4 @@ impl fmt::Display for Tag { let t = &self.text; write!(f, "# {t}") } -} \ No newline at end of file +} diff --git a/lib/src/value.rs b/lib/src/value.rs index a4db677..b843007 100644 --- a/lib/src/value.rs +++ b/lib/src/value.rs @@ -1,6 +1,12 @@ use std::fmt; -use crate::{object::{RTObject, Object}, path::Path, ink_list::InkList, value_type::{StringValue, ValueType, VariablePointerValue}, story_error::StoryError}; +use crate::{ + ink_list::InkList, + object::{Object, RTObject}, + path::Path, + story_error::StoryError, + value_type::{StringValue, ValueType, VariablePointerValue}, +}; const CAST_BOOL: u8 = 0; const CAST_INT: u8 = 1; @@ -36,44 +42,70 @@ impl fmt::Display for Value { } impl Value { - pub fn new(value:ValueType) -> Self { - Self { obj: Object::new(), value } + pub fn new(value: ValueType) -> Self { + Self { + obj: Object::new(), + value, + } } - pub fn new_bool(v:bool) -> Self { - Self { obj: Object::new(), value: ValueType::Bool(v) } + pub fn new_bool(v: bool) -> Self { + Self { + obj: Object::new(), + value: ValueType::Bool(v), + } } - pub fn new_int(v:i32) -> Self { - Self { obj: Object::new(), value: ValueType::Int(v) } + pub fn new_int(v: i32) -> Self { + Self { + obj: Object::new(), + value: ValueType::Int(v), + } } - pub fn new_float(v:f32) -> Self { - Self { obj: Object::new(), value: ValueType::Float(v) } + pub fn new_float(v: f32) -> Self { + Self { + obj: Object::new(), + value: ValueType::Float(v), + } } - pub fn new_string(v:&str) -> Self { - - Self { - obj: Object::new(), + pub fn new_string(v: &str) -> Self { + Self { + obj: Object::new(), value: ValueType::new_string(v), } } - pub fn new_divert_target(p:Path) -> Self { - Self { obj: Object::new(), value: ValueType::DivertTarget(p) } + pub fn new_divert_target(p: Path) -> Self { + Self { + obj: Object::new(), + value: ValueType::DivertTarget(p), + } } pub fn new_variable_pointer(variable_name: &str, context_index: i32) -> Self { - Self { obj: Object::new(), value: ValueType::VariablePointer(VariablePointerValue { variable_name: variable_name.to_string(), context_index }) } + Self { + obj: Object::new(), + value: ValueType::VariablePointer(VariablePointerValue { + variable_name: variable_name.to_string(), + context_index, + }), + } } pub fn new_list(l: InkList) -> Self { - Self { obj: Object::new(), value: ValueType::List(l) } + Self { + obj: Object::new(), + value: ValueType::List(l), + } } pub fn from_value_type(value_type: ValueType) -> Self { - Self { obj: Object::new(), value: value_type } + Self { + obj: Object::new(), + value: value_type, + } } pub fn is_truthy(&self) -> Result { @@ -82,8 +114,12 @@ impl Value { ValueType::Int(v) => Ok(*v != 0), ValueType::Float(v) => Ok(*v != 0.0), ValueType::String(v) => Ok(!v.string.is_empty()), - ValueType::DivertTarget(_) => Err(StoryError::InvalidStoryState("Shouldn't be checking the truthiness of a divert target".to_owned())), - ValueType::VariablePointer(_) => Err(StoryError::InvalidStoryState("Shouldn't be checking the truthiness of a variable pointer".to_owned())), + ValueType::DivertTarget(_) => Err(StoryError::InvalidStoryState( + "Shouldn't be checking the truthiness of a divert target".to_owned(), + )), + ValueType::VariablePointer(_) => Err(StoryError::InvalidStoryState( + "Shouldn't be checking the truthiness of a variable pointer".to_owned(), + )), ValueType::List(l) => Ok(!l.items.is_empty()), } } @@ -179,7 +215,6 @@ impl Value { } pub fn retain_list_origins_for_assignment(old_value: &dyn RTObject, new_value: &dyn RTObject) { - if let Some(old_list) = Self::get_list_value(old_value) { if let Some(new_list) = Self::get_list_value(new_value) { if new_list.items.is_empty() { @@ -193,112 +228,116 @@ impl Value { let v = &self.value; let ptr_to_option = (v as *const ValueType) as *const u8; - unsafe { - *ptr_to_option - } + unsafe { *ptr_to_option } } // If None is returned means that casting is not needed pub fn cast(&self, cast_dest_type: u8) -> Result, StoryError> { match &self.value { - ValueType::Bool(v) => { - match cast_dest_type { - CAST_BOOL => Ok(None), - CAST_INT => if *v { + ValueType::Bool(v) => match cast_dest_type { + CAST_BOOL => Ok(None), + CAST_INT => { + if *v { Ok(Some(Self::new_int(1))) } else { Ok(Some(Self::new_int(0))) - }, - CAST_FLOAT => if *v { + } + } + CAST_FLOAT => { + if *v { Ok(Some(Self::new_float(1.0))) } else { Ok(Some(Self::new_float(0.0))) - }, - CAST_STRING => if *v { + } + } + CAST_STRING => { + if *v { Ok(Some(Self::new_string("true"))) } else { Ok(Some(Self::new_string("false"))) - }, - _ => Err(StoryError::InvalidStoryState("Cast not allowed for bool".to_owned())), + } } + _ => Err(StoryError::InvalidStoryState( + "Cast not allowed for bool".to_owned(), + )), }, - ValueType::Int(v) => { - match cast_dest_type { - CAST_BOOL => if *v == 0 { + ValueType::Int(v) => match cast_dest_type { + CAST_BOOL => { + if *v == 0 { Ok(Some(Self::new_bool(false))) } else { Ok(Some(Self::new_bool(true))) - }, - CAST_INT => Ok(None), - CAST_FLOAT => Ok(Some(Self::new_float(*v as f32))), - CAST_STRING => Ok(Some(Self::new_string(&v.to_string()))), - _ => Err(StoryError::InvalidStoryState("Cast not allowed for int".to_owned())), + } } + CAST_INT => Ok(None), + CAST_FLOAT => Ok(Some(Self::new_float(*v as f32))), + CAST_STRING => Ok(Some(Self::new_string(&v.to_string()))), + _ => Err(StoryError::InvalidStoryState( + "Cast not allowed for int".to_owned(), + )), }, - ValueType::Float(v) => { - match cast_dest_type { - CAST_BOOL => if *v == 0.0 { + ValueType::Float(v) => match cast_dest_type { + CAST_BOOL => { + if *v == 0.0 { Ok(Some(Self::new_bool(false))) } else { Ok(Some(Self::new_bool(true))) - }, - CAST_INT => Ok(Some(Self::new_int(*v as i32))), - CAST_FLOAT => Ok(None), - CAST_STRING => Ok(Some(Self::new_string(&v.to_string()))), - _ => Err(StoryError::InvalidStoryState("Cast not allowed for float".to_owned())), + } } + CAST_INT => Ok(Some(Self::new_int(*v as i32))), + CAST_FLOAT => Ok(None), + CAST_STRING => Ok(Some(Self::new_string(&v.to_string()))), + _ => Err(StoryError::InvalidStoryState( + "Cast not allowed for float".to_owned(), + )), }, - ValueType::String(v) => { - match cast_dest_type { - CAST_INT => Ok(Some(Self::new_int(v.string.parse::().unwrap()))), - CAST_FLOAT => Ok(Some(Self::new_float(v.string.parse::().unwrap()))), - CAST_STRING => Ok(None), - _ => Err(StoryError::InvalidStoryState("Cast not allowed for string".to_owned())), - } + ValueType::String(v) => match cast_dest_type { + CAST_INT => Ok(Some(Self::new_int(v.string.parse::().unwrap()))), + CAST_FLOAT => Ok(Some(Self::new_float(v.string.parse::().unwrap()))), + CAST_STRING => Ok(None), + _ => Err(StoryError::InvalidStoryState( + "Cast not allowed for string".to_owned(), + )), }, - ValueType::List(l) => { - match cast_dest_type { - CAST_INT => { - let max = l.get_max_item(); - match max { - Some(i) => Ok(Some(Self::new_int(i.1))), - None => Ok(Some(Self::new_int(0))) - } - }, - CAST_FLOAT => { - let max = l.get_max_item(); - match max { - Some(i) => Ok(Some(Self::new_float(i.1 as f32))), - None => Ok(Some(Self::new_float(0.0))) - } - }, - CAST_LIST => Ok(None), - CAST_STRING => { - let max = l.get_max_item(); - match max { - Some(i) => Ok(Some(Self::new_string(&i.0.to_string()))), - None => Ok(Some(Self::new_string(""))) - } - }, - _ => Err(StoryError::InvalidStoryState("Cast not allowed for list".to_owned())), + ValueType::List(l) => match cast_dest_type { + CAST_INT => { + let max = l.get_max_item(); + match max { + Some(i) => Ok(Some(Self::new_int(i.1))), + None => Ok(Some(Self::new_int(0))), + } } - }, - ValueType::DivertTarget(_) => { - match cast_dest_type { - CAST_DIVERT_TARGET => { - Ok(None) - }, - _ => Err(StoryError::InvalidStoryState("Cast not allowed for divert".to_owned())), + CAST_FLOAT => { + let max = l.get_max_item(); + match max { + Some(i) => Ok(Some(Self::new_float(i.1 as f32))), + None => Ok(Some(Self::new_float(0.0))), + } } - }, - ValueType::VariablePointer(_) => { - match cast_dest_type { - CAST_VARIABLE_POINTER => { - Ok(None) - }, - _ => Err(StoryError::InvalidStoryState("Cast not allowed for variable pointer".to_owned())), + CAST_LIST => Ok(None), + CAST_STRING => { + let max = l.get_max_item(); + match max { + Some(i) => Ok(Some(Self::new_string(&i.0.to_string()))), + None => Ok(Some(Self::new_string(""))), + } } + _ => Err(StoryError::InvalidStoryState( + "Cast not allowed for list".to_owned(), + )), + }, + ValueType::DivertTarget(_) => match cast_dest_type { + CAST_DIVERT_TARGET => Ok(None), + _ => Err(StoryError::InvalidStoryState( + "Cast not allowed for divert".to_owned(), + )), + }, + ValueType::VariablePointer(_) => match cast_dest_type { + CAST_VARIABLE_POINTER => Ok(None), + _ => Err(StoryError::InvalidStoryState( + "Cast not allowed for variable pointer".to_owned(), + )), }, } } -} \ No newline at end of file +} diff --git a/lib/src/value_type.rs b/lib/src/value_type.rs index 0e30ee6..93a37fb 100644 --- a/lib/src/value_type.rs +++ b/lib/src/value_type.rs @@ -1,5 +1,4 @@ - -use crate::{path::Path, ink_list::InkList, story_error::StoryError}; +use crate::{ink_list::InkList, path::Path, story_error::StoryError}; #[repr(u8)] #[derive(Clone)] @@ -23,11 +22,12 @@ impl ValueType { break; } } - + ValueType::String(StringValue { - string: str.to_string(), - is_inline_whitespace: inline_ws, - is_newline: str.eq("\n")}) + string: str.to_string(), + is_inline_whitespace: inline_ws, + is_newline: str.eq("\n"), + }) } pub fn get_bool(&self) -> Option { @@ -60,7 +60,13 @@ impl ValueType { pub fn coerce_to_int(&self) -> Result { match self { - ValueType::Bool(v) => if *v {Ok(1)} else {Ok(0)}, + ValueType::Bool(v) => { + if *v { + Ok(1) + } else { + Ok(0) + } + } ValueType::Int(v) => Ok(*v), ValueType::Float(v) => Ok(*v as i32), _ => Err(StoryError::BadArgument("Failed to cast to int".to_owned())), @@ -69,18 +75,34 @@ impl ValueType { pub fn coerce_to_float(&self) -> Result { match self { - ValueType::Bool(v) => if *v {Ok(1.0)} else {Ok(0.0)}, + ValueType::Bool(v) => { + if *v { + Ok(1.0) + } else { + Ok(0.0) + } + } ValueType::Int(v) => Ok(*v as f32), ValueType::Float(v) => Ok(*v), - _ => Err(StoryError::BadArgument("Failed to cast to float".to_owned())), + _ => Err(StoryError::BadArgument( + "Failed to cast to float".to_owned(), + )), } } pub fn coerce_to_bool(&self) -> Result { match self { ValueType::Bool(v) => Ok(*v), - ValueType::Int(v) => if *v == 1 {Ok(true)} else {Ok(false)}, - _ => Err(StoryError::BadArgument("Failed to cast to boolean".to_owned())), + ValueType::Int(v) => { + if *v == 1 { + Ok(true) + } else { + Ok(false) + } + } + _ => Err(StoryError::BadArgument( + "Failed to cast to boolean".to_owned(), + )), } } @@ -90,17 +112,18 @@ impl ValueType { ValueType::Int(v) => Ok(v.to_string()), ValueType::Float(v) => Ok(v.to_string()), ValueType::String(v) => Ok(v.string.clone()), - _ => Err(StoryError::BadArgument("Failed to cast to float".to_owned())), + _ => Err(StoryError::BadArgument( + "Failed to cast to float".to_owned(), + )), } } } - #[derive(Clone)] pub struct StringValue { pub string: String, pub is_inline_whitespace: bool, - pub is_newline: bool + pub is_newline: bool, } impl StringValue { diff --git a/lib/src/variable_assigment.rs b/lib/src/variable_assigment.rs index 3638abe..cbebda4 100644 --- a/lib/src/variable_assigment.rs +++ b/lib/src/variable_assigment.rs @@ -30,4 +30,4 @@ impl fmt::Display for VariableAssignment { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "VarAssign to {}", self.variable_name) } -} \ No newline at end of file +} diff --git a/lib/src/variable_reference.rs b/lib/src/variable_reference.rs index 68ce966..bee9c0d 100644 --- a/lib/src/variable_reference.rs +++ b/lib/src/variable_reference.rs @@ -1,33 +1,48 @@ use std::{fmt, rc::Rc}; -use crate::{object::{Object, RTObject}, path::Path, container::Container}; - +use crate::{ + container::Container, + object::{Object, RTObject}, + path::Path, +}; pub struct VariableReference { obj: Object, pub name: String, - pub path_for_count: Option, + pub path_for_count: Option, } impl VariableReference { pub fn new(name: &str) -> Self { - VariableReference {obj: Object::new(), name: name.to_string(), path_for_count: None} + VariableReference { + obj: Object::new(), + name: name.to_string(), + path_for_count: None, + } } pub fn from_path_for_count(path_for_count: &str) -> Self { - VariableReference {obj: Object::new(), name: String::new(), path_for_count: Some(Path::new_with_components_string(Some(path_for_count)))} + VariableReference { + obj: Object::new(), + name: String::new(), + path_for_count: Some(Path::new_with_components_string(Some(path_for_count))), + } } pub fn get_container_for_count(self: &Rc) -> Result, String> { if let Some(path) = &self.path_for_count { - Ok(Object::resolve_path(self.clone(), path).container().unwrap()) + Ok(Object::resolve_path(self.clone(), path) + .container() + .unwrap()) } else { Err("Path for count is not set.".to_owned()) } } pub fn get_path_string_for_count(self: &Rc) -> Option { - self.path_for_count.as_ref().map(|path_for_count| Object::compact_path_string(self.clone(), path_for_count)) + self.path_for_count + .as_ref() + .map(|path_for_count| Object::compact_path_string(self.clone(), path_for_count)) } } @@ -47,4 +62,4 @@ impl fmt::Display for VariableReference { }, } } -} \ No newline at end of file +} diff --git a/lib/src/variables_state.rs b/lib/src/variables_state.rs index a30658a..2a54524 100644 --- a/lib/src/variables_state.rs +++ b/lib/src/variables_state.rs @@ -1,9 +1,21 @@ -use std::{collections::{HashMap, HashSet}, rc::Rc, cell::RefCell}; +use std::{ + cell::RefCell, + collections::{HashMap, HashSet}, + rc::Rc, +}; use serde_json::Map; -use crate::{callstack::CallStack, state_patch::StatePatch, variable_assigment::VariableAssignment, value::Value, list_definitions_origin::ListDefinitionsOrigin, value_type::{VariablePointerValue, ValueType}, json_write, json_read, story_error::StoryError}; - +use crate::{ + callstack::CallStack, + json_read, json_write, + list_definitions_origin::ListDefinitionsOrigin, + state_patch::StatePatch, + story_error::StoryError, + value::Value, + value_type::{ValueType, VariablePointerValue}, + variable_assigment::VariableAssignment, +}; #[derive(Clone)] pub(crate) struct VariablesState { @@ -17,7 +29,10 @@ pub(crate) struct VariablesState { } impl VariablesState { - pub fn new(callstack: Rc>, list_defs_origin: Rc) -> VariablesState { + pub fn new( + callstack: Rc>, + list_defs_origin: Rc, + ) -> VariablesState { VariablesState { global_variables: HashMap::new(), default_global_variables: HashMap::new(), @@ -31,10 +46,10 @@ impl VariablesState { pub fn start_batch_observing_variable_changes(&mut self) { self.batch_observing_variable_changes = true; - self.changed_variables_for_batch_obs = Some(HashSet::new()); + self.changed_variables_for_batch_obs = Some(HashSet::new()); } - pub fn stop_batch_observing_variable_changes(&mut self) -> Vec<(String, ValueType)>{ + pub fn stop_batch_observing_variable_changes(&mut self) -> Vec<(String, ValueType)> { self.batch_observing_variable_changes = false; let mut changed: Vec<(String, ValueType)> = Vec::with_capacity(0); @@ -53,7 +68,7 @@ impl VariablesState { } pub fn snapshot_default_globals(&mut self) { - for (k,v) in self.global_variables.iter() { + for (k, v) in self.global_variables.iter() { self.default_global_variables.insert(k.clone(), v.clone()); } } @@ -62,38 +77,37 @@ impl VariablesState { for (name, value) in self.patch.as_ref().unwrap().globals.iter() { self.global_variables.insert(name.clone(), value.clone()); } - + if let Some(changed_variables) = &mut self.changed_variables_for_batch_obs { for name in self.patch.as_ref().unwrap().changed_variables.iter() { changed_variables.insert(name.clone()); } } - + self.patch = None; } - pub fn assign ( + pub fn assign( &mut self, var_ass: &VariableAssignment, value: Rc, - ) -> Result<(), StoryError>{ + ) -> Result<(), StoryError> { let mut name = var_ass.variable_name.to_string(); let mut context_index = -1; let mut set_global; - + // Are we assigning to a global variable? if var_ass.is_new_declaration { set_global = var_ass.is_global; } else { set_global = self.global_variable_exists_with_name(&name); } - + let mut value = value; // Constructing new variable pointer reference if var_ass.is_new_declaration { - if let Some(var_pointer) = Value::get_variable_pointer_value(value.as_ref()){ - value = - self.resolve_variable_pointer(var_pointer); + if let Some(var_pointer) = Value::get_variable_pointer_value(value.as_ref()) { + value = self.resolve_variable_pointer(var_pointer); } } else { // Assign to an existing variable pointer @@ -103,32 +117,37 @@ impl VariablesState { let existing_pointer = self.get_raw_variable_with_name(&name, context_index); match existing_pointer { - Some(existing_pointer) => match Value::get_variable_pointer_value(existing_pointer.as_ref()) { - Some(pv) => { - name = pv.variable_name.to_string(); - context_index =pv.context_index; - set_global = context_index == 0; - }, - None => break, - }, + Some(existing_pointer) => { + match Value::get_variable_pointer_value(existing_pointer.as_ref()) { + Some(pv) => { + name = pv.variable_name.to_string(); + context_index = pv.context_index; + set_global = context_index == 0; + } + None => break, + } + } None => break, } } } - + if set_global { self.set_global(&name, value); } else { - self.callstack.borrow_mut().set_temporary_variable(name, value, var_ass.is_new_declaration, context_index)?; + self.callstack.borrow_mut().set_temporary_variable( + name, + value, + var_ass.is_new_declaration, + context_index, + )?; } Ok(()) } pub fn global_variable_exists_with_name(&self, name: &str) -> bool { - self.global_variables.contains_key(name) - || self - .default_global_variables.contains_key(name) + self.global_variables.contains_key(name) || self.default_global_variables.contains_key(name) } // Given a variable pointer with just the name of the target known, resolve @@ -141,8 +160,9 @@ impl VariablesState { if context_index == -1 { context_index = self.get_context_index_of_variable_named(&var_pointer.variable_name); } - - let value_of_variable_pointed_to = self.get_raw_variable_with_name(&var_pointer.variable_name, context_index); + + let value_of_variable_pointed_to = + self.get_raw_variable_with_name(&var_pointer.variable_name, context_index); // 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 @@ -153,15 +173,20 @@ impl VariablesState { return value_of_variable_pointed_to; } } - - Rc::new(Value::new_variable_pointer(&var_pointer.variable_name, context_index)) + + Rc::new(Value::new_variable_pointer( + &var_pointer.variable_name, + context_index, + )) } // returns true if the value changed and we should notify variable observers pub fn set(&mut self, variable_name: &str, value_type: ValueType) -> Result { - if !self.default_global_variables.contains_key(variable_name) { - return Err(StoryError::BadArgument(format!("Cannot assign to a variable {} that hasn't been declared in the story", variable_name))); + return Err(StoryError::BadArgument(format!( + "Cannot assign to a variable {} that hasn't been declared in the story", + variable_name + ))); } let val = Value::from_value_type(value_type); @@ -172,7 +197,6 @@ impl VariablesState { } pub fn get(&self, variable_name: &str) -> Option { - if self.patch.is_some() { if let Some(var) = self.patch.as_ref().unwrap().get_global(variable_name) { return Some(var.value.clone()); @@ -190,7 +214,6 @@ impl VariablesState { } None - } // Make copy of the variable pointer so we're not using the value direct @@ -230,13 +253,18 @@ impl VariablesState { return Some(default_global.clone()); } - if let Some(list_item_value) = self.list_defs_origin.find_single_item_list_with_name(name) { + if let Some(list_item_value) = + self.list_defs_origin.find_single_item_list_with_name(name) + { return Some(list_item_value.clone()); } } // Temporary - let var_value = self.callstack.borrow().get_temporary_variable_with_name(name, context_index); + let var_value = self + .callstack + .borrow() + .get_temporary_variable_with_name(name, context_index); var_value } @@ -260,7 +288,8 @@ impl VariablesState { if let Some(patch) = &mut self.patch { patch.set_global(name, value.clone()); } else { - self.global_variables.insert(name.to_string(), value.clone()); + self.global_variables + .insert(name.to_string(), value.clone()); } if old_value.is_none() || !Rc::ptr_eq(old_value.as_ref().unwrap(), &value) { @@ -296,17 +325,18 @@ impl VariablesState { pub fn set_callstack(&mut self, callstack: Rc>) { self.callstack = callstack; - } + } pub(crate) fn write_json(&self) -> Result { let mut jobj: Map = Map::new(); for (name, val) in self.global_variables.iter() { - // Don't write out values that are the same as the default global values let default_val = self.default_global_variables.get(name); if let Some(default_val) = default_val { - if self.val_equal(val, default_val) {continue;} + if self.val_equal(val, default_val) { + continue; + } } jobj.insert(name.clone(), json_write::write_rtobject(val.clone())?); @@ -335,7 +365,7 @@ impl VariablesState { }, ValueType::String(val) => match &default_val.value { ValueType::String(default_val) => val.string.eq(&default_val.string), - _ => false, + _ => false, }, ValueType::DivertTarget(val) => match &default_val.value { ValueType::DivertTarget(default_val) => *val == *default_val, @@ -348,20 +378,28 @@ impl VariablesState { } } - pub(crate) fn load_json(&mut self, jobj: &Map) -> Result<(), StoryError> { + pub(crate) fn load_json( + &mut self, + jobj: &Map, + ) -> Result<(), StoryError> { self.global_variables.clear(); for (k, v) in self.default_global_variables.iter() { let loaded_token = jobj.get(k); if let Some(loaded_token) = loaded_token { - self.global_variables.insert(k.to_string(), json_read::jtoken_to_runtime_object(loaded_token, None)?.into_any().downcast::().unwrap()); + self.global_variables.insert( + k.to_string(), + json_read::jtoken_to_runtime_object(loaded_token, None)? + .into_any() + .downcast::() + .unwrap(), + ); } else { self.global_variables.insert(k.clone(), v.clone()); } } - Ok(()) + Ok(()) } - } diff --git a/lib/src/void.rs b/lib/src/void.rs index cf1114f..20bd8d4 100644 --- a/lib/src/void.rs +++ b/lib/src/void.rs @@ -8,7 +8,7 @@ pub struct Void { impl Void { pub fn new() -> Self { - Void {obj: Object::new()} + Void { obj: Object::new() } } } @@ -22,4 +22,4 @@ impl fmt::Display for Void { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Void") } -} \ No newline at end of file +} diff --git a/lib/tests/basic_text_test.rs b/lib/tests/basic_text_test.rs index c8facdd..e0fe490 100644 --- a/lib/tests/basic_text_test.rs +++ b/lib/tests/basic_text_test.rs @@ -1,12 +1,13 @@ -use std::fs; use bink::{story::Story, story_error::StoryError}; +use std::{env, fs}; mod common; #[test] -fn oneline_test() -> Result<(), StoryError> { - let json_string = - fs::read_to_string("tests/data/basictext/oneline.ink.json").unwrap(); +fn oneline_test() -> Result<(), StoryError> { + println!("{}", env::current_dir().unwrap().to_string_lossy()); + + let json_string = common::get_json_string("inkfiles/basictext/oneline.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); @@ -21,8 +22,7 @@ fn oneline_test() -> Result<(), StoryError> { #[test] fn twolines_test() -> Result<(), StoryError> { - let json_string = - fs::read_to_string("tests/data/basictext/twolines.ink.json").unwrap(); + let json_string = fs::read_to_string("tests/data/basictext/twolines.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); diff --git a/lib/tests/choice_test.rs b/lib/tests/choice_test.rs index 867be4d..d58c86b 100644 --- a/lib/tests/choice_test.rs +++ b/lib/tests/choice_test.rs @@ -1,13 +1,16 @@ use bink::{story::Story, story_error::StoryError}; - mod common; #[test] -fn no_choice_test() -> Result<(), StoryError> { - let mut errors:Vec = Vec::new(); +fn no_choice_test() -> Result<(), StoryError> { + let mut errors: Vec = Vec::new(); - let text = common::run_story("tests/data/choices/no-choice-text.ink.json", None, &mut errors)?; + let text = common::run_story( + "tests/data/choices/no-choice-text.ink.json", + None, + &mut errors, + )?; assert_eq!(0, errors.len()); assert_eq!("Hello world!\nHello back!\n", common::join_text(&text)); @@ -16,39 +19,55 @@ fn no_choice_test() -> Result<(), StoryError> { } #[test] -fn one_test() -> Result<(), StoryError> { - let mut errors:Vec = Vec::new(); +fn one_test() -> Result<(), StoryError> { + let mut errors: Vec = Vec::new(); let text = common::run_story("tests/data/choices/one.ink.json", None, &mut errors)?; assert_eq!(0, errors.len()); - assert_eq!("Hello world!\nHello back!\nHello back!\n", common::join_text(&text)); + assert_eq!( + "Hello world!\nHello back!\nHello back!\n", + common::join_text(&text) + ); Ok(()) } #[test] -fn multi_choice_test() -> Result<(), StoryError> { - let mut errors:Vec = Vec::new(); +fn multi_choice_test() -> Result<(), StoryError> { + let mut errors: Vec = Vec::new(); - let text = common::run_story("tests/data/choices/multi-choice.ink.json", Some(vec![0]), &mut errors)?; + let text = common::run_story( + "tests/data/choices/multi-choice.ink.json", + Some(vec![0]), + &mut errors, + )?; assert_eq!(0, errors.len()); - assert_eq!("Hello, world!\nHello back!\nGoodbye\nHello back!\nNice to hear from you\n", common::join_text(&text)); + assert_eq!( + "Hello, world!\nHello back!\nGoodbye\nHello back!\nNice to hear from you\n", + common::join_text(&text) + ); // Select second choice - let text = common::run_story("tests/data/choices/multi-choice.ink.json", Some(vec![1]), &mut errors)?; + let text = common::run_story( + "tests/data/choices/multi-choice.ink.json", + Some(vec![1]), + &mut errors, + )?; assert_eq!(0, errors.len()); - assert_eq!("Hello, world!\nHello back!\nGoodbye\nGoodbye\nSee you later\n", common::join_text(&text)); + assert_eq!( + "Hello, world!\nHello back!\nGoodbye\nGoodbye\nSee you later\n", + common::join_text(&text) + ); Ok(()) } #[test] -fn single_choice1_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/choices/single-choice.ink.json").unwrap(); +fn single_choice1_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/choices/single-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -60,9 +79,8 @@ fn single_choice1_test() -> Result<(), StoryError> { } #[test] -fn single_choic2_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/choices/single-choice.ink.json").unwrap(); +fn single_choic2_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/choices/single-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -78,14 +96,17 @@ fn single_choic2_test() -> Result<(), StoryError> { } #[test] -fn suppress_choice_test() -> Result<(), StoryError> { +fn suppress_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/choices/suppress-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; - assert_eq!("Hello back!", story.get_current_choices().get(0).unwrap().text); + assert_eq!( + "Hello back!", + story.get_current_choices().get(0).unwrap().text + ); story.choose_choice_index(0)?; text.clear(); @@ -94,19 +115,20 @@ fn suppress_choice_test() -> Result<(), StoryError> { assert_eq!(1, text.len()); assert_eq!("Nice to hear from you.", text[0]); - Ok(()) } #[test] -fn mixed_choice_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/choices/mixed-choice.ink.json").unwrap(); +fn mixed_choice_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/choices/mixed-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; - assert_eq!("Hello back!", story.get_current_choices().get(0).unwrap().text); + assert_eq!( + "Hello back!", + story.get_current_choices().get(0).unwrap().text + ); story.choose_choice_index(0)?; text.clear(); @@ -116,17 +138,16 @@ fn mixed_choice_test() -> Result<(), StoryError> { assert_eq!("Hello right back to you!", text[0]); assert_eq!("Nice to hear from you.", text[1]); - Ok(()) } #[test] -fn varying_choice_test() -> Result<(), StoryError> { +fn varying_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/choices/varying-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; assert_eq!(2, story.get_current_choices().len()); story.choose_choice_index(0)?; @@ -135,19 +156,20 @@ fn varying_choice_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!(1, story.get_current_choices().len()); - assert_eq!("The man with the briefcase?", story.get_current_choices()[0].text); - + assert_eq!( + "The man with the briefcase?", + story.get_current_choices()[0].text + ); Ok(()) } #[test] -fn sticky_choice_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/choices/sticky-choice.ink.json").unwrap(); +fn sticky_choice_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/choices/sticky-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; assert_eq!(2, story.get_current_choices().len()); story.choose_choice_index(0)?; @@ -161,12 +183,12 @@ fn sticky_choice_test() -> Result<(), StoryError> { } #[test] -fn fallback_choice_test() -> Result<(), StoryError> { +fn fallback_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/choices/fallback-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; assert_eq!(2, story.get_current_choices().len()); @@ -174,12 +196,12 @@ fn fallback_choice_test() -> Result<(), StoryError> { } #[test] -fn fallback_choice2_test() -> Result<(), StoryError> { +fn fallback_choice2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/choices/fallback-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; assert_eq!(2, story.get_current_choices().len()); story.choose_choice_index(0)?; @@ -197,25 +219,24 @@ fn fallback_choice2_test() -> Result<(), StoryError> { } #[test] -fn conditional_choice_test() -> Result<(), StoryError> { +fn conditional_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/choices/conditional-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; assert_eq!(4, story.get_current_choices().len()); - + Ok(()) } #[test] -fn label_flow_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/choices/label-flow.ink.json").unwrap(); +fn label_flow_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/choices/label-flow.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; assert_eq!(2, story.get_current_choices().len()); story.choose_choice_index(0)?; @@ -224,18 +245,20 @@ fn label_flow_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!(2, story.get_current_choices().len()); - assert_eq!("\'Having a nice day?\'",story.get_current_choices()[0].text); + assert_eq!( + "\'Having a nice day?\'", + story.get_current_choices()[0].text + ); Ok(()) } #[test] -fn label_flow2_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/choices/label-flow.ink.json").unwrap(); +fn label_flow2_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/choices/label-flow.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; assert_eq!(2, story.get_current_choices().len()); story.choose_choice_index(1)?; @@ -244,18 +267,17 @@ fn label_flow2_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!(2, story.get_current_choices().len()); - assert_eq!("Shove him aside",story.get_current_choices()[1].text); + assert_eq!("Shove him aside", story.get_current_choices()[1].text); Ok(()) } #[test] -fn label_scope_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/choices/label-scope.ink.json").unwrap(); +fn label_scope_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/choices/label-scope.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; story.choose_choice_index(0)?; @@ -263,18 +285,17 @@ fn label_scope_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!(1, story.get_current_choices().len()); - assert_eq!("Found gatherpoint",story.get_current_choices()[0].text); + assert_eq!("Found gatherpoint", story.get_current_choices()[0].text); Ok(()) } #[test] -fn divert_choice_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/choices/divert-choice.ink.json").unwrap(); +fn divert_choice_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/choices/divert-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; assert_eq!(2, story.get_current_choices().len()); story.choose_choice_index(0)?; @@ -283,9 +304,9 @@ fn divert_choice_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!(2, text.len()); assert_eq!("You pull a face, and the soldier comes at you! You shove the guard to one side, but he comes back swinging.", text[0]); - + assert_eq!(1, story.get_current_choices().len()); - assert_eq!("Grapple and fight",story.get_current_choices()[0].text); + assert_eq!("Grapple and fight", story.get_current_choices()[0].text); Ok(()) -} \ No newline at end of file +} diff --git a/lib/tests/common/mod.rs b/lib/tests/common/mod.rs index c7ede4b..a11d725 100644 --- a/lib/tests/common/mod.rs +++ b/lib/tests/common/mod.rs @@ -1,6 +1,6 @@ #![allow(dead_code)] -use std::{error::Error, path::Path, fs}; +use std::{error::Error, fs, path::Path}; use bink::{story::Story, story_error::StoryError}; use rand::Rng; @@ -32,7 +32,6 @@ pub fn join_text(text: &Vec) -> String { sb } - pub fn run_story( filename: &str, choice_list: Option>, @@ -50,7 +49,6 @@ pub fn run_story( let mut rng = rand::thread_rng(); while story.can_continue() || !story.get_current_choices().is_empty() { - println!("{}", story.build_string_of_hierarchy()); // 2) Game content, line by line @@ -96,7 +94,13 @@ pub fn run_story( } pub fn get_json_string(filename: &str) -> Result> { - let path = Path::new(filename); + let mut path = Path::new(filename).to_path_buf(); + + if !path.exists() { + let parent = Path::new("../"); + path = parent.join(path); + } + let json = fs::read_to_string(path)?; Ok(json) } diff --git a/lib/tests/conditional_test.rs b/lib/tests/conditional_test.rs index 31ecece..cc4f02f 100644 --- a/lib/tests/conditional_test.rs +++ b/lib/tests/conditional_test.rs @@ -3,15 +3,14 @@ use bink::{story::Story, story_error::StoryError}; mod common; #[test] -fn iftrue_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/conditional/iftrue.ink.json").unwrap(); +fn iftrue_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/conditional/iftrue.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - + assert_eq!(1, text.len()); assert_eq!("The value is 1.", text[0]); @@ -19,15 +18,14 @@ fn iftrue_test() -> Result<(), StoryError> { } #[test] -fn iffalse_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/conditional/iffalse.ink.json").unwrap(); +fn iffalse_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/conditional/iffalse.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - + assert_eq!(1, text.len()); assert_eq!("The value is 3.", text[0]); @@ -35,15 +33,14 @@ fn iffalse_test() -> Result<(), StoryError> { } #[test] -fn ifelse_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/conditional/ifelse.ink.json").unwrap(); +fn ifelse_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/conditional/ifelse.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - + assert_eq!(1, text.len()); assert_eq!("The value is 1.", text[0]); @@ -51,7 +48,7 @@ fn ifelse_test() -> Result<(), StoryError> { } #[test] -fn ifelse_ext_test() -> Result<(), StoryError> { +fn ifelse_ext_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/ifelse-ext.ink.json").unwrap(); let mut story = Story::new(&json_string)?; @@ -59,7 +56,7 @@ fn ifelse_ext_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - + assert_eq!(1, text.len()); assert_eq!("The value is -1.", text[0]); @@ -67,7 +64,7 @@ fn ifelse_ext_test() -> Result<(), StoryError> { } #[test] -fn ifelse_ext_text1_test() -> Result<(), StoryError> { +fn ifelse_ext_text1_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/ifelse-ext-text1.ink.json").unwrap(); let mut story = Story::new(&json_string)?; @@ -75,7 +72,7 @@ fn ifelse_ext_text1_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - + assert_eq!(1, text.len()); assert_eq!("This is text 1.", text[0]); assert_eq!(1, story.get_current_choices().len()); @@ -89,7 +86,7 @@ fn ifelse_ext_text1_test() -> Result<(), StoryError> { } #[test] -fn ifelse_ext_text2_test() -> Result<(), StoryError> { +fn ifelse_ext_text2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/ifelse-ext-text2.ink.json").unwrap(); let mut story = Story::new(&json_string)?; @@ -97,7 +94,7 @@ fn ifelse_ext_text2_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - + assert_eq!(1, text.len()); assert_eq!("This is text 2.", text[0]); assert_eq!(1, story.get_current_choices().len()); @@ -111,7 +108,7 @@ fn ifelse_ext_text2_test() -> Result<(), StoryError> { } #[test] -fn ifelse_ext_text3_test() -> Result<(), StoryError> { +fn ifelse_ext_text3_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/ifelse-ext-text3.ink.json").unwrap(); let mut story = Story::new(&json_string)?; @@ -119,7 +116,7 @@ fn ifelse_ext_text3_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - + assert_eq!(1, text.len()); assert_eq!("This is text 3.", text[0]); assert_eq!(1, story.get_current_choices().len()); @@ -133,98 +130,99 @@ fn ifelse_ext_text3_test() -> Result<(), StoryError> { } #[test] -fn cond_text1_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/conditional/condtext.ink.json").unwrap(); +fn cond_text1_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/conditional/condtext.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; - + assert_eq!(3, text.len()); - assert_eq!("I stared at Monsieur Fogg. \"But surely you are not serious?\" I demanded.", text[1]); + assert_eq!( + "I stared at Monsieur Fogg. \"But surely you are not serious?\" I demanded.", + text[1] + ); Ok(()) } #[test] -fn cond_text2_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/conditional/condtext.ink.json").unwrap(); +fn cond_text2_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/conditional/condtext.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; story.choose_choice_index(1)?; text.clear(); common::next_all(&mut story, &mut text)?; - + assert_eq!(2, text.len()); - assert_eq!("I stared at Monsieur Fogg. \"But there must be a reason for this trip,\" I observed.", text[0]); + assert_eq!( + "I stared at Monsieur Fogg. \"But there must be a reason for this trip,\" I observed.", + text[0] + ); Ok(()) } #[test] -fn cond_opt1_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/conditional/condopt.ink.json").unwrap(); +fn cond_opt1_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/conditional/condopt.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; - + assert_eq!(1, story.get_current_choices().len()); Ok(()) } #[test] -fn cond_opt2_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/conditional/condopt.ink.json").unwrap(); +fn cond_opt2_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/conditional/condopt.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; story.choose_choice_index(1)?; text.clear(); common::next_all(&mut story, &mut text)?; - + assert_eq!(2, story.get_current_choices().len()); Ok(()) } #[test] -fn stopping_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/conditional/stopping.ink.json").unwrap(); +fn stopping_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/conditional/stopping.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("I entered the casino.", text[0]); story.choose_choice_index(0)?; - + text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("I entered the casino again.", text[0]); story.choose_choice_index(0)?; - + text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); @@ -241,24 +239,23 @@ fn stopping_test() -> Result<(), StoryError> { } #[test] -fn cycle_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/conditional/cycle.ink.json").unwrap(); +fn cycle_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/conditional/cycle.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("I held my breath.", text[0]); story.choose_choice_index(0)?; - + text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("I waited impatiently.", text[0]); story.choose_choice_index(0)?; - + text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); @@ -275,24 +272,23 @@ fn cycle_test() -> Result<(), StoryError> { } #[test] -fn once_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/conditional/once.ink.json").unwrap(); +fn once_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/conditional/once.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("Would my luck hold?", text[0]); story.choose_choice_index(0)?; - + text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("Could I win the hand?", text[0]); story.choose_choice_index(0)?; - + text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(0, text.len()); @@ -306,22 +302,21 @@ fn once_test() -> Result<(), StoryError> { } #[test] -fn shuffle_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/conditional/shuffle.ink.json").unwrap(); +fn shuffle_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/conditional/shuffle.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); story.choose_choice_index(0)?; - + text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); story.choose_choice_index(0)?; - + text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); @@ -337,22 +332,22 @@ fn shuffle_test() -> Result<(), StoryError> { } #[test] -fn shuffle_stopping() -> Result<(), StoryError> { +fn shuffle_stopping() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/shuffle_stopping.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); story.choose_choice_index(0)?; - + text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); story.choose_choice_index(0)?; - + text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); @@ -370,22 +365,22 @@ fn shuffle_stopping() -> Result<(), StoryError> { } #[test] -fn shuffle_once() -> Result<(), StoryError> { +fn shuffle_once() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/shuffle_once.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); story.choose_choice_index(0)?; - + text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); story.choose_choice_index(0)?; - + text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(0, text.len()); @@ -401,25 +396,24 @@ fn shuffle_once() -> Result<(), StoryError> { } #[test] -fn multiline_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/conditional/multiline.ink.json").unwrap(); +fn multiline_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/conditional/multiline.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("At the table, I drew a card. Ace of Hearts.", text[0]); story.choose_choice_index(0)?; - + text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(2, text.len()); assert_eq!("I drew a card. 2 of Diamonds.", text[0]); assert_eq!("\"Should I hit you again,\" the croupier asks.", text[1]); story.choose_choice_index(0)?; - + text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(2, text.len()); @@ -430,25 +424,25 @@ fn multiline_test() -> Result<(), StoryError> { } #[test] -fn multiline_divert_test() -> Result<(), StoryError> { +fn multiline_divert_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/multiline-divert.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("At the table, I drew a card. Ace of Hearts.", text[0]); story.choose_choice_index(0)?; - + text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(2, text.len()); assert_eq!("I drew a card. 2 of Diamonds.", text[0]); assert_eq!("\"Should I hit you again,\" the croupier asks.", text[1]); story.choose_choice_index(0)?; - + text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(2, text.len()); @@ -459,27 +453,27 @@ fn multiline_divert_test() -> Result<(), StoryError> { } #[test] -fn multiline_choice_test() -> Result<(), StoryError> { +fn multiline_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/conditional/multiline-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("At the table, I drew a card. Ace of Hearts.", text[0]); story.choose_choice_index(0)?; - + text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(2, story.get_current_choices().len()); story.choose_choice_index(0)?; - + text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); assert_eq!("I left the table.", text[0]); Ok(()) -} \ No newline at end of file +} diff --git a/lib/tests/divert_test.rs b/lib/tests/divert_test.rs index 83f6134..3d23123 100644 --- a/lib/tests/divert_test.rs +++ b/lib/tests/divert_test.rs @@ -3,22 +3,24 @@ use bink::{story::Story, story_error::StoryError}; mod common; #[test] -fn simple_divert_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/divert/simple-divert.ink.json").unwrap(); +fn simple_divert_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/divert/simple-divert.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; assert_eq!(2, text.len()); assert_eq!("We arrived into London at 9.45pm exactly.", text[0]); - assert_eq!("We hurried home to Savile Row as fast as we could.", text[1]); + assert_eq!( + "We hurried home to Savile Row as fast as we could.", + text[1] + ); Ok(()) } #[test] -fn invisible_divert_test() -> Result<(), StoryError> { +fn invisible_divert_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/divert/invisible-divert.ink.json").unwrap(); let mut story = Story::new(&json_string)?; @@ -26,13 +28,16 @@ fn invisible_divert_test() -> Result<(), StoryError> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); - assert_eq!("We hurried home to Savile Row as fast as we could.", text[0]); + assert_eq!( + "We hurried home to Savile Row as fast as we could.", + text[0] + ); Ok(()) } #[test] -fn divert_on_choice_test() -> Result<(), StoryError> { +fn divert_on_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/divert/divert-on-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; @@ -51,12 +56,12 @@ fn divert_on_choice_test() -> Result<(), StoryError> { } #[test] -fn complex_branching1_test() -> Result<(), StoryError> { +fn complex_branching1_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/divert/complex-branching.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; story.choose_choice_index(0)?; @@ -65,18 +70,21 @@ fn complex_branching1_test() -> Result<(), StoryError> { assert_eq!(2, text.len()); assert_eq!("\"There is not a moment to lose!\" I declared.", text[0]); - assert_eq!("We hurried home to Savile Row as fast as we could.", text[1]); + assert_eq!( + "We hurried home to Savile Row as fast as we could.", + text[1] + ); Ok(()) } #[test] -fn complex_branching2_test() -> Result<(), StoryError> { +fn complex_branching2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/divert/complex-branching.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; story.choose_choice_index(1)?; @@ -84,9 +92,18 @@ fn complex_branching2_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!(3, text.len()); - assert_eq!("\"Monsieur, let us savour this moment!\" I declared.", text[0]); - assert_eq!("My master clouted me firmly around the head and dragged me out of the door.", text[1]); - assert_eq!("He insisted that we hurried home to Savile Row as fast as we could.", text[2]); + assert_eq!( + "\"Monsieur, let us savour this moment!\" I declared.", + text[0] + ); + assert_eq!( + "My master clouted me firmly around the head and dragged me out of the door.", + text[1] + ); + assert_eq!( + "He insisted that we hurried home to Savile Row as fast as we could.", + text[2] + ); Ok(()) } diff --git a/lib/tests/function_test.rs b/lib/tests/function_test.rs index f7c0eb8..6614920 100644 --- a/lib/tests/function_test.rs +++ b/lib/tests/function_test.rs @@ -3,9 +3,8 @@ use bink::{story::Story, story_error::StoryError}; mod common; #[test] -fn fun_basic_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/function/func-basic.ink.json").unwrap(); +fn fun_basic_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/function/func-basic.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -17,9 +16,8 @@ fn fun_basic_test() -> Result<(), StoryError> { } #[test] -fn fun_none_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/function/func-none.ink.json").unwrap(); +fn fun_none_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/function/func-none.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -31,9 +29,8 @@ fn fun_none_test() -> Result<(), StoryError> { } #[test] -fn fun_inline_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/function/func-inline.ink.json").unwrap(); +fn fun_inline_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/function/func-inline.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -45,9 +42,8 @@ fn fun_inline_test() -> Result<(), StoryError> { } #[test] -fn setvar_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/function/setvar-func.ink.json").unwrap(); +fn setvar_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/function/setvar-func.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -59,7 +55,7 @@ fn setvar_test() -> Result<(), StoryError> { } #[test] -fn complex_func1_test() -> Result<(), StoryError> { +fn complex_func1_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/function/complex-func1.ink.json").unwrap(); let mut story = Story::new(&json_string)?; @@ -73,7 +69,7 @@ fn complex_func1_test() -> Result<(), StoryError> { } #[test] -fn complex_func2_test() -> Result<(), StoryError> { +fn complex_func2_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/function/complex-func2.ink.json").unwrap(); let mut story = Story::new(&json_string)?; @@ -87,7 +83,7 @@ fn complex_func2_test() -> Result<(), StoryError> { } #[test] -fn complex_func3_test() -> Result<(), StoryError> { +fn complex_func3_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/function/complex-func3.ink.json").unwrap(); let mut story = Story::new(&json_string)?; @@ -102,9 +98,8 @@ fn complex_func3_test() -> Result<(), StoryError> { } #[test] -fn rnd() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/function/rnd-func.ink.json").unwrap(); +fn rnd() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/function/rnd-func.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -119,9 +114,11 @@ fn rnd() -> Result<(), StoryError> { } #[test] -fn evaluating_function_variable_state_bug_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/function/evaluating-function-variablestate-bug.ink.json").unwrap(); +fn evaluating_function_variable_state_bug_test() -> Result<(), StoryError> { + let json_string = common::get_json_string( + "tests/data/function/evaluating-function-variablestate-bug.ink.json", + ) + .unwrap(); let mut story = Story::new(&json_string)?; assert_eq!("Start\n", story.cont()?); @@ -135,4 +132,4 @@ fn evaluating_function_variable_state_bug_test() -> Result<(), StoryError> { assert_eq!("End\n", story.cont()?); Ok(()) -} \ No newline at end of file +} diff --git a/lib/tests/gather_test.rs b/lib/tests/gather_test.rs index 597e4d8..23b747f 100644 --- a/lib/tests/gather_test.rs +++ b/lib/tests/gather_test.rs @@ -3,9 +3,8 @@ use bink::{story::Story, story_error::StoryError}; mod common; #[test] -fn gather_basic_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/gather/gather-basic.ink.json").unwrap(); +fn gather_basic_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/gather/gather-basic.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -24,9 +23,8 @@ fn gather_basic_test() -> Result<(), StoryError> { } #[test] -fn gather_chain_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/gather/gather-chain.ink.json").unwrap(); +fn gather_chain_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/gather/gather-chain.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -45,16 +43,21 @@ fn gather_chain_test() -> Result<(), StoryError> { text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(2, text.len()); - assert_eq!("I reached the road and looked about. And would you believe it?", text[0]); - assert_eq!("The road was empty. Mackie was nowhere to be seen.", text[1]); + assert_eq!( + "I reached the road and looked about. And would you believe it?", + text[0] + ); + assert_eq!( + "The road was empty. Mackie was nowhere to be seen.", + text[1] + ); Ok(()) } #[test] -fn nested_flow_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/gather/nested-flow.ink.json").unwrap(); +fn nested_flow_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/gather/nested-flow.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -75,9 +78,8 @@ fn nested_flow_test() -> Result<(), StoryError> { } #[test] -fn deep_nesting_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/gather/deep-nesting.ink.json").unwrap(); +fn deep_nesting_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/gather/deep-nesting.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -109,11 +111,9 @@ fn deep_nesting_test() -> Result<(), StoryError> { Ok(()) } - #[test] -fn complex_flow1_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/gather/complex-flow.ink.json").unwrap(); +fn complex_flow1_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/gather/complex-flow.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -123,15 +123,17 @@ fn complex_flow1_test() -> Result<(), StoryError> { text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); - assert_eq!("... but I said nothing and we passed the day in silence.", text[0]); + assert_eq!( + "... but I said nothing and we passed the day in silence.", + text[0] + ); Ok(()) } #[test] -fn complex_flow2_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/gather/complex-flow.ink.json").unwrap(); +fn complex_flow2_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/gather/complex-flow.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -158,4 +160,4 @@ fn complex_flow2_test() -> Result<(), StoryError> { assert_eq!(3, text.len()); Ok(()) -} \ No newline at end of file +} diff --git a/lib/tests/glue_test.rs b/lib/tests/glue_test.rs index ebdf5dd..96bc675 100644 --- a/lib/tests/glue_test.rs +++ b/lib/tests/glue_test.rs @@ -3,9 +3,8 @@ use bink::{story::Story, story_error::StoryError}; mod common; #[test] -fn simple_glue_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/glue/simple-glue.ink.json").unwrap(); +fn simple_glue_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/glue/simple-glue.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -17,29 +16,31 @@ fn simple_glue_test() -> Result<(), StoryError> { } #[test] -fn glue_with_divert_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/glue/glue-with-divert.ink.json").unwrap(); +fn glue_with_divert_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/glue/glue-with-divert.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; - + assert_eq!(1, text.len()); - assert_eq!("We hurried home to Savile Row as fast as we could.", text[0]); + assert_eq!( + "We hurried home to Savile Row as fast as we could.", + text[0] + ); Ok(()) } #[test] -fn has_left_right_glue_matching_test() -> Result<(), StoryError> { +fn has_left_right_glue_matching_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/glue/left-right-glue-matching.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; - + assert_eq!(2, text.len()); assert_eq!("A line.", text[0]); assert_eq!("Another line.", text[1]); @@ -48,14 +49,13 @@ fn has_left_right_glue_matching_test() -> Result<(), StoryError> { } #[test] -fn bugfix1_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/glue/testbugfix1.ink.json").unwrap(); +fn bugfix1_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/glue/testbugfix1.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; - + assert_eq!(2, text.len()); assert_eq!("A", text[0]); assert_eq!("C", text[1]); @@ -64,14 +64,13 @@ fn bugfix1_test() -> Result<(), StoryError> { } #[test] -fn bugfix2_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/glue/testbugfix2.ink.json").unwrap(); +fn bugfix2_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/glue/testbugfix2.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; - + assert_eq!(2, text.len()); //assert_eq!("A", text[0]); assert_eq!("X", text[1]); diff --git a/lib/tests/knot_test.rs b/lib/tests/knot_test.rs index 6ee94a9..5ffb630 100644 --- a/lib/tests/knot_test.rs +++ b/lib/tests/knot_test.rs @@ -3,9 +3,8 @@ use bink::{story::Story, story_error::StoryError}; mod common; #[test] -fn single_line_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/knot/single-line.ink.json").unwrap(); +fn single_line_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/knot/single-line.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -18,9 +17,8 @@ fn single_line_test() -> Result<(), StoryError> { } #[test] -fn multi_line_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/knot/multi-line.ink.json").unwrap(); +fn multi_line_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/knot/multi-line.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -35,7 +33,7 @@ fn multi_line_test() -> Result<(), StoryError> { } #[test] -fn strip_empty_lines_test() -> Result<(), StoryError> { +fn strip_empty_lines_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/knot/strip-empty-lines.ink.json").unwrap(); let mut story = Story::new(&json_string)?; @@ -52,9 +50,8 @@ fn strip_empty_lines_test() -> Result<(), StoryError> { } #[test] -fn param_strings_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/knot/param-strings.ink.json").unwrap(); +fn param_strings_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/knot/param-strings.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -71,9 +68,8 @@ fn param_strings_test() -> Result<(), StoryError> { } #[test] -fn param_ints_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/knot/param-ints.ink.json").unwrap(); +fn param_ints_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/knot/param-ints.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -90,9 +86,8 @@ fn param_ints_test() -> Result<(), StoryError> { } #[test] -fn param_floats_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/knot/param-floats.ink.json").unwrap(); +fn param_floats_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/knot/param-floats.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -109,9 +104,8 @@ fn param_floats_test() -> Result<(), StoryError> { } #[test] -fn param_vars_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/knot/param-vars.ink.json").unwrap(); +fn param_vars_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/knot/param-vars.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -128,9 +122,8 @@ fn param_vars_test() -> Result<(), StoryError> { } #[test] -fn param_multi_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/knot/param-multi.ink.json").unwrap(); +fn param_multi_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/knot/param-multi.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -147,9 +140,8 @@ fn param_multi_test() -> Result<(), StoryError> { } #[test] -fn param_recurse_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/knot/param-recurse.ink.json").unwrap(); +fn param_recurse_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/knot/param-recurse.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -159,4 +151,4 @@ fn param_recurse_test() -> Result<(), StoryError> { assert_eq!("\"The result is 120!\" you announce.", text[0]); Ok(()) -} \ No newline at end of file +} diff --git a/lib/tests/list_test.rs b/lib/tests/list_test.rs index 701d34a..789f262 100644 --- a/lib/tests/list_test.rs +++ b/lib/tests/list_test.rs @@ -6,19 +6,20 @@ mod common; #[test] fn list_basic_operations_test() -> Result<(), Box> { - let json_string = - common::get_json_string("tests/data/lists/basic-operations.ink.json")?; + let json_string = common::get_json_string("tests/data/lists/basic-operations.ink.json")?; let mut story = Story::new(&json_string)?; - assert_eq!("b, d\na, b, c, e\nb, c\nfalse\ntrue\ntrue\n", &story.continue_maximally()?); + assert_eq!( + "b, d\na, b, c, e\nb, c\nfalse\ntrue\ntrue\n", + &story.continue_maximally()? + ); Ok(()) } #[test] fn list_mixed_items_test() -> Result<(), Box> { - let json_string = - common::get_json_string("tests/data/lists/list-mixed-items.ink.json")?; + let json_string = common::get_json_string("tests/data/lists/list-mixed-items.ink.json")?; let mut story = Story::new(&json_string)?; assert_eq!("a, y, c\n", &story.continue_maximally()?); @@ -28,8 +29,7 @@ fn list_mixed_items_test() -> Result<(), Box> { #[test] fn more_list_operations_test() -> Result<(), Box> { - let json_string = - common::get_json_string("tests/data/lists/more-list-operations.ink.json")?; + let json_string = common::get_json_string("tests/data/lists/more-list-operations.ink.json")?; let mut story = Story::new(&json_string)?; assert_eq!("1\nl\nn\nl, m\nn\n", &story.continue_maximally()?); @@ -39,8 +39,7 @@ fn more_list_operations_test() -> Result<(), Box> { #[test] fn empty_list_origin_test() -> Result<(), Box> { - let json_string = - common::get_json_string("tests/data/lists/empty-list-origin.ink.json")?; + let json_string = common::get_json_string("tests/data/lists/empty-list-origin.ink.json")?; let mut story = Story::new(&json_string)?; assert_eq!("a, b\n", &story.continue_maximally()?); @@ -50,8 +49,7 @@ fn empty_list_origin_test() -> Result<(), Box> { #[test] fn list_save_load_test() -> Result<(), Box> { - let json_string = - common::get_json_string("tests/data/lists/list-save-load.ink.json")?; + let json_string = common::get_json_string("tests/data/lists/list-save-load.ink.json")?; let mut story = Story::new(&json_string)?; assert_eq!("a, x, c\n", &story.continue_maximally()?); @@ -63,7 +61,7 @@ fn list_save_load_test() -> Result<(), Box> { story.load_state(&saved_state)?; story.choose_path_string("elsewhere", true, None)?; - + assert_eq!("z\n", &story.continue_maximally()?); Ok(()) @@ -82,8 +80,7 @@ fn empty_list_origin_after_assinment_test() -> Result<(), Box> { #[test] fn list_range_test() -> Result<(), Box> { - let json_string = - common::get_json_string("tests/data/lists/list-range.ink.json")?; + let json_string = common::get_json_string("tests/data/lists/list-range.ink.json")?; let mut story = Story::new(&json_string)?; assert_eq!("Pound, Pizza, Euro, Pasta, Dollar, Curry, Paella\nEuro, Pasta, Dollar, Curry\nTwo, Three, Four, Five, Six\nPizza, Pasta\n", &story.continue_maximally()?); @@ -93,8 +90,7 @@ fn list_range_test() -> Result<(), Box> { #[test] fn list_bug_adding_element_test() -> Result<(), Box> { - let json_string = - common::get_json_string("tests/data/lists/bug-adding-element.ink.json")?; + let json_string = common::get_json_string("tests/data/lists/bug-adding-element.ink.json")?; let mut story = Story::new(&json_string)?; assert_eq!("", &story.continue_maximally()?); @@ -110,8 +106,7 @@ fn list_bug_adding_element_test() -> Result<(), Box> { #[test] fn more_list_operations2_test() -> Result<(), Box> { - let json_string = - common::get_json_string("tests/data/lists/more-list-operations2.ink.json")?; + let json_string = common::get_json_string("tests/data/lists/more-list-operations2.ink.json")?; let mut story = Story::new(&json_string)?; assert_eq!("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:c2\nlistinc:b1\n", &story.continue_maximally()?); diff --git a/lib/tests/misc_test.rs b/lib/tests/misc_test.rs index f33c462..ff833e5 100644 --- a/lib/tests/misc_test.rs +++ b/lib/tests/misc_test.rs @@ -1,33 +1,36 @@ -use bink::{story::Story, value_type::ValueType, story_error::StoryError}; +use bink::{story::Story, story_error::StoryError, value_type::ValueType}; mod common; #[test] -fn operations_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/misc/operations.ink.json").unwrap(); +fn operations_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/misc/operations.ink.json").unwrap(); let mut story = Story::new(&json_string)?; - assert_eq!("neg:-3\nmod:1\npow:27\nfloor:3\nceiling:4\nint:3\nfloat:1\n", &story.continue_maximally()?); + assert_eq!( + "neg:-3\nmod:1\npow:27\nfloor:3\nceiling:4\nint:3\nfloat:1\n", + &story.continue_maximally()? + ); Ok(()) } #[test] -fn read_counts_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/misc/read-counts.ink.json").unwrap(); +fn read_counts_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/misc/read-counts.ink.json").unwrap(); let mut story = Story::new(&json_string)?; - assert_eq!("Count start: 0 0 0\n1\n2\n3\nCount end: 3 3 3\n", &story.continue_maximally()?); + assert_eq!( + "Count start: 0 0 0\n1\n2\n3\nCount end: 3 3 3\n", + &story.continue_maximally()? + ); Ok(()) } #[test] -fn turns_since_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/misc/turns-since.ink.json").unwrap(); +fn turns_since_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/misc/turns-since.ink.json").unwrap(); let mut story = Story::new(&json_string)?; assert_eq!("0\n0\n", &story.continue_maximally()?); @@ -41,9 +44,8 @@ fn turns_since_test() -> Result<(), StoryError> { * Issue: https://github.com/bladecoder/blade-ink/issues/15 */ #[test] -fn issue15_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/misc/issue15.ink.json").unwrap(); +fn issue15_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/misc/issue15.ink.json").unwrap(); let mut story = Story::new(&json_string)?; assert_eq!("This is a test\n", story.cont()?); @@ -59,5 +61,5 @@ fn issue15_test() -> Result<(), StoryError> { } } - Ok(()) -} \ No newline at end of file + Ok(()) +} diff --git a/lib/tests/multi_flow_test.rs b/lib/tests/multi_flow_test.rs index cd5b812..0ac3d93 100644 --- a/lib/tests/multi_flow_test.rs +++ b/lib/tests/multi_flow_test.rs @@ -3,7 +3,7 @@ use bink::{story::Story, story_error::StoryError}; mod common; #[test] -fn basics_test() -> Result<(), StoryError> { +fn basics_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/runtime/multiflow-basics.ink.json").unwrap(); let mut story = Story::new(&json_string)?; @@ -26,7 +26,7 @@ fn basics_test() -> Result<(), StoryError> { } #[test] -fn multiflow_save_load_threads() -> Result<(), StoryError> { +fn multiflow_save_load_threads() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/runtime/multiflow-saveloadthreads.ink.json").unwrap(); let mut story = Story::new(&json_string)?; @@ -57,31 +57,42 @@ fn multiflow_save_load_threads() -> Result<(), StoryError> { // Test choice before reloading state before resetting story.choose_choice_index(0)?; - assert_eq!("Thread 1 red choice\nAfter thread 1 choice (red)\n", story.continue_maximally()?); + assert_eq!( + "Thread 1 red choice\nAfter thread 1 choice (red)\n", + story.continue_maximally()? + ); let mut story = Story::new(&json_string)?; // Load to pre-choice: still red, choose second choice story.load_state(&saved)?; story.choose_choice_index(1)?; - assert_eq!("Thread 2 red choice\nAfter thread 2 choice (red)\n", story.continue_maximally()?); + assert_eq!( + "Thread 2 red choice\nAfter thread 2 choice (red)\n", + story.continue_maximally()? + ); // Load: switch to blue, choose 1 story.load_state(&saved)?; story.switch_flow("Blue Flow")?; story.choose_choice_index(0)?; - assert_eq!("Thread 1 blue choice\nAfter thread 1 choice (blue)\n", story.continue_maximally()?); + assert_eq!( + "Thread 1 blue choice\nAfter thread 1 choice (blue)\n", + story.continue_maximally()? + ); // Load: switch to blue, choose 2 story.load_state(&saved)?; story.switch_flow("Blue Flow")?; story.choose_choice_index(1)?; - assert_eq!("Thread 2 blue choice\nAfter thread 2 choice (blue)\n", story.continue_maximally()?); + assert_eq!( + "Thread 2 blue choice\nAfter thread 2 choice (blue)\n", + story.continue_maximally()? + ); // Remove active blue flow, should revert back to global flow story.remove_flow("Blue Flow")?; assert_eq!("Default line 2\n", story.cont()?); - Ok(()) } diff --git a/lib/tests/runtime_test.rs b/lib/tests/runtime_test.rs index 2528366..e496639 100644 --- a/lib/tests/runtime_test.rs +++ b/lib/tests/runtime_test.rs @@ -1,7 +1,11 @@ use core::panic; -use std::{cell::RefCell, rc::Rc, error::Error}; +use std::{cell::RefCell, error::Error, rc::Rc}; -use bink::{story::Story, value_type::ValueType, story_callbacks::{VariableObserver, ExternalFunction}}; +use bink::{ + story::Story, + story_callbacks::{ExternalFunction, VariableObserver}, + value_type::ValueType, +}; mod common; @@ -13,11 +17,11 @@ struct ExtFunc4; impl ExternalFunction for ExtFunc1 { fn call(&mut self, func_name: &str, args: Vec) -> Option { println!("Calling {func_name}..."); - + let x = args[0].coerce_to_int().unwrap_or_default(); let y = args[1].coerce_to_int().unwrap_or_default(); - - Some(ValueType::Int(x - y)) + + Some(ValueType::Int(x - y)) } } @@ -35,7 +39,7 @@ impl ExternalFunction for ExtFunc3 { impl ExternalFunction for ExtFunc4 { fn call(&mut self, _: &str, args: Vec) -> Option { - Some(ValueType::Bool(!args[0].coerce_to_bool().unwrap())) + Some(ValueType::Bool(!args[0].coerce_to_bool().unwrap())) } } @@ -46,7 +50,7 @@ fn external_function() -> Result<(), Box> { let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - story.bind_external_function("externalFunction", Rc::new(RefCell::new(ExtFunc1{})), true)?; + story.bind_external_function("externalFunction", Rc::new(RefCell::new(ExtFunc1 {})), true)?; common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); @@ -62,7 +66,7 @@ fn external_function_zero_arguments() -> Result<(), Box> { let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - story.bind_external_function("externalFunction", Rc::new(RefCell::new(ExtFunc2{})), true)?; + story.bind_external_function("externalFunction", Rc::new(RefCell::new(ExtFunc2 {})), true)?; common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); @@ -78,7 +82,7 @@ fn external_function_one_arguments() -> Result<(), Box> { let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - story.bind_external_function("externalFunction", Rc::new(RefCell::new(ExtFunc3{})), true)?; + story.bind_external_function("externalFunction", Rc::new(RefCell::new(ExtFunc3 {})), true)?; common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); @@ -94,7 +98,7 @@ fn external_function_coerce_test() -> Result<(), Box> { let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - story.bind_external_function("externalFunction", Rc::new(RefCell::new(ExtFunc4{})), true)?; + story.bind_external_function("externalFunction", Rc::new(RefCell::new(ExtFunc4 {})), true)?; common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); @@ -140,13 +144,12 @@ impl VariableObserver for VObserver { } #[test] -fn variable_observers_test() -> Result<(), Box> { - let json_string = - common::get_json_string("tests/data/runtime/variable-observers.ink.json")?; +fn variable_observers_test() -> Result<(), Box> { + let json_string = common::get_json_string("tests/data/runtime/variable-observers.ink.json")?; let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - story.observe_variable("x", Rc::new(RefCell::new(VObserver { expected_value: 5})))?; + story.observe_variable("x", Rc::new(RefCell::new(VObserver { expected_value: 5 })))?; common::next_all(&mut story, &mut text)?; story.choose_choice_index(0)?; @@ -155,11 +158,9 @@ fn variable_observers_test() -> Result<(), Box> { Ok(()) } - #[test] -fn set_and_get_variable_test() -> Result<(), Box> { - let json_string = - common::get_json_string("tests/data/runtime/set-get-variables.ink.json")?; +fn set_and_get_variable_test() -> Result<(), Box> { + let json_string = common::get_json_string("tests/data/runtime/set-get-variables.ink.json")?; let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -181,11 +182,9 @@ fn set_and_get_variable_test() -> Result<(), Box> { Ok(()) } - #[test] -fn set_non_existant_variable_test() -> Result<(), Box> { - let json_string = - common::get_json_string("tests/data/runtime/set-get-variables.ink.json")?; +fn set_non_existant_variable_test() -> Result<(), Box> { + let json_string = common::get_json_string("tests/data/runtime/set-get-variables.ink.json")?; let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -212,9 +211,8 @@ fn set_non_existant_variable_test() -> Result<(), Box> { } #[test] -fn jump_knot_test() -> Result<(), Box> { - let json_string = - common::get_json_string("tests/data/runtime/jump-knot.ink.json")?; +fn jump_knot_test() -> Result<(), Box> { + let json_string = common::get_json_string("tests/data/runtime/jump-knot.ink.json")?; let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -241,9 +239,8 @@ fn jump_knot_test() -> Result<(), Box> { } #[test] -fn jump_stitch_test() -> Result<(), Box> { - let json_string = - common::get_json_string("tests/data/runtime/jump-stitch.ink.json")?; +fn jump_stitch_test() -> Result<(), Box> { + let json_string = common::get_json_string("tests/data/runtime/jump-stitch.ink.json")?; let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -270,9 +267,8 @@ fn jump_stitch_test() -> Result<(), Box> { } #[test] -fn read_visit_counts_test() -> Result<(), Box> { - let json_string = - common::get_json_string("tests/data/runtime/read-visit-counts.ink.json")?; +fn read_visit_counts_test() -> Result<(), Box> { + let json_string = common::get_json_string("tests/data/runtime/read-visit-counts.ink.json")?; let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -284,15 +280,17 @@ fn read_visit_counts_test() -> Result<(), Box> { } #[test] -fn load_save_test() -> Result<(), Box> { - let json_string = - common::get_json_string("tests/data/runtime/load-save.ink.json")?; +fn load_save_test() -> Result<(), Box> { + let json_string = common::get_json_string("tests/data/runtime/load-save.ink.json")?; let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); - assert_eq!("We arrived into London at 9.45pm exactly.", text.get(0).unwrap()); + assert_eq!( + "We arrived into London at 9.45pm exactly.", + text.get(0).unwrap() + ); // save the game state let save_string = story.save_state()?; @@ -302,12 +300,18 @@ fn load_save_test() -> Result<(), Box> { // recreate game and load state Story::new(&json_string).unwrap(); story.load_state(&save_string)?; - + story.choose_choice_index(0)?; common::next_all(&mut story, &mut text)?; - assert_eq!("\"There is not a moment to lose!\" I declared.", text.get(1).unwrap()); - assert_eq!("We hurried home to Savile Row as fast as we could.", text.get(2).unwrap()); + assert_eq!( + "\"There is not a moment to lose!\" I declared.", + text.get(1).unwrap() + ); + assert_eq!( + "We hurried home to Savile Row as fast as we could.", + text.get(2).unwrap() + ); // check that we are at the end assert!(!story.can_continue()); @@ -315,6 +319,3 @@ fn load_save_test() -> Result<(), Box> { Ok(()) } - - - diff --git a/lib/tests/stitch_test.rs b/lib/tests/stitch_test.rs index 6128d41..75124a2 100644 --- a/lib/tests/stitch_test.rs +++ b/lib/tests/stitch_test.rs @@ -3,9 +3,8 @@ use bink::{story::Story, story_error::StoryError}; mod common; #[test] -fn auto_stitch_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/stitch/auto-stitch.ink.json").unwrap(); +fn auto_stitch_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/stitch/auto-stitch.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -18,9 +17,8 @@ fn auto_stitch_test() -> Result<(), StoryError> { } #[test] -fn auto_stitch2_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/stitch/auto-stitch.ink.json").unwrap(); +fn auto_stitch2_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/stitch/auto-stitch.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -36,9 +34,8 @@ fn auto_stitch2_test() -> Result<(), StoryError> { } #[test] -fn manual_stitch_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/stitch/manual-stitch.ink.json").unwrap(); +fn manual_stitch_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/stitch/manual-stitch.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -58,9 +55,8 @@ fn manual_stitch_test() -> Result<(), StoryError> { } #[test] -fn manual_stitch2_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/stitch/manual-stitch.ink.json").unwrap(); +fn manual_stitch2_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/stitch/manual-stitch.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -77,4 +73,4 @@ fn manual_stitch2_test() -> Result<(), StoryError> { assert_eq!("I settled my master.", text[0]); Ok(()) -} \ No newline at end of file +} diff --git a/lib/tests/tag_test.rs b/lib/tests/tag_test.rs index 8066b87..e32e933 100644 --- a/lib/tests/tag_test.rs +++ b/lib/tests/tag_test.rs @@ -3,9 +3,8 @@ use bink::{story::Story, story_error::StoryError}; mod common; #[test] -fn tags_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/tags/tags.ink.json").unwrap(); +fn tags_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/tags/tags.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let global_tags = story.get_global_tags()?; @@ -42,9 +41,8 @@ fn tags_test() -> Result<(), StoryError> { } #[test] -fn tags_in_seq_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/tags/tagsInSeq.ink.json").unwrap(); +fn tags_in_seq_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/tags/tagsInSeq.ink.json").unwrap(); let mut story = Story::new(&json_string)?; assert_eq!("A red sequence.\n", story.cont()?); @@ -61,9 +59,8 @@ fn tags_in_seq_test() -> Result<(), StoryError> { } #[test] -fn tags_in_choice_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/tags/tagsInChoice.ink.json").unwrap(); +fn tags_in_choice_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/tags/tagsInChoice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; story.cont()?; @@ -82,12 +79,11 @@ fn tags_in_choice_test() -> Result<(), StoryError> { assert_eq!("one", current_tags[0]); assert_eq!("three", current_tags[1]); - Ok(()) } #[test] -fn tags_dynamic_content_test() -> Result<(), StoryError> { +fn tags_dynamic_content_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/tags/tagsDynamicContent.ink.json").unwrap(); let mut story = Story::new(&json_string)?; @@ -99,4 +95,3 @@ fn tags_dynamic_content_test() -> Result<(), StoryError> { Ok(()) } - diff --git a/lib/tests/thread_test.rs b/lib/tests/thread_test.rs index d69e1c7..decfde9 100644 --- a/lib/tests/thread_test.rs +++ b/lib/tests/thread_test.rs @@ -3,18 +3,20 @@ use bink::{story::Story, story_error::StoryError}; mod common; #[test] -fn thread_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/threads/thread-bug.ink.json").unwrap(); +fn thread_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/threads/thread-bug.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); - - assert_eq!("Here is some gold. Do you want it?\n", story.continue_maximally()?); + + assert_eq!( + "Here is some gold. Do you want it?\n", + story.continue_maximally()? + ); assert_eq!(2, story.get_current_choices().len()); assert_eq!("No", story.get_current_choices()[0].text); assert_eq!("Yes", story.get_current_choices()[1].text); story.choose_choice_index(0)?; - + assert_eq!("No\nTry again!\n", story.continue_maximally()?); assert_eq!(2, story.get_current_choices().len()); assert_eq!("No", story.get_current_choices()[0].text); @@ -23,18 +25,19 @@ fn thread_test() -> Result<(), StoryError> { assert_eq!("Yes\nYou win!\n", story.continue_maximally()?); - Ok(()) } #[test] -fn thread_test_bug() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/threads/thread-bug.ink.json").unwrap(); +fn thread_test_bug() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/threads/thread-bug.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); - - assert_eq!("Here is some gold. Do you want it?\n", story.continue_maximally()?); + + assert_eq!( + "Here is some gold. Do you want it?\n", + story.continue_maximally()? + ); assert_eq!(2, story.get_current_choices().len()); assert_eq!("No", story.get_current_choices()[0].text); assert_eq!("Yes", story.get_current_choices()[1].text); @@ -45,7 +48,7 @@ fn thread_test_bug() -> Result<(), StoryError> { story.load_state(&save_string)?; story.choose_choice_index(0)?; - + assert_eq!("No\nTry again!\n", story.continue_maximally()?); assert_eq!(2, story.get_current_choices().len()); assert_eq!("No", story.get_current_choices()[0].text); @@ -54,6 +57,5 @@ fn thread_test_bug() -> Result<(), StoryError> { assert_eq!("Yes\nYou win!\n", story.continue_maximally()?); - Ok(()) -} \ No newline at end of file +} diff --git a/lib/tests/tunnel_test.rs b/lib/tests/tunnel_test.rs index dc2892a..4e0e1fb 100644 --- a/lib/tests/tunnel_test.rs +++ b/lib/tests/tunnel_test.rs @@ -3,12 +3,13 @@ use bink::{story::Story, story_error::StoryError}; mod common; #[test] -fn tunnel_onwards_divert_override_test() -> Result<(), StoryError> { +fn tunnel_onwards_divert_override_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/tunnels/tunnel-onwards-divert-override.ink.json").unwrap(); + common::get_json_string("tests/data/tunnels/tunnel-onwards-divert-override.ink.json") + .unwrap(); let mut story = Story::new(&json_string)?; assert_eq!("This is A\nNow in B.\n", story.continue_maximally()?); Ok(()) -} \ No newline at end of file +} diff --git a/lib/tests/variable_test.rs b/lib/tests/variable_test.rs index 1f65dea..b0f12ff 100644 --- a/lib/tests/variable_test.rs +++ b/lib/tests/variable_test.rs @@ -3,7 +3,7 @@ use bink::{story::Story, story_error::StoryError}; mod common; #[test] -fn variable_declaration_test() -> Result<(), StoryError> { +fn variable_declaration_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/variable/variable-declaration.ink.json").unwrap(); let mut story = Story::new(&json_string)?; @@ -11,15 +11,17 @@ fn variable_declaration_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); - assert_eq!("\"My name is Jean Passepartout, but my friend's call me Jackie. I'm 23 years old.\"", text[0]); + assert_eq!( + "\"My name is Jean Passepartout, but my friend's call me Jackie. I'm 23 years old.\"", + text[0] + ); Ok(()) } #[test] -fn var_calc_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/variable/varcalc.ink.json").unwrap(); +fn var_calc_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/variable/varcalc.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -31,12 +33,11 @@ fn var_calc_test() -> Result<(), StoryError> { } #[test] -fn var_string_ink_bug_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/variable/varstringinc.ink.json").unwrap(); +fn var_string_ink_bug_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/variable/varstringinc.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; story.choose_choice_index(0)?; text.clear(); @@ -49,15 +50,14 @@ fn var_string_ink_bug_test() -> Result<(), StoryError> { } #[test] -fn var_divert_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/variable/var-divert.ink.json").unwrap(); +fn var_divert_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/variable/var-divert.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - + common::next_all(&mut story, &mut text)?; story.choose_choice_index(1)?; - + text.clear(); common::next_all(&mut story, &mut text)?; @@ -65,4 +65,4 @@ fn var_divert_test() -> Result<(), StoryError> { assert_eq!("Everybody dies.", text[0]); Ok(()) -} \ No newline at end of file +} diff --git a/lib/tests/variable_text_test.rs b/lib/tests/variable_text_test.rs index 53c6a3f..a02aa69 100644 --- a/lib/tests/variable_text_test.rs +++ b/lib/tests/variable_text_test.rs @@ -3,9 +3,8 @@ use bink::{story::Story, story_error::StoryError}; mod common; #[test] -fn sequence_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/variabletext/sequence.ink.json").unwrap(); +fn sequence_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/variabletext/sequence.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -33,22 +32,27 @@ fn sequence_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); - assert_eq!("The radio hissed into life. There was the white noise racket of an explosion.", text[0]); + assert_eq!( + "The radio hissed into life. There was the white noise racket of an explosion.", + text[0] + ); story.choose_choice_index(0)?; text.clear(); common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); - assert_eq!("The radio hissed into life. There was the white noise racket of an explosion.", text[0]); + assert_eq!( + "The radio hissed into life. There was the white noise racket of an explosion.", + text[0] + ); Ok(()) } #[test] -fn cycle_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/variabletext/cycle.ink.json").unwrap(); +fn cycle_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/variabletext/cycle.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -89,9 +93,8 @@ fn cycle_test() -> Result<(), StoryError> { } #[test] -fn once_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("tests/data/variabletext/once.ink.json").unwrap(); +fn once_test() -> Result<(), StoryError> { + let json_string = common::get_json_string("tests/data/variabletext/once.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -132,7 +135,7 @@ fn once_test() -> Result<(), StoryError> { } #[test] -fn empty_elements_test() -> Result<(), StoryError> { +fn empty_elements_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/variabletext/empty-elements.ink.json").unwrap(); let mut story = Story::new(&json_string)?; @@ -161,7 +164,7 @@ fn empty_elements_test() -> Result<(), StoryError> { } #[test] -fn list_in_choice_test() -> Result<(), StoryError> { +fn list_in_choice_test() -> Result<(), StoryError> { let json_string = common::get_json_string("tests/data/variabletext/list-in-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; @@ -186,4 +189,4 @@ fn list_in_choice_test() -> Result<(), StoryError> { assert_eq!("\"Hello, you!\"", story.get_current_choices()[0].text); Ok(()) -} \ No newline at end of file +} From 39ffa9733a074ebf3bcce174e4a4c4c5d590aef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Mon, 9 Oct 2023 21:42:49 +0000 Subject: [PATCH 71/91] Move inkfiles to the root folder. --- cli-player/tests/basic_tests.rs | 10 ++++- cli-player/tests/test_the_intercept.rs | 12 +++++- .../tests/data => inkfiles}/TheIntercept.ink | 0 .../data => inkfiles}/TheIntercept.ink.json | 0 .../data => inkfiles}/basictext/oneline.ink | 0 .../basictext/oneline.ink.json | 0 .../data => inkfiles}/basictext/twolines.ink | 0 .../basictext/twolines.ink.json | 0 .../choices/conditional-choice.ink | 0 .../choices/conditional-choice.ink.json | 0 .../choices/divert-choice.ink | 0 .../choices/divert-choice.ink.json | 0 .../choices/fallback-choice.ink | 0 .../choices/fallback-choice.ink.json | 0 .../data => inkfiles}/choices/label-flow.ink | 0 .../choices/label-flow.ink.json | 0 .../choices/label-scope-error.ink | 0 .../choices/label-scope-error.ink.json | 0 .../data => inkfiles}/choices/label-scope.ink | 0 .../choices/label-scope.ink.json | 0 .../choices/mixed-choice.ink | 0 .../choices/mixed-choice.ink.json | 0 .../choices/multi-choice.ink | 0 .../choices/multi-choice.ink.json | 0 .../choices/no-choice-text.ink | 0 .../choices/no-choice-text.ink.json | 0 {lib/tests/data => inkfiles}/choices/one.ink | 0 .../data => inkfiles}/choices/one.ink.json | 0 .../choices/single-choice.ink | 0 .../choices/single-choice.ink.json | 0 .../choices/sticky-choice.ink | 0 .../choices/sticky-choice.ink.json | 0 .../choices/suppress-choice.ink | 0 .../choices/suppress-choice.ink.json | 0 .../choices/varying-choice.ink | 0 .../choices/varying-choice.ink.json | 0 .../data => inkfiles}/conditional/condopt.ink | 0 .../conditional/condopt.ink.json | 0 .../conditional/condtext.ink | 0 .../conditional/condtext.ink.json | 0 .../data => inkfiles}/conditional/cycle.ink | 0 .../conditional/cycle.ink.json | 0 .../conditional/ifelse-ext-text1.ink | 0 .../conditional/ifelse-ext-text1.ink.json | 0 .../conditional/ifelse-ext-text2.ink | 0 .../conditional/ifelse-ext-text2.ink.json | 0 .../conditional/ifelse-ext-text3.ink | 0 .../conditional/ifelse-ext-text3.ink.json | 0 .../conditional/ifelse-ext.ink | 0 .../conditional/ifelse-ext.ink.json | 0 .../data => inkfiles}/conditional/ifelse.ink | 0 .../conditional/ifelse.ink.json | 0 .../data => inkfiles}/conditional/iffalse.ink | 0 .../conditional/iffalse.ink.json | 0 .../data => inkfiles}/conditional/iftrue.ink | 0 .../conditional/iftrue.ink.json | 0 .../conditional/multiline-choice.ink | 0 .../conditional/multiline-choice.ink.json | 0 .../conditional/multiline-divert.ink | 0 .../conditional/multiline-divert.ink.json | 0 .../conditional/multiline.ink | 0 .../conditional/multiline.ink.json | 0 .../data => inkfiles}/conditional/once.ink | 0 .../conditional/once.ink.json | 0 .../data => inkfiles}/conditional/shuffle.ink | 0 .../conditional/shuffle.ink.json | 0 .../conditional/shuffle_once.ink | 0 .../conditional/shuffle_once.ink.json | 0 .../conditional/shuffle_stopping.ink | 0 .../conditional/shuffle_stopping.ink.json | 0 .../conditional/stopping.ink | 0 .../conditional/stopping.ink.json | 0 .../divert/complex-branching.ink | 0 .../divert/complex-branching.ink.json | 0 .../divert/divert-on-choice.ink | 0 .../divert/divert-on-choice.ink.json | 0 .../divert/invisible-divert.ink | 0 .../divert/invisible-divert.ink.json | 0 .../divert/simple-divert.ink | 0 .../divert/simple-divert.ink.json | 0 .../function/complex-func1.ink | 0 .../function/complex-func1.ink.json | 0 .../function/complex-func2.ink | 0 .../function/complex-func2.ink.json | 0 .../function/complex-func3.ink | 0 .../function/complex-func3.ink.json | 0 .../evaluating-function-variablestate-bug.ink | 0 ...uating-function-variablestate-bug.ink.json | 0 .../data => inkfiles}/function/func-basic.ink | 0 .../function/func-basic.ink.json | 0 .../function/func-inline.ink | 0 .../function/func-inline.ink.json | 0 .../data => inkfiles}/function/func-none.ink | 0 .../function/func-none.ink.json | 0 .../data => inkfiles}/function/rnd-func.ink | 0 .../function/rnd-func.ink.json | 0 .../function/setvar-func.ink | 0 .../function/setvar-func.ink.json | 0 .../data => inkfiles}/function/test-error.ink | 0 .../function/test-error.ink.json | 0 .../data => inkfiles}/gather/complex-flow.ink | 0 .../gather/complex-flow.ink.json | 0 .../data => inkfiles}/gather/deep-nesting.ink | 0 .../gather/deep-nesting.ink.json | 0 .../data => inkfiles}/gather/gather-basic.ink | 0 .../gather/gather-basic.ink.json | 0 .../data => inkfiles}/gather/gather-chain.ink | 0 .../gather/gather-chain.ink.json | 0 .../data => inkfiles}/gather/nested-flow.ink | 0 .../gather/nested-flow.ink.json | 0 .../gather/nested-gather.ink | 0 .../gather/nested-gather.ink.json | 0 .../glue/glue-with-divert.ink | 0 .../glue/glue-with-divert.ink.json | 0 .../glue/left-right-glue-matching.ink | 0 .../glue/left-right-glue-matching.ink.json | 0 .../data => inkfiles}/glue/simple-glue.ink | 0 .../glue/simple-glue.ink.json | 0 .../data => inkfiles}/glue/testbugfix1.ink | 0 .../glue/testbugfix1.ink.json | 0 .../data => inkfiles}/glue/testbugfix2.ink | 0 .../glue/testbugfix2.ink.json | 0 .../data => inkfiles}/knot/multi-line.ink | 0 .../knot/multi-line.ink.json | 0 .../data => inkfiles}/knot/param-floats.ink | 0 .../knot/param-floats.ink.json | 0 .../data => inkfiles}/knot/param-ints.ink | 0 .../knot/param-ints.ink.json | 0 .../data => inkfiles}/knot/param-multi.ink | 0 .../knot/param-multi.ink.json | 0 .../data => inkfiles}/knot/param-recurse.ink | 0 .../knot/param-recurse.ink.json | 0 .../data => inkfiles}/knot/param-strings.ink | 0 .../knot/param-strings.ink.json | 0 .../data => inkfiles}/knot/param-vars.ink | 0 .../knot/param-vars.ink.json | 0 .../data => inkfiles}/knot/single-line.ink | 0 .../knot/single-line.ink.json | 0 .../knot/strip-empty-lines.ink | 0 .../knot/strip-empty-lines.ink.json | 0 .../lists/basic-operations.ink | 0 .../lists/basic-operations.ink.json | 0 .../lists/bug-adding-element.ink | 0 .../lists/bug-adding-element.ink.json | 0 .../empty-list-origin-after-assignment.ink | 0 ...mpty-list-origin-after-assignment.ink.json | 0 .../lists/empty-list-origin.ink | 0 .../lists/empty-list-origin.ink.json | 0 .../lists/list-mixed-items.ink | 0 .../lists/list-mixed-items.ink.json | 0 .../data => inkfiles}/lists/list-range.ink | 0 .../lists/list-range.ink.json | 0 .../lists/list-save-load.ink | 0 .../lists/list-save-load.ink.json | 0 .../lists/more-list-operations.ink | 0 .../lists/more-list-operations.ink.json | 0 .../lists/more-list-operations2.ink | 0 .../lists/more-list-operations2.ink.json | 0 {lib/tests/data => inkfiles}/misc/issue15.ink | 0 .../data => inkfiles}/misc/issue15.ink.json | 0 .../data => inkfiles}/misc/operations.ink | 0 .../misc/operations.ink.json | 0 .../data => inkfiles}/misc/read-counts.ink | 0 .../misc/read-counts.ink.json | 0 .../data => inkfiles}/misc/turns-since.ink | 0 .../misc/turns-since.ink.json | 0 .../runtime/external-function-0-arg.ink | 0 .../runtime/external-function-0-arg.ink.json | 0 .../runtime/external-function-1-arg.ink | 0 .../runtime/external-function-1-arg.ink.json | 0 .../runtime/external-function-2-arg.ink | 0 .../runtime/external-function-2-arg.ink.json | 0 .../runtime/external-function-3-arg.ink | 0 .../runtime/external-function-3-arg.ink.json | 0 .../data => inkfiles}/runtime/jump-knot.ink | 0 .../runtime/jump-knot.ink.json | 0 .../data => inkfiles}/runtime/jump-stitch.ink | 0 .../runtime/jump-stitch.ink.json | 0 .../data => inkfiles}/runtime/load-save.ink | 0 .../runtime/load-save.ink.json | 0 .../runtime/multiflow-basics.ink | 0 .../runtime/multiflow-basics.ink.json | 0 .../runtime/multiflow-saveloadthreads.ink | 0 .../multiflow-saveloadthreads.ink.json | 0 .../runtime/read-visit-counts.ink | 0 .../runtime/read-visit-counts.ink.json | 0 .../runtime/saving-loading.ink | 0 .../runtime/saving-loading.ink.json | 0 .../runtime/set-get-variables.ink | 0 .../runtime/set-get-variables.ink.json | 0 .../runtime/variable-observers.ink | 0 .../runtime/variable-observers.ink.json | 0 .../data => inkfiles}/stitch/auto-stitch.ink | 0 .../stitch/auto-stitch.ink.json | 0 .../stitch/manual-stitch.ink | 0 .../stitch/manual-stitch.ink.json | 0 {lib/tests/data => inkfiles}/tags/tags.ink | 0 .../data => inkfiles}/tags/tags.ink.json | 0 .../tags/tagsDynamicContent.ink | 0 .../tags/tagsDynamicContent.ink.json | 0 .../data => inkfiles}/tags/tagsInChoice.ink | 0 .../tags/tagsInChoice.ink.json | 0 .../data => inkfiles}/tags/tagsInSeq.ink | 0 .../data => inkfiles}/tags/tagsInSeq.ink.json | 0 {cli-player/tests/data => inkfiles}/test1.ink | 0 .../tests/data => inkfiles}/test1.ink.json | 0 .../data => inkfiles}/threads/thread-bug.ink | 0 .../threads/thread-bug.ink.json | 0 .../tunnel-onwards-divert-override.ink | 0 .../tunnel-onwards-divert-override.ink.json | 0 .../data => inkfiles}/variable/var-divert.ink | 0 .../variable/var-divert.ink.json | 0 .../data => inkfiles}/variable/varcalc.ink | 0 .../variable/varcalc.ink.json | 0 .../variable/variable-declaration.ink | 0 .../variable/variable-declaration.ink.json | 0 .../variable/varstringinc.ink | 0 .../variable/varstringinc.ink.json | 0 .../data => inkfiles}/variabletext/cycle.ink | 0 .../variabletext/cycle.ink.json | 0 .../variabletext/empty-elements.ink | 0 .../variabletext/empty-elements.ink.json | 0 .../variabletext/list-in-choice.ink | 0 .../variabletext/list-in-choice.ink.json | 0 .../data => inkfiles}/variabletext/once.ink | 0 .../variabletext/once.ink.json | 0 .../variabletext/sequence.ink | 0 .../variabletext/sequence.ink.json | 0 lib/src/story.rs | 2 +- lib/tests/basic_text_test.rs | 4 +- lib/tests/choice_test.rs | 34 ++++++++-------- lib/tests/common/mod.rs | 8 ++-- lib/tests/conditional_test.rs | 40 +++++++++---------- lib/tests/divert_test.rs | 10 ++--- lib/tests/function_test.rs | 18 ++++----- lib/tests/gather_test.rs | 12 +++--- lib/tests/glue_test.rs | 10 ++--- lib/tests/knot_test.rs | 18 ++++----- lib/tests/list_test.rs | 18 ++++----- lib/tests/misc_test.rs | 8 ++-- lib/tests/multi_flow_test.rs | 4 +- lib/tests/runtime_test.rs | 24 +++++------ lib/tests/stitch_test.rs | 8 ++-- lib/tests/tag_test.rs | 8 ++-- lib/tests/thread_test.rs | 4 +- lib/tests/tunnel_test.rs | 2 +- lib/tests/variable_test.rs | 8 ++-- lib/tests/variable_text_test.rs | 10 ++--- 248 files changed, 145 insertions(+), 127 deletions(-) rename {cli-player/tests/data => inkfiles}/TheIntercept.ink (100%) rename {cli-player/tests/data => inkfiles}/TheIntercept.ink.json (100%) rename {lib/tests/data => inkfiles}/basictext/oneline.ink (100%) rename {lib/tests/data => inkfiles}/basictext/oneline.ink.json (100%) rename {lib/tests/data => inkfiles}/basictext/twolines.ink (100%) rename {lib/tests/data => inkfiles}/basictext/twolines.ink.json (100%) rename {lib/tests/data => inkfiles}/choices/conditional-choice.ink (100%) rename {lib/tests/data => inkfiles}/choices/conditional-choice.ink.json (100%) rename {lib/tests/data => inkfiles}/choices/divert-choice.ink (100%) rename {lib/tests/data => inkfiles}/choices/divert-choice.ink.json (100%) rename {lib/tests/data => inkfiles}/choices/fallback-choice.ink (100%) rename {lib/tests/data => inkfiles}/choices/fallback-choice.ink.json (100%) rename {lib/tests/data => inkfiles}/choices/label-flow.ink (100%) rename {lib/tests/data => inkfiles}/choices/label-flow.ink.json (100%) rename {lib/tests/data => inkfiles}/choices/label-scope-error.ink (100%) rename {lib/tests/data => inkfiles}/choices/label-scope-error.ink.json (100%) rename {lib/tests/data => inkfiles}/choices/label-scope.ink (100%) rename {lib/tests/data => inkfiles}/choices/label-scope.ink.json (100%) rename {lib/tests/data => inkfiles}/choices/mixed-choice.ink (100%) rename {lib/tests/data => inkfiles}/choices/mixed-choice.ink.json (100%) rename {lib/tests/data => inkfiles}/choices/multi-choice.ink (100%) rename {lib/tests/data => inkfiles}/choices/multi-choice.ink.json (100%) rename {lib/tests/data => inkfiles}/choices/no-choice-text.ink (100%) rename {lib/tests/data => inkfiles}/choices/no-choice-text.ink.json (100%) rename {lib/tests/data => inkfiles}/choices/one.ink (100%) rename {lib/tests/data => inkfiles}/choices/one.ink.json (100%) rename {lib/tests/data => inkfiles}/choices/single-choice.ink (100%) rename {lib/tests/data => inkfiles}/choices/single-choice.ink.json (100%) rename {lib/tests/data => inkfiles}/choices/sticky-choice.ink (100%) rename {lib/tests/data => inkfiles}/choices/sticky-choice.ink.json (100%) rename {lib/tests/data => inkfiles}/choices/suppress-choice.ink (100%) rename {lib/tests/data => inkfiles}/choices/suppress-choice.ink.json (100%) rename {lib/tests/data => inkfiles}/choices/varying-choice.ink (100%) rename {lib/tests/data => inkfiles}/choices/varying-choice.ink.json (100%) rename {lib/tests/data => inkfiles}/conditional/condopt.ink (100%) rename {lib/tests/data => inkfiles}/conditional/condopt.ink.json (100%) rename {lib/tests/data => inkfiles}/conditional/condtext.ink (100%) rename {lib/tests/data => inkfiles}/conditional/condtext.ink.json (100%) rename {lib/tests/data => inkfiles}/conditional/cycle.ink (100%) rename {lib/tests/data => inkfiles}/conditional/cycle.ink.json (100%) rename {lib/tests/data => inkfiles}/conditional/ifelse-ext-text1.ink (100%) rename {lib/tests/data => inkfiles}/conditional/ifelse-ext-text1.ink.json (100%) rename {lib/tests/data => inkfiles}/conditional/ifelse-ext-text2.ink (100%) rename {lib/tests/data => inkfiles}/conditional/ifelse-ext-text2.ink.json (100%) rename {lib/tests/data => inkfiles}/conditional/ifelse-ext-text3.ink (100%) rename {lib/tests/data => inkfiles}/conditional/ifelse-ext-text3.ink.json (100%) rename {lib/tests/data => inkfiles}/conditional/ifelse-ext.ink (100%) rename {lib/tests/data => inkfiles}/conditional/ifelse-ext.ink.json (100%) rename {lib/tests/data => inkfiles}/conditional/ifelse.ink (100%) rename {lib/tests/data => inkfiles}/conditional/ifelse.ink.json (100%) rename {lib/tests/data => inkfiles}/conditional/iffalse.ink (100%) rename {lib/tests/data => inkfiles}/conditional/iffalse.ink.json (100%) rename {lib/tests/data => inkfiles}/conditional/iftrue.ink (100%) rename {lib/tests/data => inkfiles}/conditional/iftrue.ink.json (100%) rename {lib/tests/data => inkfiles}/conditional/multiline-choice.ink (100%) rename {lib/tests/data => inkfiles}/conditional/multiline-choice.ink.json (100%) rename {lib/tests/data => inkfiles}/conditional/multiline-divert.ink (100%) rename {lib/tests/data => inkfiles}/conditional/multiline-divert.ink.json (100%) rename {lib/tests/data => inkfiles}/conditional/multiline.ink (100%) rename {lib/tests/data => inkfiles}/conditional/multiline.ink.json (100%) rename {lib/tests/data => inkfiles}/conditional/once.ink (100%) rename {lib/tests/data => inkfiles}/conditional/once.ink.json (100%) rename {lib/tests/data => inkfiles}/conditional/shuffle.ink (100%) rename {lib/tests/data => inkfiles}/conditional/shuffle.ink.json (100%) rename {lib/tests/data => inkfiles}/conditional/shuffle_once.ink (100%) rename {lib/tests/data => inkfiles}/conditional/shuffle_once.ink.json (100%) rename {lib/tests/data => inkfiles}/conditional/shuffle_stopping.ink (100%) rename {lib/tests/data => inkfiles}/conditional/shuffle_stopping.ink.json (100%) rename {lib/tests/data => inkfiles}/conditional/stopping.ink (100%) rename {lib/tests/data => inkfiles}/conditional/stopping.ink.json (100%) rename {lib/tests/data => inkfiles}/divert/complex-branching.ink (100%) rename {lib/tests/data => inkfiles}/divert/complex-branching.ink.json (100%) rename {lib/tests/data => inkfiles}/divert/divert-on-choice.ink (100%) rename {lib/tests/data => inkfiles}/divert/divert-on-choice.ink.json (100%) rename {lib/tests/data => inkfiles}/divert/invisible-divert.ink (100%) rename {lib/tests/data => inkfiles}/divert/invisible-divert.ink.json (100%) rename {lib/tests/data => inkfiles}/divert/simple-divert.ink (100%) rename {lib/tests/data => inkfiles}/divert/simple-divert.ink.json (100%) rename {lib/tests/data => inkfiles}/function/complex-func1.ink (100%) rename {lib/tests/data => inkfiles}/function/complex-func1.ink.json (100%) rename {lib/tests/data => inkfiles}/function/complex-func2.ink (100%) rename {lib/tests/data => inkfiles}/function/complex-func2.ink.json (100%) rename {lib/tests/data => inkfiles}/function/complex-func3.ink (100%) rename {lib/tests/data => inkfiles}/function/complex-func3.ink.json (100%) rename {lib/tests/data => inkfiles}/function/evaluating-function-variablestate-bug.ink (100%) rename {lib/tests/data => inkfiles}/function/evaluating-function-variablestate-bug.ink.json (100%) rename {lib/tests/data => inkfiles}/function/func-basic.ink (100%) rename {lib/tests/data => inkfiles}/function/func-basic.ink.json (100%) rename {lib/tests/data => inkfiles}/function/func-inline.ink (100%) rename {lib/tests/data => inkfiles}/function/func-inline.ink.json (100%) rename {lib/tests/data => inkfiles}/function/func-none.ink (100%) rename {lib/tests/data => inkfiles}/function/func-none.ink.json (100%) rename {lib/tests/data => inkfiles}/function/rnd-func.ink (100%) rename {lib/tests/data => inkfiles}/function/rnd-func.ink.json (100%) rename {lib/tests/data => inkfiles}/function/setvar-func.ink (100%) rename {lib/tests/data => inkfiles}/function/setvar-func.ink.json (100%) rename {lib/tests/data => inkfiles}/function/test-error.ink (100%) rename {lib/tests/data => inkfiles}/function/test-error.ink.json (100%) rename {lib/tests/data => inkfiles}/gather/complex-flow.ink (100%) rename {lib/tests/data => inkfiles}/gather/complex-flow.ink.json (100%) rename {lib/tests/data => inkfiles}/gather/deep-nesting.ink (100%) rename {lib/tests/data => inkfiles}/gather/deep-nesting.ink.json (100%) rename {lib/tests/data => inkfiles}/gather/gather-basic.ink (100%) rename {lib/tests/data => inkfiles}/gather/gather-basic.ink.json (100%) rename {lib/tests/data => inkfiles}/gather/gather-chain.ink (100%) rename {lib/tests/data => inkfiles}/gather/gather-chain.ink.json (100%) rename {lib/tests/data => inkfiles}/gather/nested-flow.ink (100%) rename {lib/tests/data => inkfiles}/gather/nested-flow.ink.json (100%) rename {lib/tests/data => inkfiles}/gather/nested-gather.ink (100%) rename {lib/tests/data => inkfiles}/gather/nested-gather.ink.json (100%) rename {lib/tests/data => inkfiles}/glue/glue-with-divert.ink (100%) rename {lib/tests/data => inkfiles}/glue/glue-with-divert.ink.json (100%) rename {lib/tests/data => inkfiles}/glue/left-right-glue-matching.ink (100%) rename {lib/tests/data => inkfiles}/glue/left-right-glue-matching.ink.json (100%) rename {lib/tests/data => inkfiles}/glue/simple-glue.ink (100%) rename {lib/tests/data => inkfiles}/glue/simple-glue.ink.json (100%) rename {lib/tests/data => inkfiles}/glue/testbugfix1.ink (100%) rename {lib/tests/data => inkfiles}/glue/testbugfix1.ink.json (100%) rename {lib/tests/data => inkfiles}/glue/testbugfix2.ink (100%) rename {lib/tests/data => inkfiles}/glue/testbugfix2.ink.json (100%) rename {lib/tests/data => inkfiles}/knot/multi-line.ink (100%) rename {lib/tests/data => inkfiles}/knot/multi-line.ink.json (100%) rename {lib/tests/data => inkfiles}/knot/param-floats.ink (100%) rename {lib/tests/data => inkfiles}/knot/param-floats.ink.json (100%) rename {lib/tests/data => inkfiles}/knot/param-ints.ink (100%) rename {lib/tests/data => inkfiles}/knot/param-ints.ink.json (100%) rename {lib/tests/data => inkfiles}/knot/param-multi.ink (100%) rename {lib/tests/data => inkfiles}/knot/param-multi.ink.json (100%) rename {lib/tests/data => inkfiles}/knot/param-recurse.ink (100%) rename {lib/tests/data => inkfiles}/knot/param-recurse.ink.json (100%) rename {lib/tests/data => inkfiles}/knot/param-strings.ink (100%) rename {lib/tests/data => inkfiles}/knot/param-strings.ink.json (100%) rename {lib/tests/data => inkfiles}/knot/param-vars.ink (100%) rename {lib/tests/data => inkfiles}/knot/param-vars.ink.json (100%) rename {lib/tests/data => inkfiles}/knot/single-line.ink (100%) rename {lib/tests/data => inkfiles}/knot/single-line.ink.json (100%) rename {lib/tests/data => inkfiles}/knot/strip-empty-lines.ink (100%) rename {lib/tests/data => inkfiles}/knot/strip-empty-lines.ink.json (100%) rename {lib/tests/data => inkfiles}/lists/basic-operations.ink (100%) rename {lib/tests/data => inkfiles}/lists/basic-operations.ink.json (100%) rename {lib/tests/data => inkfiles}/lists/bug-adding-element.ink (100%) rename {lib/tests/data => inkfiles}/lists/bug-adding-element.ink.json (100%) rename {lib/tests/data => inkfiles}/lists/empty-list-origin-after-assignment.ink (100%) rename {lib/tests/data => inkfiles}/lists/empty-list-origin-after-assignment.ink.json (100%) rename {lib/tests/data => inkfiles}/lists/empty-list-origin.ink (100%) rename {lib/tests/data => inkfiles}/lists/empty-list-origin.ink.json (100%) rename {lib/tests/data => inkfiles}/lists/list-mixed-items.ink (100%) rename {lib/tests/data => inkfiles}/lists/list-mixed-items.ink.json (100%) rename {lib/tests/data => inkfiles}/lists/list-range.ink (100%) rename {lib/tests/data => inkfiles}/lists/list-range.ink.json (100%) rename {lib/tests/data => inkfiles}/lists/list-save-load.ink (100%) rename {lib/tests/data => inkfiles}/lists/list-save-load.ink.json (100%) rename {lib/tests/data => inkfiles}/lists/more-list-operations.ink (100%) rename {lib/tests/data => inkfiles}/lists/more-list-operations.ink.json (100%) rename {lib/tests/data => inkfiles}/lists/more-list-operations2.ink (100%) rename {lib/tests/data => inkfiles}/lists/more-list-operations2.ink.json (100%) rename {lib/tests/data => inkfiles}/misc/issue15.ink (100%) rename {lib/tests/data => inkfiles}/misc/issue15.ink.json (100%) rename {lib/tests/data => inkfiles}/misc/operations.ink (100%) rename {lib/tests/data => inkfiles}/misc/operations.ink.json (100%) rename {lib/tests/data => inkfiles}/misc/read-counts.ink (100%) rename {lib/tests/data => inkfiles}/misc/read-counts.ink.json (100%) rename {lib/tests/data => inkfiles}/misc/turns-since.ink (100%) rename {lib/tests/data => inkfiles}/misc/turns-since.ink.json (100%) rename {lib/tests/data => inkfiles}/runtime/external-function-0-arg.ink (100%) rename {lib/tests/data => inkfiles}/runtime/external-function-0-arg.ink.json (100%) rename {lib/tests/data => inkfiles}/runtime/external-function-1-arg.ink (100%) rename {lib/tests/data => inkfiles}/runtime/external-function-1-arg.ink.json (100%) rename {lib/tests/data => inkfiles}/runtime/external-function-2-arg.ink (100%) rename {lib/tests/data => inkfiles}/runtime/external-function-2-arg.ink.json (100%) rename {lib/tests/data => inkfiles}/runtime/external-function-3-arg.ink (100%) rename {lib/tests/data => inkfiles}/runtime/external-function-3-arg.ink.json (100%) rename {lib/tests/data => inkfiles}/runtime/jump-knot.ink (100%) rename {lib/tests/data => inkfiles}/runtime/jump-knot.ink.json (100%) rename {lib/tests/data => inkfiles}/runtime/jump-stitch.ink (100%) rename {lib/tests/data => inkfiles}/runtime/jump-stitch.ink.json (100%) rename {lib/tests/data => inkfiles}/runtime/load-save.ink (100%) rename {lib/tests/data => inkfiles}/runtime/load-save.ink.json (100%) rename {lib/tests/data => inkfiles}/runtime/multiflow-basics.ink (100%) rename {lib/tests/data => inkfiles}/runtime/multiflow-basics.ink.json (100%) rename {lib/tests/data => inkfiles}/runtime/multiflow-saveloadthreads.ink (100%) rename {lib/tests/data => inkfiles}/runtime/multiflow-saveloadthreads.ink.json (100%) rename {lib/tests/data => inkfiles}/runtime/read-visit-counts.ink (100%) rename {lib/tests/data => inkfiles}/runtime/read-visit-counts.ink.json (100%) rename {lib/tests/data => inkfiles}/runtime/saving-loading.ink (100%) rename {lib/tests/data => inkfiles}/runtime/saving-loading.ink.json (100%) rename {lib/tests/data => inkfiles}/runtime/set-get-variables.ink (100%) rename {lib/tests/data => inkfiles}/runtime/set-get-variables.ink.json (100%) rename {lib/tests/data => inkfiles}/runtime/variable-observers.ink (100%) rename {lib/tests/data => inkfiles}/runtime/variable-observers.ink.json (100%) rename {lib/tests/data => inkfiles}/stitch/auto-stitch.ink (100%) rename {lib/tests/data => inkfiles}/stitch/auto-stitch.ink.json (100%) rename {lib/tests/data => inkfiles}/stitch/manual-stitch.ink (100%) rename {lib/tests/data => inkfiles}/stitch/manual-stitch.ink.json (100%) rename {lib/tests/data => inkfiles}/tags/tags.ink (100%) rename {lib/tests/data => inkfiles}/tags/tags.ink.json (100%) rename {lib/tests/data => inkfiles}/tags/tagsDynamicContent.ink (100%) rename {lib/tests/data => inkfiles}/tags/tagsDynamicContent.ink.json (100%) rename {lib/tests/data => inkfiles}/tags/tagsInChoice.ink (100%) rename {lib/tests/data => inkfiles}/tags/tagsInChoice.ink.json (100%) rename {lib/tests/data => inkfiles}/tags/tagsInSeq.ink (100%) rename {lib/tests/data => inkfiles}/tags/tagsInSeq.ink.json (100%) rename {cli-player/tests/data => inkfiles}/test1.ink (100%) rename {cli-player/tests/data => inkfiles}/test1.ink.json (100%) rename {lib/tests/data => inkfiles}/threads/thread-bug.ink (100%) rename {lib/tests/data => inkfiles}/threads/thread-bug.ink.json (100%) rename {lib/tests/data => inkfiles}/tunnels/tunnel-onwards-divert-override.ink (100%) rename {lib/tests/data => inkfiles}/tunnels/tunnel-onwards-divert-override.ink.json (100%) rename {lib/tests/data => inkfiles}/variable/var-divert.ink (100%) rename {lib/tests/data => inkfiles}/variable/var-divert.ink.json (100%) rename {lib/tests/data => inkfiles}/variable/varcalc.ink (100%) rename {lib/tests/data => inkfiles}/variable/varcalc.ink.json (100%) rename {lib/tests/data => inkfiles}/variable/variable-declaration.ink (100%) rename {lib/tests/data => inkfiles}/variable/variable-declaration.ink.json (100%) rename {lib/tests/data => inkfiles}/variable/varstringinc.ink (100%) rename {lib/tests/data => inkfiles}/variable/varstringinc.ink.json (100%) rename {lib/tests/data => inkfiles}/variabletext/cycle.ink (100%) rename {lib/tests/data => inkfiles}/variabletext/cycle.ink.json (100%) rename {lib/tests/data => inkfiles}/variabletext/empty-elements.ink (100%) rename {lib/tests/data => inkfiles}/variabletext/empty-elements.ink.json (100%) rename {lib/tests/data => inkfiles}/variabletext/list-in-choice.ink (100%) rename {lib/tests/data => inkfiles}/variabletext/list-in-choice.ink.json (100%) rename {lib/tests/data => inkfiles}/variabletext/once.ink (100%) rename {lib/tests/data => inkfiles}/variabletext/once.ink.json (100%) rename {lib/tests/data => inkfiles}/variabletext/sequence.ink (100%) rename {lib/tests/data => inkfiles}/variabletext/sequence.ink.json (100%) diff --git a/cli-player/tests/basic_tests.rs b/cli-player/tests/basic_tests.rs index 62fcc26..ac9806a 100644 --- a/cli-player/tests/basic_tests.rs +++ b/cli-player/tests/basic_tests.rs @@ -1,13 +1,21 @@ use assert_cmd::prelude::*; use predicates::prelude::predicate; // Add methods on commands use std::io::Write; +use std::path::Path; use std::process::{Command, Stdio}; #[test] fn basic_story_test() -> Result<(), Box> { let mut cmd = Command::cargo_bin("binkplayer")?; - cmd.arg("tests/data/test1.ink.json"); + let mut path = Path::new("inkfiles/test1.ink.json").to_path_buf(); + + // Due to a bug with Cargo workspaces, for Release mode the current folder is the crate folder and for Debug mode the current folder is the root folder. + if !path.exists() { + path = Path::new("../").join(path); + } + + cmd.arg(path); cmd.stdin(Stdio::piped()); cmd.stdout(Stdio::piped()); diff --git a/cli-player/tests/test_the_intercept.rs b/cli-player/tests/test_the_intercept.rs index 3c4a16c..3c18d2a 100644 --- a/cli-player/tests/test_the_intercept.rs +++ b/cli-player/tests/test_the_intercept.rs @@ -1,12 +1,20 @@ use assert_cmd::prelude::*; use std::io::Write; +use std::path::Path; use std::process::{Command, Stdio}; #[test] -fn the_intercept_test() -> Result<(), Box> { +fn the_intercept_test() -> Result<(), Box> { let mut cmd = Command::cargo_bin("binkplayer")?; - cmd.arg("tests/data/TheIntercept.ink.json"); + let mut path = Path::new("inkfiles/TheIntercept.ink.json").to_path_buf(); + + // Due to a bug with Cargo workspaces, for Release mode the current folder is the crate folder and for Debug mode the current folder is the root folder. + if !path.exists() { + path = Path::new("../").join(path); + } + + cmd.arg(path); cmd.stdin(Stdio::piped()); cmd.stdout(Stdio::piped()); diff --git a/cli-player/tests/data/TheIntercept.ink b/inkfiles/TheIntercept.ink similarity index 100% rename from cli-player/tests/data/TheIntercept.ink rename to inkfiles/TheIntercept.ink diff --git a/cli-player/tests/data/TheIntercept.ink.json b/inkfiles/TheIntercept.ink.json similarity index 100% rename from cli-player/tests/data/TheIntercept.ink.json rename to inkfiles/TheIntercept.ink.json diff --git a/lib/tests/data/basictext/oneline.ink b/inkfiles/basictext/oneline.ink similarity index 100% rename from lib/tests/data/basictext/oneline.ink rename to inkfiles/basictext/oneline.ink diff --git a/lib/tests/data/basictext/oneline.ink.json b/inkfiles/basictext/oneline.ink.json similarity index 100% rename from lib/tests/data/basictext/oneline.ink.json rename to inkfiles/basictext/oneline.ink.json diff --git a/lib/tests/data/basictext/twolines.ink b/inkfiles/basictext/twolines.ink similarity index 100% rename from lib/tests/data/basictext/twolines.ink rename to inkfiles/basictext/twolines.ink diff --git a/lib/tests/data/basictext/twolines.ink.json b/inkfiles/basictext/twolines.ink.json similarity index 100% rename from lib/tests/data/basictext/twolines.ink.json rename to inkfiles/basictext/twolines.ink.json diff --git a/lib/tests/data/choices/conditional-choice.ink b/inkfiles/choices/conditional-choice.ink similarity index 100% rename from lib/tests/data/choices/conditional-choice.ink rename to inkfiles/choices/conditional-choice.ink diff --git a/lib/tests/data/choices/conditional-choice.ink.json b/inkfiles/choices/conditional-choice.ink.json similarity index 100% rename from lib/tests/data/choices/conditional-choice.ink.json rename to inkfiles/choices/conditional-choice.ink.json diff --git a/lib/tests/data/choices/divert-choice.ink b/inkfiles/choices/divert-choice.ink similarity index 100% rename from lib/tests/data/choices/divert-choice.ink rename to inkfiles/choices/divert-choice.ink diff --git a/lib/tests/data/choices/divert-choice.ink.json b/inkfiles/choices/divert-choice.ink.json similarity index 100% rename from lib/tests/data/choices/divert-choice.ink.json rename to inkfiles/choices/divert-choice.ink.json diff --git a/lib/tests/data/choices/fallback-choice.ink b/inkfiles/choices/fallback-choice.ink similarity index 100% rename from lib/tests/data/choices/fallback-choice.ink rename to inkfiles/choices/fallback-choice.ink diff --git a/lib/tests/data/choices/fallback-choice.ink.json b/inkfiles/choices/fallback-choice.ink.json similarity index 100% rename from lib/tests/data/choices/fallback-choice.ink.json rename to inkfiles/choices/fallback-choice.ink.json diff --git a/lib/tests/data/choices/label-flow.ink b/inkfiles/choices/label-flow.ink similarity index 100% rename from lib/tests/data/choices/label-flow.ink rename to inkfiles/choices/label-flow.ink diff --git a/lib/tests/data/choices/label-flow.ink.json b/inkfiles/choices/label-flow.ink.json similarity index 100% rename from lib/tests/data/choices/label-flow.ink.json rename to inkfiles/choices/label-flow.ink.json diff --git a/lib/tests/data/choices/label-scope-error.ink b/inkfiles/choices/label-scope-error.ink similarity index 100% rename from lib/tests/data/choices/label-scope-error.ink rename to inkfiles/choices/label-scope-error.ink diff --git a/lib/tests/data/choices/label-scope-error.ink.json b/inkfiles/choices/label-scope-error.ink.json similarity index 100% rename from lib/tests/data/choices/label-scope-error.ink.json rename to inkfiles/choices/label-scope-error.ink.json diff --git a/lib/tests/data/choices/label-scope.ink b/inkfiles/choices/label-scope.ink similarity index 100% rename from lib/tests/data/choices/label-scope.ink rename to inkfiles/choices/label-scope.ink diff --git a/lib/tests/data/choices/label-scope.ink.json b/inkfiles/choices/label-scope.ink.json similarity index 100% rename from lib/tests/data/choices/label-scope.ink.json rename to inkfiles/choices/label-scope.ink.json diff --git a/lib/tests/data/choices/mixed-choice.ink b/inkfiles/choices/mixed-choice.ink similarity index 100% rename from lib/tests/data/choices/mixed-choice.ink rename to inkfiles/choices/mixed-choice.ink diff --git a/lib/tests/data/choices/mixed-choice.ink.json b/inkfiles/choices/mixed-choice.ink.json similarity index 100% rename from lib/tests/data/choices/mixed-choice.ink.json rename to inkfiles/choices/mixed-choice.ink.json diff --git a/lib/tests/data/choices/multi-choice.ink b/inkfiles/choices/multi-choice.ink similarity index 100% rename from lib/tests/data/choices/multi-choice.ink rename to inkfiles/choices/multi-choice.ink diff --git a/lib/tests/data/choices/multi-choice.ink.json b/inkfiles/choices/multi-choice.ink.json similarity index 100% rename from lib/tests/data/choices/multi-choice.ink.json rename to inkfiles/choices/multi-choice.ink.json diff --git a/lib/tests/data/choices/no-choice-text.ink b/inkfiles/choices/no-choice-text.ink similarity index 100% rename from lib/tests/data/choices/no-choice-text.ink rename to inkfiles/choices/no-choice-text.ink diff --git a/lib/tests/data/choices/no-choice-text.ink.json b/inkfiles/choices/no-choice-text.ink.json similarity index 100% rename from lib/tests/data/choices/no-choice-text.ink.json rename to inkfiles/choices/no-choice-text.ink.json diff --git a/lib/tests/data/choices/one.ink b/inkfiles/choices/one.ink similarity index 100% rename from lib/tests/data/choices/one.ink rename to inkfiles/choices/one.ink diff --git a/lib/tests/data/choices/one.ink.json b/inkfiles/choices/one.ink.json similarity index 100% rename from lib/tests/data/choices/one.ink.json rename to inkfiles/choices/one.ink.json diff --git a/lib/tests/data/choices/single-choice.ink b/inkfiles/choices/single-choice.ink similarity index 100% rename from lib/tests/data/choices/single-choice.ink rename to inkfiles/choices/single-choice.ink diff --git a/lib/tests/data/choices/single-choice.ink.json b/inkfiles/choices/single-choice.ink.json similarity index 100% rename from lib/tests/data/choices/single-choice.ink.json rename to inkfiles/choices/single-choice.ink.json diff --git a/lib/tests/data/choices/sticky-choice.ink b/inkfiles/choices/sticky-choice.ink similarity index 100% rename from lib/tests/data/choices/sticky-choice.ink rename to inkfiles/choices/sticky-choice.ink diff --git a/lib/tests/data/choices/sticky-choice.ink.json b/inkfiles/choices/sticky-choice.ink.json similarity index 100% rename from lib/tests/data/choices/sticky-choice.ink.json rename to inkfiles/choices/sticky-choice.ink.json diff --git a/lib/tests/data/choices/suppress-choice.ink b/inkfiles/choices/suppress-choice.ink similarity index 100% rename from lib/tests/data/choices/suppress-choice.ink rename to inkfiles/choices/suppress-choice.ink diff --git a/lib/tests/data/choices/suppress-choice.ink.json b/inkfiles/choices/suppress-choice.ink.json similarity index 100% rename from lib/tests/data/choices/suppress-choice.ink.json rename to inkfiles/choices/suppress-choice.ink.json diff --git a/lib/tests/data/choices/varying-choice.ink b/inkfiles/choices/varying-choice.ink similarity index 100% rename from lib/tests/data/choices/varying-choice.ink rename to inkfiles/choices/varying-choice.ink diff --git a/lib/tests/data/choices/varying-choice.ink.json b/inkfiles/choices/varying-choice.ink.json similarity index 100% rename from lib/tests/data/choices/varying-choice.ink.json rename to inkfiles/choices/varying-choice.ink.json diff --git a/lib/tests/data/conditional/condopt.ink b/inkfiles/conditional/condopt.ink similarity index 100% rename from lib/tests/data/conditional/condopt.ink rename to inkfiles/conditional/condopt.ink diff --git a/lib/tests/data/conditional/condopt.ink.json b/inkfiles/conditional/condopt.ink.json similarity index 100% rename from lib/tests/data/conditional/condopt.ink.json rename to inkfiles/conditional/condopt.ink.json diff --git a/lib/tests/data/conditional/condtext.ink b/inkfiles/conditional/condtext.ink similarity index 100% rename from lib/tests/data/conditional/condtext.ink rename to inkfiles/conditional/condtext.ink diff --git a/lib/tests/data/conditional/condtext.ink.json b/inkfiles/conditional/condtext.ink.json similarity index 100% rename from lib/tests/data/conditional/condtext.ink.json rename to inkfiles/conditional/condtext.ink.json diff --git a/lib/tests/data/conditional/cycle.ink b/inkfiles/conditional/cycle.ink similarity index 100% rename from lib/tests/data/conditional/cycle.ink rename to inkfiles/conditional/cycle.ink diff --git a/lib/tests/data/conditional/cycle.ink.json b/inkfiles/conditional/cycle.ink.json similarity index 100% rename from lib/tests/data/conditional/cycle.ink.json rename to inkfiles/conditional/cycle.ink.json diff --git a/lib/tests/data/conditional/ifelse-ext-text1.ink b/inkfiles/conditional/ifelse-ext-text1.ink similarity index 100% rename from lib/tests/data/conditional/ifelse-ext-text1.ink rename to inkfiles/conditional/ifelse-ext-text1.ink diff --git a/lib/tests/data/conditional/ifelse-ext-text1.ink.json b/inkfiles/conditional/ifelse-ext-text1.ink.json similarity index 100% rename from lib/tests/data/conditional/ifelse-ext-text1.ink.json rename to inkfiles/conditional/ifelse-ext-text1.ink.json diff --git a/lib/tests/data/conditional/ifelse-ext-text2.ink b/inkfiles/conditional/ifelse-ext-text2.ink similarity index 100% rename from lib/tests/data/conditional/ifelse-ext-text2.ink rename to inkfiles/conditional/ifelse-ext-text2.ink diff --git a/lib/tests/data/conditional/ifelse-ext-text2.ink.json b/inkfiles/conditional/ifelse-ext-text2.ink.json similarity index 100% rename from lib/tests/data/conditional/ifelse-ext-text2.ink.json rename to inkfiles/conditional/ifelse-ext-text2.ink.json diff --git a/lib/tests/data/conditional/ifelse-ext-text3.ink b/inkfiles/conditional/ifelse-ext-text3.ink similarity index 100% rename from lib/tests/data/conditional/ifelse-ext-text3.ink rename to inkfiles/conditional/ifelse-ext-text3.ink diff --git a/lib/tests/data/conditional/ifelse-ext-text3.ink.json b/inkfiles/conditional/ifelse-ext-text3.ink.json similarity index 100% rename from lib/tests/data/conditional/ifelse-ext-text3.ink.json rename to inkfiles/conditional/ifelse-ext-text3.ink.json diff --git a/lib/tests/data/conditional/ifelse-ext.ink b/inkfiles/conditional/ifelse-ext.ink similarity index 100% rename from lib/tests/data/conditional/ifelse-ext.ink rename to inkfiles/conditional/ifelse-ext.ink diff --git a/lib/tests/data/conditional/ifelse-ext.ink.json b/inkfiles/conditional/ifelse-ext.ink.json similarity index 100% rename from lib/tests/data/conditional/ifelse-ext.ink.json rename to inkfiles/conditional/ifelse-ext.ink.json diff --git a/lib/tests/data/conditional/ifelse.ink b/inkfiles/conditional/ifelse.ink similarity index 100% rename from lib/tests/data/conditional/ifelse.ink rename to inkfiles/conditional/ifelse.ink diff --git a/lib/tests/data/conditional/ifelse.ink.json b/inkfiles/conditional/ifelse.ink.json similarity index 100% rename from lib/tests/data/conditional/ifelse.ink.json rename to inkfiles/conditional/ifelse.ink.json diff --git a/lib/tests/data/conditional/iffalse.ink b/inkfiles/conditional/iffalse.ink similarity index 100% rename from lib/tests/data/conditional/iffalse.ink rename to inkfiles/conditional/iffalse.ink diff --git a/lib/tests/data/conditional/iffalse.ink.json b/inkfiles/conditional/iffalse.ink.json similarity index 100% rename from lib/tests/data/conditional/iffalse.ink.json rename to inkfiles/conditional/iffalse.ink.json diff --git a/lib/tests/data/conditional/iftrue.ink b/inkfiles/conditional/iftrue.ink similarity index 100% rename from lib/tests/data/conditional/iftrue.ink rename to inkfiles/conditional/iftrue.ink diff --git a/lib/tests/data/conditional/iftrue.ink.json b/inkfiles/conditional/iftrue.ink.json similarity index 100% rename from lib/tests/data/conditional/iftrue.ink.json rename to inkfiles/conditional/iftrue.ink.json diff --git a/lib/tests/data/conditional/multiline-choice.ink b/inkfiles/conditional/multiline-choice.ink similarity index 100% rename from lib/tests/data/conditional/multiline-choice.ink rename to inkfiles/conditional/multiline-choice.ink diff --git a/lib/tests/data/conditional/multiline-choice.ink.json b/inkfiles/conditional/multiline-choice.ink.json similarity index 100% rename from lib/tests/data/conditional/multiline-choice.ink.json rename to inkfiles/conditional/multiline-choice.ink.json diff --git a/lib/tests/data/conditional/multiline-divert.ink b/inkfiles/conditional/multiline-divert.ink similarity index 100% rename from lib/tests/data/conditional/multiline-divert.ink rename to inkfiles/conditional/multiline-divert.ink diff --git a/lib/tests/data/conditional/multiline-divert.ink.json b/inkfiles/conditional/multiline-divert.ink.json similarity index 100% rename from lib/tests/data/conditional/multiline-divert.ink.json rename to inkfiles/conditional/multiline-divert.ink.json diff --git a/lib/tests/data/conditional/multiline.ink b/inkfiles/conditional/multiline.ink similarity index 100% rename from lib/tests/data/conditional/multiline.ink rename to inkfiles/conditional/multiline.ink diff --git a/lib/tests/data/conditional/multiline.ink.json b/inkfiles/conditional/multiline.ink.json similarity index 100% rename from lib/tests/data/conditional/multiline.ink.json rename to inkfiles/conditional/multiline.ink.json diff --git a/lib/tests/data/conditional/once.ink b/inkfiles/conditional/once.ink similarity index 100% rename from lib/tests/data/conditional/once.ink rename to inkfiles/conditional/once.ink diff --git a/lib/tests/data/conditional/once.ink.json b/inkfiles/conditional/once.ink.json similarity index 100% rename from lib/tests/data/conditional/once.ink.json rename to inkfiles/conditional/once.ink.json diff --git a/lib/tests/data/conditional/shuffle.ink b/inkfiles/conditional/shuffle.ink similarity index 100% rename from lib/tests/data/conditional/shuffle.ink rename to inkfiles/conditional/shuffle.ink diff --git a/lib/tests/data/conditional/shuffle.ink.json b/inkfiles/conditional/shuffle.ink.json similarity index 100% rename from lib/tests/data/conditional/shuffle.ink.json rename to inkfiles/conditional/shuffle.ink.json diff --git a/lib/tests/data/conditional/shuffle_once.ink b/inkfiles/conditional/shuffle_once.ink similarity index 100% rename from lib/tests/data/conditional/shuffle_once.ink rename to inkfiles/conditional/shuffle_once.ink diff --git a/lib/tests/data/conditional/shuffle_once.ink.json b/inkfiles/conditional/shuffle_once.ink.json similarity index 100% rename from lib/tests/data/conditional/shuffle_once.ink.json rename to inkfiles/conditional/shuffle_once.ink.json diff --git a/lib/tests/data/conditional/shuffle_stopping.ink b/inkfiles/conditional/shuffle_stopping.ink similarity index 100% rename from lib/tests/data/conditional/shuffle_stopping.ink rename to inkfiles/conditional/shuffle_stopping.ink diff --git a/lib/tests/data/conditional/shuffle_stopping.ink.json b/inkfiles/conditional/shuffle_stopping.ink.json similarity index 100% rename from lib/tests/data/conditional/shuffle_stopping.ink.json rename to inkfiles/conditional/shuffle_stopping.ink.json diff --git a/lib/tests/data/conditional/stopping.ink b/inkfiles/conditional/stopping.ink similarity index 100% rename from lib/tests/data/conditional/stopping.ink rename to inkfiles/conditional/stopping.ink diff --git a/lib/tests/data/conditional/stopping.ink.json b/inkfiles/conditional/stopping.ink.json similarity index 100% rename from lib/tests/data/conditional/stopping.ink.json rename to inkfiles/conditional/stopping.ink.json diff --git a/lib/tests/data/divert/complex-branching.ink b/inkfiles/divert/complex-branching.ink similarity index 100% rename from lib/tests/data/divert/complex-branching.ink rename to inkfiles/divert/complex-branching.ink diff --git a/lib/tests/data/divert/complex-branching.ink.json b/inkfiles/divert/complex-branching.ink.json similarity index 100% rename from lib/tests/data/divert/complex-branching.ink.json rename to inkfiles/divert/complex-branching.ink.json diff --git a/lib/tests/data/divert/divert-on-choice.ink b/inkfiles/divert/divert-on-choice.ink similarity index 100% rename from lib/tests/data/divert/divert-on-choice.ink rename to inkfiles/divert/divert-on-choice.ink diff --git a/lib/tests/data/divert/divert-on-choice.ink.json b/inkfiles/divert/divert-on-choice.ink.json similarity index 100% rename from lib/tests/data/divert/divert-on-choice.ink.json rename to inkfiles/divert/divert-on-choice.ink.json diff --git a/lib/tests/data/divert/invisible-divert.ink b/inkfiles/divert/invisible-divert.ink similarity index 100% rename from lib/tests/data/divert/invisible-divert.ink rename to inkfiles/divert/invisible-divert.ink diff --git a/lib/tests/data/divert/invisible-divert.ink.json b/inkfiles/divert/invisible-divert.ink.json similarity index 100% rename from lib/tests/data/divert/invisible-divert.ink.json rename to inkfiles/divert/invisible-divert.ink.json diff --git a/lib/tests/data/divert/simple-divert.ink b/inkfiles/divert/simple-divert.ink similarity index 100% rename from lib/tests/data/divert/simple-divert.ink rename to inkfiles/divert/simple-divert.ink diff --git a/lib/tests/data/divert/simple-divert.ink.json b/inkfiles/divert/simple-divert.ink.json similarity index 100% rename from lib/tests/data/divert/simple-divert.ink.json rename to inkfiles/divert/simple-divert.ink.json diff --git a/lib/tests/data/function/complex-func1.ink b/inkfiles/function/complex-func1.ink similarity index 100% rename from lib/tests/data/function/complex-func1.ink rename to inkfiles/function/complex-func1.ink diff --git a/lib/tests/data/function/complex-func1.ink.json b/inkfiles/function/complex-func1.ink.json similarity index 100% rename from lib/tests/data/function/complex-func1.ink.json rename to inkfiles/function/complex-func1.ink.json diff --git a/lib/tests/data/function/complex-func2.ink b/inkfiles/function/complex-func2.ink similarity index 100% rename from lib/tests/data/function/complex-func2.ink rename to inkfiles/function/complex-func2.ink diff --git a/lib/tests/data/function/complex-func2.ink.json b/inkfiles/function/complex-func2.ink.json similarity index 100% rename from lib/tests/data/function/complex-func2.ink.json rename to inkfiles/function/complex-func2.ink.json diff --git a/lib/tests/data/function/complex-func3.ink b/inkfiles/function/complex-func3.ink similarity index 100% rename from lib/tests/data/function/complex-func3.ink rename to inkfiles/function/complex-func3.ink diff --git a/lib/tests/data/function/complex-func3.ink.json b/inkfiles/function/complex-func3.ink.json similarity index 100% rename from lib/tests/data/function/complex-func3.ink.json rename to inkfiles/function/complex-func3.ink.json diff --git a/lib/tests/data/function/evaluating-function-variablestate-bug.ink b/inkfiles/function/evaluating-function-variablestate-bug.ink similarity index 100% rename from lib/tests/data/function/evaluating-function-variablestate-bug.ink rename to inkfiles/function/evaluating-function-variablestate-bug.ink diff --git a/lib/tests/data/function/evaluating-function-variablestate-bug.ink.json b/inkfiles/function/evaluating-function-variablestate-bug.ink.json similarity index 100% rename from lib/tests/data/function/evaluating-function-variablestate-bug.ink.json rename to inkfiles/function/evaluating-function-variablestate-bug.ink.json diff --git a/lib/tests/data/function/func-basic.ink b/inkfiles/function/func-basic.ink similarity index 100% rename from lib/tests/data/function/func-basic.ink rename to inkfiles/function/func-basic.ink diff --git a/lib/tests/data/function/func-basic.ink.json b/inkfiles/function/func-basic.ink.json similarity index 100% rename from lib/tests/data/function/func-basic.ink.json rename to inkfiles/function/func-basic.ink.json diff --git a/lib/tests/data/function/func-inline.ink b/inkfiles/function/func-inline.ink similarity index 100% rename from lib/tests/data/function/func-inline.ink rename to inkfiles/function/func-inline.ink diff --git a/lib/tests/data/function/func-inline.ink.json b/inkfiles/function/func-inline.ink.json similarity index 100% rename from lib/tests/data/function/func-inline.ink.json rename to inkfiles/function/func-inline.ink.json diff --git a/lib/tests/data/function/func-none.ink b/inkfiles/function/func-none.ink similarity index 100% rename from lib/tests/data/function/func-none.ink rename to inkfiles/function/func-none.ink diff --git a/lib/tests/data/function/func-none.ink.json b/inkfiles/function/func-none.ink.json similarity index 100% rename from lib/tests/data/function/func-none.ink.json rename to inkfiles/function/func-none.ink.json diff --git a/lib/tests/data/function/rnd-func.ink b/inkfiles/function/rnd-func.ink similarity index 100% rename from lib/tests/data/function/rnd-func.ink rename to inkfiles/function/rnd-func.ink diff --git a/lib/tests/data/function/rnd-func.ink.json b/inkfiles/function/rnd-func.ink.json similarity index 100% rename from lib/tests/data/function/rnd-func.ink.json rename to inkfiles/function/rnd-func.ink.json diff --git a/lib/tests/data/function/setvar-func.ink b/inkfiles/function/setvar-func.ink similarity index 100% rename from lib/tests/data/function/setvar-func.ink rename to inkfiles/function/setvar-func.ink diff --git a/lib/tests/data/function/setvar-func.ink.json b/inkfiles/function/setvar-func.ink.json similarity index 100% rename from lib/tests/data/function/setvar-func.ink.json rename to inkfiles/function/setvar-func.ink.json diff --git a/lib/tests/data/function/test-error.ink b/inkfiles/function/test-error.ink similarity index 100% rename from lib/tests/data/function/test-error.ink rename to inkfiles/function/test-error.ink diff --git a/lib/tests/data/function/test-error.ink.json b/inkfiles/function/test-error.ink.json similarity index 100% rename from lib/tests/data/function/test-error.ink.json rename to inkfiles/function/test-error.ink.json diff --git a/lib/tests/data/gather/complex-flow.ink b/inkfiles/gather/complex-flow.ink similarity index 100% rename from lib/tests/data/gather/complex-flow.ink rename to inkfiles/gather/complex-flow.ink diff --git a/lib/tests/data/gather/complex-flow.ink.json b/inkfiles/gather/complex-flow.ink.json similarity index 100% rename from lib/tests/data/gather/complex-flow.ink.json rename to inkfiles/gather/complex-flow.ink.json diff --git a/lib/tests/data/gather/deep-nesting.ink b/inkfiles/gather/deep-nesting.ink similarity index 100% rename from lib/tests/data/gather/deep-nesting.ink rename to inkfiles/gather/deep-nesting.ink diff --git a/lib/tests/data/gather/deep-nesting.ink.json b/inkfiles/gather/deep-nesting.ink.json similarity index 100% rename from lib/tests/data/gather/deep-nesting.ink.json rename to inkfiles/gather/deep-nesting.ink.json diff --git a/lib/tests/data/gather/gather-basic.ink b/inkfiles/gather/gather-basic.ink similarity index 100% rename from lib/tests/data/gather/gather-basic.ink rename to inkfiles/gather/gather-basic.ink diff --git a/lib/tests/data/gather/gather-basic.ink.json b/inkfiles/gather/gather-basic.ink.json similarity index 100% rename from lib/tests/data/gather/gather-basic.ink.json rename to inkfiles/gather/gather-basic.ink.json diff --git a/lib/tests/data/gather/gather-chain.ink b/inkfiles/gather/gather-chain.ink similarity index 100% rename from lib/tests/data/gather/gather-chain.ink rename to inkfiles/gather/gather-chain.ink diff --git a/lib/tests/data/gather/gather-chain.ink.json b/inkfiles/gather/gather-chain.ink.json similarity index 100% rename from lib/tests/data/gather/gather-chain.ink.json rename to inkfiles/gather/gather-chain.ink.json diff --git a/lib/tests/data/gather/nested-flow.ink b/inkfiles/gather/nested-flow.ink similarity index 100% rename from lib/tests/data/gather/nested-flow.ink rename to inkfiles/gather/nested-flow.ink diff --git a/lib/tests/data/gather/nested-flow.ink.json b/inkfiles/gather/nested-flow.ink.json similarity index 100% rename from lib/tests/data/gather/nested-flow.ink.json rename to inkfiles/gather/nested-flow.ink.json diff --git a/lib/tests/data/gather/nested-gather.ink b/inkfiles/gather/nested-gather.ink similarity index 100% rename from lib/tests/data/gather/nested-gather.ink rename to inkfiles/gather/nested-gather.ink diff --git a/lib/tests/data/gather/nested-gather.ink.json b/inkfiles/gather/nested-gather.ink.json similarity index 100% rename from lib/tests/data/gather/nested-gather.ink.json rename to inkfiles/gather/nested-gather.ink.json diff --git a/lib/tests/data/glue/glue-with-divert.ink b/inkfiles/glue/glue-with-divert.ink similarity index 100% rename from lib/tests/data/glue/glue-with-divert.ink rename to inkfiles/glue/glue-with-divert.ink diff --git a/lib/tests/data/glue/glue-with-divert.ink.json b/inkfiles/glue/glue-with-divert.ink.json similarity index 100% rename from lib/tests/data/glue/glue-with-divert.ink.json rename to inkfiles/glue/glue-with-divert.ink.json diff --git a/lib/tests/data/glue/left-right-glue-matching.ink b/inkfiles/glue/left-right-glue-matching.ink similarity index 100% rename from lib/tests/data/glue/left-right-glue-matching.ink rename to inkfiles/glue/left-right-glue-matching.ink diff --git a/lib/tests/data/glue/left-right-glue-matching.ink.json b/inkfiles/glue/left-right-glue-matching.ink.json similarity index 100% rename from lib/tests/data/glue/left-right-glue-matching.ink.json rename to inkfiles/glue/left-right-glue-matching.ink.json diff --git a/lib/tests/data/glue/simple-glue.ink b/inkfiles/glue/simple-glue.ink similarity index 100% rename from lib/tests/data/glue/simple-glue.ink rename to inkfiles/glue/simple-glue.ink diff --git a/lib/tests/data/glue/simple-glue.ink.json b/inkfiles/glue/simple-glue.ink.json similarity index 100% rename from lib/tests/data/glue/simple-glue.ink.json rename to inkfiles/glue/simple-glue.ink.json diff --git a/lib/tests/data/glue/testbugfix1.ink b/inkfiles/glue/testbugfix1.ink similarity index 100% rename from lib/tests/data/glue/testbugfix1.ink rename to inkfiles/glue/testbugfix1.ink diff --git a/lib/tests/data/glue/testbugfix1.ink.json b/inkfiles/glue/testbugfix1.ink.json similarity index 100% rename from lib/tests/data/glue/testbugfix1.ink.json rename to inkfiles/glue/testbugfix1.ink.json diff --git a/lib/tests/data/glue/testbugfix2.ink b/inkfiles/glue/testbugfix2.ink similarity index 100% rename from lib/tests/data/glue/testbugfix2.ink rename to inkfiles/glue/testbugfix2.ink diff --git a/lib/tests/data/glue/testbugfix2.ink.json b/inkfiles/glue/testbugfix2.ink.json similarity index 100% rename from lib/tests/data/glue/testbugfix2.ink.json rename to inkfiles/glue/testbugfix2.ink.json diff --git a/lib/tests/data/knot/multi-line.ink b/inkfiles/knot/multi-line.ink similarity index 100% rename from lib/tests/data/knot/multi-line.ink rename to inkfiles/knot/multi-line.ink diff --git a/lib/tests/data/knot/multi-line.ink.json b/inkfiles/knot/multi-line.ink.json similarity index 100% rename from lib/tests/data/knot/multi-line.ink.json rename to inkfiles/knot/multi-line.ink.json diff --git a/lib/tests/data/knot/param-floats.ink b/inkfiles/knot/param-floats.ink similarity index 100% rename from lib/tests/data/knot/param-floats.ink rename to inkfiles/knot/param-floats.ink diff --git a/lib/tests/data/knot/param-floats.ink.json b/inkfiles/knot/param-floats.ink.json similarity index 100% rename from lib/tests/data/knot/param-floats.ink.json rename to inkfiles/knot/param-floats.ink.json diff --git a/lib/tests/data/knot/param-ints.ink b/inkfiles/knot/param-ints.ink similarity index 100% rename from lib/tests/data/knot/param-ints.ink rename to inkfiles/knot/param-ints.ink diff --git a/lib/tests/data/knot/param-ints.ink.json b/inkfiles/knot/param-ints.ink.json similarity index 100% rename from lib/tests/data/knot/param-ints.ink.json rename to inkfiles/knot/param-ints.ink.json diff --git a/lib/tests/data/knot/param-multi.ink b/inkfiles/knot/param-multi.ink similarity index 100% rename from lib/tests/data/knot/param-multi.ink rename to inkfiles/knot/param-multi.ink diff --git a/lib/tests/data/knot/param-multi.ink.json b/inkfiles/knot/param-multi.ink.json similarity index 100% rename from lib/tests/data/knot/param-multi.ink.json rename to inkfiles/knot/param-multi.ink.json diff --git a/lib/tests/data/knot/param-recurse.ink b/inkfiles/knot/param-recurse.ink similarity index 100% rename from lib/tests/data/knot/param-recurse.ink rename to inkfiles/knot/param-recurse.ink diff --git a/lib/tests/data/knot/param-recurse.ink.json b/inkfiles/knot/param-recurse.ink.json similarity index 100% rename from lib/tests/data/knot/param-recurse.ink.json rename to inkfiles/knot/param-recurse.ink.json diff --git a/lib/tests/data/knot/param-strings.ink b/inkfiles/knot/param-strings.ink similarity index 100% rename from lib/tests/data/knot/param-strings.ink rename to inkfiles/knot/param-strings.ink diff --git a/lib/tests/data/knot/param-strings.ink.json b/inkfiles/knot/param-strings.ink.json similarity index 100% rename from lib/tests/data/knot/param-strings.ink.json rename to inkfiles/knot/param-strings.ink.json diff --git a/lib/tests/data/knot/param-vars.ink b/inkfiles/knot/param-vars.ink similarity index 100% rename from lib/tests/data/knot/param-vars.ink rename to inkfiles/knot/param-vars.ink diff --git a/lib/tests/data/knot/param-vars.ink.json b/inkfiles/knot/param-vars.ink.json similarity index 100% rename from lib/tests/data/knot/param-vars.ink.json rename to inkfiles/knot/param-vars.ink.json diff --git a/lib/tests/data/knot/single-line.ink b/inkfiles/knot/single-line.ink similarity index 100% rename from lib/tests/data/knot/single-line.ink rename to inkfiles/knot/single-line.ink diff --git a/lib/tests/data/knot/single-line.ink.json b/inkfiles/knot/single-line.ink.json similarity index 100% rename from lib/tests/data/knot/single-line.ink.json rename to inkfiles/knot/single-line.ink.json diff --git a/lib/tests/data/knot/strip-empty-lines.ink b/inkfiles/knot/strip-empty-lines.ink similarity index 100% rename from lib/tests/data/knot/strip-empty-lines.ink rename to inkfiles/knot/strip-empty-lines.ink diff --git a/lib/tests/data/knot/strip-empty-lines.ink.json b/inkfiles/knot/strip-empty-lines.ink.json similarity index 100% rename from lib/tests/data/knot/strip-empty-lines.ink.json rename to inkfiles/knot/strip-empty-lines.ink.json diff --git a/lib/tests/data/lists/basic-operations.ink b/inkfiles/lists/basic-operations.ink similarity index 100% rename from lib/tests/data/lists/basic-operations.ink rename to inkfiles/lists/basic-operations.ink diff --git a/lib/tests/data/lists/basic-operations.ink.json b/inkfiles/lists/basic-operations.ink.json similarity index 100% rename from lib/tests/data/lists/basic-operations.ink.json rename to inkfiles/lists/basic-operations.ink.json diff --git a/lib/tests/data/lists/bug-adding-element.ink b/inkfiles/lists/bug-adding-element.ink similarity index 100% rename from lib/tests/data/lists/bug-adding-element.ink rename to inkfiles/lists/bug-adding-element.ink diff --git a/lib/tests/data/lists/bug-adding-element.ink.json b/inkfiles/lists/bug-adding-element.ink.json similarity index 100% rename from lib/tests/data/lists/bug-adding-element.ink.json rename to inkfiles/lists/bug-adding-element.ink.json diff --git a/lib/tests/data/lists/empty-list-origin-after-assignment.ink b/inkfiles/lists/empty-list-origin-after-assignment.ink similarity index 100% rename from lib/tests/data/lists/empty-list-origin-after-assignment.ink rename to inkfiles/lists/empty-list-origin-after-assignment.ink diff --git a/lib/tests/data/lists/empty-list-origin-after-assignment.ink.json b/inkfiles/lists/empty-list-origin-after-assignment.ink.json similarity index 100% rename from lib/tests/data/lists/empty-list-origin-after-assignment.ink.json rename to inkfiles/lists/empty-list-origin-after-assignment.ink.json diff --git a/lib/tests/data/lists/empty-list-origin.ink b/inkfiles/lists/empty-list-origin.ink similarity index 100% rename from lib/tests/data/lists/empty-list-origin.ink rename to inkfiles/lists/empty-list-origin.ink diff --git a/lib/tests/data/lists/empty-list-origin.ink.json b/inkfiles/lists/empty-list-origin.ink.json similarity index 100% rename from lib/tests/data/lists/empty-list-origin.ink.json rename to inkfiles/lists/empty-list-origin.ink.json diff --git a/lib/tests/data/lists/list-mixed-items.ink b/inkfiles/lists/list-mixed-items.ink similarity index 100% rename from lib/tests/data/lists/list-mixed-items.ink rename to inkfiles/lists/list-mixed-items.ink diff --git a/lib/tests/data/lists/list-mixed-items.ink.json b/inkfiles/lists/list-mixed-items.ink.json similarity index 100% rename from lib/tests/data/lists/list-mixed-items.ink.json rename to inkfiles/lists/list-mixed-items.ink.json diff --git a/lib/tests/data/lists/list-range.ink b/inkfiles/lists/list-range.ink similarity index 100% rename from lib/tests/data/lists/list-range.ink rename to inkfiles/lists/list-range.ink diff --git a/lib/tests/data/lists/list-range.ink.json b/inkfiles/lists/list-range.ink.json similarity index 100% rename from lib/tests/data/lists/list-range.ink.json rename to inkfiles/lists/list-range.ink.json diff --git a/lib/tests/data/lists/list-save-load.ink b/inkfiles/lists/list-save-load.ink similarity index 100% rename from lib/tests/data/lists/list-save-load.ink rename to inkfiles/lists/list-save-load.ink diff --git a/lib/tests/data/lists/list-save-load.ink.json b/inkfiles/lists/list-save-load.ink.json similarity index 100% rename from lib/tests/data/lists/list-save-load.ink.json rename to inkfiles/lists/list-save-load.ink.json diff --git a/lib/tests/data/lists/more-list-operations.ink b/inkfiles/lists/more-list-operations.ink similarity index 100% rename from lib/tests/data/lists/more-list-operations.ink rename to inkfiles/lists/more-list-operations.ink diff --git a/lib/tests/data/lists/more-list-operations.ink.json b/inkfiles/lists/more-list-operations.ink.json similarity index 100% rename from lib/tests/data/lists/more-list-operations.ink.json rename to inkfiles/lists/more-list-operations.ink.json diff --git a/lib/tests/data/lists/more-list-operations2.ink b/inkfiles/lists/more-list-operations2.ink similarity index 100% rename from lib/tests/data/lists/more-list-operations2.ink rename to inkfiles/lists/more-list-operations2.ink diff --git a/lib/tests/data/lists/more-list-operations2.ink.json b/inkfiles/lists/more-list-operations2.ink.json similarity index 100% rename from lib/tests/data/lists/more-list-operations2.ink.json rename to inkfiles/lists/more-list-operations2.ink.json diff --git a/lib/tests/data/misc/issue15.ink b/inkfiles/misc/issue15.ink similarity index 100% rename from lib/tests/data/misc/issue15.ink rename to inkfiles/misc/issue15.ink diff --git a/lib/tests/data/misc/issue15.ink.json b/inkfiles/misc/issue15.ink.json similarity index 100% rename from lib/tests/data/misc/issue15.ink.json rename to inkfiles/misc/issue15.ink.json diff --git a/lib/tests/data/misc/operations.ink b/inkfiles/misc/operations.ink similarity index 100% rename from lib/tests/data/misc/operations.ink rename to inkfiles/misc/operations.ink diff --git a/lib/tests/data/misc/operations.ink.json b/inkfiles/misc/operations.ink.json similarity index 100% rename from lib/tests/data/misc/operations.ink.json rename to inkfiles/misc/operations.ink.json diff --git a/lib/tests/data/misc/read-counts.ink b/inkfiles/misc/read-counts.ink similarity index 100% rename from lib/tests/data/misc/read-counts.ink rename to inkfiles/misc/read-counts.ink diff --git a/lib/tests/data/misc/read-counts.ink.json b/inkfiles/misc/read-counts.ink.json similarity index 100% rename from lib/tests/data/misc/read-counts.ink.json rename to inkfiles/misc/read-counts.ink.json diff --git a/lib/tests/data/misc/turns-since.ink b/inkfiles/misc/turns-since.ink similarity index 100% rename from lib/tests/data/misc/turns-since.ink rename to inkfiles/misc/turns-since.ink diff --git a/lib/tests/data/misc/turns-since.ink.json b/inkfiles/misc/turns-since.ink.json similarity index 100% rename from lib/tests/data/misc/turns-since.ink.json rename to inkfiles/misc/turns-since.ink.json diff --git a/lib/tests/data/runtime/external-function-0-arg.ink b/inkfiles/runtime/external-function-0-arg.ink similarity index 100% rename from lib/tests/data/runtime/external-function-0-arg.ink rename to inkfiles/runtime/external-function-0-arg.ink diff --git a/lib/tests/data/runtime/external-function-0-arg.ink.json b/inkfiles/runtime/external-function-0-arg.ink.json similarity index 100% rename from lib/tests/data/runtime/external-function-0-arg.ink.json rename to inkfiles/runtime/external-function-0-arg.ink.json diff --git a/lib/tests/data/runtime/external-function-1-arg.ink b/inkfiles/runtime/external-function-1-arg.ink similarity index 100% rename from lib/tests/data/runtime/external-function-1-arg.ink rename to inkfiles/runtime/external-function-1-arg.ink diff --git a/lib/tests/data/runtime/external-function-1-arg.ink.json b/inkfiles/runtime/external-function-1-arg.ink.json similarity index 100% rename from lib/tests/data/runtime/external-function-1-arg.ink.json rename to inkfiles/runtime/external-function-1-arg.ink.json diff --git a/lib/tests/data/runtime/external-function-2-arg.ink b/inkfiles/runtime/external-function-2-arg.ink similarity index 100% rename from lib/tests/data/runtime/external-function-2-arg.ink rename to inkfiles/runtime/external-function-2-arg.ink diff --git a/lib/tests/data/runtime/external-function-2-arg.ink.json b/inkfiles/runtime/external-function-2-arg.ink.json similarity index 100% rename from lib/tests/data/runtime/external-function-2-arg.ink.json rename to inkfiles/runtime/external-function-2-arg.ink.json diff --git a/lib/tests/data/runtime/external-function-3-arg.ink b/inkfiles/runtime/external-function-3-arg.ink similarity index 100% rename from lib/tests/data/runtime/external-function-3-arg.ink rename to inkfiles/runtime/external-function-3-arg.ink diff --git a/lib/tests/data/runtime/external-function-3-arg.ink.json b/inkfiles/runtime/external-function-3-arg.ink.json similarity index 100% rename from lib/tests/data/runtime/external-function-3-arg.ink.json rename to inkfiles/runtime/external-function-3-arg.ink.json diff --git a/lib/tests/data/runtime/jump-knot.ink b/inkfiles/runtime/jump-knot.ink similarity index 100% rename from lib/tests/data/runtime/jump-knot.ink rename to inkfiles/runtime/jump-knot.ink diff --git a/lib/tests/data/runtime/jump-knot.ink.json b/inkfiles/runtime/jump-knot.ink.json similarity index 100% rename from lib/tests/data/runtime/jump-knot.ink.json rename to inkfiles/runtime/jump-knot.ink.json diff --git a/lib/tests/data/runtime/jump-stitch.ink b/inkfiles/runtime/jump-stitch.ink similarity index 100% rename from lib/tests/data/runtime/jump-stitch.ink rename to inkfiles/runtime/jump-stitch.ink diff --git a/lib/tests/data/runtime/jump-stitch.ink.json b/inkfiles/runtime/jump-stitch.ink.json similarity index 100% rename from lib/tests/data/runtime/jump-stitch.ink.json rename to inkfiles/runtime/jump-stitch.ink.json diff --git a/lib/tests/data/runtime/load-save.ink b/inkfiles/runtime/load-save.ink similarity index 100% rename from lib/tests/data/runtime/load-save.ink rename to inkfiles/runtime/load-save.ink diff --git a/lib/tests/data/runtime/load-save.ink.json b/inkfiles/runtime/load-save.ink.json similarity index 100% rename from lib/tests/data/runtime/load-save.ink.json rename to inkfiles/runtime/load-save.ink.json diff --git a/lib/tests/data/runtime/multiflow-basics.ink b/inkfiles/runtime/multiflow-basics.ink similarity index 100% rename from lib/tests/data/runtime/multiflow-basics.ink rename to inkfiles/runtime/multiflow-basics.ink diff --git a/lib/tests/data/runtime/multiflow-basics.ink.json b/inkfiles/runtime/multiflow-basics.ink.json similarity index 100% rename from lib/tests/data/runtime/multiflow-basics.ink.json rename to inkfiles/runtime/multiflow-basics.ink.json diff --git a/lib/tests/data/runtime/multiflow-saveloadthreads.ink b/inkfiles/runtime/multiflow-saveloadthreads.ink similarity index 100% rename from lib/tests/data/runtime/multiflow-saveloadthreads.ink rename to inkfiles/runtime/multiflow-saveloadthreads.ink diff --git a/lib/tests/data/runtime/multiflow-saveloadthreads.ink.json b/inkfiles/runtime/multiflow-saveloadthreads.ink.json similarity index 100% rename from lib/tests/data/runtime/multiflow-saveloadthreads.ink.json rename to inkfiles/runtime/multiflow-saveloadthreads.ink.json diff --git a/lib/tests/data/runtime/read-visit-counts.ink b/inkfiles/runtime/read-visit-counts.ink similarity index 100% rename from lib/tests/data/runtime/read-visit-counts.ink rename to inkfiles/runtime/read-visit-counts.ink diff --git a/lib/tests/data/runtime/read-visit-counts.ink.json b/inkfiles/runtime/read-visit-counts.ink.json similarity index 100% rename from lib/tests/data/runtime/read-visit-counts.ink.json rename to inkfiles/runtime/read-visit-counts.ink.json diff --git a/lib/tests/data/runtime/saving-loading.ink b/inkfiles/runtime/saving-loading.ink similarity index 100% rename from lib/tests/data/runtime/saving-loading.ink rename to inkfiles/runtime/saving-loading.ink diff --git a/lib/tests/data/runtime/saving-loading.ink.json b/inkfiles/runtime/saving-loading.ink.json similarity index 100% rename from lib/tests/data/runtime/saving-loading.ink.json rename to inkfiles/runtime/saving-loading.ink.json diff --git a/lib/tests/data/runtime/set-get-variables.ink b/inkfiles/runtime/set-get-variables.ink similarity index 100% rename from lib/tests/data/runtime/set-get-variables.ink rename to inkfiles/runtime/set-get-variables.ink diff --git a/lib/tests/data/runtime/set-get-variables.ink.json b/inkfiles/runtime/set-get-variables.ink.json similarity index 100% rename from lib/tests/data/runtime/set-get-variables.ink.json rename to inkfiles/runtime/set-get-variables.ink.json diff --git a/lib/tests/data/runtime/variable-observers.ink b/inkfiles/runtime/variable-observers.ink similarity index 100% rename from lib/tests/data/runtime/variable-observers.ink rename to inkfiles/runtime/variable-observers.ink diff --git a/lib/tests/data/runtime/variable-observers.ink.json b/inkfiles/runtime/variable-observers.ink.json similarity index 100% rename from lib/tests/data/runtime/variable-observers.ink.json rename to inkfiles/runtime/variable-observers.ink.json diff --git a/lib/tests/data/stitch/auto-stitch.ink b/inkfiles/stitch/auto-stitch.ink similarity index 100% rename from lib/tests/data/stitch/auto-stitch.ink rename to inkfiles/stitch/auto-stitch.ink diff --git a/lib/tests/data/stitch/auto-stitch.ink.json b/inkfiles/stitch/auto-stitch.ink.json similarity index 100% rename from lib/tests/data/stitch/auto-stitch.ink.json rename to inkfiles/stitch/auto-stitch.ink.json diff --git a/lib/tests/data/stitch/manual-stitch.ink b/inkfiles/stitch/manual-stitch.ink similarity index 100% rename from lib/tests/data/stitch/manual-stitch.ink rename to inkfiles/stitch/manual-stitch.ink diff --git a/lib/tests/data/stitch/manual-stitch.ink.json b/inkfiles/stitch/manual-stitch.ink.json similarity index 100% rename from lib/tests/data/stitch/manual-stitch.ink.json rename to inkfiles/stitch/manual-stitch.ink.json diff --git a/lib/tests/data/tags/tags.ink b/inkfiles/tags/tags.ink similarity index 100% rename from lib/tests/data/tags/tags.ink rename to inkfiles/tags/tags.ink diff --git a/lib/tests/data/tags/tags.ink.json b/inkfiles/tags/tags.ink.json similarity index 100% rename from lib/tests/data/tags/tags.ink.json rename to inkfiles/tags/tags.ink.json diff --git a/lib/tests/data/tags/tagsDynamicContent.ink b/inkfiles/tags/tagsDynamicContent.ink similarity index 100% rename from lib/tests/data/tags/tagsDynamicContent.ink rename to inkfiles/tags/tagsDynamicContent.ink diff --git a/lib/tests/data/tags/tagsDynamicContent.ink.json b/inkfiles/tags/tagsDynamicContent.ink.json similarity index 100% rename from lib/tests/data/tags/tagsDynamicContent.ink.json rename to inkfiles/tags/tagsDynamicContent.ink.json diff --git a/lib/tests/data/tags/tagsInChoice.ink b/inkfiles/tags/tagsInChoice.ink similarity index 100% rename from lib/tests/data/tags/tagsInChoice.ink rename to inkfiles/tags/tagsInChoice.ink diff --git a/lib/tests/data/tags/tagsInChoice.ink.json b/inkfiles/tags/tagsInChoice.ink.json similarity index 100% rename from lib/tests/data/tags/tagsInChoice.ink.json rename to inkfiles/tags/tagsInChoice.ink.json diff --git a/lib/tests/data/tags/tagsInSeq.ink b/inkfiles/tags/tagsInSeq.ink similarity index 100% rename from lib/tests/data/tags/tagsInSeq.ink rename to inkfiles/tags/tagsInSeq.ink diff --git a/lib/tests/data/tags/tagsInSeq.ink.json b/inkfiles/tags/tagsInSeq.ink.json similarity index 100% rename from lib/tests/data/tags/tagsInSeq.ink.json rename to inkfiles/tags/tagsInSeq.ink.json diff --git a/cli-player/tests/data/test1.ink b/inkfiles/test1.ink similarity index 100% rename from cli-player/tests/data/test1.ink rename to inkfiles/test1.ink diff --git a/cli-player/tests/data/test1.ink.json b/inkfiles/test1.ink.json similarity index 100% rename from cli-player/tests/data/test1.ink.json rename to inkfiles/test1.ink.json diff --git a/lib/tests/data/threads/thread-bug.ink b/inkfiles/threads/thread-bug.ink similarity index 100% rename from lib/tests/data/threads/thread-bug.ink rename to inkfiles/threads/thread-bug.ink diff --git a/lib/tests/data/threads/thread-bug.ink.json b/inkfiles/threads/thread-bug.ink.json similarity index 100% rename from lib/tests/data/threads/thread-bug.ink.json rename to inkfiles/threads/thread-bug.ink.json diff --git a/lib/tests/data/tunnels/tunnel-onwards-divert-override.ink b/inkfiles/tunnels/tunnel-onwards-divert-override.ink similarity index 100% rename from lib/tests/data/tunnels/tunnel-onwards-divert-override.ink rename to inkfiles/tunnels/tunnel-onwards-divert-override.ink diff --git a/lib/tests/data/tunnels/tunnel-onwards-divert-override.ink.json b/inkfiles/tunnels/tunnel-onwards-divert-override.ink.json similarity index 100% rename from lib/tests/data/tunnels/tunnel-onwards-divert-override.ink.json rename to inkfiles/tunnels/tunnel-onwards-divert-override.ink.json diff --git a/lib/tests/data/variable/var-divert.ink b/inkfiles/variable/var-divert.ink similarity index 100% rename from lib/tests/data/variable/var-divert.ink rename to inkfiles/variable/var-divert.ink diff --git a/lib/tests/data/variable/var-divert.ink.json b/inkfiles/variable/var-divert.ink.json similarity index 100% rename from lib/tests/data/variable/var-divert.ink.json rename to inkfiles/variable/var-divert.ink.json diff --git a/lib/tests/data/variable/varcalc.ink b/inkfiles/variable/varcalc.ink similarity index 100% rename from lib/tests/data/variable/varcalc.ink rename to inkfiles/variable/varcalc.ink diff --git a/lib/tests/data/variable/varcalc.ink.json b/inkfiles/variable/varcalc.ink.json similarity index 100% rename from lib/tests/data/variable/varcalc.ink.json rename to inkfiles/variable/varcalc.ink.json diff --git a/lib/tests/data/variable/variable-declaration.ink b/inkfiles/variable/variable-declaration.ink similarity index 100% rename from lib/tests/data/variable/variable-declaration.ink rename to inkfiles/variable/variable-declaration.ink diff --git a/lib/tests/data/variable/variable-declaration.ink.json b/inkfiles/variable/variable-declaration.ink.json similarity index 100% rename from lib/tests/data/variable/variable-declaration.ink.json rename to inkfiles/variable/variable-declaration.ink.json diff --git a/lib/tests/data/variable/varstringinc.ink b/inkfiles/variable/varstringinc.ink similarity index 100% rename from lib/tests/data/variable/varstringinc.ink rename to inkfiles/variable/varstringinc.ink diff --git a/lib/tests/data/variable/varstringinc.ink.json b/inkfiles/variable/varstringinc.ink.json similarity index 100% rename from lib/tests/data/variable/varstringinc.ink.json rename to inkfiles/variable/varstringinc.ink.json diff --git a/lib/tests/data/variabletext/cycle.ink b/inkfiles/variabletext/cycle.ink similarity index 100% rename from lib/tests/data/variabletext/cycle.ink rename to inkfiles/variabletext/cycle.ink diff --git a/lib/tests/data/variabletext/cycle.ink.json b/inkfiles/variabletext/cycle.ink.json similarity index 100% rename from lib/tests/data/variabletext/cycle.ink.json rename to inkfiles/variabletext/cycle.ink.json diff --git a/lib/tests/data/variabletext/empty-elements.ink b/inkfiles/variabletext/empty-elements.ink similarity index 100% rename from lib/tests/data/variabletext/empty-elements.ink rename to inkfiles/variabletext/empty-elements.ink diff --git a/lib/tests/data/variabletext/empty-elements.ink.json b/inkfiles/variabletext/empty-elements.ink.json similarity index 100% rename from lib/tests/data/variabletext/empty-elements.ink.json rename to inkfiles/variabletext/empty-elements.ink.json diff --git a/lib/tests/data/variabletext/list-in-choice.ink b/inkfiles/variabletext/list-in-choice.ink similarity index 100% rename from lib/tests/data/variabletext/list-in-choice.ink rename to inkfiles/variabletext/list-in-choice.ink diff --git a/lib/tests/data/variabletext/list-in-choice.ink.json b/inkfiles/variabletext/list-in-choice.ink.json similarity index 100% rename from lib/tests/data/variabletext/list-in-choice.ink.json rename to inkfiles/variabletext/list-in-choice.ink.json diff --git a/lib/tests/data/variabletext/once.ink b/inkfiles/variabletext/once.ink similarity index 100% rename from lib/tests/data/variabletext/once.ink rename to inkfiles/variabletext/once.ink diff --git a/lib/tests/data/variabletext/once.ink.json b/inkfiles/variabletext/once.ink.json similarity index 100% rename from lib/tests/data/variabletext/once.ink.json rename to inkfiles/variabletext/once.ink.json diff --git a/lib/tests/data/variabletext/sequence.ink b/inkfiles/variabletext/sequence.ink similarity index 100% rename from lib/tests/data/variabletext/sequence.ink rename to inkfiles/variabletext/sequence.ink diff --git a/lib/tests/data/variabletext/sequence.ink.json b/inkfiles/variabletext/sequence.ink.json similarity index 100% rename from lib/tests/data/variabletext/sequence.ink.json rename to inkfiles/variabletext/sequence.ink.json diff --git a/lib/src/story.rs b/lib/src/story.rs index f02071b..8c280fb 100644 --- a/lib/src/story.rs +++ b/lib/src/story.rs @@ -141,7 +141,7 @@ impl Story { story.reset_globals()?; if version != INK_VERSION_CURRENT { - story.add_error("WARNING: Version of ink used to build story doesn't match current version of engine. Non-critical, but recommend synchronising.", true); + story.add_error(&format!("WARNING: Version of ink used to build story ({}) doesn't match current version ({}) of engine. Non-critical, but recommend synchronising.", version, INK_VERSION_CURRENT), true); } Ok(story) diff --git a/lib/tests/basic_text_test.rs b/lib/tests/basic_text_test.rs index e0fe490..9f658b9 100644 --- a/lib/tests/basic_text_test.rs +++ b/lib/tests/basic_text_test.rs @@ -1,5 +1,5 @@ use bink::{story::Story, story_error::StoryError}; -use std::{env, fs}; +use std::env; mod common; @@ -22,7 +22,7 @@ fn oneline_test() -> Result<(), StoryError> { #[test] fn twolines_test() -> Result<(), StoryError> { - let json_string = fs::read_to_string("tests/data/basictext/twolines.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/basictext/twolines.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); diff --git a/lib/tests/choice_test.rs b/lib/tests/choice_test.rs index d58c86b..9579383 100644 --- a/lib/tests/choice_test.rs +++ b/lib/tests/choice_test.rs @@ -7,7 +7,7 @@ fn no_choice_test() -> Result<(), StoryError> { let mut errors: Vec = Vec::new(); let text = common::run_story( - "tests/data/choices/no-choice-text.ink.json", + "inkfiles/choices/no-choice-text.ink.json", None, &mut errors, )?; @@ -22,7 +22,7 @@ fn no_choice_test() -> Result<(), StoryError> { fn one_test() -> Result<(), StoryError> { let mut errors: Vec = Vec::new(); - let text = common::run_story("tests/data/choices/one.ink.json", None, &mut errors)?; + let text = common::run_story("inkfiles/choices/one.ink.json", None, &mut errors)?; assert_eq!(0, errors.len()); assert_eq!( @@ -38,7 +38,7 @@ fn multi_choice_test() -> Result<(), StoryError> { let mut errors: Vec = Vec::new(); let text = common::run_story( - "tests/data/choices/multi-choice.ink.json", + "inkfiles/choices/multi-choice.ink.json", Some(vec![0]), &mut errors, )?; @@ -51,7 +51,7 @@ fn multi_choice_test() -> Result<(), StoryError> { // Select second choice let text = common::run_story( - "tests/data/choices/multi-choice.ink.json", + "inkfiles/choices/multi-choice.ink.json", Some(vec![1]), &mut errors, )?; @@ -67,7 +67,7 @@ fn multi_choice_test() -> Result<(), StoryError> { #[test] fn single_choice1_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/choices/single-choice.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/choices/single-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -80,7 +80,7 @@ fn single_choice1_test() -> Result<(), StoryError> { #[test] fn single_choic2_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/choices/single-choice.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/choices/single-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -98,7 +98,7 @@ fn single_choic2_test() -> Result<(), StoryError> { #[test] fn suppress_choice_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/choices/suppress-choice.ink.json").unwrap(); + common::get_json_string("inkfiles/choices/suppress-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -120,7 +120,7 @@ fn suppress_choice_test() -> Result<(), StoryError> { #[test] fn mixed_choice_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/choices/mixed-choice.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/choices/mixed-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -144,7 +144,7 @@ fn mixed_choice_test() -> Result<(), StoryError> { #[test] fn varying_choice_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/choices/varying-choice.ink.json").unwrap(); + common::get_json_string("inkfiles/choices/varying-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -166,7 +166,7 @@ fn varying_choice_test() -> Result<(), StoryError> { #[test] fn sticky_choice_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/choices/sticky-choice.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/choices/sticky-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -185,7 +185,7 @@ fn sticky_choice_test() -> Result<(), StoryError> { #[test] fn fallback_choice_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/choices/fallback-choice.ink.json").unwrap(); + common::get_json_string("inkfiles/choices/fallback-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -198,7 +198,7 @@ fn fallback_choice_test() -> Result<(), StoryError> { #[test] fn fallback_choice2_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/choices/fallback-choice.ink.json").unwrap(); + common::get_json_string("inkfiles/choices/fallback-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -221,7 +221,7 @@ fn fallback_choice2_test() -> Result<(), StoryError> { #[test] fn conditional_choice_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/choices/conditional-choice.ink.json").unwrap(); + common::get_json_string("inkfiles/choices/conditional-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -233,7 +233,7 @@ fn conditional_choice_test() -> Result<(), StoryError> { #[test] fn label_flow_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/choices/label-flow.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/choices/label-flow.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -255,7 +255,7 @@ fn label_flow_test() -> Result<(), StoryError> { #[test] fn label_flow2_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/choices/label-flow.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/choices/label-flow.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -274,7 +274,7 @@ fn label_flow2_test() -> Result<(), StoryError> { #[test] fn label_scope_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/choices/label-scope.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/choices/label-scope.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -292,7 +292,7 @@ fn label_scope_test() -> Result<(), StoryError> { #[test] fn divert_choice_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/choices/divert-choice.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/choices/divert-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); diff --git a/lib/tests/common/mod.rs b/lib/tests/common/mod.rs index a11d725..c9955bc 100644 --- a/lib/tests/common/mod.rs +++ b/lib/tests/common/mod.rs @@ -1,6 +1,6 @@ #![allow(dead_code)] -use std::{error::Error, fs, path::Path}; +use std::{error::Error, path::Path, fs}; use bink::{story::Story, story_error::StoryError}; use rand::Rng; @@ -32,6 +32,7 @@ pub fn join_text(text: &Vec) -> String { sb } + pub fn run_story( filename: &str, choice_list: Option>, @@ -49,6 +50,7 @@ pub fn run_story( let mut rng = rand::thread_rng(); while story.can_continue() || !story.get_current_choices().is_empty() { + println!("{}", story.build_string_of_hierarchy()); // 2) Game content, line by line @@ -96,9 +98,9 @@ pub fn run_story( pub fn get_json_string(filename: &str) -> Result> { let mut path = Path::new(filename).to_path_buf(); + // Due to a bug with Cargo workspaces, for Release mode the current folder is the crate folder and for Debug mode the current folder is the root folder. if !path.exists() { - let parent = Path::new("../"); - path = parent.join(path); + path = Path::new("../").join(path); } let json = fs::read_to_string(path)?; diff --git a/lib/tests/conditional_test.rs b/lib/tests/conditional_test.rs index cc4f02f..fc3e4b1 100644 --- a/lib/tests/conditional_test.rs +++ b/lib/tests/conditional_test.rs @@ -4,7 +4,7 @@ mod common; #[test] fn iftrue_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/conditional/iftrue.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/conditional/iftrue.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); @@ -19,7 +19,7 @@ fn iftrue_test() -> Result<(), StoryError> { #[test] fn iffalse_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/conditional/iffalse.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/conditional/iffalse.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); @@ -34,7 +34,7 @@ fn iffalse_test() -> Result<(), StoryError> { #[test] fn ifelse_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/conditional/ifelse.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/conditional/ifelse.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); @@ -50,7 +50,7 @@ fn ifelse_test() -> Result<(), StoryError> { #[test] fn ifelse_ext_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/conditional/ifelse-ext.ink.json").unwrap(); + common::get_json_string("inkfiles/conditional/ifelse-ext.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); @@ -66,7 +66,7 @@ fn ifelse_ext_test() -> Result<(), StoryError> { #[test] fn ifelse_ext_text1_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/conditional/ifelse-ext-text1.ink.json").unwrap(); + common::get_json_string("inkfiles/conditional/ifelse-ext-text1.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); @@ -88,7 +88,7 @@ fn ifelse_ext_text1_test() -> Result<(), StoryError> { #[test] fn ifelse_ext_text2_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/conditional/ifelse-ext-text2.ink.json").unwrap(); + common::get_json_string("inkfiles/conditional/ifelse-ext-text2.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); @@ -110,7 +110,7 @@ fn ifelse_ext_text2_test() -> Result<(), StoryError> { #[test] fn ifelse_ext_text3_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/conditional/ifelse-ext-text3.ink.json").unwrap(); + common::get_json_string("inkfiles/conditional/ifelse-ext-text3.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); @@ -131,7 +131,7 @@ fn ifelse_ext_text3_test() -> Result<(), StoryError> { #[test] fn cond_text1_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/conditional/condtext.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/conditional/condtext.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -152,7 +152,7 @@ fn cond_text1_test() -> Result<(), StoryError> { #[test] fn cond_text2_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/conditional/condtext.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/conditional/condtext.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -173,7 +173,7 @@ fn cond_text2_test() -> Result<(), StoryError> { #[test] fn cond_opt1_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/conditional/condopt.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/conditional/condopt.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -190,7 +190,7 @@ fn cond_opt1_test() -> Result<(), StoryError> { #[test] fn cond_opt2_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/conditional/condopt.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/conditional/condopt.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -207,7 +207,7 @@ fn cond_opt2_test() -> Result<(), StoryError> { #[test] fn stopping_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/conditional/stopping.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/conditional/stopping.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -240,7 +240,7 @@ fn stopping_test() -> Result<(), StoryError> { #[test] fn cycle_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/conditional/cycle.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/conditional/cycle.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -273,7 +273,7 @@ fn cycle_test() -> Result<(), StoryError> { #[test] fn once_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/conditional/once.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/conditional/once.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -303,7 +303,7 @@ fn once_test() -> Result<(), StoryError> { #[test] fn shuffle_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/conditional/shuffle.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/conditional/shuffle.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -334,7 +334,7 @@ fn shuffle_test() -> Result<(), StoryError> { #[test] fn shuffle_stopping() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/conditional/shuffle_stopping.ink.json").unwrap(); + common::get_json_string("inkfiles/conditional/shuffle_stopping.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -367,7 +367,7 @@ fn shuffle_stopping() -> Result<(), StoryError> { #[test] fn shuffle_once() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/conditional/shuffle_once.ink.json").unwrap(); + common::get_json_string("inkfiles/conditional/shuffle_once.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -397,7 +397,7 @@ fn shuffle_once() -> Result<(), StoryError> { #[test] fn multiline_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/conditional/multiline.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/conditional/multiline.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -426,7 +426,7 @@ fn multiline_test() -> Result<(), StoryError> { #[test] fn multiline_divert_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/conditional/multiline-divert.ink.json").unwrap(); + common::get_json_string("inkfiles/conditional/multiline-divert.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); @@ -455,7 +455,7 @@ fn multiline_divert_test() -> Result<(), StoryError> { #[test] fn multiline_choice_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/conditional/multiline-choice.ink.json").unwrap(); + common::get_json_string("inkfiles/conditional/multiline-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); let mut text: Vec = Vec::new(); diff --git a/lib/tests/divert_test.rs b/lib/tests/divert_test.rs index 3d23123..532224c 100644 --- a/lib/tests/divert_test.rs +++ b/lib/tests/divert_test.rs @@ -4,7 +4,7 @@ mod common; #[test] fn simple_divert_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/divert/simple-divert.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/divert/simple-divert.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -22,7 +22,7 @@ fn simple_divert_test() -> Result<(), StoryError> { #[test] fn invisible_divert_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/divert/invisible-divert.ink.json").unwrap(); + common::get_json_string("inkfiles/divert/invisible-divert.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -39,7 +39,7 @@ fn invisible_divert_test() -> Result<(), StoryError> { #[test] fn divert_on_choice_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/divert/divert-on-choice.ink.json").unwrap(); + common::get_json_string("inkfiles/divert/divert-on-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -58,7 +58,7 @@ fn divert_on_choice_test() -> Result<(), StoryError> { #[test] fn complex_branching1_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/divert/complex-branching.ink.json").unwrap(); + common::get_json_string("inkfiles/divert/complex-branching.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -81,7 +81,7 @@ fn complex_branching1_test() -> Result<(), StoryError> { #[test] fn complex_branching2_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/divert/complex-branching.ink.json").unwrap(); + common::get_json_string("inkfiles/divert/complex-branching.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); diff --git a/lib/tests/function_test.rs b/lib/tests/function_test.rs index 6614920..9f8b19e 100644 --- a/lib/tests/function_test.rs +++ b/lib/tests/function_test.rs @@ -4,7 +4,7 @@ mod common; #[test] fn fun_basic_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/function/func-basic.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/function/func-basic.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -17,7 +17,7 @@ fn fun_basic_test() -> Result<(), StoryError> { #[test] fn fun_none_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/function/func-none.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/function/func-none.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -30,7 +30,7 @@ fn fun_none_test() -> Result<(), StoryError> { #[test] fn fun_inline_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/function/func-inline.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/function/func-inline.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -43,7 +43,7 @@ fn fun_inline_test() -> Result<(), StoryError> { #[test] fn setvar_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/function/setvar-func.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/function/setvar-func.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -57,7 +57,7 @@ fn setvar_test() -> Result<(), StoryError> { #[test] fn complex_func1_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/function/complex-func1.ink.json").unwrap(); + common::get_json_string("inkfiles/function/complex-func1.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -71,7 +71,7 @@ fn complex_func1_test() -> Result<(), StoryError> { #[test] fn complex_func2_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/function/complex-func2.ink.json").unwrap(); + common::get_json_string("inkfiles/function/complex-func2.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -85,7 +85,7 @@ fn complex_func2_test() -> Result<(), StoryError> { #[test] fn complex_func3_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/function/complex-func3.ink.json").unwrap(); + common::get_json_string("inkfiles/function/complex-func3.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -99,7 +99,7 @@ fn complex_func3_test() -> Result<(), StoryError> { #[test] fn rnd() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/function/rnd-func.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/function/rnd-func.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -116,7 +116,7 @@ fn rnd() -> Result<(), StoryError> { #[test] fn evaluating_function_variable_state_bug_test() -> Result<(), StoryError> { let json_string = common::get_json_string( - "tests/data/function/evaluating-function-variablestate-bug.ink.json", + "inkfiles/function/evaluating-function-variablestate-bug.ink.json", ) .unwrap(); let mut story = Story::new(&json_string)?; diff --git a/lib/tests/gather_test.rs b/lib/tests/gather_test.rs index 23b747f..aaa02a2 100644 --- a/lib/tests/gather_test.rs +++ b/lib/tests/gather_test.rs @@ -4,7 +4,7 @@ mod common; #[test] fn gather_basic_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/gather/gather-basic.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/gather/gather-basic.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -24,7 +24,7 @@ fn gather_basic_test() -> Result<(), StoryError> { #[test] fn gather_chain_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/gather/gather-chain.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/gather/gather-chain.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -57,7 +57,7 @@ fn gather_chain_test() -> Result<(), StoryError> { #[test] fn nested_flow_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/gather/nested-flow.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/gather/nested-flow.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -79,7 +79,7 @@ fn nested_flow_test() -> Result<(), StoryError> { #[test] fn deep_nesting_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/gather/deep-nesting.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/gather/deep-nesting.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -113,7 +113,7 @@ fn deep_nesting_test() -> Result<(), StoryError> { #[test] fn complex_flow1_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/gather/complex-flow.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/gather/complex-flow.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -133,7 +133,7 @@ fn complex_flow1_test() -> Result<(), StoryError> { #[test] fn complex_flow2_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/gather/complex-flow.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/gather/complex-flow.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); diff --git a/lib/tests/glue_test.rs b/lib/tests/glue_test.rs index 96bc675..53d2cf9 100644 --- a/lib/tests/glue_test.rs +++ b/lib/tests/glue_test.rs @@ -4,7 +4,7 @@ mod common; #[test] fn simple_glue_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/glue/simple-glue.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/glue/simple-glue.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -17,7 +17,7 @@ fn simple_glue_test() -> Result<(), StoryError> { #[test] fn glue_with_divert_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/glue/glue-with-divert.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/glue/glue-with-divert.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -35,7 +35,7 @@ fn glue_with_divert_test() -> Result<(), StoryError> { #[test] fn has_left_right_glue_matching_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/glue/left-right-glue-matching.ink.json").unwrap(); + common::get_json_string("inkfiles/glue/left-right-glue-matching.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -50,7 +50,7 @@ fn has_left_right_glue_matching_test() -> Result<(), StoryError> { #[test] fn bugfix1_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/glue/testbugfix1.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/glue/testbugfix1.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -65,7 +65,7 @@ fn bugfix1_test() -> Result<(), StoryError> { #[test] fn bugfix2_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/glue/testbugfix2.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/glue/testbugfix2.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); diff --git a/lib/tests/knot_test.rs b/lib/tests/knot_test.rs index 5ffb630..40413e7 100644 --- a/lib/tests/knot_test.rs +++ b/lib/tests/knot_test.rs @@ -4,7 +4,7 @@ mod common; #[test] fn single_line_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/knot/single-line.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/knot/single-line.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -18,7 +18,7 @@ fn single_line_test() -> Result<(), StoryError> { #[test] fn multi_line_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/knot/multi-line.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/knot/multi-line.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -35,7 +35,7 @@ fn multi_line_test() -> Result<(), StoryError> { #[test] fn strip_empty_lines_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/knot/strip-empty-lines.ink.json").unwrap(); + common::get_json_string("inkfiles/knot/strip-empty-lines.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -51,7 +51,7 @@ fn strip_empty_lines_test() -> Result<(), StoryError> { #[test] fn param_strings_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/knot/param-strings.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/knot/param-strings.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -69,7 +69,7 @@ fn param_strings_test() -> Result<(), StoryError> { #[test] fn param_ints_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/knot/param-ints.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/knot/param-ints.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -87,7 +87,7 @@ fn param_ints_test() -> Result<(), StoryError> { #[test] fn param_floats_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/knot/param-floats.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/knot/param-floats.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -105,7 +105,7 @@ fn param_floats_test() -> Result<(), StoryError> { #[test] fn param_vars_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/knot/param-vars.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/knot/param-vars.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -123,7 +123,7 @@ fn param_vars_test() -> Result<(), StoryError> { #[test] fn param_multi_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/knot/param-multi.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/knot/param-multi.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -141,7 +141,7 @@ fn param_multi_test() -> Result<(), StoryError> { #[test] fn param_recurse_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/knot/param-recurse.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/knot/param-recurse.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); diff --git a/lib/tests/list_test.rs b/lib/tests/list_test.rs index 789f262..c6934a0 100644 --- a/lib/tests/list_test.rs +++ b/lib/tests/list_test.rs @@ -6,7 +6,7 @@ mod common; #[test] fn list_basic_operations_test() -> Result<(), Box> { - let json_string = common::get_json_string("tests/data/lists/basic-operations.ink.json")?; + let json_string = common::get_json_string("inkfiles/lists/basic-operations.ink.json")?; let mut story = Story::new(&json_string)?; assert_eq!( @@ -19,7 +19,7 @@ fn list_basic_operations_test() -> Result<(), Box> { #[test] fn list_mixed_items_test() -> Result<(), Box> { - let json_string = common::get_json_string("tests/data/lists/list-mixed-items.ink.json")?; + let json_string = common::get_json_string("inkfiles/lists/list-mixed-items.ink.json")?; let mut story = Story::new(&json_string)?; assert_eq!("a, y, c\n", &story.continue_maximally()?); @@ -29,7 +29,7 @@ fn list_mixed_items_test() -> Result<(), Box> { #[test] fn more_list_operations_test() -> Result<(), Box> { - let json_string = common::get_json_string("tests/data/lists/more-list-operations.ink.json")?; + let json_string = common::get_json_string("inkfiles/lists/more-list-operations.ink.json")?; let mut story = Story::new(&json_string)?; assert_eq!("1\nl\nn\nl, m\nn\n", &story.continue_maximally()?); @@ -39,7 +39,7 @@ fn more_list_operations_test() -> Result<(), Box> { #[test] fn empty_list_origin_test() -> Result<(), Box> { - let json_string = common::get_json_string("tests/data/lists/empty-list-origin.ink.json")?; + let json_string = common::get_json_string("inkfiles/lists/empty-list-origin.ink.json")?; let mut story = Story::new(&json_string)?; assert_eq!("a, b\n", &story.continue_maximally()?); @@ -49,7 +49,7 @@ fn empty_list_origin_test() -> Result<(), Box> { #[test] fn list_save_load_test() -> Result<(), Box> { - let json_string = common::get_json_string("tests/data/lists/list-save-load.ink.json")?; + let json_string = common::get_json_string("inkfiles/lists/list-save-load.ink.json")?; let mut story = Story::new(&json_string)?; assert_eq!("a, x, c\n", &story.continue_maximally()?); @@ -70,7 +70,7 @@ fn list_save_load_test() -> Result<(), Box> { #[test] fn empty_list_origin_after_assinment_test() -> Result<(), Box> { let json_string = - common::get_json_string("tests/data/lists/empty-list-origin-after-assignment.ink.json")?; + common::get_json_string("inkfiles/lists/empty-list-origin-after-assignment.ink.json")?; let mut story = Story::new(&json_string)?; assert_eq!("a, b, c\n", &story.continue_maximally()?); @@ -80,7 +80,7 @@ fn empty_list_origin_after_assinment_test() -> Result<(), Box> { #[test] fn list_range_test() -> Result<(), Box> { - let json_string = common::get_json_string("tests/data/lists/list-range.ink.json")?; + let json_string = common::get_json_string("inkfiles/lists/list-range.ink.json")?; let mut story = Story::new(&json_string)?; assert_eq!("Pound, Pizza, Euro, Pasta, Dollar, Curry, Paella\nEuro, Pasta, Dollar, Curry\nTwo, Three, Four, Five, Six\nPizza, Pasta\n", &story.continue_maximally()?); @@ -90,7 +90,7 @@ fn list_range_test() -> Result<(), Box> { #[test] fn list_bug_adding_element_test() -> Result<(), Box> { - let json_string = common::get_json_string("tests/data/lists/bug-adding-element.ink.json")?; + let json_string = common::get_json_string("inkfiles/lists/bug-adding-element.ink.json")?; let mut story = Story::new(&json_string)?; assert_eq!("", &story.continue_maximally()?); @@ -106,7 +106,7 @@ fn list_bug_adding_element_test() -> Result<(), Box> { #[test] fn more_list_operations2_test() -> Result<(), Box> { - let json_string = common::get_json_string("tests/data/lists/more-list-operations2.ink.json")?; + let json_string = common::get_json_string("inkfiles/lists/more-list-operations2.ink.json")?; let mut story = Story::new(&json_string)?; assert_eq!("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:c2\nlistinc:b1\n", &story.continue_maximally()?); diff --git a/lib/tests/misc_test.rs b/lib/tests/misc_test.rs index ff833e5..4922f09 100644 --- a/lib/tests/misc_test.rs +++ b/lib/tests/misc_test.rs @@ -4,7 +4,7 @@ mod common; #[test] fn operations_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/misc/operations.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/misc/operations.ink.json").unwrap(); let mut story = Story::new(&json_string)?; assert_eq!( @@ -17,7 +17,7 @@ fn operations_test() -> Result<(), StoryError> { #[test] fn read_counts_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/misc/read-counts.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/misc/read-counts.ink.json").unwrap(); let mut story = Story::new(&json_string)?; assert_eq!( @@ -30,7 +30,7 @@ fn read_counts_test() -> Result<(), StoryError> { #[test] fn turns_since_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/misc/turns-since.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/misc/turns-since.ink.json").unwrap(); let mut story = Story::new(&json_string)?; assert_eq!("0\n0\n", &story.continue_maximally()?); @@ -45,7 +45,7 @@ fn turns_since_test() -> Result<(), StoryError> { */ #[test] fn issue15_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/misc/issue15.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/misc/issue15.ink.json").unwrap(); let mut story = Story::new(&json_string)?; assert_eq!("This is a test\n", story.cont()?); diff --git a/lib/tests/multi_flow_test.rs b/lib/tests/multi_flow_test.rs index 0ac3d93..5921f76 100644 --- a/lib/tests/multi_flow_test.rs +++ b/lib/tests/multi_flow_test.rs @@ -5,7 +5,7 @@ mod common; #[test] fn basics_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/runtime/multiflow-basics.ink.json").unwrap(); + common::get_json_string("inkfiles/runtime/multiflow-basics.ink.json").unwrap(); let mut story = Story::new(&json_string)?; story.switch_flow("First")?; @@ -28,7 +28,7 @@ fn basics_test() -> Result<(), StoryError> { #[test] fn multiflow_save_load_threads() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/runtime/multiflow-saveloadthreads.ink.json").unwrap(); + common::get_json_string("inkfiles/runtime/multiflow-saveloadthreads.ink.json").unwrap(); let mut story = Story::new(&json_string)?; // Default flow diff --git a/lib/tests/runtime_test.rs b/lib/tests/runtime_test.rs index e496639..e6fc60a 100644 --- a/lib/tests/runtime_test.rs +++ b/lib/tests/runtime_test.rs @@ -46,7 +46,7 @@ impl ExternalFunction for ExtFunc4 { #[test] fn external_function() -> Result<(), Box> { let json_string = - common::get_json_string("tests/data/runtime/external-function-2-arg.ink.json")?; + common::get_json_string("inkfiles/runtime/external-function-2-arg.ink.json")?; let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -62,7 +62,7 @@ fn external_function() -> Result<(), Box> { #[test] fn external_function_zero_arguments() -> Result<(), Box> { let json_string = - common::get_json_string("tests/data/runtime/external-function-0-arg.ink.json")?; + common::get_json_string("inkfiles/runtime/external-function-0-arg.ink.json")?; let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -78,7 +78,7 @@ fn external_function_zero_arguments() -> Result<(), Box> { #[test] fn external_function_one_arguments() -> Result<(), Box> { let json_string = - common::get_json_string("tests/data/runtime/external-function-1-arg.ink.json")?; + common::get_json_string("inkfiles/runtime/external-function-1-arg.ink.json")?; let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -94,7 +94,7 @@ fn external_function_one_arguments() -> Result<(), Box> { #[test] fn external_function_coerce_test() -> Result<(), Box> { let json_string = - common::get_json_string("tests/data/runtime/external-function-1-arg.ink.json")?; + common::get_json_string("inkfiles/runtime/external-function-1-arg.ink.json")?; let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -110,7 +110,7 @@ fn external_function_coerce_test() -> Result<(), Box> { #[test] fn external_function_fallback_test() -> Result<(), Box> { let json_string = - common::get_json_string("tests/data/runtime/external-function-2-arg.ink.json")?; + common::get_json_string("inkfiles/runtime/external-function-2-arg.ink.json")?; let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -145,7 +145,7 @@ impl VariableObserver for VObserver { #[test] fn variable_observers_test() -> Result<(), Box> { - let json_string = common::get_json_string("tests/data/runtime/variable-observers.ink.json")?; + let json_string = common::get_json_string("inkfiles/runtime/variable-observers.ink.json")?; let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -160,7 +160,7 @@ fn variable_observers_test() -> Result<(), Box> { #[test] fn set_and_get_variable_test() -> Result<(), Box> { - let json_string = common::get_json_string("tests/data/runtime/set-get-variables.ink.json")?; + let json_string = common::get_json_string("inkfiles/runtime/set-get-variables.ink.json")?; let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -184,7 +184,7 @@ fn set_and_get_variable_test() -> Result<(), Box> { #[test] fn set_non_existant_variable_test() -> Result<(), Box> { - let json_string = common::get_json_string("tests/data/runtime/set-get-variables.ink.json")?; + let json_string = common::get_json_string("inkfiles/runtime/set-get-variables.ink.json")?; let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -212,7 +212,7 @@ fn set_non_existant_variable_test() -> Result<(), Box> { #[test] fn jump_knot_test() -> Result<(), Box> { - let json_string = common::get_json_string("tests/data/runtime/jump-knot.ink.json")?; + let json_string = common::get_json_string("inkfiles/runtime/jump-knot.ink.json")?; let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -240,7 +240,7 @@ fn jump_knot_test() -> Result<(), Box> { #[test] fn jump_stitch_test() -> Result<(), Box> { - let json_string = common::get_json_string("tests/data/runtime/jump-stitch.ink.json")?; + let json_string = common::get_json_string("inkfiles/runtime/jump-stitch.ink.json")?; let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -268,7 +268,7 @@ fn jump_stitch_test() -> Result<(), Box> { #[test] fn read_visit_counts_test() -> Result<(), Box> { - let json_string = common::get_json_string("tests/data/runtime/read-visit-counts.ink.json")?; + let json_string = common::get_json_string("inkfiles/runtime/read-visit-counts.ink.json")?; let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -281,7 +281,7 @@ fn read_visit_counts_test() -> Result<(), Box> { #[test] fn load_save_test() -> Result<(), Box> { - let json_string = common::get_json_string("tests/data/runtime/load-save.ink.json")?; + let json_string = common::get_json_string("inkfiles/runtime/load-save.ink.json")?; let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); diff --git a/lib/tests/stitch_test.rs b/lib/tests/stitch_test.rs index 75124a2..529c6e9 100644 --- a/lib/tests/stitch_test.rs +++ b/lib/tests/stitch_test.rs @@ -4,7 +4,7 @@ mod common; #[test] fn auto_stitch_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/stitch/auto-stitch.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/stitch/auto-stitch.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -18,7 +18,7 @@ fn auto_stitch_test() -> Result<(), StoryError> { #[test] fn auto_stitch2_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/stitch/auto-stitch.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/stitch/auto-stitch.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -35,7 +35,7 @@ fn auto_stitch2_test() -> Result<(), StoryError> { #[test] fn manual_stitch_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/stitch/manual-stitch.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/stitch/manual-stitch.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -56,7 +56,7 @@ fn manual_stitch_test() -> Result<(), StoryError> { #[test] fn manual_stitch2_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/stitch/manual-stitch.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/stitch/manual-stitch.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); diff --git a/lib/tests/tag_test.rs b/lib/tests/tag_test.rs index e32e933..40ccfa5 100644 --- a/lib/tests/tag_test.rs +++ b/lib/tests/tag_test.rs @@ -4,7 +4,7 @@ mod common; #[test] fn tags_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/tags/tags.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/tags/tags.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let global_tags = story.get_global_tags()?; @@ -42,7 +42,7 @@ fn tags_test() -> Result<(), StoryError> { #[test] fn tags_in_seq_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/tags/tagsInSeq.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/tags/tagsInSeq.ink.json").unwrap(); let mut story = Story::new(&json_string)?; assert_eq!("A red sequence.\n", story.cont()?); @@ -60,7 +60,7 @@ fn tags_in_seq_test() -> Result<(), StoryError> { #[test] fn tags_in_choice_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/tags/tagsInChoice.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/tags/tagsInChoice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; story.cont()?; @@ -85,7 +85,7 @@ fn tags_in_choice_test() -> Result<(), StoryError> { #[test] fn tags_dynamic_content_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/tags/tagsDynamicContent.ink.json").unwrap(); + common::get_json_string("inkfiles/tags/tagsDynamicContent.ink.json").unwrap(); let mut story = Story::new(&json_string)?; assert_eq!("tag\n", story.cont()?); diff --git a/lib/tests/thread_test.rs b/lib/tests/thread_test.rs index decfde9..df6d1c2 100644 --- a/lib/tests/thread_test.rs +++ b/lib/tests/thread_test.rs @@ -4,7 +4,7 @@ mod common; #[test] fn thread_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/threads/thread-bug.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/threads/thread-bug.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); @@ -30,7 +30,7 @@ fn thread_test() -> Result<(), StoryError> { #[test] fn thread_test_bug() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/threads/thread-bug.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/threads/thread-bug.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); diff --git a/lib/tests/tunnel_test.rs b/lib/tests/tunnel_test.rs index 4e0e1fb..c92a25b 100644 --- a/lib/tests/tunnel_test.rs +++ b/lib/tests/tunnel_test.rs @@ -5,7 +5,7 @@ mod common; #[test] fn tunnel_onwards_divert_override_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/tunnels/tunnel-onwards-divert-override.ink.json") + common::get_json_string("inkfiles/tunnels/tunnel-onwards-divert-override.ink.json") .unwrap(); let mut story = Story::new(&json_string)?; diff --git a/lib/tests/variable_test.rs b/lib/tests/variable_test.rs index b0f12ff..826ade5 100644 --- a/lib/tests/variable_test.rs +++ b/lib/tests/variable_test.rs @@ -5,7 +5,7 @@ mod common; #[test] fn variable_declaration_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/variable/variable-declaration.ink.json").unwrap(); + common::get_json_string("inkfiles/variable/variable-declaration.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -21,7 +21,7 @@ fn variable_declaration_test() -> Result<(), StoryError> { #[test] fn var_calc_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/variable/varcalc.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/variable/varcalc.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -34,7 +34,7 @@ fn var_calc_test() -> Result<(), StoryError> { #[test] fn var_string_ink_bug_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/variable/varstringinc.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/variable/varstringinc.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -51,7 +51,7 @@ fn var_string_ink_bug_test() -> Result<(), StoryError> { #[test] fn var_divert_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/variable/var-divert.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/variable/var-divert.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); diff --git a/lib/tests/variable_text_test.rs b/lib/tests/variable_text_test.rs index a02aa69..fe746b9 100644 --- a/lib/tests/variable_text_test.rs +++ b/lib/tests/variable_text_test.rs @@ -4,7 +4,7 @@ mod common; #[test] fn sequence_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/variabletext/sequence.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/variabletext/sequence.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -52,7 +52,7 @@ fn sequence_test() -> Result<(), StoryError> { #[test] fn cycle_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/variabletext/cycle.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/variabletext/cycle.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -94,7 +94,7 @@ fn cycle_test() -> Result<(), StoryError> { #[test] fn once_test() -> Result<(), StoryError> { - let json_string = common::get_json_string("tests/data/variabletext/once.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/variabletext/once.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -137,7 +137,7 @@ fn once_test() -> Result<(), StoryError> { #[test] fn empty_elements_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/variabletext/empty-elements.ink.json").unwrap(); + common::get_json_string("inkfiles/variabletext/empty-elements.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -166,7 +166,7 @@ fn empty_elements_test() -> Result<(), StoryError> { #[test] fn list_in_choice_test() -> Result<(), StoryError> { let json_string = - common::get_json_string("tests/data/variabletext/list-in-choice.ink.json").unwrap(); + common::get_json_string("inkfiles/variabletext/list-in-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); From eaa294427c4fe5328c8b5ae0d5300bffa064d593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Mon, 9 Oct 2023 21:55:39 +0000 Subject: [PATCH 72/91] fmt + more GHA checks --- .github/workflows/ci.yml | 7 +++++++ cli-player/Cargo.toml | 2 +- cli-player/tests/test_the_intercept.rs | 2 +- lib/tests/choice_test.rs | 12 ++++-------- lib/tests/common/mod.rs | 4 +--- lib/tests/conditional_test.rs | 3 +-- lib/tests/divert_test.rs | 6 ++---- lib/tests/function_test.rs | 16 ++++++---------- lib/tests/knot_test.rs | 3 +-- lib/tests/runtime_test.rs | 15 +++++---------- lib/tests/tag_test.rs | 3 +-- 11 files changed, 30 insertions(+), 43 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b41ed50..8699675 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,5 +21,12 @@ jobs: - name: Build run: cargo build --verbose + + - name: Check Clippy + run: cargo clippy --verbose + + - name: Check fmt + run: cargo fmt -- --check + - name: Test run: cargo test --verbose diff --git a/cli-player/Cargo.toml b/cli-player/Cargo.toml index 0487a57..9153782 100644 --- a/cli-player/Cargo.toml +++ b/cli-player/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "binkplayer" -version = "0.1.0" +version = "0.9.0" description = """ Console player for compiled .json Ink story files. """ diff --git a/cli-player/tests/test_the_intercept.rs b/cli-player/tests/test_the_intercept.rs index 3c18d2a..77e33b9 100644 --- a/cli-player/tests/test_the_intercept.rs +++ b/cli-player/tests/test_the_intercept.rs @@ -4,7 +4,7 @@ use std::path::Path; use std::process::{Command, Stdio}; #[test] -fn the_intercept_test() -> Result<(), Box> { +fn the_intercept_test() -> Result<(), Box> { let mut cmd = Command::cargo_bin("binkplayer")?; let mut path = Path::new("inkfiles/TheIntercept.ink.json").to_path_buf(); diff --git a/lib/tests/choice_test.rs b/lib/tests/choice_test.rs index 9579383..0da2693 100644 --- a/lib/tests/choice_test.rs +++ b/lib/tests/choice_test.rs @@ -97,8 +97,7 @@ fn single_choic2_test() -> Result<(), StoryError> { #[test] fn suppress_choice_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("inkfiles/choices/suppress-choice.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/choices/suppress-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -143,8 +142,7 @@ fn mixed_choice_test() -> Result<(), StoryError> { #[test] fn varying_choice_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("inkfiles/choices/varying-choice.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/choices/varying-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -184,8 +182,7 @@ fn sticky_choice_test() -> Result<(), StoryError> { #[test] fn fallback_choice_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("inkfiles/choices/fallback-choice.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/choices/fallback-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -197,8 +194,7 @@ fn fallback_choice_test() -> Result<(), StoryError> { #[test] fn fallback_choice2_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("inkfiles/choices/fallback-choice.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/choices/fallback-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); diff --git a/lib/tests/common/mod.rs b/lib/tests/common/mod.rs index c9955bc..983b5ef 100644 --- a/lib/tests/common/mod.rs +++ b/lib/tests/common/mod.rs @@ -1,6 +1,6 @@ #![allow(dead_code)] -use std::{error::Error, path::Path, fs}; +use std::{error::Error, fs, path::Path}; use bink::{story::Story, story_error::StoryError}; use rand::Rng; @@ -32,7 +32,6 @@ pub fn join_text(text: &Vec) -> String { sb } - pub fn run_story( filename: &str, choice_list: Option>, @@ -50,7 +49,6 @@ pub fn run_story( let mut rng = rand::thread_rng(); while story.can_continue() || !story.get_current_choices().is_empty() { - println!("{}", story.build_string_of_hierarchy()); // 2) Game content, line by line diff --git a/lib/tests/conditional_test.rs b/lib/tests/conditional_test.rs index fc3e4b1..9855e33 100644 --- a/lib/tests/conditional_test.rs +++ b/lib/tests/conditional_test.rs @@ -49,8 +49,7 @@ fn ifelse_test() -> Result<(), StoryError> { #[test] fn ifelse_ext_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("inkfiles/conditional/ifelse-ext.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/conditional/ifelse-ext.ink.json").unwrap(); let mut story = Story::new(&json_string)?; println!("{}", story.build_string_of_hierarchy()); diff --git a/lib/tests/divert_test.rs b/lib/tests/divert_test.rs index 532224c..9f74fdd 100644 --- a/lib/tests/divert_test.rs +++ b/lib/tests/divert_test.rs @@ -21,8 +21,7 @@ fn simple_divert_test() -> Result<(), StoryError> { #[test] fn invisible_divert_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("inkfiles/divert/invisible-divert.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/divert/invisible-divert.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -38,8 +37,7 @@ fn invisible_divert_test() -> Result<(), StoryError> { #[test] fn divert_on_choice_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("inkfiles/divert/divert-on-choice.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/divert/divert-on-choice.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); diff --git a/lib/tests/function_test.rs b/lib/tests/function_test.rs index 9f8b19e..b312fb2 100644 --- a/lib/tests/function_test.rs +++ b/lib/tests/function_test.rs @@ -56,8 +56,7 @@ fn setvar_test() -> Result<(), StoryError> { #[test] fn complex_func1_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("inkfiles/function/complex-func1.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/function/complex-func1.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -70,8 +69,7 @@ fn complex_func1_test() -> Result<(), StoryError> { #[test] fn complex_func2_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("inkfiles/function/complex-func2.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/function/complex-func2.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -84,8 +82,7 @@ fn complex_func2_test() -> Result<(), StoryError> { #[test] fn complex_func3_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("inkfiles/function/complex-func3.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/function/complex-func3.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; @@ -115,10 +112,9 @@ fn rnd() -> Result<(), StoryError> { #[test] fn evaluating_function_variable_state_bug_test() -> Result<(), StoryError> { - let json_string = common::get_json_string( - "inkfiles/function/evaluating-function-variablestate-bug.ink.json", - ) - .unwrap(); + let json_string = + common::get_json_string("inkfiles/function/evaluating-function-variablestate-bug.ink.json") + .unwrap(); let mut story = Story::new(&json_string)?; assert_eq!("Start\n", story.cont()?); diff --git a/lib/tests/knot_test.rs b/lib/tests/knot_test.rs index 40413e7..82f0aaa 100644 --- a/lib/tests/knot_test.rs +++ b/lib/tests/knot_test.rs @@ -34,8 +34,7 @@ fn multi_line_test() -> Result<(), StoryError> { #[test] fn strip_empty_lines_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("inkfiles/knot/strip-empty-lines.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/knot/strip-empty-lines.ink.json").unwrap(); let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); diff --git a/lib/tests/runtime_test.rs b/lib/tests/runtime_test.rs index e6fc60a..a4ba90d 100644 --- a/lib/tests/runtime_test.rs +++ b/lib/tests/runtime_test.rs @@ -45,8 +45,7 @@ impl ExternalFunction for ExtFunc4 { #[test] fn external_function() -> Result<(), Box> { - let json_string = - common::get_json_string("inkfiles/runtime/external-function-2-arg.ink.json")?; + let json_string = common::get_json_string("inkfiles/runtime/external-function-2-arg.ink.json")?; let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -61,8 +60,7 @@ fn external_function() -> Result<(), Box> { #[test] fn external_function_zero_arguments() -> Result<(), Box> { - let json_string = - common::get_json_string("inkfiles/runtime/external-function-0-arg.ink.json")?; + let json_string = common::get_json_string("inkfiles/runtime/external-function-0-arg.ink.json")?; let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -77,8 +75,7 @@ fn external_function_zero_arguments() -> Result<(), Box> { #[test] fn external_function_one_arguments() -> Result<(), Box> { - let json_string = - common::get_json_string("inkfiles/runtime/external-function-1-arg.ink.json")?; + let json_string = common::get_json_string("inkfiles/runtime/external-function-1-arg.ink.json")?; let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -93,8 +90,7 @@ fn external_function_one_arguments() -> Result<(), Box> { #[test] fn external_function_coerce_test() -> Result<(), Box> { - let json_string = - common::get_json_string("inkfiles/runtime/external-function-1-arg.ink.json")?; + let json_string = common::get_json_string("inkfiles/runtime/external-function-1-arg.ink.json")?; let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); @@ -109,8 +105,7 @@ fn external_function_coerce_test() -> Result<(), Box> { #[test] fn external_function_fallback_test() -> Result<(), Box> { - let json_string = - common::get_json_string("inkfiles/runtime/external-function-2-arg.ink.json")?; + let json_string = common::get_json_string("inkfiles/runtime/external-function-2-arg.ink.json")?; let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); diff --git a/lib/tests/tag_test.rs b/lib/tests/tag_test.rs index 40ccfa5..9b50ca6 100644 --- a/lib/tests/tag_test.rs +++ b/lib/tests/tag_test.rs @@ -84,8 +84,7 @@ fn tags_in_choice_test() -> Result<(), StoryError> { #[test] fn tags_dynamic_content_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("inkfiles/tags/tagsDynamicContent.ink.json").unwrap(); + let json_string = common::get_json_string("inkfiles/tags/tagsDynamicContent.ink.json").unwrap(); let mut story = Story::new(&json_string)?; assert_eq!("tag\n", story.cont()?); From f6f6e95ac741300e33402a06e58273b70f6bcb80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Tue, 10 Oct 2023 10:35:19 +0000 Subject: [PATCH 73/91] Write documentation for public Fns. --- .github/workflows/ci.yml | 6 +-- cli-player/Cargo.toml | 1 + clib/Cargo.toml | 7 ++- lib/Cargo.toml | 4 +- lib/src/choice.rs | 15 ++++-- lib/src/json_write.rs | 2 +- lib/src/lib.rs | 2 +- lib/src/story.rs | 100 ++++++++++++++++++++++++++++++++++--- lib/src/story_callbacks.rs | 32 ++++++++++++ lib/src/story_error.rs | 4 ++ lib/src/story_state.rs | 10 ++-- lib/src/value_type.rs | 19 +++++-- 12 files changed, 172 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8699675..66cc700 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,13 +20,13 @@ jobs: override: true - name: Build - run: cargo build --verbose + run: cargo build - name: Check Clippy - run: cargo clippy --verbose + run: cargo clippy - name: Check fmt run: cargo fmt -- --check - name: Test - run: cargo test --verbose + run: cargo test diff --git a/cli-player/Cargo.toml b/cli-player/Cargo.toml index 9153782..a07c49a 100644 --- a/cli-player/Cargo.toml +++ b/cli-player/Cargo.toml @@ -5,6 +5,7 @@ description = """ Console player for compiled .json Ink story files. """ authors = ["Rafael Garcia "] +license = "Apache-2.0" edition = "2021" [[bin]] diff --git a/clib/Cargo.toml b/clib/Cargo.toml index 548e109..250aae8 100644 --- a/clib/Cargo.toml +++ b/clib/Cargo.toml @@ -1,6 +1,11 @@ [package] name = "binkc" version = "0.1.0" +description = """ +C bindings for the `bink` library. +""" +license = "Apache-2.0" +authors = ["Rafael Garcia "] edition = "2021" [lib] @@ -9,4 +14,4 @@ path = "src/lib.rs" crate-type = ["cdylib"] [dependencies] -bink = { "version" = "0.9.0", path = "../lib" } \ No newline at end of file +bink = { "version" = "0.9.0", path = "../lib" } diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 570955c..619dce7 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -5,8 +5,7 @@ authors = ["Rafael Garcia "] description = """ This is a Rust port of inkle's ink, a scripting language for writing interactive narrative. """ -license-file = "LICENSE" -readme = "README.md" +license = "Apache-2.0" repository = "https://github.com/bladecoder/blade-ink-rs/" edition = "2021" @@ -20,4 +19,3 @@ serde_json = "1.0.93" strum = { version = "0.25.0", features = ["derive"] } as-any = "0.3.0" rand = "0.8.5" - diff --git a/lib/src/choice.rs b/lib/src/choice.rs index ca68801..0a49264 100644 --- a/lib/src/choice.rs +++ b/lib/src/choice.rs @@ -1,3 +1,4 @@ +//! A generated Choice from the story. use core::fmt; use std::cell::RefCell; @@ -11,16 +12,20 @@ pub struct Choice { obj: Object, thread_at_generation: RefCell>, pub(crate) original_thread_index: RefCell, + /// Get the path to the original choice point - where was this choice defined in the story? pub(crate) source_path: String, pub(crate) target_path: Path, - pub is_invisible_default: bool, + pub(crate) is_invisible_default: bool, pub tags: Vec, + /// The original index into currentChoices list on the Story when + /// this Choice was generated, for convenience. pub index: RefCell, + /// The main text to presented to the player for this Choice. pub text: String, } impl Choice { - pub fn new( + pub(crate) fn new( target_path: Path, source_path: String, is_invisible_default: bool, @@ -41,7 +46,7 @@ impl Choice { } } - pub fn new_from_json( + pub(crate) fn new_from_json( path_string_on_choice: &str, source_path: String, text: &str, @@ -61,11 +66,11 @@ impl Choice { } } - pub fn set_thread_at_generation(&self, thread: Thread) { + pub(crate) fn set_thread_at_generation(&self, thread: Thread) { self.thread_at_generation.replace(Some(thread)); } - pub fn get_thread_at_generation(&self) -> Option { + pub(crate) fn get_thread_at_generation(&self) -> Option { self.thread_at_generation .borrow() .as_ref() diff --git a/lib/src/json_write.rs b/lib/src/json_write.rs index 81bef20..8418fea 100644 --- a/lib/src/json_write.rs +++ b/lib/src/json_write.rs @@ -260,7 +260,7 @@ pub fn write_choice(choice: &Choice) -> serde_json::Value { let mut jobj: Map = Map::new(); jobj.insert("text".to_owned(), json!(choice.text)); - jobj.insert("index".to_owned(), json!(choice.index)); + jobj.insert("index".to_owned(), json!(*choice.index.borrow())); jobj.insert("originalChoicePath".to_owned(), json!(choice.source_path)); jobj.insert( "originalThreadIndex".to_owned(), diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 0d21f04..5205f28 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,4 +1,4 @@ -//! This is a Rust port of inkle's [ink](https://github.com/inkle/ink), a scripting language for writing interactive narrative. +//! This is a Rust port of inkle's [Ink](https://github.com/inkle/ink), a scripting language for writing interactive narrative. //! `bink` is fully compatible with the original version. mod callstack; diff --git a/lib/src/story.rs b/lib/src/story.rs index 8c280fb..da8f22a 100644 --- a/lib/src/story.rs +++ b/lib/src/story.rs @@ -1,5 +1,4 @@ -#![allow(unused_variables, dead_code)] - +//! This is the entry point to load and run an Ink story. use std::{ cell::RefCell, collections::{HashMap, VecDeque}, @@ -36,7 +35,9 @@ use crate::{ void::Void, }; +/// The current version of the ink story file format. pub const INK_VERSION_CURRENT: i32 = 21; +/// The minimum legacy version of ink that can be loaded by the current version of the code. const INK_VERSION_MINIMUM_COMPATIBLE: i32 = 18; #[derive(PartialEq)] @@ -46,6 +47,8 @@ enum OutputStateChange { NewlineRemoved, } +/// A Story is the core struct that represents a complete Ink narrative, and +/// manages the evaluation and state of it. pub struct Story { main_content_container: Rc, state: StoryState, @@ -65,6 +68,7 @@ pub struct Story { } impl Story { + /// Construct a Story object using a JSON string compiled through inklecate. pub fn new(json_string: &str) -> Result { let json: serde_json::Value = match serde_json::from_str(json_string) { Ok(value) => value, @@ -1081,11 +1085,11 @@ impl Story { } } - if let Some(tag) = obj.as_ref().as_any().downcast_ref::() { + if obj.as_ref().as_any().downcast_ref::().is_some() { content_to_retain.push_back(obj.clone()); } - if let Some(sv) = Value::get_string_value(obj.as_ref()) { + if Value::get_string_value(obj.as_ref()).is_some() { content_stack_for_string.push_back(obj.clone()); } } @@ -1524,7 +1528,7 @@ impl Story { let found_value: Rc; // Explicit read count value - if let Some(p) = &var_ref.path_for_count { + if var_ref.path_for_count.is_some() { let container = var_ref.get_container_for_count(); let count = self .get_state_mut() @@ -1716,6 +1720,11 @@ impl Story { successful_increment } + /// The vector of Choice objects available at the current point in + /// the Story. This vector will be populated as the Story is stepped + /// through with the cont() method. Once can_continue becomes + /// false, this vector will be populated, and is usually + /// (but not always) on the final cont() step. pub fn get_current_choices(&self) -> Vec> { // Don't include invisible choices for external usage. let mut choices = Vec::new(); @@ -1732,14 +1741,26 @@ impl Story { choices } + /// Whether the currentErrors list contains any errors. + /// THIS MAY BE REMOVED - you should be setting an error handler directly + /// using Story.onError. pub fn has_error(&self) -> bool { self.get_state().has_error() } + /// Any errors generated during evaluation of the Story. pub fn get_current_errors(&self) -> &Vec { self.get_state().get_current_errors() } + /// Any warnings generated during evaluation of the Story. + pub fn get_current_warnings(&self) -> &Vec { + self.get_state().get_current_warnings() + } + + /// 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. pub fn choose_choice_index(&mut self, choice_index: usize) -> Result<(), StoryError> { let choices = self.get_current_choices(); if choice_index >= choices.len() { @@ -1867,7 +1888,7 @@ impl Story { choice_only_str_val.string.to_string() } - pub fn pointer_at_path( + pub(crate) fn pointer_at_path( main_content_container: &Rc, path: &Path, ) -> Result { @@ -1997,6 +2018,8 @@ impl Story { } } + /// Evaluates a function defined in ink, and gathers the possibly multi-line text as generated by the function. + /// This text output is any text written as normal content within the function, as opposed to the return value, as returned with `~ return`. pub fn evaluate_function( &mut self, func_name: &str, @@ -2102,10 +2125,15 @@ impl Story { )) } + /// Get any global tags associated with the story. These are defined as + /// hash tags defined at the very top of the story. pub fn get_global_tags(&self) -> Result, StoryError> { self.tags_at_start_of_flow_container_with_path_string("") } + /// 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. pub fn tags_for_content_at_path(&self, path: &str) -> Result, StoryError> { self.tags_at_start_of_flow_container_with_path_string(path) } @@ -2162,11 +2190,47 @@ impl Story { self.main_content_container.content_at_path(path, 0, -1) } + /// Gets a list of tags as defined with '#' in source that were seen + /// during the latest cont() call. pub fn get_current_tags(&mut self) -> Result, StoryError> { self.if_async_we_cant("call currentTags since it's a work in progress")?; Ok(self.get_state_mut().get_current_tags()) } + /// Change the current position of the story to the given path. From here you can + /// call `cont()` to evaluate the next line. + /// + /// The path string is a dot-separated path as used internally by the engine. + /// These examples should work: + /// + ///```ink + /// myKnot + /// myKnot.myStitch + ///``` + /// + /// Note however that this won't necessarily work: + /// + ///```ink + /// 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 error. pub fn choose_path_string( &mut self, path: &str, @@ -2221,6 +2285,7 @@ impl Story { Ok(()) } + /// Changes the current flow. pub fn switch_flow(&mut self, flow_name: &str) -> Result<(), StoryError> { self.if_async_we_cant("switch flow")?; @@ -2236,10 +2301,16 @@ impl Story { Ok(()) } + /// Removes the specified flow. pub fn remove_flow(&mut self, flow_name: &str) -> Result<(), StoryError> { self.get_state_mut().remove_flow_internal(flow_name) } + /// Removes the specified flow. + pub fn switch_to_default_flow(&mut self) { + self.get_state_mut().switch_to_default_flow_internal(); + } + pub(crate) fn if_async_we_cant(&self, activity_str: &str) -> Result<(), StoryError> { if self.async_continue_active { return Err(StoryError::InvalidStoryState(format!("Can't {}. Story is in the middle of a ContinueAsync(). Make more continue_async() calls or a single cont() call beforehand.", activity_str))); @@ -2248,6 +2319,8 @@ impl Story { Ok(()) } + /// Set the value of a named global ink variable. + /// The types available are the standard ink types. pub fn set_variable( &mut self, variable_name: &str, @@ -2265,22 +2338,37 @@ impl Story { Ok(()) } + /// Get the value of a named global ink variable. + /// The types available are the standard ink types. pub fn get_variable(&self, variable_name: &str) -> Option { self.get_state().variables_state.get(variable_name) } + /// Exports the current state to json format, in order to save the game. pub fn save_state(&self) -> Result { self.get_state().to_json() } + /// Loads a previously saved state in JSON format. pub fn load_state(&mut self, json_state: &str) -> Result<(), StoryError> { self.get_state_mut().load_json(json_state) } + /// 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: + /// + ///```ink + /// knot + /// knot.stitch + ///``` pub fn get_visit_count_at_path_string(&self, path_string: &str) -> Result { self.get_state().visit_count_at_path_string(path_string) } + /// 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 external + /// function, but you don't want it to fail to run. pub fn set_allow_external_function_fallbacks(&mut self, v: bool) { self.allow_external_function_fallbacks = v; } diff --git a/lib/src/story_callbacks.rs b/lib/src/story_callbacks.rs index 5d64210..1f9bc76 100644 --- a/lib/src/story_callbacks.rs +++ b/lib/src/story_callbacks.rs @@ -1,3 +1,4 @@ +//! For setting the callbacks functions that will be called while the story is processing. use std::{cell::RefCell, collections::HashSet, rc::Rc}; use crate::{ @@ -6,10 +7,12 @@ use crate::{ value_type::ValueType, void::Void, }; +/// Defines the method that will be called when a observed global variable changes. pub trait VariableObserver { fn changed(&mut self, variable_name: &str, value: &ValueType); } +/// Defines the method callback that implements the external function. pub trait ExternalFunction { fn call(&mut self, func_name: &str, args: Vec) -> Option; } @@ -19,6 +22,7 @@ pub(crate) struct ExternalFunctionDef { lookahead_safe: bool, } +/// Defines the method that will be called when an error occurs while executing the story. pub trait ErrorHandler { fn error(&mut self, message: &str, error_type: ErrorType); } @@ -32,10 +36,23 @@ pub enum ErrorType { } impl Story { + /// Assing the error handler for all runtime errors in ink - i.e. problems + /// with the source ink itself that are only discovered when playing + /// the story. + /// It's strongly recommended that you assign an error handler to your + /// story instance to avoid getting exceptions for ink errors. pub fn set_error_handler(&mut self, err_handler: Rc>) { self.on_error = Some(err_handler); } + /// 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.set_variable`. pub fn observe_variable( &mut self, variable_name: &str, @@ -65,6 +82,10 @@ impl Story { Ok(()) } + /// 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 None, then the observer will be removed + /// from all variables that it's subscribed to. pub fn remove_variable_observer( &mut self, observer: &Rc>, @@ -116,6 +137,17 @@ impl Story { } } + /// Bind a Rust function to an ink EXTERNAL function declaration. + /// + /// * `lookahead_safe` - 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. pub fn bind_external_function( &mut self, func_name: &str, diff --git a/lib/src/story_error.rs b/lib/src/story_error.rs index 6064a55..4b7fd49 100644 --- a/lib/src/story_error.rs +++ b/lib/src/story_error.rs @@ -1,3 +1,6 @@ +//! Error that represents an error when running a Story at runtime. +//! An error being returned of this type is typically when there's +//! a bug in your ink, rather than in the ink engine itself! use core::fmt; #[derive(Debug)] @@ -6,6 +9,7 @@ pub enum StoryError { BadJson(String), BadArgument(String), } + impl StoryError { pub(crate) fn get_message(&self) -> &str { match self { diff --git a/lib/src/story_state.rs b/lib/src/story_state.rs index 8e18a90..748c8ca 100644 --- a/lib/src/story_state.rs +++ b/lib/src/story_state.rs @@ -1,5 +1,3 @@ -#![allow(unused_variables, dead_code)] - use std::{cell::RefCell, collections::HashMap, rc::Rc}; use crate::{ @@ -309,7 +307,7 @@ impl StoryState { pub fn output_stream_ends_in_newline(&self) -> bool { if !self.get_output_stream().is_empty() { for e in self.get_output_stream().iter().rev() { - if let Some(cmd) = e.as_any().downcast_ref::() { + if e.as_any().is::() { break; } @@ -646,7 +644,7 @@ impl StoryState { if remove_whitespace_from >= 0 { i = remove_whitespace_from; while i < output_stream.len() as i32 { - if let Some(text) = Value::get_string_value(output_stream[i as usize].as_ref()) { + if Value::get_string_value(output_stream[i as usize].as_ref()).is_some() { output_stream.remove(i as usize); } else { i += 1; @@ -777,7 +775,7 @@ impl StoryState { // 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 let Some(named_flows) = &self.named_flows { + if self.named_flows.is_some() { let mut nf = self.named_flows.clone(); nf.as_mut().unwrap().insert( copy.current_flow.name.to_string(), @@ -1456,7 +1454,7 @@ impl StoryState { Ok(()) } - fn switch_to_default_flow_internal(&mut self) { + pub(crate) fn switch_to_default_flow_internal(&mut self) { if self.named_flows.is_some() { self.switch_flow_internal(DEFAULT_FLOW_NAME); } diff --git a/lib/src/value_type.rs b/lib/src/value_type.rs index 93a37fb..5dc377a 100644 --- a/lib/src/value_type.rs +++ b/lib/src/value_type.rs @@ -1,3 +1,4 @@ +//! A combination of an Ink value with its type. use crate::{ink_list::InkList, path::Path, story_error::StoryError}; #[repr(u8)] @@ -13,6 +14,7 @@ pub enum ValueType { } impl ValueType { + /// Creates a new ValueType with the String type. pub fn new_string(str: &str) -> ValueType { let mut inline_ws = true; @@ -30,6 +32,7 @@ impl ValueType { }) } + /// Gets the internal boolean value or None if the ValueType is not a ValueType::Bool pub fn get_bool(&self) -> Option { match self { ValueType::Bool(v) => Some(*v), @@ -37,6 +40,7 @@ impl ValueType { } } + /// Gets the internal i32 value or None if the ValueType is not a ValueType::Int pub fn get_int(&self) -> Option { match self { ValueType::Int(v) => Some(*v), @@ -44,6 +48,7 @@ impl ValueType { } } + /// Gets the internal f32 value or None if the ValueType is not a ValueType::Float pub fn get_float(&self) -> Option { match self { ValueType::Float(v) => Some(*v), @@ -51,6 +56,7 @@ impl ValueType { } } + /// Gets the internal string value or None if the ValueType is not a ValueType::String pub fn get_str(&self) -> Option<&str> { match self { ValueType::String(v) => Some(&v.string), @@ -58,6 +64,7 @@ impl ValueType { } } + /// Try to convert the internal value of this ValueType to i32 pub fn coerce_to_int(&self) -> Result { match self { ValueType::Bool(v) => { @@ -73,6 +80,7 @@ impl ValueType { } } + /// Try to convert the internal value of this ValueType to f32 pub fn coerce_to_float(&self) -> Result { match self { ValueType::Bool(v) => { @@ -90,6 +98,7 @@ impl ValueType { } } + /// Try to convert the internal value of this ValueType to bool pub fn coerce_to_bool(&self) -> Result { match self { ValueType::Bool(v) => Ok(*v), @@ -106,6 +115,7 @@ impl ValueType { } } + /// Try to convert the internal value of this ValueType to String pub fn coerce_to_string(&self) -> Result { match self { ValueType::Bool(v) => Ok(v.to_string()), @@ -121,9 +131,10 @@ impl ValueType { #[derive(Clone)] pub struct StringValue { + /// The internal string value. pub string: String, - pub is_inline_whitespace: bool, - pub is_newline: bool, + pub(crate) is_inline_whitespace: bool, + pub(crate) is_newline: bool, } impl StringValue { @@ -134,11 +145,11 @@ impl StringValue { #[derive(Clone, PartialEq)] pub struct VariablePointerValue { - pub variable_name: String, + pub(crate) variable_name: String, // 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) - pub context_index: i32, + pub(crate) context_index: i32, } From 8a6639d8ff5d10849b0af6fec8293a4cdad98019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Tue, 10 Oct 2023 16:47:34 +0000 Subject: [PATCH 74/91] Added basic features to clib. We can now run an Ink story using C! --- clib/Makefile | 29 ++++++------ clib/binkc.h.in | 5 ++ clib/src/cchoices.rs | 32 +++++++++++++ clib/src/cstory.rs | 43 ++++++++++++++++- clib/src/lib.rs | 1 + clib/tests/binkc_test.c | 101 ++++++++++++++++++++++++++++++++++------ 6 files changed, 181 insertions(+), 30 deletions(-) create mode 100644 clib/src/cchoices.rs diff --git a/clib/Makefile b/clib/Makefile index db6fb66..2ab5bf2 100644 --- a/clib/Makefile +++ b/clib/Makefile @@ -4,14 +4,11 @@ VERSION_MAJOR=$(shell echo $(VERSION) | cut -f1 -d.) VERSION_MINOR=$(shell echo $(VERSION) | cut -f2 -d.) VERSION_MICRO=$(shell echo $(VERSION) | cut -f3 -d.) CLIB_SO_DEV=libbinkc.so -CLIB_SO_MAN=$(CLIB_SO_DEV).$(VERSION_MAJOR) CLIB_SO_FULL=$(CLIB_SO_DEV).$(VERSION) -CLI_EXEC=binkplayer CLIB_HEADER=binkc.h CLIB_SO_DEV_RELEASE=../target/release/$(CLIB_SO_DEV) CLIB_SO_DEV_DEBUG=../target/debug/$(CLIB_SO_DEV) CLIB_PKG_CONFIG=binkc.pc -CLI_EXEC_RELEASE=../target/release/$(CLI_EXEC) PREFIX ?= /usr/local #outdir is used by COPR as well: https://docs.pagure.org/copr.copr/user_documentation.html @@ -26,23 +23,27 @@ endif INCLUDE_DIR ?= $(PREFIX)/include PKG_CONFIG_LIBDIR ?= $(LIBDIR)/pkgconfig -MAN_DIR ?= $(PREFIX)/share/man SKIP_VENDOR_CREATION ?=0 RELEASE ?=0 .PHONY: debug -debug: - cd .. && cargo build --all - ln -sfv $(CLIB_SO_DEV) ../target/debug/$(CLIB_SO_FULL) - ln -sfv $(CLIB_SO_DEV) ../target/debug/$(CLIB_SO_MAN) +debug: $(CLIB_HEADER) $(CLIB_SO_DEV_DEBUG) + mv $(CLIB_HEADER) ../target/debug/$(shell basename $(CLIB_HEADER)) + mv $(CLIB_SO_DEV_DEBUG) ../target/debug/$(CLIB_SO_FULL) + ln -sfv $(CLIB_SO_FULL) ../target/debug/$(CLIB_SO_DEV) -$(CLI_EXEC_RELEASE) $(CLIB_SO_DEV_RELEASE): +$(CLIB_SO_DEV_RELEASE): cd .. && cargo build --all --release -$(CLIB_SO_DEV_DEBUG): debug +$(CLIB_SO_DEV_DEBUG): + cd .. && cargo build --all clib: $(CLIB_HEADER) $(CLIB_SO_DEV_RELEASE) $(CLIB_PKG_CONFIG) + mv $(CLIB_HEADER) ../target/release/$(shell basename $(CLIB_HEADER)) + mv $(CLIB_PKG_CONFIG) ../target/release/$(shell basename $(CLIB_PKG_CONFIG)) + mv $(CLIB_SO_DEV_RELEASE) ../target/release/$(CLIB_SO_FULL) + ln -sfv $(CLIB_SO_FULL) ../target/release/$(CLIB_SO_DEV) .PHONY: $(CLIB_HEADER) $(CLIB_HEADER): $(CLIB_HEADER).in @@ -66,9 +67,8 @@ $(CLIB_PKG_CONFIG): $(CLIB_PKG_CONFIG).in clib_check: $(CLIB_SO_DEV_DEBUG) $(CLIB_HEADER) $(eval TMPDIR := $(shell mktemp -d)) cp $(CLIB_SO_DEV_DEBUG) $(TMPDIR)/$(CLIB_SO_FULL) - ln -sfv $(CLIB_SO_FULL) $(TMPDIR)/$(CLIB_SO_MAN) ln -sfv $(CLIB_SO_FULL) $(TMPDIR)/$(CLIB_SO_DEV) - cp $(CLIB_HEADER) $(TMPDIR)/$(shell basename $(CLIB_HEADER)) + mv $(CLIB_HEADER) $(TMPDIR)/$(shell basename $(CLIB_HEADER)) cc -g -Wall -Wextra -L$(TMPDIR) -I$(TMPDIR) \ -o $(TMPDIR)/binkc_test clib/tests/binkc_test.c -lbinkc LD_LIBRARY_PATH=$(TMPDIR) \ @@ -83,10 +83,7 @@ rust_check: check: rust_check clib_check -run_test: $(CLIB_SO_DEV_DEBUG) $(CLIB_HEADER) - mv $(CLIB_SO_DEV_DEBUG) ../target/debug/$(CLIB_SO_FULL) - ln -sfv $(CLIB_SO_FULL) ../target/debug/$(CLIB_SO_DEV) - cp $(CLIB_HEADER) ../target/debug/$(shell basename $(CLIB_HEADER)) +run_test: debug cc -g -Wall -Wextra -L../target/debug -I../target/debug \ -o ../target/debug/binkc_test ./tests/binkc_test.c -lbinkc LD_LIBRARY_PATH=../target/debug \ diff --git a/clib/binkc.h.in b/clib/binkc.h.in index acd2b92..54ed1e2 100644 --- a/clib/binkc.h.in +++ b/clib/binkc.h.in @@ -22,12 +22,17 @@ extern "C" { #define BINKC_FAIL_NULL_POINTER 2 struct binkc_story; +struct binkc_choices; int binkc_story_new(struct binkc_story **story, char *json_string, char **err_msg); void binkc_story_free(struct binkc_story *story); int binkc_story_can_continue(struct binkc_story *story, bool *can_continue); int binkc_story_cont(struct binkc_story *story, char **line, char **err_msg); +int binkc_story_get_current_choices(struct binkc_story *story, struct binkc_choices **choices, size_t *len); +int binkc_story_choose_choice_index(struct binkc_story *story, size_t choice_index); +void binkc_choices_free(struct binkc_choices *choices); +int binkc_choices_get_text( struct binkc_choices *choices, size_t idx, char **text); void binkc_cstring_free(char *cstring); diff --git a/clib/src/cchoices.rs b/clib/src/cchoices.rs new file mode 100644 index 0000000..40d2fc7 --- /dev/null +++ b/clib/src/cchoices.rs @@ -0,0 +1,32 @@ +use std::{ffi::CString, os::raw::c_char, rc::Rc}; + +use bink::choice::Choice; + +use crate::{BINKC_FAIL, BINKC_FAIL_NULL_POINTER, BINKC_OK}; + +#[allow(clippy::not_unsafe_ptr_arg_deref)] +#[no_mangle] +pub extern "C" fn binkc_choices_get_text( + choices: *const Vec>, + idx: usize, + text: *mut *mut c_char, +) -> u32 { + if choices.is_null() { + return BINKC_FAIL_NULL_POINTER; + } + + let choices: &Vec> = unsafe { &*choices }; + + let choice = choices.get(idx); + + match choice { + Some(choice) => unsafe { + *text = CString::new(choice.text.as_str()).unwrap().into_raw(); + }, + None => { + return BINKC_FAIL; + } + } + + BINKC_OK +} diff --git a/clib/src/cstory.rs b/clib/src/cstory.rs index 8b2755a..bb71d3c 100644 --- a/clib/src/cstory.rs +++ b/clib/src/cstory.rs @@ -1,9 +1,10 @@ use std::{ ffi::{CStr, CString}, os::raw::c_char, + rc::Rc, }; -use bink::story::Story; +use bink::{choice::Choice, story::Story}; use crate::{BINKC_FAIL, BINKC_FAIL_NULL_POINTER, BINKC_OK}; @@ -92,3 +93,43 @@ pub extern "C" fn binkc_story_cont( }, } } + +#[allow(clippy::not_unsafe_ptr_arg_deref)] +#[no_mangle] +pub extern "C" fn binkc_story_get_current_choices( + story: *mut Story, + choices: *mut *mut Vec>, + len: *mut usize, +) -> u32 { + if story.is_null() { + return BINKC_FAIL_NULL_POINTER; + } + + let story: &mut Story = unsafe { &mut *story }; + + let result = Box::new(story.get_current_choices()); + + unsafe { + *len = result.len(); + *choices = Box::into_raw(result); + } + + BINKC_OK +} + +#[allow(clippy::not_unsafe_ptr_arg_deref)] +#[no_mangle] +pub extern "C" fn binkc_story_choose_choice_index(story: *mut Story, choice_index: usize) -> u32 { + if story.is_null() { + return BINKC_FAIL_NULL_POINTER; + } + + let story: &mut Story = unsafe { &mut *story }; + + let result = story.choose_choice_index(choice_index); + + match result { + Ok(_) => BINKC_OK, + Err(_) => BINKC_FAIL, + } +} diff --git a/clib/src/lib.rs b/clib/src/lib.rs index 67eef34..5f91ab9 100644 --- a/clib/src/lib.rs +++ b/clib/src/lib.rs @@ -2,6 +2,7 @@ use std::{ffi::CString, os::raw::c_char}; +pub mod cchoices; pub mod cstory; const BINKC_OK: u32 = 0; diff --git a/clib/tests/binkc_test.c b/clib/tests/binkc_test.c index 43d210c..80c327d 100644 --- a/clib/tests/binkc_test.c +++ b/clib/tests/binkc_test.c @@ -14,37 +14,112 @@ void finish(int rc, struct binkc_story *story, char *err_msg) { void check_ret(int ret, struct binkc_story *story, char *err_msg) { if (ret != BINKC_OK) { - printf("Error: %s\n", err_msg); + if(err_msg != NULL) + perror(err_msg); + finish(EXIT_FAILURE, story, err_msg); } } +void print_choices(struct binkc_choices *choices, size_t len) { + for (size_t i=0; i < len; i++) { + char *text = NULL; + int ret = binkc_choices_get_text(choices, i, &text); + if (ret != BINKC_OK) { + finish(EXIT_FAILURE, NULL, NULL); + } + + printf("%lu. %s\n", i+1, text); + binkc_cstring_free(text); + } +} + +char* read_json_file(const char* filename) { + FILE* file = fopen(filename, "r"); + if (!file) { + perror("Failed to open file"); + return NULL; + } + + fseek(file, 0, SEEK_END); + long fileSize = ftell(file); + fseek(file, 0, SEEK_SET); + + char* jsonString = (char*)malloc(fileSize + 1); + if (!jsonString) { + perror("Memory allocation failed"); + fclose(file); + return NULL; + } + + size_t bytesRead = fread(jsonString, 1, fileSize, file); + if ((long)bytesRead != fileSize) { + perror("Failed to read file"); + free(jsonString); + fclose(file); + return NULL; + } + + jsonString[fileSize] = '\0'; + + fclose(file); + + return jsonString; +} + + int main(void) { uint32_t ret = BINKC_OK; struct binkc_story *story = NULL; + struct binkc_choices *choices = NULL; char *err_msg = NULL; char *line = NULL; - char *json_string = "{\"inkVersion\":21,\"root\":[[\"^Line.\",\"\\n\",[\"done\",{\"#n\":\"g-0\"}],null],\"done\",null],\"listDefs\":{}}"; + // char *json_string = "{\"inkVersion\":21,\"root\":[[\"^Line.\",\"\\n\",[\"done\",{\"#n\":\"g-0\"}],null],\"done\",null],\"listDefs\":{}}"; - ret = binkc_story_new(&story, json_string, &err_msg); + char *json_string = read_json_file("../inkfiles/TheIntercept.ink.json"); + if(json_string == NULL) + exit(EXIT_FAILURE); + ret = binkc_story_new(&story, json_string, &err_msg); check_ret(ret, story, err_msg); + free(json_string); - bool can_continue; - ret = binkc_story_can_continue(story, &can_continue); - check_ret(ret, story, err_msg); + bool end = false; - while (can_continue) { - ret = binkc_story_cont(story, &line, &err_msg); - check_ret(ret, story, err_msg); - puts(line); - binkc_cstring_free(line); - + while(!end) { + bool can_continue; ret = binkc_story_can_continue(story, &can_continue); check_ret(ret, story, err_msg); + + while (can_continue) { + ret = binkc_story_cont(story, &line, &err_msg); + check_ret(ret, story, err_msg); + puts(line); + binkc_cstring_free(line); + + ret = binkc_story_can_continue(story, &can_continue); + check_ret(ret, story, err_msg); + } + + // Obtain and print choices + size_t len = 0; + ret = binkc_story_get_current_choices(story, &choices, &len); + check_ret(ret, story, NULL); + //printf("Num. choices: %lu\n", len); + + if (len !=0) { + print_choices(choices, len); + printf("\n"); + // Always choose the first option + ret = binkc_story_choose_choice_index(story, 0); + check_ret(ret, story, NULL); + } else { + end = true; + } + } - printf("Ok.\n"); + printf("Story ended ok.\n"); finish(EXIT_SUCCESS, story, err_msg); } \ No newline at end of file From ccb9e519154d26d5d507cca12ad3379c706c4162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Tue, 10 Oct 2023 22:32:43 +0000 Subject: [PATCH 75/91] Added basic doc. --- README.md | 68 +++++++++++++++++++++++++++++++++++++++++- cli-player/src/main.rs | 1 + lib/Cargo.toml | 2 ++ lib/src/lib.rs | 36 +++++++++++++++++++++- 4 files changed, 105 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7dc1dde..cf35d70 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,72 @@ # blade-ink (bink) This is a Rust port of inkle's [ink](https://github.com/inkle/ink), a scripting language for writing interactive narrative. -`bink` is fully compatible with the original version. +`bink` is fully compatible with the reference version and supports all the language features. +To know more about the Ink language, you can check [the oficial documentation](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md). +This repository/workspace contains 3 crates: + +- `lib` is the `bink` lib crate. It will be published to crates.io and it would be easyly used adding it to your project as a dependency. +- `cli-player` contains an implementation of a cli player (called `binkplayer`) to run .json.ink story files directly from the console. +- `clib` is a C binding of the `bink` library ready to be used in C or any other program that can uses C libraries. + +## Using the bink library crate + +Here it is a quick example that uses the basic features to play an Ink story using the `bink` crate. + +```rust +// story is the entry point of the `bink` lib. +// json_string is a string with all the contents of the .ink.json file. +let mut story = Story::new(json_string)?; + +let mut end = false; + +while !end { + while story.can_continue() { + let line = story.cont()?; + + println!("{}", line); + } + + let choices = story.get_current_choices(); + if !choices.is_empty() { + // read_input is a method that you should implement + // to get the choice selected by the user. + let choice_idx = read_input(&choices)?; + // set the option selected by the user + story.choose_choice_index(choice_idx)?; + } else { + end = true; + } +} +``` + +The `bink` library support all the **Ink** language features, including threads, multi-flows, variable set/get from code, variable observing, external functions, tags on choices, etc. Examples of uses of all these features can be found in the `lib/tests` folder. + + +## Running Ink stories with *binkplayer* + +When you run `cargo build` in the workspace root folder, the `binkplayer` binary will be compiled and found in `target/debug`. You can play any of the `.ink.json` file using it. + +In the `inkfiles` folder we can found many Ink test stories to test the Ink language capabilities. And also we have **The Intercept**, a full featured story created by **Inkle** also included in the `inkfiles` folder. You can run **The Intercept** running the next command in your console. + +```bash +$ target/debug/binkplayer inkfiles/TheIntercept.ink.json +``` + +## Using the C bindings + +You can build the C bindings using the Makefile inside the clib folder. + +To create the library in the target/release folder use + +```bash + $ make clib +``` + +This will create the `libbinkc.so.x.x.x`, where x.x.x is the version of the library, and the `binkc.h` ready to include in your C projects. + +The C bindings is a work in progress. In the current state, only the basic functionality to play an Ink story is finish. + +We can find an example of use in C in the `clib/tests/binkc_test.c` file. It plays **The Intercept** story included in the `inkfiles` folder, choosing always the first option presented to the user. \ No newline at end of file diff --git a/cli-player/src/main.rs b/cli-player/src/main.rs index 775a5ac..0f72ed8 100644 --- a/cli-player/src/main.rs +++ b/cli-player/src/main.rs @@ -1,3 +1,4 @@ +//! Console player that can runs compiled `.ink.json` story files writen in the **Ink** language. use std::cell::RefCell; use std::io::Write; diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 619dce7..1f6c410 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -7,6 +7,8 @@ This is a Rust port of inkle's ink, a scripting language for writing interactive """ license = "Apache-2.0" repository = "https://github.com/bladecoder/blade-ink-rs/" +keywords = ["ink", "gamedev", "narrative"] +categories = ["gamedev"] edition = "2021" [lib] diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 5205f28..2023442 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,5 +1,39 @@ //! This is a Rust port of inkle's [Ink](https://github.com/inkle/ink), a scripting language for writing interactive narrative. -//! `bink` is fully compatible with the original version. +//! `bink` is fully compatible with the reference version and supports all the language features. +//! +//! To know more about the Ink language, you can check [the oficial documentation](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md). +//! +//! Here it is a quick example that uses the basic features to play an Ink story using the `bink` crate. +//! +//! ```no_run +//! // story is the entry point of the `bink` lib. +//! // json_string is a string with all the contents of the .ink.json file. +//! let mut story = Story::new(json_string)?; +//! +//! let mut end = false; +//! +//! while !end { +//! while story.can_continue() { +//! let line = story.cont()?; +//! +//! println!("{}", line); +//! } +//! +//! let choices = story.get_current_choices(); +//! if !choices.is_empty() { +//! // read_input is a method that you should implement +//! // to get the choice selected by the user. +//! let choice_idx = read_input(&choices)?; +//! // set the option selected by the user +//! story.choose_choice_index(choice_idx)?; +//! } else { +//! end = true; +//! } +//! } +//! ``` +//! +//! The `bink` library support all the **Ink** language features, including threads, multi-flows, variable set/get from code, variable observing, external functions, +//! tags on choices, etc. Examples of uses of all these features will be added to this documentation in the future, but meanwhile, all the examples can be found in the `lib/tests` folder in the source code of this crate. mod callstack; pub mod choice; From 41bbe1ef2510f8e392d9a5cd8b2780c701966cef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Tue, 10 Oct 2023 23:02:24 +0000 Subject: [PATCH 76/91] Rename bink crate by bladeink because bink already exists in cargo.io --- README.md | 16 ++++++++-------- cli-player/Cargo.toml | 2 +- cli-player/src/main.rs | 4 ++-- clib/Cargo.toml | 4 ++-- clib/src/cchoices.rs | 2 +- clib/src/cstory.rs | 2 +- clib/src/lib.rs | 2 +- lib/Cargo.toml | 4 ++-- lib/src/lib.rs | 10 +++++----- lib/tests/basic_text_test.rs | 2 +- lib/tests/choice_test.rs | 2 +- lib/tests/common/mod.rs | 2 +- lib/tests/conditional_test.rs | 2 +- lib/tests/divert_test.rs | 2 +- lib/tests/function_test.rs | 2 +- lib/tests/gather_test.rs | 2 +- lib/tests/glue_test.rs | 2 +- lib/tests/knot_test.rs | 2 +- lib/tests/list_test.rs | 2 +- lib/tests/misc_test.rs | 2 +- lib/tests/multi_flow_test.rs | 2 +- lib/tests/runtime_test.rs | 2 +- lib/tests/stitch_test.rs | 2 +- lib/tests/tag_test.rs | 2 +- lib/tests/thread_test.rs | 2 +- lib/tests/tunnel_test.rs | 2 +- lib/tests/variable_test.rs | 2 +- lib/tests/variable_text_test.rs | 2 +- 28 files changed, 42 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index cf35d70..fbf3f8c 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,22 @@ -# blade-ink (bink) +# Blade Ink This is a Rust port of inkle's [ink](https://github.com/inkle/ink), a scripting language for writing interactive narrative. -`bink` is fully compatible with the reference version and supports all the language features. +`bladeink` is fully compatible with the reference version and supports all the language features. To know more about the Ink language, you can check [the oficial documentation](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md). This repository/workspace contains 3 crates: -- `lib` is the `bink` lib crate. It will be published to crates.io and it would be easyly used adding it to your project as a dependency. +- `lib` is the `bladeink` lib crate. It will be published to crates.io and it would be easily used adding it to your project as a dependency. - `cli-player` contains an implementation of a cli player (called `binkplayer`) to run .json.ink story files directly from the console. -- `clib` is a C binding of the `bink` library ready to be used in C or any other program that can uses C libraries. +- `clib` is a C binding of the `bladeink` library ready to be used in C or any other program that can uses C libraries. -## Using the bink library crate +## Using the bladeink library crate -Here it is a quick example that uses the basic features to play an Ink story using the `bink` crate. +Here it is a quick example that uses the basic features to play an Ink story using the `bladeink` crate. ```rust -// story is the entry point of the `bink` lib. +// story is the entry point of the `bladeink` lib. // json_string is a string with all the contents of the .ink.json file. let mut story = Story::new(json_string)?; @@ -42,7 +42,7 @@ while !end { } ``` -The `bink` library support all the **Ink** language features, including threads, multi-flows, variable set/get from code, variable observing, external functions, tags on choices, etc. Examples of uses of all these features can be found in the `lib/tests` folder. +The `bladeink` library support all the **Ink** language features, including threads, multi-flows, variable set/get from code, variable observing, external functions, tags on choices, etc. Examples of uses of all these features can be found in the `lib/tests` folder. ## Running Ink stories with *binkplayer* diff --git a/cli-player/Cargo.toml b/cli-player/Cargo.toml index a07c49a..b992a16 100644 --- a/cli-player/Cargo.toml +++ b/cli-player/Cargo.toml @@ -14,7 +14,7 @@ path = "src/main.rs" [dependencies] anyhow = "1.0.75" -bink = { "version" = "0.9.0", path = "../lib" } +bladeink = { "version" = "0.9.0", path = "../lib" } clap = { "version" = "4.4.6", features = ["derive"] } [dev-dependencies] diff --git a/cli-player/src/main.rs b/cli-player/src/main.rs index 0f72ed8..7cd365d 100644 --- a/cli-player/src/main.rs +++ b/cli-player/src/main.rs @@ -5,8 +5,8 @@ use std::io::Write; use std::{error::Error, fs, io, path::Path, rc::Rc}; use anyhow::Context; -use bink::story_callbacks::{ErrorHandler, ErrorType}; -use bink::{choice::Choice, story::Story}; +use bladeink::story_callbacks::{ErrorHandler, ErrorType}; +use bladeink::{choice::Choice, story::Story}; use clap::Parser; #[derive(Parser)] diff --git a/clib/Cargo.toml b/clib/Cargo.toml index 250aae8..87c59d6 100644 --- a/clib/Cargo.toml +++ b/clib/Cargo.toml @@ -2,7 +2,7 @@ name = "binkc" version = "0.1.0" description = """ -C bindings for the `bink` library. +C bindings for the `bladeink` library. """ license = "Apache-2.0" authors = ["Rafael Garcia "] @@ -14,4 +14,4 @@ path = "src/lib.rs" crate-type = ["cdylib"] [dependencies] -bink = { "version" = "0.9.0", path = "../lib" } +bladeink = { "version" = "0.9.0", path = "../lib" } diff --git a/clib/src/cchoices.rs b/clib/src/cchoices.rs index 40d2fc7..6c66337 100644 --- a/clib/src/cchoices.rs +++ b/clib/src/cchoices.rs @@ -1,6 +1,6 @@ use std::{ffi::CString, os::raw::c_char, rc::Rc}; -use bink::choice::Choice; +use bladeink::choice::Choice; use crate::{BINKC_FAIL, BINKC_FAIL_NULL_POINTER, BINKC_OK}; diff --git a/clib/src/cstory.rs b/clib/src/cstory.rs index bb71d3c..14bf477 100644 --- a/clib/src/cstory.rs +++ b/clib/src/cstory.rs @@ -4,7 +4,7 @@ use std::{ rc::Rc, }; -use bink::{choice::Choice, story::Story}; +use bladeink::{choice::Choice, story::Story}; use crate::{BINKC_FAIL, BINKC_FAIL_NULL_POINTER, BINKC_OK}; diff --git a/clib/src/lib.rs b/clib/src/lib.rs index 5f91ab9..87417ae 100644 --- a/clib/src/lib.rs +++ b/clib/src/lib.rs @@ -1,4 +1,4 @@ -//! C API for bink. +//! C API for bladeink. use std::{ffi::CString, os::raw::c_char}; diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 1f6c410..7fe4f14 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "bink" +name = "bladeink" version = "0.9.0" authors = ["Rafael Garcia "] description = """ @@ -12,7 +12,7 @@ categories = ["gamedev"] edition = "2021" [lib] -name = "bink" +name = "bladeink" path = "src/lib.rs" [dependencies] diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 2023442..91c37fb 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,12 +1,12 @@ //! This is a Rust port of inkle's [Ink](https://github.com/inkle/ink), a scripting language for writing interactive narrative. -//! `bink` is fully compatible with the reference version and supports all the language features. +//! `bladeink` is fully compatible with the reference version and supports all the language features. //! //! To know more about the Ink language, you can check [the oficial documentation](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md). //! -//! Here it is a quick example that uses the basic features to play an Ink story using the `bink` crate. +//! Here it is a quick example that uses the basic features to play an Ink story using the `bladeink` crate. //! -//! ```no_run -//! // story is the entry point of the `bink` lib. +//! ```ignore +//! // story is the entry point of the `bladeink` lib. //! // json_string is a string with all the contents of the .ink.json file. //! let mut story = Story::new(json_string)?; //! @@ -32,7 +32,7 @@ //! } //! ``` //! -//! The `bink` library support all the **Ink** language features, including threads, multi-flows, variable set/get from code, variable observing, external functions, +//! The `bladeink` library support all the **Ink** language features, including threads, multi-flows, variable set/get from code, variable observing, external functions, //! tags on choices, etc. Examples of uses of all these features will be added to this documentation in the future, but meanwhile, all the examples can be found in the `lib/tests` folder in the source code of this crate. mod callstack; diff --git a/lib/tests/basic_text_test.rs b/lib/tests/basic_text_test.rs index 9f658b9..09af29c 100644 --- a/lib/tests/basic_text_test.rs +++ b/lib/tests/basic_text_test.rs @@ -1,4 +1,4 @@ -use bink::{story::Story, story_error::StoryError}; +use bladeink::{story::Story, story_error::StoryError}; use std::env; mod common; diff --git a/lib/tests/choice_test.rs b/lib/tests/choice_test.rs index 0da2693..e8fff92 100644 --- a/lib/tests/choice_test.rs +++ b/lib/tests/choice_test.rs @@ -1,4 +1,4 @@ -use bink::{story::Story, story_error::StoryError}; +use bladeink::{story::Story, story_error::StoryError}; mod common; diff --git a/lib/tests/common/mod.rs b/lib/tests/common/mod.rs index 983b5ef..b57aba1 100644 --- a/lib/tests/common/mod.rs +++ b/lib/tests/common/mod.rs @@ -2,7 +2,7 @@ use std::{error::Error, fs, path::Path}; -use bink::{story::Story, story_error::StoryError}; +use bladeink::{story::Story, story_error::StoryError}; use rand::Rng; pub fn next_all(story: &mut Story, text: &mut Vec) -> Result<(), StoryError> { diff --git a/lib/tests/conditional_test.rs b/lib/tests/conditional_test.rs index 9855e33..7ddcb9e 100644 --- a/lib/tests/conditional_test.rs +++ b/lib/tests/conditional_test.rs @@ -1,4 +1,4 @@ -use bink::{story::Story, story_error::StoryError}; +use bladeink::{story::Story, story_error::StoryError}; mod common; diff --git a/lib/tests/divert_test.rs b/lib/tests/divert_test.rs index 9f74fdd..b61c698 100644 --- a/lib/tests/divert_test.rs +++ b/lib/tests/divert_test.rs @@ -1,4 +1,4 @@ -use bink::{story::Story, story_error::StoryError}; +use bladeink::{story::Story, story_error::StoryError}; mod common; diff --git a/lib/tests/function_test.rs b/lib/tests/function_test.rs index b312fb2..1394f1d 100644 --- a/lib/tests/function_test.rs +++ b/lib/tests/function_test.rs @@ -1,4 +1,4 @@ -use bink::{story::Story, story_error::StoryError}; +use bladeink::{story::Story, story_error::StoryError}; mod common; diff --git a/lib/tests/gather_test.rs b/lib/tests/gather_test.rs index aaa02a2..aa58c2f 100644 --- a/lib/tests/gather_test.rs +++ b/lib/tests/gather_test.rs @@ -1,4 +1,4 @@ -use bink::{story::Story, story_error::StoryError}; +use bladeink::{story::Story, story_error::StoryError}; mod common; diff --git a/lib/tests/glue_test.rs b/lib/tests/glue_test.rs index 53d2cf9..3162adc 100644 --- a/lib/tests/glue_test.rs +++ b/lib/tests/glue_test.rs @@ -1,4 +1,4 @@ -use bink::{story::Story, story_error::StoryError}; +use bladeink::{story::Story, story_error::StoryError}; mod common; diff --git a/lib/tests/knot_test.rs b/lib/tests/knot_test.rs index 82f0aaa..c15d72d 100644 --- a/lib/tests/knot_test.rs +++ b/lib/tests/knot_test.rs @@ -1,4 +1,4 @@ -use bink::{story::Story, story_error::StoryError}; +use bladeink::{story::Story, story_error::StoryError}; mod common; diff --git a/lib/tests/list_test.rs b/lib/tests/list_test.rs index c6934a0..aca24f6 100644 --- a/lib/tests/list_test.rs +++ b/lib/tests/list_test.rs @@ -1,6 +1,6 @@ use std::error::Error; -use bink::story::Story; +use bladeink::story::Story; mod common; diff --git a/lib/tests/misc_test.rs b/lib/tests/misc_test.rs index 4922f09..f19eb85 100644 --- a/lib/tests/misc_test.rs +++ b/lib/tests/misc_test.rs @@ -1,4 +1,4 @@ -use bink::{story::Story, story_error::StoryError, value_type::ValueType}; +use bladeink::{story::Story, story_error::StoryError, value_type::ValueType}; mod common; diff --git a/lib/tests/multi_flow_test.rs b/lib/tests/multi_flow_test.rs index 5921f76..21ca6da 100644 --- a/lib/tests/multi_flow_test.rs +++ b/lib/tests/multi_flow_test.rs @@ -1,4 +1,4 @@ -use bink::{story::Story, story_error::StoryError}; +use bladeink::{story::Story, story_error::StoryError}; mod common; diff --git a/lib/tests/runtime_test.rs b/lib/tests/runtime_test.rs index a4ba90d..efb6f88 100644 --- a/lib/tests/runtime_test.rs +++ b/lib/tests/runtime_test.rs @@ -1,7 +1,7 @@ use core::panic; use std::{cell::RefCell, error::Error, rc::Rc}; -use bink::{ +use bladeink::{ story::Story, story_callbacks::{ExternalFunction, VariableObserver}, value_type::ValueType, diff --git a/lib/tests/stitch_test.rs b/lib/tests/stitch_test.rs index 529c6e9..39e3604 100644 --- a/lib/tests/stitch_test.rs +++ b/lib/tests/stitch_test.rs @@ -1,4 +1,4 @@ -use bink::{story::Story, story_error::StoryError}; +use bladeink::{story::Story, story_error::StoryError}; mod common; diff --git a/lib/tests/tag_test.rs b/lib/tests/tag_test.rs index 9b50ca6..f93cf2e 100644 --- a/lib/tests/tag_test.rs +++ b/lib/tests/tag_test.rs @@ -1,4 +1,4 @@ -use bink::{story::Story, story_error::StoryError}; +use bladeink::{story::Story, story_error::StoryError}; mod common; diff --git a/lib/tests/thread_test.rs b/lib/tests/thread_test.rs index df6d1c2..4cf1721 100644 --- a/lib/tests/thread_test.rs +++ b/lib/tests/thread_test.rs @@ -1,4 +1,4 @@ -use bink::{story::Story, story_error::StoryError}; +use bladeink::{story::Story, story_error::StoryError}; mod common; diff --git a/lib/tests/tunnel_test.rs b/lib/tests/tunnel_test.rs index c92a25b..8d8bce8 100644 --- a/lib/tests/tunnel_test.rs +++ b/lib/tests/tunnel_test.rs @@ -1,4 +1,4 @@ -use bink::{story::Story, story_error::StoryError}; +use bladeink::{story::Story, story_error::StoryError}; mod common; diff --git a/lib/tests/variable_test.rs b/lib/tests/variable_test.rs index 826ade5..787de1f 100644 --- a/lib/tests/variable_test.rs +++ b/lib/tests/variable_test.rs @@ -1,4 +1,4 @@ -use bink::{story::Story, story_error::StoryError}; +use bladeink::{story::Story, story_error::StoryError}; mod common; diff --git a/lib/tests/variable_text_test.rs b/lib/tests/variable_text_test.rs index fe746b9..6682695 100644 --- a/lib/tests/variable_text_test.rs +++ b/lib/tests/variable_text_test.rs @@ -1,4 +1,4 @@ -use bink::{story::Story, story_error::StoryError}; +use bladeink::{story::Story, story_error::StoryError}; mod common; From e818af191ad2c761a62ba714ddd41271570f51f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Tue, 10 Oct 2023 23:06:23 +0000 Subject: [PATCH 77/91] Update crate category. --- README.md | 2 +- lib/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fbf3f8c..59b151c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ To know more about the Ink language, you can check [the oficial documentation](h This repository/workspace contains 3 crates: -- `lib` is the `bladeink` lib crate. It will be published to crates.io and it would be easily used adding it to your project as a dependency. +- `lib` is the `bladeink` lib crate. It is available in crates.io and it can be used in your project as a dependency. - `cli-player` contains an implementation of a cli player (called `binkplayer`) to run .json.ink story files directly from the console. - `clib` is a C binding of the `bladeink` library ready to be used in C or any other program that can uses C libraries. diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 7fe4f14..a4a2853 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -8,7 +8,7 @@ This is a Rust port of inkle's ink, a scripting language for writing interactive license = "Apache-2.0" repository = "https://github.com/bladecoder/blade-ink-rs/" keywords = ["ink", "gamedev", "narrative"] -categories = ["gamedev"] +categories = ["game-development"] edition = "2021" [lib] From cbc8d058089d8439ba1d9cd5a13c9478ae25b0a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Tue, 10 Oct 2023 23:11:56 +0000 Subject: [PATCH 78/91] Added README to Cargo.toml files. --- README.md | 2 +- cli-player/Cargo.toml | 1 + clib/Cargo.toml | 1 + lib/Cargo.toml | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 59b151c..317f2ce 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ This is a Rust port of inkle's [ink](https://github.com/inkle/ink), a scripting To know more about the Ink language, you can check [the oficial documentation](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md). -This repository/workspace contains 3 crates: +The **Blade Ink** project contains 3 crates: - `lib` is the `bladeink` lib crate. It is available in crates.io and it can be used in your project as a dependency. - `cli-player` contains an implementation of a cli player (called `binkplayer`) to run .json.ink story files directly from the console. diff --git a/cli-player/Cargo.toml b/cli-player/Cargo.toml index b992a16..25a8785 100644 --- a/cli-player/Cargo.toml +++ b/cli-player/Cargo.toml @@ -6,6 +6,7 @@ Console player for compiled .json Ink story files. """ authors = ["Rafael Garcia "] license = "Apache-2.0" +readme = "../README.md" edition = "2021" [[bin]] diff --git a/clib/Cargo.toml b/clib/Cargo.toml index 87c59d6..23fae6f 100644 --- a/clib/Cargo.toml +++ b/clib/Cargo.toml @@ -6,6 +6,7 @@ C bindings for the `bladeink` library. """ license = "Apache-2.0" authors = ["Rafael Garcia "] +readme = "../README.md" edition = "2021" [lib] diff --git a/lib/Cargo.toml b/lib/Cargo.toml index a4a2853..de661b8 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -9,6 +9,7 @@ license = "Apache-2.0" repository = "https://github.com/bladecoder/blade-ink-rs/" keywords = ["ink", "gamedev", "narrative"] categories = ["game-development"] +readme = "../README.md" edition = "2021" [lib] From 7d7db9c46aaa7a12c9c631159b97e9155073b41d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Tue, 10 Oct 2023 23:14:29 +0000 Subject: [PATCH 79/91] Update version to publish the crate again with the README.md --- lib/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index de661b8..6a15bda 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bladeink" -version = "0.9.0" +version = "0.9.1" authors = ["Rafael Garcia "] description = """ This is a Rust port of inkle's ink, a scripting language for writing interactive narrative. From 89114dbd3c6278ba8e8453e060068770ec7a8531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Tue, 10 Oct 2023 23:22:03 +0000 Subject: [PATCH 80/91] Some documentation fixes. --- README.md | 6 +++--- lib/Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 317f2ce..05dca81 100644 --- a/README.md +++ b/README.md @@ -42,12 +42,12 @@ while !end { } ``` -The `bladeink` library support all the **Ink** language features, including threads, multi-flows, variable set/get from code, variable observing, external functions, tags on choices, etc. Examples of uses of all these features can be found in the `lib/tests` folder. +The `bladeink` library support all the **Ink** language features, including threads, multi-flows, variable set/get from code, variable observing, external functions, tags on choices, etc. Examples of uses of all these features can be found in the `lib/tests` folder in the source code. ## Running Ink stories with *binkplayer* -When you run `cargo build` in the workspace root folder, the `binkplayer` binary will be compiled and found in `target/debug`. You can play any of the `.ink.json` file using it. +If you download the source code repository, you can run `cargo build` in the workspace root folder, the `binkplayer` binary will be compiled and found in `target/debug`. You can play any of the `.ink.json` file using it. In the `inkfiles` folder we can found many Ink test stories to test the Ink language capabilities. And also we have **The Intercept**, a full featured story created by **Inkle** also included in the `inkfiles` folder. You can run **The Intercept** running the next command in your console. @@ -57,7 +57,7 @@ $ target/debug/binkplayer inkfiles/TheIntercept.ink.json ## Using the C bindings -You can build the C bindings using the Makefile inside the clib folder. +If you download the source code repository, you can build the C bindings using the Makefile inside the clib folder. To create the library in the target/release folder use diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 6a15bda..fc7bdf7 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bladeink" -version = "0.9.1" +version = "0.9.2" authors = ["Rafael Garcia "] description = """ This is a Rust port of inkle's ink, a scripting language for writing interactive narrative. From 426596af1b61ecbe949b8b9198a368c80967ea8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Wed, 11 Oct 2023 18:40:59 +0000 Subject: [PATCH 81/91] Added method to retrieve current tags to the C bindings. --- .github/workflows/release.yml | 177 ++++++++++++++++++++++++++++++++++ README.md | 2 +- cli-player/Cargo.toml | 2 +- clib/Cargo.toml | 2 +- clib/Makefile | 8 +- clib/VERSION | 2 +- clib/binkc.h.in | 5 + clib/src/cchoices.rs | 10 ++ clib/src/cstory.rs | 26 +++++ clib/src/ctags.rs | 42 ++++++++ clib/src/lib.rs | 1 + clib/tests/binkc_tags_test.c | 112 +++++++++++++++++++++ clib/tests/binkc_test.c | 2 + 13 files changed, 385 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 clib/src/ctags.rs create mode 100644 clib/tests/binkc_tags_test.c diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b5700d3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,177 @@ +# The way this works is the following: +# +# The create-release job runs purely to initialize the GitHub release itself +# and to output upload_url for the following job. +# +# The build-release job runs only once create-release is finished. It gets the +# release upload URL from create-release job outputs, then builds the release +# executables for each supported platform and attaches them as release assets +# to the previously created release. +# +# The key here is that we create the release only once. +# +# Reference: +# https://eugene-babichenko.github.io/blog/2020/05/09/github-actions-cross-platform-auto-releases/ + +name: release +on: + push: + # Enable when testing release infrastructure on a branch. + # branches: + # - ag/work + tags: + - "[0-9]+.[0-9]+.[0-9]+" +jobs: + create-release: + name: create-release + runs-on: ubuntu-latest + # env: + # Set to force version number, e.g., when no tag exists. + # BINKC_VERSION: TEST-0.0.0 + outputs: + upload_url: ${{ steps.release.outputs.upload_url }} + binkc_version: ${{ env.BINKC_VERSION }} + steps: + - name: Get the release version from the tag + shell: bash + if: env.BINKC_VERSION == '' + run: | + # Apparently, this is the right way to get a tag name. Really? + # + # See: https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027 + echo "BINKC_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + echo "version is: ${{ env.BINKC_VERSION }}" + - name: Create GitHub release + id: release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ env.BINKC_VERSION }} + release_name: ${{ env.BINKC_VERSION }} + + build-release: + name: build-release + needs: ['create-release'] + runs-on: ${{ matrix.os }} + env: + # For some builds, we use cross to test on 32-bit and big-endian + # systems. + CARGO: cargo + # When CARGO is set to CROSS, this is set to `--target matrix.target`. + TARGET_FLAGS: "" + # When CARGO is set to CROSS, TARGET_DIR includes matrix.target. + TARGET_DIR: ./target + # Emit backtraces on panics. + RUST_BACKTRACE: 1 + # Build static releases with PCRE2. + PCRE2_SYS_STATIC: 1 + strategy: + matrix: + build: [linux, linux-arm, macos, win-msvc, win-gnu, win32-msvc] + include: + - build: linux + os: ubuntu-18.04 + rust: nightly + target: x86_64-unknown-linux-musl + - build: linux-arm + os: ubuntu-18.04 + rust: nightly + target: arm-unknown-linux-gnueabihf + - build: macos + os: macos-latest + rust: nightly + target: x86_64-apple-darwin + - build: win-msvc + os: windows-2019 + rust: nightly + target: x86_64-pc-windows-msvc + - build: win-gnu + os: windows-2019 + rust: nightly-x86_64-gnu + target: x86_64-pc-windows-gnu + - build: win32-msvc + os: windows-2019 + rust: nightly + target: i686-pc-windows-msvc + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + fetch-depth: 1 + + - name: Install packages (Ubuntu) + if: matrix.os == 'ubuntu-18.04' + run: | + ci/ubuntu-install-packages + + - name: Install packages (macOS) + if: matrix.os == 'macos-latest' + run: | + ci/macos-install-packages + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + profile: minimal + override: true + target: ${{ matrix.target }} + + - name: Use Cross + shell: bash + run: | + cargo install cross + echo "CARGO=cross" >> $GITHUB_ENV + echo "TARGET_FLAGS=--target ${{ matrix.target }}" >> $GITHUB_ENV + echo "TARGET_DIR=./target/${{ matrix.target }}" >> $GITHUB_ENV + + - name: Show command used for Cargo + run: | + echo "cargo command is: ${{ env.CARGO }}" + echo "target flag is: ${{ env.TARGET_FLAGS }}" + echo "target dir is: ${{ env.TARGET_DIR }}" + + - name: Build release binary + run: ${{ env.CARGO }} build --verbose --release {{ env.TARGET_FLAGS }} + + # - name: Strip release binary (linux and macos) + # if: matrix.build == 'linux' || matrix.build == 'macos' + # run: strip "target/${{ matrix.target }}/release/libbinkc.so" + + # - name: Strip release binary (arm) + # if: matrix.build == 'linux-arm' + # run: | + # docker run --rm -v \ + # "$PWD/target:/target:Z" \ + # rustembedded/cross:arm-unknown-linux-gnueabihf \ + # arm-linux-gnueabihf-strip \ + # /target/arm-unknown-linux-gnueabihf/release/libbinkc.so + + - name: Build archive + shell: bash + run: | + outdir="$(ci/cargo-out-dir "${{ env.TARGET_DIR }}")" + staging="libbinkc-${{ needs.create-release.outputs.binkc_version }}-${{ matrix.target }}" + mkdir "$staging" + + if [ "${{ matrix.os }}" = "windows-2019" ]; then + cp "target/${{ matrix.target }}/release/libbinkc.dll" "$staging/" + 7z a "$staging.zip" "$staging" + echo "ASSET=$staging.zip" >> $GITHUB_ENV + else + cp "target/${{ matrix.target }}/release/libbinkc.so" "$staging/" + tar czf "$staging.tar.gz" "$staging" + echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV + fi + + - name: Upload release archive + uses: actions/upload-release-asset@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create-release.outputs.upload_url }} + asset_path: ${{ env.ASSET }} + asset_name: ${{ env.ASSET }} + asset_content_type: application/octet-stream \ No newline at end of file diff --git a/README.md b/README.md index 05dca81..80d821b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Blade Ink -This is a Rust port of inkle's [ink](https://github.com/inkle/ink), a scripting language for writing interactive narrative. +This is a Rust port of Inkle's [Ink](https://github.com/inkle/ink), a scripting language for writing interactive narrative. `bladeink` is fully compatible with the reference version and supports all the language features. diff --git a/cli-player/Cargo.toml b/cli-player/Cargo.toml index 25a8785..3d24368 100644 --- a/cli-player/Cargo.toml +++ b/cli-player/Cargo.toml @@ -15,7 +15,7 @@ path = "src/main.rs" [dependencies] anyhow = "1.0.75" -bladeink = { "version" = "0.9.0", path = "../lib" } +bladeink = { path = "../lib" } clap = { "version" = "4.4.6", features = ["derive"] } [dev-dependencies] diff --git a/clib/Cargo.toml b/clib/Cargo.toml index 23fae6f..5bac093 100644 --- a/clib/Cargo.toml +++ b/clib/Cargo.toml @@ -15,4 +15,4 @@ path = "src/lib.rs" crate-type = ["cdylib"] [dependencies] -bladeink = { "version" = "0.9.0", path = "../lib" } +bladeink = { path = "../lib" } diff --git a/clib/Makefile b/clib/Makefile index 2ab5bf2..8d4f39f 100644 --- a/clib/Makefile +++ b/clib/Makefile @@ -28,7 +28,7 @@ SKIP_VENDOR_CREATION ?=0 RELEASE ?=0 .PHONY: debug -debug: $(CLIB_HEADER) $(CLIB_SO_DEV_DEBUG) +debug: $(CLIB_SO_DEV_DEBUG) $(CLIB_HEADER) mv $(CLIB_HEADER) ../target/debug/$(shell basename $(CLIB_HEADER)) mv $(CLIB_SO_DEV_DEBUG) ../target/debug/$(CLIB_SO_FULL) ln -sfv $(CLIB_SO_FULL) ../target/debug/$(CLIB_SO_DEV) @@ -83,11 +83,15 @@ rust_check: check: rust_check clib_check -run_test: debug +test: clean debug cc -g -Wall -Wextra -L../target/debug -I../target/debug \ -o ../target/debug/binkc_test ./tests/binkc_test.c -lbinkc LD_LIBRARY_PATH=../target/debug \ ../target/debug/binkc_test + cc -g -Wall -Wextra -L../target/debug -I../target/debug \ + -o ../target/debug/binkc_tags_test ./tests/binkc_tags_test.c -lbinkc + LD_LIBRARY_PATH=../target/debug \ + ../target/debug/binkc_tags_test clean: - cd .. && cargo clean diff --git a/clib/VERSION b/clib/VERSION index 6e8bf73..0ea3a94 100644 --- a/clib/VERSION +++ b/clib/VERSION @@ -1 +1 @@ -0.1.0 +0.2.0 diff --git a/clib/binkc.h.in b/clib/binkc.h.in index 54ed1e2..62b6b21 100644 --- a/clib/binkc.h.in +++ b/clib/binkc.h.in @@ -23,6 +23,7 @@ extern "C" { struct binkc_story; struct binkc_choices; +struct binkc_tags; int binkc_story_new(struct binkc_story **story, char *json_string, char **err_msg); void binkc_story_free(struct binkc_story *story); @@ -30,10 +31,14 @@ int binkc_story_can_continue(struct binkc_story *story, bool *can_continue); int binkc_story_cont(struct binkc_story *story, char **line, char **err_msg); int binkc_story_get_current_choices(struct binkc_story *story, struct binkc_choices **choices, size_t *len); int binkc_story_choose_choice_index(struct binkc_story *story, size_t choice_index); +int binkc_story_get_current_tags(struct binkc_story *story, struct binkc_tags **tags, size_t *len); void binkc_choices_free(struct binkc_choices *choices); int binkc_choices_get_text( struct binkc_choices *choices, size_t idx, char **text); +void binkc_tags_free(struct binkc_tags *tags); +int binkc_tags_get( struct binkc_tags *tags, size_t idx, char **tag); + void binkc_cstring_free(char *cstring); #ifdef __cplusplus diff --git a/clib/src/cchoices.rs b/clib/src/cchoices.rs index 6c66337..c16bcc4 100644 --- a/clib/src/cchoices.rs +++ b/clib/src/cchoices.rs @@ -30,3 +30,13 @@ pub extern "C" fn binkc_choices_get_text( BINKC_OK } + +#[allow(clippy::not_unsafe_ptr_arg_deref)] +#[no_mangle] +pub extern "C" fn binkc_choices_free(choices: *mut Vec>) { + if !choices.is_null() { + unsafe { + drop(Box::from_raw(choices)); + } + } +} diff --git a/clib/src/cstory.rs b/clib/src/cstory.rs index 14bf477..579d0b6 100644 --- a/clib/src/cstory.rs +++ b/clib/src/cstory.rs @@ -133,3 +133,29 @@ pub extern "C" fn binkc_story_choose_choice_index(story: *mut Story, choice_inde Err(_) => BINKC_FAIL, } } + +#[allow(clippy::not_unsafe_ptr_arg_deref)] +#[no_mangle] +pub extern "C" fn binkc_story_get_current_tags( + story: *mut Story, + tags: *mut *mut Vec, + len: *mut usize, +) -> u32 { + if story.is_null() { + return BINKC_FAIL_NULL_POINTER; + } + + let story: &mut Story = unsafe { &mut *story }; + + let result = story.get_current_tags(); + + match result { + Ok(result) => unsafe { + *len = result.len(); + *tags = Box::into_raw(Box::new(result)); + }, + Err(_) => return BINKC_FAIL, + } + + BINKC_OK +} diff --git a/clib/src/ctags.rs b/clib/src/ctags.rs new file mode 100644 index 0000000..96fa723 --- /dev/null +++ b/clib/src/ctags.rs @@ -0,0 +1,42 @@ +use std::{ffi::CString, os::raw::c_char, rc::Rc}; + +use bladeink::choice::Choice; + +use crate::{BINKC_FAIL, BINKC_FAIL_NULL_POINTER, BINKC_OK}; + +#[allow(clippy::not_unsafe_ptr_arg_deref)] +#[no_mangle] +pub extern "C" fn binkc_tags_get( + tags: *const Vec, + idx: usize, + tag: *mut *mut c_char, +) -> u32 { + if tags.is_null() { + return BINKC_FAIL_NULL_POINTER; + } + + let tags: &Vec = unsafe { &*tags }; + + let t = tags.get(idx); + + match t { + Some(t) => unsafe { + *tag = CString::new(t.as_str()).unwrap_or_default().into_raw(); + }, + None => { + return BINKC_FAIL; + } + } + + BINKC_OK +} + +#[allow(clippy::not_unsafe_ptr_arg_deref)] +#[no_mangle] +pub extern "C" fn binkc_tags_free(tags: *mut Vec) { + if !tags.is_null() { + unsafe { + drop(Box::from_raw(tags)); + } + } +} diff --git a/clib/src/lib.rs b/clib/src/lib.rs index 87417ae..57c9786 100644 --- a/clib/src/lib.rs +++ b/clib/src/lib.rs @@ -4,6 +4,7 @@ use std::{ffi::CString, os::raw::c_char}; pub mod cchoices; pub mod cstory; +pub mod ctags; const BINKC_OK: u32 = 0; const BINKC_FAIL: u32 = 1; diff --git a/clib/tests/binkc_tags_test.c b/clib/tests/binkc_tags_test.c new file mode 100644 index 0000000..79f0fd5 --- /dev/null +++ b/clib/tests/binkc_tags_test.c @@ -0,0 +1,112 @@ +#include +#include +#include +#include +#include +#include +#include + + +void finish(int rc, struct binkc_story *story, char *err_msg) { + binkc_cstring_free(err_msg); + binkc_story_free(story); + exit(rc); +} + +void check_ret(int ret, struct binkc_story *story, char *err_msg) { + if (ret != BINKC_OK) { + if(err_msg != NULL) + perror(err_msg); + + finish(EXIT_FAILURE, story, err_msg); + } +} + +char* read_json_file(const char* filename) { + FILE* file = fopen(filename, "r"); + if (!file) { + perror("Failed to open file"); + return NULL; + } + + fseek(file, 0, SEEK_END); + long fileSize = ftell(file); + fseek(file, 0, SEEK_SET); + + char* jsonString = (char*)malloc(fileSize + 1); + if (!jsonString) { + perror("Memory allocation failed"); + fclose(file); + return NULL; + } + + size_t bytesRead = fread(jsonString, 1, fileSize, file); + if ((long)bytesRead != fileSize) { + perror("Failed to read file"); + free(jsonString); + fclose(file); + return NULL; + } + + jsonString[fileSize] = '\0'; + + fclose(file); + + return jsonString; +} + + +int main(void) { + uint32_t ret = BINKC_OK; + struct binkc_story *story = NULL; + struct binkc_tags *tags = NULL; + char *err_msg = NULL; + char *line = NULL; + + char *json_string = read_json_file("../inkfiles/tags/tagsDynamicContent.ink.json"); + if(json_string == NULL) + exit(EXIT_FAILURE); + + ret = binkc_story_new(&story, json_string, &err_msg); + check_ret(ret, story, err_msg); + free(json_string); + + ret = binkc_story_cont(story, &line, &err_msg); + check_ret(ret, story, err_msg); + puts(line); + + if (strcmp(line, "tag\n") != 0) { + puts("expected line"); + finish(EXIT_FAILURE, NULL, NULL); + } + + binkc_cstring_free(line); + + // Obtain and print tags + size_t len = 0; + ret = binkc_story_get_current_tags(story, &tags, &len); + check_ret(ret, story, NULL); + + if (len != 1) { + printf("expected len==1, actual=%lu", len); + finish(EXIT_FAILURE, story, NULL); + } + + char *tag = NULL; + ret = binkc_tags_get(tags, 0, &tag); + if (ret != BINKC_OK) { + puts("error getting tag 0"); + finish(EXIT_FAILURE, NULL, NULL); + } + + printf("TAG: %s\n", tag); + + if (strcmp(tag, "pic8red.jpg") != 0 ) + finish(EXIT_FAILURE, NULL, NULL); + + binkc_cstring_free(tag); + + puts("Story ended ok.\n"); + + finish(EXIT_SUCCESS, story, err_msg); +} \ No newline at end of file diff --git a/clib/tests/binkc_test.c b/clib/tests/binkc_test.c index 80c327d..e8a736c 100644 --- a/clib/tests/binkc_test.c +++ b/clib/tests/binkc_test.c @@ -110,6 +110,8 @@ int main(void) { if (len !=0) { print_choices(choices, len); printf("\n"); + binkc_choices_free(choices); + // Always choose the first option ret = binkc_story_choose_choice_index(story, 0); check_ret(ret, story, NULL); From 350373b181dbf9846b753d90978308690a83dc43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Thu, 12 Oct 2023 11:37:05 +0000 Subject: [PATCH 82/91] bump clib version --- clib/Cargo.toml | 2 +- clib/VERSION | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/clib/Cargo.toml b/clib/Cargo.toml index 5bac093..c1ae9ab 100644 --- a/clib/Cargo.toml +++ b/clib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "binkc" -version = "0.1.0" +version = "0.2.0" description = """ C bindings for the `bladeink` library. """ diff --git a/clib/VERSION b/clib/VERSION index 0ea3a94..341cf11 100644 --- a/clib/VERSION +++ b/clib/VERSION @@ -1 +1 @@ -0.2.0 +0.2.0 \ No newline at end of file From 7a6b7bf273eadcead403b761451f3379a1a9b5c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Fri, 13 Oct 2023 00:13:54 +0200 Subject: [PATCH 83/91] Update release infra for clib. --- .github/workflows/release.yml | 71 +++++++++++++++++++---------------- Cross.toml | 14 +++++++ clib/Cargo.toml | 2 +- clib/src/ctags.rs | 4 +- 4 files changed, 55 insertions(+), 36 deletions(-) create mode 100644 Cross.toml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b5700d3..d6031f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,21 +13,21 @@ # Reference: # https://eugene-babichenko.github.io/blog/2020/05/09/github-actions-cross-platform-auto-releases/ -name: release +name: Release on: push: # Enable when testing release infrastructure on a branch. - # branches: - # - ag/work + branches: + - release-infra tags: - - "[0-9]+.[0-9]+.[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+" jobs: create-release: name: create-release runs-on: ubuntu-latest - # env: + env: # Set to force version number, e.g., when no tag exists. - # BINKC_VERSION: TEST-0.0.0 + BINKC_VERSION: TEST-0.0.0 outputs: upload_url: ${{ steps.release.outputs.upload_url }} binkc_version: ${{ env.BINKC_VERSION }} @@ -68,14 +68,14 @@ jobs: PCRE2_SYS_STATIC: 1 strategy: matrix: - build: [linux, linux-arm, macos, win-msvc, win-gnu, win32-msvc] + build: [linux, linux-arm, macos, win-msvc, win-gnu] include: - build: linux - os: ubuntu-18.04 + os: ubuntu-latest rust: nightly target: x86_64-unknown-linux-musl - build: linux-arm - os: ubuntu-18.04 + os: ubuntu-latest rust: nightly target: arm-unknown-linux-gnueabihf - build: macos @@ -90,10 +90,10 @@ jobs: os: windows-2019 rust: nightly-x86_64-gnu target: x86_64-pc-windows-gnu - - build: win32-msvc - os: windows-2019 - rust: nightly - target: i686-pc-windows-msvc + # - build: win32-msvc + # os: windows-2019 + # rust: nightly + # target: i686-pc-windows-msvc steps: - name: Checkout repository @@ -101,15 +101,15 @@ jobs: with: fetch-depth: 1 - - name: Install packages (Ubuntu) - if: matrix.os == 'ubuntu-18.04' - run: | - ci/ubuntu-install-packages + # - name: Install packages (Ubuntu) + # if: matrix.os == 'ubuntu-18.04' + # run: | + # ci/ubuntu-install-packages - - name: Install packages (macOS) - if: matrix.os == 'macos-latest' - run: | - ci/macos-install-packages + # - name: Install packages (macOS) + # if: matrix.os == 'macos-latest' + # run: | + # ci/macos-install-packages - name: Install Rust uses: actions-rs/toolchain@v1 @@ -119,13 +119,13 @@ jobs: override: true target: ${{ matrix.target }} - - name: Use Cross - shell: bash - run: | - cargo install cross - echo "CARGO=cross" >> $GITHUB_ENV - echo "TARGET_FLAGS=--target ${{ matrix.target }}" >> $GITHUB_ENV - echo "TARGET_DIR=./target/${{ matrix.target }}" >> $GITHUB_ENV + # - name: Use Cross + # shell: bash + # run: | + # cargo install cross + # echo "CARGO=cross" >> $GITHUB_ENV + # echo "TARGET_FLAGS=--target ${{ matrix.target }}" >> $GITHUB_ENV + # echo "TARGET_DIR=./target/${{ matrix.target }}" >> $GITHUB_ENV - name: Show command used for Cargo run: | @@ -134,7 +134,7 @@ jobs: echo "target dir is: ${{ env.TARGET_DIR }}" - name: Build release binary - run: ${{ env.CARGO }} build --verbose --release {{ env.TARGET_FLAGS }} + run: ${{ env.CARGO }} build --verbose --release ${{ env.TARGET_FLAGS }} # - name: Strip release binary (linux and macos) # if: matrix.build == 'linux' || matrix.build == 'macos' @@ -152,16 +152,23 @@ jobs: - name: Build archive shell: bash run: | - outdir="$(ci/cargo-out-dir "${{ env.TARGET_DIR }}")" + # outdir="$(ci/cargo-out-dir "${{ env.TARGET_DIR }}")" staging="libbinkc-${{ needs.create-release.outputs.binkc_version }}-${{ matrix.target }}" mkdir "$staging" if [ "${{ matrix.os }}" = "windows-2019" ]; then - cp "target/${{ matrix.target }}/release/libbinkc.dll" "$staging/" + cp "target/release/binkc.dll" "$staging/" + cp "target/release/binkc.lib" "$staging/" 7z a "$staging.zip" "$staging" echo "ASSET=$staging.zip" >> $GITHUB_ENV + elif [ "${{ matrix.build }}" = "macos" ]; then + cp "target/release/libbinkc.dylib" "$staging/" + cp "target/release/libbinkc.a" "$staging/" + tar czf "$staging.tar.gz" "$staging" + echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV else - cp "target/${{ matrix.target }}/release/libbinkc.so" "$staging/" + cp "target/release/libbinkc.so" "$staging/" + cp "target/release/libbinkc.a" "$staging/" tar czf "$staging.tar.gz" "$staging" echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV fi diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 0000000..b334e5d --- /dev/null +++ b/Cross.toml @@ -0,0 +1,14 @@ +[target.x86_64-unknown-linux-musl] +image = "burntsushi/cross:x86_64-unknown-linux-musl" + +[target.i686-unknown-linux-gnu] +image = "burntsushi/cross:i686-unknown-linux-gnu" + +[target.aarch64-unknown-linux-gnu] +image = "burntsushi/cross:aarch64-unknown-linux-gnu" + +[target.powerpc64-unknown-linux-gnu] +image = "burntsushi/cross:powerpc64-unknown-linux-gnu" + +[target.s390x-unknown-linux-gnu] +image = "burntsushi/cross:s390x-unknown-linux-gnu" diff --git a/clib/Cargo.toml b/clib/Cargo.toml index c1ae9ab..b1a5e66 100644 --- a/clib/Cargo.toml +++ b/clib/Cargo.toml @@ -12,7 +12,7 @@ edition = "2021" [lib] name = "binkc" path = "src/lib.rs" -crate-type = ["cdylib"] +crate-type = ["cdylib", "staticlib"] [dependencies] bladeink = { path = "../lib" } diff --git a/clib/src/ctags.rs b/clib/src/ctags.rs index 96fa723..52b363e 100644 --- a/clib/src/ctags.rs +++ b/clib/src/ctags.rs @@ -1,6 +1,4 @@ -use std::{ffi::CString, os::raw::c_char, rc::Rc}; - -use bladeink::choice::Choice; +use std::{ffi::CString, os::raw::c_char}; use crate::{BINKC_FAIL, BINKC_FAIL_NULL_POINTER, BINKC_OK}; From 413b26a30751288f49a3dad0b91b9afc642c946b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Fri, 13 Oct 2023 11:22:54 +0000 Subject: [PATCH 84/91] The C bindings has now his own repo. --- .github/workflows/release.yml | 184 ---------------------------------- Cargo.toml | 8 +- Cross.toml | 14 --- README.md | 29 ++---- cli-player/Cargo.toml | 3 +- cli-player/README.md | 18 ++++ clib/Cargo.toml | 18 ---- clib/Makefile | 121 ---------------------- clib/VERSION | 1 - clib/binkc.h.in | 48 --------- clib/binkc.pc.in | 11 -- clib/src/cchoices.rs | 42 -------- clib/src/cstory.rs | 161 ----------------------------- clib/src/ctags.rs | 40 -------- clib/src/lib.rs | 21 ---- clib/tests/binkc_tags_test.c | 112 --------------------- clib/tests/binkc_test.c | 127 ----------------------- 17 files changed, 32 insertions(+), 926 deletions(-) delete mode 100644 .github/workflows/release.yml delete mode 100644 Cross.toml create mode 100644 cli-player/README.md delete mode 100644 clib/Cargo.toml delete mode 100644 clib/Makefile delete mode 100644 clib/VERSION delete mode 100644 clib/binkc.h.in delete mode 100644 clib/binkc.pc.in delete mode 100644 clib/src/cchoices.rs delete mode 100644 clib/src/cstory.rs delete mode 100644 clib/src/ctags.rs delete mode 100644 clib/src/lib.rs delete mode 100644 clib/tests/binkc_tags_test.c delete mode 100644 clib/tests/binkc_test.c diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index d6031f1..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,184 +0,0 @@ -# The way this works is the following: -# -# The create-release job runs purely to initialize the GitHub release itself -# and to output upload_url for the following job. -# -# The build-release job runs only once create-release is finished. It gets the -# release upload URL from create-release job outputs, then builds the release -# executables for each supported platform and attaches them as release assets -# to the previously created release. -# -# The key here is that we create the release only once. -# -# Reference: -# https://eugene-babichenko.github.io/blog/2020/05/09/github-actions-cross-platform-auto-releases/ - -name: Release -on: - push: - # Enable when testing release infrastructure on a branch. - branches: - - release-infra - tags: - - "v[0-9]+.[0-9]+.[0-9]+" -jobs: - create-release: - name: create-release - runs-on: ubuntu-latest - env: - # Set to force version number, e.g., when no tag exists. - BINKC_VERSION: TEST-0.0.0 - outputs: - upload_url: ${{ steps.release.outputs.upload_url }} - binkc_version: ${{ env.BINKC_VERSION }} - steps: - - name: Get the release version from the tag - shell: bash - if: env.BINKC_VERSION == '' - run: | - # Apparently, this is the right way to get a tag name. Really? - # - # See: https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027 - echo "BINKC_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - echo "version is: ${{ env.BINKC_VERSION }}" - - name: Create GitHub release - id: release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ env.BINKC_VERSION }} - release_name: ${{ env.BINKC_VERSION }} - - build-release: - name: build-release - needs: ['create-release'] - runs-on: ${{ matrix.os }} - env: - # For some builds, we use cross to test on 32-bit and big-endian - # systems. - CARGO: cargo - # When CARGO is set to CROSS, this is set to `--target matrix.target`. - TARGET_FLAGS: "" - # When CARGO is set to CROSS, TARGET_DIR includes matrix.target. - TARGET_DIR: ./target - # Emit backtraces on panics. - RUST_BACKTRACE: 1 - # Build static releases with PCRE2. - PCRE2_SYS_STATIC: 1 - strategy: - matrix: - build: [linux, linux-arm, macos, win-msvc, win-gnu] - include: - - build: linux - os: ubuntu-latest - rust: nightly - target: x86_64-unknown-linux-musl - - build: linux-arm - os: ubuntu-latest - rust: nightly - target: arm-unknown-linux-gnueabihf - - build: macos - os: macos-latest - rust: nightly - target: x86_64-apple-darwin - - build: win-msvc - os: windows-2019 - rust: nightly - target: x86_64-pc-windows-msvc - - build: win-gnu - os: windows-2019 - rust: nightly-x86_64-gnu - target: x86_64-pc-windows-gnu - # - build: win32-msvc - # os: windows-2019 - # rust: nightly - # target: i686-pc-windows-msvc - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - with: - fetch-depth: 1 - - # - name: Install packages (Ubuntu) - # if: matrix.os == 'ubuntu-18.04' - # run: | - # ci/ubuntu-install-packages - - # - name: Install packages (macOS) - # if: matrix.os == 'macos-latest' - # run: | - # ci/macos-install-packages - - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ matrix.rust }} - profile: minimal - override: true - target: ${{ matrix.target }} - - # - name: Use Cross - # shell: bash - # run: | - # cargo install cross - # echo "CARGO=cross" >> $GITHUB_ENV - # echo "TARGET_FLAGS=--target ${{ matrix.target }}" >> $GITHUB_ENV - # echo "TARGET_DIR=./target/${{ matrix.target }}" >> $GITHUB_ENV - - - name: Show command used for Cargo - run: | - echo "cargo command is: ${{ env.CARGO }}" - echo "target flag is: ${{ env.TARGET_FLAGS }}" - echo "target dir is: ${{ env.TARGET_DIR }}" - - - name: Build release binary - run: ${{ env.CARGO }} build --verbose --release ${{ env.TARGET_FLAGS }} - - # - name: Strip release binary (linux and macos) - # if: matrix.build == 'linux' || matrix.build == 'macos' - # run: strip "target/${{ matrix.target }}/release/libbinkc.so" - - # - name: Strip release binary (arm) - # if: matrix.build == 'linux-arm' - # run: | - # docker run --rm -v \ - # "$PWD/target:/target:Z" \ - # rustembedded/cross:arm-unknown-linux-gnueabihf \ - # arm-linux-gnueabihf-strip \ - # /target/arm-unknown-linux-gnueabihf/release/libbinkc.so - - - name: Build archive - shell: bash - run: | - # outdir="$(ci/cargo-out-dir "${{ env.TARGET_DIR }}")" - staging="libbinkc-${{ needs.create-release.outputs.binkc_version }}-${{ matrix.target }}" - mkdir "$staging" - - if [ "${{ matrix.os }}" = "windows-2019" ]; then - cp "target/release/binkc.dll" "$staging/" - cp "target/release/binkc.lib" "$staging/" - 7z a "$staging.zip" "$staging" - echo "ASSET=$staging.zip" >> $GITHUB_ENV - elif [ "${{ matrix.build }}" = "macos" ]; then - cp "target/release/libbinkc.dylib" "$staging/" - cp "target/release/libbinkc.a" "$staging/" - tar czf "$staging.tar.gz" "$staging" - echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV - else - cp "target/release/libbinkc.so" "$staging/" - cp "target/release/libbinkc.a" "$staging/" - tar czf "$staging.tar.gz" "$staging" - echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV - fi - - - name: Upload release archive - uses: actions/upload-release-asset@v1.0.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create-release.outputs.upload_url }} - asset_path: ${{ env.ASSET }} - asset_name: ${{ env.ASSET }} - asset_content_type: application/octet-stream \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 7a08d80..bc3355c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,3 @@ [workspace] -members = [ - "cli-player", - "lib", - "clib", -] -resolver = "2" \ No newline at end of file +members = ["cli-player", "lib"] +resolver = "2" diff --git a/Cross.toml b/Cross.toml deleted file mode 100644 index b334e5d..0000000 --- a/Cross.toml +++ /dev/null @@ -1,14 +0,0 @@ -[target.x86_64-unknown-linux-musl] -image = "burntsushi/cross:x86_64-unknown-linux-musl" - -[target.i686-unknown-linux-gnu] -image = "burntsushi/cross:i686-unknown-linux-gnu" - -[target.aarch64-unknown-linux-gnu] -image = "burntsushi/cross:aarch64-unknown-linux-gnu" - -[target.powerpc64-unknown-linux-gnu] -image = "burntsushi/cross:powerpc64-unknown-linux-gnu" - -[target.s390x-unknown-linux-gnu] -image = "burntsushi/cross:s390x-unknown-linux-gnu" diff --git a/README.md b/README.md index 80d821b..c866e41 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,6 @@ This is a Rust port of Inkle's [Ink](https://github.com/inkle/ink), a scripting To know more about the Ink language, you can check [the oficial documentation](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md). -The **Blade Ink** project contains 3 crates: - -- `lib` is the `bladeink` lib crate. It is available in crates.io and it can be used in your project as a dependency. -- `cli-player` contains an implementation of a cli player (called `binkplayer`) to run .json.ink story files directly from the console. -- `clib` is a C binding of the `bladeink` library ready to be used in C or any other program that can uses C libraries. - ## Using the bladeink library crate Here it is a quick example that uses the basic features to play an Ink story using the `bladeink` crate. @@ -42,31 +36,28 @@ while !end { } ``` -The `bladeink` library support all the **Ink** language features, including threads, multi-flows, variable set/get from code, variable observing, external functions, tags on choices, etc. Examples of uses of all these features can be found in the `lib/tests` folder in the source code. +The `bladeink` library supports all the **Ink** language features, including threads, multi-flows, variable set/get from code, variable observing, external functions, tags on choices, etc. Examples of uses of all these features can be found in the `lib/tests` folder in the [source code](https://github.com/bladecoder/blade-ink-rs/tree/main/lib/tests). ## Running Ink stories with *binkplayer* -If you download the source code repository, you can run `cargo build` in the workspace root folder, the `binkplayer` binary will be compiled and found in `target/debug`. You can play any of the `.ink.json` file using it. +The Blade Ink project includes a program to run Ink stories in your terminal. -In the `inkfiles` folder we can found many Ink test stories to test the Ink language capabilities. And also we have **The Intercept**, a full featured story created by **Inkle** also included in the `inkfiles` folder. You can run **The Intercept** running the next command in your console. +You can install it from crates.io: ```bash -$ target/debug/binkplayer inkfiles/TheIntercept.ink.json +$ cargo install binkplayer +$ binkplayer ``` -## Using the C bindings +Or, if you download the source code repository, you can run `cargo build` in the workspace root folder, the `binkplayer` binary will be compiled and found in `target/debug`. You can play any `.ink.json` (Ink compiled files). -If you download the source code repository, you can build the C bindings using the Makefile inside the clib folder. - -To create the library in the target/release folder use +In the `inkfiles` folder we can found many Ink test stories to test the Ink language capabilities. And also we have **The Intercept**, a full featured story created by **Inkle**, also included in the `inkfiles` folder. You can run **The Intercept** running the next command in your console. ```bash - $ make clib +$ target/debug/binkplayer inkfiles/TheIntercept.ink.json ``` -This will create the `libbinkc.so.x.x.x`, where x.x.x is the version of the library, and the `binkc.h` ready to include in your C projects. - -The C bindings is a work in progress. In the current state, only the basic functionality to play an Ink story is finish. +## Using Blade Ink in C -We can find an example of use in C in the `clib/tests/binkc_test.c` file. It plays **The Intercept** story included in the `inkfiles` folder, choosing always the first option presented to the user. \ No newline at end of file +There are available C bindings to use Blade Ink in your C projects. Check it out [here](https://github.com/bladecoder/blade-ink-ffi). \ No newline at end of file diff --git a/cli-player/Cargo.toml b/cli-player/Cargo.toml index 3d24368..8d7ecdd 100644 --- a/cli-player/Cargo.toml +++ b/cli-player/Cargo.toml @@ -5,8 +5,9 @@ description = """ Console player for compiled .json Ink story files. """ authors = ["Rafael Garcia "] +repository = "https://github.com/bladecoder/blade-ink-rs/" license = "Apache-2.0" -readme = "../README.md" +readme = "README.md" edition = "2021" [[bin]] diff --git a/cli-player/README.md b/cli-player/README.md new file mode 100644 index 0000000..c56f43c --- /dev/null +++ b/cli-player/README.md @@ -0,0 +1,18 @@ +# Blade Ink Player + +This is a program to run Ink stories in your terminal. + +This program uses the Blade Ink Rust library available as a [crate](https://crates.io/crates/bladeink). + +The Blade Ink library is a Rust port of Inkle's [Ink](https://github.com/inkle/ink), a scripting language for writing interactive narrative. + +To know more about the Ink language, you can check [the oficial documentation](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md). + + +## Running Ink stories with *binkplayer* + +binkplayer runs the Ink compiled story passed as a parameter. + +```bash +$ binkplayer +``` diff --git a/clib/Cargo.toml b/clib/Cargo.toml deleted file mode 100644 index b1a5e66..0000000 --- a/clib/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "binkc" -version = "0.2.0" -description = """ -C bindings for the `bladeink` library. -""" -license = "Apache-2.0" -authors = ["Rafael Garcia "] -readme = "../README.md" -edition = "2021" - -[lib] -name = "binkc" -path = "src/lib.rs" -crate-type = ["cdylib", "staticlib"] - -[dependencies] -bladeink = { path = "../lib" } diff --git a/clib/Makefile b/clib/Makefile deleted file mode 100644 index 8d4f39f..0000000 --- a/clib/Makefile +++ /dev/null @@ -1,121 +0,0 @@ -ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) -VERSION=$(shell cat $(ROOT_DIR)/VERSION) -VERSION_MAJOR=$(shell echo $(VERSION) | cut -f1 -d.) -VERSION_MINOR=$(shell echo $(VERSION) | cut -f2 -d.) -VERSION_MICRO=$(shell echo $(VERSION) | cut -f3 -d.) -CLIB_SO_DEV=libbinkc.so -CLIB_SO_FULL=$(CLIB_SO_DEV).$(VERSION) -CLIB_HEADER=binkc.h -CLIB_SO_DEV_RELEASE=../target/release/$(CLIB_SO_DEV) -CLIB_SO_DEV_DEBUG=../target/debug/$(CLIB_SO_DEV) -CLIB_PKG_CONFIG=binkc.pc -PREFIX ?= /usr/local - -#outdir is used by COPR as well: https://docs.pagure.org/copr.copr/user_documentation.html -outdir ?= $(ROOT_DIR) - -CPU_BITS = $(shell getconf LONG_BIT) -ifeq ($(CPU_BITS), 32) - LIBDIR ?= $(PREFIX)/lib -else - LIBDIR ?= $(PREFIX)/lib$(CPU_BITS) -endif - -INCLUDE_DIR ?= $(PREFIX)/include -PKG_CONFIG_LIBDIR ?= $(LIBDIR)/pkgconfig - -SKIP_VENDOR_CREATION ?=0 -RELEASE ?=0 - -.PHONY: debug -debug: $(CLIB_SO_DEV_DEBUG) $(CLIB_HEADER) - mv $(CLIB_HEADER) ../target/debug/$(shell basename $(CLIB_HEADER)) - mv $(CLIB_SO_DEV_DEBUG) ../target/debug/$(CLIB_SO_FULL) - ln -sfv $(CLIB_SO_FULL) ../target/debug/$(CLIB_SO_DEV) - -$(CLIB_SO_DEV_RELEASE): - cd .. && cargo build --all --release - -$(CLIB_SO_DEV_DEBUG): - cd .. && cargo build --all - -clib: $(CLIB_HEADER) $(CLIB_SO_DEV_RELEASE) $(CLIB_PKG_CONFIG) - mv $(CLIB_HEADER) ../target/release/$(shell basename $(CLIB_HEADER)) - mv $(CLIB_PKG_CONFIG) ../target/release/$(shell basename $(CLIB_PKG_CONFIG)) - mv $(CLIB_SO_DEV_RELEASE) ../target/release/$(CLIB_SO_FULL) - ln -sfv $(CLIB_SO_FULL) ../target/release/$(CLIB_SO_DEV) - -.PHONY: $(CLIB_HEADER) -$(CLIB_HEADER): $(CLIB_HEADER).in - cp $(CLIB_HEADER).in $(CLIB_HEADER) - sed -i -e 's/@_VERSION_MAJOR@/$(VERSION_MAJOR)/' \ - $(CLIB_HEADER) - sed -i -e 's/@_VERSION_MINOR@/$(VERSION_MINOR)/' \ - $(CLIB_HEADER) - sed -i -e 's/@_VERSION_MICRO@/$(VERSION_MICRO)/' \ - $(CLIB_HEADER) - -.PHONY: $(CLIB_PKG_CONFIG) -$(CLIB_PKG_CONFIG): $(CLIB_PKG_CONFIG).in - cp $(CLIB_PKG_CONFIG).in $(CLIB_PKG_CONFIG) - sed -i -e 's|@VERSION@|$(VERSION)|' $(CLIB_PKG_CONFIG) - sed -i -e 's|@PREFIX@|$(PREFIX)|' $(CLIB_PKG_CONFIG) - sed -i -e 's|@LIBDIR@|$(LIBDIR)|' $(CLIB_PKG_CONFIG) - sed -i -e 's|@INCLUDE_DIR@|$(INCLUDE_DIR)|' $(CLIB_PKG_CONFIG) - -.PHONY: clib_check -clib_check: $(CLIB_SO_DEV_DEBUG) $(CLIB_HEADER) - $(eval TMPDIR := $(shell mktemp -d)) - cp $(CLIB_SO_DEV_DEBUG) $(TMPDIR)/$(CLIB_SO_FULL) - ln -sfv $(CLIB_SO_FULL) $(TMPDIR)/$(CLIB_SO_DEV) - mv $(CLIB_HEADER) $(TMPDIR)/$(shell basename $(CLIB_HEADER)) - cc -g -Wall -Wextra -L$(TMPDIR) -I$(TMPDIR) \ - -o $(TMPDIR)/binkc_test clib/tests/binkc_test.c -lbinkc - LD_LIBRARY_PATH=$(TMPDIR) \ - valgrind --trace-children=yes --leak-check=full \ - --error-exitcode=1 \ - $(TMPDIR)/binkc_test 1>/dev/null - rm -rf $(TMPDIR) - -rust_check: - cd .. && cargo test -- --show-output; - cd clib - -check: rust_check clib_check - -test: clean debug - cc -g -Wall -Wextra -L../target/debug -I../target/debug \ - -o ../target/debug/binkc_test ./tests/binkc_test.c -lbinkc - LD_LIBRARY_PATH=../target/debug \ - ../target/debug/binkc_test - cc -g -Wall -Wextra -L../target/debug -I../target/debug \ - -o ../target/debug/binkc_tags_test ./tests/binkc_tags_test.c -lbinkc - LD_LIBRARY_PATH=../target/debug \ - ../target/debug/binkc_tags_test - -clean: - - cd .. && cargo clean - - rm -f target/debug/$(CLIB_SO_MAN) - - rm -f target/debug/$(CLIB_SO_FULL) - - rm -f $(CLIB_HEADER) - -install: $(CLI_EXEC_RELEASE) clib - install -p -v -D -m755 $(CLI_EXEC_RELEASE) \ - $(DESTDIR)$(PREFIX)/bin/$(CLI_EXEC) - install -p -D -m755 $(CLIB_SO_DEV_RELEASE) \ - $(DESTDIR)$(LIBDIR)/$(CLIB_SO_FULL) - ln -sfv $(CLIB_SO_FULL) $(DESTDIR)$(LIBDIR)/$(CLIB_SO_MAN) - ln -sfv $(CLIB_SO_FULL) $(DESTDIR)$(LIBDIR)/$(CLIB_SO_DEV) - install -p -v -D -m644 $(CLIB_HEADER) \ - $(DESTDIR)$(INCLUDE_DIR)/$(shell basename $(CLIB_HEADER)) - install -p -v -D -m644 $(CLIB_PKG_CONFIG) \ - $(DESTDIR)$(PKG_CONFIG_LIBDIR)/$(shell basename $(CLIB_PKG_CONFIG)) - -uninstall: - - rm -fv $(DESTDIR)$(PREFIX)/bin/$(CLI_EXEC) - - rm -fv $(DESTDIR)$(LIBDIR)/$(CLIB_SO_DEV) - - rm -fv $(DESTDIR)$(LIBDIR)/$(CLIB_SO_MAN) - - rm -fv $(DESTDIR)$(LIBDIR)/$(CLIB_SO_FULL) - - rm -fv $(DESTDIR)$(INCLUDE_DIR)/$(shell basename $(CLIB_HEADER)) - - rm -fv $(DESTDIR)$(INCLUDE_DIR)/$(shell basename $(CLIB_PKG_CONFIG)) - diff --git a/clib/VERSION b/clib/VERSION deleted file mode 100644 index 341cf11..0000000 --- a/clib/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.2.0 \ No newline at end of file diff --git a/clib/binkc.h.in b/clib/binkc.h.in deleted file mode 100644 index 62b6b21..0000000 --- a/clib/binkc.h.in +++ /dev/null @@ -1,48 +0,0 @@ -#ifndef _LIBBINKC_H_ -#define _LIBBINKC_H_ - -#ifdef __cplusplus -extern "C" { -#endif - -#include -#include - -#define BINKC_VERSION_MAJOR @_VERSION_MAJOR@ -#define BINKC_VERSION_MINOR @_VERSION_MINOR@ -#define BINKC_VERSION_MICRO @_VERSION_MICRO@ - -#define BINKC_VERSION \ - ((BINKC_VERSION_MAJOR * 10000) + \ - (BINKC_VERSION_MINOR * 100) + \ - BINKC_VERSION_MICRO) - -#define BINKC_OK 0 -#define BINKC_FAIL 1 -#define BINKC_FAIL_NULL_POINTER 2 - -struct binkc_story; -struct binkc_choices; -struct binkc_tags; - -int binkc_story_new(struct binkc_story **story, char *json_string, char **err_msg); -void binkc_story_free(struct binkc_story *story); -int binkc_story_can_continue(struct binkc_story *story, bool *can_continue); -int binkc_story_cont(struct binkc_story *story, char **line, char **err_msg); -int binkc_story_get_current_choices(struct binkc_story *story, struct binkc_choices **choices, size_t *len); -int binkc_story_choose_choice_index(struct binkc_story *story, size_t choice_index); -int binkc_story_get_current_tags(struct binkc_story *story, struct binkc_tags **tags, size_t *len); - -void binkc_choices_free(struct binkc_choices *choices); -int binkc_choices_get_text( struct binkc_choices *choices, size_t idx, char **text); - -void binkc_tags_free(struct binkc_tags *tags); -int binkc_tags_get( struct binkc_tags *tags, size_t idx, char **tag); - -void binkc_cstring_free(char *cstring); - -#ifdef __cplusplus -} /* extern "C" */ -#endif - -#endif /* End of _LIBBINKC_H_ */ diff --git a/clib/binkc.pc.in b/clib/binkc.pc.in deleted file mode 100644 index f6e5c11..0000000 --- a/clib/binkc.pc.in +++ /dev/null @@ -1,11 +0,0 @@ -prefix=@PREFIX@ -exec_prefix=${prefix} -libdir=@LIBDIR@ -includedir=@INCLUDE_DIR@/ - -Name: binkc -Version: @VERSION@ -Description: BInk C binding library -Requires: -Libs: -L${libdir} -lbinkc -Cflags: -I${includedir} diff --git a/clib/src/cchoices.rs b/clib/src/cchoices.rs deleted file mode 100644 index c16bcc4..0000000 --- a/clib/src/cchoices.rs +++ /dev/null @@ -1,42 +0,0 @@ -use std::{ffi::CString, os::raw::c_char, rc::Rc}; - -use bladeink::choice::Choice; - -use crate::{BINKC_FAIL, BINKC_FAIL_NULL_POINTER, BINKC_OK}; - -#[allow(clippy::not_unsafe_ptr_arg_deref)] -#[no_mangle] -pub extern "C" fn binkc_choices_get_text( - choices: *const Vec>, - idx: usize, - text: *mut *mut c_char, -) -> u32 { - if choices.is_null() { - return BINKC_FAIL_NULL_POINTER; - } - - let choices: &Vec> = unsafe { &*choices }; - - let choice = choices.get(idx); - - match choice { - Some(choice) => unsafe { - *text = CString::new(choice.text.as_str()).unwrap().into_raw(); - }, - None => { - return BINKC_FAIL; - } - } - - BINKC_OK -} - -#[allow(clippy::not_unsafe_ptr_arg_deref)] -#[no_mangle] -pub extern "C" fn binkc_choices_free(choices: *mut Vec>) { - if !choices.is_null() { - unsafe { - drop(Box::from_raw(choices)); - } - } -} diff --git a/clib/src/cstory.rs b/clib/src/cstory.rs deleted file mode 100644 index 579d0b6..0000000 --- a/clib/src/cstory.rs +++ /dev/null @@ -1,161 +0,0 @@ -use std::{ - ffi::{CStr, CString}, - os::raw::c_char, - rc::Rc, -}; - -use bladeink::{choice::Choice, story::Story}; - -use crate::{BINKC_FAIL, BINKC_FAIL_NULL_POINTER, BINKC_OK}; - -#[allow(clippy::not_unsafe_ptr_arg_deref)] -#[no_mangle] -pub extern "C" fn binkc_story_new( - story: *mut *mut Story, - json_string: *const c_char, - err_msg: *mut *mut c_char, -) -> u32 { - if story.is_null() || err_msg.is_null() { - return BINKC_FAIL_NULL_POINTER; - } - - unsafe { - *story = std::ptr::null_mut(); - *err_msg = std::ptr::null_mut(); - } - - let c_str: &CStr = unsafe { CStr::from_ptr(json_string) }; - let str_slice: &str = c_str.to_str().unwrap(); - - let result = Story::new(str_slice); - - match result { - Ok(s) => unsafe { - *story = Box::into_raw(Box::new(s)); - BINKC_OK - }, - Err(e) => unsafe { - *err_msg = CString::new(e.to_string()).unwrap().into_raw(); - BINKC_FAIL - }, - } -} - -#[allow(clippy::not_unsafe_ptr_arg_deref)] -#[no_mangle] -pub extern "C" fn binkc_story_free(story: *mut Story) { - if !story.is_null() { - unsafe { - drop(Box::from_raw(story)); - } - } -} - -#[allow(clippy::not_unsafe_ptr_arg_deref)] -#[no_mangle] -pub extern "C" fn binkc_story_can_continue(story: *mut Story, can_continue: *mut bool) -> u32 { - if story.is_null() { - return BINKC_FAIL_NULL_POINTER; - } - - let story: &mut Story = unsafe { &mut *story }; - - unsafe { - *can_continue = story.can_continue(); - } - - BINKC_OK -} - -#[allow(clippy::not_unsafe_ptr_arg_deref)] -#[no_mangle] -pub extern "C" fn binkc_story_cont( - story: *mut Story, - line: *mut *mut c_char, - err_msg: *mut *mut c_char, -) -> u32 { - if story.is_null() { - return BINKC_FAIL_NULL_POINTER; - } - - let story: &mut Story = unsafe { &mut *story }; - - let result = story.cont(); - - match result { - Ok(l) => unsafe { - *line = CString::new(l).unwrap().into_raw(); - BINKC_OK - }, - Err(e) => unsafe { - *err_msg = CString::new(e.to_string()).unwrap().into_raw(); - BINKC_FAIL - }, - } -} - -#[allow(clippy::not_unsafe_ptr_arg_deref)] -#[no_mangle] -pub extern "C" fn binkc_story_get_current_choices( - story: *mut Story, - choices: *mut *mut Vec>, - len: *mut usize, -) -> u32 { - if story.is_null() { - return BINKC_FAIL_NULL_POINTER; - } - - let story: &mut Story = unsafe { &mut *story }; - - let result = Box::new(story.get_current_choices()); - - unsafe { - *len = result.len(); - *choices = Box::into_raw(result); - } - - BINKC_OK -} - -#[allow(clippy::not_unsafe_ptr_arg_deref)] -#[no_mangle] -pub extern "C" fn binkc_story_choose_choice_index(story: *mut Story, choice_index: usize) -> u32 { - if story.is_null() { - return BINKC_FAIL_NULL_POINTER; - } - - let story: &mut Story = unsafe { &mut *story }; - - let result = story.choose_choice_index(choice_index); - - match result { - Ok(_) => BINKC_OK, - Err(_) => BINKC_FAIL, - } -} - -#[allow(clippy::not_unsafe_ptr_arg_deref)] -#[no_mangle] -pub extern "C" fn binkc_story_get_current_tags( - story: *mut Story, - tags: *mut *mut Vec, - len: *mut usize, -) -> u32 { - if story.is_null() { - return BINKC_FAIL_NULL_POINTER; - } - - let story: &mut Story = unsafe { &mut *story }; - - let result = story.get_current_tags(); - - match result { - Ok(result) => unsafe { - *len = result.len(); - *tags = Box::into_raw(Box::new(result)); - }, - Err(_) => return BINKC_FAIL, - } - - BINKC_OK -} diff --git a/clib/src/ctags.rs b/clib/src/ctags.rs deleted file mode 100644 index 52b363e..0000000 --- a/clib/src/ctags.rs +++ /dev/null @@ -1,40 +0,0 @@ -use std::{ffi::CString, os::raw::c_char}; - -use crate::{BINKC_FAIL, BINKC_FAIL_NULL_POINTER, BINKC_OK}; - -#[allow(clippy::not_unsafe_ptr_arg_deref)] -#[no_mangle] -pub extern "C" fn binkc_tags_get( - tags: *const Vec, - idx: usize, - tag: *mut *mut c_char, -) -> u32 { - if tags.is_null() { - return BINKC_FAIL_NULL_POINTER; - } - - let tags: &Vec = unsafe { &*tags }; - - let t = tags.get(idx); - - match t { - Some(t) => unsafe { - *tag = CString::new(t.as_str()).unwrap_or_default().into_raw(); - }, - None => { - return BINKC_FAIL; - } - } - - BINKC_OK -} - -#[allow(clippy::not_unsafe_ptr_arg_deref)] -#[no_mangle] -pub extern "C" fn binkc_tags_free(tags: *mut Vec) { - if !tags.is_null() { - unsafe { - drop(Box::from_raw(tags)); - } - } -} diff --git a/clib/src/lib.rs b/clib/src/lib.rs deleted file mode 100644 index 57c9786..0000000 --- a/clib/src/lib.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! C API for bladeink. - -use std::{ffi::CString, os::raw::c_char}; - -pub mod cchoices; -pub mod cstory; -pub mod ctags; - -const BINKC_OK: u32 = 0; -const BINKC_FAIL: u32 = 1; -const BINKC_FAIL_NULL_POINTER: u32 = 2; - -#[allow(clippy::not_unsafe_ptr_arg_deref)] -#[no_mangle] -pub extern "C" fn binkc_cstring_free(cstring: *mut c_char) { - unsafe { - if !cstring.is_null() { - drop(CString::from_raw(cstring)); - } - } -} diff --git a/clib/tests/binkc_tags_test.c b/clib/tests/binkc_tags_test.c deleted file mode 100644 index 79f0fd5..0000000 --- a/clib/tests/binkc_tags_test.c +++ /dev/null @@ -1,112 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - - -void finish(int rc, struct binkc_story *story, char *err_msg) { - binkc_cstring_free(err_msg); - binkc_story_free(story); - exit(rc); -} - -void check_ret(int ret, struct binkc_story *story, char *err_msg) { - if (ret != BINKC_OK) { - if(err_msg != NULL) - perror(err_msg); - - finish(EXIT_FAILURE, story, err_msg); - } -} - -char* read_json_file(const char* filename) { - FILE* file = fopen(filename, "r"); - if (!file) { - perror("Failed to open file"); - return NULL; - } - - fseek(file, 0, SEEK_END); - long fileSize = ftell(file); - fseek(file, 0, SEEK_SET); - - char* jsonString = (char*)malloc(fileSize + 1); - if (!jsonString) { - perror("Memory allocation failed"); - fclose(file); - return NULL; - } - - size_t bytesRead = fread(jsonString, 1, fileSize, file); - if ((long)bytesRead != fileSize) { - perror("Failed to read file"); - free(jsonString); - fclose(file); - return NULL; - } - - jsonString[fileSize] = '\0'; - - fclose(file); - - return jsonString; -} - - -int main(void) { - uint32_t ret = BINKC_OK; - struct binkc_story *story = NULL; - struct binkc_tags *tags = NULL; - char *err_msg = NULL; - char *line = NULL; - - char *json_string = read_json_file("../inkfiles/tags/tagsDynamicContent.ink.json"); - if(json_string == NULL) - exit(EXIT_FAILURE); - - ret = binkc_story_new(&story, json_string, &err_msg); - check_ret(ret, story, err_msg); - free(json_string); - - ret = binkc_story_cont(story, &line, &err_msg); - check_ret(ret, story, err_msg); - puts(line); - - if (strcmp(line, "tag\n") != 0) { - puts("expected line"); - finish(EXIT_FAILURE, NULL, NULL); - } - - binkc_cstring_free(line); - - // Obtain and print tags - size_t len = 0; - ret = binkc_story_get_current_tags(story, &tags, &len); - check_ret(ret, story, NULL); - - if (len != 1) { - printf("expected len==1, actual=%lu", len); - finish(EXIT_FAILURE, story, NULL); - } - - char *tag = NULL; - ret = binkc_tags_get(tags, 0, &tag); - if (ret != BINKC_OK) { - puts("error getting tag 0"); - finish(EXIT_FAILURE, NULL, NULL); - } - - printf("TAG: %s\n", tag); - - if (strcmp(tag, "pic8red.jpg") != 0 ) - finish(EXIT_FAILURE, NULL, NULL); - - binkc_cstring_free(tag); - - puts("Story ended ok.\n"); - - finish(EXIT_SUCCESS, story, err_msg); -} \ No newline at end of file diff --git a/clib/tests/binkc_test.c b/clib/tests/binkc_test.c deleted file mode 100644 index e8a736c..0000000 --- a/clib/tests/binkc_test.c +++ /dev/null @@ -1,127 +0,0 @@ -#include -#include -#include -#include -#include -#include - - -void finish(int rc, struct binkc_story *story, char *err_msg) { - binkc_cstring_free(err_msg); - binkc_story_free(story); - exit(rc); -} - -void check_ret(int ret, struct binkc_story *story, char *err_msg) { - if (ret != BINKC_OK) { - if(err_msg != NULL) - perror(err_msg); - - finish(EXIT_FAILURE, story, err_msg); - } -} - -void print_choices(struct binkc_choices *choices, size_t len) { - for (size_t i=0; i < len; i++) { - char *text = NULL; - int ret = binkc_choices_get_text(choices, i, &text); - if (ret != BINKC_OK) { - finish(EXIT_FAILURE, NULL, NULL); - } - - printf("%lu. %s\n", i+1, text); - binkc_cstring_free(text); - } -} - -char* read_json_file(const char* filename) { - FILE* file = fopen(filename, "r"); - if (!file) { - perror("Failed to open file"); - return NULL; - } - - fseek(file, 0, SEEK_END); - long fileSize = ftell(file); - fseek(file, 0, SEEK_SET); - - char* jsonString = (char*)malloc(fileSize + 1); - if (!jsonString) { - perror("Memory allocation failed"); - fclose(file); - return NULL; - } - - size_t bytesRead = fread(jsonString, 1, fileSize, file); - if ((long)bytesRead != fileSize) { - perror("Failed to read file"); - free(jsonString); - fclose(file); - return NULL; - } - - jsonString[fileSize] = '\0'; - - fclose(file); - - return jsonString; -} - - -int main(void) { - uint32_t ret = BINKC_OK; - struct binkc_story *story = NULL; - struct binkc_choices *choices = NULL; - char *err_msg = NULL; - char *line = NULL; - // char *json_string = "{\"inkVersion\":21,\"root\":[[\"^Line.\",\"\\n\",[\"done\",{\"#n\":\"g-0\"}],null],\"done\",null],\"listDefs\":{}}"; - - char *json_string = read_json_file("../inkfiles/TheIntercept.ink.json"); - if(json_string == NULL) - exit(EXIT_FAILURE); - - ret = binkc_story_new(&story, json_string, &err_msg); - check_ret(ret, story, err_msg); - free(json_string); - - bool end = false; - - while(!end) { - bool can_continue; - ret = binkc_story_can_continue(story, &can_continue); - check_ret(ret, story, err_msg); - - while (can_continue) { - ret = binkc_story_cont(story, &line, &err_msg); - check_ret(ret, story, err_msg); - puts(line); - binkc_cstring_free(line); - - ret = binkc_story_can_continue(story, &can_continue); - check_ret(ret, story, err_msg); - } - - // Obtain and print choices - size_t len = 0; - ret = binkc_story_get_current_choices(story, &choices, &len); - check_ret(ret, story, NULL); - //printf("Num. choices: %lu\n", len); - - if (len !=0) { - print_choices(choices, len); - printf("\n"); - binkc_choices_free(choices); - - // Always choose the first option - ret = binkc_story_choose_choice_index(story, 0); - check_ret(ret, story, NULL); - } else { - end = true; - } - - } - - printf("Story ended ok.\n"); - - finish(EXIT_SUCCESS, story, err_msg); -} \ No newline at end of file From 9c684c2f4832101f008e824ee42e15b054d72fe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Fri, 13 Oct 2023 11:24:40 +0000 Subject: [PATCH 85/91] Added version to binkplayer --- cli-player/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli-player/Cargo.toml b/cli-player/Cargo.toml index 8d7ecdd..f520fcc 100644 --- a/cli-player/Cargo.toml +++ b/cli-player/Cargo.toml @@ -16,7 +16,7 @@ path = "src/main.rs" [dependencies] anyhow = "1.0.75" -bladeink = { path = "../lib" } +bladeink = { path = "../lib", "version" = "0.9.2" } clap = { "version" = "4.4.6", features = ["derive"] } [dev-dependencies] From 11f191405c4e701278e52da781de3a4a2223d68c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Fri, 13 Oct 2023 11:25:53 +0000 Subject: [PATCH 86/91] Created new version to upload the doc. changes in README. --- lib/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index fc7bdf7..ad20a8b 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bladeink" -version = "0.9.2" +version = "0.9.3" authors = ["Rafael Garcia "] description = """ This is a Rust port of inkle's ink, a scripting language for writing interactive narrative. From 34bd90588daa5efa6a9d4dd31f199f6dbf97f34d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Fri, 13 Oct 2023 17:00:37 +0000 Subject: [PATCH 87/91] binkplayer autoplay and divert features. --- cli-player/Cargo.toml | 1 + cli-player/src/main.rs | 59 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/cli-player/Cargo.toml b/cli-player/Cargo.toml index f520fcc..b07984d 100644 --- a/cli-player/Cargo.toml +++ b/cli-player/Cargo.toml @@ -18,6 +18,7 @@ path = "src/main.rs" anyhow = "1.0.75" bladeink = { path = "../lib", "version" = "0.9.2" } clap = { "version" = "4.4.6", features = ["derive"] } +rand = "0.8.5" [dev-dependencies] assert_cmd = "2.0.12" diff --git a/cli-player/src/main.rs b/cli-player/src/main.rs index 7cd365d..b64885b 100644 --- a/cli-player/src/main.rs +++ b/cli-player/src/main.rs @@ -8,10 +8,17 @@ use anyhow::Context; use bladeink::story_callbacks::{ErrorHandler, ErrorType}; use bladeink::{choice::Choice, story::Story}; use clap::Parser; +use rand::Rng; -#[derive(Parser)] +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] struct Args { + /// The compiled .ink.json file pub json_filename: String, + + /// Choose options randomly + #[arg(short, default_value_t = false)] + pub auto_play: bool, } enum Command { @@ -20,6 +27,7 @@ enum Command { Help(), Load(String), Save(String), + DivertPath(String), } struct EHandler { @@ -68,7 +76,19 @@ fn main() -> Result<(), Box> { let choices = story.get_current_choices(); if !choices.is_empty() { - let command = read_input(&choices)?; + let command = if args.auto_play { + let i = rand::thread_rng().gen_range(0..choices.len()); + + println!(); + print_choices(&choices); + println!(); + println!("?>{i}"); + + Command::Choose(i) + } else { + read_input(&choices)? + }; + end = process_command(command, &mut story)?; } else { end = true; @@ -91,8 +111,18 @@ fn process_command(command: Command, story: &mut Story) -> Result { let json_string = story.save_state()?; save_json(&filename, &json_string)?; + println!("Ok.") } - Command::Help() => println!("Commands:\n\tload \n\tsave \n\tquit\n\t"), + Command::DivertPath(path) => { + let result = story.choose_path_string(&path, true, None); + + if let Err(desc) = result { + println!("") + } + } + Command::Help() => println!( + "Commands:\n\tload \n\tsave \n\t-> \n\tquit\n\t" + ), } Ok(false) @@ -114,6 +144,7 @@ fn read_input(choices: &Vec>) -> Result> { print!("?>"); io::stdout().flush()?; + line.clear(); let _b1 = std::io::stdin().read_line(&mut line)?; let trimmed = line.trim(); @@ -127,11 +158,15 @@ fn read_input(choices: &Vec>) -> Result> { Ok(v) => { if v < 1 || v > choices.len() as i32 { print_error("option out of range"); + continue; } else { return Ok(Command::Choose((v - 1) as usize)); } } - Err(_) => print_error("unrecognized option or command"), + Err(_) => { + print_error("unrecognized option or command"); + continue; + } } } @@ -147,7 +182,21 @@ fn read_input(choices: &Vec>) -> Result> { print_error("incorrect filename"); } - "save" => return Ok(Command::Save(words[1].trim().to_string())), + "save" => { + if words.len() == 2 { + return Ok(Command::Save(words[1].trim().to_string())); + } + + print_error("incorrect filename"); + } + + "->" => { + if words.len() == 2 { + return Ok(Command::DivertPath(words[1].trim().to_string())); + } + + print_error("incorrect divert"); + } _ => print_error("unrecognized option or command"), } } From b37e0e89cad5af6ad1d4574f3ccb4a4d4361b9c1 Mon Sep 17 00:00:00 2001 From: IFcoltransG <47414286+IFcoltransG@users.noreply.github.com> Date: Mon, 23 Oct 2023 21:28:53 +1300 Subject: [PATCH 88/91] Improve documentation * Update high-level example of library * Re-word grammar in readme and top-level docs * Add safety comment to casting enum to u8 * Improve docs for choice module * Improve docs for story error module * Improve docs for ValueType * Improve docs for story and story callback modules * Add docs for other structs in ValueType's mod * Link docs for modules to important structs * Fix overwrite from merging into wrong version of readme * Wording changes to readme and top-level module doc --- README.md | 18 ++++--- lib/src/choice.rs | 10 ++-- lib/src/lib.rs | 24 ++++++---- lib/src/story.rs | 96 +++++++++++++++++++++----------------- lib/src/story_callbacks.rs | 41 +++++++++------- lib/src/story_error.rs | 10 ++-- lib/src/value.rs | 4 ++ lib/src/value_type.rs | 25 ++++++---- 8 files changed, 133 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index c866e41..daf820b 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,20 @@ # Blade Ink -This is a Rust port of Inkle's [Ink](https://github.com/inkle/ink), a scripting language for writing interactive narrative. +This is a Rust port of Inkle's [Ink](https://github.com/inkle/ink), a scripting language for writing interactive narratives. -`bladeink` is fully compatible with the reference version and supports all the language features. +`bladeink` is fully compatible with the reference version and supports all its language features. -To know more about the Ink language, you can check [the oficial documentation](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md). +To learn more about the Ink language, you can check [the official documentation](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md). ## Using the bladeink library crate -Here it is a quick example that uses the basic features to play an Ink story using the `bladeink` crate. +Here is a quick example that uses basic features to play an Ink story using the `bladeink` crate. ```rust // story is the entry point of the `bladeink` lib. // json_string is a string with all the contents of the .ink.json file. let mut story = Story::new(json_string)?; -let mut end = false; - -while !end { +loop { while story.can_continue() { let line = story.cont()?; @@ -31,7 +29,7 @@ while !end { // set the option selected by the user story.choose_choice_index(choice_idx)?; } else { - end = true; + break; } } ``` @@ -52,7 +50,7 @@ $ binkplayer Or, if you download the source code repository, you can run `cargo build` in the workspace root folder, the `binkplayer` binary will be compiled and found in `target/debug`. You can play any `.ink.json` (Ink compiled files). -In the `inkfiles` folder we can found many Ink test stories to test the Ink language capabilities. And also we have **The Intercept**, a full featured story created by **Inkle**, also included in the `inkfiles` folder. You can run **The Intercept** running the next command in your console. +In the `inkfiles` folder you can find many Ink test stories to test the Ink language capabilities. And also we have **The Intercept**, a full featured story created by **Inkle**, also included in the `inkfiles` folder. You can run **The Intercept** running the next command in your console. ```bash $ target/debug/binkplayer inkfiles/TheIntercept.ink.json @@ -60,4 +58,4 @@ $ target/debug/binkplayer inkfiles/TheIntercept.ink.json ## Using Blade Ink in C -There are available C bindings to use Blade Ink in your C projects. Check it out [here](https://github.com/bladecoder/blade-ink-ffi). \ No newline at end of file +There are C bindings available to use Blade Ink in your C projects. Check it out [here](https://github.com/bladecoder/blade-ink-ffi). diff --git a/lib/src/choice.rs b/lib/src/choice.rs index 0a49264..3ca82f9 100644 --- a/lib/src/choice.rs +++ b/lib/src/choice.rs @@ -1,4 +1,4 @@ -//! A generated Choice from the story. +//! A generated [`Choice`] from the story. use core::fmt; use std::cell::RefCell; @@ -8,6 +8,7 @@ use crate::{ path::Path, }; +/// Represents a choice generated by a [`Story`](crate::story::Story). pub struct Choice { obj: Object, thread_at_generation: RefCell>, @@ -16,11 +17,12 @@ pub struct Choice { pub(crate) source_path: String, pub(crate) target_path: Path, pub(crate) is_invisible_default: bool, + /// Ink tags attached to this `Choice`. pub tags: Vec, - /// The original index into currentChoices list on the Story when - /// this Choice was generated, for convenience. + /// The original index into `currentChoices` list on the [`Story`](crate::story::Story) when + /// this `Choice` was generated, for convenience. pub index: RefCell, - /// The main text to presented to the player for this Choice. + /// The main text to present to the player for this `Choice`. pub text: String, } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 91c37fb..ac86c43 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,18 +1,20 @@ -//! This is a Rust port of inkle's [Ink](https://github.com/inkle/ink), a scripting language for writing interactive narrative. -//! `bladeink` is fully compatible with the reference version and supports all the language features. +//! This is a Rust port of inkle's [Ink](https://github.com/inkle/ink), a scripting language for writing interactive narratives. +//! `bladeink` is fully compatible with the reference version and supports all its language features. //! -//! To know more about the Ink language, you can check [the oficial documentation](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md). +//! To learn more about the Ink language, you can check [the official documentation](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md). //! -//! Here it is a quick example that uses the basic features to play an Ink story using the `bladeink` crate. +//! Here is a quick example that uses basic features to play an Ink story using the `bladeink` crate. //! -//! ```ignore +//! ``` +//! # use bladeink::{story::Story, story_error::StoryError}; +//! # fn main() -> Result<(), StoryError> { +//! # let json_string = r##"{"root":["done",null],"listDefs":{},"inkVersion":21}"##; +//! # let read_input = |_:&_| Ok(0); //! // story is the entry point of the `bladeink` lib. //! // json_string is a string with all the contents of the .ink.json file. //! let mut story = Story::new(json_string)?; //! -//! let mut end = false; -//! -//! while !end { +//! loop { //! while story.can_continue() { //! let line = story.cont()?; //! @@ -27,12 +29,14 @@ //! // set the option selected by the user //! story.choose_choice_index(choice_idx)?; //! } else { -//! end = true; +//! break; //! } //! } +//! # Ok(()) +//! # } //! ``` //! -//! The `bladeink` library support all the **Ink** language features, including threads, multi-flows, variable set/get from code, variable observing, external functions, +//! The `bladeink` library supports all the **Ink** language features, including threads, multi-flows, variable set/get from code, variable observing, external functions, //! tags on choices, etc. Examples of uses of all these features will be added to this documentation in the future, but meanwhile, all the examples can be found in the `lib/tests` folder in the source code of this crate. mod callstack; diff --git a/lib/src/story.rs b/lib/src/story.rs index da8f22a..fbfef53 100644 --- a/lib/src/story.rs +++ b/lib/src/story.rs @@ -1,4 +1,4 @@ -//! This is the entry point to load and run an Ink story. +//! [`Story`] is the entry point to load and run an Ink story. use std::{ cell::RefCell, collections::{HashMap, VecDeque}, @@ -35,7 +35,7 @@ use crate::{ void::Void, }; -/// The current version of the ink story file format. +/// The current version of the Ink story file format. pub const INK_VERSION_CURRENT: i32 = 21; /// The minimum legacy version of ink that can be loaded by the current version of the code. const INK_VERSION_MINIMUM_COMPATIBLE: i32 = 18; @@ -47,8 +47,8 @@ enum OutputStateChange { NewlineRemoved, } -/// A Story is the core struct that represents a complete Ink narrative, and -/// manages the evaluation and state of it. +/// A `Story` is the core struct representing a complete Ink narrative, +/// managing evaluation and state. pub struct Story { main_content_container: Rc, state: StoryState, @@ -68,7 +68,7 @@ pub struct Story { } impl Story { - /// Construct a Story object using a JSON string compiled through inklecate. + /// Construct a `Story` out of a JSON string that was compiled with `inklecate`. pub fn new(json_string: &str) -> Result { let json: serde_json::Value = match serde_json::from_str(json_string) { Ok(value) => value, @@ -188,6 +188,8 @@ impl Story { Ok(()) } + /// Creates a string representing the hierarchy of objects and containers + /// in a story. pub fn build_string_of_hierarchy(&self) -> String { let mut sb = String::new(); @@ -201,15 +203,19 @@ impl Story { sb } + /// `true` if the story is not waiting for user input from [`choose_choice_index`](Story::choose_choice_index). pub fn can_continue(&self) -> bool { self.get_state().can_continue() } + /// Tries to continue pulling text from the story. pub fn cont(&mut self) -> Result { self.continue_async(0.0)?; self.get_current_text() } + /// Continues the story until a choice or error is reached. + /// If a choice is reached, returns all text produced along the way. pub fn continue_maximally(&mut self) -> Result { self.if_async_we_cant("continue_maximally")?; @@ -222,6 +228,7 @@ impl Story { Ok(sb) } + /// Continues running the story code for the specified number of milliseconds. pub fn continue_async(&mut self, millisecs_limit_async: f32) -> Result<(), StoryError> { if !self.has_validated_externals { self.validate_external_bindings()?; @@ -509,6 +516,9 @@ impl Story { Ok(false) } + /// The string of output text available at the current point in + /// the `Story`. This string will be built as the `Story` is stepped + /// through with the [`cont`](Story::cont) method. pub fn get_current_text(&mut self) -> Result { self.if_async_we_cant("call currentText since it's a work in progress")?; Ok(self.get_state_mut().get_current_text()) @@ -1720,11 +1730,11 @@ impl Story { successful_increment } - /// The vector of Choice objects available at the current point in - /// the Story. This vector will be populated as the Story is stepped - /// through with the cont() method. Once can_continue becomes - /// false, this vector will be populated, and is usually - /// (but not always) on the final cont() step. + /// The vector of [`Choice`](crate::choice::Choice) objects available at the current point in + /// the `Story`. This vector will be populated as the `Story` is stepped + /// through with the [`cont`](Story::cont) method. Once [`can_continue`](Story::can_continue) becomes + /// `false`, this vector will be populated, and is usually + /// (but not always) on the final [`cont`](Story::cont) step. pub fn get_current_choices(&self) -> Vec> { // Don't include invisible choices for external usage. let mut choices = Vec::new(); @@ -1741,26 +1751,27 @@ impl Story { choices } - /// Whether the currentErrors list contains any errors. - /// THIS MAY BE REMOVED - you should be setting an error handler directly + /// Whether the `currentErrors` list contains any errors. + /// + /// THIS METHOD MAY BE REMOVED IN FUTURE -- you should be setting an error handler directly /// using Story.onError. pub fn has_error(&self) -> bool { self.get_state().has_error() } - /// Any errors generated during evaluation of the Story. + /// Any critical errors generated during evaluation of the `Story`. pub fn get_current_errors(&self) -> &Vec { self.get_state().get_current_errors() } - /// Any warnings generated during evaluation of the Story. + /// Any warnings generated during evaluation of the `Story`. pub fn get_current_warnings(&self) -> &Vec { self.get_state().get_current_warnings() } - /// 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. + /// Chooses the [`Choice`](crate::choice::Choice) from the `currentChoices` list with the given + /// index. Internally, this sets the current content path to what + /// the [`Choice`](crate::choice::Choice) points to, ready to continue story evaluation. pub fn choose_choice_index(&mut self, choice_index: usize) -> Result<(), StoryError> { let choices = self.get_current_choices(); if choice_index >= choices.len() { @@ -2018,8 +2029,9 @@ impl Story { } } - /// Evaluates a function defined in ink, and gathers the possibly multi-line text as generated by the function. - /// This text output is any text written as normal content within the function, as opposed to the return value, as returned with `~ return`. + /// Evaluates a function defined in ink, and gathers the (possibly multi-line) text the function produces while executing. + /// This output text is any text written as normal content within the function, + /// as opposed to the ink function's return value, which is specified by `~ return` in the ink. pub fn evaluate_function( &mut self, func_name: &str, @@ -2190,15 +2202,15 @@ impl Story { self.main_content_container.content_at_path(path, 0, -1) } - /// Gets a list of tags as defined with '#' in source that were seen - /// during the latest cont() call. + /// Gets a list of tags defined with '#' in the ink source that were seen + /// during the most recent [`cont`](Story::cont) call. pub fn get_current_tags(&mut self) -> Result, StoryError> { self.if_async_we_cant("call currentTags since it's a work in progress")?; Ok(self.get_state_mut().get_current_tags()) } /// Change the current position of the story to the given path. From here you can - /// call `cont()` to evaluate the next line. + /// call [`cont()`](Story::cont) to evaluate the next line. /// /// The path string is a dot-separated path as used internally by the engine. /// These examples should work: @@ -2216,20 +2228,20 @@ impl Story { /// /// ...because of the way that content is nested within a weave structure. /// - /// By default this will reset the callstack beforehand, which means that any + /// Usually you would 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. + /// discarded. This is different from the behaviour of [`choose_choice_index`](Story::choose_choice_index), which + /// will always keep the callstack, since the choices are known to come from a + /// correct state, and their source thread is known. /// - /// 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. + /// You have the option of passing `false` to the `reset_callstack` parameter if you + /// don't want this behaviour, leaving any active threads, tunnels or + /// function calls intact. /// - /// This is potentially dangerous! If you're in the middle of a tunnel, + /// Not reseting the call stack 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 + /// 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, `choose_path_string` /// will throw an error. pub fn choose_path_string( &mut self, @@ -2285,7 +2297,7 @@ impl Story { Ok(()) } - /// Changes the current flow. + /// Changes from the current flow to the specified one. pub fn switch_flow(&mut self, flow_name: &str) -> Result<(), StoryError> { self.if_async_we_cant("switch flow")?; @@ -2301,19 +2313,19 @@ impl Story { Ok(()) } - /// Removes the specified flow. + /// Removes the specified flow from the story. pub fn remove_flow(&mut self, flow_name: &str) -> Result<(), StoryError> { self.get_state_mut().remove_flow_internal(flow_name) } - /// Removes the specified flow. + /// Switches to the default flow, keeping the current flow around for later. pub fn switch_to_default_flow(&mut self) { self.get_state_mut().switch_to_default_flow_internal(); } pub(crate) fn if_async_we_cant(&self, activity_str: &str) -> Result<(), StoryError> { if self.async_continue_active { - return Err(StoryError::InvalidStoryState(format!("Can't {}. Story is in the middle of a ContinueAsync(). Make more continue_async() calls or a single cont() call beforehand.", activity_str))); + return Err(StoryError::InvalidStoryState(format!("Can't {}. Story is in the middle of a continue_async(). Make more continue_async() calls or a single cont() call beforehand.", activity_str))); } Ok(()) @@ -2344,7 +2356,7 @@ impl Story { self.get_state().variables_state.get(variable_name) } - /// Exports the current state to json format, in order to save the game. + /// Exports the current state to JSON format, in order to save the game. pub fn save_state(&self) -> Result { self.get_state().to_json() } @@ -2354,7 +2366,7 @@ impl Story { self.get_state_mut().load_json(json_state) } - /// Gets the visit/read count of a particular Container at the given path. + /// 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: /// ///```ink @@ -2365,10 +2377,10 @@ impl Story { self.get_state().visit_count_at_path_string(path_string) } - /// 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 external - /// function, but you don't want it to fail to run. + /// An ink file can provide a fallback function for when when an `EXTERNAL` has been left + /// unbound by the client, in which case the fallback will be called instead. Useful when + /// testing a story in play-mode, when it's not possible to write a client-side external + /// function, but when you don't want it to completely fail to run. pub fn set_allow_external_function_fallbacks(&mut self, v: bool) { self.allow_external_function_fallbacks = v; } diff --git a/lib/src/story_callbacks.rs b/lib/src/story_callbacks.rs index 1f9bc76..711547e 100644 --- a/lib/src/story_callbacks.rs +++ b/lib/src/story_callbacks.rs @@ -1,4 +1,4 @@ -//! For setting the callbacks functions that will be called while the story is processing. +//! For setting the callbacks functions that will be called while the [`Story`] is processing. use std::{cell::RefCell, collections::HashSet, rc::Rc}; use crate::{ @@ -7,12 +7,12 @@ use crate::{ value_type::ValueType, void::Void, }; -/// Defines the method that will be called when a observed global variable changes. +/// Defines the method that will be called when an observed global variable changes. pub trait VariableObserver { fn changed(&mut self, variable_name: &str, value: &ValueType); } -/// Defines the method callback that implements the external function. +/// Defines the method callback implementing an external function. pub trait ExternalFunction { fn call(&mut self, func_name: &str, args: Vec) -> Option; } @@ -27,32 +27,34 @@ pub trait ErrorHandler { fn error(&mut self, message: &str, error_type: ErrorType); } +/// Types of errors an Ink story might throw. #[derive(PartialEq, Clone, Copy)] pub enum ErrorType { - // You should probably fix this, but it's not critical + /// Problem that is not critical, but should be fixed. Warning, - // Critical error that can't be recovered from + /// Critical error that can't be recovered from. Error, } +/// Methods dealing with callback handlers. impl Story { - /// Assing the error handler for all runtime errors in ink - i.e. problems + /// Assign the error handler for all runtime errors in ink -- i.e. problems /// with the source ink itself that are only discovered when playing /// the story. /// It's strongly recommended that you assign an error handler to your - /// story instance to avoid getting exceptions for ink errors. + /// story instance, to avoid getting panics for ink errors. pub fn set_error_handler(&mut self, err_handler: Rc>) { self.on_error = Some(err_handler); } - /// When the named global variable changes it's value, the observer will be + /// When the specified 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.set_variable`. + /// [`story.set_variable`](Story::set_variable). pub fn observe_variable( &mut self, variable_name: &str, @@ -82,7 +84,7 @@ impl Story { Ok(()) } - /// Removes the variable observer, to stop getting variable change notifications. + /// Removes a 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 None, then the observer will be removed /// from all variables that it's subscribed to. @@ -137,16 +139,21 @@ impl Story { } } - /// Bind a Rust function to an ink EXTERNAL function declaration. + /// Bind a Rust function to an ink `EXTERNAL` function declaration. /// + /// Arguments: + /// * `func_name` - The name of the function you're binding the handler to. + /// * `function` - The handler that will be called whenever Ink runs that + /// `EXTERNAL` function. /// * `lookahead_safe` - 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 + /// glue that will the current line with the next. It's + /// possible that a function can appear to be called twice, + /// and earlier than expected. 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 + /// of the function will not change), then you can pass `true`. + /// If your function might have side effects or return different results each time it's called, + /// pass `false` to avoid these extra calls, especially if you want some action /// to be performed in game code when this function is called. pub fn bind_external_function( &mut self, @@ -173,7 +180,7 @@ impl Story { Ok(()) } - /// Remove a binding for a named EXTERNAL ink function. + /// Remove the binding for a named EXTERNAL ink function. pub fn unbind_external_function(&mut self, func_name: &str) -> Result<(), StoryError> { self.if_async_we_cant("unbind an external a function")?; diff --git a/lib/src/story_error.rs b/lib/src/story_error.rs index 4b7fd49..96e2e01 100644 --- a/lib/src/story_error.rs +++ b/lib/src/story_error.rs @@ -1,12 +1,16 @@ -//! Error that represents an error when running a Story at runtime. -//! An error being returned of this type is typically when there's -//! a bug in your ink, rather than in the ink engine itself! +//! Errors that happen at runtime, when running a [`Story`](crate::story::Story). use core::fmt; +/// Error that represents an error when running a [`Story`](crate::story::Story) at runtime. +/// An error of this type typically means there's +/// a bug in your ink, rather than in the ink engine itself! #[derive(Debug)] pub enum StoryError { + /// Story is in an invalid state. InvalidStoryState(String), + /// JSON for the ink was not valid. BadJson(String), + /// A method was called with an inappropriate argument. BadArgument(String), } diff --git a/lib/src/value.rs b/lib/src/value.rs index b843007..aae33ca 100644 --- a/lib/src/value.rs +++ b/lib/src/value.rs @@ -227,6 +227,10 @@ impl Value { pub fn get_cast_ordinal(&self) -> u8 { let v = &self.value; + // SAFETY: `ValueType` is `repr(u8)` so every variant has the layout + // of a struct with its first field being the `u8` discriminant, + // ensuring the `u8` can be read from a pointer to the enum. + // See e.g. https://doc.rust-lang.org/std/mem/fn.discriminant.html#accessing-the-numeric-value-of-the-discriminant let ptr_to_option = (v as *const ValueType) as *const u8; unsafe { *ptr_to_option } } diff --git a/lib/src/value_type.rs b/lib/src/value_type.rs index 5dc377a..9ca60c6 100644 --- a/lib/src/value_type.rs +++ b/lib/src/value_type.rs @@ -1,20 +1,25 @@ //! A combination of an Ink value with its type. use crate::{ink_list::InkList, path::Path, story_error::StoryError}; +/// An Ink value, tagged with its type. #[repr(u8)] #[derive(Clone)] pub enum ValueType { Bool(bool), Int(i32), Float(f32), + /// An Ink list value. List(InkList), + /// Ink string, constructed with [`new_string`](ValueType::new_string) String(StringValue), + /// Reference to an Ink divert. DivertTarget(Path), + /// Reference to an Ink variable. VariablePointer(VariablePointerValue), } impl ValueType { - /// Creates a new ValueType with the String type. + /// Creates a new `ValueType` for a `String`. pub fn new_string(str: &str) -> ValueType { let mut inline_ws = true; @@ -32,7 +37,7 @@ impl ValueType { }) } - /// Gets the internal boolean value or None if the ValueType is not a ValueType::Bool + /// Gets the internal boolean, value or `None` if the `ValueType` is not a [`ValueType::Bool`] pub fn get_bool(&self) -> Option { match self { ValueType::Bool(v) => Some(*v), @@ -40,7 +45,7 @@ impl ValueType { } } - /// Gets the internal i32 value or None if the ValueType is not a ValueType::Int + /// Gets the internal `i32` value, or `None` if the `ValueType` is not a [`ValueType::Int`] pub fn get_int(&self) -> Option { match self { ValueType::Int(v) => Some(*v), @@ -48,7 +53,7 @@ impl ValueType { } } - /// Gets the internal f32 value or None if the ValueType is not a ValueType::Float + /// Gets the internal `f32` value, or `None` if the `ValueType` is not a [`ValueType::Float`] pub fn get_float(&self) -> Option { match self { ValueType::Float(v) => Some(*v), @@ -56,7 +61,7 @@ impl ValueType { } } - /// Gets the internal string value or None if the ValueType is not a ValueType::String + /// Gets the internal string value, or `None` if the `ValueType` is not a [`ValueType::String`] pub fn get_str(&self) -> Option<&str> { match self { ValueType::String(v) => Some(&v.string), @@ -64,7 +69,7 @@ impl ValueType { } } - /// Try to convert the internal value of this ValueType to i32 + /// Tries to convert the internal value of this `ValueType` to `i32` pub fn coerce_to_int(&self) -> Result { match self { ValueType::Bool(v) => { @@ -80,7 +85,7 @@ impl ValueType { } } - /// Try to convert the internal value of this ValueType to f32 + /// Tries to convert the internal value of this `ValueType` to `f32` pub fn coerce_to_float(&self) -> Result { match self { ValueType::Bool(v) => { @@ -98,7 +103,7 @@ impl ValueType { } } - /// Try to convert the internal value of this ValueType to bool + /// Tries to convert the internal value of this `ValueType` to `bool` pub fn coerce_to_bool(&self) -> Result { match self { ValueType::Bool(v) => Ok(*v), @@ -115,7 +120,7 @@ impl ValueType { } } - /// Try to convert the internal value of this ValueType to String + /// Tries to convert the internal value of this `ValueType` to `String` pub fn coerce_to_string(&self) -> Result { match self { ValueType::Bool(v) => Ok(v.to_string()), @@ -129,6 +134,7 @@ impl ValueType { } } +/// Ink runtime representation of a string. #[derive(Clone)] pub struct StringValue { /// The internal string value. @@ -143,6 +149,7 @@ impl StringValue { } } +/// Ink runtime representation of a reference to a variable. #[derive(Clone, PartialEq)] pub struct VariablePointerValue { pub(crate) variable_name: String, From c7194c504b2a74d0c12dc99016a94f24619e098a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Mon, 23 Oct 2023 12:26:31 +0000 Subject: [PATCH 89/91] Support optional threadsafe through a feature --- cli-player/src/main.rs | 13 +- lib/Cargo.toml | 3 + lib/src/callstack.rs | 15 +- lib/src/choice.rs | 20 +-- lib/src/choice_point.rs | 12 +- lib/src/container.rs | 33 ++-- lib/src/divert.rs | 17 +- lib/src/flow.rs | 21 ++- lib/src/ink_list.rs | 11 +- lib/src/json_read.rs | 60 +++---- lib/src/json_write.rs | 10 +- lib/src/lib.rs | 14 ++ lib/src/list_definitions_origin.rs | 10 +- lib/src/native_function_call.rs | 250 +++++++++++++++-------------- lib/src/object.rs | 34 ++-- lib/src/pointer.rs | 11 +- lib/src/search_result.rs | 12 +- lib/src/state_patch.rs | 17 +- lib/src/story.rs | 93 ++++++----- lib/src/story_callbacks.rs | 30 ++-- lib/src/story_state.rs | 53 +++--- lib/src/variable_reference.rs | 7 +- lib/src/variables_state.rs | 41 +++-- lib/tests/runtime_test.rs | 13 +- 24 files changed, 410 insertions(+), 390 deletions(-) diff --git a/cli-player/src/main.rs b/cli-player/src/main.rs index b64885b..c0c53f0 100644 --- a/cli-player/src/main.rs +++ b/cli-player/src/main.rs @@ -1,12 +1,11 @@ //! Console player that can runs compiled `.ink.json` story files writen in the **Ink** language. -use std::cell::RefCell; - use std::io::Write; -use std::{error::Error, fs, io, path::Path, rc::Rc}; +use std::{error::Error, fs, io, path::Path}; use anyhow::Context; use bladeink::story_callbacks::{ErrorHandler, ErrorType}; use bladeink::{choice::Choice, story::Story}; +use bladeink::{BrCell, Brc}; use clap::Parser; use rand::Rng; @@ -35,8 +34,8 @@ struct EHandler { } impl EHandler { - pub fn new() -> Rc> { - Rc::new(RefCell::new(EHandler { + pub fn new() -> Brc> { + Brc::new(BrCell::new(EHandler { should_terminate: false, })) } @@ -128,13 +127,13 @@ fn process_command(command: Command, story: &mut Story) -> Result]) { +fn print_choices(choices: &[Brc]) { for (i, c) in choices.iter().enumerate() { println!("{}. {}", i + 1, c.text); } } -fn read_input(choices: &Vec>) -> Result> { +fn read_input(choices: &Vec>) -> Result> { let mut line = String::new(); loop { diff --git a/lib/Cargo.toml b/lib/Cargo.toml index ad20a8b..3f77d1b 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -16,6 +16,9 @@ edition = "2021" name = "bladeink" path = "src/lib.rs" +[features] +threadsafe = [] + [dependencies] serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.93" diff --git a/lib/src/callstack.rs b/lib/src/callstack.rs index 39049a3..3eb2fc8 100644 --- a/lib/src/callstack.rs +++ b/lib/src/callstack.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, rc::Rc}; +use std::collections::HashMap; use serde_json::{json, Map}; @@ -12,12 +12,13 @@ use crate::{ story::Story, story_error::StoryError, value::Value, + Brc, }; pub struct Element { pub current_pointer: Pointer, pub in_expression_evaluation: bool, - pub temporary_variables: HashMap>, + pub temporary_variables: HashMap>, pub push_pop_type: PushPopType, pub evaluation_stack_height_when_pushed: usize, pub function_start_in_output_stream: i32, @@ -69,7 +70,7 @@ impl Thread { } pub fn from_json( - main_content_container: &Rc, + main_content_container: &Brc, j_obj: &Map, ) -> Result { let mut thread = Thread::new(); @@ -215,7 +216,7 @@ pub struct CallStack { } impl CallStack { - pub fn new(main_content_container: Rc) -> CallStack { + pub fn new(main_content_container: Brc) -> CallStack { let mut cs = CallStack { thread_counter: 0, start_of_root: Pointer::start_of(main_content_container), @@ -357,7 +358,7 @@ impl CallStack { pub fn set_temporary_variable( &mut self, name: String, - value: Rc, + value: Brc, declare_new: bool, mut context_index: i32, ) -> Result<(), StoryError> { @@ -406,7 +407,7 @@ impl CallStack { &self, name: &str, context_index: i32, - ) -> Option> { + ) -> Option> { let mut context_index = context_index; if context_index == -1 { context_index = self.get_current_element_index() + 1; @@ -457,7 +458,7 @@ impl CallStack { pub fn load_json( &mut self, - main_content_container: &Rc, + main_content_container: &Brc, j_obj: &Map, ) -> Result<(), StoryError> { self.threads.clear(); diff --git a/lib/src/choice.rs b/lib/src/choice.rs index 3ca82f9..be5a32d 100644 --- a/lib/src/choice.rs +++ b/lib/src/choice.rs @@ -1,18 +1,18 @@ //! A generated [`Choice`] from the story. use core::fmt; -use std::cell::RefCell; use crate::{ callstack::Thread, object::{Object, RTObject}, path::Path, + BrCell, }; /// Represents a choice generated by a [`Story`](crate::story::Story). pub struct Choice { obj: Object, - thread_at_generation: RefCell>, - pub(crate) original_thread_index: RefCell, + thread_at_generation: BrCell>, + pub(crate) original_thread_index: BrCell, /// Get the path to the original choice point - where was this choice defined in the story? pub(crate) source_path: String, pub(crate) target_path: Path, @@ -21,7 +21,7 @@ pub struct Choice { pub tags: Vec, /// The original index into `currentChoices` list on the [`Story`](crate::story::Story) when /// this `Choice` was generated, for convenience. - pub index: RefCell, + pub index: BrCell, /// The main text to present to the player for this `Choice`. pub text: String, } @@ -40,10 +40,10 @@ impl Choice { target_path, is_invisible_default, tags, - index: RefCell::new(0), - original_thread_index: RefCell::new(0), + index: BrCell::new(0), + original_thread_index: BrCell::new(0), text, - thread_at_generation: RefCell::new(Some(thread_at_generation)), + thread_at_generation: BrCell::new(Some(thread_at_generation)), source_path, } } @@ -60,10 +60,10 @@ impl Choice { target_path: Path::new_with_components_string(Some(path_string_on_choice)), is_invisible_default: false, tags: Vec::new(), - index: RefCell::new(index), - original_thread_index: RefCell::new(original_thread_index), + index: BrCell::new(index), + original_thread_index: BrCell::new(original_thread_index), text: text.to_string(), - thread_at_generation: RefCell::new(None), + thread_at_generation: BrCell::new(None), source_path, } } diff --git a/lib/src/choice_point.rs b/lib/src/choice_point.rs index a0d467d..e0b43e3 100644 --- a/lib/src/choice_point.rs +++ b/lib/src/choice_point.rs @@ -1,10 +1,10 @@ use core::fmt; -use std::{cell::RefCell, rc::Rc}; use crate::{ container::Container, object::{Object, RTObject}, path::Path, + BrCell, Brc, }; pub struct ChoicePoint { @@ -14,7 +14,7 @@ pub struct ChoicePoint { is_invisible_default: bool, once_only: bool, has_condition: bool, - path_on_choice: RefCell, + path_on_choice: BrCell, } impl ChoicePoint { @@ -26,13 +26,13 @@ impl ChoicePoint { is_invisible_default: (flags & 8) > 0, once_only: (flags & 16) > 0, has_condition: (flags & 1) > 0, - path_on_choice: RefCell::new(Path::new_with_components_string(Some( + path_on_choice: BrCell::new(Path::new_with_components_string(Some( path_string_on_choice, ))), } } - pub fn get_choice_target(self: &Rc) -> Option> { + pub fn get_choice_target(self: &Brc) -> Option> { Object::resolve_path(self.clone(), &self.path_on_choice.borrow()).container() } @@ -76,7 +76,7 @@ impl ChoicePoint { self.once_only } - pub fn get_path_on_choice(self: &Rc) -> Path { + pub fn get_path_on_choice(self: &Brc) -> Path { // Resolve any relative paths to global ones as we come across them if self.path_on_choice.borrow().is_relative() { if let Some(choice_target_obj) = self.get_choice_target() { @@ -87,7 +87,7 @@ impl ChoicePoint { self.path_on_choice.borrow().clone() } - pub fn get_path_string_on_choice(self: &Rc) -> String { + pub fn get_path_string_on_choice(self: &Brc) -> String { Object::compact_path_string(self.clone(), &self.get_path_on_choice()) } } diff --git a/lib/src/container.rs b/lib/src/container.rs index 6d3db5d..be07cda 100644 --- a/lib/src/container.rs +++ b/lib/src/container.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, fmt, rc::Rc}; +use std::{collections::HashMap, fmt}; use as_any::Downcast; @@ -8,6 +8,7 @@ use crate::{ search_result::SearchResult, value::Value, value_type::ValueType, + Brc, }; const COUNTFLAGS_VISITS: i32 = 1; @@ -17,8 +18,8 @@ const COUNTFLAGS_COUNTSTARTONLY: i32 = 4; pub struct Container { obj: Object, pub name: Option, - pub content: Vec>, - pub named_content: HashMap>, + pub content: Vec>, + pub named_content: HashMap>, pub visits_should_be_counted: bool, pub turn_index_should_be_counted: bool, pub counting_at_start_only: bool, @@ -28,9 +29,9 @@ impl Container { pub fn new( name: Option, count_flags: i32, - content: Vec>, - named_content: HashMap>, - ) -> Rc { + content: Vec>, + named_content: HashMap>, + ) -> Brc { let mut named_content = named_content; content.iter().for_each(|o| { @@ -44,7 +45,7 @@ impl Container { let (visits_should_be_counted, turn_index_should_be_counted, counting_at_start_only) = Container::split_count_flags(count_flags); - let c = Rc::new(Container { + let c = Brc::new(Container { obj: Object::new(), content, named_content, @@ -131,11 +132,11 @@ impl Container { sb.push('\n'); } - let mut only_named: HashMap> = HashMap::new(); + let mut only_named: HashMap> = HashMap::new(); for (k, v) in self.named_content.iter() { - let o: Rc = v.clone(); - if self.content.iter().any(|e| Rc::ptr_eq(e, &o)) { + let o: Brc = v.clone(); + if self.content.iter().any(|e| Brc::ptr_eq(e, &o)) { continue; } else { only_named.insert(k.clone(), v.clone()); @@ -168,12 +169,12 @@ impl Container { } } - pub fn get_path(self: &Rc) -> Path { + pub fn get_path(self: &Brc) -> Path { Object::get_path(self.as_ref()) } pub fn content_at_path( - self: &Rc, + self: &Brc, path: &Path, partial_path_start: usize, mut partial_path_length: i32, @@ -185,7 +186,7 @@ impl Container { let mut approximate = false; let mut current_container = Some(self.clone()); - let mut current_obj: Rc = self.clone(); + let mut current_obj: Brc = self.clone(); for i in partial_path_start..partial_path_length as usize { let comp = path.get_component(i); @@ -259,7 +260,7 @@ impl Container { ) } - fn content_with_path_component(&self, component: &Component) -> Option> { + fn content_with_path_component(&self, component: &Component) -> Option> { if component.is_index() { if let Some(index) = component.index { if index < self.content.len() { @@ -270,7 +271,7 @@ impl Container { // When path is out of range, quietly return None // (useful as we step/increment forwards through content) return match self.get_object().get_parent() { - Some(o) => Some(o as Rc), + Some(o) => Some(o as Brc), None => None, }; } else if let Some(found_content) = self.named_content.get(component.name.as_ref().unwrap()) @@ -281,7 +282,7 @@ impl Container { None } - pub fn get_named_only_content(&self) -> HashMap> { + pub fn get_named_only_content(&self) -> HashMap> { let mut named_only_content_dict = HashMap::new(); for (key, value) in self.named_content.iter() { diff --git a/lib/src/divert.rs b/lib/src/divert.rs index e74b9e1..0c20da3 100644 --- a/lib/src/divert.rs +++ b/lib/src/divert.rs @@ -1,4 +1,4 @@ -use std::{cell::RefCell, fmt, rc::Rc}; +use std::fmt; use crate::{ container::Container, @@ -6,12 +6,13 @@ use crate::{ path::{Component, Path}, pointer::{self, Pointer}, push_pop::PushPopType, + BrCell, Brc, }; pub struct Divert { obj: Object, - target_pointer: RefCell, - target_path: RefCell>, + target_pointer: BrCell, + target_path: BrCell>, pub external_args: usize, pub is_conditional: bool, pub is_external: bool, @@ -37,8 +38,8 @@ impl Divert { stack_push_type, is_external, external_args, - target_pointer: RefCell::new(pointer::NULL.clone()), - target_path: RefCell::new(Self::target_path_string(target_path)), + target_pointer: BrCell::new(pointer::NULL.clone()), + target_path: BrCell::new(Self::target_path_string(target_path)), variable_divert_name: var_divert_name, } } @@ -47,7 +48,7 @@ impl Divert { value.map(|value| Path::new_with_components_string(Some(value))) } - pub fn get_target_path_string(self: &Rc) -> Option { + pub fn get_target_path_string(self: &Brc) -> Option { self.get_target_path() .as_ref() .map(|p| self.compact_path_string(p)) @@ -79,7 +80,7 @@ impl Divert { } } - pub fn get_target_pointer(self: &Rc) -> Pointer { + pub fn get_target_pointer(self: &Brc) -> Pointer { let target_pointer_null = self.target_pointer.borrow().is_null(); if target_pointer_null { let target_obj = @@ -115,7 +116,7 @@ impl Divert { self.target_pointer.borrow().clone() } - pub fn get_target_path(self: &Rc) -> Option { + pub fn get_target_path(self: &Brc) -> Option { // Resolve any relative paths to global ones as we come across them let target_path = self.target_path.borrow(); diff --git a/lib/src/flow.rs b/lib/src/flow.rs index 0d01521..1254072 100644 --- a/lib/src/flow.rs +++ b/lib/src/flow.rs @@ -1,5 +1,3 @@ -use std::{cell::RefCell, rc::Rc}; - use serde_json::Map; use crate::{ @@ -9,21 +7,22 @@ use crate::{ json_read, json_write, object::RTObject, story_error::StoryError, + BrCell, Brc, }; #[derive(Clone)] pub(crate) struct Flow { pub name: String, - pub callstack: Rc>, - pub output_stream: Vec>, - pub current_choices: Vec>, + pub callstack: Brc>, + pub output_stream: Vec>, + pub current_choices: Vec>, } impl Flow { - pub fn new(name: &str, main_content_container: Rc) -> Flow { + pub fn new(name: &str, main_content_container: Brc) -> Flow { Flow { name: name.to_string(), - callstack: Rc::new(RefCell::new(CallStack::new(main_content_container))), + callstack: Brc::new(BrCell::new(CallStack::new(main_content_container))), output_stream: Vec::new(), current_choices: Vec::new(), } @@ -31,12 +30,12 @@ impl Flow { pub fn from_json( name: &str, - main_content_container: Rc, + main_content_container: Brc, j_obj: &Map, ) -> Result { let mut flow = Self { name: name.to_string(), - callstack: Rc::new(RefCell::new(CallStack::new(main_content_container.clone()))), + callstack: Brc::new(BrCell::new(CallStack::new(main_content_container.clone()))), output_stream: json_read::jarray_to_runtime_obj_list( j_obj .get("outputStream") @@ -55,7 +54,7 @@ impl Flow { )? .iter() .map(|o| o.clone().into_any().downcast::().unwrap()) - .collect::>>(), + .collect::>>(), }; flow.callstack.borrow_mut().load_json( @@ -131,7 +130,7 @@ impl Flow { pub fn load_flow_choice_threads( &mut self, j_choice_threads: Option<&serde_json::Value>, - main_content_container: Rc, + main_content_container: Brc, ) -> Result<(), StoryError> { for choice in self.current_choices.iter_mut() { self.callstack diff --git a/lib/src/ink_list.rs b/lib/src/ink_list.rs index 14e521c..de006da 100644 --- a/lib/src/ink_list.rs +++ b/lib/src/ink_list.rs @@ -1,25 +1,26 @@ use core::fmt; -use std::{cell::RefCell, collections::HashMap}; +use std::collections::HashMap; use crate::{ ink_list_item::InkListItem, list_definition::ListDefinition, list_definitions_origin::ListDefinitionsOrigin, story_error::StoryError, value_type::ValueType, + BrCell, }; #[derive(Clone)] pub struct InkList { pub items: HashMap, - pub origins: RefCell>, + pub origins: BrCell>, // we need an origin when we only have the definition (the list has not elemetns) - initial_origin_names: RefCell>, + initial_origin_names: BrCell>, } impl InkList { pub fn new() -> Self { Self { items: HashMap::new(), - origins: RefCell::new(Vec::with_capacity(0)), - initial_origin_names: RefCell::new(Vec::with_capacity(0)), + origins: BrCell::new(Vec::with_capacity(0)), + initial_origin_names: BrCell::new(Vec::with_capacity(0)), } } diff --git a/lib/src/json_read.rs b/lib/src/json_read.rs index a40e899..58d7037 100644 --- a/lib/src/json_read.rs +++ b/lib/src/json_read.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, rc::Rc}; +use std::collections::HashMap; use serde_json::Map; @@ -9,26 +9,26 @@ use crate::{ list_definitions_origin::ListDefinitionsOrigin, native_function_call::NativeFunctionCall, object::RTObject, path::Path, push_pop::PushPopType, story_error::StoryError, tag::Tag, value::Value, variable_assigment::VariableAssignment, variable_reference::VariableReference, - void::Void, + void::Void, Brc, }; pub fn jtoken_to_runtime_object( token: &serde_json::Value, name: Option, -) -> Result, StoryError> { +) -> Result, StoryError> { match token { serde_json::Value::Null => Err(StoryError::BadJson(format!( "Failed to convert token to runtime RTObject: {}", token ))), - serde_json::Value::Bool(value) => Ok(Rc::new(Value::new_bool(value.to_owned()))), + serde_json::Value::Bool(value) => Ok(Brc::new(Value::new_bool(value.to_owned()))), serde_json::Value::Number(_) => { if token.is_i64() { let val: i32 = token.as_i64().unwrap().try_into().unwrap(); - Ok(Rc::new(Value::new_int(val))) + Ok(Brc::new(Value::new_int(val))) } else { let val: f32 = token.as_f64().unwrap() as f32; - Ok(Rc::new(Value::new_float(val))) + Ok(Brc::new(Value::new_float(val))) } } @@ -38,18 +38,18 @@ pub fn jtoken_to_runtime_object( // String value let first_char = str.chars().next().unwrap(); if first_char == '^' { - return Ok(Rc::new(Value::new_string(&str[1..]))); + return Ok(Brc::new(Value::new_string(&str[1..]))); } else if first_char == '\n' && str.len() == 1 { - return Ok(Rc::new(Value::new_string("\n"))); + return Ok(Brc::new(Value::new_string("\n"))); } // Glue if "<>".eq(str) { - return Ok(Rc::new(Glue::new())); + return Ok(Brc::new(Glue::new())); } if let Some(control_command) = ControlCommand::new_from_name(str) { - return Ok(Rc::new(control_command)); + return Ok(Brc::new(control_command)); } // Native functions @@ -61,12 +61,12 @@ pub fn jtoken_to_runtime_object( call_str = "^"; } if let Some(native_function_call) = NativeFunctionCall::new_from_name(call_str) { - return Ok(Rc::new(native_function_call)); + return Ok(Brc::new(native_function_call)); } // Void if "void".eq(str) { - return Ok(Rc::new(Void::new())); + return Ok(Brc::new(Void::new())); } Err(StoryError::BadJson(format!( @@ -80,7 +80,7 @@ pub fn jtoken_to_runtime_object( let prop_value = obj.get("^->"); if let Some(prop_value) = prop_value { - return Ok(Rc::new(Value::new_divert_target( + return Ok(Brc::new(Value::new_divert_target( Path::new_with_components_string(prop_value.as_str()), ))); } @@ -97,7 +97,7 @@ pub fn jtoken_to_runtime_object( contex_index = v.as_i64().unwrap() as i32; } - let var_ptr = Rc::new(Value::new_variable_pointer(variable_name, contex_index)); + let var_ptr = Brc::new(Value::new_variable_pointer(variable_name, contex_index)); return Ok(var_ptr); } @@ -160,7 +160,7 @@ pub fn jtoken_to_runtime_object( } } - return Ok(Rc::new(Divert::new( + return Ok(Brc::new(Divert::new( pushes_to_stack, div_push_type, external, @@ -181,7 +181,7 @@ pub fn jtoken_to_runtime_object( flags = f.as_u64().unwrap(); } - return Ok(Rc::new(ChoicePoint::new( + return Ok(Brc::new(ChoicePoint::new( flags as i32, path_string_on_choice, ))); @@ -190,12 +190,12 @@ pub fn jtoken_to_runtime_object( // // Variable reference let prop_value = obj.get("VAR?"); if let Some(name) = prop_value { - return Ok(Rc::new(VariableReference::new(name.as_str().unwrap()))); + return Ok(Brc::new(VariableReference::new(name.as_str().unwrap()))); } let prop_value = obj.get("CNT?"); if let Some(v) = prop_value { - return Ok(Rc::new(VariableReference::from_path_for_count( + return Ok(Brc::new(VariableReference::from_path_for_count( v.as_str().unwrap(), ))); } @@ -224,7 +224,7 @@ pub fn jtoken_to_runtime_object( let prop_value = obj.get("re"); let is_new_decl = prop_value.is_none(); - let var_ass = Rc::new(VariableAssignment::new( + let var_ass = Brc::new(VariableAssignment::new( var_name, is_new_decl, is_global_var, @@ -235,7 +235,7 @@ pub fn jtoken_to_runtime_object( // Legacy Tag prop_value = obj.get("#"); if let Some(prop_value) = prop_value { - return Ok(Rc::new(Tag::new(prop_value.as_str().unwrap()))); + return Ok(Brc::new(Tag::new(prop_value.as_str().unwrap()))); } // List value @@ -263,7 +263,7 @@ pub fn jtoken_to_runtime_object( raw_list.items.insert(item, v.as_i64().unwrap() as i32); } - return Ok(Rc::new(Value::new_list(raw_list))); + return Ok(Brc::new(Value::new_list(raw_list))); } // Used when serialising save state only @@ -282,7 +282,7 @@ pub fn jtoken_to_runtime_object( fn jarray_to_container( jarray: &Vec, name: Option, -) -> Result, StoryError> { +) -> Result, StoryError> { // Final object in the array is always a combination of // - named content // - a "#f" key with the countFlags @@ -291,7 +291,7 @@ fn jarray_to_container( let mut name: Option = name; let mut flags = 0; - let mut named_only_content: HashMap> = HashMap::new(); + let mut named_only_content: HashMap> = HashMap::new(); if let Some(terminating_obj) = terminating_obj { for (k, v) in terminating_obj { @@ -325,14 +325,14 @@ fn jarray_to_container( pub fn jarray_to_runtime_obj_list( jarray: &Vec, skip_last: bool, -) -> Result>, StoryError> { +) -> Result>, StoryError> { let mut count = jarray.len(); if skip_last { count -= 1; } - let mut list: Vec> = Vec::with_capacity(jarray.len()); + let mut list: Vec> = Vec::with_capacity(jarray.len()); for jtok in jarray.iter().take(count) { let runtime_obj = jtoken_to_runtime_object(jtok, None); @@ -342,14 +342,16 @@ pub fn jarray_to_runtime_obj_list( Ok(list) } -fn jobject_to_choice(obj: &Map) -> Result, StoryError> { +fn jobject_to_choice( + obj: &Map, +) -> Result, StoryError> { let text = obj.get("text").unwrap().as_str().unwrap(); let index = obj.get("index").unwrap().as_u64().unwrap() as usize; let source_path = obj.get("originalChoicePath").unwrap().as_str().unwrap(); let original_thread_index = obj.get("originalThreadIndex").unwrap().as_i64().unwrap() as usize; let path_string_on_choice = obj.get("targetPath").unwrap().as_str().unwrap(); - Ok(Rc::new(Choice::new_from_json( + Ok(Brc::new(Choice::new_from_json( path_string_on_choice, source_path.to_string(), text, @@ -379,8 +381,8 @@ pub fn jtoken_to_list_definitions( pub(crate) fn jobject_to_hashmap_values( jobj: &Map, -) -> Result>, StoryError> { - let mut dict: HashMap> = HashMap::new(); +) -> Result>, StoryError> { + let mut dict: HashMap> = HashMap::new(); for (k, v) in jobj.iter() { dict.insert( diff --git a/lib/src/json_write.rs b/lib/src/json_write.rs index 8418fea..31380bf 100644 --- a/lib/src/json_write.rs +++ b/lib/src/json_write.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, rc::Rc}; +use std::collections::HashMap; use serde_json::{json, Map}; @@ -7,11 +7,11 @@ use crate::{ control_command::ControlCommand, divert::Divert, glue::Glue, ink_list::InkList, native_function_call::NativeFunctionCall, object::RTObject, push_pop::PushPopType, story_error::StoryError, tag::Tag, value::Value, variable_assigment::VariableAssignment, - variable_reference::VariableReference, void::Void, + variable_reference::VariableReference, void::Void, Brc, }; pub fn write_dictionary_values( - objs: &HashMap>, + objs: &HashMap>, ) -> Result { let mut jobjs: Map = Map::new(); @@ -22,7 +22,7 @@ pub fn write_dictionary_values( Ok(serde_json::Value::Object(jobjs)) } -pub fn write_rtobject(o: Rc) -> Result { +pub fn write_rtobject(o: Brc) -> Result { if let Some(c) = o.as_any().downcast_ref::() { return write_rt_container(c, false); } @@ -275,7 +275,7 @@ pub fn write_choice(choice: &Choice) -> serde_json::Value { } pub(crate) fn write_list_rt_objs( - objs: &[Rc], + objs: &[Brc], ) -> Result { let mut c_array: Vec = Vec::new(); diff --git a/lib/src/lib.rs b/lib/src/lib.rs index ac86c43..a65543a 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -39,6 +39,8 @@ //! The `bladeink` library supports all the **Ink** language features, including threads, multi-flows, variable set/get from code, variable observing, external functions, //! tags on choices, etc. Examples of uses of all these features will be added to this documentation in the future, but meanwhile, all the examples can be found in the `lib/tests` folder in the source code of this crate. +use std::{cell::RefCell, rc::Rc}; + mod callstack; pub mod choice; mod choice_point; @@ -71,3 +73,15 @@ mod variable_assigment; mod variable_reference; mod variables_state; mod void; + +#[cfg(not(feature = "threadsafe"))] +pub type Brc = Rc; + +#[cfg(not(feature = "threadsafe"))] +pub type BrCell = RefCell; + +#[cfg(feature = "threadsafe")] +pub type Brc = Arc; + +#[cfg(feature = "threadsafe")] +pub type BrCell = Mutex; diff --git a/lib/src/list_definitions_origin.rs b/lib/src/list_definitions_origin.rs index 382b503..45d57d0 100644 --- a/lib/src/list_definitions_origin.rs +++ b/lib/src/list_definitions_origin.rs @@ -1,11 +1,11 @@ -use std::{collections::HashMap, rc::Rc}; +use std::collections::HashMap; -use crate::{ink_list::InkList, list_definition::ListDefinition, value::Value}; +use crate::{ink_list::InkList, list_definition::ListDefinition, value::Value, Brc}; #[derive(Clone)] pub struct ListDefinitionsOrigin { lists: HashMap, - all_unambiguous_list_value_cache: HashMap>, + all_unambiguous_list_value_cache: HashMap>, } impl ListDefinitionsOrigin { @@ -24,7 +24,7 @@ impl ListDefinitionsOrigin { let mut l = InkList::new(); l.items.insert(key.clone(), *val); - let list_value = Rc::new(Value::new_list(l)); + let list_value = Brc::new(Value::new_list(l)); list_definitions_origin .all_unambiguous_list_value_cache @@ -42,7 +42,7 @@ impl ListDefinitionsOrigin { self.lists.get(name) } - pub fn find_single_item_list_with_name(&self, name: &str) -> Option<&Rc> { + pub fn find_single_item_list_with_name(&self, name: &str) -> Option<&Brc> { self.all_unambiguous_list_value_cache.get(name) } } diff --git a/lib/src/native_function_call.rs b/lib/src/native_function_call.rs index ee22396..6f3a5f2 100644 --- a/lib/src/native_function_call.rs +++ b/lib/src/native_function_call.rs @@ -1,4 +1,4 @@ -use std::{fmt, rc::Rc}; +use std::fmt; use crate::{ ink_list::InkList, @@ -7,6 +7,7 @@ use crate::{ value::Value, value_type::ValueType, void::Void, + Brc, }; #[derive(Debug, PartialEq, Clone, Copy)] @@ -206,8 +207,8 @@ impl NativeFunctionCall { pub(crate) fn call( &self, - params: Vec>, - ) -> Result, StoryError> { + params: Vec>, + ) -> Result, StoryError> { if self.get_number_of_parameters() != params.len() { return Err(StoryError::InvalidStoryState( "Unexpected number of parameters".to_owned(), @@ -239,8 +240,8 @@ impl NativeFunctionCall { fn call_binary_list_operation( &self, - params: &[Rc], - ) -> Result, StoryError> { + params: &[Brc], + ) -> Result, StoryError> { // List-Int addition/subtraction returns a List (e.g., "alpha" + 1 = "beta") if (self.op == Op::Add || self.op == Op::Subtract) && Value::get_list_value(params[0].as_ref()).is_some() @@ -265,7 +266,7 @@ impl NativeFunctionCall { } }; - return Ok(Rc::new(Value::new_bool(result))); + return Ok(Brc::new(Value::new_bool(result))); } // Normal (list • list) operation @@ -285,7 +286,7 @@ impl NativeFunctionCall { ))) } - fn call_list_increment_operation(&self, list_int_params: &[Rc]) -> Rc { + fn call_list_increment_operation(&self, list_int_params: &[Brc]) -> Brc { let list_val = Value::get_list_value(list_int_params[0].as_ref()).unwrap(); let int_val = Value::get_int_value(list_int_params[1].as_ref()).unwrap(); @@ -315,10 +316,10 @@ impl NativeFunctionCall { } } - Rc::new(Value::new_list(result_raw_list)) + Brc::new(Value::new_list(result_raw_list)) } - fn call_type(&self, coerced_params: Vec>) -> Result, StoryError> { + fn call_type(&self, coerced_params: Vec>) -> Result, StoryError> { match self.op { Op::Add => self.add_op(&coerced_params), Op::Subtract => self.subtract_op(&coerced_params), @@ -356,10 +357,10 @@ impl NativeFunctionCall { fn coerce_values_to_single_type( &self, - params: Vec>, - ) -> Result>, StoryError> { + params: Vec>, + ) -> Result>, StoryError> { let mut dest_type = 1; // Int - let mut result: Vec> = Vec::new(); + let mut result: Vec> = Vec::new(); for obj in params.iter() { // Find out what the output type is @@ -376,7 +377,7 @@ impl NativeFunctionCall { for obj in params.iter() { if let Some(v) = obj.as_ref().as_any().downcast_ref::() { match v.cast(dest_type)? { - Some(casted_value) => result.push(Rc::new(casted_value)), + Some(casted_value) => result.push(Brc::new(casted_value)), None => { if let Ok(obj) = obj.clone().into_any().downcast::() { result.push(obj); @@ -394,28 +395,28 @@ impl NativeFunctionCall { Ok(result) } - fn and_op(&self, params: &[Rc]) -> Result, StoryError> { + fn and_op(&self, params: &[Brc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Bool(op1) => match params[1].value { - ValueType::Bool(op2) => Ok(Rc::new(Value::new_bool(*op1 && op2))), + ValueType::Bool(op2) => Ok(Brc::new(Value::new_bool(*op1 && op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Ok(Rc::new(Value::new_bool(*op1 != 0 && op2 != 0))), + ValueType::Int(op2) => Ok(Brc::new(Value::new_bool(*op1 != 0 && op2 != 0))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Ok(Rc::new(Value::new_bool(*op1 != 0.0 && op2 != 0.0))), + ValueType::Float(op2) => Ok(Brc::new(Value::new_bool(*op1 != 0.0 && op2 != 0.0))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Ok(Rc::new(Value::new_bool( + ValueType::List(op2) => Ok(Brc::new(Value::new_bool( !op1.items.is_empty() && !op2.items.is_empty(), ))), _ => Err(StoryError::InvalidStoryState( @@ -428,22 +429,22 @@ impl NativeFunctionCall { } } - fn greater_op(&self, params: &[Rc]) -> Result, StoryError> { + fn greater_op(&self, params: &[Brc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Ok(Rc::new(Value::new_bool(*op1 > op2))), + ValueType::Int(op2) => Ok(Brc::new(Value::new_bool(*op1 > op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Ok(Rc::new(Value::new_bool(*op1 > op2))), + ValueType::Float(op2) => Ok(Brc::new(Value::new_bool(*op1 > op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Ok(Rc::new(Value::new_bool(op1.greater_than(op2)))), + ValueType::List(op2) => Ok(Brc::new(Value::new_bool(op1.greater_than(op2)))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), @@ -454,22 +455,22 @@ impl NativeFunctionCall { } } - fn less_op(&self, params: &[Rc]) -> Result, StoryError> { + fn less_op(&self, params: &[Brc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Ok(Rc::new(Value::new_bool(*op1 < op2))), + ValueType::Int(op2) => Ok(Brc::new(Value::new_bool(*op1 < op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Ok(Rc::new(Value::new_bool(*op1 < op2))), + ValueType::Float(op2) => Ok(Brc::new(Value::new_bool(*op1 < op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Ok(Rc::new(Value::new_bool(op1.less_than(op2)))), + ValueType::List(op2) => Ok(Brc::new(Value::new_bool(op1.less_than(op2)))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), @@ -482,24 +483,24 @@ impl NativeFunctionCall { fn greater_than_or_equals_op( &self, - params: &[Rc], - ) -> Result, StoryError> { + params: &[Brc], + ) -> Result, StoryError> { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Ok(Rc::new(Value::new_bool(*op1 >= op2))), + ValueType::Int(op2) => Ok(Brc::new(Value::new_bool(*op1 >= op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Ok(Rc::new(Value::new_bool(*op1 >= op2))), + ValueType::Float(op2) => Ok(Brc::new(Value::new_bool(*op1 >= op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::List(op1) => match ¶ms[1].value { ValueType::List(op2) => { - Ok(Rc::new(Value::new_bool(op1.greater_than_or_equals(op2)))) + Ok(Brc::new(Value::new_bool(op1.greater_than_or_equals(op2)))) } _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), @@ -511,22 +512,25 @@ impl NativeFunctionCall { } } - fn less_than_or_equals_op(&self, params: &[Rc]) -> Result, StoryError> { + fn less_than_or_equals_op( + &self, + params: &[Brc], + ) -> Result, StoryError> { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Ok(Rc::new(Value::new_bool(*op1 <= op2))), + ValueType::Int(op2) => Ok(Brc::new(Value::new_bool(*op1 <= op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Ok(Rc::new(Value::new_bool(*op1 <= op2))), + ValueType::Float(op2) => Ok(Brc::new(Value::new_bool(*op1 <= op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Ok(Rc::new(Value::new_bool(op1.less_than_or_equals(op2)))), + ValueType::List(op2) => Ok(Brc::new(Value::new_bool(op1.less_than_or_equals(op2)))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), @@ -537,22 +541,22 @@ impl NativeFunctionCall { } } - fn subtract_op(&self, params: &[Rc]) -> Result, StoryError> { + fn subtract_op(&self, params: &[Brc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Ok(Rc::new(Value::new_int(*op1 - op2))), + ValueType::Int(op2) => Ok(Brc::new(Value::new_int(*op1 - op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Ok(Rc::new(Value::new_float(*op1 - op2))), + ValueType::Float(op2) => Ok(Brc::new(Value::new_float(*op1 - op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Ok(Rc::new(Value::new_list(op1.without(op2)))), + ValueType::List(op2) => Ok(Brc::new(Value::new_list(op1.without(op2)))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), @@ -563,16 +567,16 @@ impl NativeFunctionCall { } } - fn add_op(&self, params: &[Rc]) -> Result, StoryError> { + fn add_op(&self, params: &[Brc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Ok(Rc::new(Value::new_int(op1 + op2))), + ValueType::Int(op2) => Ok(Brc::new(Value::new_int(op1 + op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Ok(Rc::new(Value::new_float(op1 + op2))), + ValueType::Float(op2) => Ok(Brc::new(Value::new_float(op1 + op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), @@ -582,14 +586,14 @@ impl NativeFunctionCall { let mut sb = String::new(); sb.push_str(&op1.string); sb.push_str(&op2.string); - Ok(Rc::new(Value::new_string(&sb))) + Ok(Brc::new(Value::new_string(&sb))) } _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Ok(Rc::new(Value::new_list(op1.union(op2)))), + ValueType::List(op2) => Ok(Brc::new(Value::new_list(op1.union(op2)))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), @@ -600,16 +604,16 @@ impl NativeFunctionCall { } } - fn divide_op(&self, params: &[Rc]) -> Result, StoryError> { + fn divide_op(&self, params: &[Brc]) -> Result, StoryError> { match params[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Ok(Rc::new(Value::new_int(op1 / op2))), + ValueType::Int(op2) => Ok(Brc::new(Value::new_int(op1 / op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Ok(Rc::new(Value::new_float(op1 / op2))), + ValueType::Float(op2) => Ok(Brc::new(Value::new_float(op1 / op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), @@ -620,16 +624,18 @@ impl NativeFunctionCall { } } - fn pow_op(&self, params: &[Rc]) -> Result, StoryError> { + fn pow_op(&self, params: &[Brc]) -> Result, StoryError> { match params[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Ok(Rc::new(Value::new_float((op1 as f32).powf(op2 as f32)))), + ValueType::Int(op2) => { + Ok(Brc::new(Value::new_float((op1 as f32).powf(op2 as f32)))) + } _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Ok(Rc::new(Value::new_float(op1.powf(op2)))), + ValueType::Float(op2) => Ok(Brc::new(Value::new_float(op1.powf(op2)))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), @@ -640,16 +646,16 @@ impl NativeFunctionCall { } } - fn multiply_op(&self, params: &[Rc]) -> Result, StoryError> { + fn multiply_op(&self, params: &[Brc]) -> Result, StoryError> { match params[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Ok(Rc::new(Value::new_int(op1 * op2))), + ValueType::Int(op2) => Ok(Brc::new(Value::new_int(op1 * op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Ok(Rc::new(Value::new_float(op1 * op2))), + ValueType::Float(op2) => Ok(Brc::new(Value::new_float(op1 * op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), @@ -660,28 +666,28 @@ impl NativeFunctionCall { } } - fn or_op(&self, params: &[Rc]) -> Result, StoryError> { + fn or_op(&self, params: &[Brc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Bool(op1) => match params[1].value { - ValueType::Bool(op2) => Ok(Rc::new(Value::new_bool(*op1 || op2))), + ValueType::Bool(op2) => Ok(Brc::new(Value::new_bool(*op1 || op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Ok(Rc::new(Value::new_bool(*op1 != 0 || op2 != 0))), + ValueType::Int(op2) => Ok(Brc::new(Value::new_bool(*op1 != 0 || op2 != 0))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Ok(Rc::new(Value::new_bool(*op1 != 0.0 || op2 != 0.0))), + ValueType::Float(op2) => Ok(Brc::new(Value::new_bool(*op1 != 0.0 || op2 != 0.0))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Ok(Rc::new(Value::new_bool( + ValueType::List(op2) => Ok(Brc::new(Value::new_bool( !op1.items.is_empty() || !op2.items.is_empty(), ))), _ => Err(StoryError::InvalidStoryState( @@ -694,11 +700,11 @@ impl NativeFunctionCall { } } - fn not_op(&self, params: &[Rc]) -> Result, StoryError> { + fn not_op(&self, params: &[Brc]) -> Result, StoryError> { match ¶ms[0].value { - ValueType::Int(op1) => Ok(Rc::new(Value::new_bool(*op1 == 0))), - ValueType::Float(op1) => Ok(Rc::new(Value::new_bool(*op1 == 0.0))), - ValueType::List(op1) => Ok(Rc::new(Value::new_int(match op1.items.is_empty() { + ValueType::Int(op1) => Ok(Brc::new(Value::new_bool(*op1 == 0))), + ValueType::Float(op1) => Ok(Brc::new(Value::new_bool(*op1 == 0.0))), + ValueType::List(op1) => Ok(Brc::new(Value::new_int(match op1.items.is_empty() { true => 1, false => 0, }))), @@ -708,16 +714,16 @@ impl NativeFunctionCall { } } - fn min_op(&self, params: &[Rc]) -> Result, StoryError> { + fn min_op(&self, params: &[Brc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Ok(Rc::new(Value::new_int(i32::min(*op1, op2)))), + ValueType::Int(op2) => Ok(Brc::new(Value::new_int(i32::min(*op1, op2)))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Ok(Rc::new(Value::new_float(f32::min(*op1, op2)))), + ValueType::Float(op2) => Ok(Brc::new(Value::new_float(f32::min(*op1, op2)))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), @@ -728,16 +734,16 @@ impl NativeFunctionCall { } } - fn max_op(&self, params: &[Rc]) -> Result, StoryError> { + fn max_op(&self, params: &[Brc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Ok(Rc::new(Value::new_int(i32::max(*op1, op2)))), + ValueType::Int(op2) => Ok(Brc::new(Value::new_int(i32::max(*op1, op2)))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Ok(Rc::new(Value::new_float(f32::max(*op1, op2)))), + ValueType::Float(op2) => Ok(Brc::new(Value::new_float(f32::max(*op1, op2)))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), @@ -748,40 +754,40 @@ impl NativeFunctionCall { } } - fn equal_op(&self, params: &[Rc]) -> Result, StoryError> { + fn equal_op(&self, params: &[Brc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Bool(op1) => match params[1].value { - ValueType::Bool(op2) => Ok(Rc::new(Value::new_bool(*op1 == op2))), + ValueType::Bool(op2) => Ok(Brc::new(Value::new_bool(*op1 == op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Ok(Rc::new(Value::new_bool(*op1 == op2))), + ValueType::Int(op2) => Ok(Brc::new(Value::new_bool(*op1 == op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Ok(Rc::new(Value::new_bool(*op1 == op2))), + ValueType::Float(op2) => Ok(Brc::new(Value::new_bool(*op1 == op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::String(op1) => match ¶ms[1].value { - ValueType::String(op2) => Ok(Rc::new(Value::new_bool(op1.string.eq(&op2.string)))), + ValueType::String(op2) => Ok(Brc::new(Value::new_bool(op1.string.eq(&op2.string)))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Ok(Rc::new(Value::new_bool(op1.eq(op2)))), + ValueType::List(op2) => Ok(Brc::new(Value::new_bool(op1.eq(op2)))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::DivertTarget(op1) => match ¶ms[1].value { - ValueType::DivertTarget(op2) => Ok(Rc::new(Value::new_bool(op1.eq(op2)))), + ValueType::DivertTarget(op2) => Ok(Brc::new(Value::new_bool(op1.eq(op2)))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), @@ -792,40 +798,42 @@ impl NativeFunctionCall { } } - fn not_equals_op(&self, params: &[Rc]) -> Result, StoryError> { + fn not_equals_op(&self, params: &[Brc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::Bool(op1) => match params[1].value { - ValueType::Bool(op2) => Ok(Rc::new(Value::new_bool(*op1 != op2))), + ValueType::Bool(op2) => Ok(Brc::new(Value::new_bool(*op1 != op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Ok(Rc::new(Value::new_bool(*op1 != op2))), + ValueType::Int(op2) => Ok(Brc::new(Value::new_bool(*op1 != op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Ok(Rc::new(Value::new_bool(*op1 != op2))), + ValueType::Float(op2) => Ok(Brc::new(Value::new_bool(*op1 != op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::String(op1) => match ¶ms[1].value { - ValueType::String(op2) => Ok(Rc::new(Value::new_bool(!op1.string.eq(&op2.string)))), + ValueType::String(op2) => { + Ok(Brc::new(Value::new_bool(!op1.string.eq(&op2.string)))) + } _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Ok(Rc::new(Value::new_bool(!op1.eq(op2)))), + ValueType::List(op2) => Ok(Brc::new(Value::new_bool(!op1.eq(op2)))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::DivertTarget(op1) => match ¶ms[1].value { - ValueType::DivertTarget(op2) => Ok(Rc::new(Value::new_bool(!op1.eq(op2)))), + ValueType::DivertTarget(op2) => Ok(Brc::new(Value::new_bool(!op1.eq(op2)))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), @@ -836,16 +844,16 @@ impl NativeFunctionCall { } } - fn mod_op(&self, params: &[Rc]) -> Result, StoryError> { + fn mod_op(&self, params: &[Brc]) -> Result, StoryError> { match params[0].value { ValueType::Int(op1) => match params[1].value { - ValueType::Int(op2) => Ok(Rc::new(Value::new_int(op1 % op2))), + ValueType::Int(op2) => Ok(Brc::new(Value::new_int(op1 % op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::Float(op1) => match params[1].value { - ValueType::Float(op2) => Ok(Rc::new(Value::new_float(op1 % op2))), + ValueType::Float(op2) => Ok(Brc::new(Value::new_float(op1 % op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), @@ -856,10 +864,10 @@ impl NativeFunctionCall { } } - fn intersect_op(&self, params: &[Rc]) -> Result, StoryError> { + fn intersect_op(&self, params: &[Brc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Ok(Rc::new(Value::new_list(op1.intersect(op2)))), + ValueType::List(op2) => Ok(Brc::new(Value::new_list(op1.intersect(op2)))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), @@ -870,18 +878,18 @@ impl NativeFunctionCall { } } - fn has(&self, params: &[Rc]) -> Result, StoryError> { + fn has(&self, params: &[Brc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::String(op1) => match ¶ms[1].value { ValueType::String(op2) => { - Ok(Rc::new(Value::new_bool(op1.string.contains(&op2.string)))) + Ok(Brc::new(Value::new_bool(op1.string.contains(&op2.string)))) } _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Ok(Rc::new(Value::new_bool(op1.contains(op2)))), + ValueType::List(op2) => Ok(Brc::new(Value::new_bool(op1.contains(op2)))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), @@ -892,18 +900,18 @@ impl NativeFunctionCall { } } - fn hasnt(&self, params: &[Rc]) -> Result, StoryError> { + fn hasnt(&self, params: &[Brc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::String(op1) => match ¶ms[1].value { ValueType::String(op2) => { - Ok(Rc::new(Value::new_bool(!op1.string.contains(&op2.string)))) + Ok(Brc::new(Value::new_bool(!op1.string.contains(&op2.string)))) } _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), }, ValueType::List(op1) => match ¶ms[1].value { - ValueType::List(op2) => Ok(Rc::new(Value::new_bool(!op1.contains(op2)))), + ValueType::List(op2) => Ok(Brc::new(Value::new_bool(!op1.contains(op2)))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), @@ -914,11 +922,11 @@ impl NativeFunctionCall { } } - fn value_of_list_op(&self, params: &[Rc]) -> Result, StoryError> { + fn value_of_list_op(&self, params: &[Brc]) -> Result, StoryError> { match ¶ms[0].value { ValueType::List(op1) => match op1.get_max_item() { - Some(i) => Ok(Rc::new(Value::new_int(i.1))), - None => Ok(Rc::new(Value::new_int(0))), + Some(i) => Ok(Brc::new(Value::new_int(i.1))), + None => Ok(Brc::new(Value::new_int(0))), }, _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), @@ -926,95 +934,95 @@ impl NativeFunctionCall { } } - fn all_op(&self, params: &[Rc]) -> Result, StoryError> { + fn all_op(&self, params: &[Brc]) -> Result, StoryError> { match ¶ms[0].value { - ValueType::List(op1) => Ok(Rc::new(Value::new_list(op1.get_all()))), + ValueType::List(op1) => Ok(Brc::new(Value::new_list(op1.get_all()))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), } } - fn inverse_op(&self, params: &[Rc]) -> Result, StoryError> { + fn inverse_op(&self, params: &[Brc]) -> Result, StoryError> { match ¶ms[0].value { - ValueType::List(op1) => Ok(Rc::new(Value::new_list(op1.inverse()))), + ValueType::List(op1) => Ok(Brc::new(Value::new_list(op1.inverse()))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), } } - fn count_op(&self, params: &[Rc]) -> Result, StoryError> { + fn count_op(&self, params: &[Brc]) -> Result, StoryError> { match ¶ms[0].value { - ValueType::List(op1) => Ok(Rc::new(Value::new_int(op1.items.len() as i32))), + ValueType::List(op1) => Ok(Brc::new(Value::new_int(op1.items.len() as i32))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), } } - fn list_max_op(&self, params: &[Rc]) -> Result, StoryError> { + fn list_max_op(&self, params: &[Brc]) -> Result, StoryError> { match ¶ms[0].value { - ValueType::List(op1) => Ok(Rc::new(Value::new_list(op1.max_as_list()))), + ValueType::List(op1) => Ok(Brc::new(Value::new_list(op1.max_as_list()))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), } } - fn list_min_op(&self, params: &[Rc]) -> Result, StoryError> { + fn list_min_op(&self, params: &[Brc]) -> Result, StoryError> { match ¶ms[0].value { - ValueType::List(op1) => Ok(Rc::new(Value::new_list(op1.min_as_list()))), + ValueType::List(op1) => Ok(Brc::new(Value::new_list(op1.min_as_list()))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), } } - fn negate_op(&self, params: &[Rc]) -> Result, StoryError> { + fn negate_op(&self, params: &[Brc]) -> Result, StoryError> { match ¶ms[0].value { - ValueType::Int(op1) => Ok(Rc::new(Value::new_int(-op1))), - ValueType::Float(op1) => Ok(Rc::new(Value::new_float(-op1))), + ValueType::Int(op1) => Ok(Brc::new(Value::new_int(-op1))), + ValueType::Float(op1) => Ok(Brc::new(Value::new_float(-op1))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), } } - fn floor_op(&self, params: &[Rc]) -> Result, StoryError> { + fn floor_op(&self, params: &[Brc]) -> Result, StoryError> { match ¶ms[0].value { - ValueType::Int(op1) => Ok(Rc::new(Value::new_int(*op1))), - ValueType::Float(op1) => Ok(Rc::new(Value::new_float(op1.floor()))), + ValueType::Int(op1) => Ok(Brc::new(Value::new_int(*op1))), + ValueType::Float(op1) => Ok(Brc::new(Value::new_float(op1.floor()))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), } } - fn ceiling_op(&self, params: &[Rc]) -> Result, StoryError> { + fn ceiling_op(&self, params: &[Brc]) -> Result, StoryError> { match ¶ms[0].value { - ValueType::Int(op1) => Ok(Rc::new(Value::new_int(*op1))), - ValueType::Float(op1) => Ok(Rc::new(Value::new_float(op1.ceil()))), + ValueType::Int(op1) => Ok(Brc::new(Value::new_int(*op1))), + ValueType::Float(op1) => Ok(Brc::new(Value::new_float(op1.ceil()))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), } } - fn int_op(&self, params: &[Rc]) -> Result, StoryError> { + fn int_op(&self, params: &[Brc]) -> Result, StoryError> { match ¶ms[0].value { - ValueType::Int(op1) => Ok(Rc::new(Value::new_int(*op1))), - ValueType::Float(op1) => Ok(Rc::new(Value::new_int(*op1 as i32))), + ValueType::Int(op1) => Ok(Brc::new(Value::new_int(*op1))), + ValueType::Float(op1) => Ok(Brc::new(Value::new_int(*op1 as i32))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), } } - fn float_op(&self, params: &[Rc]) -> Result, StoryError> { + fn float_op(&self, params: &[Brc]) -> Result, StoryError> { match ¶ms[0].value { - ValueType::Int(op1) => Ok(Rc::new(Value::new_float(*op1 as f32))), - ValueType::Float(op1) => Ok(Rc::new(Value::new_float(*op1))), + ValueType::Int(op1) => Ok(Brc::new(Value::new_float(*op1 as f32))), + ValueType::Float(op1) => Ok(Brc::new(Value::new_float(*op1))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), diff --git a/lib/src/object.rs b/lib/src/object.rs index c17fa86..dd54360 100644 --- a/lib/src/object.rs +++ b/lib/src/object.rs @@ -1,9 +1,4 @@ -use std::{ - any::Any, - cell::RefCell, - fmt::Display, - rc::{Rc, Weak}, -}; +use std::{any::Any, fmt::Display, rc::Weak}; use as_any::{AsAny, Downcast}; @@ -11,19 +6,20 @@ use crate::{ container::Container, path::{Component, Path}, search_result::SearchResult, + BrCell, Brc, }; pub struct Object { - parent: RefCell>, - path: RefCell>, + parent: BrCell>, + path: BrCell>, //debug_metadata: DebugMetadata, } impl Object { pub fn new() -> Object { Object { - parent: RefCell::new(Weak::new()), - path: RefCell::new(None), + parent: BrCell::new(Weak::new()), + path: BrCell::new(None), } } @@ -31,12 +27,12 @@ impl Object { self.parent.borrow().upgrade().is_none() } - pub fn get_parent(&self) -> Option> { + pub fn get_parent(&self) -> Option> { self.parent.borrow().upgrade() } - pub fn set_parent(&self, parent: &Rc) { - self.parent.replace(Rc::downgrade(parent)); + pub fn set_parent(&self, parent: &Brc) { + self.parent.replace(Brc::downgrade(parent)); } pub fn get_path(rtobject: &dyn RTObject) -> Path { @@ -105,7 +101,7 @@ impl Object { .clone() } - pub fn resolve_path(rtobject: Rc, path: &Path) -> SearchResult { + pub fn resolve_path(rtobject: Brc, path: &Path) -> SearchResult { if path.is_relative() { let mut p = path.clone(); let mut nearest_container = rtobject.clone().into_any().downcast::().ok(); @@ -121,7 +117,7 @@ impl Object { } } - pub fn convert_path_to_relative(rtobject: &Rc, global_path: &Path) -> Path { + pub fn convert_path_to_relative(rtobject: &Brc, global_path: &Path) -> Path { // 1. Find last shared ancestor // 2. Drill up using ".." style (actually represented as "^") // 3. Re-build downward chain from common ancestor @@ -160,7 +156,7 @@ impl Object { Path::new(&new_path_comps, true) } - pub fn compact_path_string(rtobject: Rc, other_path: &Path) -> String { + pub fn compact_path_string(rtobject: Brc, other_path: &Path) -> String { let global_path_str: String; let relative_path_str: String; @@ -182,7 +178,7 @@ impl Object { } } - pub fn get_root_container(rtobject: Rc) -> Rc { + pub fn get_root_container(rtobject: Brc) -> Brc { let mut ancestor = rtobject; while let Some(p) = ancestor.get_object().get_parent() { @@ -203,12 +199,12 @@ impl Default for Object { } pub trait IntoAny: AsAny { - fn into_any(self: Rc) -> Rc; + fn into_any(self: Brc) -> Brc; } impl IntoAny for T { #[inline(always)] - fn into_any(self: Rc) -> Rc { + fn into_any(self: Brc) -> Brc { self } } diff --git a/lib/src/pointer.rs b/lib/src/pointer.rs index 6d40a25..7ad8749 100644 --- a/lib/src/pointer.rs +++ b/lib/src/pointer.rs @@ -1,25 +1,26 @@ -use std::{fmt, rc::Rc}; +use std::fmt; use crate::{ container::Container, object::RTObject, path::{Component, Path}, + Brc, }; pub const NULL: Pointer = Pointer::new(None, -1); #[derive(Clone, Default)] pub struct Pointer { - pub container: Option>, + pub container: Option>, pub index: i32, } impl Pointer { - pub const fn new(container: Option>, index: i32) -> Pointer { + pub const fn new(container: Option>, index: i32) -> Pointer { Pointer { container, index } } - pub fn resolve(&self) -> Option> { + pub fn resolve(&self) -> Option> { match &self.container { Some(container) => { if self.index < 0 || container.content.is_empty() { @@ -55,7 +56,7 @@ impl Pointer { Some(container.get_path()) } - pub fn start_of(container: Rc) -> Pointer { + pub fn start_of(container: Brc) -> Pointer { Pointer { container: Some(container), index: 0, diff --git a/lib/src/search_result.rs b/lib/src/search_result.rs index 9887e7e..44b196a 100644 --- a/lib/src/search_result.rs +++ b/lib/src/search_result.rs @@ -1,15 +1,13 @@ -use std::rc::Rc; - -use crate::{container::Container, object::RTObject}; +use crate::{container::Container, object::RTObject, Brc}; #[derive(Clone)] pub struct SearchResult { - pub obj: Rc, + pub obj: Brc, pub approximate: bool, } impl SearchResult { - pub fn new(obj: Rc, approximate: bool) -> Self { + pub fn new(obj: Brc, approximate: bool) -> Self { SearchResult { obj, approximate } } @@ -20,7 +18,7 @@ impl SearchResult { } } - pub fn correct_obj(&self) -> Option> { + pub fn correct_obj(&self) -> Option> { if self.approximate { None } else { @@ -28,7 +26,7 @@ impl SearchResult { } } - pub fn container(&self) -> Option> { + pub fn container(&self) -> Option> { let c = self.obj.clone().into_any().downcast::(); match c { diff --git a/lib/src/state_patch.rs b/lib/src/state_patch.rs index 36f5671..eaed3e1 100644 --- a/lib/src/state_patch.rs +++ b/lib/src/state_patch.rs @@ -1,13 +1,10 @@ -use std::{ - collections::{HashMap, HashSet}, - rc::Rc, -}; +use std::collections::{HashMap, HashSet}; -use crate::{container::Container, object::Object, value::Value}; +use crate::{container::Container, object::Object, value::Value, Brc}; #[derive(Clone)] pub struct StatePatch { - pub globals: HashMap>, + pub globals: HashMap>, pub changed_variables: HashSet, pub visit_counts: HashMap, pub turn_indices: HashMap, @@ -31,21 +28,21 @@ impl StatePatch { } } - pub fn get_visit_count(&self, container: &Rc) -> Option { + pub fn get_visit_count(&self, container: &Brc) -> Option { let key = Object::get_path(container.as_ref()).to_string(); self.visit_counts.get(&key).copied() } - pub fn set_visit_count(&mut self, container: &Rc, count: i32) { + pub fn set_visit_count(&mut self, container: &Brc, count: i32) { let key = Object::get_path(container.as_ref()).to_string(); self.visit_counts.insert(key, count); } - pub fn get_global(&self, name: &str) -> Option> { + pub fn get_global(&self, name: &str) -> Option> { self.globals.get(name).cloned() } - pub fn set_global(&mut self, name: &str, value: Rc) { + pub fn set_global(&mut self, name: &str, value: Brc) { self.globals.insert(name.to_string(), value); } diff --git a/lib/src/story.rs b/lib/src/story.rs index fbfef53..8670a7a 100644 --- a/lib/src/story.rs +++ b/lib/src/story.rs @@ -1,8 +1,6 @@ //! [`Story`] is the entry point to load and run an Ink story. use std::{ - cell::RefCell, collections::{HashMap, VecDeque}, - rc::Rc, time::Instant, }; @@ -33,6 +31,7 @@ use crate::{ variable_assigment::VariableAssignment, variable_reference::VariableReference, void::Void, + BrCell, Brc, }; /// The current version of the Ink story file format. @@ -50,17 +49,17 @@ enum OutputStateChange { /// A `Story` is the core struct representing a complete Ink narrative, /// managing evaluation and state. pub struct Story { - main_content_container: Rc, + main_content_container: Brc, state: StoryState, - temporary_evaluation_container: Option>, + temporary_evaluation_container: Option>, recursive_continue_count: usize, async_continue_active: bool, async_saving: bool, - prev_containers: Vec>, - list_definitions: Rc, - pub(crate) on_error: Option>>, + prev_containers: Vec>, + list_definitions: Brc, + pub(crate) on_error: Option>>, pub(crate) state_snapshot_at_last_new_line: Option, - pub(crate) variable_observers: HashMap>>>, + pub(crate) variable_observers: HashMap>>>, pub(crate) has_validated_externals: bool, pub(crate) allow_external_function_fallbacks: bool, pub(crate) saw_lookahead_unsafe_function_after_new_line: bool, @@ -103,7 +102,7 @@ impl Story { }; let list_definitions = match json.get("listDefs") { - Some(def) => Rc::new(json_read::jtoken_to_list_definitions(def)?), + Some(def) => Brc::new(json_read::jtoken_to_list_definitions(def)?), None => { return Err( StoryError::BadJson("List Definitions node for ink not found. Are you sure it's a valid .ink.json file?" @@ -524,7 +523,7 @@ impl Story { Ok(self.get_state_mut().get_current_text()) } - pub(crate) fn get_main_content_container(&self) -> Rc { + pub(crate) fn get_main_content_container(&self) -> Brc { match self.temporary_evaluation_container.as_ref() { Some(c) => c.clone(), None => self.main_content_container.clone(), @@ -682,7 +681,7 @@ impl Story { .get_callstack() .borrow() .context_for_variable_named(&var_pointer.variable_name); - current_content_obj = Some(Rc::new(Value::new_variable_pointer( + current_content_obj = Some(Brc::new(Value::new_variable_pointer( &var_pointer.variable_name, context_idx as i32, ))); @@ -733,7 +732,7 @@ impl Story { // Is a default invisible choice the ONLY choice? // var invisibleChoices = allChoices.Where (c => // c.choicePoint.isInvisibleDefault).ToList(); - let mut invisible_choices: Vec> = Vec::new(); + let mut invisible_choices: Vec> = Vec::new(); for c in all_choices { if c.is_invisible_default { invisible_choices.push(c.clone()); @@ -837,7 +836,7 @@ impl Story { self.state_snapshot_at_last_new_line = None; } - fn visit_container(&mut self, container: &Rc, at_start: bool) { + fn visit_container(&mut self, container: &Brc, at_start: bool) { if !container.counting_at_start_only || at_start { if container.visits_should_be_counted { self.get_state_mut() @@ -853,7 +852,7 @@ impl Story { fn perform_logic_and_flow_control( &mut self, - content_obj: &Option>, + content_obj: &Option>, ) -> Result { let content_obj = match content_obj { Some(content_obj) => content_obj.clone(), @@ -957,8 +956,8 @@ impl Story { // the // only problem is when exporting text for viewing, it // skips over numbers etc. - let text: Rc = - Rc::new(Value::new_string(&output.to_string())); + let text: Brc = + Brc::new(Value::new_string(&output.to_string())); self.get_state_mut().push_to_output_stream(text); } @@ -1078,8 +1077,8 @@ impl Story { // Since we're iterating backward through the content, // build a stack so that when we build the string, // it's in the right order - let mut content_stack_for_string: VecDeque> = VecDeque::new(); - let mut content_to_retain: VecDeque> = VecDeque::new(); + let mut content_stack_for_string: VecDeque> = VecDeque::new(); + let mut content_to_retain: VecDeque> = VecDeque::new(); let mut output_count_consumed = 0; @@ -1127,18 +1126,18 @@ impl Story { // Return to expression evaluation (from content mode) self.get_state().set_in_expression_evaluation(true); self.get_state_mut() - .push_evaluation_stack(Rc::new(Value::new_string(&sb))); + .push_evaluation_stack(Brc::new(Value::new_string(&sb))); } CommandType::NoOp => {} CommandType::ChoiceCount => { let choice_count = self.get_state().get_generated_choices().len(); self.get_state_mut() - .push_evaluation_stack(Rc::new(Value::new_int(choice_count as i32))); + .push_evaluation_stack(Brc::new(Value::new_int(choice_count as i32))); } CommandType::Turns => { let current_turn = self.get_state().current_turn_index; self.get_state_mut() - .push_evaluation_stack(Rc::new(Value::new_int(current_turn + 1))); + .push_evaluation_stack(Brc::new(Value::new_int(current_turn + 1))); } CommandType::TurnsSince | CommandType::ReadCount => { let target = self.get_state_mut().pop_evaluation_stack(); @@ -1192,7 +1191,7 @@ impl Story { } self.get_state_mut() - .push_evaluation_stack(Rc::new(Value::new_int(either_count))); + .push_evaluation_stack(Brc::new(Value::new_int(either_count))); } CommandType::Random => { let mut max_int = None; @@ -1244,7 +1243,7 @@ impl Story { let chosen_value = (next_random % random_range as u32) as i32 + min_value; self.get_state_mut() - .push_evaluation_stack(Rc::new(Value::new_int(chosen_value))); + .push_evaluation_stack(Brc::new(Value::new_int(chosen_value))); self.get_state_mut().previous_random = self.get_state().previous_random + 1; } @@ -1269,18 +1268,18 @@ impl Story { // SEED_RANDOM returns nothing. self.get_state_mut() - .push_evaluation_stack(Rc::new(Void::new())); + .push_evaluation_stack(Brc::new(Void::new())); } CommandType::VisitIndex => { let cpc = self.get_state().get_current_pointer().container.unwrap(); let count = self.get_state_mut().visit_count_for_container(&cpc) - 1; // index // not count self.get_state_mut() - .push_evaluation_stack(Rc::new(Value::new_int(count))); + .push_evaluation_stack(Brc::new(Value::new_int(count))); } CommandType::SequenceShuffleIndex => { let shuffle_index = self.next_sequence_shuffle_index()?; - let v = Rc::new(Value::new_int(shuffle_index)); + let v = Brc::new(Value::new_int(shuffle_index)); self.get_state_mut().push_evaluation_stack(v); } CommandType::StartThread => { @@ -1354,7 +1353,7 @@ impl Story { } self.get_state_mut() - .push_evaluation_stack(Rc::new(generated_list_value.unwrap())); + .push_evaluation_stack(Brc::new(generated_list_value.unwrap())); } CommandType::ListRange => { let mut p = self.get_state_mut().pop_evaluation_stack(); @@ -1377,7 +1376,7 @@ impl Story { .list_with_sub_range(&min.unwrap().value, &max.unwrap().value); self.get_state_mut() - .push_evaluation_stack(Rc::new(Value::new_list(result))); + .push_evaluation_stack(Brc::new(Value::new_list(result))); } CommandType::ListRandom => { let o = self.get_state_mut().pop_evaluation_stack(); @@ -1424,7 +1423,7 @@ impl Story { }; self.get_state_mut() - .push_evaluation_stack(Rc::new(Value::new_list(new_list))); + .push_evaluation_stack(Brc::new(Value::new_list(new_list))); } CommandType::BeginTag => self .get_state_mut() @@ -1491,7 +1490,7 @@ impl Story { } let choice_tag = - Rc::new(Tag::new(&StoryState::clean_output_whitespace(&sb))); + Brc::new(Tag::new(&StoryState::clean_output_whitespace(&sb))); // Pushing to the evaluation stack means it gets picked up // when a Choice is generated from the next Choice Point. self.get_state_mut().push_evaluation_stack(choice_tag); @@ -1535,7 +1534,7 @@ impl Story { .into_any() .downcast::() { - let found_value: Rc; + let found_value: Brc; // Explicit read count value if var_ref.path_for_count.is_some() { @@ -1543,7 +1542,7 @@ impl Story { let count = self .get_state_mut() .visit_count_for_container(container.as_ref().unwrap()); - found_value = Rc::new(Value::new_int(count)); + found_value = Brc::new(Value::new_int(count)); } // Normal variable reference else { @@ -1556,7 +1555,7 @@ impl Story { None => { self.add_error(&format!("Variable not found: '{}'. 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.", var_ref.name), true); - found_value = Rc::new(Value::new_int(0)); + found_value = Brc::new(Value::new_int(0)); } } } @@ -1637,7 +1636,7 @@ impl Story { // something to chomp on if it needs it if self.get_state().get_in_expression_evaluation() { self.get_state_mut() - .push_evaluation_stack(Rc::new(Void::new())); + .push_evaluation_stack(Brc::new(Void::new())); } did_pop = true; @@ -1696,13 +1695,13 @@ impl Story { break; } - let rto: Rc = container; + let rto: Brc = container; let index_in_ancestor = next_ancestor .as_ref() .unwrap() .content .iter() - .position(|s| Rc::ptr_eq(s, &rto)); + .position(|s| Brc::ptr_eq(s, &rto)); if index_in_ancestor.is_none() { break; } @@ -1735,7 +1734,7 @@ impl Story { /// through with the [`cont`](Story::cont) method. Once [`can_continue`](Story::can_continue) becomes /// `false`, this vector will be populated, and is usually /// (but not always) on the final [`cont`](Story::cont) step. - pub fn get_current_choices(&self) -> Vec> { + pub fn get_current_choices(&self) -> Vec> { // Don't include invisible choices for external usage. let mut choices = Vec::new(); @@ -1804,7 +1803,7 @@ impl Story { Ok(()) } - fn is_truthy(&self, obj: Rc) -> Result { + fn is_truthy(&self, obj: Brc) -> Result { let truthy = false; if let Some(val) = obj.as_ref().as_any().downcast_ref::() { @@ -1820,8 +1819,8 @@ impl Story { fn process_choice( &mut self, - choice_point: &Rc, - ) -> Result>, StoryError> { + choice_point: &Brc, + ) -> Result>, StoryError> { let mut show_choice = true; // Don't create choice if choice point doesn't pass conditional @@ -1863,7 +1862,7 @@ impl Story { start_text.push_str(&choice_only_text); - let choice = Rc::new(Choice::new( + let choice = Brc::new(Choice::new( choice_point.get_path_on_choice(), Object::get_path(choice_point.as_ref()).to_string(), choice_point.is_invisible_default(), @@ -1900,7 +1899,7 @@ impl Story { } pub(crate) fn pointer_at_path( - main_content_container: &Rc, + main_content_container: &Brc, path: &Path, ) -> Result { if path.len() == 0 { @@ -1930,9 +1929,9 @@ impl Story { result }; - let main_container: Rc = main_content_container.clone(); + let main_container: Brc = main_content_container.clone(); - if Rc::ptr_eq(&result.obj, &main_container) && path_length_to_use > 0 { + if Brc::ptr_eq(&result.obj, &main_container) && path_length_to_use > 0 { return Err(StoryError::InvalidStoryState(format!( "Failed to find content at path '{}', and no approximation of it was possible.", path @@ -1996,7 +1995,7 @@ impl Story { if !self .prev_containers .iter() - .any(|e| Rc::ptr_eq(e, ¤t_container)) + .any(|e| Brc::ptr_eq(e, ¤t_container)) || current_container.counting_at_start_only { // Check whether this ancestor container is being entered at the start, @@ -2005,7 +2004,7 @@ impl Story { .content .first() .map(|first_child| { - Rc::ptr_eq(first_child, ¤t_child_of_container) + Brc::ptr_eq(first_child, ¤t_child_of_container) && all_children_entered_at_start }) .unwrap_or(false); @@ -2081,7 +2080,7 @@ impl Story { .complete_function_evaluation_from_game() } - pub(crate) fn knot_container_with_name(&self, name: &str) -> Option> { + pub(crate) fn knot_container_with_name(&self, name: &str) -> Option> { let named_container = self.main_content_container.named_content.get(name); named_container.cloned() diff --git a/lib/src/story_callbacks.rs b/lib/src/story_callbacks.rs index 711547e..71f819b 100644 --- a/lib/src/story_callbacks.rs +++ b/lib/src/story_callbacks.rs @@ -1,10 +1,10 @@ //! For setting the callbacks functions that will be called while the [`Story`] is processing. -use std::{cell::RefCell, collections::HashSet, rc::Rc}; +use std::collections::HashSet; use crate::{ container::Container, divert::Divert, object::RTObject, pointer::Pointer, push_pop::PushPopType, story::Story, story_error::StoryError, value::Value, - value_type::ValueType, void::Void, + value_type::ValueType, void::Void, BrCell, Brc, }; /// Defines the method that will be called when an observed global variable changes. @@ -18,7 +18,7 @@ pub trait ExternalFunction { } pub(crate) struct ExternalFunctionDef { - function: Rc>, + function: Brc>, lookahead_safe: bool, } @@ -43,7 +43,7 @@ impl Story { /// the story. /// It's strongly recommended that you assign an error handler to your /// story instance, to avoid getting panics for ink errors. - pub fn set_error_handler(&mut self, err_handler: Rc>) { + pub fn set_error_handler(&mut self, err_handler: Brc>) { self.on_error = Some(err_handler); } @@ -58,7 +58,7 @@ impl Story { pub fn observe_variable( &mut self, variable_name: &str, - observer: Rc>, + observer: Brc>, ) -> Result<(), StoryError> { self.if_async_we_cant("observe a new variable")?; @@ -76,7 +76,7 @@ impl Story { v.push(observer); } None => { - let v: Vec>> = vec![observer]; + let v: Vec>> = vec![observer]; self.variable_observers.insert(variable_name.to_string(), v); } } @@ -90,7 +90,7 @@ impl Story { /// from all variables that it's subscribed to. pub fn remove_variable_observer( &mut self, - observer: &Rc>, + observer: &Brc>, specific_variable_name: Option<&str>, ) -> Result<(), StoryError> { self.if_async_we_cant("remove a variable observer")?; @@ -99,7 +99,7 @@ impl Story { match specific_variable_name { Some(specific_variable_name) => { if let Some(v) = self.variable_observers.get_mut(specific_variable_name) { - let index = v.iter().position(|x| Rc::ptr_eq(x, observer)).unwrap(); + let index = v.iter().position(|x| Brc::ptr_eq(x, observer)).unwrap(); v.remove(index); if v.is_empty() { @@ -112,7 +112,7 @@ impl Story { let mut keys_to_remove = Vec::new(); for (k, v) in self.variable_observers.iter_mut() { - let index = v.iter().position(|x| Rc::ptr_eq(x, observer)).unwrap(); + let index = v.iter().position(|x| Brc::ptr_eq(x, observer)).unwrap(); v.remove(index); if v.is_empty() { @@ -158,7 +158,7 @@ impl Story { pub fn bind_external_function( &mut self, func_name: &str, - function: Rc>, + function: Brc>, lookahead_safe: bool, ) -> Result<(), StoryError> { self.if_async_we_cant("bind an external function")?; @@ -264,9 +264,9 @@ impl Story { .call(func_name, arguments); // Convert return value (if any) to a type that the ink engine can use - let return_obj: Rc = match func_result { - Some(func_result) => Rc::new(Value::new(func_result)), - None => Rc::new(Void::new()), + let return_obj: Brc = match func_result { + Some(func_result) => Brc::new(Value::new(func_result)), + None => Brc::new(Void::new()), }; self.get_state_mut().push_evaluation_stack(return_obj); @@ -309,7 +309,7 @@ impl Story { fn validate_external_bindings_container( &self, - c: &Rc, + c: &Brc, missing_externals: &mut std::collections::HashSet, ) -> Result<(), StoryError> { for inner_content in c.content.iter() { @@ -344,7 +344,7 @@ impl Story { fn validate_external_bindings_rtobject( &self, - o: &Rc, + o: &Brc, missing_externals: &mut std::collections::HashSet, ) -> Result<(), StoryError> { let divert = o.clone().into_any().downcast::().ok(); diff --git a/lib/src/story_state.rs b/lib/src/story_state.rs index 748c8ca..ab95f7f 100644 --- a/lib/src/story_state.rs +++ b/lib/src/story_state.rs @@ -1,4 +1,4 @@ -use std::{cell::RefCell, collections::HashMap, rc::Rc}; +use std::collections::HashMap; use crate::{ callstack::CallStack, @@ -21,6 +21,7 @@ use crate::{ value_type::ValueType, variables_state::VariablesState, void::Void, + BrCell, Brc, }; use rand::Rng; @@ -38,8 +39,8 @@ pub(crate) struct StoryState { output_stream_tags_dirty: bool, pub variables_state: VariablesState, alive_flow_names_dirty: bool, - pub evaluation_stack: Vec>, - main_content_container: Rc, + pub evaluation_stack: Vec>, + main_content_container: Brc, current_errors: Vec, current_warnings: Vec, current_text: Option, @@ -52,13 +53,13 @@ pub(crate) struct StoryState { pub story_seed: i32, pub previous_random: i32, current_tags: Vec, - list_definitions: Rc, + list_definitions: Brc, } impl StoryState { pub fn new( - main_content_container: Rc, - list_definitions: Rc, + main_content_container: Brc, + list_definitions: Brc, ) -> StoryState { let current_flow = Flow::new(DEFAULT_FLOW_NAME, main_content_container.clone()); let callstack = current_flow.callstack.clone(); @@ -111,7 +112,7 @@ impl StoryState { .clone() } - pub fn get_callstack(&self) -> &Rc> { + pub fn get_callstack(&self) -> &Brc> { &self.current_flow.callstack } @@ -119,7 +120,7 @@ impl StoryState { self.did_safe_exit = did_safe_exit; } - pub fn reset_output(&mut self, objs: Option>>) { + pub fn reset_output(&mut self, objs: Option>>) { self.get_output_stream_mut().clear(); if let Some(objs) = objs { for o in objs { @@ -129,11 +130,11 @@ impl StoryState { self.output_stream_dirty(); } - pub fn get_generated_choices_mut(&mut self) -> &mut Vec> { + pub fn get_generated_choices_mut(&mut self) -> &mut Vec> { &mut self.current_flow.current_choices } - pub fn get_generated_choices(&self) -> &Vec> { + pub fn get_generated_choices(&self) -> &Vec> { &self.current_flow.current_choices } @@ -153,11 +154,11 @@ impl StoryState { &self.current_warnings } - pub fn get_output_stream(&self) -> &Vec> { + pub fn get_output_stream(&self) -> &Vec> { &self.current_flow.output_stream } - fn get_output_stream_mut(&mut self) -> &mut Vec> { + fn get_output_stream_mut(&mut self) -> &mut Vec> { &mut self.current_flow.output_stream } @@ -348,7 +349,7 @@ impl StoryState { .in_expression_evaluation = value; } - pub fn push_evaluation_stack(&mut self, obj: Rc) { + pub fn push_evaluation_stack(&mut self, obj: Brc) { if let Some(list) = Value::get_list_value(obj.as_ref()) { let origin_names = list.get_origin_names(); @@ -365,7 +366,7 @@ impl StoryState { self.evaluation_stack.push(obj); } - pub fn push_to_output_stream(&mut self, obj: Rc) { + pub fn push_to_output_stream(&mut self, obj: Brc) { let text = { let obj = obj.clone(); match obj.into_any().downcast::() { @@ -382,7 +383,7 @@ impl StoryState { if let Some(list_text) = list_text { for text_obj in list_text { - self.push_to_output_stream_individual(Rc::new(text_obj)); + self.push_to_output_stream_individual(Brc::new(text_obj)); } self.output_stream_dirty(); return; @@ -392,7 +393,7 @@ impl StoryState { self.push_to_output_stream_individual(obj); } - pub fn increment_visit_count_for_container(&mut self, container: &Rc) { + pub fn increment_visit_count_for_container(&mut self, container: &Brc) { let has_patch = self.patch.is_some(); if has_patch { @@ -415,7 +416,7 @@ impl StoryState { } } - pub fn visit_count_for_container(&mut self, container: &Rc) -> i32 { + pub fn visit_count_for_container(&mut self, container: &Brc) -> i32 { if !container.visits_should_be_counted { // TODO @@ -526,7 +527,7 @@ impl StoryState { Some(list_texts) } - fn push_to_output_stream_individual(&mut self, obj: Rc) { + fn push_to_output_stream_individual(&mut self, obj: Brc) { let glue = obj.clone().into_any().downcast::(); let text = Value::get_string_value(obj.as_ref()); let mut include_in_output = true; @@ -741,7 +742,7 @@ impl StoryState { .current_pointer = Pointer::start_of(self.main_content_container.clone()) } - pub fn get_current_choices(&self) -> Option<&Vec>> { + pub fn get_current_choices(&self) -> Option<&Vec>> { // 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. @@ -764,7 +765,7 @@ impl StoryState { // If the patch is applied, then this new flow will replace the old one in // _namedFlows copy.current_flow.name = self.current_flow.name.clone(); - copy.current_flow.callstack = Rc::new(RefCell::new(CallStack::new_from( + copy.current_flow.callstack = Brc::new(BrCell::new(CallStack::new_from( &self.current_flow.callstack.as_ref().borrow(), ))); copy.current_flow.current_choices = self.current_flow.current_choices.clone(); @@ -874,16 +875,16 @@ impl StoryState { self.output_stream_dirty(); } - pub fn pop_evaluation_stack(&mut self) -> Rc { + pub fn pop_evaluation_stack(&mut self) -> Brc { self.evaluation_stack.pop().unwrap() } pub fn pop_evaluation_stack_multiple( &mut self, number_of_objects: usize, - ) -> Vec> { + ) -> Vec> { let start = self.evaluation_stack.len() - number_of_objects; - let obj: Vec> = self.evaluation_stack.drain(start..).collect(); + let obj: Vec> = self.evaluation_stack.drain(start..).collect(); obj } @@ -969,13 +970,13 @@ impl StoryState { } } - pub fn peek_evaluation_stack(&self) -> Option<&Rc> { + pub fn peek_evaluation_stack(&self) -> Option<&Brc> { self.evaluation_stack.last() } pub fn start_function_evaluation_from_game( &mut self, - func_container: Rc, + func_container: Brc, arguments: Option<&Vec>, ) -> Result<(), StoryError> { self.get_callstack().borrow_mut().push( @@ -1012,7 +1013,7 @@ impl StoryState { } }; - self.push_evaluation_stack(Rc::new(value)); + self.push_evaluation_stack(Brc::new(value)); } } diff --git a/lib/src/variable_reference.rs b/lib/src/variable_reference.rs index bee9c0d..96854c6 100644 --- a/lib/src/variable_reference.rs +++ b/lib/src/variable_reference.rs @@ -1,9 +1,10 @@ -use std::{fmt, rc::Rc}; +use std::fmt; use crate::{ container::Container, object::{Object, RTObject}, path::Path, + Brc, }; pub struct VariableReference { @@ -29,7 +30,7 @@ impl VariableReference { } } - pub fn get_container_for_count(self: &Rc) -> Result, String> { + pub fn get_container_for_count(self: &Brc) -> Result, String> { if let Some(path) = &self.path_for_count { Ok(Object::resolve_path(self.clone(), path) .container() @@ -39,7 +40,7 @@ impl VariableReference { } } - pub fn get_path_string_for_count(self: &Rc) -> Option { + pub fn get_path_string_for_count(self: &Brc) -> Option { self.path_for_count .as_ref() .map(|path_for_count| Object::compact_path_string(self.clone(), path_for_count)) diff --git a/lib/src/variables_state.rs b/lib/src/variables_state.rs index 2a54524..837341d 100644 --- a/lib/src/variables_state.rs +++ b/lib/src/variables_state.rs @@ -1,8 +1,4 @@ -use std::{ - cell::RefCell, - collections::{HashMap, HashSet}, - rc::Rc, -}; +use std::collections::{HashMap, HashSet}; use serde_json::Map; @@ -15,23 +11,24 @@ use crate::{ value::Value, value_type::{ValueType, VariablePointerValue}, variable_assigment::VariableAssignment, + BrCell, Brc, }; #[derive(Clone)] pub(crate) struct VariablesState { - pub global_variables: HashMap>, - pub default_global_variables: HashMap>, + pub global_variables: HashMap>, + pub default_global_variables: HashMap>, pub batch_observing_variable_changes: bool, - pub callstack: Rc>, + pub callstack: Brc>, pub changed_variables_for_batch_obs: Option>, pub patch: Option, - list_defs_origin: Rc, + list_defs_origin: Brc, } impl VariablesState { pub fn new( - callstack: Rc>, - list_defs_origin: Rc, + callstack: Brc>, + list_defs_origin: Brc, ) -> VariablesState { VariablesState { global_variables: HashMap::new(), @@ -90,7 +87,7 @@ impl VariablesState { pub fn assign( &mut self, var_ass: &VariableAssignment, - value: Rc, + value: Brc, ) -> Result<(), StoryError> { let mut name = var_ass.variable_name.to_string(); let mut context_index = -1; @@ -155,7 +152,7 @@ impl VariablesState { // pointer that more specifically points to the exact instance: whether it's // global, // or the exact position of a temporary on the callstack. - fn resolve_variable_pointer(&self, var_pointer: &VariablePointerValue) -> Rc { + fn resolve_variable_pointer(&self, var_pointer: &VariablePointerValue) -> Brc { let mut context_index = var_pointer.context_index; if context_index == -1 { context_index = self.get_context_index_of_variable_named(&var_pointer.variable_name); @@ -174,7 +171,7 @@ impl VariablesState { } } - Rc::new(Value::new_variable_pointer( + Brc::new(Value::new_variable_pointer( &var_pointer.variable_name, context_index, )) @@ -191,7 +188,7 @@ impl VariablesState { let val = Value::from_value_type(value_type); - let notify = self.set_global(variable_name, Rc::new(val)); + let notify = self.set_global(variable_name, Brc::new(val)); Ok(notify) } @@ -229,7 +226,7 @@ impl VariablesState { return self.callstack.borrow().get_current_element_index(); } - fn get_raw_variable_with_name(&self, name: &str, context_index: i32) -> Option> { + fn get_raw_variable_with_name(&self, name: &str, context_index: i32) -> Option> { // 0 context = global if context_index == 0 || context_index == -1 { if let Some(patch) = &self.patch { @@ -270,8 +267,8 @@ impl VariablesState { } // Returns true if global var has changed and we need to notify observers - fn set_global(&mut self, name: &str, value: Rc) -> bool { - let mut old_value: Option> = None; + fn set_global(&mut self, name: &str, value: Brc) -> bool { + let mut old_value: Option> = None; if let Some(patch) = &self.patch { old_value = patch.get_global(name); @@ -292,7 +289,7 @@ impl VariablesState { .insert(name.to_string(), value.clone()); } - if old_value.is_none() || !Rc::ptr_eq(old_value.as_ref().unwrap(), &value) { + if old_value.is_none() || !Brc::ptr_eq(old_value.as_ref().unwrap(), &value) { if self.batch_observing_variable_changes { if let Some(patch) = &mut self.patch { patch.add_changed_variable(name); @@ -307,7 +304,7 @@ impl VariablesState { false } - pub fn get_variable_with_name(&self, name: &str, context_index: i32) -> Option> { + pub fn get_variable_with_name(&self, name: &str, context_index: i32) -> Option> { let var_value = self.get_raw_variable_with_name(name, context_index); // Get value from pointer? if let Some(vv) = var_value.clone() { @@ -319,11 +316,11 @@ impl VariablesState { var_value } - fn value_at_variable_pointer(&self, pointer: &VariablePointerValue) -> Option> { + fn value_at_variable_pointer(&self, pointer: &VariablePointerValue) -> Option> { self.get_variable_with_name(&pointer.variable_name, pointer.context_index) } - pub fn set_callstack(&mut self, callstack: Rc>) { + pub fn set_callstack(&mut self, callstack: Brc>) { self.callstack = callstack; } diff --git a/lib/tests/runtime_test.rs b/lib/tests/runtime_test.rs index efb6f88..91292d7 100644 --- a/lib/tests/runtime_test.rs +++ b/lib/tests/runtime_test.rs @@ -1,10 +1,11 @@ use core::panic; -use std::{cell::RefCell, error::Error, rc::Rc}; +use std::error::Error; use bladeink::{ story::Story, story_callbacks::{ExternalFunction, VariableObserver}, value_type::ValueType, + BrCell, Brc, }; mod common; @@ -49,7 +50,7 @@ fn external_function() -> Result<(), Box> { let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - story.bind_external_function("externalFunction", Rc::new(RefCell::new(ExtFunc1 {})), true)?; + story.bind_external_function("externalFunction", Brc::new(BrCell::new(ExtFunc1 {})), true)?; common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); @@ -64,7 +65,7 @@ fn external_function_zero_arguments() -> Result<(), Box> { let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - story.bind_external_function("externalFunction", Rc::new(RefCell::new(ExtFunc2 {})), true)?; + story.bind_external_function("externalFunction", Brc::new(BrCell::new(ExtFunc2 {})), true)?; common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); @@ -79,7 +80,7 @@ fn external_function_one_arguments() -> Result<(), Box> { let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - story.bind_external_function("externalFunction", Rc::new(RefCell::new(ExtFunc3 {})), true)?; + story.bind_external_function("externalFunction", Brc::new(BrCell::new(ExtFunc3 {})), true)?; common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); @@ -94,7 +95,7 @@ fn external_function_coerce_test() -> Result<(), Box> { let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - story.bind_external_function("externalFunction", Rc::new(RefCell::new(ExtFunc4 {})), true)?; + story.bind_external_function("externalFunction", Brc::new(BrCell::new(ExtFunc4 {})), true)?; common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); @@ -144,7 +145,7 @@ fn variable_observers_test() -> Result<(), Box> { let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - story.observe_variable("x", Rc::new(RefCell::new(VObserver { expected_value: 5 })))?; + story.observe_variable("x", Brc::new(BrCell::new(VObserver { expected_value: 5 })))?; common::next_all(&mut story, &mut text)?; story.choose_choice_index(0)?; From a0f477284b81a6b91079cfe836322e09b4377c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Mon, 23 Oct 2023 14:12:30 +0000 Subject: [PATCH 90/91] Use RwLock instead of Mutex and use clause. --- lib/src/lib.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/src/lib.rs b/lib/src/lib.rs index a65543a..8cae7bd 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -39,7 +39,11 @@ //! The `bladeink` library supports all the **Ink** language features, including threads, multi-flows, variable set/get from code, variable observing, external functions, //! tags on choices, etc. Examples of uses of all these features will be added to this documentation in the future, but meanwhile, all the examples can be found in the `lib/tests` folder in the source code of this crate. -use std::{cell::RefCell, rc::Rc}; +use std::{ + cell::RefCell, + rc::Rc, + sync::{Arc, RwLock}, +}; mod callstack; pub mod choice; @@ -84,4 +88,4 @@ pub type BrCell = RefCell; pub type Brc = Arc; #[cfg(feature = "threadsafe")] -pub type BrCell = Mutex; +pub type BrCell = RwLock; From 5acd5c71b1d04c81fbd663c05cd9db2afae43285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Mon, 23 Oct 2023 16:29:36 +0000 Subject: [PATCH 91/91] move threadsafe feature to his own threadsafe.rs file. --- cli-player/src/main.rs | 2 +- lib/Cargo.toml | 2 ++ lib/src/callstack.rs | 2 +- lib/src/choice.rs | 2 +- lib/src/choice_point.rs | 3 ++- lib/src/container.rs | 2 +- lib/src/divert.rs | 3 ++- lib/src/flow.rs | 3 ++- lib/src/ink_list.rs | 4 ++-- lib/src/json_read.rs | 4 ++-- lib/src/json_write.rs | 4 ++-- lib/src/lib.rs | 19 +------------------ lib/src/list_definitions_origin.rs | 2 +- lib/src/native_function_call.rs | 5 +++-- lib/src/object.rs | 3 ++- lib/src/pointer.rs | 2 +- lib/src/search_result.rs | 2 +- lib/src/state_patch.rs | 2 +- lib/src/story.rs | 3 ++- lib/src/story_callbacks.rs | 4 ++-- lib/src/story_state.rs | 3 ++- lib/src/threadsafe.rs | 28 ++++++++++++++++++++++++++++ lib/src/variable_reference.rs | 2 +- lib/src/variables_state.rs | 3 ++- lib/tests/runtime_test.rs | 3 ++- 25 files changed, 67 insertions(+), 45 deletions(-) create mode 100644 lib/src/threadsafe.rs diff --git a/cli-player/src/main.rs b/cli-player/src/main.rs index c0c53f0..03c1340 100644 --- a/cli-player/src/main.rs +++ b/cli-player/src/main.rs @@ -5,7 +5,7 @@ use std::{error::Error, fs, io, path::Path}; use anyhow::Context; use bladeink::story_callbacks::{ErrorHandler, ErrorType}; use bladeink::{choice::Choice, story::Story}; -use bladeink::{BrCell, Brc}; +use bladeink::{threadsafe::BrCell, threadsafe::Brc}; use clap::Parser; use rand::Rng; diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 3f77d1b..4d0b662 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -17,6 +17,8 @@ name = "bladeink" path = "src/lib.rs" [features] +# Uncomment the line below while developing the threadsafe feature +# default = ["threadsafe"] threadsafe = [] [dependencies] diff --git a/lib/src/callstack.rs b/lib/src/callstack.rs index 3eb2fc8..71cecc5 100644 --- a/lib/src/callstack.rs +++ b/lib/src/callstack.rs @@ -11,8 +11,8 @@ use crate::{ push_pop::PushPopType, story::Story, story_error::StoryError, + threadsafe::Brc, value::Value, - Brc, }; pub struct Element { diff --git a/lib/src/choice.rs b/lib/src/choice.rs index be5a32d..aee417c 100644 --- a/lib/src/choice.rs +++ b/lib/src/choice.rs @@ -5,7 +5,7 @@ use crate::{ callstack::Thread, object::{Object, RTObject}, path::Path, - BrCell, + threadsafe::BrCell, }; /// Represents a choice generated by a [`Story`](crate::story::Story). diff --git a/lib/src/choice_point.rs b/lib/src/choice_point.rs index e0b43e3..fe1deea 100644 --- a/lib/src/choice_point.rs +++ b/lib/src/choice_point.rs @@ -4,7 +4,8 @@ use crate::{ container::Container, object::{Object, RTObject}, path::Path, - BrCell, Brc, + threadsafe::BrCell, + threadsafe::Brc, }; pub struct ChoicePoint { diff --git a/lib/src/container.rs b/lib/src/container.rs index be07cda..6ef8696 100644 --- a/lib/src/container.rs +++ b/lib/src/container.rs @@ -6,9 +6,9 @@ use crate::{ object::{Object, RTObject}, path::{Component, Path}, search_result::SearchResult, + threadsafe::Brc, value::Value, value_type::ValueType, - Brc, }; const COUNTFLAGS_VISITS: i32 = 1; diff --git a/lib/src/divert.rs b/lib/src/divert.rs index 0c20da3..9b30344 100644 --- a/lib/src/divert.rs +++ b/lib/src/divert.rs @@ -6,7 +6,8 @@ use crate::{ path::{Component, Path}, pointer::{self, Pointer}, push_pop::PushPopType, - BrCell, Brc, + threadsafe::BrCell, + threadsafe::Brc, }; pub struct Divert { diff --git a/lib/src/flow.rs b/lib/src/flow.rs index 1254072..d539a7c 100644 --- a/lib/src/flow.rs +++ b/lib/src/flow.rs @@ -7,7 +7,8 @@ use crate::{ json_read, json_write, object::RTObject, story_error::StoryError, - BrCell, Brc, + threadsafe::BrCell, + threadsafe::Brc, }; #[derive(Clone)] diff --git a/lib/src/ink_list.rs b/lib/src/ink_list.rs index de006da..97e272a 100644 --- a/lib/src/ink_list.rs +++ b/lib/src/ink_list.rs @@ -3,8 +3,8 @@ use std::collections::HashMap; use crate::{ ink_list_item::InkListItem, list_definition::ListDefinition, - list_definitions_origin::ListDefinitionsOrigin, story_error::StoryError, value_type::ValueType, - BrCell, + list_definitions_origin::ListDefinitionsOrigin, story_error::StoryError, threadsafe::BrCell, + value_type::ValueType, }; #[derive(Clone)] diff --git a/lib/src/json_read.rs b/lib/src/json_read.rs index 58d7037..c861126 100644 --- a/lib/src/json_read.rs +++ b/lib/src/json_read.rs @@ -8,8 +8,8 @@ use crate::{ ink_list_item::InkListItem, list_definition::ListDefinition, list_definitions_origin::ListDefinitionsOrigin, native_function_call::NativeFunctionCall, object::RTObject, path::Path, push_pop::PushPopType, story_error::StoryError, tag::Tag, - value::Value, variable_assigment::VariableAssignment, variable_reference::VariableReference, - void::Void, Brc, + threadsafe::Brc, value::Value, variable_assigment::VariableAssignment, + variable_reference::VariableReference, void::Void, }; pub fn jtoken_to_runtime_object( diff --git a/lib/src/json_write.rs b/lib/src/json_write.rs index 31380bf..2af0676 100644 --- a/lib/src/json_write.rs +++ b/lib/src/json_write.rs @@ -6,8 +6,8 @@ use crate::{ choice::Choice, choice_point::ChoicePoint, container::Container, control_command::ControlCommand, divert::Divert, glue::Glue, ink_list::InkList, native_function_call::NativeFunctionCall, object::RTObject, push_pop::PushPopType, - story_error::StoryError, tag::Tag, value::Value, variable_assigment::VariableAssignment, - variable_reference::VariableReference, void::Void, Brc, + story_error::StoryError, tag::Tag, threadsafe::Brc, value::Value, + variable_assigment::VariableAssignment, variable_reference::VariableReference, void::Void, }; pub fn write_dictionary_values( diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 8cae7bd..c2edd9b 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -39,12 +39,6 @@ //! The `bladeink` library supports all the **Ink** language features, including threads, multi-flows, variable set/get from code, variable observing, external functions, //! tags on choices, etc. Examples of uses of all these features will be added to this documentation in the future, but meanwhile, all the examples can be found in the `lib/tests` folder in the source code of this crate. -use std::{ - cell::RefCell, - rc::Rc, - sync::{Arc, RwLock}, -}; - mod callstack; pub mod choice; mod choice_point; @@ -71,21 +65,10 @@ pub mod story_callbacks; pub mod story_error; mod story_state; mod tag; +pub mod threadsafe; mod value; pub mod value_type; mod variable_assigment; mod variable_reference; mod variables_state; mod void; - -#[cfg(not(feature = "threadsafe"))] -pub type Brc = Rc; - -#[cfg(not(feature = "threadsafe"))] -pub type BrCell = RefCell; - -#[cfg(feature = "threadsafe")] -pub type Brc = Arc; - -#[cfg(feature = "threadsafe")] -pub type BrCell = RwLock; diff --git a/lib/src/list_definitions_origin.rs b/lib/src/list_definitions_origin.rs index 45d57d0..0d06272 100644 --- a/lib/src/list_definitions_origin.rs +++ b/lib/src/list_definitions_origin.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use crate::{ink_list::InkList, list_definition::ListDefinition, value::Value, Brc}; +use crate::{ink_list::InkList, list_definition::ListDefinition, threadsafe::Brc, value::Value}; #[derive(Clone)] pub struct ListDefinitionsOrigin { diff --git a/lib/src/native_function_call.rs b/lib/src/native_function_call.rs index 6f3a5f2..0f05f14 100644 --- a/lib/src/native_function_call.rs +++ b/lib/src/native_function_call.rs @@ -4,10 +4,11 @@ use crate::{ ink_list::InkList, object::{Object, RTObject}, story_error::StoryError, + threadsafe::brcell_borrow, + threadsafe::Brc, value::Value, value_type::ValueType, void::Void, - Brc, }; #[derive(Debug, PartialEq, Clone, Copy)] @@ -301,7 +302,7 @@ impl NativeFunctionCall { } }; - let origins = list_val.origins.borrow(); + let origins = brcell_borrow(&list_val.origins); let item_origin = origins.iter().find(|origin| { origin.get_name() == list_item.get_origin_name().unwrap_or(&"".to_owned()) diff --git a/lib/src/object.rs b/lib/src/object.rs index dd54360..734bc80 100644 --- a/lib/src/object.rs +++ b/lib/src/object.rs @@ -6,7 +6,8 @@ use crate::{ container::Container, path::{Component, Path}, search_result::SearchResult, - BrCell, Brc, + threadsafe::BrCell, + threadsafe::Brc, }; pub struct Object { diff --git a/lib/src/pointer.rs b/lib/src/pointer.rs index 7ad8749..e851a7b 100644 --- a/lib/src/pointer.rs +++ b/lib/src/pointer.rs @@ -4,7 +4,7 @@ use crate::{ container::Container, object::RTObject, path::{Component, Path}, - Brc, + threadsafe::Brc, }; pub const NULL: Pointer = Pointer::new(None, -1); diff --git a/lib/src/search_result.rs b/lib/src/search_result.rs index 44b196a..b029227 100644 --- a/lib/src/search_result.rs +++ b/lib/src/search_result.rs @@ -1,4 +1,4 @@ -use crate::{container::Container, object::RTObject, Brc}; +use crate::{container::Container, object::RTObject, threadsafe::Brc}; #[derive(Clone)] pub struct SearchResult { diff --git a/lib/src/state_patch.rs b/lib/src/state_patch.rs index eaed3e1..1f41ab8 100644 --- a/lib/src/state_patch.rs +++ b/lib/src/state_patch.rs @@ -1,6 +1,6 @@ use std::collections::{HashMap, HashSet}; -use crate::{container::Container, object::Object, value::Value, Brc}; +use crate::{container::Container, object::Object, threadsafe::Brc, value::Value}; #[derive(Clone)] pub struct StatePatch { diff --git a/lib/src/story.rs b/lib/src/story.rs index 8670a7a..0bae48f 100644 --- a/lib/src/story.rs +++ b/lib/src/story.rs @@ -26,12 +26,13 @@ use crate::{ story_error::StoryError, story_state::StoryState, tag::Tag, + threadsafe::BrCell, + threadsafe::Brc, value::Value, value_type::ValueType, variable_assigment::VariableAssignment, variable_reference::VariableReference, void::Void, - BrCell, Brc, }; /// The current version of the Ink story file format. diff --git a/lib/src/story_callbacks.rs b/lib/src/story_callbacks.rs index 71f819b..eaa2f1d 100644 --- a/lib/src/story_callbacks.rs +++ b/lib/src/story_callbacks.rs @@ -3,8 +3,8 @@ use std::collections::HashSet; use crate::{ container::Container, divert::Divert, object::RTObject, pointer::Pointer, - push_pop::PushPopType, story::Story, story_error::StoryError, value::Value, - value_type::ValueType, void::Void, BrCell, Brc, + push_pop::PushPopType, story::Story, story_error::StoryError, threadsafe::BrCell, + threadsafe::Brc, value::Value, value_type::ValueType, void::Void, }; /// Defines the method that will be called when an observed global variable changes. diff --git a/lib/src/story_state.rs b/lib/src/story_state.rs index ab95f7f..b7c9b14 100644 --- a/lib/src/story_state.rs +++ b/lib/src/story_state.rs @@ -17,11 +17,12 @@ use crate::{ story::{Story, INK_VERSION_CURRENT}, story_error::StoryError, tag::Tag, + threadsafe::BrCell, + threadsafe::Brc, value::Value, value_type::ValueType, variables_state::VariablesState, void::Void, - BrCell, Brc, }; use rand::Rng; diff --git a/lib/src/threadsafe.rs b/lib/src/threadsafe.rs new file mode 100644 index 0000000..56c7076 --- /dev/null +++ b/lib/src/threadsafe.rs @@ -0,0 +1,28 @@ +#[allow(unused_imports)] +use std::{ + cell::RefCell, + rc::Rc, + sync::{Arc, RwLock}, +}; + +#[cfg(not(feature = "threadsafe"))] +pub type Brc = Rc; + +#[cfg(not(feature = "threadsafe"))] +pub type BrCell = RefCell; + +#[cfg(not(feature = "threadsafe"))] +pub(crate) fn brcell_borrow(cell: &BrCell) -> std::cell::Ref<'_, T> { + cell.borrow() +} + +#[cfg(feature = "threadsafe")] +pub type Brc = Arc; + +#[cfg(feature = "threadsafe")] +pub type BrCell = RwLock; + +#[cfg(feature = "threadsafe")] +pub(crate) fn brcell_borrow<'a, T>(cell: &'a BrCell) -> std::sync::RwLockReadGuard<'a, T> { + cell.read().unwrap() +} diff --git a/lib/src/variable_reference.rs b/lib/src/variable_reference.rs index 96854c6..bb2da9b 100644 --- a/lib/src/variable_reference.rs +++ b/lib/src/variable_reference.rs @@ -4,7 +4,7 @@ use crate::{ container::Container, object::{Object, RTObject}, path::Path, - Brc, + threadsafe::Brc, }; pub struct VariableReference { diff --git a/lib/src/variables_state.rs b/lib/src/variables_state.rs index 837341d..d05626b 100644 --- a/lib/src/variables_state.rs +++ b/lib/src/variables_state.rs @@ -8,10 +8,11 @@ use crate::{ list_definitions_origin::ListDefinitionsOrigin, state_patch::StatePatch, story_error::StoryError, + threadsafe::BrCell, + threadsafe::Brc, value::Value, value_type::{ValueType, VariablePointerValue}, variable_assigment::VariableAssignment, - BrCell, Brc, }; #[derive(Clone)] diff --git a/lib/tests/runtime_test.rs b/lib/tests/runtime_test.rs index 91292d7..c538cc3 100644 --- a/lib/tests/runtime_test.rs +++ b/lib/tests/runtime_test.rs @@ -4,8 +4,9 @@ use std::error::Error; use bladeink::{ story::Story, story_callbacks::{ExternalFunction, VariableObserver}, + threadsafe::BrCell, + threadsafe::Brc, value_type::ValueType, - BrCell, Brc, }; mod common;