diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index fea6ac8..29423c1 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.79.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 index 6fa0ffe..66cc700 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,9 @@ name: Rust CI -on: [push, pull_request] +on: + push: + branches: + - main jobs: build: @@ -26,6 +29,4 @@ jobs: run: cargo fmt -- --check - name: Test - run: | - cargo test - cargo test --features stream-json-parser + run: cargo test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 35b9104..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: Upload Rust Packages - -on: - release: - types: [published] - -permissions: - contents: write - -jobs: - upload-binkplayer: - runs-on: ${{ matrix.os }} - strategy: - matrix: - build: [linux, windows, macos] - include: - - build: linux - os: ubuntu-latest - exec: binkplayer - platform: x86_64-linux - - build: macos - os: macos-latest - exec: binkplayer - platform: x86_64-macos - - build: windows - os: windows-latest - exec: binkplayer.exe - platform: x86_64-windows - steps: - - uses: actions/checkout@v3 - - name: Set up Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - - name: Build package - run: | - cargo build --release - - name: Upload binkplayer to Github release - run: | - staging="binkplayer-${{github.event.release.tag_name}}-${{ matrix.platform }}" - mkdir "$staging" - cp target/release/${{ matrix.exec }} "$staging/" - - if [ "${{ matrix.os }}" = "windows-latest" ]; then - 7z a "$staging.zip" "$staging" - gh release upload ${{github.event.release.tag_name}} "$staging.zip" - else - tar czf "$staging.tar.gz" "$staging" - gh release upload ${{github.event.release.tag_name}} "$staging.tar.gz" - fi - env: - GITHUB_TOKEN: ${{ github.TOKEN }} - shell: bash - - publish: - name: Publish packages in crates.io - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - - name: Publish - run: | - cargo publish -p bladeink --token ${CRATES_TOKEN} - cargo publish -p binkplayer --token ${CRATES_TOKEN} - env: - CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} diff --git a/LICENSE b/LICENSE index 6a5cf8b..261eeb9 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2023 Rafael Garcia Moreno + 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. diff --git a/cli-player/Cargo.toml b/cli-player/Cargo.toml index b899055..b07984d 100644 --- a/cli-player/Cargo.toml +++ b/cli-player/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "binkplayer" -version = "1.2.1" +version = "0.9.0" description = """ Console player for compiled .json Ink story files. """ @@ -16,9 +16,9 @@ path = "src/main.rs" [dependencies] anyhow = "1.0.75" -bladeink = { path = "../lib", "version" = "1.1.0" } +bladeink = { path = "../lib", "version" = "0.9.2" } clap = { "version" = "4.4.6", features = ["derive"] } -rand = "0.9.2" +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 a9951b6..03c1340 100644 --- a/cli-player/src/main.rs +++ b/cli-player/src/main.rs @@ -1,17 +1,11 @@ -//! Console player that can runs compiled `.ink.json` story files writen in the -//! **Ink** language. -use std::cell::RefCell; - -use std::{error::Error, fs, io, io::Write, path::Path, rc::Rc}; +//! Console player that can runs compiled `.ink.json` story files writen in the **Ink** language. +use std::io::Write; +use std::{error::Error, fs, io, path::Path}; use anyhow::Context; -use bladeink::{ - choice::Choice, - story::{ - errors::{ErrorHandler, ErrorType}, - Story, - }, -}; +use bladeink::story_callbacks::{ErrorHandler, ErrorType}; +use bladeink::{choice::Choice, story::Story}; +use bladeink::{threadsafe::BrCell, threadsafe::Brc}; use clap::Parser; use rand::Rng; @@ -24,10 +18,6 @@ struct Args { /// Choose options randomly #[arg(short, default_value_t = false)] pub auto_play: bool, - - /// Forbid external function fallbacks - #[arg(short = 'e', default_value_t = false)] - pub forbid_external_fallbacks: bool, } enum Command { @@ -37,7 +27,6 @@ enum Command { Load(String), Save(String), DivertPath(String), - Flow(String), } struct EHandler { @@ -45,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, })) } @@ -54,7 +43,7 @@ impl EHandler { impl ErrorHandler for EHandler { fn error(&mut self, message: &str, error_type: ErrorType) { - eprintln!("{}", message); + println!("{}", message); if error_type == ErrorType::Error { self.should_terminate = true; @@ -66,13 +55,12 @@ fn main() -> Result<(), Box> { let args = Args::parse(); let json_string = get_json_string(&args.json_filename)?; - // REMOVE BOM if exists + // 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 err_handler = EHandler::new(); story.set_error_handler(err_handler.clone()); - story.set_allow_external_function_fallbacks(!args.forbid_external_fallbacks); let mut end = false; @@ -80,23 +68,20 @@ fn main() -> Result<(), Box> { while story.can_continue() { let line = story.cont()?; - print!("{}", line); - - let tags = story.get_current_tags()?; + let trimmed = line.trim(); - if !tags.is_empty() { - println!("# tags: {}", tags.join(", ")); - } + println!("{}", trimmed); } let choices = story.get_current_choices(); if !choices.is_empty() { let command = if args.auto_play { - let i = rand::rng().random_range(0..choices.len()); + let i = rand::thread_rng().gen_range(0..choices.len()); println!(); print_choices(&choices); - println!("?> {}", i + 1); + println!(); + println!("?>{i}"); Command::Choose(i) } else { @@ -127,13 +112,6 @@ fn process_command(command: Command, story: &mut Story) -> Result { - let result = story.switch_flow(&flow); - - if let Err(desc) = result { - println!("") - } - } Command::DivertPath(path) => { let result = story.choose_path_string(&path, true, None); @@ -142,30 +120,27 @@ fn process_command(command: Command, story: &mut Story) -> Result println!( - "Commands:\n\tload \n\tsave \n\t-> \n\tswitch \n\tquit\n\t" + "Commands:\n\tload \n\tsave \n\t-> \n\tquit\n\t" ), } Ok(false) } -fn print_choices(choices: &[Rc]) { +fn print_choices(choices: &[Brc]) { for (i, c) in choices.iter().enumerate() { - println!("{}: {}", i + 1, c.text); - - if !c.tags.is_empty() { - println!("# tags: {}", c.tags.join(", ")); - } + println!("{}. {}", i + 1, c.text); } } -fn read_input(choices: &[Rc]) -> Result> { +fn read_input(choices: &Vec>) -> Result> { let mut line = String::new(); loop { println!(); print_choices(choices); - print!("?> "); + println!(); + print!("?>"); io::stdout().flush()?; line.clear(); @@ -214,14 +189,6 @@ fn read_input(choices: &[Rc]) -> Result> { print_error("incorrect filename"); } - "switch" => { - if words.len() == 2 { - return Ok(Command::Flow(words[1].trim().to_string())); - } - - print_error("incorrect flow name"); - } - "->" => { if words.len() == 2 { return Ok(Command::DivertPath(words[1].trim().to_string())); @@ -235,7 +202,7 @@ fn read_input(choices: &[Rc]) -> Result> { } fn print_error(error: &str) { - eprintln!("<{error}>"); + println!("<{error}>"); } fn get_json_string(filename: &str) -> Result> { diff --git a/cli-player/tests/basic_tests.rs b/cli-player/tests/basic_tests.rs index 4a97e72..ac9806a 100644 --- a/cli-player/tests/basic_tests.rs +++ b/cli-player/tests/basic_tests.rs @@ -1,10 +1,8 @@ use assert_cmd::prelude::*; use predicates::prelude::predicate; // Add methods on commands -use std::{ - io::Write, - path::Path, - process::{Command, Stdio}, -}; +use std::io::Write; +use std::path::Path; +use std::process::{Command, Stdio}; #[test] fn basic_story_test() -> Result<(), Box> { @@ -12,8 +10,7 @@ fn basic_story_test() -> Result<(), Box> { 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. + // 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); } @@ -32,7 +29,7 @@ fn basic_story_test() -> Result<(), Box> { assert!(output.status.success()); assert!(output_str.contains("Test conditional choices")); - assert!(output_str.contains("1: one")); + assert!(output_str.contains("1. one")); assert!(output_str.ends_with("one\n")); Ok(()) diff --git a/cli-player/tests/test_the_intercept.rs b/cli-player/tests/test_the_intercept.rs index 4acc54a..77e33b9 100644 --- a/cli-player/tests/test_the_intercept.rs +++ b/cli-player/tests/test_the_intercept.rs @@ -1,9 +1,7 @@ use assert_cmd::prelude::*; -use std::{ - io::Write, - path::Path, - process::{Command, Stdio}, -}; +use std::io::Write; +use std::path::Path; +use std::process::{Command, Stdio}; #[test] fn the_intercept_test() -> Result<(), Box> { @@ -11,8 +9,7 @@ fn the_intercept_test() -> Result<(), Box> { 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. + // 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); } @@ -31,9 +28,9 @@ fn the_intercept_test() -> Result<(), Box> { 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")); + assert!(output_str.contains("1. Hut 14")); + assert!(output_str.contains("3. Wait")); + assert!(output_str.contains("3. Divert")); Ok(()) } diff --git a/inkfiles/lists/list-all.ink b/inkfiles/lists/list-all.ink deleted file mode 100644 index 5c83e3e..0000000 --- a/inkfiles/lists/list-all.ink +++ /dev/null @@ -1,3 +0,0 @@ -LIST a = A -LIST b = B -{LIST_ALL(A + B)} \ No newline at end of file diff --git a/inkfiles/lists/list-all.ink.json b/inkfiles/lists/list-all.ink.json deleted file mode 100644 index 2d1f34a..0000000 --- a/inkfiles/lists/list-all.ink.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "inkVersion": 21, - "root": [ - [ - "ev", - { - "VAR?": "A" - }, - { - "VAR?": "B" - }, - "+", - "LIST_ALL", - "out", - "/ev", - "\n", - [ - "done", - { - "#f": 5, - "#n": "g-0" - } - ], - null - ], - "done", - { - "global decl": [ - "ev", - { - "list": {}, - "origins": [ - "a" - ] - }, - { - "VAR=": "a" - }, - { - "list": {}, - "origins": [ - "b" - ] - }, - { - "VAR=": "b" - }, - "/ev", - "end", - null - ], - "#f": 1 - } - ], - "listDefs": { - "a": { - "A": 1 - }, - "b": { - "B": 1 - } - } -} \ No newline at end of file diff --git a/inkfiles/lists/list-comparison.ink b/inkfiles/lists/list-comparison.ink deleted file mode 100644 index eceaeef..0000000 --- a/inkfiles/lists/list-comparison.ink +++ /dev/null @@ -1,18 +0,0 @@ -VAR currentActor = "Bobby" - -LIST listOfActors = P, A, S, C -VAR s = -> set_actor --> start - -===function set_actor(x) -{ x: -- P: ~ currentActor = "Philippe" -- A: ~ currentActor = "Andre" -- else: ~ currentActor = "Bobby" -} - -=== start === -{s(P)} Hey, my name is {currentActor}. What about yours? -{s(A)} I am {currentActor} and I need my rheumatism pills! -{s(P)} Would you like me, {currentActor}, to get some more for you? --> END \ No newline at end of file diff --git a/inkfiles/lists/list-comparison.ink.json b/inkfiles/lists/list-comparison.ink.json deleted file mode 100644 index 0a0928f..0000000 --- a/inkfiles/lists/list-comparison.ink.json +++ /dev/null @@ -1 +0,0 @@ -{"inkVersion":21,"root":[[{"->":"start"},["done",{"#f":5,"#n":"g-0"}],null],"done",{"set_actor":[{"temp=":"x"},"ev",{"VAR?":"x"},"/ev",["du","ev",{"VAR?":"P"},"==","/ev",{"->":".^.b","c":true},{"b":["pop","\n","ev","str","^Philippe","/str","/ev",{"VAR=":"currentActor","re":true},{"->":".^.^.^.7"},null]}],["du","ev",{"VAR?":"A"},"==","/ev",{"->":".^.b","c":true},{"b":["pop","\n","ev","str","^Andre","/str","/ev",{"VAR=":"currentActor","re":true},{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["pop","\n","ev","str","^Bobby","/str","/ev",{"VAR=":"currentActor","re":true},{"->":".^.^.^.7"},null]}],"nop","\n",{"#f":3}],"start":["ev",{"VAR?":"P"},{"f()":"s","var":true},"out","/ev","^ Hey, my name is ","ev",{"VAR?":"currentActor"},"out","/ev","^. What about yours?","\n","ev",{"VAR?":"A"},{"f()":"s","var":true},"out","/ev","^ I am ","ev",{"VAR?":"currentActor"},"out","/ev","^ and I need my rheumatism pills!","\n","ev",{"VAR?":"P"},{"f()":"s","var":true},"out","/ev","^ Would you like me, ","ev",{"VAR?":"currentActor"},"out","/ev","^, to get some more for you?","\n","end",{"#f":1}],"global decl":["ev","str","^Bobby","/str",{"VAR=":"currentActor"},{"list":{},"origins":["listOfActors"]},{"VAR=":"listOfActors"},{"^->":"set_actor"},{"VAR=":"s"},"/ev","end",null],"#f":1}],"listDefs":{"listOfActors":{"P":1,"A":2,"S":3,"C":4}}} \ No newline at end of file diff --git a/inkfiles/misc/i18n.ink b/inkfiles/misc/i18n.ink deleted file mode 100644 index 73a6f4c..0000000 --- a/inkfiles/misc/i18n.ink +++ /dev/null @@ -1,3 +0,0 @@ -áéíóú ñ -你好 #áé -你好世界 diff --git a/inkfiles/misc/i18n.ink.json b/inkfiles/misc/i18n.ink.json deleted file mode 100644 index b5d3f94..0000000 --- a/inkfiles/misc/i18n.ink.json +++ /dev/null @@ -1 +0,0 @@ -{"inkVersion":21,"root":[["^áéíóú ñ","\n","^你好 ","#","^áé","/#","\n","^你好世界","\n",["done",{"#f":5,"#n":"g-0"}],null],"done",{"#f":1}],"listDefs":{}} \ No newline at end of file diff --git a/inkfiles/misc/newlines_with_string_eval.ink b/inkfiles/misc/newlines_with_string_eval.ink deleted file mode 100644 index fc86cb3..0000000 --- a/inkfiles/misc/newlines_with_string_eval.ink +++ /dev/null @@ -1,9 +0,0 @@ -A -~temp someTemp = string() -B -A -{string()} -B -=== function string() - ~ return "{3}" -} \ No newline at end of file diff --git a/inkfiles/misc/newlines_with_string_eval.ink.json b/inkfiles/misc/newlines_with_string_eval.ink.json deleted file mode 100644 index 4116b2b..0000000 --- a/inkfiles/misc/newlines_with_string_eval.ink.json +++ /dev/null @@ -1 +0,0 @@ -{"inkVersion":21,"root":[["^A","\n","ev",{"f()":"string"},"/ev",{"temp=":"someTemp"},"\n","^B","\n","^A","\n","ev",{"f()":"string"},"out","/ev","\n","^B","\n",["done",{"#f":5,"#n":"g-0"}],null],"done",{"string":["ev","str","ev",3,"out","/ev","/str","/ev","~ret",{"#f":1}],"global decl":["ev","/ev","end",null],"#f":1}],"listDefs":{}} \ No newline at end of file diff --git a/inkfiles/tags/tagsInChoiceDynamic.ink b/inkfiles/tags/tagsInChoiceDynamic.ink deleted file mode 100644 index a5fcedc..0000000 --- a/inkfiles/tags/tagsInChoiceDynamic.ink +++ /dev/null @@ -1,6 +0,0 @@ -VAR name = "Name" -// Should add tag 'tag Name' to choice at runtime -+ [Choice #tag {name}] -+ [Choice2 #tag 1 {name} 2 3 4] -+ [Choice #{name} tag 1 2 3 4] -->END \ No newline at end of file diff --git a/inkfiles/tags/tagsInChoiceDynamic.ink.json b/inkfiles/tags/tagsInChoiceDynamic.ink.json deleted file mode 100644 index 612192d..0000000 --- a/inkfiles/tags/tagsInChoiceDynamic.ink.json +++ /dev/null @@ -1 +0,0 @@ -{"inkVersion":21,"root":[["ev","str","^Choice ","#","^tag ","ev",{"VAR?":"name"},"out","/ev","/#","/str","/ev",{"*":"0.c-0","flg":4},"ev","str","^Choice2 ","#","^tag 1 ","ev",{"VAR?":"name"},"out","/ev","^ 2 3 4","/#","/str","/ev",{"*":"0.c-1","flg":4},"ev","str","^Choice ","#","ev",{"VAR?":"name"},"out","/ev","^ tag 1 2 3 4","/#","/str","/ev",{"*":"0.c-2","flg":4},{"c-0":["\n",{"->":"0.g-0"},null],"c-1":["\n",{"->":"0.g-0"},null],"c-2":["\n","end",{"->":"0.g-0"},null],"g-0":["done",null]}],"done",{"global decl":["ev","str","^Name","/str",{"VAR=":"name"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 7eefbb5..4d0b662 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bladeink" -version = "1.2.1" +version = "0.9.3" authors = ["Rafael Garcia "] description = """ This is a Rust port of inkle's ink, a scripting language for writing interactive narrative. @@ -16,16 +16,14 @@ edition = "2021" name = "bladeink" path = "src/lib.rs" +[features] +# Uncomment the line below while developing the threadsafe feature +# default = ["threadsafe"] +threadsafe = [] + [dependencies] serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.93" -strum = { version = "0.27.2", features = ["derive"] } +strum = { version = "0.25.0", features = ["derive"] } as-any = "0.3.0" -rand = "0.9.2" -web-time = "1.1.0" - -[features] -stream-json-parser = [] - -[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] -getrandom = { version = "0.3.3", features = ["wasm_js"] } +rand = "0.8.5" diff --git a/lib/src/callstack.rs b/lib/src/callstack.rs index c8a6591..71cecc5 100644 --- a/lib/src/callstack.rs +++ b/lib/src/callstack.rs @@ -1,24 +1,24 @@ -use std::{collections::HashMap, rc::Rc}; +use std::collections::HashMap; use serde_json::{json, Map}; use crate::{ container::Container, - json::{json_read, json_write}, + json_read, json_write, object::Object, path::Path, pointer::{self, Pointer}, push_pop::PushPopType, story::Story, story_error::StoryError, + threadsafe::Brc, value::Value, }; -#[derive(Clone)] 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, @@ -39,9 +39,21 @@ 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 + } } -#[derive(Clone)] pub struct Thread { pub callstack: Vec, pub previous_pointer: Pointer, @@ -58,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(); @@ -103,7 +115,7 @@ impl Thread { pointer.index = pointer_index; if thread_pointer_result.approximate { - // TODO warning not accessible from here + // 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())); } } @@ -137,6 +149,19 @@ impl Thread { Ok(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 + } + pub(crate) fn write_json(&self) -> Result { let mut thread: Map = Map::new(); @@ -184,7 +209,6 @@ impl Thread { } } -#[derive(Clone)] pub struct CallStack { thread_counter: usize, start_of_root: Pointer, @@ -192,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), @@ -204,6 +228,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 fn get_current_element(&self) -> &Element { let thread = self.threads.last().unwrap(); let cs = &thread.callstack; @@ -244,7 +282,7 @@ impl CallStack { } pub fn push_thread(&mut self) { - let mut new_thread = self.get_current_thread().clone(); + let mut new_thread = self.get_current_thread().copy(); self.thread_counter += 1; new_thread.thread_index = self.thread_counter; self.threads.push(new_thread); @@ -311,7 +349,7 @@ impl CallStack { } pub fn fork_thread(&mut self) -> Thread { - let mut forked_thread = self.get_current_thread().clone(); + let mut forked_thread = self.get_current_thread().copy(); self.thread_counter += 1; forked_thread.thread_index = self.thread_counter; forked_thread @@ -320,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> { @@ -365,14 +403,12 @@ impl CallStack { 0 } - // Get variable value, dereferencing a variable pointer if necessary pub fn get_temporary_variable_with_name( &self, name: &str, context_index: i32, - ) -> Option> { + ) -> Option> { let mut context_index = context_index; - // contextIndex 0 means global, so index is actually 1-based if context_index == -1 { context_index = self.get_current_element_index() + 1; } @@ -422,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 fb55f9d..aee417c 100644 --- a/lib/src/choice.rs +++ b/lib/src/choice.rs @@ -1,19 +1,18 @@ //! A generated [`Choice`] from the story. use core::fmt; -use std::cell::RefCell; use crate::{ callstack::Thread, object::{Object, RTObject}, path::Path, + threadsafe::BrCell, }; /// Represents a choice generated by a [`Story`](crate::story::Story). -#[derive(Clone)] 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, @@ -22,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, } @@ -41,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, } } @@ -55,17 +54,16 @@ impl Choice { text: &str, index: usize, original_thread_index: usize, - choice_tags: Vec, ) -> Choice { Choice { obj: Object::new(), target_path: Path::new_with_components_string(Some(path_string_on_choice)), is_invisible_default: false, - tags: choice_tags, - index: RefCell::new(index), - original_thread_index: RefCell::new(original_thread_index), + tags: Vec::new(), + 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, } } @@ -78,7 +76,7 @@ impl Choice { self.thread_at_generation .borrow() .as_ref() - .map(|t| t.clone()) + .map(|t| t.copy()) } } diff --git a/lib/src/choice_point.rs b/lib/src/choice_point.rs index a0d467d..fe1deea 100644 --- a/lib/src/choice_point.rs +++ b/lib/src/choice_point.rs @@ -1,10 +1,11 @@ use core::fmt; -use std::{cell::RefCell, rc::Rc}; use crate::{ container::Container, object::{Object, RTObject}, path::Path, + threadsafe::BrCell, + threadsafe::Brc, }; pub struct ChoicePoint { @@ -14,7 +15,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 +27,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 +77,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 +88,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 199294d..6ef8696 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; @@ -6,6 +6,7 @@ use crate::{ object::{Object, RTObject}, path::{Component, Path}, search_result::SearchResult, + threadsafe::Brc, value::Value, value_type::ValueType, }; @@ -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); @@ -206,24 +207,13 @@ impl Container { break; } - let found_obj = found_obj.unwrap(); - - // Are we about to loop into another container? - // Is the object a container as expected? It might - // no longer be if the content has shuffled around, so what - // was originally a container no longer is. - let next_container = found_obj.clone().into_any().downcast::(); - if (i as i32) < (partial_path_length - 1) && next_container.is_err() { - approximate = true; - break; - } - - current_obj = found_obj; - current_container = if let Ok(container) = next_container { - Some(container) - } else { - None - }; + 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) @@ -270,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() { @@ -281,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()) @@ -292,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..9b30344 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,14 @@ use crate::{ path::{Component, Path}, pointer::{self, Pointer}, push_pop::PushPopType, + threadsafe::BrCell, + threadsafe::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 +39,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 +49,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 +81,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 +117,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 db07aa1..d539a7c 100644 --- a/lib/src/flow.rs +++ b/lib/src/flow.rs @@ -1,29 +1,29 @@ -use std::{cell::RefCell, rc::Rc}; - use serde_json::Map; use crate::{ callstack::{CallStack, Thread}, choice::Choice, container::Container, - json::{json_read, json_write}, + json_read, json_write, object::RTObject, story_error::StoryError, + threadsafe::BrCell, + threadsafe::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 +31,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 +55,7 @@ impl Flow { )? .iter() .map(|o| o.clone().into_any().downcast::().unwrap()) - .collect::>>(), + .collect::>>(), }; flow.callstack.borrow_mut().load_json( @@ -131,13 +131,13 @@ 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 .borrow() .get_thread_with_index(*choice.original_thread_index.borrow()) - .map(|o| choice.set_thread_at_generation(o.clone())) + .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())) diff --git a/lib/src/ink_list.rs b/lib/src/ink_list.rs index fda22c0..97e272a 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, + list_definitions_origin::ListDefinitionsOrigin, story_error::StoryError, threadsafe::BrCell, + value_type::ValueType, }; #[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)), } } @@ -182,9 +183,7 @@ impl InkList { let mut list = InkList::new(); for origin in self.origins.borrow_mut().iter_mut() { - for (k, v) in origin.get_items().iter() { - list.items.insert(k.clone(), *v); - } + list.items = origin.get_items().clone() } list diff --git a/lib/src/json/json_read_stream.rs b/lib/src/json/json_read_stream.rs deleted file mode 100644 index 139e3e2..0000000 --- a/lib/src/json/json_read_stream.rs +++ /dev/null @@ -1,648 +0,0 @@ -//! This is a JSON parser that process the JSON in a streaming fashion. It can be used as a replacement for the Serde based parser. -//! This is useful for large JSON files that don't fit in memory hence the JSON is not loaded all at once as Serde does. -//! This parser has been used to load 'The Intercept' example story in an ESP32-s2 microcontroller with an external RAM of 2MB. With the Serde based parser, it is impossible, it does not have enogh memory to load the story. - -use std::{collections::HashMap, rc::Rc}; - -use crate::{ - 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::{INK_VERSION_CURRENT, INK_VERSION_MINIMUM_COMPATIBLE}, - story_error::StoryError, - tag::Tag, - value::Value, - variable_assigment::VariableAssignment, - variable_reference::VariableReference, - void::Void, -}; - -use super::json_tokenizer::{JsonTokenizer, JsonValue}; - -pub fn load_from_string( - s: &str, -) -> Result<(i32, Rc, Rc), StoryError> { - let mut tok = JsonTokenizer::new_from_str(s); - - parse(&mut tok) -} - -fn parse( - tok: &mut JsonTokenizer, -) -> Result<(i32, Rc, Rc), StoryError> { - tok.expect('{')?; - - let version_key = tok.read_obj_key()?; - - if version_key != "inkVersion" { - return Err(StoryError::BadJson( - "ink version number not found. Are you sure it's a valid .ink.json file?".to_owned(), - )); - } - - let version: i32 = tok.read_number().unwrap().as_integer().unwrap(); - - if version > INK_VERSION_CURRENT { - 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(), - )); - } - - tok.expect(',')?; - - let root_key = tok.read_obj_key()?; - - if root_key != "root" { - return Err(StoryError::BadJson( - "Root node for ink not found. Are you sure it's a valid .ink.json file?".to_owned(), - )); - } - - let root_value = tok.read_value()?; - let main_content_container = match jtoken_to_runtime_object(tok, root_value, None)? { - ArrayElement::RTObject(rt_obj) => rt_obj, - _ => { - return Err(StoryError::BadJson( - "Root node for ink is not a container?".to_owned(), - )) - } - }; - - 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(), - )); - }; - - let main_content_container = main_content_container.unwrap(); // unwrap: checked for err above - - tok.expect(',')?; - let list_defs_key = tok.read_obj_key()?; - - if list_defs_key != "listDefs" { - return Err(StoryError::BadJson( - "List Definitions node for ink not found. Are you sure it's a valid .ink.json file?" - .to_owned(), - )); - } - - let list_defs = Rc::new(jtoken_to_list_definitions(tok)?); - - tok.expect('}')?; - - Ok((version, main_content_container, list_defs)) -} - -enum ArrayElement { - RTObject(Rc), - LastElement(i32, Option, HashMap>), - NullElement, -} - -fn jtoken_to_runtime_object( - tok: &mut JsonTokenizer, - value: JsonValue, - name: Option, -) -> Result { - match value { - JsonValue::Null => Ok(ArrayElement::NullElement), - JsonValue::Boolean(value) => Ok(ArrayElement::RTObject(Rc::new(Value::new::(value)))), - JsonValue::Number(value) => { - if value.is_integer() { - let val: i32 = value.as_integer().unwrap(); - Ok(ArrayElement::RTObject(Rc::new(Value::new::(val)))) - } else { - let val: f32 = value.as_float().unwrap(); - Ok(ArrayElement::RTObject(Rc::new(Value::new::(val)))) - } - } - JsonValue::String(value) => { - let str = value.as_str(); - - // String value - let first_char = str.chars().next().unwrap(); - if first_char == '^' { - return Ok(ArrayElement::RTObject(Rc::new(Value::new::<&str>( - &str[1..], - )))); - } else if first_char == '\n' && str.len() == 1 { - return Ok(ArrayElement::RTObject(Rc::new(Value::new::<&str>("\n")))); - } - - // Glue - if "<>".eq(str) { - return Ok(ArrayElement::RTObject(Rc::new(Glue::new()))); - } - - if let Some(control_command) = ControlCommand::new_from_name(str) { - return Ok(ArrayElement::RTObject(Rc::new(control_command))); - } - - // 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. - 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(ArrayElement::RTObject(Rc::new(native_function_call))); - } - - // Void - if "void".eq(str) { - return Ok(ArrayElement::RTObject(Rc::new(Void::new()))); - } - - Err(StoryError::BadJson(format!( - "Failed to convert token to runtime RTObject: {}", - str - ))) - } - JsonValue::Array => Ok(ArrayElement::RTObject(jarray_to_container(tok, name)?)), - JsonValue::Object => { - let prop = tok.read_obj_key()?; - let prop_value = tok.read_value()?; - - // Divert target value to path - if prop == "^->" { - tok.expect('}')?; - return Ok(ArrayElement::RTObject(Rc::new(Value::new::( - Path::new_with_components_string(prop_value.as_str()), - )))); - } - - // // VariablePointerValue - if prop == "^var" { - let variable_name = prop_value.as_str().unwrap(); - let mut contex_index = -1; - - if tok.peek()? == ',' { - tok.expect(',')?; - tok.expect_obj_key("ci")?; - contex_index = tok.read_number().unwrap().as_integer().unwrap(); - } - - let var_ptr = Rc::new(Value::new_variable_pointer(variable_name, contex_index)); - tok.expect('}')?; - return Ok(ArrayElement::RTObject(var_ptr)); - } - - // // Divert - let mut is_divert = false; - let mut pushes_to_stack = false; - let mut div_push_type = PushPopType::Function; - let mut external = false; - - if prop == "->" { - is_divert = true; - } else if prop == "f()" { - is_divert = true; - pushes_to_stack = true; - div_push_type = PushPopType::Function; - } else if prop == "->t->" { - is_divert = true; - pushes_to_stack = true; - div_push_type = PushPopType::Tunnel; - } else if prop == "x()" { - is_divert = true; - external = true; - pushes_to_stack = false; - div_push_type = PushPopType::Function; - } - - if is_divert { - let target = prop_value.as_str().unwrap().to_string(); - - let mut var_divert_name: Option = None; - let mut target_path: Option = None; - - let mut conditional = false; - let mut external_args = 0; - - while tok.peek()? == ',' { - tok.expect(',')?; - let prop = tok.read_obj_key()?; - let prop_value = tok.read_value()?; - - // Variable target - if prop == "var" { - var_divert_name = Some(target.clone()); - } else if prop == "c" { - conditional = true; - } else if prop == "exArgs" { - external_args = prop_value.as_integer().unwrap() as usize; - } - } - - if var_divert_name.is_none() { - target_path = Some(target); - } - - tok.expect('}')?; - return Ok(ArrayElement::RTObject(Rc::new(Divert::new( - pushes_to_stack, - div_push_type, - external, - external_args, - conditional, - var_divert_name, - target_path.as_deref(), - )))); - } - - // Choice - if prop == "*" { - let mut flags = 0; - let path_string_on_choice = prop_value.as_str().unwrap(); - - if tok.peek()? == ',' { - tok.expect(',')?; - tok.expect_obj_key("flg")?; - flags = tok.read_number().unwrap().as_integer().unwrap(); - } - - tok.expect('}')?; - return Ok(ArrayElement::RTObject(Rc::new(ChoicePoint::new( - flags, - path_string_on_choice, - )))); - } - - // Variable reference - if prop == "VAR?" { - tok.expect('}')?; - return Ok(ArrayElement::RTObject(Rc::new(VariableReference::new( - prop_value.as_str().unwrap(), - )))); - } - - if prop == "CNT?" { - tok.expect('}')?; - return Ok(ArrayElement::RTObject(Rc::new( - VariableReference::from_path_for_count(prop_value.as_str().unwrap()), - ))); - } - - // Variable assignment - let mut is_var_ass = false; - let mut is_global_var = false; - - if prop == "VAR=" { - is_var_ass = true; - is_global_var = true; - } else if prop == "temp=" { - is_var_ass = true; - is_global_var = false; - } - - if is_var_ass { - let var_name = prop_value.as_str().unwrap(); - let mut is_new_decl = true; - - if tok.peek()? == ',' { - tok.expect(',')?; - tok.expect_obj_key("re")?; - let _ = tok.read_boolean()?; - is_new_decl = false; - } - - let var_ass = Rc::new(VariableAssignment::new( - var_name, - is_new_decl, - is_global_var, - )); - tok.expect('}')?; - return Ok(ArrayElement::RTObject(var_ass)); - } - - // // Legacy Tag - if prop == "#" { - tok.expect('}')?; - return Ok(ArrayElement::RTObject(Rc::new(Tag::new( - prop_value.as_str().unwrap(), - )))); - } - - // List value - if prop == "list" { - let list_content = parse_list(tok)?; - let mut raw_list = InkList::new(); - - if tok.peek()? == ',' { - tok.expect(',')?; - tok.expect_obj_key("origins")?; - - // read array of strings - tok.expect('[')?; - - let mut names = Vec::new(); - while tok.peek()? != ']' { - let name = tok.read_string()?; - names.push(name); - - if tok.peek()? != ']' { - tok.expect(',')?; - } - } - - tok.expect(']')?; - - raw_list.set_initial_origin_names(names); - } - - for (k, v) in list_content { - let item = InkListItem::from_full_name(k.as_str()); - raw_list.items.insert(item, v); - } - - tok.expect('}')?; - return Ok(ArrayElement::RTObject(Rc::new(Value::new::( - raw_list, - )))); - } - - // Used when serialising save state only - if prop == "originalChoicePath" { - todo!("originalChoicePath"); - // return jobject_to_choice(obj); // TODO - } - - // Last Element - let mut flags = 0; - let mut name: Option = None; - let mut named_only_content: HashMap> = HashMap::new(); - - let mut p = prop.clone(); - let mut pv = prop_value; - - loop { - if p == "#f" { - flags = pv.as_integer().unwrap(); - } else if p == "#n" { - name = Some(pv.as_str().unwrap().to_string()); - } else { - let named_content_item = jtoken_to_runtime_object(tok, pv, Some(p.clone()))?; - - let named_content_item = match named_content_item { - ArrayElement::RTObject(rt_obj) => rt_obj, - _ => { - return Err(StoryError::BadJson( - "Named content is not a runtime object".to_owned(), - )) - } - }; - - let named_sub_container = named_content_item - .into_any() - .downcast::() - .unwrap(); - - named_only_content.insert(p, named_sub_container); - } - - if tok.peek()? == ',' { - tok.expect(',')?; - p = tok.read_obj_key()?; - pv = tok.read_value()?; - } else if tok.peek()? == '}' { - tok.expect('}')?; - return Ok(ArrayElement::LastElement(flags, name, named_only_content)); - } else { - break; - } - } - - Err(StoryError::BadJson(format!( - "Failed to convert token to runtime RTObject: {}", - prop - ))) - } - } -} - -fn parse_list(tok: &mut JsonTokenizer) -> Result, StoryError> { - let mut list_content: HashMap = HashMap::new(); - - while tok.peek()? != '}' { - let key = tok.read_obj_key()?; - let value = tok.read_number().unwrap().as_integer().unwrap(); - list_content.insert(key, value); - - if tok.peek()? != '}' { - tok.expect(',')?; - } - } - - tok.expect('}')?; - - Ok(list_content) -} - -fn jarray_to_container( - tok: &mut JsonTokenizer, - name: Option, -) -> Result, StoryError> { - let (content, named) = jarray_to_runtime_obj_list(tok)?; - - // 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 = name; - let mut flags = 0; - let mut named_only_content: HashMap> = HashMap::new(); - - if let Some(ArrayElement::LastElement(f, n, named_content)) = named { - flags = f; - - if n.is_some() { - name = n; - } - - named_only_content = named_content; - } - - let container = Container::new(name, flags, content, named_only_content); - Ok(container) -} - -fn jarray_to_runtime_obj_list( - tok: &mut JsonTokenizer, -) -> Result<(Vec>, Option), StoryError> { - let mut list: Vec> = Vec::new(); - let mut last_element: Option = None; - - while tok.peek()? != ']' { - let val = tok.read_value()?; - let runtime_obj = jtoken_to_runtime_object(tok, val, None)?; - - match runtime_obj { - ArrayElement::LastElement(flags, name, named_only_content) => { - last_element = Some(ArrayElement::LastElement(flags, name, named_only_content)); - break; - } - ArrayElement::RTObject(rt_obj) => list.push(rt_obj), - ArrayElement::NullElement => { - // Only the last element can be null - if tok.peek()? != ']' { - return Err(StoryError::BadJson( - "Only the last element can be null".to_owned(), - )); - } - } - } - - if tok.peek()? != ']' { - tok.expect(',')?; - } - } - - tok.expect(']')?; - - Ok((list, last_element)) -} - -fn jtoken_to_list_definitions( - tok: &mut JsonTokenizer, -) -> Result { - let mut all_defs: Vec = Vec::with_capacity(0); - - tok.expect('{')?; - - while tok.peek()? != '}' { - let name = tok.read_obj_key()?; - tok.expect('{')?; - - let items = parse_list(tok)?; - let def = ListDefinition::new(name, items); - all_defs.push(def); - - if tok.peek()? != '}' { - tok.expect(',')?; - } - } - - tok.expect('}')?; - - Ok(ListDefinitionsOrigin::new(&mut all_defs)) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn simple_load() { - let s = r##"{"inkVersion":21,"root":[["^Line.","\n",["done",{"#n":"g-0"}],null],"done",null],"listDefs":{}}"##; - let _ = load_from_string(s).unwrap(); - } - - #[test] - fn load_list() { - let s = r##" - { - "inkVersion": 21, - "root": [ - [ - "ev", - { - "VAR?": "A" - }, - { - "VAR?": "B" - }, - "+", - "LIST_ALL", - "out", - "/ev", - "\n", - [ - "done", - { - "#f": 5, - "#n": "g-0" - } - ], - null - ], - "done", - { - "global decl": [ - "ev", - { - "list": {}, - "origins": [ - "a" - ] - }, - { - "VAR=": "a" - }, - { - "list": {}, - "origins": [ - "b" - ] - }, - { - "VAR=": "b" - }, - "/ev", - "end", - null - ], - "#f": 1 - } - ], - "listDefs": { - "a": { - "A": 1 - }, - "b": { - "B": 1 - } - } - } - "##; - let _ = load_from_string(s).unwrap(); - } - - #[test] - fn load_choice() { - let s = r##"{"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":{}}"##; - let (_, container, _) = load_from_string(s).unwrap(); - let mut sb = String::new(); - container.build_string_of_hierarchy(&mut sb, 0, None); - println!("{}", sb); - } - - #[test] - fn load_iffalse() { - let s = r##"{"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":{}}"##; - let (_, container, _) = load_from_string(s).unwrap(); - let mut sb = String::new(); - container.build_string_of_hierarchy(&mut sb, 0, None); - println!("{}", sb); - } -} diff --git a/lib/src/json/json_tokenizer.rs b/lib/src/json/json_tokenizer.rs deleted file mode 100644 index 6acd7a6..0000000 --- a/lib/src/json/json_tokenizer.rs +++ /dev/null @@ -1,309 +0,0 @@ -//! Tokenizer for the streamed JSON parser. -use std::io::{self, Read}; - -#[derive(Debug)] -pub(super) enum Number { - Int(i32), - Float(f32), -} - -impl Number { - pub(super) fn as_integer(&self) -> Option { - match self { - Number::Int(n) => Some(*n), - Number::Float(n) => Some(*n as i32), - } - } - - pub(super) fn as_float(&self) -> Option { - match self { - Number::Int(n) => Some(*n as f32), - Number::Float(n) => Some(*n), - } - } - - pub(super) fn is_integer(&self) -> bool { - match self { - Number::Int(_) => true, - Number::Float(_) => false, - } - } -} - -#[derive(Debug)] -pub(super) enum JsonValue { - Array, - Object, - String(String), - Number(Number), - Boolean(bool), - Null, -} - -impl JsonValue { - pub(super) fn as_str(&self) -> Option<&str> { - match self { - JsonValue::String(s) => Some(s), - _ => None, - } - } - - pub(super) fn as_integer(&self) -> Option { - match self { - JsonValue::Number(n) => n.as_integer(), - _ => None, - } - } -} - -pub(super) struct JsonTokenizer<'a> { - json: &'a [u8], - lookahead: Option, - skip_whitespaces: bool, -} - -impl<'a> JsonTokenizer<'a> { - pub(super) fn new_from_str(s: &'a str) -> JsonTokenizer<'a> { - JsonTokenizer { - json: s.as_bytes(), - lookahead: None, - skip_whitespaces: true, - } - } - - pub(super) fn read(&mut self) -> io::Result { - let c = match self.lookahead { - Some(c) => { - self.lookahead = None; - c - } - None => self.read_no_lookahead()?, - }; - - Ok(c) - } - - fn read_no_lookahead(&mut self) -> io::Result { - let c = loop { - let c = self.read_utf8_char()?; - - if !self.skip_whitespaces || !c.is_whitespace() { - break c; - } - }; - - Ok(c) - } - - fn read_utf8_char(&mut self) -> io::Result { - let mut temp_buf = [0; 1]; - let mut utf8_char = Vec::new(); - - // Read bytes until a valid UTF-8 character is formed - loop { - self.json.read_exact(&mut temp_buf)?; - utf8_char.push(temp_buf[0]); - - if let Ok(utf8_str) = std::str::from_utf8(&utf8_char) { - if let Some(ch) = utf8_str.chars().next() { - return Ok(ch); - } - } - - // If we have read 4 bytes and still not a valid character, return an error - if utf8_char.len() >= 4 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "Invalid UTF-8 sequence", - )); - } - } - } - - pub(super) fn peek(&mut self) -> io::Result { - match self.lookahead { - Some(c) => Ok(c), - None => { - let c = self.read_no_lookahead()?; - self.lookahead = Some(c); - Ok(c) - } - } - } - - pub(super) fn read_boolean(&mut self) -> io::Result { - let string = self.read_until_separator()?; - - match string.trim() { - "true" => Ok(true), - "false" => Ok(false), - _ => Err(io::Error::new( - io::ErrorKind::InvalidData, - "Invalid boolean format", - )), - } - } - - pub(super) fn read_null(&mut self) -> io::Result<()> { - let string = self.read_until_separator()?; - - if string.trim() == "null" { - Ok(()) - } else { - Err(io::Error::new( - io::ErrorKind::InvalidData, - "Invalid null format", - )) - } - } - - pub(super) fn read_number(&mut self) -> io::Result { - let number_str = self.read_until_separator()?; - let number_str = number_str.trim(); - - // Check if the number is an integer - if let Ok(num) = number_str.parse::() { - return Ok(Number::Int(num)); - } - - // Convert the accumulated string to a f32 - match number_str.parse::() { - Ok(num) => Ok(Number::Float(num)), - Err(_) => Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("Invalid number format: '{}'", number_str), - )), - } - } - - pub(super) fn read_string(&mut self) -> io::Result { - let mut result = String::new(); - let mut escape = false; - - self.expect('"')?; - self.skip_whitespaces = false; - - while let Ok(c) = self.read() { - if escape { - // Handle escape sequences - match c { - '\\' => result.push('\\'), - '"' => result.push('"'), - 'n' => result.push('\n'), - // 't' => result.push('\t'), - // 'r' => result.push('\r'), - // Add other escape sequences as needed - // _ => result.push(c), // Push the character as is if unknown escape - _ => {} - } - escape = false; - } else if c == '\\' { - escape = true; - } else if c == '"' { - self.skip_whitespaces = true; - break; // End of the quoted string - } else { - result.push(c); - } - } - - if !escape { - Ok(result) - } else { - Err(io::Error::new( - io::ErrorKind::InvalidData, - "Unterminated string", - )) - } - } - - fn read_until_separator(&mut self) -> io::Result { - let mut result = String::new(); - - self.skip_whitespaces = false; - - while !self.next_is_separator() { - let c = self.read()?; - result.push(c); - } - - self.skip_whitespaces = true; - - Ok(result) - } - - fn next_is_separator(&mut self) -> bool { - match self.peek() { - Ok(c) => c == ',' || c == '}' || c == ']', - Err(_) => true, - } - } - - pub(super) fn expect(&mut self, c: char) -> io::Result<()> { - while let Ok(c2) = self.read() { - if !c2.is_whitespace() { - if c2 == c { - return Ok(()); - } else { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("Expected '{}', found '{}'", c, c2), - )); - } - } - } - - Err(io::Error::new( - io::ErrorKind::UnexpectedEof, - "Unexpected end of file", - )) - } - - pub(super) fn read_obj_key(&mut self) -> io::Result { - let s = self.read_string(); - self.expect(':')?; - s - } - - pub(super) fn expect_obj_key(&mut self, expected: &str) -> io::Result<()> { - let s = self.read_string()?; - if s != expected { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("Expected '{}', found '{}'", expected, s), - )); - } - let _ = self.expect(':'); - Ok(()) - } - - pub(super) fn read_value(&mut self) -> io::Result { - //self.skip_whitespaces()?; - match self.peek()? { - '[' => { - self.read()?; - Ok(JsonValue::Array) - } - '{' => { - self.read()?; - Ok(JsonValue::Object) - } - '"' => { - let s = self.read_string()?; - Ok(JsonValue::String(s)) - } - 't' | 'f' => { - let b = self.read_boolean()?; - Ok(JsonValue::Boolean(b)) - } - 'n' => { - self.read_null()?; - Ok(JsonValue::Null) - } - _ => { - let n = self.read_number()?; - Ok(JsonValue::Number(n)) - } - } - } -} diff --git a/lib/src/json/mod.rs b/lib/src/json/mod.rs deleted file mode 100644 index b5745e6..0000000 --- a/lib/src/json/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod json_read; -pub mod json_read_stream; -mod json_tokenizer; -pub mod json_write; diff --git a/lib/src/json/json_read.rs b/lib/src/json_read.rs similarity index 69% rename from lib/src/json/json_read.rs rename to lib/src/json_read.rs index 72f72a2..c861126 100644 --- a/lib/src/json/json_read.rs +++ b/lib/src/json_read.rs @@ -1,107 +1,34 @@ -use std::{collections::HashMap, rc::Rc}; +use std::collections::HashMap; use serde_json::Map; use crate::{ - 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::{INK_VERSION_CURRENT, INK_VERSION_MINIMUM_COMPATIBLE}, - story_error::StoryError, - tag::Tag, - value::Value, - variable_assigment::VariableAssignment, - variable_reference::VariableReference, - void::Void, + 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, + threadsafe::Brc, value::Value, variable_assigment::VariableAssignment, + variable_reference::VariableReference, void::Void, }; -pub fn load_from_string( - s: &str, -) -> Result<(i32, Rc, Rc), StoryError> { - let json: serde_json::Value = match serde_json::from_str(s) { - Ok(value) => value, - 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(StoryError::BadJson( - "ink version number not found. Are you sure it's a valid .ink.json file?".to_owned(), - )); - } - - let version: i32 = version_opt.unwrap().as_i64().unwrap().try_into().unwrap(); - - if version > INK_VERSION_CURRENT { - 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())); - } - - let root_token = match json.get("root") { - Some(value) => value, - None => { - return Err(StoryError::BadJson( - "Root node for ink not found. Are you sure it's a valid .ink.json file?".to_owned(), - )) - } - }; - - let list_definitions = match json.get("listDefs") { - Some(def) => Rc::new(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?" - .to_owned(), - )), - }; - - let main_content_container = jtoken_to_runtime_object(root_token, None)?; - - 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(), - )); - }; - - let main_content_container = main_content_container.unwrap(); // unwrap: checked for err above - - Ok((version, main_content_container, list_definitions)) -} - 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::(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::(val))) + Ok(Brc::new(Value::new_int(val))) } else { let val: f32 = token.as_f64().unwrap() as f32; - Ok(Rc::new(Value::new::(val))) + Ok(Brc::new(Value::new_float(val))) } } @@ -111,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::<&str>(&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::<&str>("\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 @@ -134,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!( @@ -153,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::( + return Ok(Brc::new(Value::new_divert_target( Path::new_with_components_string(prop_value.as_str()), ))); } @@ -170,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); } @@ -233,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, @@ -254,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, ))); @@ -263,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(), ))); } @@ -297,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, @@ -308,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 @@ -336,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::(raw_list))); + return Ok(Brc::new(Value::new_list(raw_list))); } // Used when serialising save state only @@ -355,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 @@ -364,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 { @@ -398,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); @@ -415,38 +342,24 @@ 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(); - let choice_tags = jarray_to_tags(obj); - Ok(Rc::new(Choice::new_from_json( + Ok(Brc::new(Choice::new_from_json( path_string_on_choice, source_path.to_string(), text, index, original_thread_index, - choice_tags, ))) } -fn jarray_to_tags(obj: &Map) -> Vec { - let mut tags: Vec = Vec::new(); - - let prop_value = obj.get("tags"); - if let Some(pv) = prop_value { - let tags_array = pv.as_array().unwrap(); - for tag in tags_array { - tags.push(tag.as_str().unwrap().to_string()); - } - } - - tags -} - pub fn jtoken_to_list_definitions( def: &serde_json::Value, ) -> Result { @@ -468,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/json_write.rs b/lib/src/json_write.rs similarity index 85% rename from lib/src/json/json_write.rs rename to lib/src/json_write.rs index 4a02990..2af0676 100644 --- a/lib/src/json/json_write.rs +++ b/lib/src/json_write.rs @@ -1,30 +1,17 @@ -use std::{collections::HashMap, rc::Rc}; +use std::collections::HashMap; use serde_json::{json, Map}; 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, - path::Path, - push_pop::PushPopType, - story_error::StoryError, - tag::Tag, - value::Value, - value_type::{StringValue, VariablePointerValue}, - variable_assigment::VariableAssignment, - variable_reference::VariableReference, - void::Void, + 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, threadsafe::Brc, value::Value, + variable_assigment::VariableAssignment, variable_reference::VariableReference, void::Void, }; pub fn write_dictionary_values( - objs: &HashMap>, + objs: &HashMap>, ) -> Result { let mut jobjs: Map = Map::new(); @@ -35,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); } @@ -92,15 +79,15 @@ pub fn write_rtobject(o: Rc) -> Result(o.as_ref()) { + if let Some(v) = Value::get_int_value(o.as_ref()) { return Ok(json!(v)); } - if let Some(v) = Value::get_value::(o.as_ref()) { + if let Some(v) = Value::get_float_value(o.as_ref()) { return Ok(json!(v)); } - if let Some(v) = Value::get_value::<&StringValue>(o.as_ref()) { + if let Some(v) = Value::get_string_value(o.as_ref()) { let mut s = String::new(); if v.is_newline { @@ -113,17 +100,17 @@ pub fn write_rtobject(o: Rc) -> Result(o.as_ref()) { + if let Some(v) = Value::get_list_value(o.as_ref()) { return Ok(write_ink_list(v)); } - if let Some(v) = Value::get_value::<&Path>(o.as_ref()) { + 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_value::<&VariablePointerValue>(o.as_ref()) { + 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)); @@ -284,22 +271,11 @@ pub fn write_choice(choice: &Choice) -> serde_json::Value { json!(choice.target_path.to_string()), ); - jobj.insert("tags".to_owned(), write_choice_tags(choice)); - serde_json::Value::Object(jobj) } -fn write_choice_tags(choice: &Choice) -> serde_json::Value { - let mut tags: Vec = Vec::new(); - for t in &choice.tags { - tags.push(json!(t)); - } - - serde_json::Value::Array(tags) -} - 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 dab3c4a..c2edd9b 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,17 +1,15 @@ //! 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. +//! `bladeink` is fully compatible with the reference version and supports all its language features. //! //! To learn more about the Ink language, you can check [the official documentation](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md). //! -//! Here is a quick example that uses 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. //! //! ``` //! # use bladeink::{story::Story, story_error::StoryError}; //! # fn main() -> Result<(), StoryError> { -//! # let json_string = r##"{"inkVersion":21, "root":["done",null],"listDefs":{}}"##; -//! # let read_input = |_:&_| 0; +//! # 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)?; @@ -27,23 +25,19 @@ //! if !choices.is_empty() { //! // read_input is a method that you should implement //! // to get the choice selected by the user. -//! let choice_idx:usize = read_input(&choices); +//! let choice_idx = read_input(&choices)?; //! // set the option selected by the user //! story.choose_choice_index(choice_idx)?; //! } else { -//! break; +//! break; //! } //! } //! # Ok(()) //! # } //! ``` //! -//! 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. +//! 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; pub mod choice; @@ -55,7 +49,8 @@ mod flow; mod glue; mod ink_list; mod ink_list_item; -mod json; +mod json_read; +mod json_write; mod list_definition; mod list_definitions_origin; mod native_function_call; @@ -66,9 +61,11 @@ mod push_pop; mod search_result; mod state_patch; pub mod story; +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; diff --git a/lib/src/list_definitions_origin.rs b/lib/src/list_definitions_origin.rs index 53e4c0d..0d06272 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, threadsafe::Brc, value::Value}; #[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::(l)); + let list_value = Brc::new(Value::new_list(l)); list_definitions_origin .all_unambiguous_list_value_cache @@ -42,11 +42,7 @@ impl ListDefinitionsOrigin { self.lists.get(name) } - pub fn find_single_item_list_with_name(&self, name: &str) -> Option<&Rc> { - if name.trim().is_empty() { - return None; - } - + 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 c6980fa..0f05f14 100644 --- a/lib/src/native_function_call.rs +++ b/lib/src/native_function_call.rs @@ -1,9 +1,11 @@ -use std::{fmt, rc::Rc}; +use std::fmt; use crate::{ ink_list::InkList, object::{Object, RTObject}, story_error::StoryError, + threadsafe::brcell_borrow, + threadsafe::Brc, value::Value, value_type::ValueType, void::Void, @@ -206,8 +208,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(), @@ -218,10 +220,10 @@ impl NativeFunctionCall { for p in ¶ms { if p.as_ref().as_any().is::() { - return Err(StoryError::InvalidStoryState(format!("Attempting to perform {} on a void value. Did you forget to 'return' a value from a function you called here?", Self::get_name(self.op)))); + 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_value::<&InkList>(p.as_ref()).is_some() { + if Value::get_list_value(p.as_ref()).is_some() { has_list = true; } } @@ -239,12 +241,12 @@ 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_value::<&InkList>(params[0].as_ref()).is_some() - && Value::get_value::(params[1].as_ref()).is_some() + && 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)); } @@ -254,8 +256,8 @@ impl NativeFunctionCall { // And/or with any other type requires coercion to bool if (self.op == Op::And || self.op == Op::Or) - && (Value::get_value::<&InkList>(params[0].as_ref()).is_none() - || Value::get_value::<&InkList>(params[1].as_ref()).is_none()) + && (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 { @@ -265,12 +267,12 @@ impl NativeFunctionCall { } }; - return Ok(Rc::new(Value::new::(result))); + return Ok(Brc::new(Value::new_bool(result))); } // Normal (list • list) operation - if Value::get_value::<&InkList>(params[0].as_ref()).is_some() - && Value::get_value::<&InkList>(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()]; @@ -285,9 +287,9 @@ impl NativeFunctionCall { ))) } - fn call_list_increment_operation(&self, list_int_params: &[Rc]) -> Rc { - let list_val = Value::get_value::<&InkList>(list_int_params[0].as_ref()).unwrap(); - let int_val = Value::get_value::(list_int_params[1].as_ref()).unwrap(); + 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(); let mut result_raw_list = InkList::new(); @@ -300,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()) @@ -315,10 +317,10 @@ impl NativeFunctionCall { } } - Rc::new(Value::new::(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 +358,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 +378,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 +396,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::(*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::(*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::(*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::( + ValueType::List(op2) => Ok(Brc::new(Value::new_bool( !op1.items.is_empty() && !op2.items.is_empty(), ))), _ => Err(StoryError::InvalidStoryState( @@ -428,22 +430,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::(*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::(*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::(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 +456,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::(*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::(*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::(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 +484,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::(*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::(*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::(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,24 +513,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::(*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::(*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::(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(), )), @@ -539,22 +542,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::(*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::(*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::(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(), )), @@ -565,16 +568,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::(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::(op1 + op2))), + ValueType::Float(op2) => Ok(Brc::new(Value::new_float(op1 + op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), @@ -584,14 +587,14 @@ impl NativeFunctionCall { let mut sb = String::new(); sb.push_str(&op1.string); sb.push_str(&op2.string); - Ok(Rc::new(Value::new::<&str>(&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::(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(), )), @@ -602,16 +605,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::(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::(op1 / op2))), + ValueType::Float(op2) => Ok(Brc::new(Value::new_float(op1 / op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), @@ -622,18 +625,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::((op1 as f32).powf(op2 as f32)))) + 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::(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(), )), @@ -644,16 +647,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::(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::(op1 * op2))), + ValueType::Float(op2) => Ok(Brc::new(Value::new_float(op1 * op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), @@ -664,28 +667,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::(*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::(*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::(*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::( + ValueType::List(op2) => Ok(Brc::new(Value::new_bool( !op1.items.is_empty() || !op2.items.is_empty(), ))), _ => Err(StoryError::InvalidStoryState( @@ -698,11 +701,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::(*op1 == 0))), - ValueType::Float(op1) => Ok(Rc::new(Value::new::(*op1 == 0.0))), - ValueType::List(op1) => Ok(Rc::new(Value::new::(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, }))), @@ -712,16 +715,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::(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::(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(), )), @@ -732,16 +735,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::(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::(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(), )), @@ -752,42 +755,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::(*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::(*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::(*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::(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::(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::(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(), )), @@ -798,42 +799,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::(*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::(*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::(*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::(!op1.string.eq(&op2.string)))) + 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::(!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::(!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(), )), @@ -844,16 +845,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::(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::(op1 % op2))), + ValueType::Float(op2) => Ok(Brc::new(Value::new_float(op1 % op2))), _ => Err(StoryError::InvalidStoryState( "Operation not available for type.".to_owned(), )), @@ -864,10 +865,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::(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(), )), @@ -878,18 +879,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::( - op1.string.contains(&op2.string), - ))), + ValueType::String(op2) => { + 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::(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(), )), @@ -900,18 +901,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::( - !op1.string.contains(&op2.string), - ))), + ValueType::String(op2) => { + 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::(!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(), )), @@ -922,11 +923,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::(i.1))), - None => Ok(Rc::new(Value::new::(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(), @@ -934,95 +935,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::(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::(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::(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::(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::(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::(-op1))), - ValueType::Float(op1) => Ok(Rc::new(Value::new::(-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::(*op1))), - ValueType::Float(op1) => Ok(Rc::new(Value::new::(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::(*op1))), - ValueType::Float(op1) => Ok(Rc::new(Value::new::(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::(*op1))), - ValueType::Float(op1) => Ok(Rc::new(Value::new::(*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::(*op1 as f32))), - ValueType::Float(op1) => Ok(Rc::new(Value::new::(*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 1d05821..734bc80 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,20 +6,21 @@ use crate::{ container::Container, path::{Component, Path}, search_result::SearchResult, + threadsafe::BrCell, + threadsafe::Brc, }; -#[derive(Clone)] 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), } } @@ -32,12 +28,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 { @@ -50,7 +46,7 @@ impl Object { let mut comps: Vec = Vec::new(); let mut container = rtobject.get_object().get_parent(); - let mut child = rtobject; + let mut child = rtobject.clone(); let mut child_rc; while let Some(c) = container { @@ -106,7 +102,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(); @@ -122,7 +118,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 @@ -161,7 +157,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; @@ -183,7 +179,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() { @@ -204,12 +200,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..e851a7b 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}, + threadsafe::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..b029227 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, threadsafe::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 791235c..1f41ab8 100644 --- a/lib/src/state_patch.rs +++ b/lib/src/state_patch.rs @@ -1,43 +1,48 @@ -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, threadsafe::Brc, 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, } impl StatePatch { - pub fn new() -> StatePatch { - StatePatch { - globals: HashMap::new(), - changed_variables: HashSet::new(), - visit_counts: HashMap::new(), - turn_indices: HashMap::new(), + 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(), + }, } } - 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 new file mode 100644 index 0000000..0bae48f --- /dev/null +++ b/lib/src/story.rs @@ -0,0 +1,2387 @@ +//! [`Story`] is the entry point to load and run an Ink story. +use std::{ + collections::{HashMap, VecDeque}, + time::Instant, +}; + +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, + search_result::SearchResult, + story_callbacks::{ErrorHandler, ErrorType, ExternalFunctionDef, VariableObserver}, + 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, +}; + +/// 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)] +enum OutputStateChange { + NoChange, + ExtendedBeyondNewline, + NewlineRemoved, +} + +/// A `Story` is the core struct representing a complete Ink narrative, +/// managing evaluation and state. +pub struct Story { + main_content_container: Brc, + state: StoryState, + temporary_evaluation_container: Option>, + recursive_continue_count: usize, + async_continue_active: bool, + async_saving: bool, + 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) 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 { + /// 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, + 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(StoryError::BadJson( + "ink version number not found. Are you sure it's a valid .ink.json file?" + .to_owned(), + )); + } + + let version: i32 = version_opt.unwrap().as_i64().unwrap().try_into().unwrap(); + + if version > INK_VERSION_CURRENT { + 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())); + } + + let root_token = match json.get("root") { + Some(value) => value, + None => { + return Err(StoryError::BadJson( + "Root node for ink not found. Are you sure it's a valid .ink.json file?" + .to_owned(), + )) + } + }; + + let list_definitions = match json.get("listDefs") { + 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?" + .to_owned()), + ) + } + }; + + let main_content_container = json_read::jtoken_to_runtime_object(root_token, None)?; + + 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(), + )); + }; + + let main_content_container = main_content_container.unwrap(); // unwrap: checked for err above + + let mut story = Story { + 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, + saw_lookahead_unsafe_function_after_new_line: false, + state_snapshot_at_last_new_line: None, + on_error: None, + 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), + }; + + story.reset_globals()?; + + if version != INK_VERSION_CURRENT { + 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) + } + + #[inline] + pub(crate) fn get_state(&self) -> &StoryState { + &self.state + } + + #[inline] + pub(crate) fn get_state_mut(&mut self) -> &mut StoryState { + &mut self.state + } + + 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, + )?; + + // Continue, but without validating external bindings, + // since we may be doing this reset at initialisation time. + self.continue_internal(0.0)?; + + self.get_state().set_current_pointer(original_pointer); + } + + self.get_state_mut() + .variables_state + .snapshot_default_globals(); + + 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(); + + let cp = self.get_state().get_current_pointer().resolve(); + + let cp = cp.as_ref().map(|cp| cp.as_ref()); + + self.main_content_container + .build_string_of_hierarchy(&mut sb, 0, cp); + + 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")?; + + let mut sb = String::new(); + + while self.can_continue() { + sb.push_str(&self.cont()?); + } + + 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()?; + } + + self.continue_internal(millisecs_limit_async) + } + + 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; + + // 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(StoryError::InvalidStoryState( + "Can't continue - should check can_continue before calling Continue".to_owned(), + )); + } + + self.get_state_mut().set_did_safe_exit(false); + + 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 + // for the outermost call. + if self.recursive_continue_count == 1 { + self.state + .variables_state + .start_batch_observing_variable_changes(); + } + } + + // 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(e) => { + self.add_error(e.get_message(), false); + 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.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() + && !self.get_state().is_did_safe_exit() + && self.temporary_evaluation_container.is_none() + { + if self + .state + .get_callstack() + .borrow() + .can_pop_type(Some(PushPopType::Tunnel)) + { + self.add_error("unexpectedly reached end of content. Do you need a '->->' to return from a tunnel?", false); + } else if self + .state + .get_callstack() + .borrow() + .can_pop_type(Some(PushPopType::Function)) + { + self.add_error( + "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, + ); + } else { + self.add_error("unexpectedly reached end of content for unknown reason. Please debug compiler!", 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 { + 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; + } + + 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.get_state().has_error() || self.get_state().has_warning() { + match &self.on_error { + Some(on_err) => { + if self.get_state().has_error() { + for err in self.get_state().get_current_errors() { + 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.borrow_mut().error(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.get_state().has_error() { + sb.push_str(&self.get_state().get_current_errors().len().to_string()); + + if self.get_state().get_current_errors().len() == 1 { + sb.push_str(" error"); + } else { + sb.push_str(" errors"); + } + + if self.get_state().has_warning() { + sb.push_str(" and "); + } + } + + 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"); + } + } + + sb.push_str(". It is strongly suggested that you assign an error handler to story.onError. The first issue was: "); + + 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(), + ); + } + + return Err(StoryError::InvalidStoryState(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 + .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() + { + // 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, + ); + + // 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.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 + // ~ complexCalculation() // don't actually need this unless it generates text + if self.state_snapshot_at_last_new_line.is_none() { + 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(); + } + } + } + + 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()) + } + + 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(), + } + } + + 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(); //unwrap: state_snapshot_at_last_new_line checked Some in previous fn + + 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 + // the save was started but before the snapshot was made. + if !self.async_saving { + self.get_state_mut().apply_any_patch(); + } + } + + 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); + + if !is_warning { + self.get_state_mut().force_end(); + } + } + + fn reset_errors(&mut self) { + self.get_state_mut().reset_errors(); + } + + 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 Ok(()); + } + + // 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.get_state_mut().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 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? + if self.get_state().get_current_pointer().is_null() { + return Ok(()); + } + + if is_logic_or_flow_control { + should_add_to_stream = false; + } + + // Choice with condition? + if let Some(cco) = ¤t_content_obj { + // 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 { + self.get_state_mut() + .get_generated_choices_mut() + .push(choice); + } + + current_content_obj = None; + 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. + let var_pointer = + Value::get_variable_pointer_value(current_content_obj.as_ref().unwrap().as_ref()); + + 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(Brc::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()); + } + // Output stream content (i.e. not expression evaluation) + else { + self.get_state_mut() + .push_to_output_stream(current_content_obj.as_ref().unwrap().clone()); + } + } + + // 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. + 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(); + } + } + } + + Ok(()) + } + + 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 Ok(()), + }; + + // 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.is_empty() || all_choices.len() > invisible_choices.len() { + return Ok(()); + } + + 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.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); + } + + 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.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() + && 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(&mut self) { + // 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) { + // Normally we want to integrate the patch + // into the main global/counts dictionaries. + // However, if we're in the middle of async + // saving, we simply stay in a "patching" state, + // albeit with the newer cloned patch. + + if !self.async_saving { + self.get_state_mut().apply_any_patch(); + } + + // No longer need the snapshot. + self.state_snapshot_at_last_new_line = None; + } + + 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() + .increment_visit_count_for_container(container); + } + + if container.turn_index_should_be_counted { + self.get_state_mut() + .record_turn_index_visit_to_container(container); + } + } + } + + 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 Ok(false), + }; + + // Divert + 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)? { + return Ok(true); + } + } + + 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(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); + } else { + 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 { + 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, + )?; + return Ok(true); + } else { + 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, + ); + } + + if self.get_state().diverted_pointer.is_null() && !current_divert.is_external { + // error(format!("Divert resolution failed: {:?}", current_divert)); + } + + return Ok(true); + } + + 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(), + )); + } + + 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 + // output + 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 + // the + // only problem is when exporting text for viewing, it + // skips over numbers etc. + let text: Brc = + Brc::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(), + )); + } + 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 { + 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.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()); + } + + 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() + { + 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(), + ); + + 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() + ))); + } 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, + )?; + self.get_state_mut().set_diverted_pointer(p); + } + } + } + CommandType::BeginString => { + 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(), + )); + } + + 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 + 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.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::BeginString { + break; + } + } + + if obj.as_ref().as_any().downcast_ref::().is_some() { + content_to_retain.push_back(obj.clone()); + } + + if Value::get_string_value(obj.as_ref()).is_some() { + content_stack_for_string.push_back(obj.clone()); + } + } + + // Consume the content that was produced for this string + 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()); + } + + // 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.get_state().set_in_expression_evaluation(true); + self.get_state_mut() + .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(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(Brc::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(); + if Value::get_int_value(target.as_ref()).is_some() { + extra_note = format!(". Did you accidentally pass a read count ('knot_name') instead of a target {}", + "('-> knot_name')?").to_owned(); + } + + 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(); + + let otmp = self.content_at_path(target).correct_obj(); + let container = match &otmp { + Some(o) => o.clone().into_any().downcast::().ok(), + None => None, + }; + + 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); + } + } + 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, + ); + } + } + + self.get_state_mut() + .push_evaluation_stack(Brc::new(Value::new_int(either_count))); + } + 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() { + 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(), + )); + } + + let min_value = min_int.unwrap(); + let max_value = max_int.unwrap(); + + let random_range = max_value - min_value + 1; + + if random_range <= 0 { + 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; + + 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(Brc::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() { + return Err(StoryError::InvalidStoryState( + "Invalid value passed to SEED_RANDOM".to_owned(), + )); + } + + // 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(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(Brc::new(Value::new_int(count))); + } + CommandType::SequenceShuffleIndex => { + let shuffle_index = self.next_sequence_shuffle_index()?; + let v = Brc::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 + // 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()?; + } + // 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(), + )); + 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() + ))); + } + + if generated_list_value.is_none() { + generated_list_value = Some(Value::new_list(InkList::new())); + } + + self.get_state_mut() + .push_evaluation_stack(Brc::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() { + 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); + + self.get_state_mut() + .push_evaluation_stack(Brc::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(), + )); + } + + 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, 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); + + self.get_state_mut().previous_random = next_random as i32; + + new_list + } + }; + + self.get_state_mut() + .push_evaluation_stack(Brc::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 + // + // 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 { + return Err(StoryError::InvalidStoryState("Unexpected ControlCommand while extracting tag from choice".to_owned())); + } + } + + 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 = + 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); + } + // 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 Ok(true); + } + + // Variable assignment + 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 + // within + // 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() + .variables_state + .assign(var_ass, assigned_val)?; + + return Ok(true); + } + + // Variable reference + if let Ok(var_ref) = content_obj + .clone() + .into_any() + .downcast::() + { + let found_value: Brc; + + // Explicit read count value + if var_ref.path_for_count.is_some() { + let container = var_ref.get_container_for_count(); + let count = self + .get_state_mut() + .visit_count_for_container(container.as_ref().unwrap()); + found_value = Brc::new(Value::new_int(count)); + } + // Normal variable reference + else { + 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 = Brc::new(Value::new_int(0)); + } + } + } + + self.get_state_mut().push_evaluation_stack(found_value); + + return Ok(true); + } + + // 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()); + + let result = func.call(func_params)?; + self.get_state_mut().push_evaluation_stack(result); + + return Ok(true); + } + + Ok(false) + } + + fn next_content(&mut self) -> Result<(), StoryError> { + // Setting previousContentObject is critical for + // VisitChangedContainersDueToDivert + let cp = self.get_state().get_current_pointer(); + self.get_state_mut().set_previous_pointer(cp); + + // Divert step? + 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.get_state().get_current_pointer().is_null() { + return Ok(()); + } + + // 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 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 { + // Pop from the call stack + 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(Brc::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()?; + + did_pop = true; + } else { + self.get_state_mut() + .try_exit_function_evaluation_from_game(); + } + + // Step past the point where we last called out + if did_pop && !self.get_state().get_current_pointer().is_null() { + self.next_content()?; + } + } + + Ok(()) + } + + 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(); + pointer.index += 1; + + 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(); + + if next_ancestor.is_none() { + break; + } + + let rto: Brc = container; + let index_in_ancestor = next_ancestor + .as_ref() + .unwrap() + .content + .iter() + .position(|s| Brc::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; + + successful_increment = true; + } + + if !successful_increment { + pointer = pointer::NULL.clone(); + } + + self.get_state() + .get_callstack() + .as_ref() + .borrow_mut() + .get_current_element_mut() + .current_pointer = pointer; + + successful_increment + } + + /// 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(); + + 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()); + choices.push(c.clone()); + } + } + } + + choices + } + + /// 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 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`. + pub fn get_current_warnings(&self) -> &Vec { + self.get_state().get_current_warnings() + } + + /// 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() { + 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. + // 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.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)?; + + // Take a note of newly visited containers for read counts etc + self.visit_changed_containers_due_to_divert(); + + Ok(()) + } + + fn is_truthy(&self, obj: Brc) -> Result { + 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))); + } + + return val.is_truthy(); + } + + Ok(truthy) + } + + fn process_choice( + &mut self, + choice_point: &Brc, + ) -> 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)? { + show_choice = 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() { + 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; + } + } + + // 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 !show_choice { + return Ok(None); + } + + start_text.push_str(&choice_only_text); + + 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(), + tags, + self.get_state().get_callstack().borrow_mut().fork_thread(), + start_text.trim().to_string(), + )); + + Ok(Some(choice)) + } + + 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(); + + 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(crate) fn pointer_at_path( + main_content_container: &Brc, + 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 = + 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.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.container(); + p.index = -1; + + result + }; + + let main_container: Brc = main_content_container.clone(); + + 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 + ))); + } else if result.approximate { + // TODO + // 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()) + { + 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 all_children_entered_at_start = true; + + while let Some(current_container) = current_container_ancestor { + if !self + .prev_containers + .iter() + .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, + // by checking whether the child Object is the first. + let entering_at_start = current_container + .content + .first() + .map(|first_child| { + Brc::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; + } + } + } + + /// 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, + 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(), + )); + } + + // 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('\''); + + return Err(StoryError::BadArgument(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 + self.get_state_mut() + .complete_function_evaluation_from_game() + } + + 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() + } + + 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 { + 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(), + )); + }; + + 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 Ok(chosen_index); + } + } + + Err(StoryError::InvalidStoryState( + "Should never reach here".to_owned(), + )) + } + + /// 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) + } + + 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; + } 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( + 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 { + break; + } + } + } + } + + Ok(tags) + } + + fn content_at_path(&self, path: &Path) -> SearchResult { + self.main_content_container.content_at_path(path, 0, -1) + } + + /// 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()`](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: + /// + ///```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. + /// + /// 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 [`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 `reset_callstack` parameter if you + /// don't want this behaviour, leaving any active threads, tunnels or + /// function calls intact. + /// + /// 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, `choose_path_string` + /// will throw an error. + 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 { + 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())); + } + + 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)?; + + Ok(()) + } + + fn reset_callstack(&mut self) -> Result<(), StoryError> { + self.if_async_we_cant("ResetCallstack")?; + + self.get_state_mut().force_end(); + + Ok(()) + } + + /// 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")?; + + if self.async_saving { + 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(()) + } + + /// 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) + } + + /// 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 continue_async(). Make more continue_async() calls or a single cont() call beforehand.", activity_str))); + } + + 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, + 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(()) + } + + /// 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 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/choices.rs b/lib/src/story/choices.rs deleted file mode 100644 index d19ef50..0000000 --- a/lib/src/story/choices.rs +++ /dev/null @@ -1,181 +0,0 @@ -use crate::{ - choice::Choice, choice_point::ChoicePoint, object::Object, path::Path, story::Story, - story_error::StoryError, tag::Tag, value::Value, value_type::StringValue, -}; -use std::rc::Rc; -/// # Choices -/// Methods to get and select choices. -impl Story { - /// 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() { - 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. - // 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.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(()) - } - - pub(crate) 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(()) - } - - pub(crate) 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)? { - show_choice = 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() { - 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; - } - } - - // 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 !show_choice { - 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(), - )); - - Ok(Some(choice)) - } - - pub(crate) 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 Ok(()), - }; - - // 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.is_empty() || all_choices.len() > invisible_choices.len() { - return Ok(()); - } - - 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.get_state() - .get_callstack() - .as_ref() - .borrow_mut() - .set_current_thread(choice.get_thread_at_generation().unwrap().clone()); - - // 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); - } - - self.choose_path(&choice.target_path, false) - } - - 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_value::<&StringValue>(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(); - tags.insert(0, tag.get_text().clone()); // popped in reverse - // order - } - - choice_only_str_val.string.to_string() - } -} diff --git a/lib/src/story/control_logic.rs b/lib/src/story/control_logic.rs deleted file mode 100644 index 67725d2..0000000 --- a/lib/src/story/control_logic.rs +++ /dev/null @@ -1,699 +0,0 @@ -use crate::{ - container::Container, - control_command::{CommandType, ControlCommand}, - divert::Divert, - ink_list::InkList, - ink_list_item::InkListItem, - native_function_call::NativeFunctionCall, - object::RTObject, - path::Path, - pointer, - push_pop::PushPopType, - story::Story, - story_error::StoryError, - story_state::StoryState, - tag::Tag, - value::Value, - value_type::{StringValue, ValueType}, - variable_assigment::VariableAssignment, - variable_reference::VariableReference, - void::Void, -}; -use rand::{rngs::StdRng, Rng, SeedableRng}; -use std::{ - collections::{HashMap, VecDeque}, - rc::Rc, -}; - -/// # Control and Logic -/// Methods for performing logic and flow control. -impl Story { - pub(crate) 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 Ok(false), - }; // Divert - 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)? { - return Ok(true); - } - } - - 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(target) = Value::get_value::<&Path>(var_contents.as_ref()) { - let p = Self::pointer_at_path(&self.main_content_container, target)?; - self.get_state_mut().set_diverted_pointer(p); - } else { - 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 { - 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, - )?; - return Ok(true); - } else { - 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, - ); - } - - if self.get_state().diverted_pointer.is_null() && !current_divert.is_external { - // error(format!("Divert resolution failed: {:?}", - // current_divert)); - } - - return Ok(true); - } - - 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(), - )); - } - - 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 - // output - 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 - // the - // only problem is when exporting text for viewing, it - // skips over numbers etc. - let text: Rc = - Rc::new(Value::new::<&str>(&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(), - )); - } - 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 { - 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.get_state_mut().pop_evaluation_stack(); - if let Some(v) = Value::get_value::<&Path>(popped.as_ref()) { - 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 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(), - ); - 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() - ))); - } 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, - )?; - self.get_state_mut().set_diverted_pointer(p); - } - } - } - CommandType::BeginString => { - 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(), - )); - } - - 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 - 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.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::BeginString { - break; - } - } - - if obj.as_ref().as_any().downcast_ref::().is_some() { - content_to_retain.push_back(obj.clone()); - } - - if Value::get_value::<&StringValue>(obj.as_ref()).is_some() { - content_stack_for_string.push_back(obj.clone()); - } - } - - // Consume the content that was produced for this string - 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. - - while let Some(rescue_tag) = content_to_retain.pop_back() { - self.get_state_mut().push_to_output_stream(rescue_tag); - } - - // 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.get_state().set_in_expression_evaluation(true); - self.get_state_mut() - .push_evaluation_stack(Rc::new(Value::new::<&str>(&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::(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::(current_turn + 1))); - } - CommandType::TurnsSince | CommandType::ReadCount => { - let target = self.get_state_mut().pop_evaluation_stack(); - if Value::get_value::<&Path>(target.as_ref()).is_none() { - let mut extra_note = "".to_owned(); - if Value::get_value::(target.as_ref()).is_some() { - extra_note = format!(". Did you accidentally pass a read count ('knot_name') instead of a target {}", - "('-> knot_name')?").to_owned(); - } - - return Err(StoryError::InvalidStoryState(format!("TURNS_SINCE expected a divert target (knot, stitch, label name), but saw {} {}", target - , extra_note))); - } - - let target = Value::get_value::<&Path>(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 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); - } - } - 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, - ); - } - } - - self.get_state_mut() - .push_evaluation_stack(Rc::new(Value::new::(either_count))); - } - CommandType::Random => { - let mut max_int = None; - let o = self.get_state_mut().pop_evaluation_stack(); - if let Some(v) = Value::get_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_value::(o.as_ref()) { - min_int = Some(v); - } - - if min_int.is_none() { - 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(), - )); - } - - let min_value = min_int.unwrap(); - let max_value = max_int.unwrap(); - let random_range = max_value - min_value + 1; - if random_range <= 0 { - 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; - let mut rng = StdRng::seed_from_u64(result_seed as u64); - let next_random = rng.random::(); - let chosen_value = (next_random % random_range as u32) as i32 + min_value; - self.get_state_mut() - .push_evaluation_stack(Rc::new(Value::new::(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_value::(o.as_ref()) { - seed = Some(v); - } - - if seed.is_none() { - return Err(StoryError::InvalidStoryState( - "Invalid value passed to SEED_RANDOM".to_owned(), - )); - } - - // 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.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::(count))); - } - CommandType::SequenceShuffleIndex => { - let shuffle_index = self.next_sequence_shuffle_index()?; - let v = Rc::new(Value::new::(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 - // 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()?; - } - // 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_value::(o.as_ref()) { - int_val = Some(v); - } - - let o = self.get_state_mut().pop_evaluation_stack(); - if let Some(s) = Value::get_value::<&StringValue>(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(), - )); - generated_list_value = Some(Value::new::(l)); - } - } else { - 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::(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_value::<&InkList>(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(), - )); - } - - 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::(result))); - } - CommandType::ListRandom => { - let o = self.get_state_mut().pop_evaluation_stack(); - let list = Value::get_value::<&InkList>(o.as_ref()); - if list.is_none() { - return Err(StoryError::InvalidStoryState( - "Expected list for LIST_RANDOM".to_owned(), - )); - } - - 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.random::(); - let list_item_index = (next_random as usize) % list.items.len(); // 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); - self.get_state_mut().previous_random = next_random as i32; - new_list - } - }; - self.get_state_mut() - .push_evaluation_stack(Rc::new(Value::new::(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 - // - // 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 { - return Err(StoryError::InvalidStoryState("Unexpected ControlCommand while extracting tag from choice".to_owned())); - } - } - - if let Some(sv) = Value::get_value::<&StringValue>(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.iter().rev() { - 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 Ok(true); - } - - // Variable assignment - 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 - // within - // 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() - .variables_state - .assign(var_ass, assigned_val)?; - return Ok(true); - } - - // Variable reference - if let Ok(var_ref) = content_obj - .clone() - .into_any() - .downcast::() - { - let found_value: Rc; // Explicit read count value - if var_ref.path_for_count.is_some() { - let container = var_ref.get_container_for_count(); - let count = self - .get_state_mut() - .visit_count_for_container(container.as_ref().unwrap()); - found_value = Rc::new(Value::new::(count)); - } - // Normal variable reference - else { - 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::(0)); - } - } - } - - self.get_state_mut().push_evaluation_stack(found_value); - return Ok(true); - } - - // 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()); - let result = func.call(func_params)?; - self.get_state_mut().push_evaluation_stack(result); - return Ok(true); - } - - Ok(false) - } -} diff --git a/lib/src/story/errors.rs b/lib/src/story/errors.rs deleted file mode 100644 index 7a15118..0000000 --- a/lib/src/story/errors.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::{cell::RefCell, rc::Rc}; - -use crate::story::Story; - -/// 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); -} - -/// Types of errors an Ink story might throw. -#[derive(PartialEq, Clone, Copy)] -pub enum ErrorType { - /// Problem that is not critical, but should be fixed. - Warning, - /// Critical error that can't be recovered from. - Error, -} - -/// # Errors -/// Methods to check for errors. -impl Story { - /// 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 panics for ink errors. - pub fn set_error_handler(&mut self, err_handler: Rc>) { - self.on_error = Some(err_handler); - } - - pub(crate) 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); - - if !is_warning { - self.get_state_mut().force_end(); - } - } - - pub(crate) fn reset_errors(&mut self) { - self.get_state_mut().reset_errors(); - } - - /// 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 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`. - pub fn get_current_warnings(&self) -> &Vec { - self.get_state().get_current_warnings() - } -} diff --git a/lib/src/story/flow.rs b/lib/src/story/flow.rs deleted file mode 100644 index 230ecc8..0000000 --- a/lib/src/story/flow.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::{story::Story, story_error::StoryError}; - -/// # Flow -/// Methods to work with flows and the call-stack. -impl Story { - pub(crate) fn reset_callstack(&mut self) -> Result<(), StoryError> { - self.if_async_we_cant("ResetCallstack")?; - - self.get_state_mut().force_end(); - - Ok(()) - } - - /// 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")?; - - if self.async_saving { - 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(()) - } - - /// 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) - } - - /// 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(); - } -} diff --git a/lib/src/story/mod.rs b/lib/src/story/mod.rs deleted file mode 100644 index 4027251..0000000 --- a/lib/src/story/mod.rs +++ /dev/null @@ -1,191 +0,0 @@ -//! [`Story`] is the entry point to load and run an Ink story. -use crate::{ - container::Container, - list_definitions_origin::ListDefinitionsOrigin, - story::{ - errors::ErrorHandler, external_functions::ExternalFunctionDef, - variable_observer::VariableObserver, - }, - story_state::StoryState, -}; -use std::{cell::RefCell, collections::HashMap, rc::Rc}; - -/// 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. -pub const INK_VERSION_MINIMUM_COMPATIBLE: i32 = 18; - -#[derive(PartialEq)] -pub(crate) enum OutputStateChange { - NoChange, - ExtendedBeyondNewline, - NewlineRemoved, -} - -/// A `Story` is the core struct representing a complete Ink narrative, -/// managing evaluation and state. -pub struct Story { - main_content_container: Rc, - state: StoryState, - 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>>, - 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, -} -mod misc { - use crate::{ - json::{json_read, json_read_stream}, - object::{Object, RTObject}, - path::Path, - story::{Story, INK_VERSION_CURRENT}, - story_error::StoryError, - story_state::StoryState, - value::Value, - }; - use rand::{rngs::StdRng, Rng, SeedableRng}; - use std::{collections::HashMap, rc::Rc}; - - impl Story { - /// Construct a `Story` out of a JSON string that was compiled with - /// `inklecate`. - pub fn new(json_string: &str) -> Result { - let (version, main_content_container, list_definitions) = - if cfg!(feature = "stream-json-parser") { - json_read_stream::load_from_string(json_string)? - } else { - json_read::load_from_string(json_string)? - }; - - let mut story = Story { - 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, - saw_lookahead_unsafe_function_after_new_line: false, - state_snapshot_at_last_new_line: None, - on_error: None, - 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), - }; - - story.reset_globals()?; - - if version != INK_VERSION_CURRENT { - 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) - } - - /// 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(); - - let cp = self.get_state().get_current_pointer().resolve(); - - let cp = cp.as_ref().map(|cp| cp.as_ref()); - - self.main_content_container - .build_string_of_hierarchy(&mut sb, 0, cp); - - sb - } - - pub(crate) fn is_truthy(&self, obj: Rc) -> Result { - let truthy = false; - - if let Some(val) = obj.as_ref().as_any().downcast_ref::() { - if let Some(target_path) = Value::get_value::<&Path>(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(); - } - - Ok(truthy) - } - - pub(crate) 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_value::(pop_evaluation_stack.as_ref()) { - v - } else { - 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_value::(self.get_state_mut().pop_evaluation_stack().as_ref()) - { - v - } else { - 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 - // - 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 - .random::() - .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(), - )) - } - } -} - -mod choices; -mod control_logic; -pub mod errors; -pub mod external_functions; -mod flow; -mod navigation; -mod progress; -mod state; -mod tags; -pub mod variable_observer; diff --git a/lib/src/story/navigation.rs b/lib/src/story/navigation.rs deleted file mode 100644 index 5502858..0000000 --- a/lib/src/story/navigation.rs +++ /dev/null @@ -1,315 +0,0 @@ -use crate::{ - container::Container, - object::{Object, RTObject}, - path::Path, - pointer::{self, Pointer}, - push_pop::PushPopType, - search_result::SearchResult, - story::Story, - story_error::StoryError, - value_type::ValueType, -}; -use std::rc::Rc; - -/// # Navigation -/// Methods to access specific sections of the story. -impl Story { - pub(crate) fn get_main_content_container(&self) -> Rc { - match self.temporary_evaluation_container.as_ref() { - Some(c) => c.clone(), - None => self.main_content_container.clone(), - } - } - - /// Change the current position of the story to the given path. From - /// here you can 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: - /// - /// ```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. - /// - /// 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 - /// [`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 `reset_callstack` - /// parameter if you don't want this behaviour, leaving any active - /// threads, tunnels or function calls intact. - /// - /// 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, - /// `choose_path_string` will throw an error. - 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 { - 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())); - } - - 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)?; - - Ok(()) - } - - /// 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, - 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(), - )); - } - - // 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('\''); - - return Err(StoryError::BadArgument(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 - self.get_state_mut() - .complete_function_evaluation_from_game() - } - - pub(crate) 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()) - { - 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 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; - } - } - } - - pub(crate) 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 = - 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.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.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 { - return Err(StoryError::InvalidStoryState(format!( - "Failed to find content at path '{}', and no approximation of it was possible.", - path - ))); - } else if result.approximate { - // TODO - // self.add_error(&format!("Failed to find content at path '{}', - // so it was approximated to: '{}'.", path - // , result.obj.unwrap().get_path()), true); - } - - Ok(p) - } - - 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() - } - - pub(crate) fn content_at_path(&self, path: &Path) -> SearchResult { - self.main_content_container.content_at_path(path, 0, -1) - } - - /// 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) - } -} diff --git a/lib/src/story/progress.rs b/lib/src/story/progress.rs deleted file mode 100644 index 23e21d3..0000000 --- a/lib/src/story/progress.rs +++ /dev/null @@ -1,738 +0,0 @@ -use crate::{ - choice::Choice, - choice_point::ChoicePoint, - container::Container, - control_command::{CommandType, ControlCommand}, - object::RTObject, - pointer::{self, Pointer}, - push_pop::PushPopType, - story::{errors::ErrorType, OutputStateChange, Story}, - story_error::StoryError, - value::Value, - value_type::VariablePointerValue, - void::Void, -}; -use std::{self, rc::Rc}; - -/// # Story Progress -/// Methods to move the story forwards. -impl Story { - /// `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")?; - - let mut sb = String::new(); - - while self.can_continue() { - sb.push_str(&self.cont()?); - } - - 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()?; - } - - self.continue_internal(millisecs_limit_async) - } - - 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 continue_async(). Make more continue_async() calls or a single cont() call beforehand.", activity_str))); - } - - Ok(()) - } - - pub(crate) 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; - - // 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(StoryError::InvalidStoryState( - "Can't continue - should check can_continue before calling Continue".to_owned(), - )); - } - - self.get_state_mut().set_did_safe_exit(false); - - 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 - // for the outermost call. - if self.recursive_continue_count == 1 { - self.state.variables_state.start_variable_observation(); - } - } else if self.async_continue_active && !is_async_time_limited { - self.async_continue_active = false; - } - - // Start timing (only when necessary) - let duration_stopwatch = match self.async_continue_active { - true => Some(web_time::Instant::now()), - false => None, - }; - - 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(e) => { - self.add_error(e.get_message(), false); - break; - } - } - - if output_stream_ends_in_newline { - break; - } - - // Run out of async time? - if self.async_continue_active - && duration_stopwatch.as_ref().unwrap().elapsed().as_millis() as f32 - > millisecs_limit_async - { - break; - } - - if !self.can_continue() { - break; - } - } - - let mut changed_variables_to_observe = None; - - // 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.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() - && !self.get_state().is_did_safe_exit() - && self.temporary_evaluation_container.is_none() - { - if self - .state - .get_callstack() - .borrow() - .can_pop_type(Some(PushPopType::Tunnel)) - { - self.add_error("unexpectedly reached end of content. Do you need a '->->' to return from a tunnel?", false); - } else if self - .state - .get_callstack() - .borrow() - .can_pop_type(Some(PushPopType::Function)) - { - self.add_error( - "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, - ); - } else { - self.add_error("unexpectedly reached end of content for unknown reason. Please debug compiler!", 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 { - changed_variables_to_observe = - Some(self.state.variables_state.complete_variable_observation()); - } - - 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.get_state().has_error() || self.get_state().has_warning() { - match &self.on_error { - Some(on_err) => { - if self.get_state().has_error() { - for err in self.get_state().get_current_errors() { - 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.borrow_mut().error(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.get_state().has_error() { - sb.push_str(&self.get_state().get_current_errors().len().to_string()); - - if self.get_state().get_current_errors().len() == 1 { - sb.push_str(" error"); - } else { - sb.push_str(" errors"); - } - - if self.get_state().has_warning() { - sb.push_str(" and "); - } - } - - 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"); - } - } - - sb.push_str(". It is strongly suggested that you assign an error handler to story.onError. The first issue was: "); - - 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(), - ); - } - - return Err(StoryError::InvalidStoryState(sb)); - } - } - } - - // Send out variable observation events at the last second, since it might trigger new ink to be run - if let Some(changed) = changed_variables_to_observe { - for (variable_name, value) in changed { - self.notify_variable_changed(&variable_name, &value); - } - } - - Ok(()) - } - - pub(crate) 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 - .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() - { - // 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, - ); - - // 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.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 - // ~ complexCalculation() // don't actually need this unless it generates - // text - if self.state_snapshot_at_last_new_line.is_none() { - 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(); - } - } - } - - Ok(false) - } - - pub(crate) 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 Ok(()); - } - - // 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.get_state_mut().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 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? - if self.get_state().get_current_pointer().is_null() { - return Ok(()); - } - - if is_logic_or_flow_control { - should_add_to_stream = false; - } - - // Choice with condition? - if let Some(cco) = ¤t_content_obj { - // 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 { - self.get_state_mut() - .get_generated_choices_mut() - .push(choice); - } - - current_content_obj = None; - 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. - let var_pointer = Value::get_value::<&VariablePointerValue>( - current_content_obj.as_ref().unwrap().as_ref(), - ); - - 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, - ))); - } - } - - // 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()); - } - // Output stream content (i.e. not expression evaluation) - else { - self.get_state_mut() - .push_to_output_stream(current_content_obj.as_ref().unwrap().clone()); - } - } - - // 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. - 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(); - } - } - } - - Ok(()) - } - - pub(crate) fn next_content(&mut self) -> Result<(), StoryError> { - // Setting previousContentObject is critical for - // VisitChangedContainersDueToDivert - let cp = self.get_state().get_current_pointer(); - self.get_state_mut().set_previous_pointer(cp); - - // Divert step? - 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.get_state().get_current_pointer().is_null() { - return Ok(()); - } - - // 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 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 { - // Pop from the call stack - 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())); - } - - 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()?; - - did_pop = true; - } else { - self.get_state_mut() - .try_exit_function_evaluation_from_game(); - } - - // Step past the point where we last called out - if did_pop && !self.get_state().get_current_pointer().is_null() { - self.next_content()?; - } - } - - Ok(()) - } - - pub(crate) 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(); - pointer.index += 1; - - 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(); - - if next_ancestor.is_none() { - break; - } - - 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; - - successful_increment = true; - } - - if !successful_increment { - pointer = pointer::NULL.clone(); - } - - self.get_state() - .get_callstack() - .as_ref() - .borrow_mut() - .get_current_element_mut() - .current_pointer = pointer; - - successful_increment - } - - pub(crate) 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.is_empty() - && curr_text.as_bytes()[prev_text.len() - 1] == b'\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.as_bytes().iter().skip(prev_text.len()) { - if *c != b' ' && *c != b'\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 - } - - pub(crate) 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); - } - - if container.turn_index_should_be_counted { - self.get_state_mut() - .record_turn_index_visit_to_container(container); - } - } - } - - /// 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(); - - 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()); - choices.push(c.clone()); - } - } - } - - choices - } - - /// 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()) - } - - /// String representation of the location where the story currently is. - pub fn get_current_path(&self) -> Option { - self.get_state().current_path_string() - } -} diff --git a/lib/src/story/state.rs b/lib/src/story/state.rs deleted file mode 100644 index 43cb81f..0000000 --- a/lib/src/story/state.rs +++ /dev/null @@ -1,137 +0,0 @@ -use crate::{ - path::Path, story::Story, story_error::StoryError, story_state::StoryState, - value_type::ValueType, -}; - -/// # State -/// Methods to read and write story state. -impl Story { - #[inline] - pub(crate) fn get_state(&self) -> &StoryState { - &self.state - } - - #[inline] - pub(crate) fn get_state_mut(&mut self) -> &mut StoryState { - &mut self.state - } - - pub(crate) 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, - )?; - - // Continue, but without validating external bindings, - // since we may be doing this reset at initialisation time. - self.continue_internal(0.0)?; - - self.get_state().set_current_pointer(original_pointer); - } - - self.get_state_mut() - .variables_state - .snapshot_default_globals(); - - 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, - 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(()) - } - - /// 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) - } - - pub(crate) 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(); // unwrap: state_snapshot_at_last_new_line checked Some in previous fn - - 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 - // the save was started but before the snapshot was made. - if !self.async_saving { - self.get_state_mut().apply_any_patch(); - } - } - - pub(crate) fn state_snapshot(&mut self) { - // tmp_state contains the new state and current state is stored in snapshot - let mut tmp_state = self.state.copy_and_start_patching(false); - std::mem::swap(&mut tmp_state, &mut self.state); - self.state_snapshot_at_last_new_line = Some(tmp_state); - } - - pub(crate) 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. - - if !self.async_saving { - self.get_state_mut().apply_any_patch(); - } - - // No longer need the snapshot. - self.state_snapshot_at_last_new_line = None; - } - - /// 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) - } - - /// Reset the Story back to its initial state as it was when it was first constructed. - pub fn reset_state(&mut self) -> Result<(), StoryError> { - self.if_async_we_cant("ResetState")?; - - self.state = StoryState::new( - self.main_content_container.clone(), - self.list_definitions.clone(), - ); - - self.reset_globals()?; - - Ok(()) - } -} diff --git a/lib/src/story/tags.rs b/lib/src/story/tags.rs deleted file mode 100644 index 4280922..0000000 --- a/lib/src/story/tags.rs +++ /dev/null @@ -1,83 +0,0 @@ -use crate::{ - container::Container, - control_command::{CommandType, ControlCommand}, - path::Path, - story::Story, - story_error::StoryError, - value::Value, - value_type::StringValue, -}; - -/// # Tags -/// Methods to read tags. -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) - } - - pub(crate) 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; - } 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_value::<&StringValue>(content.as_ref()) - { - tags.push(string_value.string.clone()); - } else { - return Err( - 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 { - break; - } - } - } - } - - Ok(tags) - } - - /// 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()) - } -} diff --git a/lib/src/story/variable_observer.rs b/lib/src/story/variable_observer.rs deleted file mode 100644 index 1b19195..0000000 --- a/lib/src/story/variable_observer.rs +++ /dev/null @@ -1,107 +0,0 @@ -use std::{cell::RefCell, rc::Rc}; - -use crate::{story::Story, story_error::StoryError, value_type::ValueType}; - -/// 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); -} - -/// # Variable Observers -/// Methods dealing with variable observer callbacks that will be called while -/// the [`Story`] is processing. -impl Story { - /// 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). - 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(()) - } - - /// 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. - 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/external_functions.rs b/lib/src/story_callbacks.rs similarity index 61% rename from lib/src/story/external_functions.rs rename to lib/src/story_callbacks.rs index b238ea0..eaa2f1d 100644 --- a/lib/src/story/external_functions.rs +++ b/lib/src/story_callbacks.rs @@ -1,32 +1,142 @@ -use std::{cell::RefCell, collections::HashSet, rc::Rc}; +//! For setting the callbacks functions that will be called while the [`Story`] is processing. +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, + 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. +pub trait VariableObserver { + fn changed(&mut self, variable_name: &str, value: &ValueType); +} + /// Defines the method callback implementing an external function. pub trait ExternalFunction { fn call(&mut self, func_name: &str, args: Vec) -> Option; } pub(crate) struct ExternalFunctionDef { - function: Rc>, + function: Brc>, lookahead_safe: bool, } -/// # External Functions -/// Methods dealing with external function call handlers that will be called -/// while [`Story`] is processing. +/// 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); +} + +/// Types of errors an Ink story might throw. +#[derive(PartialEq, Clone, Copy)] +pub enum ErrorType { + /// Problem that is not critical, but should be fixed. + Warning, + /// Critical error that can't be recovered from. + Error, +} + +/// Methods dealing with callback handlers. impl Story { - /// 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; + /// 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 panics for ink errors. + pub fn set_error_handler(&mut self, err_handler: Brc>) { + self.on_error = Some(err_handler); + } + + /// 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). + pub fn observe_variable( + &mut self, + variable_name: &str, + observer: Brc>, + ) -> 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(()) + } + + /// 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. + pub fn remove_variable_observer( + &mut self, + observer: &Brc>, + 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| Brc::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| Brc::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); + } + } } /// Bind a Rust function to an ink `EXTERNAL` function declaration. @@ -42,14 +152,13 @@ impl Story { /// 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`. - /// 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. + /// 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, func_name: &str, - function: Rc>, + function: Brc>, lookahead_safe: bool, ) -> Result<(), StoryError> { self.if_async_we_cant("bind an external function")?; @@ -94,35 +203,6 @@ impl Story { // 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.get_state().in_string_evaluation() { - // 16th Jan 2023: Example ink that was failing: - // - // A line above - // ~ temp text = "{theFunc()}" - // {text} - // - // === function theFunc() - // { external(): - // Boom - // } - // - // EXTERNAL external() - // - // What was happening: The external() call would exit out early due to - // _stateSnapshotAtLastNewline having a value, leaving the evaluation stack - // without a return value on it. When the if-statement tried to pop a value, - // the evaluation stack would be empty, and there would be an exception. - // - // The snapshot rewinding code is only designed to work when outside of - // string generation code (there's a check for that in the snapshot rewinding code), - // hence these things are incompatible, you can't have unsafe functions that - // cause snapshot rewinding in the middle of string generation. - // - self.add_error(&format!("External function {} could not be called because 1) it wasn't marked as lookaheadSafe when BindExternalFunction was called and 2) the story is in the middle of string generation, either because choice text is being generated, or because you have ink like \"hello {{func()}}\". You can work around this by generating the result of your function into a temporary variable before the string or choice gets generated: ~ temp x = {}()", func_name, func_name), false); - - return Ok(()); - } - 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(()); @@ -184,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_value_type(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); @@ -229,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() { @@ -264,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_error.rs b/lib/src/story_error.rs index ea265e0..96e2e01 100644 --- a/lib/src/story_error.rs +++ b/lib/src/story_error.rs @@ -26,12 +26,6 @@ impl StoryError { impl std::error::Error for StoryError {} -impl std::convert::From for StoryError { - fn from(err: std::io::Error) -> StoryError { - StoryError::BadJson(err.to_string()) - } -} - impl fmt::Display for StoryError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { diff --git a/lib/src/story_state.rs b/lib/src/story_state.rs index eed538e..b7c9b14 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, @@ -7,8 +7,7 @@ use crate::{ control_command::{CommandType, ControlCommand}, flow::Flow, glue::Glue, - ink_list::InkList, - json::{json_read, json_write}, + json_read, json_write, list_definitions_origin::ListDefinitionsOrigin, object::{Object, RTObject}, path::Path, @@ -18,8 +17,10 @@ use crate::{ story::{Story, INK_VERSION_CURRENT}, story_error::StoryError, tag::Tag, + threadsafe::BrCell, + threadsafe::Brc, value::Value, - value_type::{StringValue, ValueType}, + value_type::ValueType, variables_state::VariablesState, void::Void, }; @@ -39,8 +40,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, @@ -53,19 +54,19 @@ 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(); - let mut rng = rand::rng(); - let story_seed = rng.random_range(0..100); + let mut rng = rand::thread_rng(); + let story_seed = rng.gen_range(0..100); let state = StoryState { current_flow, @@ -104,23 +105,6 @@ impl StoryState { !self.current_errors.is_empty() } - /// String representation of the location where the story currently is. - pub fn current_path_string(&self) -> Option { - let pointer = self.get_current_pointer(); - pointer.get_path().map(|path| path.to_string()) - } - - /// Get the previous state of currentPathString, which can be helpful - /// for finding out where the story was before it ended (when the path - /// string becomes null) - /// - /// Marked as dead code by now. - #[allow(dead_code)] - pub fn previous_path_string(&self) -> Option { - let pointer = self.get_previous_pointer(); - pointer.get_path().map(|path| path.to_string()) - } - pub fn get_current_pointer(&self) -> Pointer { self.get_callstack() .borrow() @@ -129,7 +113,7 @@ impl StoryState { .clone() } - pub fn get_callstack(&self) -> &Rc> { + pub fn get_callstack(&self) -> &Brc> { &self.current_flow.callstack } @@ -137,7 +121,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 { @@ -147,11 +131,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 } @@ -171,11 +155,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 } @@ -201,7 +185,13 @@ impl StoryState { let mut in_tag = false; for output_obj in self.get_output_stream() { - let text_content = Value::get_value::<&StringValue>(output_obj.as_ref()); + let text_content = match output_obj.as_ref().as_any().downcast_ref::() { + Some(v) => match &v.value { + ValueType::String(s) => Some(s), + _ => None, + }, + None => None, + }; if let (false, Some(text_content)) = (in_tag, text_content) { sb.push_str(&text_content.string); @@ -259,9 +249,7 @@ impl StoryState { _ => {} } } else if in_tag { - if let Some(string_value) = - Value::get_value::<&StringValue>(output_obj.as_ref()) - { + 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::() { @@ -362,8 +350,8 @@ impl StoryState { .in_expression_evaluation = value; } - pub fn push_evaluation_stack(&mut self, obj: Rc) { - if let Some(list) = Value::get_value::<&InkList>(obj.as_ref()) { + 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(); list.origins.borrow_mut().clear(); @@ -379,15 +367,24 @@ impl StoryState { self.evaluation_stack.push(obj); } - pub fn push_to_output_stream(&mut self, obj: Rc) { - let text = Value::get_value::<&StringValue>(obj.as_ref()); + pub fn push_to_output_stream(&mut self, obj: Brc) { + 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, + } + }; 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.push_to_output_stream_individual(Brc::new(text_obj)); } self.output_stream_dirty(); return; @@ -397,7 +394,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 { @@ -420,7 +417,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 @@ -500,10 +497,10 @@ impl StoryState { if head_first_newline_idx != -1 { if head_first_newline_idx > 0 { - let leading_spaces = Value::new::<&str>(&text[0..head_first_newline_idx as usize]); + let leading_spaces = Value::new_string(&text[0..head_first_newline_idx as usize]); list_texts.push(leading_spaces); } - list_texts.push(Value::new::<&str>("\n")); + list_texts.push(Value::new_string("\n")); inner_str_start = head_last_newline_idx + 1; } @@ -513,14 +510,14 @@ impl StoryState { 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::<&str>(inner_str_text)); + 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::<&str>("\n")); + 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::<&str>( + let trailing_spaces = Value::new_string( &text[(tail_last_newline_idx + 1) as usize ..(num_spaces + tail_last_newline_idx + 1) as usize], ); @@ -531,9 +528,9 @@ 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_value::<&StringValue>(obj.as_ref()); + 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 @@ -560,7 +557,6 @@ impl StoryState { let mut glue_trim_index = -1; for (i, o) in self.get_output_stream().iter().rev().enumerate() { - let i = self.get_output_stream().len() - i - 1; if let Some(c) = o.as_ref().as_any().downcast_ref::() { if c.command_type == CommandType::BeginString { if i as i32 >= function_trim_index { @@ -635,7 +631,7 @@ impl StoryState { 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_value::<&StringValue>(obj.as_ref()) { + } else if let Some(sv) = Value::get_string_value(obj.as_ref()) { if sv.is_non_whitespace() { break; } else if sv.is_newline { @@ -650,7 +646,7 @@ impl StoryState { if remove_whitespace_from >= 0 { i = remove_whitespace_from; while i < output_stream.len() as i32 { - if Value::get_value::<&StringValue>(output_stream[i as usize].as_ref()).is_some() { + if Value::get_string_value(output_stream[i as usize].as_ref()).is_some() { output_stream.remove(i as usize); } else { i += 1; @@ -747,7 +743,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. @@ -758,41 +754,25 @@ impl StoryState { Some(&self.current_flow.current_choices) } - pub fn copy_and_start_patching(&self, for_background_save: bool) -> StoryState { + pub fn copy_and_start_patching(&self) -> StoryState { let mut copy = StoryState::new( self.main_content_container.clone(), self.list_definitions.clone(), ); - copy.patch = Some(self.patch.clone().unwrap_or_else(StatePatch::new)); + 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( - self.current_flow.callstack.as_ref().borrow().clone(), - )); + 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(); copy.current_flow.output_stream = self.current_flow.output_stream.clone(); copy.output_stream_dirty(); - // When background saving we need to make copies of choices since they each have - // a snapshot of the thread at the time of generation since the game could progress - // significantly and threads modified during the save process. - // However, when doing internal saving and restoring of snapshots this isn't an issue, - // and we can simply ref-copy the choices with their existing threads. - if for_background_save { - copy.current_flow.current_choices = - Vec::with_capacity(self.current_flow.current_choices.len()); - - for choice in self.current_flow.current_choices.iter() { - let c = choice.as_ref().clone(); - copy.current_flow.current_choices.push(Rc::new(c)); - } - } else { - copy.current_flow.current_choices = self.current_flow.current_choices.clone(); - } - // 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 @@ -896,16 +876,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 } @@ -978,7 +958,7 @@ impl StoryState { break; } - if let Some(txt) = Value::get_value::<&StringValue>(obj.as_ref()) { + 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(); @@ -991,13 +971,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( @@ -1023,18 +1003,18 @@ impl StoryState { if let Some(arguments) = arguments { for arg in arguments { let value = match arg { - ValueType::Bool(v) => Value::new::(*v), - ValueType::Int(v) => Value::new::(*v), - ValueType::Float(v) => Value::new::(*v), - ValueType::List(v) => Value::new::(v.clone()), - ValueType::String(v) => Value::new::<&str>(&v.string), + 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)); + self.push_evaluation_stack(Brc::new(value)); } } @@ -1092,7 +1072,7 @@ 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(ValueType::new::<&str>(&p.to_string()))); + return Ok(Some(ValueType::new_string(&p.to_string()))); } // Other types can just have their exact object type: 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/value.rs b/lib/src/value.rs index b091a76..aae33ca 100644 --- a/lib/src/value.rs +++ b/lib/src/value.rs @@ -41,107 +41,47 @@ impl fmt::Display for Value { } } -impl> From for Value { - fn from(value: T) -> Self { - Self::new_value_type(value.into()) - } -} - -impl<'val> TryFrom<&'val dyn RTObject> for &'val StringValue { - type Error = (); - fn try_from(o: &dyn RTObject) -> Result<&StringValue, Self::Error> { - match o.as_any().downcast_ref::() { - Some(v) => match &v.value { - ValueType::String(v) => Ok(v), - _ => Err(()), - }, - None => Err(()), - } - } -} -impl<'val> TryFrom<&'val dyn RTObject> for &'val VariablePointerValue { - type Error = (); - fn try_from(o: &dyn RTObject) -> Result<&VariablePointerValue, Self::Error> { - match o.as_any().downcast_ref::() { - Some(v) => match &v.value { - ValueType::VariablePointer(v) => Ok(v), - _ => Err(()), - }, - None => Err(()), - } - } -} -impl<'val> TryFrom<&'val dyn RTObject> for &'val Path { - type Error = (); - fn try_from(o: &dyn RTObject) -> Result<&Path, Self::Error> { - match o.as_any().downcast_ref::() { - Some(v) => match &v.value { - ValueType::DivertTarget(p) => Ok(p), - _ => Err(()), - }, - None => Err(()), - } - } -} -impl TryFrom<&dyn RTObject> for i32 { - type Error = (); - fn try_from(o: &dyn RTObject) -> Result { - match o.as_any().downcast_ref::() { - Some(v) => match &v.value { - ValueType::Int(v) => Ok(*v), - _ => Err(()), - }, - None => Err(()), +impl Value { + pub fn new(value: ValueType) -> Self { + Self { + obj: Object::new(), + value, } } -} -impl TryFrom<&dyn RTObject> for f32 { - type Error = (); - fn try_from(o: &dyn RTObject) -> Result { - match o.as_any().downcast_ref::() { - Some(v) => match &v.value { - ValueType::Float(v) => Ok(*v), - _ => Err(()), - }, - None => Err(()), + + pub fn new_bool(v: bool) -> Self { + Self { + obj: Object::new(), + value: ValueType::Bool(v), } } -} -impl<'val> TryFrom<&'val mut dyn RTObject> for &'val mut InkList { - type Error = (); - fn try_from(o: &mut dyn RTObject) -> Result<&mut InkList, Self::Error> { - match o.as_any_mut().downcast_mut::() { - Some(v) => match &mut v.value { - ValueType::List(v) => Ok(v), - _ => Err(()), - }, - None => Err(()), + + pub fn new_int(v: i32) -> Self { + Self { + obj: Object::new(), + value: ValueType::Int(v), } } -} -impl<'val> TryFrom<&'val dyn RTObject> for &'val InkList { - type Error = (); - fn try_from(o: &dyn RTObject) -> Result<&InkList, Self::Error> { - match o.as_any().downcast_ref::() { - Some(v) => match &v.value { - ValueType::List(v) => Ok(v), - _ => Err(()), - }, - None => Err(()), + + pub fn new_float(v: f32) -> Self { + Self { + obj: Object::new(), + value: ValueType::Float(v), } } -} -impl Value { - pub fn new_value_type(valuetype: ValueType) -> Self { + pub fn new_string(v: &str) -> Self { Self { obj: Object::new(), - value: valuetype, + value: ValueType::new_string(v), } } - pub fn new>(v: T) -> Self { - v.into() + 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 { @@ -154,20 +94,17 @@ impl Value { } } - pub fn get_value<'val, T>(o: &'val dyn RTObject) -> Option - where - &'val dyn RTObject: TryInto, - { - o.try_into().ok() + pub fn new_list(l: InkList) -> Self { + Self { + obj: Object::new(), + value: ValueType::List(l), + } } - 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 from_value_type(value_type: ValueType) -> Self { + Self { + obj: Object::new(), + value: value_type, } } @@ -187,9 +124,99 @@ impl Value { } } + 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, + } + } + + 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, + } + } + + 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 { + ValueType::Int(v) => Some(*v), + _ => None, + }, + None => None, + } + } + + 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 { + 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 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_value::<&InkList>(old_value) { - if let Some(new_list) = Self::get_value::<&InkList>(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.is_empty() { new_list.set_initial_origin_names(old_list.get_origin_names()); } @@ -215,23 +242,23 @@ impl Value { CAST_BOOL => Ok(None), CAST_INT => { if *v { - Ok(Some(Self::new::(1))) + Ok(Some(Self::new_int(1))) } else { - Ok(Some(Self::new::(0))) + Ok(Some(Self::new_int(0))) } } CAST_FLOAT => { if *v { - Ok(Some(Self::new::(1.0))) + Ok(Some(Self::new_float(1.0))) } else { - Ok(Some(Self::new::(0.0))) + Ok(Some(Self::new_float(0.0))) } } CAST_STRING => { if *v { - Ok(Some(Self::new::<&str>("true"))) + Ok(Some(Self::new_string("true"))) } else { - Ok(Some(Self::new::<&str>("false"))) + Ok(Some(Self::new_string("false"))) } } _ => Err(StoryError::InvalidStoryState( @@ -241,14 +268,14 @@ impl Value { ValueType::Int(v) => match cast_dest_type { CAST_BOOL => { if *v == 0 { - Ok(Some(Self::new::(false))) + Ok(Some(Self::new_bool(false))) } else { - Ok(Some(Self::new::(true))) + Ok(Some(Self::new_bool(true))) } } CAST_INT => Ok(None), - CAST_FLOAT => Ok(Some(Self::new::(*v as f32))), - CAST_STRING => Ok(Some(Self::new::<&str>(&v.to_string()))), + 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(), )), @@ -256,21 +283,21 @@ impl Value { ValueType::Float(v) => match cast_dest_type { CAST_BOOL => { if *v == 0.0 { - Ok(Some(Self::new::(false))) + Ok(Some(Self::new_bool(false))) } else { - Ok(Some(Self::new::(true))) + Ok(Some(Self::new_bool(true))) } } - CAST_INT => Ok(Some(Self::new::(*v as i32))), + CAST_INT => Ok(Some(Self::new_int(*v as i32))), CAST_FLOAT => Ok(None), - CAST_STRING => Ok(Some(Self::new::<&str>(&v.to_string()))), + 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::(v.string.parse::().unwrap()))), - CAST_FLOAT => Ok(Some(Self::new::(v.string.parse::().unwrap()))), + 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(), @@ -280,23 +307,23 @@ impl Value { CAST_INT => { let max = l.get_max_item(); match max { - Some(i) => Ok(Some(Self::new::(i.1))), - None => Ok(Some(Self::new::(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) => Ok(Some(Self::new::(i.1 as f32))), - None => Ok(Some(Self::new::(0.0))), + 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::<&str>(&i.0.to_string()))), - None => Ok(Some(Self::new::<&str>(""))), + Some(i) => Ok(Some(Self::new_string(&i.0.to_string()))), + None => Ok(Some(Self::new_string(""))), } } _ => Err(StoryError::InvalidStoryState( diff --git a/lib/src/value_type.rs b/lib/src/value_type.rs index fa5bf4b..9ca60c6 100644 --- a/lib/src/value_type.rs +++ b/lib/src/value_type.rs @@ -10,7 +10,7 @@ pub enum ValueType { Float(f32), /// An Ink list value. List(InkList), - /// Ink string, constructed with [`new_string`](ValueType::new::<&str>) + /// Ink string, constructed with [`new_string`](ValueType::new_string) String(StringValue), /// Reference to an Ink divert. DivertTarget(Path), @@ -18,105 +18,56 @@ pub enum ValueType { VariablePointer(VariablePointerValue), } -impl From for ValueType { - fn from(value: bool) -> ValueType { - ValueType::Bool(value) - } -} - -impl From for ValueType { - fn from(value: i32) -> ValueType { - ValueType::Int(value) - } -} - -impl From for ValueType { - fn from(value: f32) -> ValueType { - ValueType::Float(value) - } -} - -impl From<&str> for ValueType { - fn from(value: &str) -> ValueType { - let inline_ws = value.chars().all(|c| c == ' ' || c == '\t'); +impl ValueType { + /// Creates a new `ValueType` for a `String`. + 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: value.to_string(), + string: str.to_string(), is_inline_whitespace: inline_ws, - is_newline: value.eq("\n"), + is_newline: str.eq("\n"), }) } -} - -impl From for ValueType { - fn from(value: InkList) -> ValueType { - ValueType::List(value) - } -} - -impl From for ValueType { - fn from(value: Path) -> ValueType { - ValueType::DivertTarget(value) - } -} - -impl From for ValueType { - fn from(value: VariablePointerValue) -> Self { - ValueType::VariablePointer(value) - } -} -impl TryFrom<&ValueType> for bool { - type Error = (); - fn try_from(value: &ValueType) -> Result { - match value { - ValueType::Bool(v) => Ok(*v), - _ => Err(()), + /// 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), + _ => None, } } -} -impl TryFrom<&ValueType> for i32 { - type Error = (); - fn try_from(value: &ValueType) -> Result { - match value { - ValueType::Int(v) => Ok(*v), - _ => Err(()), + /// 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), + _ => None, } } -} -impl TryFrom<&ValueType> for f32 { - type Error = (); - fn try_from(value: &ValueType) -> Result { - match value { - ValueType::Float(v) => Ok(*v), - _ => Err(()), + /// 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), + _ => None, } } -} -impl<'val> TryFrom<&'val ValueType> for &'val str { - type Error = (); - fn try_from(value: &'val ValueType) -> Result { - match value { - ValueType::String(v) => Ok(&v.string), - _ => Err(()), + /// 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), + _ => None, } } -} - -impl ValueType { - pub fn new>(v: T) -> Self { - v.into() - } - - pub fn get<'val, T>(&'val self) -> Option - where - &'val Self: TryInto, - { - self.try_into().ok() - } /// Tries to convert the internal value of this `ValueType` to `i32` pub fn coerce_to_int(&self) -> Result { diff --git a/lib/src/variable_reference.rs b/lib/src/variable_reference.rs index bee9c0d..bb2da9b 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, + threadsafe::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 d467912..d05626b 100644 --- a/lib/src/variables_state.rs +++ b/lib/src/variables_state.rs @@ -1,17 +1,15 @@ -use std::{ - cell::RefCell, - collections::{HashMap, HashSet}, - rc::Rc, -}; +use std::collections::{HashMap, HashSet}; use serde_json::Map; use crate::{ callstack::CallStack, - json::{json_read, json_write}, + json_read, json_write, list_definitions_origin::ListDefinitionsOrigin, state_patch::StatePatch, story_error::StoryError, + threadsafe::BrCell, + threadsafe::Brc, value::Value, value_type::{ValueType, VariablePointerValue}, variable_assigment::VariableAssignment, @@ -19,19 +17,19 @@ use crate::{ #[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(), @@ -44,15 +42,15 @@ impl VariablesState { } } - pub fn start_variable_observation(&mut self) { + pub fn start_batch_observing_variable_changes(&mut self) { self.batch_observing_variable_changes = true; self.changed_variables_for_batch_obs = Some(HashSet::new()); } - pub fn complete_variable_observation(&mut self) -> HashMap { + pub fn stop_batch_observing_variable_changes(&mut self) -> Vec<(String, ValueType)> { self.batch_observing_variable_changes = false; - let mut changed_vars = HashMap::with_capacity(0); + 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. @@ -60,20 +58,11 @@ impl VariablesState { for variable_name in changed_variables_for_batch_obs { let current_value = self.global_variables.get(&variable_name).unwrap(); - changed_vars.insert(variable_name, current_value.value.clone()); + changed.push((variable_name, current_value.value.clone())); } } - // Patch may still be active - e.g. if we were in the middle of a background save - if let Some(patch) = &self.patch { - for variable_name in patch.changed_variables.iter() { - if let Some(patched_val) = patch.get_global(variable_name) { - changed_vars.insert(variable_name.to_string(), patched_val.value.clone()); - } - } - } - - changed_vars + changed } pub fn snapshot_default_globals(&mut self) { @@ -99,7 +88,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; @@ -115,7 +104,7 @@ impl VariablesState { let mut value = value; // Constructing new variable pointer reference if var_ass.is_new_declaration { - if let Some(var_pointer) = Value::get_value::<&VariablePointerValue>(value.as_ref()) { + if let Some(var_pointer) = Value::get_variable_pointer_value(value.as_ref()) { value = self.resolve_variable_pointer(var_pointer); } } else { @@ -127,7 +116,7 @@ impl VariablesState { match existing_pointer { Some(existing_pointer) => { - match Value::get_value::<&VariablePointerValue>(existing_pointer.as_ref()) { + match Value::get_variable_pointer_value(existing_pointer.as_ref()) { Some(pv) => { name = pv.variable_name.to_string(); context_index = pv.context_index; @@ -164,7 +153,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); @@ -178,14 +167,12 @@ 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 Value::get_value::<&VariablePointerValue>(value_of_variable_pointed_to.as_ref()) - .is_some() - { + if Value::get_variable_pointer_value(value_of_variable_pointed_to.as_ref()).is_some() { return value_of_variable_pointed_to; } } - Rc::new(Value::new_variable_pointer( + Brc::new(Value::new_variable_pointer( &var_pointer.variable_name, context_index, )) @@ -200,9 +187,9 @@ impl VariablesState { ))); } - let val = Value::new_value_type(value_type); + 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) } @@ -240,7 +227,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 { @@ -281,8 +268,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); @@ -303,7 +290,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); @@ -318,11 +305,11 @@ 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() { - if let Some(var_pointer) = Value::get_value::<&VariablePointerValue>(vv.as_ref()) { + if let Some(var_pointer) = Value::get_variable_pointer_value(vv.as_ref()) { return self.value_at_variable_pointer(var_pointer); } } @@ -330,11 +317,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/choice_test.rs b/lib/tests/choice_test.rs index 2b071a1..e8fff92 100644 --- a/lib/tests/choice_test.rs +++ b/lib/tests/choice_test.rs @@ -104,7 +104,7 @@ fn suppress_choice_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!( "Hello back!", - story.get_current_choices().first().unwrap().text + story.get_current_choices().get(0).unwrap().text ); story.choose_choice_index(0)?; @@ -126,7 +126,7 @@ fn mixed_choice_test() -> Result<(), StoryError> { common::next_all(&mut story, &mut text)?; assert_eq!( "Hello back!", - story.get_current_choices().first().unwrap().text + story.get_current_choices().get(0).unwrap().text ); story.choose_choice_index(0)?; diff --git a/lib/tests/common/mod.rs b/lib/tests/common/mod.rs index 4520d28..b57aba1 100644 --- a/lib/tests/common/mod.rs +++ b/lib/tests/common/mod.rs @@ -46,7 +46,7 @@ pub fn run_story( let mut choice_list_index = 0; - let mut rng = rand::rng(); + let mut rng = rand::thread_rng(); while story.can_continue() || !story.get_current_choices().is_empty() { println!("{}", story.build_string_of_hierarchy()); @@ -80,11 +80,11 @@ pub fn run_story( story.choose_choice_index(choice_list[choice_list_index])?; choice_list_index += 1; } else { - let random_choice_index = rng.random_range(0..len); + let random_choice_index = rng.gen_range(0..len); story.choose_choice_index(random_choice_index)?; } } else { - let random_choice_index = rng.random_range(0..len); + let random_choice_index = rng.gen_range(0..len); story.choose_choice_index(random_choice_index)?; } } diff --git a/lib/tests/conditional_test.rs b/lib/tests/conditional_test.rs index 29a9669..7ddcb9e 100644 --- a/lib/tests/conditional_test.rs +++ b/lib/tests/conditional_test.rs @@ -321,16 +321,6 @@ fn shuffle_test() -> Result<(), StoryError> { 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()); diff --git a/lib/tests/function_test.rs b/lib/tests/function_test.rs index 909a1b6..1394f1d 100644 --- a/lib/tests/function_test.rs +++ b/lib/tests/function_test.rs @@ -123,7 +123,7 @@ 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().get::<&str>().unwrap()); + assert_eq!("RIGHT", result?.unwrap().get_str().unwrap()); assert_eq!("End\n", story.cont()?); diff --git a/lib/tests/list_test.rs b/lib/tests/list_test.rs index 522af8f..aca24f6 100644 --- a/lib/tests/list_test.rs +++ b/lib/tests/list_test.rs @@ -113,23 +113,3 @@ fn more_list_operations2_test() -> Result<(), Box> { Ok(()) } - -#[test] -fn list_all_bug_test() -> Result<(), Box> { - let json_string = common::get_json_string("inkfiles/lists/list-all.ink.json")?; - let mut story = Story::new(&json_string)?; - - assert_eq!("A, B\n", &story.continue_maximally()?); - - Ok(()) -} - -#[test] -fn list_comparison_test() -> Result<(), Box> { - let json_string = common::get_json_string("inkfiles/lists/list-comparison.ink.json")?; - let mut story = Story::new(&json_string)?; - - assert_eq!("Hey, my name is Philippe. What about yours?\nI am Andre and I need my rheumatism pills!\nWould you like me, Philippe, to get some more for you?\n", &story.continue_maximally()?); - - Ok(()) -} diff --git a/lib/tests/misc_test.rs b/lib/tests/misc_test.rs index b0000ff..f19eb85 100644 --- a/lib/tests/misc_test.rs +++ b/lib/tests/misc_test.rs @@ -1,5 +1,3 @@ -use std::error::Error; - use bladeink::{story::Story, story_error::StoryError, value_type::ValueType}; mod common; @@ -65,28 +63,3 @@ fn issue15_test() -> Result<(), StoryError> { Ok(()) } - -#[test] -fn newlines_with_string_eval_test() -> Result<(), Box> { - let json_string = common::get_json_string("inkfiles/misc/newlines_with_string_eval.ink.json")?; - let mut story = Story::new(&json_string)?; - - assert_eq!("A\nB\nA\n3\nB\n", &story.continue_maximally()?); - - Ok(()) -} - -#[test] -fn i18n() -> Result<(), StoryError> { - let json_string = common::get_json_string("inkfiles/misc/i18n.ink.json").unwrap(); - let mut story = Story::new(&json_string)?; - - assert_eq!("áéíóú ñ\n", story.cont()?); - assert_eq!("你好\n", story.cont()?); - let current_tags = story.get_current_tags()?; - assert_eq!(1, current_tags.len()); - assert_eq!("áé", current_tags[0]); - assert_eq!("你好世界\n", story.cont()?); - - Ok(()) -} diff --git a/lib/tests/runtime_test.rs b/lib/tests/runtime_test.rs index ec9fcdb..c538cc3 100644 --- a/lib/tests/runtime_test.rs +++ b/lib/tests/runtime_test.rs @@ -1,8 +1,11 @@ use core::panic; -use std::{cell::RefCell, error::Error, rc::Rc}; +use std::error::Error; use bladeink::{ - story::{external_functions::ExternalFunction, variable_observer::VariableObserver, Story}, + story::Story, + story_callbacks::{ExternalFunction, VariableObserver}, + threadsafe::BrCell, + threadsafe::Brc, value_type::ValueType, }; @@ -26,13 +29,13 @@ impl ExternalFunction for ExtFunc1 { impl ExternalFunction for ExtFunc2 { fn call(&mut self, _: &str, _: Vec) -> Option { - Some(ValueType::new::<&str>("Hello world")) + Some(ValueType::new_string("Hello world")) } } impl ExternalFunction for ExtFunc3 { fn call(&mut self, _: &str, args: Vec) -> Option { - Some(ValueType::Bool(args[0].get::().unwrap() != 1)) + Some(ValueType::Bool(args[0].get_int().unwrap() != 1)) } } @@ -48,7 +51,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()); @@ -63,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", Brc::new(BrCell::new(ExtFunc2 {})), true)?; common::next_all(&mut story, &mut text)?; assert_eq!(1, text.len()); @@ -78,7 +81,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()); @@ -93,7 +96,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()); @@ -143,16 +146,11 @@ fn variable_observers_test() -> Result<(), Box> { let mut story = Story::new(&json_string)?; let mut text: Vec = Vec::new(); - let observer = Rc::new(RefCell::new(VObserver { expected_value: 5 })); - story.observe_variable("x", observer.clone())?; + story.observe_variable("x", Brc::new(BrCell::new(VObserver { expected_value: 5 })))?; common::next_all(&mut story, &mut text)?; story.choose_choice_index(0)?; common::next_all(&mut story, &mut text)?; - assert_eq!(10, story.get_variable("x").unwrap().get::().unwrap()); - - // Check that the observer's expected_value is now 10 - assert_eq!(observer.borrow().expected_value, 10); Ok(()) } @@ -164,11 +162,11 @@ fn set_and_get_variable_test() -> Result<(), Box> { let mut text: Vec = Vec::new(); common::next_all(&mut story, &mut text)?; - assert_eq!(10, story.get_variable("x").unwrap().get::().unwrap()); + assert_eq!(10, story.get_variable("x").unwrap().get_int().unwrap()); story.set_variable("x", &ValueType::Int(15))?; - assert_eq!(15, story.get_variable("x").unwrap().get::().unwrap()); + assert_eq!(15, story.get_variable("x").unwrap().get_int().unwrap()); story.choose_choice_index(0)?; @@ -189,14 +187,14 @@ fn set_non_existant_variable_test() -> Result<(), Box> { common::next_all(&mut story, &mut text)?; - let result = story.set_variable("y", &ValueType::new::<&str>("earth")); + let result = story.set_variable("y", &ValueType::new_string("earth")); assert!(result.is_err()); - assert_eq!(10, story.get_variable("x").unwrap().get::().unwrap()); + assert_eq!(10, story.get_variable("x").unwrap().get_int().unwrap()); story.set_variable("x", &ValueType::Int(15))?; - assert_eq!(15, story.get_variable("x").unwrap().get::().unwrap()); + assert_eq!(15, story.get_variable("x").unwrap().get_int().unwrap()); story.choose_choice_index(0)?; diff --git a/lib/tests/tag_test.rs b/lib/tests/tag_test.rs index c108441..f93cf2e 100644 --- a/lib/tests/tag_test.rs +++ b/lib/tests/tag_test.rs @@ -82,31 +82,6 @@ fn tags_in_choice_test() -> Result<(), StoryError> { Ok(()) } -#[test] -fn tags_in_choice_dynamic_content_test() -> Result<(), StoryError> { - let json_string = - common::get_json_string("inkfiles/tags/tagsInChoiceDynamic.ink.json").unwrap(); - let mut story = Story::new(&json_string)?; - - story.cont()?; // Avanzar una vez - let current_tags = story.get_current_tags()?; - assert_eq!(0, current_tags.len()); - - let choices = story.get_current_choices(); - assert_eq!(3, choices.len()); - - assert_eq!(1, choices[0].tags.len()); - assert_eq!("tag Name", choices[0].tags[0]); - - assert_eq!(1, choices[1].tags.len()); - assert_eq!("tag 1 Name 2 3 4", choices[1].tags[0]); - - assert_eq!(1, choices[2].tags.len()); - assert_eq!("Name tag 1 2 3 4", choices[2].tags[0]); - - Ok(()) -} - #[test] fn tags_dynamic_content_test() -> Result<(), StoryError> { let json_string = common::get_json_string("inkfiles/tags/tagsDynamicContent.ink.json").unwrap();