├── .gitignore ├── src ├── evaluator │ ├── mod.rs │ ├── dialogue_runner_resource.rs │ └── dialogue_runner.rs ├── yarn_asset.rs ├── parser │ ├── mod.rs │ ├── nodes.rs │ ├── structs.rs │ ├── nom_supreme_yeah.rs │ ├── dump.rs │ ├── working_choice_blocks.rs │ ├── header.rs │ ├── common.rs │ ├── choices_alt.rs │ ├── body_backup.rs │ ├── old.rs │ ├── body copy.rs │ └── body.rs ├── yarn_loader.rs ├── lib.rs └── old.rs ├── examples ├── basic │ ├── basic.png │ ├── commands.png │ ├── basic.rs │ └── commands.rs ├── bubbles │ ├── bubbles.gif │ └── bubbles.rs └── portraits │ ├── portraits.gif │ ├── portraits.png │ └── portraits.rs ├── assets ├── sounds │ ├── doorClose_1.ogg │ ├── doorOpen_1.ogg │ └── mixkit_ambient_sound_in_the_desert_2488.ogg ├── textures │ ├── portrait0.png │ ├── portrait1.png │ ├── portrait2.png │ └── portraits.svg ├── fonts │ └── FiraMono-Medium.ttf └── dialogues │ ├── micro.yarn │ ├── single_node_simple.yarn │ ├── single_node_simple_commands.yarn │ ├── single_node_nested_choices.yarn │ ├── two_nodes_jump_simple.yarn │ ├── two_nodes_jump_nested_choices.yarn │ ├── single_node_three_characters.yarn │ ├── two_nodes_jump_multiple_characters.yarn │ └── complex.yarn ├── Cargo.toml ├── LICENSE_MIT.md ├── todo.md ├── README.md ├── LICENSE_APACHE.md └── tests ├── parser_test.rs └── runner_test.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /src/evaluator/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod dialogue_runner; 2 | pub use dialogue_runner::*; 3 | -------------------------------------------------------------------------------- /examples/basic/basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaosat-dev/bevy_mod_yarn/HEAD/examples/basic/basic.png -------------------------------------------------------------------------------- /assets/sounds/doorClose_1.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaosat-dev/bevy_mod_yarn/HEAD/assets/sounds/doorClose_1.ogg -------------------------------------------------------------------------------- /assets/sounds/doorOpen_1.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaosat-dev/bevy_mod_yarn/HEAD/assets/sounds/doorOpen_1.ogg -------------------------------------------------------------------------------- /assets/textures/portrait0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaosat-dev/bevy_mod_yarn/HEAD/assets/textures/portrait0.png -------------------------------------------------------------------------------- /assets/textures/portrait1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaosat-dev/bevy_mod_yarn/HEAD/assets/textures/portrait1.png -------------------------------------------------------------------------------- /assets/textures/portrait2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaosat-dev/bevy_mod_yarn/HEAD/assets/textures/portrait2.png -------------------------------------------------------------------------------- /examples/basic/commands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaosat-dev/bevy_mod_yarn/HEAD/examples/basic/commands.png -------------------------------------------------------------------------------- /examples/bubbles/bubbles.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaosat-dev/bevy_mod_yarn/HEAD/examples/bubbles/bubbles.gif -------------------------------------------------------------------------------- /assets/fonts/FiraMono-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaosat-dev/bevy_mod_yarn/HEAD/assets/fonts/FiraMono-Medium.ttf -------------------------------------------------------------------------------- /examples/portraits/portraits.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaosat-dev/bevy_mod_yarn/HEAD/examples/portraits/portraits.gif -------------------------------------------------------------------------------- /examples/portraits/portraits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaosat-dev/bevy_mod_yarn/HEAD/examples/portraits/portraits.png -------------------------------------------------------------------------------- /assets/sounds/mixkit_ambient_sound_in_the_desert_2488.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaosat-dev/bevy_mod_yarn/HEAD/assets/sounds/mixkit_ambient_sound_in_the_desert_2488.ogg -------------------------------------------------------------------------------- /assets/dialogues/micro.yarn: -------------------------------------------------------------------------------- 1 | title: B 2 | --- 3 | it was a beautiful day , said nobody 4 | -> A 5 | -> A1 6 | -> A1-1 7 | -> A1-1-1 8 | -> A1-2 9 | -> A2 10 | -> B 11 | === 12 | -------------------------------------------------------------------------------- /assets/dialogues/single_node_simple.yarn: -------------------------------------------------------------------------------- 1 | title: Start 2 | --- 3 | it was a beautiful day , said nobody 4 | Lamik: hi ! 5 | Dona: good morning , how are you ? 6 | -> Lamik: are you asking me ? 7 | Dona: yes 8 | -> Lamik: fine ! 9 | Dona: good to hear 10 | === -------------------------------------------------------------------------------- /assets/dialogues/single_node_simple_commands.yarn: -------------------------------------------------------------------------------- 1 | title: Start 2 | --- 3 | <> 4 | Lamik: hi ! 5 | Dona: hi there Lamik ! 6 | <> 7 | Dona: oh, sorry , could you please close the door, it is very windy outside. 8 | Lamik: yes, of course ! 9 | <> 10 | === -------------------------------------------------------------------------------- /assets/dialogues/single_node_nested_choices.yarn: -------------------------------------------------------------------------------- 1 | title: Test_node 2 | tags: 3 | colorID: 0 4 | position: 567,-265 5 | --- 6 | Dona: what is wrong ? 7 | -> Grumpy: are you asking me ? 8 | Dona: sure 9 | -> Grumpy: are you sure ? 10 | Dona: yes 11 | -> Grumpy: what ? 12 | Dona: what what? 13 | -> Grumpy: lots of things 14 | Dona: oh no ! 15 | Bob: and me ? 16 | === 17 | -------------------------------------------------------------------------------- /src/yarn_asset.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::YarnNode; 2 | use bevy::{ 3 | prelude::Asset, 4 | reflect::{TypePath, TypeUuid}, 5 | }; 6 | use std::collections::HashMap; 7 | 8 | #[derive(Asset, Debug, Default, TypeUuid, TypePath, Clone)] 9 | #[uuid = "2ede09ba-8be6-4fe4-8f7a-8a1b3ea96b3b"] 10 | pub struct YarnAsset { 11 | pub raw: String, 12 | pub nodes: HashMap, 13 | } 14 | -------------------------------------------------------------------------------- /assets/dialogues/two_nodes_jump_simple.yarn: -------------------------------------------------------------------------------- 1 | title: Foo 2 | tags: #test_tag othertag 3 | colorID: 1 4 | position: 301,-291 5 | --- 6 | Lamik: Hi there ! 7 | Dona: Hello ! 8 | <> 9 | === 10 | title: Bar 11 | tags: 12 | colorID: 0 13 | position: 567,-265 14 | --- 15 | Lamik: bleh bleh 16 | Dona: what is wrong ? 17 | -> Lamik: nothing 18 | Dona: oh ok 19 | -> Lamik: everything! 20 | <> 21 | === 22 | -------------------------------------------------------------------------------- /src/parser/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | pub mod common; 4 | pub use common::*; 5 | 6 | pub mod header; 7 | pub use header::*; 8 | 9 | pub mod body; 10 | pub use body::*; 11 | 12 | pub mod nodes; 13 | pub use nodes::*; 14 | 15 | pub mod structs; 16 | pub use structs::*; 17 | 18 | /// main entry point 19 | pub fn parse_yarn_nodes(yarn_text: &str) -> HashMap { 20 | if let Ok(result) = yarn_nodes(yarn_text) { 21 | return result.1; 22 | } 23 | return HashMap::new(); 24 | } 25 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_mod_yarn" 3 | version = "0.3.0" 4 | edition = "2021" 5 | 6 | 7 | [dependencies] 8 | nom = "7.1.2" 9 | nom-supreme = "0.8.0" 10 | bevy = { version = "0.12", default-features = false, features = ["bevy_asset"] } 11 | 12 | [dev-dependencies] 13 | bevy="0.12" 14 | 15 | [[example]] 16 | name = "basic" 17 | path = "examples/basic/basic.rs" 18 | 19 | [[example]] 20 | name = "commands" 21 | path = "examples/basic/commands.rs" 22 | 23 | [[example]] 24 | name = "portraits" 25 | path = "examples/portraits/portraits.rs" 26 | 27 | 28 | [[example]] 29 | name = "bubbles" 30 | path = "examples/bubbles/bubbles.rs" -------------------------------------------------------------------------------- /assets/dialogues/two_nodes_jump_nested_choices.yarn: -------------------------------------------------------------------------------- 1 | title: Test_node 2 | --- 3 | it was a beautiful day , said nobody 4 | Lamik: hi ! 5 | Dona: good morning , how are you ? 6 | -> Lamik: are you asking me ? 7 | Dona: yes 8 | -> Lamik: fine ! 9 | Dona: good to hear 10 | Dona: so... what have you been up to ? 11 | -> Lamik: i have started working on the most AMAZING project ever ! 12 | Dona: ohh cool , tell me more !! 13 | -> Lamik: short version then 14 | Lamik: a mechanical bunny of doom ! 15 | -> Lamik: the long version ? here goes 16 | <> 17 | -> Lamik: too early to tell 18 | Dona: oh ok, well, anyway, gotta go ! 19 | Lamik: ok, bye ! 20 | Dona: see you soon ! 21 | === 22 | title: project_long 23 | --- 24 | Lamik: so as I was saying, a mechanical bunny of doom ! 25 | Lamik: ... with floppy ears of course 26 | === 27 | -------------------------------------------------------------------------------- /assets/dialogues/single_node_three_characters.yarn: -------------------------------------------------------------------------------- 1 | title: Start 2 | tags: #test_tag othertag 3 | colorID: 1 4 | position: 301,-291 5 | --- 6 | Lamik: Hi folks ! 7 | Dona: Hi Lamik ! 8 | Blob: Heyyas ! 9 | Dona: how are you doing ? 10 | ->Lamik: fine fine and you ? 11 | ->Lamik: meh 12 | Blob: so shall we get something to eat ? 13 | Dona: good idea Blob! I am starving ! 14 | -> Lamik: me too ! 15 | Dona: I heard there is a new abysall horror sushi place in town ! 16 | Blob: ooh sounds just like my thing 17 | Lamik: yess !! 18 | Lamik: Nothing beats screaming unameable horrors on your plate ! 19 | Dona: :) 20 | Blob: let's go ! 21 | -> Lamik: not really hungry... 22 | Dona: come on, I am sure you will be hungry by the time we get there 23 | Blob: yeah , Lamik, it's been so long since we last ate together 24 | Lamik: hmm ok then, you got me convinced ! 25 | === -------------------------------------------------------------------------------- /assets/dialogues/two_nodes_jump_multiple_characters.yarn: -------------------------------------------------------------------------------- 1 | title: Start 2 | tags: #test_tag othertag 3 | colorID: 1 4 | position: 301,-291 5 | --- 6 | Lamik: Hi there ! 7 | Dona: Hello ! 8 | Lamik: how are you doing ? 9 | <> 10 | Bob: Hi ! 11 | Grumpy: Grumble ! 12 | Bob: Oh hello there grumpy ! 13 | Dona: fine and you ? 14 | -> Lamik: doing ok 15 | Dona: good to hear :) 16 | Grumpy: whatever ! 17 | Bob: cool cool cool ! 18 | Dona: let's have a party then ! 19 | Grumpy: NO! 20 | Bob: sure, whatever.. 21 | Lamik: yeah ! 22 | <> 23 | -> Lamik: not so great, sadly :( 24 | Dona: oh, what is the matter ? 25 | Lamik: have not caught any crabs. 26 | Lamik: continuing to talk here outside of choices 27 | === 28 | title: Other_node 29 | tags: 30 | colorID: 0 31 | position: 567,-265 32 | --- 33 | Lamik: bleh bleh 34 | Dona: what is wrong ? 35 | -> Grumpy: and what about me ? 36 | Bob: sure 37 | -> Grumpy: yes ? 38 | -> Grumpy: what ? 39 | -> Grumpy: another answer 40 | 41 | Bob: and me ? 42 | === 43 | -------------------------------------------------------------------------------- /src/parser/nodes.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use nom::{ 4 | bytes::complete::{tag, take_until}, 5 | multi::separated_list0, 6 | IResult, 7 | }; 8 | 9 | use super::{body, header, YarnNode}; 10 | 11 | pub fn yarn_nodes(input: &str) -> IResult<&str, HashMap> { 12 | let (input, nodes_raw) = separated_list0(tag("==="), take_until("==="))(input)?; 13 | let mut yarn_nodes: HashMap = HashMap::new(); 14 | 15 | for node_raw in nodes_raw.iter() { 16 | let mut node = YarnNode { 17 | ..Default::default() 18 | }; 19 | if let Ok((body_raw, (title, tags))) = header(node_raw) { 20 | node.title = title.to_string(); 21 | node.tags = tags; 22 | 23 | if let Ok((_, root_node)) = body(body_raw.trim_start()) { 24 | node.branch = root_node; 25 | } 26 | yarn_nodes.insert(title.to_string(), node); 27 | } else { 28 | println!("ERROR") 29 | // ERROR !! 30 | } 31 | } 32 | Ok((input, yarn_nodes)) 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE_MIT.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Mark "kaosat-dev" Moissette 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/yarn_loader.rs: -------------------------------------------------------------------------------- 1 | use std::str::Utf8Error; 2 | 3 | use bevy::{ 4 | asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext}, 5 | utils::{thiserror, BoxedFuture}, 6 | }; 7 | 8 | use crate::prelude::{parse_yarn_nodes, YarnAsset}; 9 | 10 | #[derive(Default)] 11 | pub struct YarnAssetLoader; 12 | 13 | /// Possible errors that can be produced by [`YarnAssetLoader`] 14 | #[non_exhaustive] 15 | #[derive(thiserror::Error, Debug)] 16 | pub enum YarnAssetLoaderError { 17 | ///An [IO](std::io) Error 18 | #[error("Could not load file: {0}")] 19 | Io(#[from] std::io::Error), 20 | 21 | #[error("Could not load file: {0}")] 22 | Utf8(#[from] Utf8Error), 23 | } 24 | 25 | impl AssetLoader for YarnAssetLoader { 26 | type Asset = YarnAsset; 27 | type Settings = (); 28 | type Error = YarnAssetLoaderError; 29 | 30 | fn load<'a>( 31 | &'a self, 32 | reader: &'a mut Reader, 33 | _settings: &'a Self::Settings, 34 | _load_context: &'a mut LoadContext, 35 | ) -> BoxedFuture<'a, Result> { 36 | Box::pin(async move { 37 | let mut bytes = Vec::new(); 38 | reader.read_to_end(&mut bytes).await?; 39 | let data_str = std::str::from_utf8(&bytes)?; 40 | let asset = YarnAsset { 41 | nodes: parse_yarn_nodes(data_str), 42 | raw: data_str.into(), 43 | }; 44 | Ok(asset) 45 | }) 46 | } 47 | 48 | fn extensions(&self) -> &[&str] { 49 | &["yarn"] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/parser/structs.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Default, PartialEq)] 2 | pub struct YarnNode { 3 | pub title: String, 4 | pub tags: Vec, 5 | pub branch: Branch, 6 | } 7 | 8 | #[derive(Debug, Clone, Default, PartialEq)] 9 | pub struct Branch { 10 | pub statements: Vec, 11 | } 12 | 13 | #[derive(Debug, Clone, Default, PartialEq)] 14 | pub struct Dialogue { 15 | pub who: String, 16 | pub what: String, 17 | pub attributes: Vec, 18 | pub tags: Vec, 19 | } 20 | 21 | #[derive(Debug, Clone, Default, PartialEq)] 22 | pub struct Choice { 23 | pub branches: Vec, 24 | pub tags: Vec, 25 | } 26 | 27 | #[derive(Debug, Clone, Default, PartialEq)] 28 | pub struct YarnCommand { 29 | pub name: String, 30 | pub params: String, 31 | pub command_type: Commands, // FIXME: meh, this should perhaps replace the YarnCommand completely ? 32 | pub tags: Vec, 33 | } 34 | 35 | #[derive(Debug, Clone, Default, PartialEq)] 36 | pub enum Commands { 37 | Declare, 38 | Set, 39 | Jump, 40 | Stop, 41 | #[default] 42 | Generic, 43 | } 44 | 45 | #[derive(Debug, Clone, PartialEq)] 46 | pub enum Statements { 47 | Dialogue(Dialogue), 48 | Choice(Choice), 49 | Command(YarnCommand), 50 | 51 | // Fixme not sure, these are convenience enums make parsing easier but might not be the most practical 52 | ChoiceBranch(Branch), 53 | Empty, 54 | Exit, 55 | } 56 | 57 | // TODO: perhaps add a trait for all statements, and attach tags there ? or add tags to all base enum entries, and change Vec to an iterable "Branches" 58 | -------------------------------------------------------------------------------- /assets/dialogues/complex.yarn: -------------------------------------------------------------------------------- 1 | title: Start 2 | tags: #test_tag othertag 3 | colorID: 1 4 | position: 301,-291 5 | --- 6 | Lamik: Hi there ! 7 | Dona: Hello ! 8 | Lamik: how are you doing ? 9 | <> 10 | Bob: Hi ! 11 | Grumpy: Grumble ! 12 | Bob: Oh hello there grumpy ! 13 | Dona: fine and you ? 14 | -> Lamik: doing ok 15 | Dona: good to hear :) 16 | Grumpy: whatever ! 17 | Bob: cool cool cool ! 18 | Dona: let's have a party then ! 19 | Grumpy: NO! 20 | Bob: sure, whatever.. 21 | Lamik: yeah ! 22 | <> 23 | -> Lamik: not so great, sadly :( 24 | Dona: oh, what is the matter ? 25 | Lamik: have not caught any crabs. 26 | <> 27 | Lamik: continuing to talk here outside of choices 28 | === 29 | title: Other_node 30 | tags: 31 | colorID: 0 32 | position: 567,-265 33 | --- 34 | Lamik: bleh bleh 35 | Dona: what is wrong ? 36 | -> Grumpy: and what about me ? 37 | Bob: sure 38 | -> Grumpy: yes ? 39 | -> Grumpy: what ? 40 | -> Grumpy: another answer 41 | 42 | Bob: and me ? 43 | === 44 | title: nested_node 45 | tags: 46 | colorID: 0 47 | position: 567,-265 48 | --- 49 | Dona: what is wrong ? 50 | Lamik: let's do some nesting 51 | -> Grumpy: and what about me ? 52 | Bob: sure 53 | -> Grumpy: yes ? 54 | Bob: what 55 | -> Grumpy: more nesting ? 56 | Bob: not sure 57 | -> Grumpy: or not ? 58 | Bob: sigh 59 | -> no more nesting 60 | -> Grumpy: what ? 61 | -> Grumpy: another answer 62 | 63 | Bob: and now back to our normal programming 64 | Yuri: really ? 65 | Bob: yeah, so a boring one 66 | === 67 | -------------------------------------------------------------------------------- /src/parser/nom_supreme_yeah.rs: -------------------------------------------------------------------------------- 1 | use nom::{ 2 | bytes::complete::{take_till, take_until, take_while_m_n, take_while}, 3 | sequence::{tuple, pair}, 4 | multi::{separated_list1, many0_count, many0, separated_list0, many1, many_till}, 5 | IResult, Parser, combinator::{eof, recognize}, 6 | branch::alt, character::{complete::alpha1, streaming::alphanumeric1}, 7 | 8 | }; 9 | use nom_supreme::{error::ErrorTree, final_parser::final_parser}; 10 | use nom_supreme::tag::streaming::tag; 11 | use nom_supreme::ParserExt; 12 | // use nom_supreme::tag::streaming::tag; 13 | 14 | #[derive(Debug, PartialEq)] 15 | pub struct Color { 16 | pub red: u8, 17 | pub green: u8, 18 | pub blue: u8, 19 | } 20 | 21 | fn from_hex( 22 | input: &str, 23 | ) -> Result { 24 | u8::from_str_radix(input, 16) 25 | } 26 | 27 | fn is_hex_digit(c: char) -> bool { 28 | c.is_digit(16) 29 | } 30 | 31 | fn hex_primary( 32 | input: &str, 33 | ) -> IResult<&str, u8, ErrorTree<&str>> { 34 | take_while_m_n(2, 2, is_hex_digit) 35 | .context("Should be a 2 digit hex code") 36 | .map_res(from_hex) 37 | .parse(input) 38 | } 39 | 40 | fn hex_color( 41 | input: &str, 42 | ) -> IResult<&str, Color, ErrorTree<&str>> { 43 | tuple((hex_primary, hex_primary, hex_primary)) 44 | .preceded_by(tag("#")) 45 | .parse(input) 46 | .map(|(input, (red, green, blue))| { 47 | (input, Color { red, green, blue }) 48 | }) 49 | } 50 | 51 | 52 | 53 | pub fn identifier2(input: &str) -> IResult<&str, &str, ErrorTree<&str>> { 54 | recognize( 55 | pair( 56 | alt((alpha1, tag("_"))), 57 | many0_count(alt((alphanumeric1, tag("_")))) 58 | ) 59 | )(input) 60 | } 61 | 62 | 63 | pub fn foo_bar_baz(input: &str) -> IResult<&str, &str, ErrorTree<&str>> { 64 | 65 | 66 | //let bla = many_till( eof) 67 | tag("->").precedes(tag(" ")).precedes(identifier2).parse(input) 68 | // tag("foo").preceded_by(tag("#")).complete().parse(input) 69 | // tuple(()) 70 | } 71 | 72 | pub fn hex_color_final( 73 | input: &str, 74 | ) -> Result<&str, ErrorTree<&str>> { 75 | let test_input = " 76 | (aa)(fine) 77 | (dsf)(sdf)(sdfezae)(azezae) 78 | (other)"; 79 | let foo = "#foobar"; 80 | let foo = "-> yes"; 81 | 82 | final_parser(foo_bar_baz)(foo) 83 | } -------------------------------------------------------------------------------- /src/parser/dump.rs: -------------------------------------------------------------------------------- 1 | // dump for unused code, will get removed later 2 | 3 | 4 | use nom::{ 5 | bytes::complete::{tag, is_not, take_till, take_until, take_while_m_n, take_while}, 6 | branch::alt, 7 | error::ParseError, 8 | 9 | IResult, 10 | multi::{separated_list1, many0_count, many0, separated_list0, many1, many_till, count}, 11 | character::complete::{newline, alphanumeric0, anychar, alpha1, alphanumeric1, multispace0, space0, digit0, one_of, char, line_ending, not_line_ending}, 12 | sequence::{delimited, preceded, terminated, pair, separated_pair, tuple }, 13 | combinator::{recognize, opt, not, eof, map}, 14 | InputTakeAtPosition, 15 | number::complete::{float, recognize_float} 16 | }; 17 | use nom::{ 18 | bytes::complete::{tag, is_not, take_till, take_until}, 19 | branch::alt, 20 | error::ParseError, 21 | 22 | IResult, multi::{separated_list1, many0_count, many0, separated_list0, many1}, 23 | character::complete::{newline, alphanumeric0, anychar, alpha1, alphanumeric1, multispace0, space0, digit0, one_of, char}, 24 | sequence::{delimited, preceded, terminated, pair, separated_pair, tuple, }, 25 | combinator::{recognize, opt, not}, 26 | InputTakeAtPosition, 27 | number::complete::{float, recognize_float} 28 | }; 29 | 30 | 31 | /* 32 | pub fn get_current_branch(mut choices_stack: Vec, current_branch: Branch) -> Branch{ 33 | 34 | if choices_stack.len()> 0 { 35 | return *choices_stack.last_mut() 36 | .expect("we should always have one item in the stack here") 37 | .branches.last_mut() 38 | .expect("we always have at least one branch") 39 | }else { 40 | return current_branch 41 | } 42 | }*/ 43 | 44 | 45 | pub fn state_pop(mut stack: Vec, mut current_branch : Branch, mut current_branches: Vec) -> Branch{ 46 | current_branches.push(current_branch.clone()); 47 | 48 | if stack.len() > 0 { 49 | current_branch = stack.pop().unwrap(); 50 | if current_branches.len() > 0 { 51 | current_branch.statements.push( // need to be pushed to the parent branch, so that is why we pop() first 52 | Statements::Choice(Choice { branches: current_branches.clone() , ..Default::default()} ) 53 | ); 54 | } 55 | } 56 | println!("nesting level {}", stack.len()); 57 | 58 | current_branches = vec![]; 59 | 60 | current_branch 61 | } 62 | -------------------------------------------------------------------------------- /src/parser/working_choice_blocks.rs: -------------------------------------------------------------------------------- 1 | use nom::{ 2 | character::complete::{alphanumeric1, newline, space0}, 3 | multi::{separated_list1, many_till}, 4 | sequence::{pair, preceded, terminated}, 5 | IResult, 6 | combinator:: {recognize, eof}, 7 | branch:: {alt}, 8 | bytes::complete::{tag, take_until} 9 | }; 10 | use nom::combinator::opt; 11 | use crate::Statements; 12 | 13 | use super::{identifier, spacey, statement_dialogue, statement_command}; 14 | use nom_supreme::error::ErrorTree; 15 | 16 | #[derive(Debug)] 17 | pub struct Branch { 18 | pub lines: Vec 19 | } 20 | 21 | #[derive(Debug)] 22 | pub struct Choice { 23 | pub branches: Vec 24 | } 25 | 26 | 27 | fn seperator_empty_lines(input: &str) -> IResult<&str, (char, &str)> { 28 | pair(newline, empty_line)(input) 29 | } 30 | 31 | fn seperator_other(input: &str) -> IResult<&str, (char, &str)> { 32 | pair(newline, tag(":"))(input) 33 | } 34 | 35 | 36 | fn empty_line(input: &str) -> IResult<&str, &str> { 37 | recognize( 38 | many_till(space0, alt(( tag("\n"), eof )) ) 39 | )(input) 40 | } 41 | 42 | 43 | /// TODO : this would actually be the generic line parser 44 | fn line_inner(input: &str) -> IResult<&str, &str> { 45 | spacey(identifier)(input) 46 | } 47 | 48 | fn branch_content(input: &str) -> IResult<&str, Branch> { 49 | //println!("CONTENT: {:?}", input); 50 | let(input, _) = opt(spacey(tag("->")))(input) ?; 51 | let(rest, lines) = separated_list1( 52 | newline, 53 | line_inner 54 | )(input)?; 55 | //println!("branch_content {:?} {:?}", rest, bla); 56 | 57 | let branch = Branch { 58 | lines: lines.iter().map(|x|x.to_string()).collect() 59 | }; 60 | 61 | Ok((rest, branch)) 62 | } 63 | 64 | /// parses out blocks containing multiple choices 65 | fn choices_block(input: &str) -> IResult<&str, Choice, > { 66 | 67 | let (input, branches) = 68 | separated_list1( 69 | newline, 70 | branch_content 71 | 72 | )(input) ?; 73 | 74 | Ok((input, Choice{branches})) 75 | } 76 | 77 | 78 | ///parses blocks containing multiple choices, seperate by empty lines 79 | /// TODO: this not QUITE right, as we would need to only use this when starting with a choice thingy '->' 80 | fn root_blocks(input: &str) -> IResult<&str, Vec> { 81 | separated_list1( 82 | seperator_empty_lines, choices_block // alt((seperator_empty_lines, seperator_other)) 83 | )(input) 84 | } 85 | 86 | 87 | pub fn parse_all_yeah(input: &str) -> IResult<&str, Vec> { 88 | root_blocks(input) 89 | } 90 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod evaluator; 2 | pub mod parser; 3 | mod yarn_asset; 4 | mod yarn_loader; 5 | 6 | use bevy::prelude::{App, AssetApp, Plugin}; 7 | pub use evaluator::DialogueRunner; 8 | pub use yarn_asset::YarnAsset; 9 | pub use yarn_loader::YarnAssetLoader; 10 | 11 | /// A Bevy plugin for yarn dialogue files 12 | /// 13 | /// Add this plugin to your Bevy app to get access to 14 | /// the DialogueRunner component 15 | /// ``` 16 | /// # use bevy::prelude::*; 17 | /// # use bevy_mod_yarn::prelude::*; 18 | /// 19 | /// # use bevy::asset::AssetPlugin; 20 | /// # use bevy::app::AppExit; 21 | /// 22 | /// fn main() { 23 | /// App::new() 24 | /// .add_plugins(DefaultPlugins) 25 | /// .add_plugin(YarnPlugin) 26 | /// .init_resource::() 27 | /// 28 | /// .add_startup_system(setup) 29 | /// .add_system(dialogue_init) 30 | /// .run(); 31 | /// } 32 | /// // only needed for manual loading, not when using tools like [bevy_asset_loader](https://github.com/NiklasEi/bevy_asset_loader) 33 | /// #[derive(Resource, Default)] 34 | /// struct State { 35 | /// handle: Handle, 36 | /// done: bool 37 | /// } 38 | /// 39 | /// fn setup( 40 | /// mut state: ResMut, 41 | /// asset_server: Res, 42 | /// mut commands: bevy::prelude::Commands 43 | /// ){ 44 | /// 45 | /// // load the yarn dialogue file 46 | /// state.handle = asset_server.load("dialogues/single_node_simple.yarn"); 47 | /// 48 | /// // any other bevy setup 49 | /// } 50 | /// // spawn a dialogueRunner 51 | /// fn dialogue_init(mut state: ResMut, dialogues: Res>, mut commands: bevy::prelude::Commands) { 52 | /// if let Some(dialogues)= dialogues.get(&state.handle) { 53 | /// if !state.done { 54 | /// commands.spawn( DialogueRunner::new(dialogues.clone(), "Start")); 55 | /// state.done = true; 56 | /// } 57 | /// } 58 | /// } 59 | /// 60 | /// ``` 61 | 62 | #[derive(Default)] 63 | pub struct YarnPlugin; 64 | impl Plugin for YarnPlugin { 65 | fn build(&self, app: &mut App) { 66 | app.init_asset::() 67 | .init_asset_loader::(); 68 | } 69 | } 70 | 71 | /// Most commonly used types 72 | pub mod prelude { 73 | 74 | pub use crate::YarnPlugin; 75 | #[doc(hidden)] 76 | pub use crate::{ 77 | evaluator::dialogue_runner::*, 78 | parser::{ 79 | parse_yarn_nodes, statement_choice, statement_command, statement_dialogue, structs::*, 80 | }, 81 | yarn_asset::YarnAsset, 82 | yarn_loader::YarnAssetLoader, 83 | }; 84 | } 85 | 86 | #[doc = include_str!("../README.md")] 87 | #[cfg(doctest)] 88 | struct ReadmeDoctests; 89 | -------------------------------------------------------------------------------- /src/parser/header.rs: -------------------------------------------------------------------------------- 1 | use nom::{ 2 | branch::alt, 3 | bytes::complete::{tag, tag_no_case, take_until}, 4 | character::complete::{alpha1, alphanumeric1, newline, not_line_ending}, 5 | combinator::recognize, 6 | multi::{many0, many0_count, separated_list1}, 7 | sequence::{pair, tuple}, 8 | IResult, 9 | }; 10 | 11 | use super::{body::till_end, identifier, spacey}; 12 | 13 | /// called header tags in the yarn spec , but that is confusing 14 | #[derive(Debug, Clone)] 15 | pub enum HeaderLine { 16 | Title(String), 17 | Tags(Vec), 18 | Discard, 19 | } 20 | 21 | ///Parsing identifiers that must start with a # and may contain underscores, letters and numbers and : 22 | pub fn header_tag_identifier(input: &str) -> IResult<&str, &str> { 23 | recognize(pair( 24 | alt(( 25 | alpha1, 26 | tag("_"), 27 | tag("#"), // for this one, the pound at the start is optional, unlike for the tag_identifier 28 | )), 29 | many0_count(alt((alphanumeric1, tag("_"), tag(":")))), 30 | ))(input) 31 | } 32 | 33 | /// from the yarn docs: Node titles must start with a letter, and can contain letters, numbers and underscores. 34 | pub fn title(input: &str) -> IResult<&str, HeaderLine> { 35 | let (input, _) = tuple((spacey(tag_no_case("title")), tag(":")))(input)?; 36 | let (input, title) = spacey(identifier)(input)?; 37 | if input.len() > 0 { 38 | // Err("Invalid title") 39 | } 40 | 41 | Ok((input, HeaderLine::Title(title.into()))) 42 | } 43 | 44 | pub fn header_tags(input: &str) -> IResult<&str, HeaderLine> { 45 | let (input, _) = tuple((spacey(tag_no_case("tags")), tag(":")))(input)?; 46 | // 0...n tags 47 | let (input, tags) = many0(spacey(header_tag_identifier))(input)?; 48 | 49 | Ok(( 50 | input, 51 | HeaderLine::Tags(tags.iter().map(|x| x.to_string()).collect()), 52 | )) 53 | } 54 | 55 | /// to discard header tags/lines we do not care about 56 | pub fn discard(input: &str) -> IResult<&str, HeaderLine> { 57 | let (input, _) = till_end(input)?; 58 | Ok((input, HeaderLine::Discard)) 59 | } 60 | 61 | pub fn header(input: &str) -> IResult<&str, (String, Vec)> { 62 | let (input, header) = take_until("---")(input)?; 63 | let (input, _) = tag("---")(input)?; 64 | let (_, header) = separated_list1(newline, not_line_ending)(header)?; 65 | 66 | let mut _title: String = "".into(); 67 | let mut _tags: Vec = vec![]; 68 | for line in header.iter() { 69 | let (_, header_line) = alt((title, header_tags, discard))(line)?; 70 | match header_line { 71 | HeaderLine::Title(title) => { 72 | _title = title; 73 | } 74 | HeaderLine::Tags(tags) => { 75 | _tags = tags; 76 | } 77 | _ => {} 78 | } 79 | } 80 | 81 | Ok((input, (_title, _tags))) 82 | } 83 | -------------------------------------------------------------------------------- /src/parser/common.rs: -------------------------------------------------------------------------------- 1 | use nom::{ 2 | branch::alt, 3 | bytes::complete::{is_not, tag}, 4 | character::complete::{alpha1, alphanumeric1, space0}, 5 | combinator::recognize, 6 | error::ParseError, 7 | multi::{many0_count, separated_list1}, 8 | number::complete::recognize_float, 9 | sequence::{delimited, pair}, 10 | IResult, 11 | }; 12 | 13 | ///Parsing identifiers that may start with a letter (or underscore) and may contain underscores, letters and numbers 14 | pub fn identifier(input: &str) -> IResult<&str, &str> { 15 | recognize(pair( 16 | alt((alpha1, tag("_"))), 17 | many0_count(alt((alphanumeric1, tag("_")))), 18 | ))(input) 19 | } 20 | 21 | ///Parsing identifiers that must start with a # and may contain underscores, letters and numbers and : 22 | pub fn tag_identifier(input: &str) -> IResult<&str, &str> { 23 | recognize(pair( 24 | tag("#"), 25 | many0_count(alt((alphanumeric1, tag("_"), tag(":")))), 26 | ))(input) 27 | } 28 | 29 | ///Parsing variables that must start with a letter (or underscore) and may contain underscores, letters and numbers 30 | pub fn variable_identifier(input: &str) -> IResult<&str, &str> { 31 | recognize(pair( 32 | tag("$"), 33 | many0_count(alt((alphanumeric1, tag("_"), tag(".")))), 34 | ))(input) 35 | } 36 | 37 | pub fn variable(input: &str) -> IResult<&str, &str> { 38 | let (input, variable) = identifier(input)?; 39 | Ok((input, variable)) 40 | } 41 | 42 | pub fn operator(input: &str) -> IResult<&str, &str> { 43 | alt(( 44 | tag("=="), 45 | tag("<"), 46 | tag(">"), 47 | tag("<="), // FIXME: does not work ? 48 | tag(">="), // FIXME: does not work ? 49 | ))(input) 50 | } 51 | 52 | pub fn expression(input: &str) -> IResult<&str, &str> { 53 | let (input, variable) = variable_identifier(input)?; 54 | // println!("expression {}", variable); 55 | Ok((input, variable)) 56 | } 57 | 58 | pub fn parse_params(input: &str) -> IResult<&str, Vec<&str>> { 59 | let (input, params) = separated_list1( 60 | tag(" "), 61 | alt((variable, recognize_float, expression, operator)), // each item in parameters can be either a variable, a number (float), an expression (FIXME: operator should not be here) 62 | )(input)?; // should be alt(variable, numeric) 63 | Ok((input, params)) 64 | } 65 | 66 | pub fn any_non_whitespace(input: &str) -> IResult<&str, &str> { 67 | is_not(" ")(input) 68 | // not(space0)(input) 69 | } 70 | 71 | // TODO move to utils 72 | /* 73 | use nom::{ 74 | IResult, 75 | error::ParseError, 76 | combinator::value, 77 | sequence::delimited, 78 | character::complete::multispace0, 79 | };*/ 80 | 81 | /// A combinator that takes a parser `inner` and produces a parser that also consumes both leading and 82 | /// trailing whitespace, returning the output of `inner`. 83 | pub fn spacey<'a, F, O, E: ParseError<&'a str>>( 84 | inner: F, 85 | ) -> impl FnMut(&'a str) -> IResult<&'a str, O, E> 86 | where 87 | F: FnMut(&'a str) -> IResult<&'a str, O, E>, 88 | { 89 | delimited(space0, inner, space0) 90 | } 91 | -------------------------------------------------------------------------------- /examples/basic/basic.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_mod_yarn::prelude::*; 3 | 4 | fn main() { 5 | App::new() 6 | .add_plugins((DefaultPlugins, YarnPlugin)) 7 | .init_resource::() 8 | .add_systems(Startup, setup) 9 | .add_systems( 10 | Update, 11 | (dialogue_init, dialogue_navigation, dialogue_display), 12 | ) 13 | .run(); 14 | } 15 | 16 | #[derive(Resource, Default)] 17 | struct State { 18 | handle: Handle, 19 | done: bool, 20 | } 21 | 22 | fn setup( 23 | mut state: ResMut, 24 | asset_server: Res, 25 | mut commands: bevy::prelude::Commands, 26 | ) { 27 | // load the yarn dialogue file 28 | state.handle = asset_server.load("dialogues/single_node_simple.yarn"); 29 | 30 | // setup a simple 2d camera 31 | commands.spawn(Camera2dBundle { 32 | transform: Transform::from_xyz(0.0, 5.0, 0.0), 33 | ..default() 34 | }); 35 | 36 | commands.spawn( 37 | TextBundle::from_section( 38 | "", 39 | TextStyle { 40 | font: asset_server.load("fonts/FiraMono-Medium.ttf"), 41 | font_size: 18.0, 42 | color: Color::WHITE, 43 | }, 44 | ) 45 | .with_style(Style { 46 | position_type: PositionType::Absolute, 47 | top: Val::Px(10.0), 48 | left: Val::Px(10.0), 49 | ..default() 50 | }), 51 | ); 52 | } 53 | 54 | fn dialogue_init( 55 | mut state: ResMut, 56 | dialogues: Res>, 57 | mut commands: bevy::prelude::Commands, 58 | ) { 59 | if let Some(dialogues) = dialogues.get(&state.handle) { 60 | if !state.done { 61 | commands.spawn(DialogueRunner::new(dialogues.clone(), "Start")); 62 | state.done = true; 63 | } 64 | } 65 | } 66 | 67 | fn dialogue_navigation(keys: Res>, mut runners: Query<&mut DialogueRunner>) { 68 | if let Ok(mut runner) = runners.get_single_mut() { 69 | if keys.just_pressed(KeyCode::Return) { 70 | runner.next_entry(); 71 | } 72 | if keys.just_pressed(KeyCode::Down) { 73 | println!("next choice"); 74 | runner.next_choice() 75 | } 76 | if keys.just_pressed(KeyCode::Up) { 77 | println!("prev choice"); 78 | runner.prev_choice() 79 | } 80 | } 81 | } 82 | 83 | fn dialogue_display(runners: Query<&DialogueRunner>, mut text: Query<&mut Text>) { 84 | let mut text = text.single_mut(); 85 | let text = &mut text.sections[0].value; 86 | *text = "".to_string(); 87 | text.push_str("------------------------------\n"); 88 | 89 | if let Ok(runner) = runners.get_single() { 90 | match runner.current_statement() { 91 | Statements::Dialogue(dialogue) => { 92 | text.push_str(&format!("{:?}: {:?}\n", dialogue.who, dialogue.what)); 93 | } 94 | Statements::Choice(_) => { 95 | let (choices, current_choice_index) = runner.get_current_choices(); 96 | for (index, dialogue) in choices.iter().enumerate() { 97 | if index == current_choice_index { 98 | text.push_str(&format!("--> {:?}: {:?}\n", dialogue.who, dialogue.what)); 99 | } else { 100 | text.push_str(&format!("{:?}: {:?}\n", dialogue.who, dialogue.what)); 101 | } 102 | } 103 | } 104 | Statements::Exit => { 105 | text.push_str("end of the dialogue! (Exit)"); 106 | } 107 | _ => {} 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | 2 | ## Initial release 3 | 4 | - [x] add plugin 5 | possibly plugins ? (ie to split parsing from runner ) 6 | - [x] rename parse_yarn_nodes_nom to something more adapted & nicer 7 | - [x] for choices, perhaps return the current choice index together with the list of choices from get_current_choices 8 | - [x] error handling if start node name is not found 9 | - [x] fix issue with nested choices in runner 10 | - [ ] add examples 11 | - [x] two characters (player + 1) 12 | - very simple display/ui 13 | - [x] rpg style changing portraits 14 | - [x] speech bubbles, multiple characters (3d) 15 | 16 | - [x] first two examples should use camera2D 17 | - [x] examples should provide at least some command use 18 | - [x] cleanup warnings 19 | - [ ] add basic useage docs 20 | - [ ] add captures of the examples 21 | - [ ] remove use as resource examples in docstrings for now 22 | 23 | ## General 24 | 25 | - [x] default to first node in yarn file if no start node is specified ? 26 | => bad idea, explicit errors are better 27 | - [ ] accessibility features how to? screen readers etc 28 | - [x] basic nodes parsing (header + body) 29 | - [ ] details 30 | - [x] dialogues: with or without character names 31 | - [ ] dialogues: interpolated values 32 | - [ ] dialogues: attributes 33 | - [ ] dialogues: Escaping Text 34 | 35 | - [x] choices: blank line to close a list of choices 36 | - [x] choices: nested/ indentation handling 37 | - [x] commands: basic parsing 38 | 39 | - [ ] conditional expressions 40 | - [ ] inject tags into statements (might require bigger changes given the current approach) 41 | - [x] fix empties being pushed into statements list 42 | - [x] fix choice arrow still present in the first Statement in a choice 43 | - [x] tags parsing 44 | - [ ] expressions parsing 45 | - [x] add testing 46 | - [x] yarn files with title & some basic dialogues 47 | - [x] yarn files with title & multiple nested levels of choices 48 | 49 | - [x] fix overly sensitive indentation issues 50 | - [x] we might need to track choice indentation seperatly from Dialogue/Command indentation 51 | ie this works : 52 | -> Lamik: everything! 53 | <> 54 | but this does not 55 | -> Lamik: everything! 56 | <> // notice the space at the start of the line: that changes the "current_indentation" in our tracking, messing things up 57 | 58 | - [ ] evaluator: 59 | - [x] fix issue with reaching end of a branch and not jumping back to previous level/root level 60 | - [x] remove need to pass yarn_asset to every next_entry call 61 | - [x] add ability to specify a specific choice directly in addition of previous/next choices 62 | - [ ] rename next_entry() to next() ? 63 | - [ ] next_entry() should return an Option perhaps ? (closer to an iterator, error handling etc) 64 | - [ ] fix command parsing : should dotted parameters be allowed? ie "file.ogg" for example 65 | - [ ] add support for multiple yarn files in a managed way : see https://docs.yarnspinner.dev/using-yarnspinner-with-unity/importing-yarn-files/yarn-projects 66 | - [ ] change "Commands" to YarnCommands, as it otherwise clashes with bevy's Commands 67 | - [x] bevy 0.10.1 support 68 | - [ ] bevy 0.11 support on bevy_main branch (low priority, only if time allows) 69 | - [ ] does the api of the DialogueRunner make sense ? 70 | - [ ] ironically as I never do oop , it seems to have some code smell 71 | - [ ] its api seems off for a component ? should it be an asset ? not quite as there can be a few different runners active ? and specific to entities ? hmm not quite as a runner actually runs dialogues for MULTIPLE characters 72 | - [ ] there are some parts of Bevy (namely audio), with ASSETS with api (audio.play) 73 | - [ ] should the api be part of the YarnAsset ?? 74 | -> not really, take a look at bevy_kira_audio , the loaded assets are seperate 75 | https://github.com/NiklasEi/bevy_kira_audio/tree/main/src 76 | -> loaders load an AudioSource , just a struct with TypeUuid like our yarn_asset https://github.com/NiklasEi/bevy_kira_audio/blob/main/src/source/mp3_loader.rs https://github.com/NiklasEi/bevy_kira_audio/blob/main/src/source/mod.rs#L20 77 | -> then there are audiochannels (a RESOURCE with an api) 78 | https://github.com/NiklasEi/bevy_kira_audio/blob/main/src/channel/typed.rs#L20 79 | 80 | 81 | 82 | - [ ] add testing 83 | - [ ] add examples 84 | - [ ] add docs -------------------------------------------------------------------------------- /src/parser/choices_alt.rs: -------------------------------------------------------------------------------- 1 | use nom::{ 2 | character::complete::{alphanumeric1, newline, space0, not_line_ending}, 3 | multi::{separated_list1, many_till}, 4 | sequence::{pair, preceded, terminated, tuple}, 5 | IResult, 6 | combinator:: {recognize, eof}, 7 | branch:: {alt}, 8 | bytes::complete::{tag, take_until} 9 | }; 10 | use nom::combinator::opt; 11 | 12 | use super::{identifier, spacey, statement_dialogue, statement_command}; 13 | use nom_supreme::error::ErrorTree; 14 | 15 | #[derive(Debug)] 16 | pub struct Branch2 { 17 | pub lines: Vec 18 | } 19 | 20 | #[derive(Debug)] 21 | pub struct Choice { 22 | pub branches: Vec 23 | } 24 | 25 | 26 | fn seperator_empty_lines(input: &str) -> IResult<&str, (char, &str)> { 27 | pair(newline, empty_line)(input) 28 | } 29 | 30 | fn seperator_other(input: &str) -> IResult<&str, (char, &str)> { 31 | pair(newline, tag(":"))(input) 32 | } 33 | 34 | 35 | fn empty_line(input: &str) -> IResult<&str, &str> { 36 | recognize( 37 | many_till(space0, alt(( tag("\n"), eof )) ) 38 | )(input) 39 | } 40 | 41 | 42 | fn line_w(input: &str) -> IResult<&str, Statements> { 43 | let (input, what) = spacey(not_line_ending)(input)?; 44 | println!("TUTU {:?} INPUT {:?}", what, input); 45 | let result = Statements::Dialogue(Dialogue { who: "nobody".to_string(), what: what.to_string(), ..Default::default() }); 46 | Ok((input, result)) 47 | } 48 | 49 | fn line_ww(input: &str) -> IResult<&str, Statements> { 50 | let (input, what) = tuple( (spacey(identifier) , spacey(tag(":")), not_line_ending ))(input)?; 51 | let (who, _, what) = what; 52 | println!("TOTO {:?} INPUT {:?}", what, input); 53 | 54 | let result = Statements::Dialogue(Dialogue { who: who.to_string(), what: what.to_string(), ..Default::default() }); 55 | Ok((input, result)) 56 | } 57 | 58 | fn line_test(input: &str) -> IResult<&str, Statements> { 59 | 60 | let (input, result) = alt(( 61 | line_ww, 62 | line_w, 63 | //not_line_ending, 64 | )) 65 | (input)?; 66 | 67 | 68 | Ok((input, result)) 69 | } 70 | 71 | fn foobazbar0(input: &str) -> IResult<&str, Statements> { 72 | let (input, what) = spacey(identifier)(input)?; 73 | 74 | let result = Statements::Dialogue(Dialogue { who: "nobody".to_string(), what: what.to_string(), ..Default::default() }); 75 | 76 | Ok((input, result)) 77 | } 78 | 79 | 80 | /// TODO : this would actually be the generic line parser 81 | fn line_inner(input: &str) -> IResult<&str, Statements> { 82 | 83 | let (input, what) = alt(( 84 | statement_command, 85 | line_ww, 86 | // line_w, 87 | foobazbar0, 88 | // spacey(line_w) 89 | )) 90 | (input)?; // this could be a good escape hatch 91 | // println!("taking {:?}", what); 92 | 93 | // let (input, what) = not_line_ending(input)?; // this could be a good escape hatch 94 | 95 | // let (rest, ww) = line_test(what)?; 96 | //tuple(( spacey(identifier), spacey(tag(":")) ))(what)?; 97 | // println!("BLA BLA REST: {} {:?}", rest, bla); 98 | //spacey(identifier)(input)?; 99 | Ok(( 100 | input, 101 | what 102 | // Statements::Dialogue(Dialogue { who: "who".to_string(), what: what.to_string() }) 103 | )) 104 | /*alt(( 105 | // statement_command, 106 | spacey(statement_dialogue), 107 | // spacey(identifier) 108 | ))(input)*/ 109 | } 110 | 111 | fn branch_content(input: &str) -> IResult<&str, Branch2> { 112 | //println!("CONTENT: {:?}", input); 113 | let(input, _) = opt(spacey(tag("->")))(input) ?; 114 | let(rest, lines) = separated_list1( 115 | newline, 116 | line_inner 117 | )(input)?; 118 | //println!("branch_content {:?} {:?}", rest, bla); 119 | 120 | let branch = Branch2 {lines }; 121 | 122 | Ok((rest, branch)) 123 | } 124 | 125 | /// parses out blocks containing multiple choices 126 | fn choices_block(input: &str) -> IResult<&str, Choice, > { 127 | let (input, branches) = 128 | separated_list1( 129 | newline, 130 | branch_content 131 | )(input) ?; 132 | Ok((input, Choice{branches})) 133 | } 134 | 135 | 136 | ///parses blocks containing multiple choices, seperate by empty lines 137 | /// TODO: this not QUITE right, as we would need to only use this when starting with a choice thingy '->' 138 | fn root_blocks(input: &str) -> IResult<&str, Vec> { 139 | separated_list1( 140 | seperator_empty_lines, choices_block // alt((seperator_empty_lines, seperator_other)) 141 | )(input) 142 | } 143 | 144 | 145 | pub fn parse_all_yarn(input: &str) -> IResult<&str, Vec> { 146 | root_blocks(input) 147 | } 148 | -------------------------------------------------------------------------------- /src/parser/body_backup.rs: -------------------------------------------------------------------------------- 1 | pub fn body(input: &str) -> IResult<&str, Branch> { 2 | let (input, lines) = statement_base(input)?; // TODO: use nom's map 3 | 4 | let mut current_branches: Vec = vec![]; // stores the data for the current choice node , if any 5 | let mut current_branch : Branch = Branch { statements: vec![], ..Default::default() }; // this is the root branch after the end of the parsing 6 | let mut stack: Vec = vec![]; 7 | 8 | // todo: create a choice when no choice branch is active, append all branches from ChoiceBranches 9 | // remember choice "groups" are delimited by : 10 | // - empty white line 11 | // - a different indentation 12 | 13 | // FIXME/ hack, only works for a single level: TODO: use the stack len to determine how deep we are ! 14 | let mut is_in_choice = false; 15 | let mut previous_indentation:usize = 0; 16 | for (line, tags, indentation) in lines.iter() { 17 | // the order of these is important !! 18 | let (_, statement) = alt(( 19 | statement_empty_line, 20 | statement_command, 21 | statement_choice, 22 | statement_dialogue_who_what, 23 | statement_dialogue_what, 24 | ) 25 | )(line)?; 26 | let tags: Vec = tags.clone().iter().map(|x|x.to_string()).collect(); 27 | 28 | println!("statement {:?}, tags: {:?}" ,statement.clone(), tags); 29 | match statement.clone(){ 30 | Statements::ChoiceBranch(branch) => { 31 | // println!("we have a new branch ! {:?}", branch); 32 | // IF non nested, the branch is on the same level as previous branches 33 | if is_in_choice { 34 | //println!("already in branch STACK: {:?}", stack); 35 | // push the previous choice branch to the list of branches in the choice 36 | current_branches.push(current_branch.clone()); 37 | if stack.len() > 0 { 38 | current_branch = stack.pop().unwrap(); 39 | } 40 | }else { 41 | //println!("not in choice before this"); 42 | is_in_choice = true; 43 | } 44 | 45 | //println!("current before {:?} STACK: {:?}", current_branch, stack); 46 | stack.push(current_branch); 47 | current_branch = branch; 48 | //println!("current after {:?} STACK: {:?}", current_branch, stack); 49 | println!("nesting level {}", stack.len()); 50 | 51 | } 52 | Statements::Empty => { 53 | 54 | // IF we had an open CHOICE still gathering branches, add a choice with all current branches 55 | if is_in_choice { 56 | current_branches.push(current_branch.clone()); 57 | // println!("gathering branches into a choice {:?}", current_branches); 58 | } 59 | is_in_choice = false; 60 | 61 | 62 | //If we have an empty line, OR if the IDENTATION IS LESS pop back to the previous level branch 63 | if stack.len() > 0 { 64 | current_branch = stack.pop().unwrap(); 65 | //println!("going back to {:?} ", current_branch); 66 | 67 | if current_branches.len() > 0 { 68 | current_branch.statements.push( // need to be pushed to the parent branch, so that is why we pop() first 69 | Statements::Choice(crate::Choice { branches: current_branches.clone() , ..Default::default()} ) 70 | ); 71 | } 72 | } 73 | println!("nesting level {}", stack.len()); 74 | 75 | current_branches = vec![]; 76 | } 77 | _=> { 78 | /* if indentation < previousIndentation { 79 | // TODO: end choice gathering if any 80 | }*/ 81 | // we push everything else to the current branch 82 | current_branch.statements.push(statement); 83 | println!("nesting level {}", stack.len()); 84 | } 85 | } 86 | previous_indentation = indentation.clone(); 87 | } 88 | // push the last branch 89 | // current_branches.push(current_branch); 90 | // println!("stack, {:?}", stack); 91 | // here current_branch should be the root branch 92 | // display_dialogue_tree(¤t_branch, 0); 93 | 94 | /* 95 | for branch in current_branches.iter(){ 96 | println!("branch: "); 97 | for statement in branch.statements.iter(){ 98 | println!(" {:?}", statement); 99 | } 100 | } */ 101 | 102 | 103 | Ok((input, current_branch)) 104 | } 105 | -------------------------------------------------------------------------------- /examples/basic/commands.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_mod_yarn::prelude::*; 3 | 4 | fn main() { 5 | App::new() 6 | .add_plugins((DefaultPlugins, YarnPlugin)) 7 | .init_resource::() 8 | .add_systems(Startup, setup) 9 | .add_systems( 10 | Update, 11 | ( 12 | dialogue_init, 13 | dialogue_navigation, 14 | dialogue_display, 15 | dialogue_commands, 16 | ), 17 | ) 18 | .run(); 19 | } 20 | 21 | #[derive(Resource, Default)] 22 | struct State { 23 | handle: Handle, 24 | done: bool, 25 | } 26 | 27 | fn setup( 28 | mut state: ResMut, 29 | mut commands: bevy::prelude::Commands, 30 | asset_server: Res, 31 | ) { 32 | // load the yarn dialogue file 33 | state.handle = asset_server.load("dialogues/single_node_simple_commands.yarn"); 34 | 35 | // setup a simple 2d camera 36 | commands.spawn(Camera2dBundle { 37 | transform: Transform::from_xyz(0.0, 5.0, 0.0), 38 | ..default() 39 | }); 40 | 41 | commands.spawn( 42 | TextBundle::from_section( 43 | "", 44 | TextStyle { 45 | font: asset_server.load("fonts/FiraMono-Medium.ttf"), 46 | font_size: 18.0, 47 | color: Color::WHITE, 48 | }, 49 | ) 50 | .with_style(Style { 51 | position_type: PositionType::Absolute, 52 | top: Val::Px(10.0), 53 | left: Val::Px(10.0), 54 | ..default() 55 | }), 56 | ); 57 | } 58 | 59 | fn dialogue_init( 60 | mut state: ResMut, 61 | dialogues: Res>, 62 | mut commands: bevy::prelude::Commands, 63 | ) { 64 | if let Some(dialogues) = dialogues.get(&state.handle) { 65 | if !state.done { 66 | commands.spawn(DialogueRunner::new(dialogues.clone(), "Start")); 67 | state.done = true; 68 | } 69 | } 70 | } 71 | 72 | fn dialogue_navigation(keys: Res>, mut runners: Query<&mut DialogueRunner>) { 73 | if let Ok(mut runner) = runners.get_single_mut() { 74 | if keys.just_pressed(KeyCode::Return) { 75 | runner.next_entry(); 76 | } 77 | if keys.just_pressed(KeyCode::Down) { 78 | println!("next choice"); 79 | runner.next_choice() 80 | } 81 | if keys.just_pressed(KeyCode::Up) { 82 | println!("prev choice"); 83 | runner.prev_choice() 84 | } 85 | } 86 | } 87 | 88 | fn dialogue_display(runners: Query<&DialogueRunner>, mut text: Query<&mut Text>) { 89 | let mut text = text.single_mut(); 90 | let text = &mut text.sections[0].value; 91 | *text = "".to_string(); 92 | text.push_str("------------------------------\n"); 93 | 94 | if let Ok(runner) = runners.get_single() { 95 | match runner.current_statement() { 96 | Statements::Dialogue(dialogue) => { 97 | text.push_str(&format!("{:?}: {:?}\n", dialogue.who, dialogue.what)); 98 | } 99 | Statements::Choice(_) => { 100 | let (choices, current_choice_index) = runner.get_current_choices(); 101 | for (index, dialogue) in choices.iter().enumerate() { 102 | if index == current_choice_index { 103 | text.push_str(&format!("--> {:?}: {:?}\n", dialogue.who, dialogue.what)); 104 | } else { 105 | text.push_str(&format!("{:?}: {:?}\n", dialogue.who, dialogue.what)); 106 | } 107 | } 108 | } 109 | Statements::Exit => { 110 | text.push_str("end of the dialogue! (Exit)"); 111 | } 112 | _ => {} 113 | } 114 | } 115 | } 116 | 117 | /// Marker component for our music entity 118 | #[derive(Component)] 119 | struct AudioTag; 120 | 121 | fn dialogue_commands( 122 | mut runners: Query<&mut DialogueRunner>, 123 | asset_server: Res, 124 | mut commands: bevy::prelude::Commands, 125 | ) { 126 | if let Ok(mut runner) = runners.get_single_mut() { 127 | match runner.current_statement() { 128 | Statements::Command(command) => { 129 | println!("running command {:?}", command); 130 | match command.name.as_str() { 131 | "play_audio" => { 132 | let audio_path = format!("sounds/{}.ogg", command.params); 133 | // println!("audio {}", audio_path); 134 | //let music = asset_server.load(audio_path); 135 | // audio.play(music); 136 | 137 | commands.spawn(( 138 | AudioBundle { 139 | source: asset_server.load(audio_path), 140 | settings: PlaybackSettings::DESPAWN, 141 | }, 142 | AudioTag, 143 | )); 144 | } 145 | _ => {} 146 | } 147 | 148 | runner.next_entry(); 149 | } 150 | _ => { 151 | // println!("other stuff") 152 | } 153 | } 154 | } 155 | 156 | // 157 | // 158 | } 159 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | Bevy_mod_yarn 4 |

5 | 6 | 7 |
8 | 9 | Parser + interpreter/runner for the [YarnSpinner](https://github.com/YarnSpinnerTool/YarnSpinner) dialogue file format for the [Bevy Engine](https://github.com/bevyengine/bevy) 10 | It allows you to create branching narrative / dialogues for games , ie , Rpgs, Visual novels, adventure games, etc in Bevy ! 11 | 12 | 13 | This project is still in the early stages, but it is already usable as it is for some basic Yarn scripts. 14 | 15 | Since I am using it myself and will be relying on it heavilly for some of my projects (yeah for dogfooding :) ), 16 | I am aiming to be able to parse & support as much of the Yarn Syntax as possible. 17 | 18 | ## Usage 19 | 20 | Here's a minimal usage example: 21 | 22 | ```toml 23 | # Cargo.toml 24 | [dependencies] 25 | bevy_mod_yarn = { git = "https://github.com/kaosat-dev/bevy_mod_yarn", branch = "main" } 26 | ``` 27 | 28 | ```rust no_run 29 | use bevy::prelude::*; 30 | use bevy_mod_yarn::prelude::*; 31 | 32 | fn main() { 33 | App::new() 34 | .add_plugins(DefaultPlugins) 35 | .add_plugin(YarnPlugin) 36 | .init_resource::() // only needed for manual loading 37 | 38 | .add_startup_system(setup) 39 | .add_system(dialogue_init) 40 | .add_system(dialogue_navigation) 41 | .run(); 42 | } 43 | 44 | // only needed for manual loading, not when using tools like [bevy_asset_loader](https://github.com/NiklasEi/bevy_asset_loader) 45 | #[derive(Resource, Default)] 46 | struct State { 47 | handle: Handle, 48 | done: bool 49 | } 50 | 51 | fn setup( 52 | mut state: ResMut, 53 | asset_server: Res, 54 | mut commands: bevy::prelude::Commands 55 | ) { 56 | // load the yarn dialogue file 57 | state.handle = asset_server.load("dialogues/single_node_simple.yarn"); 58 | } 59 | 60 | fn dialogue_init(mut state: ResMut, dialogues: Res>, mut commands: bevy::prelude::Commands) { 61 | if let Some(dialogues)= dialogues.get(&state.handle) { 62 | if !state.done { 63 | commands.spawn( 64 | DialogueRunner::new(dialogues.clone(), "Start") 65 | ); 66 | state.done = true; 67 | } 68 | } 69 | } 70 | 71 | fn dialogue_navigation( 72 | keys: Res>, 73 | mut runners: Query<&mut DialogueRunner>, 74 | ) { 75 | if let Ok(mut runner) = runners.get_single_mut() { 76 | if keys.just_pressed(KeyCode::Return) { 77 | runner.next_entry(); 78 | } 79 | if keys.just_pressed(KeyCode::Down) { 80 | runner.next_choice() 81 | } 82 | if keys.just_pressed(KeyCode::Up) { 83 | runner.prev_choice() 84 | } 85 | } 86 | } 87 | 88 | ``` 89 | this is taken from the 'basic' [example](./examples//basic/basic.rs) 90 | 91 | see the examples below for more details , how to display your dialogues etc 92 | 93 | ## Examples 94 | 95 | This crate provides different examples for different features/ways to use within Bevy 96 | 97 | ### [Basic](./examples/basic) 98 | 99 | - simplest possible usage 100 | 101 | ![demo](./examples/basic/basic.png) 102 | 103 | run it with 104 | 105 | ```cargo run --example basic``` 106 | 107 | ### [Commands](./examples/commands) 108 | 109 | - using `Yarn` commands with Bevy systems to play audio files during the dialogue flow 110 | 111 | ![demo](./examples/basic/commands.png) 112 | 113 | run it with 114 | 115 | ```cargo run --example commands``` 116 | 117 | ### [Portraits](./examples/portraits) 118 | 119 | - a barebones "old school rpg dialogue with Character portraits" ie changing character portraits based on who is talking in your dialogue 120 | 121 | ![demo](./examples/portraits/portraits.gif) 122 | 123 | run it with 124 | 125 | ```cargo run --example portraits``` 126 | 127 | ### [Bubbles](./examples/bubbles) 128 | 129 | - a barebones "speech bubbles" (ok, just text, but still :) over characters heads in 3D 130 | 131 | ![demo](./examples/bubbles/bubbles.gif) 132 | 133 | run it with 134 | 135 | ```cargo run --example bubbles``` 136 | 137 | ## Development status 138 | 139 | - [x] basic nodes parsing (header + body) 140 | - [x] dialogues: with or without character names 141 | - [x] choices/ options: blank line to close a list of choices 142 | - [x] choices/ options: nested/ indentation handling 143 | - [x] commands: basic parsing & handling 144 | - [x] tags parsing 145 | - [ ] tags available inside statements 146 | - [ ] expressions parsing 147 | - [ ] conditional expressions 148 | - [ ] dialogues: conditional branching with expressions 149 | - [ ] dialogues: interpolated values 150 | - [ ] dialogues: attributes 151 | 152 | 153 | I will put it on crates.io once I feel it is useable enough. 154 | 155 | ## What this tool does: 156 | 157 | - provide a [parser](./src/parser/) (written with Nom). Not specific to Bevy, will likely be extracted into its own Crate down the line. 158 | - provide an [asset loader](./src/yarn_loader.rs) for Bevy 159 | - provide a [plugin](./src/lib.rs) for Bevy 160 | 161 | - some additional data structures and functions to deal with the Yarn Format inside bevy, in a minimalistic manner 162 | 163 | ## What this tool does not: 164 | 165 | - provide complex UI or predefined ways to interact with the dialogues inside Bevy, for a few reasons 166 | * Bevy's UI is still constantly evolving 167 | * Other UI tools for Bevy are very promising, like [Belly](https://github.com/jkb0o/belly), [Kayak](https://github.com/StarArawn/kayak_ui) or [Egui](https://github.com/mvlabat/bevy_egui), they are not "standard" however 168 | * everyone has their preferences 169 | - you will find some varied examples for use with Bevy to get you started 170 | 171 | ## License 172 | 173 | Dual-licensed under either of 174 | 175 | - Apache License, Version 2.0, ([LICENSE-APACHE](/LICENSE_APACHE) or https://www.apache.org/licenses/LICENSE-2.0) 176 | - MIT license ([LICENSE-MIT](/LICENSE_MIT) or https://opensource.org/licenses/MIT) 177 | 178 | at your option. 179 | 180 | 181 | ## Compatible Bevy versions 182 | 183 | The main branch is compatible with the latest Bevy release (0.10.1), while the branch `bevy_main` tries to track the `main` branch of Bevy (not started yet, PRs updating the tracked commit are welcome). 184 | 185 | Compatibility of `bevy_mod_yarn` versions: 186 | | `bevy_mod_yarn` | `bevy` | 187 | | :-- | :-- | 188 | | `0.3` | `0.12` | 189 | | `0.2` | `0.11` | 190 | | `0.1` | `0.10` | 191 | | `main` | `latest` | 192 | | `bevy_main` | `main` | 193 | 194 | 195 | ## Contribution 196 | 197 | Unless you explicitly state otherwise, any contribution intentionally submitted 198 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any 199 | additional terms or conditions. 200 | 201 | 202 | 203 | [bevy]: https://bevyengine.org/ -------------------------------------------------------------------------------- /examples/portraits/portraits.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_mod_yarn::prelude::*; 3 | 4 | fn main() { 5 | App::new() 6 | .add_plugins(DefaultPlugins.set(WindowPlugin { 7 | primary_window: Some(Window { 8 | resolution: (800., 600.).into(), 9 | ..default() 10 | }), 11 | ..default() 12 | })) 13 | .add_plugins(YarnPlugin) 14 | .init_resource::() 15 | .add_systems(Startup, setup) 16 | .add_systems( 17 | Update, 18 | (dialogue_init, dialogue_navigation, dialogue_display), 19 | ) 20 | .run(); 21 | } 22 | 23 | #[derive(Resource, Default)] 24 | struct State { 25 | handle: Handle, 26 | done: bool, 27 | } 28 | 29 | #[derive(Component)] 30 | 31 | struct CharacterName(String); 32 | 33 | #[derive(Component)] 34 | 35 | struct CharacterPortraitPath(String); 36 | 37 | #[derive(Component)] 38 | 39 | struct CharacterPortrait(Handle); 40 | 41 | // marker components 42 | #[derive(Component)] 43 | struct DialogueTextMarker; 44 | 45 | #[derive(Component)] 46 | struct DialogueNameMarker; 47 | 48 | fn setup( 49 | mut state: ResMut, 50 | asset_server: Res, 51 | mut commands: bevy::prelude::Commands, 52 | ) { 53 | // load the yarn file 54 | state.handle = asset_server.load("dialogues/two_nodes_jump_nested_choices.yarn"); 55 | 56 | // setup a simple 2d camera 57 | commands.spawn(Camera2dBundle { 58 | transform: Transform::from_xyz(0.0, 5.0, 0.0), 59 | ..default() 60 | }); 61 | 62 | // for character dialogue text 63 | commands.spawn(( 64 | TextBundle::from_section( 65 | "", 66 | TextStyle { 67 | font: asset_server.load("fonts/FiraMono-Medium.ttf"), 68 | font_size: 18.0, 69 | color: Color::WHITE, 70 | }, 71 | ) 72 | .with_style(Style { 73 | position_type: PositionType::Absolute, 74 | bottom: Val::Px(30.0), 75 | left: Val::Px(80.0), 76 | ..default() 77 | }), 78 | DialogueTextMarker, 79 | )); 80 | 81 | // for character names 82 | commands.spawn(( 83 | TextBundle::from_section( 84 | "", 85 | TextStyle { 86 | font: asset_server.load("fonts/FiraMono-Medium.ttf"), 87 | font_size: 18.0, 88 | color: Color::GOLD, 89 | }, 90 | ) 91 | .with_style(Style { 92 | position_type: PositionType::Absolute, 93 | bottom: Val::Px(60.0), 94 | left: Val::Px(80.0), 95 | ..default() 96 | }), 97 | DialogueNameMarker, 98 | )); 99 | 100 | commands 101 | .spawn(NodeBundle { 102 | style: Style { 103 | position_type: PositionType::Absolute, 104 | bottom: Val::Px(10.0), 105 | left: Val::Px(10.0), 106 | ..default() 107 | }, 108 | ..default() 109 | }) 110 | .with_children(|parent| { 111 | // bevy logo (image) 112 | parent 113 | .spawn(ImageBundle { 114 | style: Style { 115 | min_width: Val::Px(64.), 116 | min_height: Val::Px(64.), 117 | ..default() 118 | }, 119 | image: asset_server.load("textures/portrait0.png").into(), 120 | ..default() 121 | }) 122 | .with_children(|parent| { 123 | // alt text 124 | parent.spawn(TextBundle::from_section("portraits", TextStyle::default())); 125 | }); 126 | }); 127 | 128 | // spawn our characters 129 | commands.spawn(( 130 | CharacterName("Lamik".to_string()), 131 | CharacterPortraitPath("textures/portrait1.png".to_string()), 132 | CharacterPortrait(asset_server.load("textures/portrait1.png").into()), 133 | )); 134 | 135 | commands.spawn(( 136 | CharacterName("Dona".to_string()), 137 | CharacterPortraitPath("textures/portrait2.png".to_string()), 138 | CharacterPortrait(asset_server.load("textures/portrait2.png").into()), 139 | )); 140 | } 141 | 142 | fn dialogue_init( 143 | mut state: ResMut, 144 | dialogues: Res>, 145 | mut commands: bevy::prelude::Commands, 146 | ) { 147 | if let Some(dialogues) = dialogues.get(&state.handle) { 148 | if !state.done { 149 | commands.spawn(DialogueRunner::new(dialogues.clone(), "Test_node")); 150 | state.done = true; 151 | } 152 | } 153 | } 154 | 155 | fn dialogue_navigation(keys: Res>, mut runners: Query<&mut DialogueRunner>) { 156 | if let Ok(mut runner) = runners.get_single_mut() { 157 | if keys.just_pressed(KeyCode::Return) { 158 | runner.next_entry(); 159 | } 160 | if keys.just_pressed(KeyCode::Down) { 161 | println!("next choice"); 162 | runner.next_choice() 163 | } 164 | if keys.just_pressed(KeyCode::Up) { 165 | println!("prev choice"); 166 | runner.prev_choice() 167 | } 168 | } 169 | } 170 | 171 | fn dialogue_display( 172 | runners: Query<&DialogueRunner>, 173 | mut name_display: Query<&mut Text, (With, Without)>, 174 | mut text: Query<&mut Text, (With, Without)>, 175 | mut portrait: Query<&mut UiImage>, 176 | characters: Query<(&CharacterName, &CharacterPortrait)>, 177 | ) { 178 | let mut text = text.single_mut(); 179 | let text = &mut text.sections[0].value; 180 | *text = "".to_string(); 181 | text.push_str("------------------------------\n"); 182 | 183 | let mut name_display = name_display.single_mut(); 184 | let name_display = &mut name_display.sections[0].value; 185 | *name_display = "".to_string(); 186 | 187 | let mut portrait = portrait.single_mut(); 188 | 189 | if let Ok(runner) = runners.get_single() { 190 | match runner.current_statement() { 191 | Statements::Dialogue(dialogue) => { 192 | // println!("{:?}: {:?}", dialogue.who, dialogue.what); 193 | text.push_str(&format!("{}\n", dialogue.what)); 194 | 195 | // FIXME: very inneficient, but does the job, perhaps switch to for each to break out early 196 | for (name, portrait_img) in characters.iter() { 197 | if name.0 == dialogue.who { 198 | portrait.texture = portrait_img.0.clone(); 199 | name_display.push_str(&name.0); 200 | } 201 | } 202 | } 203 | Statements::Choice(_) => { 204 | let (choices, current_choice_index) = runner.get_current_choices(); 205 | for (index, dialogue) in choices.iter().enumerate() { 206 | *name_display = "".to_string(); 207 | 208 | if index == current_choice_index { 209 | // text.push_str(&format!("--> {:?}: {:?}\n", dialogue.who, dialogue.what)); 210 | text.push_str(&format!("--> {}\n", dialogue.what)); 211 | } else { 212 | // text.push_str(&format!("{:?}: {:?}\n", dialogue.who, dialogue.what)); 213 | text.push_str(&format!("{}\n", dialogue.what)); 214 | } 215 | 216 | // FIXME: very inneficient, but does the job, perhaps switch to for each to break out early 217 | for (name, portrait_img) in characters.iter() { 218 | if name.0 == dialogue.who { 219 | portrait.texture = portrait_img.0.clone(); 220 | name_display.push_str(&name.0); 221 | } 222 | } 223 | } 224 | } 225 | Statements::Exit => { 226 | text.push_str("end of the dialogue! (Exit)"); 227 | } 228 | _ => {} 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /assets/textures/portraits.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 42 | 51 | 56 | 62 | 71 | 76 | 82 | 86 | 90 | 94 | 98 | 102 | 106 | 110 | 114 | 118 | 122 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /examples/bubbles/bubbles.rs: -------------------------------------------------------------------------------- 1 | use bevy::{prelude::*, render::primitives::Aabb}; 2 | use bevy_mod_yarn::prelude::*; 3 | 4 | fn main() { 5 | App::new() 6 | .add_plugins(DefaultPlugins.set(WindowPlugin { 7 | primary_window: Some(Window { 8 | resolution: (1024., 600.).into(), 9 | ..default() 10 | }), 11 | ..default() 12 | })) 13 | .add_plugins(YarnPlugin) 14 | .init_resource::() 15 | .add_systems(Startup, setup) 16 | .add_systems( 17 | Update, 18 | (dialogue_init, dialogue_navigation, dialogue_display), 19 | ) 20 | .run(); 21 | } 22 | 23 | #[derive(Resource, Default)] 24 | struct State { 25 | handle: Handle, 26 | done: bool, 27 | } 28 | 29 | #[derive(Component)] 30 | 31 | struct CharacterName(String); 32 | 33 | // marker components 34 | #[derive(Component)] 35 | struct DialogueTextMarker; 36 | #[derive(Component)] 37 | struct DialogueNameMarker; 38 | 39 | fn setup( 40 | mut state: ResMut, 41 | asset_server: Res, 42 | mut commands: bevy::prelude::Commands, 43 | 44 | mut meshes: ResMut>, 45 | mut materials: ResMut>, 46 | ) { 47 | // load the yarn file 48 | state.handle = asset_server.load("dialogues/single_node_three_characters.yarn"); 49 | 50 | // bevy boilerplate 51 | commands.spawn(Camera3dBundle { 52 | transform: Transform::from_xyz(6.0, 12.0, 6.0) 53 | .looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y), 54 | ..default() 55 | }); 56 | commands.spawn(PointLightBundle { 57 | point_light: PointLight { 58 | intensity: 122000.0, 59 | range: 150., 60 | shadows_enabled: true, 61 | color: Color::ORANGE, 62 | ..default() 63 | }, 64 | transform: Transform::from_xyz(8.0, 16.0, 18.0), 65 | ..default() 66 | }); 67 | 68 | // ground plane 69 | commands.spawn(PbrBundle { 70 | mesh: meshes.add(shape::Plane::from_size(50.0).into()), 71 | material: materials.add(Color::SILVER.into()), 72 | ..default() 73 | }); 74 | 75 | // for character dialogue text 76 | commands.spawn(( 77 | TextBundle::from_section( 78 | "", 79 | TextStyle { 80 | font: asset_server.load("fonts/FiraMono-Medium.ttf"), 81 | font_size: 18.0, 82 | color: Color::WHITE, 83 | }, 84 | ) 85 | .with_style(Style { 86 | position_type: PositionType::Absolute, 87 | bottom: Val::Px(30.0), 88 | left: Val::Px(80.0), 89 | max_width: Val::Px(300.0), 90 | max_height: Val::Px(40.0), 91 | ..default() 92 | }), 93 | DialogueTextMarker, 94 | )); 95 | 96 | // character stand ins in the 3d world 97 | commands.spawn(( 98 | CharacterName("Lamik".to_string()), 99 | PbrBundle { 100 | mesh: meshes.add(Mesh::from(shape::Capsule::default())), 101 | material: materials.add(Color::rgb(0.8, 0.1, 0.1).into()), 102 | transform: Transform::from_xyz(0.0, 1.0, 0.0), 103 | ..default() 104 | }, 105 | )); 106 | 107 | commands.spawn(( 108 | CharacterName("Dona".to_string()), 109 | PbrBundle { 110 | mesh: meshes.add(Mesh::from(shape::Capsule::default())), 111 | material: materials.add(Color::rgb(0.2, 0.8, 0.1).into()), 112 | transform: Transform::from_xyz(3.0, 1.0, 0.0), 113 | ..default() 114 | }, 115 | )); 116 | 117 | commands.spawn(( 118 | CharacterName("Blob".to_string()), 119 | PbrBundle { 120 | mesh: meshes.add(Mesh::from(shape::Capsule::default())), 121 | material: materials.add(Color::rgb(0.1, 0.3, 0.8).into()), 122 | transform: Transform::from_xyz(1.0, 1.0, 5.0), 123 | ..default() 124 | }, 125 | )); 126 | } 127 | 128 | fn dialogue_init( 129 | mut state: ResMut, 130 | dialogues: Res>, 131 | mut commands: bevy::prelude::Commands, 132 | ) { 133 | if let Some(dialogues) = dialogues.get(&state.handle) { 134 | if !state.done { 135 | commands.spawn(DialogueRunner::new(dialogues.clone(), "Start")); 136 | state.done = true; 137 | } 138 | } 139 | } 140 | 141 | fn dialogue_navigation(keys: Res>, mut runners: Query<&mut DialogueRunner>) { 142 | if let Ok(mut runner) = runners.get_single_mut() { 143 | if keys.just_pressed(KeyCode::Return) { 144 | runner.next_entry(); 145 | } 146 | if keys.just_pressed(KeyCode::Down) { 147 | println!("next choice"); 148 | runner.next_choice() 149 | } 150 | if keys.just_pressed(KeyCode::Up) { 151 | println!("prev choice"); 152 | runner.prev_choice() 153 | } 154 | } 155 | } 156 | 157 | fn dialogue_display( 158 | runners: Query<&DialogueRunner>, 159 | mut text: Query< 160 | (&mut Text, &mut Style, &Node), 161 | (With, Without), 162 | >, // &CalculatedSize 163 | characters: Query<(&CharacterName, &GlobalTransform, &Aabb)>, 164 | 165 | // for projection & placing bubble above characters 166 | windows: Query<&Window>, 167 | cameras: Query<(&Camera, &GlobalTransform)>, 168 | ) { 169 | let mut text_data = text.single_mut(); 170 | let text = &mut text_data.0.sections[0].value; 171 | *text = "".to_string(); 172 | // text.push_str("------------------------------\n"); 173 | 174 | let mut bubble_style = text_data.1; 175 | let bubble_size = text_data.2.size(); 176 | 177 | let window = windows.single(); 178 | 179 | let camera = cameras.single(); 180 | let (camera, camera_global_transform) = camera; 181 | 182 | if let Ok(runner) = runners.get_single() { 183 | match runner.current_statement() { 184 | Statements::Dialogue(dialogue) => { 185 | // println!("{:?}: {:?}", dialogue.who, dialogue.what); 186 | text.push_str(&format!("{}\n", dialogue.what)); 187 | 188 | // FIXME: very inneficient, but does the job, perhaps switch to for each to break out early 189 | for (name, global_transform, aabb) in characters.iter() { 190 | if name.0 == dialogue.who { 191 | // we want to position bubbles ABOVE meshes, sadly the builtin Aabb component does not seem to have correct data, 192 | place_bubble( 193 | aabb, 194 | global_transform, 195 | camera, 196 | camera_global_transform, 197 | window, 198 | &bubble_size, 199 | &mut bubble_style, 200 | ); 201 | } 202 | } 203 | } 204 | Statements::Choice(_) => { 205 | let (choices, current_choice_index) = runner.get_current_choices(); 206 | for (index, dialogue) in choices.iter().enumerate() { 207 | // *name_display = "".to_string(); 208 | 209 | if index == current_choice_index { 210 | // text.push_str(&format!("--> {:?}: {:?}\n", dialogue.who, dialogue.what)); 211 | text.push_str(&format!("--> {}\n", dialogue.what)); 212 | } else { 213 | // text.push_str(&format!("{:?}: {:?}\n", dialogue.who, dialogue.what)); 214 | text.push_str(&format!("{}\n", dialogue.what)); 215 | } 216 | 217 | // FIXME: very inneficient, but does the job, perhaps switch to for each to break out early 218 | for (name, global_transform, aabb) in characters.iter() { 219 | if name.0 == dialogue.who { 220 | place_bubble( 221 | aabb, 222 | global_transform, 223 | camera, 224 | camera_global_transform, 225 | window, 226 | &bubble_size, 227 | &mut bubble_style, 228 | ); 229 | // portrait.texture = portrait_img.0.clone(); 230 | // name_display.push_str(&name.0); 231 | } 232 | } 233 | } 234 | } 235 | Statements::Exit => { 236 | text.push_str(""); //"end of the node! (Exit)"); 237 | } 238 | _ => {} 239 | } 240 | } 241 | } 242 | 243 | fn place_bubble( 244 | aabb: &Aabb, 245 | global_transform: &GlobalTransform, 246 | camera: &Camera, 247 | camera_global_transform: &GlobalTransform, 248 | window: &Window, 249 | bubble_size: &Vec2, 250 | mut bubble_style: &mut Style, 251 | ) { 252 | let vertical_offset = aabb.half_extents.y; 253 | let mut offset_position = global_transform.translation().clone(); 254 | offset_position.y += vertical_offset; 255 | 256 | match camera.world_to_ndc(camera_global_transform, offset_position) { 257 | Some(coords) => { 258 | // println!("unprojected coords {}",coords); 259 | let width = bubble_size.x / 2.0; 260 | let height = bubble_size.y / 2.0; 261 | let mapped_x = coords.x * window.width() * 0.5; 262 | let mapped_y = coords.y * window.height() * 0.5; 263 | let mid_x = window.width() / 2.0 - width; 264 | let mid_y = window.height() / 2.0 - height; 265 | bubble_style.left = Val::Px(mid_x + mapped_x); 266 | bubble_style.top = Val::Px(mid_y - mapped_y - 50.); 267 | bubble_style.display = Display::Flex; // since it was hidden, display it again (avoids text position flicker) 268 | } 269 | None => {} 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/parser/old.rs: -------------------------------------------------------------------------------- 1 | 2 | fn namespaced_identifier(input: &str) -> IResult<&str, &str> { 3 | /*let foo = recognize( 4 | identifier, 5 | tag(":"), 6 | identifier 7 | )(input)?;*/ 8 | let (input, id) = identifier(input)?; 9 | // let (input, _) = tag(":")(input)?; 10 | // let (input, id2) = identifier(input)?; 11 | // let result = format!("{}:{}", id, id2); 12 | Ok((input, id)) 13 | } 14 | 15 | fn tag_bla (input: &str) -> IResult<&str, &str> { 16 | preceded(tag("#"),identifier)(input) 17 | } 18 | 19 | 20 | fn bliblibli (input: &str) -> IResult<&str, &str> { 21 | Ok((input, "")) 22 | } 23 | //let (input, data ) = separated_list1(tag("==="), anychar)(input)?; 24 | //Ok((input, data)) 25 | // input.split_at_position_complete(tag("===")) // char::is_whitespace 26 | // take_till(|c| c == "===")(input) 27 | // preceded(multispace0, take_until(tag(".rar"))) 28 | //let (input, node_raw) = take_until("===")(input)?; 29 | //let (input, second_raw) = take_until("===")(input)?; 30 | 31 | // let (input, out ) = separated_list1(tag("==="), anything)(input)?; 32 | //many0(take_until("===")) 33 | 34 | //let (input, node) = separated_list1(tag(node_seperator), line)(input)?; 35 | // separated_list1(newline(input), line)(input)?; 36 | 37 | fn foo () { 38 | let (input, tags) = opt( 39 | alt(( 40 | separated_list0(tag(" "), spacey(tag_identifier)), // either a whitespace seperate list 41 | many0(spacey(tag_identifier)), // or a single item 42 | )) 43 | )(input)?; 44 | 45 | let (input, tags) = many0(spacey(tag_identifier))(input)?; 46 | } 47 | 48 | 49 | pub fn inner(input: &str) -> IResult<&str, Vec<&str>> { 50 | 51 | let (input, res) = many_till( delimited( 52 | spacey(tag("(")), 53 | identifier, 54 | spacey(tag(")")) 55 | ), alt(( tag("\n"), eof )) )(input)?; 56 | 57 | Ok((input, res.0)) 58 | } 59 | 60 | 61 | fn nesting_test() { 62 | let test_input = " 63 | (aa)(fine) 64 | (dsf)(sdf)(sdfezae)(azezae) 65 | (other)"; 66 | let (input, bli) = many_till(inner, eof )(test_input) ?; 67 | } 68 | 69 | 70 | 71 | 72 | 73 | pub fn inner(input: &str) -> IResult<&str, Vec<&str>> { 74 | 75 | let (input, res) = many_till( delimited( 76 | spacey(tag("(")), 77 | identifier, 78 | spacey(tag(")")) 79 | ), alt(( tag("\n"), eof )) )(input)?; 80 | 81 | Ok((input, res.0)) 82 | } 83 | 84 | 85 | pub fn inner2(input: &str) -> IResult<&str, &str> { 86 | 87 | let (input, _bla) = spacey(tag("aaa"))(input)?; 88 | /*let (input, res) = many_till( delimited( 89 | spacey(tag("(")), 90 | identifier, 91 | spacey(tag(")")) 92 | ), empty_line )(input)?;*/ 93 | 94 | Ok((input, "")) 95 | } 96 | 97 | 98 | pub fn ggdgdfgdf(input: &str) -> IResult<&str, (&str, &str, &str, &str, &str)> { 99 | tuple(( spacey(tag("aaa")) ,tag("\n"), spacey(tag("aaa")),tag("\n"), empty_line2 ))(input) 100 | } 101 | 102 | pub fn six(input: &str) -> IResult<&str, (&str, &str, &str, &str, &str)> { 103 | tuple(( spacey(identifier) , alt(( tag("\n"), eof )), spacey(identifier), alt(( tag("\n"), eof )), alt(( empty_line2, eof )) ))(input) 104 | } 105 | 106 | pub fn seventh_inner(input: &str) -> IResult<&str, &str> { 107 | terminated(spacey(identifier), tag("\n")) (input) 108 | } 109 | 110 | pub fn seventh(input: &str) -> IResult<&str, (&str, &str)> { 111 | terminated( 112 | tuple(( seventh_inner ,tag("\n") )), 113 | alt(( empty_line2, eof )) 114 | )(input) 115 | // separated_list1(sep, f) 116 | //pair(first, second) 117 | } 118 | 119 | pub fn eighth_inner(input: &str) -> IResult<&str, (&str, &str)> { 120 | // terminated(spacey(identifier), tag("\n")) (input) 121 | tuple(( 122 | spacey(identifier) , 123 | alt(( tag("\n"), eof )) 124 | ))(input) 125 | } 126 | 127 | pub fn eigth(input: &str) -> IResult<&str, ((&str, &str), (&str, &str), &str)> { 128 | tuple(( 129 | eighth_inner, 130 | eighth_inner, // this does not work many0(eighth_inner), 131 | alt(( empty_line2, eof )) 132 | )) 133 | (input) 134 | } 135 | 136 | 137 | pub fn ninth(input: &str) -> IResult<&str,(Vec<(&str, &str)>, &str)> { 138 | many_till(eighth_inner, alt(( empty_line2, eof )))(input) 139 | } 140 | 141 | 142 | pub fn lines_test(input: &str) -> IResult<&str, Vec<&str>> { 143 | // let (input, bla) =separated_list1(newline, line)(input)?; 144 | 145 | let test_input = " 146 | (aa)(fine) 147 | (dsf)(sdf)(sdfezae)(azezae) 148 | (other)"; 149 | let (input, bli) = many_till(inner, eof )(test_input) ?; 150 | /* returns a 151 | result: [ 152 | ["aa", "fine"], 153 | ["dsf", "sdf", "sdfezae", "azezae"] 154 | ] 155 | now imagine if those where Vec / branches ... 156 | */ 157 | 158 | println!("FIRST TEST: input:{} /// result: {:?}", input, bli.0); 159 | 160 | 161 | /*let test_input = "aaa 162 | aaa 163 | 164 | aaa"; 165 | let(input , bli) = many_till(inner2, eof )(test_input)?; 166 | println!("SECOND TEST: input:{} /// result: {:?}", input, bli.0);*/ 167 | 168 | let test_input =" \n"; 169 | let bli = empty_line2(test_input); 170 | println!("THIRD TEST: result: {:?}", bli); 171 | 172 | 173 | let test_input = "aaa 174 | aaa 175 | 176 | "; 177 | let (input, bli) = ggdgdfgdf(test_input) ?; 178 | println!("FOURTH TEST: input:{} /// result: {:?}", input, bli); 179 | 180 | 181 | let test_input = "aaa 182 | aaa 183 | 184 | aaa 185 | aaa 186 | "; 187 | let (input, bli) = many0(ggdgdfgdf)(test_input) ?; 188 | println!("FIFTH TEST: input:{} /// result: {:?}", input, bli); 189 | 190 | let test_input = "foo 191 | bar 192 | 193 | baz 194 | biz 195 | 196 | "; 197 | let (input, bli) = many0(six)(test_input) ?; 198 | println!("SIXTH TEST: input:{} /// result: {:?}", input, bli); 199 | 200 | // FAILS 201 | let test_input = "foo 202 | bar 203 | 204 | baz 205 | biz 206 | "; 207 | let (input, bli) = many0(seventh)(test_input) ?; 208 | println!("SEVENTH TEST: input:{} /// result: {:?}", input, bli); 209 | 210 | let test_input = "foo 211 | bar 212 | 213 | baz 214 | biz 215 | "; 216 | let (input, bli) = many0(eigth)(test_input) ?; 217 | println!("EIGHT TEST: input:{} /// result: {:?}", input, bli); 218 | 219 | let test_input = " 220 | foo 221 | bar 222 | 223 | baz 224 | biz 225 | "; 226 | let (input, bli) = many0(ninth)(test_input) ?; 227 | println!("NINTH TEST: input:{} /// result: {:?}", input, bli); 228 | 229 | 230 | let bla = vec![]; 231 | Ok((input, bla)) 232 | } 233 | 234 | 235 | 236 | fn whitespaces_count(input: &str) -> usize { 237 | input 238 | .chars() 239 | .take_while(|ch| ch.is_whitespace() && *ch != '\n') 240 | .count() 241 | } 242 | 243 | pub fn hack_parse_choice_block(input: &str) -> IResult<&str, &str> { 244 | //let (input, white_spaces) = many0_count(tag(" "))(input)?; 245 | //let (input, (white_spaces, _) ) = tuple(( many0_count(space0), tag("->") ))(input)?; 246 | let (input, (white_spaces, _) ) = tuple(( many0_count(tag(" ")), tag("->") ))(input)?; 247 | println!("HACK {} whitespaces: {}", input, white_spaces); 248 | 249 | // hack to find an choice block ending with a blank line 250 | let lines_raw: Vec<&str> = input.split("\n").collect(); 251 | let mut matching_index = 0; 252 | 253 | for (index, line) in lines_raw.iter().enumerate() { 254 | println!("line {}", line); 255 | if is_line_empty(format!("{}\n", line).as_str()) { // horrible 256 | /*if whitespaces_count(line) == white_spaces { 257 | println!("matching whitespaces"); 258 | }*/ 259 | println!("found an empty line, we are at end of choice block"); 260 | matching_index = index; 261 | break; 262 | } 263 | } 264 | 265 | let binding = lines_raw[0..matching_index].join("\n"); 266 | let mut block:&str = binding.as_str(); 267 | let rest = lines_raw[matching_index..].join("\n").as_str(); // impossible to return, we would need to get to the same line index using the nom parsers & return that remain 268 | 269 | 270 | // let (input, _) = many0(take_until(newline))(input)?;//separated_list1(newline, line)(input)?; 271 | 272 | println!("block {}", block); 273 | 274 | 275 | Ok(("", "")) 276 | } 277 | 278 | /* 279 | fn seperator_empty_lines(input &str) -> IResult<&str, &str> { 280 | 281 | }*/ 282 | 283 | /* 284 | 285 | let yarn_text = "Bob: Hi ! 286 | Grumpy: Grumble ! 287 | Bob: Oh hello there grumpy ! 288 | Dona: fine and you ? 289 | 290 | -> block1_choiceA: grumble fsdf sdfsdf sdfds fsd sdf sdf 291 | fg: fuck off 292 | <> 293 | -> block1_choiceB: some dialogue 294 | <> 295 | 296 | -> block2_choiceA 297 | dfs 298 | df 299 | qsdqsd 300 | 301 | -> block3_choiceA 302 | -> block3_choiceB 303 | sdfdsf 304 | sdf 305 | -> block3_choiceC 306 | "; 307 | if let Ok((_, tree)) = parse_all_yarn(yarn_text) { 308 | println!("blocks {}", tree.len()); 309 | for choice in tree.iter(){ 310 | println!("choice: "); 311 | for branch in choice.branches.iter() { 312 | println!(" branch "); 313 | for line in branch.lines.iter() { 314 | println!(" line {:?}", line); 315 | 316 | } 317 | } 318 | } 319 | // println!("parse_all_yeah {:?}", parse_all_yarn( 320 | 321 | }else { 322 | println!("failed to parse"); 323 | } */ 324 | 325 | // we reverse iterate two by two do un pop & create things as they should be 326 | /*let mut two_by_two = choices_stack.iter().rev() 327 | .zip(choices_stack.iter().rev().skip(1)) 328 | .collect::>(); 329 | for (cur, mut prev) in two_by_two.iter_mut() { 330 | println!("cur {:?} prev {:?}", cur, prev); 331 | prev.branches.last_mut().unwrap().statements.push( 332 | Statements::Choice(**cur) 333 | ); 334 | //current_branch.statements.push(Statements::Choice(choice)); 335 | 336 | }*/ -------------------------------------------------------------------------------- /src/evaluator/dialogue_runner_resource.rs: -------------------------------------------------------------------------------- 1 | use bevy::ecs::component::Component; 2 | use bevy::ecs::system::Resource; 3 | use bevy::asset::Handle; 4 | use bevy::asset::HandleId; 5 | 6 | use crate::prelude::{Branch, Statements, Dialogue, Commands, YarnAsset}; 7 | 8 | 9 | #[derive(Debug, Resource)] 10 | pub struct DialogueRunner{ 11 | /// what yarn script are we using for this dialogue tracker 12 | pub yarn_asset: Handle,//Option<&'a YarnAsset>, 13 | pub current_node_name: String, 14 | pub current_statement_index: usize, 15 | pub current_choice_index: usize, 16 | 17 | pub current_branch: Branch, 18 | 19 | branches_stack: Vec, 20 | indices_stack: Vec<(usize, usize)> // we want to resume where we where 21 | } 22 | 23 | /*impl Default for DialogueRunner { 24 | fn default() -> Self { 25 | 26 | DialogueRunner { 27 | yarn_asset: None, 28 | current_node_name: "".into(), 29 | current_statement_index: 0, 30 | current_choice_index: 0, 31 | current_branch: Branch { statements: vec![] }, 32 | 33 | branches_stack: vec![], 34 | indices_stack: vec![] 35 | } 36 | } 37 | 38 | }*/ 39 | 40 | impl DialogueRunner { 41 | pub fn new(yarn_asset: Handle, start_node_name: String) -> DialogueRunner { 42 | // yarn_asset. 43 | DialogueRunner { 44 | yarn_asset: Some(yarn_asset), 45 | current_node_name: start_node_name.clone(), 46 | current_statement_index: 0, 47 | current_choice_index: 0 , 48 | current_branch: yarn_asset.nodes[&start_node_name.clone()].branch.clone(), 49 | 50 | branches_stack: vec![], 51 | indices_stack: vec![] 52 | } 53 | } 54 | 55 | /*pub fn start(dialogue_source: Handle, start_node_name: String) -> DialogueRunner { 56 | 57 | }*/ 58 | 59 | /* TODO: repurpose this as 'start' ? 60 | pub fn set_current_branch(&mut self, yarn_asset: &YarnAsset) { 61 | //let default = &yarn_asset.nodes[&self.current_node_name]; 62 | self.current_branch = yarn_asset.nodes[&self.current_node_name].branch.clone(); // FIXME: self.current_node_name might not be set correcly, add safeguard 63 | }*/ 64 | 65 | pub fn current_statement(&self) -> Statements { 66 | let current_statement = self.current_branch.statements[self.current_statement_index].clone(); 67 | current_statement 68 | } 69 | 70 | /// go to next entry if available, currently also validates the selected choice 71 | /// TODO: perhaps this should be called next_statement() ? or even just next() ? 72 | /// TODO: this should either return an Option or another error signifier (ie for example if there is no node for jumping etc) 73 | pub fn next_entry(&mut self) -> Statements { 74 | if self.yarn_asset.is_none() { 75 | // FIXME: not graceful at all !! 76 | panic!("no yarn asset for this dialogue runner") 77 | } 78 | let yarn_asset = self.yarn_asset.unwrap(); 79 | println!("next entry"); 80 | //FIXME yuck 81 | // this is to deal with choices 82 | let old_entry = self.current_branch.statements[self.current_statement_index].clone(); 83 | match old_entry { 84 | Statements::Choice(ref choice) => { 85 | println!("choice"); 86 | self.branches_stack.push(self.current_branch.clone()); 87 | self.indices_stack.push((self.current_choice_index, self.current_statement_index)); 88 | 89 | self.current_branch = choice.branches[self.current_choice_index].clone(); 90 | self.current_choice_index = 0; 91 | self.current_statement_index = 0; 92 | return self.current_branch.statements[self.current_statement_index].clone(); 93 | }, 94 | Statements::Exit => { 95 | println!("dialogues done") 96 | }, 97 | _=> {} 98 | } 99 | 100 | if self.current_statement_index + 1 < self.current_branch.statements.len() { 101 | self.current_statement_index +=1; 102 | } 103 | else { 104 | println!("last in current branch reached"); 105 | if self.branches_stack.len() > 0 { 106 | self.current_branch = self.branches_stack.pop().unwrap(); 107 | let (choice_index, statement_index) = self.indices_stack.pop().unwrap(); 108 | self.current_choice_index = 0; // reset choice to first choice 109 | self.current_statement_index = statement_index + 1 ; // FIXME: check if this is a valid statement !! 110 | } 111 | } 112 | let current_entry = self.current_branch.statements[self.current_statement_index].clone(); 113 | println!("current entry {:?}",current_entry); 114 | match current_entry { 115 | Statements::Command(command) => { 116 | println!("EXECUTE COMMAND {:?}", command); 117 | match command.command_type { 118 | Commands::Jump => { 119 | if yarn_asset.nodes.contains_key(&command.params){ 120 | // we jump to the other named node and return the first item from there 121 | // we also reset everything 122 | self.current_statement_index = 0; 123 | self.current_choice_index = 0; 124 | self.current_node_name = command.params.clone(); 125 | self.current_branch = yarn_asset.nodes[&self.current_node_name].branch.clone(); 126 | 127 | return self.current_branch.statements[self.current_statement_index].clone(); 128 | }else { 129 | println!("no node named {} found in the yarn file", &command.params); 130 | return self.next_entry(); 131 | } 132 | } 133 | _=> { 134 | return self.next_entry(); 135 | } 136 | } 137 | }, 138 | Statements::Choice(ref choices) => { 139 | println!("choice"); 140 | // self.current_branch = choices[self.current_choice_index].clone(); 141 | // self.current_choice_index = 0; 142 | // here we select the current choice: FIXME: should it be explictely another , seperate command ? like "validate choice ??" 143 | return current_entry; 144 | }, 145 | _=> { 146 | println!("line"); 147 | return current_entry; 148 | } 149 | } 150 | } 151 | 152 | /// go to the next choice, goes to 0 when overflowing 153 | pub fn next_choice(&mut self){ 154 | match self.current_statement() { 155 | Statements::Choice(ref choice) => { 156 | self.current_choice_index += 1; 157 | if self.current_choice_index >= choice.branches.len() { 158 | self.current_choice_index = 0; 159 | } 160 | } 161 | _ => { 162 | println!("not a choice !"); 163 | } 164 | } 165 | } 166 | 167 | /// go to the previous choice, goes to choices.len() -1 when underflowing 168 | pub fn prev_choice(&mut self){ 169 | match self.current_statement() { 170 | Statements::Choice(ref choice) => { 171 | if self.current_choice_index == 0 { 172 | self.current_choice_index = choice.branches.len() - 1; 173 | } else { 174 | self.current_choice_index -= 1; 175 | } 176 | } 177 | _ => { 178 | println!("not a choice !"); 179 | } 180 | } 181 | } 182 | 183 | /// 184 | pub fn specific_choice(&mut self, choice_index: usize) { 185 | match self.current_statement() { 186 | Statements::Choice(ref choice) => { 187 | if choice_index != 0 && choice_index < choice.branches.len() { 188 | self.current_choice_index = choice_index; 189 | } 190 | } 191 | _ => { 192 | println!("not a choice !"); 193 | } 194 | } 195 | } 196 | 197 | 198 | // TODO: these two functions are only needed because we do no keep a Dialogue in the branch data structure ... (a valid Choice HAS to have one, the root branch does not have one, obviously) 199 | pub fn get_current_choice_branch_first(&self) -> Result { 200 | let current_statement_index = self.current_statement(); 201 | match current_statement_index { 202 | Statements::Choice(ref choice) => { 203 | let current_choice_index = &choice.branches[self.current_choice_index]; 204 | let first = ¤t_choice_index.statements[0]; 205 | match first { 206 | Statements::Dialogue(dialogue) => { 207 | Ok(dialogue.clone()) 208 | }, 209 | _ => { 210 | Err("the first entry in the choice is not a Line".to_string()) 211 | } 212 | } 213 | 214 | }, 215 | _ => { 216 | Err("the current item is not a choice".to_string()) 217 | } 218 | } 219 | } 220 | 221 | /// helper function for choices: gives you a list of dialogues (ie, who, what), for example when 222 | /// you want to display the list current choices to the player 223 | pub fn get_current_choices (&self) -> Vec { 224 | let current_statement_index = self.current_statement(); 225 | match current_statement_index { 226 | Statements::Choice(ref choice) => { 227 | return choice.branches 228 | .iter() 229 | .map(|branch| { 230 | let first = &branch.statements[0]; 231 | match first { 232 | Statements::Dialogue(dialogue) => { 233 | return dialogue.clone() 234 | }, 235 | _=> { 236 | return Dialogue{..Default::default()} 237 | } 238 | } 239 | }).collect(); 240 | }, 241 | _=> { 242 | return vec![]; 243 | } 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/old.rs: -------------------------------------------------------------------------------- 1 | // TODO: testbed, remove later 2 | use std::{fs, collections::HashMap}; 3 | 4 | mod parser; 5 | 6 | use bevy_mod_yarn::parser::interpolated_value; 7 | 8 | use crate::{parser::{title, yarn_commands, identifier, tag_identifier, variable_identifier, statement_dialogue, parse_params, yarn_conditionals, header_tags, attributes, header, parse_yarn_nodes, statement_base, statement_choice, body::{self, display_dialogue_tree}}}; 9 | 10 | fn main() { 11 | /*println!("title {:?}",title("title: Start\n")); 12 | println!("title {:?}",title("title: Other_node\n")); 13 | println!("title {:?}",title(" title : Other_node\n")); 14 | println!("title {:?}",title(" title: Other_node \n")); 15 | println!("title {:?}",title(" title: Or_nodeç:sdfsdfs___sdf \n")); // INVALID only letters, numbers & underscores allowed 16 | println!("title {:?}",title(" title: __Or_nodeçsdfsdfs___sdf \n")); // INVALID: underscore a the start, a node title must start with a letter 17 | 18 | 19 | println!("header_tags {:?}",header_tags("tags: \n")); 20 | println!("header_tags {:?}",header_tags(" tags: \n")); 21 | println!("header_tags {:?}",header_tags(" tags: #blabla\n")); 22 | println!("header_tags {:?}",header_tags(" tags: #camera2 #background:conductor_cabin \n")); 23 | println!("header_tags {:?}",header_tags(" tags: blabla\n")); // again yarn's "no need for pound for tags in the header vs tags in the body ..." 24 | println!("header_tags {:?}",header_tags(" tags: #camera2 background:conductor_cabin \n")); // same 25 | 26 | 27 | println!("header {:?}",header(" 28 | title: Bar 29 | tags: #my_tag other and_another 30 | colorID: 0 31 | position: 567,-265 32 | --- 33 | ")); 34 | 35 | 36 | 37 | // commands 38 | println!("commands {:?}", yarn_commands("<>")); 39 | println!("commands {:?}", yarn_commands("<>")); 40 | println!("commands {:?}", yarn_commands("<>")); 41 | println!("commands {:?}", yarn_commands(" <> ")); 42 | println!("commands {:?}", yarn_commands("<>")); 43 | println!("commands {:?}", yarn_commands("<>")); 44 | println!("commands {:?}", yarn_commands("<>")); 45 | 46 | // identifiers 47 | println!("identifier {:?}", identifier("foo")); 48 | println!("identifier {:?}", identifier("foo_bar")); 49 | println!("identifier {:?}", identifier("#_foo_bar"));// INVALID 50 | 51 | // tag identifiers 52 | println!("tag_identifier {:?}", tag_identifier("#_foo_bar")); 53 | println!("tag_identifier {:?}", tag_identifier("#foo:bar")); 54 | println!("tag_identifier {:?}", tag_identifier("#foo:bar_baz:biz:boz")); 55 | println!("tag_identifier {:?}", tag_identifier("#camera2")); 56 | println!("tag_identifier {:?}", tag_identifier("#background:conductor_cabin")); 57 | 58 | // variable identifiers 59 | println!("variable_identifier {:?}", variable_identifier("$_foo_bar")); 60 | println!("variable_identifier {:?}", variable_identifier("_foo_bar")); // INVALID 61 | println!("variable_identifier {:?}", variable_identifier("$_foo.bar.baz")); 62 | 63 | // 64 | println!("attributes {:?}", attributes("Oh, [wave]hello[/wave] there!")); 65 | println!("attributes {:?}", attributes("Oh, [wave]hello[/wave] there! [dance] party time ![/dance] ")); 66 | 67 | // params (FIXME: actually expressions) 68 | println!("parse_params {:?}", parse_params("$gold_amount == 10")); 69 | println!("parse_params {:?}", parse_params("$gold_amount < 10")); 70 | println!("parse_params {:?}", parse_params("$gold_amount > 10")); 71 | println!("parse_params {:?}", parse_params("$gold_amount >= 10")); 72 | println!("parse_params {:?}", parse_params("$gold_amount <= 10")); 73 | 74 | 75 | // line variations 76 | 77 | // simple line cases 78 | println!("statement_dialogue {:?}", statement_dialogue(" Lamik:Hi I said to him")); 79 | println!("statement_dialogue {:?}", statement_dialogue(" Lamik: Hi I said to him\n")); 80 | println!("statement_dialogue {:?}", statement_dialogue("Lamik:Hi I said to him\n")); 81 | println!("statement_dialogue {:?}", statement_dialogue(" Lamik: Hi I said to him ____sdfd__\n")); 82 | println!("statement_dialogue {:?}", statement_dialogue("Hi I said to him\n")); 83 | println!("statement_dialogue {:?}", statement_dialogue("Hi I said to him, withouth new line")); 84 | 85 | // simple line cases with tags 86 | println!("statement_dialogue {:?}", statement_dialogue("Homer: Hi, I'd like to order a tire balancing. #sarcastic #duplicate\n")); 87 | println!("statement_dialogue {:?}", statement_dialogue("Homer: Hi, I'd like to order a tire balancing. #tone:sarcastic #duplicate\n")); 88 | 89 | println!("statement_choice {:?}", statement_choice("-> Lamik: not so great, sadly :(\n")); 90 | println!("statement_choice {:?}", statement_choice(" -> Lamik: not so great, sadly :(\n")); 91 | println!("statement_choice {:?}", statement_choice(" -> Lamik: not so great, sadly #tag :(\n")); 92 | 93 | 94 | // conditional 95 | // println!("yarn_conditionals {:?}", yarn_conditionals("<>dfsdfs<>")); 96 | 97 | 98 | println!("yarn_conditionals {:?}", yarn_conditionals("-> Sure I am! The boss knows me! < 10>>")); 99 | println!("yarn_conditionals {:?}", yarn_conditionals("<>Baker: Well, you can't afford one!<>")); // INVALID 100 | println!("yarn_conditionals {:?}", yarn_conditionals("<> Baker: Well, you can't afford one! <>")); 101 | println!("yarn_conditionals {:?}", yarn_conditionals("<> Baker: Well, you can't afford one! <> Baker: Here you go! <>")); 102 | */ 103 | // simple line with character 104 | /* 105 | assert_eq!( 106 | statement_dialogue("Lamik: Hi there !"), 107 | Ok(( 108 | "", 109 | Statements::Dialogue( 110 | Dialogue { who: "Lamik".into(), what: "Hi there !".into()} 111 | ) 112 | )) 113 | );*/ 114 | 115 | // opt(space0), 116 | 117 | // println!("header tags: {:?}",header_tags(" tags: sdf_1 zerze #bla")); 118 | // commands 119 | 120 | // println!("AAAAAH {:?}", hex_color_final("")); 121 | 122 | /*println!("parse_foo {:?}", parse_foo("-> opt1 123 | fg 124 | opt2 125 | bar 126 | 127 | -> other 128 | dfs 129 | df 130 | ")); 131 | 132 | let bla = "-> block1_choiceA 133 | fg 134 | -> block1_choiceB 135 | bar 136 | 137 | -> block2_choiceA 138 | dfs 139 | df 140 | "; 141 | println!("parse_foo2 {:?}", parse_foo2( "-> block1_choiceA 142 | fg 143 | -> block1_choiceB 144 | bar 145 | 146 | -> block2_choiceA 147 | dfs 148 | df 149 | "));*/ 150 | 151 | // println!("parse_bar {:?}",parse_bar("") ); 152 | 153 | let foobazbar = "Lamik: Hi there ! 154 | Dona: Hello ! 155 | Lamik: how are you doing ? 156 | <> #command_tag_what_the 157 | Bob: Hi ! 158 | Grumpy: Grumble ! 159 | Bob: Oh hello there grumpy ! #grumpy_thingy 160 | Dona: fine and you ? 161 | -> Lamik: doing ok #an_option_tag 162 | Dona: good to hear :) 163 | Grumpy: whatever ! 164 | Bob: cool cool cool ! 165 | Dona: let's have a party then ! 166 | Grumpy: NO! 167 | Bob: sure, whatever.. 168 | Lamik: yeah ! 169 | <> 170 | -> Lamik: not so great, sadly :( 171 | Dona: oh, what is the matter ? 172 | Lamik: have not caught any crabs. 173 | 174 | Lamik: well this was nice, but I am tired now 175 | Dona: oh ok then 176 | -> Lamik: want me to stay ? 177 | 178 | -> and another! 179 | 180 | ==="; 181 | let test_text = "some: text here \n some other text there#hash#otherhash \n something that ends sooner #hashtag #other\n -> a CHOICE ! \n << dsfdsf sdf>> \n sqd[wave]sdfds[/wave] \n"; 182 | 183 | //println!("statement_base {:?}", statement_base("some text here \n some other text there#hash#otherhash \n something that ends sooner #hashtag #other\n -> dsfdsf \n << dsfdsf sdf>> \n sqd[wave]sdfds[/wave] \n")); 184 | //println!("foobur {:?}", body(foobazbar)); 185 | 186 | let other_text = "Lamik: Hi there ! 187 | <> 188 | Dona: Hello ! 189 | -> Lamik: doing ok 190 | 191 | "; 192 | 193 | 194 | 195 | /* 196 | different cases for nesting 197 | // basic_eof 198 | A: how are you 199 | -> B: fine 200 | -> B: not ok 201 | EOF 202 | 203 | basic_blank_line 204 | A: how are you 205 | -> B: fine 206 | -> B: not ok 207 | BLANK_LINE 208 | EOF 209 | 210 | // basic_eof + 211 | A: how are you 212 | -> B: fine 213 | A: cool 214 | -> B: not ok 215 | A: oh no 216 | EOF 217 | 218 | basic_blank_line + 219 | A: how are you 220 | -> B: fine 221 | A: cool 222 | -> B: not ok 223 | A: oh no 224 | BLANK_LINE 225 | EOF 226 | 227 | // dedent_eof 228 | A: how are you 229 | -> B: fine 230 | A: unrelated // dedent: ends the choice above 231 | -> B: another choice 232 | A: oh no 233 | EOF 234 | 235 | // multi_indent_eof 236 | A: how are you 237 | -> B: fine 238 | -> B: a layer 239 | -> B: another layer // here we would have pop the state & close options until we are back at level 0 240 | EOF 241 | 242 | 243 | */ 244 | println!("interpolated {:?}", interpolated_value("you now have {$coins}, congratulations !")); 245 | 246 | let file_path = "./assets/micro.yarn"; // simple, micro minimal.yarn barebones.yarn 247 | 248 | let contents = fs::read_to_string(file_path) 249 | .expect("Should have been able to read the file"); 250 | let parsed = parse_yarn_nodes(&contents); 251 | for (_node_name, node) in parsed.iter() { 252 | println!("NODE({}):", node.title); 253 | println!(" Statements tree"); 254 | display_dialogue_tree(&node.branch, 1); 255 | } 256 | 257 | let file_path = "./assets/other.yarn"; // simple, micro minimal.yarn barebones.yarn 258 | println!("indentation return"); 259 | let contents = fs::read_to_string(file_path) 260 | .expect("Should have been able to read the file"); 261 | let parsed = parse_yarn_nodes(&contents); 262 | for (_node_name, node) in parsed.iter() { 263 | println!("NODE({}):", node.title); 264 | println!(" Statements tree"); 265 | display_dialogue_tree(&node.branch, 1); 266 | } 267 | 268 | let file_path = "./assets/complex.yarn"; // simple, micro minimal.yarn barebones.yarn 269 | println!("COMPLEX"); 270 | let contents = fs::read_to_string(file_path) 271 | .expect("Should have been able to read the file"); 272 | let parsed = parse_yarn_nodes(&contents); 273 | for (_node_name, node) in parsed.iter() { 274 | println!("NODE({}):", node.title); 275 | println!(" Statements tree"); 276 | display_dialogue_tree(&node.branch, 1); 277 | } 278 | 279 | 280 | 281 | println!("the end"); 282 | // parser(); 283 | } 284 | 285 | 286 | 287 | 288 | -------------------------------------------------------------------------------- /src/evaluator/dialogue_runner.rs: -------------------------------------------------------------------------------- 1 | use bevy::ecs::component::Component; 2 | // use bevy::ecs::system::Resource; 3 | // use bevy::asset::Handle; 4 | use crate::prelude::{Branch, Commands, Dialogue, Statements, YarnAsset}; 5 | 6 | #[derive(Debug, Component)] 7 | pub struct DialogueRunner { 8 | /// what yarn script are we using for this dialogue tracker 9 | pub yarn_asset: Option, 10 | pub current_node_name: String, 11 | pub current_statement_index: usize, 12 | pub current_choice_index: usize, 13 | 14 | pub current_branch: Branch, 15 | 16 | branches_stack: Vec, 17 | indices_stack: Vec<(usize, usize)>, // we want to resume where we where 18 | } 19 | 20 | impl Default for DialogueRunner { 21 | fn default() -> Self { 22 | DialogueRunner { 23 | yarn_asset: None, 24 | current_node_name: "".into(), 25 | current_statement_index: 0, 26 | current_choice_index: 0, 27 | current_branch: Branch { statements: vec![] }, 28 | 29 | branches_stack: vec![], 30 | indices_stack: vec![], 31 | } 32 | } 33 | } 34 | 35 | impl DialogueRunner { 36 | pub fn new(yarn_asset: YarnAsset, start_node_name: &str) -> DialogueRunner { 37 | let start_node_name = &start_node_name.clone().to_string(); 38 | // let current_branch: Branch = Branch { statements: vec![] }; // to handle case where there is no matching start node ? 39 | if !yarn_asset.nodes.contains_key(start_node_name) { 40 | panic!("yarn file does not contain node {:?}", start_node_name) 41 | } 42 | 43 | DialogueRunner { 44 | yarn_asset: Some(yarn_asset.clone()), 45 | current_node_name: start_node_name.clone(), 46 | current_statement_index: 0, 47 | current_choice_index: 0, 48 | current_branch: yarn_asset.nodes[&start_node_name.clone()].branch.clone(), 49 | 50 | branches_stack: vec![], 51 | indices_stack: vec![], 52 | } 53 | } 54 | 55 | pub fn set_current_branch(&mut self, yarn_asset: &YarnAsset) { 56 | //let default = &yarn_asset.nodes[&self.current_node_name]; 57 | self.current_branch = yarn_asset.nodes[&self.current_node_name].branch.clone(); 58 | // FIXME: self.current_node_name might not be set correcly, add safeguard 59 | } 60 | 61 | pub fn current_statement(&self) -> Statements { 62 | let current_statement = 63 | self.current_branch.statements[self.current_statement_index].clone(); 64 | current_statement 65 | } 66 | 67 | /// go to next entry if available, currently also validates the selected choice 68 | /// TODO: perhaps this should be called next_statement() ? or even just next() ? 69 | /// TODO: this should either return an Option or another error signifier (ie for example if there is no node for jumping etc) 70 | pub fn next_entry(&mut self) -> Statements { 71 | if self.yarn_asset.is_none() { 72 | panic!("no yarn asset for this dialogue runner") 73 | } 74 | let yarn_asset = self.yarn_asset.as_mut().unwrap(); 75 | // println!("next entry"); 76 | 77 | //FIXME yuck: not an ideal way to deal with choice selection 78 | // this is to deal with choices 79 | let current_entry = self.current_branch.statements[self.current_statement_index].clone(); 80 | match current_entry { 81 | Statements::Choice(ref choice) => { 82 | println!("choice"); 83 | self.branches_stack.push(self.current_branch.clone()); 84 | self.indices_stack 85 | .push((self.current_choice_index, self.current_statement_index)); 86 | 87 | self.current_branch = choice.branches[self.current_choice_index].clone(); 88 | self.current_choice_index = 0; 89 | self.current_statement_index = 0; 90 | return self.current_branch.statements[self.current_statement_index].clone(); 91 | } 92 | Statements::Exit => { 93 | println!("dialogues done"); 94 | return Statements::Exit; 95 | } 96 | _ => {} 97 | } 98 | 99 | if self.current_statement_index + 1 < self.current_branch.statements.len() { 100 | self.current_statement_index += 1; 101 | } else { 102 | // FIXME: not super clean way to pop until empty/ back in a normal flow 103 | while self.current_statement_index <= self.current_branch.statements.len() 104 | && self.branches_stack.len() > 0 105 | { 106 | self.current_branch = self.branches_stack.pop().unwrap(); 107 | let (_choice_index, statement_index) = self.indices_stack.pop().unwrap(); 108 | self.current_choice_index = 0; // reset choice to first choice // FIXME: should it use the choice index above ? 109 | self.current_statement_index = statement_index + 1; // FIXME: check if this is a valid statement !! 110 | } 111 | } 112 | let current_entry = self.current_branch.statements[self.current_statement_index].clone(); 113 | 114 | match current_entry { 115 | Statements::Command(command) => { 116 | match command.command_type { 117 | Commands::Declare => { 118 | // TODO: remove duplicate code 119 | // TODO: implement 120 | 121 | if self.current_statement_index + 1 < self.current_branch.statements.len() { 122 | self.current_statement_index += 1; 123 | } 124 | return self.current_branch.statements[self.current_statement_index] 125 | .clone(); 126 | } 127 | Commands::Set => { 128 | // TODO: remove duplicate code 129 | // TODO: implement 130 | if self.current_statement_index + 1 < self.current_branch.statements.len() { 131 | self.current_statement_index += 1; 132 | } 133 | return self.current_branch.statements[self.current_statement_index] 134 | .clone(); 135 | } 136 | Commands::Jump => { 137 | if yarn_asset.nodes.contains_key(&command.params) { 138 | // we jump to the other named node and return the first item from there 139 | // we also reset everything 140 | self.current_statement_index = 0; 141 | self.current_choice_index = 0; 142 | self.current_node_name = command.params.clone(); 143 | self.current_branch = 144 | yarn_asset.nodes[&self.current_node_name].branch.clone(); 145 | return self.current_branch.statements[self.current_statement_index] 146 | .clone(); 147 | } else { 148 | panic!("no node named {} found in the yarn file!", &command.params); 149 | } 150 | } 151 | Commands::Stop => { 152 | // TODO: remove duplicate code 153 | return Statements::Exit; 154 | } 155 | _ => { 156 | // for any non internal / Generic command, you need to handle going to the next entry yourself by calling runner.next_entry() 157 | return self.current_branch.statements[self.current_statement_index] 158 | .clone(); 159 | } 160 | } 161 | } 162 | _ => { 163 | return current_entry; 164 | } 165 | } 166 | } 167 | 168 | /// go to the next choice, goes to 0 when overflowing 169 | pub fn next_choice(&mut self) { 170 | match self.current_statement() { 171 | Statements::Choice(ref choice) => { 172 | self.current_choice_index += 1; 173 | if self.current_choice_index >= choice.branches.len() { 174 | self.current_choice_index = 0; 175 | } 176 | } 177 | _ => { 178 | println!("not a choice !"); 179 | } 180 | } 181 | } 182 | 183 | /// go to the previous choice, goes to choices.len() -1 when underflowing 184 | pub fn prev_choice(&mut self) { 185 | match self.current_statement() { 186 | Statements::Choice(ref choice) => { 187 | if self.current_choice_index == 0 { 188 | self.current_choice_index = choice.branches.len() - 1; 189 | } else { 190 | self.current_choice_index -= 1; 191 | } 192 | } 193 | _ => { 194 | println!("not a choice !"); 195 | } 196 | } 197 | } 198 | 199 | /// 200 | pub fn specific_choice(&mut self, choice_index: usize) { 201 | match self.current_statement() { 202 | Statements::Choice(ref choice) => { 203 | if choice_index != 0 && choice_index < choice.branches.len() { 204 | self.current_choice_index = choice_index; 205 | } 206 | } 207 | _ => { 208 | println!("not a choice !"); 209 | } 210 | } 211 | } 212 | 213 | // TODO: these two functions are only needed because we do no keep a Dialogue in the branch data structure ... (a valid Choice HAS to have one, the root branch does not have one, obviously) 214 | pub fn get_current_choice_branch_first(&self) -> Result { 215 | let current_statement_index = self.current_statement(); 216 | match current_statement_index { 217 | Statements::Choice(ref choice) => { 218 | let current_choice_index = &choice.branches[self.current_choice_index]; 219 | let first = ¤t_choice_index.statements[0]; 220 | match first { 221 | Statements::Dialogue(dialogue) => Ok(dialogue.clone()), 222 | _ => Err("the first entry in the choice is not a Line".to_string()), 223 | } 224 | } 225 | _ => Err("the current item is not a choice".to_string()), 226 | } 227 | } 228 | 229 | /// helper function for choices: gives you a list of dialogues (ie, who, what), for example when 230 | /// you want to display the list current choices to the player 231 | pub fn get_current_choices(&self) -> (Vec, usize) { 232 | let current_statement = self.current_statement(); 233 | match current_statement { 234 | Statements::Choice(ref choice) => { 235 | return ( 236 | choice 237 | .branches 238 | .iter() 239 | .map(|branch| { 240 | let first = &branch.statements[0]; 241 | match first { 242 | Statements::Dialogue(dialogue) => return dialogue.clone(), 243 | _ => { 244 | return Dialogue { 245 | ..Default::default() 246 | } 247 | } 248 | } 249 | }) 250 | .collect(), 251 | self.current_choice_index, 252 | ); 253 | } 254 | _ => { 255 | return (vec![], self.current_choice_index); 256 | } 257 | } 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /LICENSE_APACHE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2023] [Mark "kaosat-dev" Moissette] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /src/parser/body copy.rs: -------------------------------------------------------------------------------- 1 | use nom::{ 2 | bytes::complete::{tag, is_not, take_till, take_until, take_while_m_n, take_while}, 3 | branch::alt, 4 | error::ParseError, 5 | 6 | IResult, 7 | multi::{separated_list1, many0_count, many0, separated_list0, many1, many_till, count}, 8 | character::complete::{newline, alphanumeric0, anychar, alpha1, alphanumeric1, multispace0, space0, digit0, one_of, char, line_ending, not_line_ending}, 9 | sequence::{delimited, preceded, terminated, pair, separated_pair, tuple }, 10 | combinator::{recognize, opt, not, eof, map}, 11 | InputTakeAtPosition, 12 | number::complete::{float, recognize_float} 13 | }; 14 | 15 | use super::{YarnCommand, Statements, Dialogue, Choice, Branch}; 16 | use super::{spacey, parse_params, identifier, tag_identifier }; 17 | 18 | // TODO, replace parse_params with an EXPRESSION 19 | pub fn yarn_commands(input: &str) -> IResult<&str, YarnCommand> { 20 | let (input, params) = delimited(spacey(tag("<<")), take_until(">>"), spacey(tag(">>")))(input)?; 21 | let (_, params) = parse_params(params)?; 22 | let command = YarnCommand{name: params[0].to_string(), params: params[1..].join(","), ..Default::default()}; 23 | Ok((input, command)) 24 | } 25 | 26 | pub fn statement_command(input: &str) -> IResult<&str, Statements> { 27 | map(spacey(yarn_commands), |command: YarnCommand| Statements::Command(command))(input) 28 | } 29 | 30 | 31 | pub fn statement_choice(input: &str) -> IResult<&str, Statements> { //(Statements, usize) 32 | let (input, indentations)= take_until("->")(input)?; //tuple(( many0_count(space0), tag("->") ))(input)?; 33 | let (input, _) = tag("->")(input)?; 34 | let (input, rest) = till_end(input)?; 35 | let (input, dialogue) = statement_dialogue(rest)?; 36 | let choice = Statements::ChoiceBranch(Branch{ statements: vec![dialogue], ..Default::default() }); 37 | Ok(( input, choice)) 38 | //Ok(( input, (dialogue, indentations.len()) )) 39 | } 40 | 41 | 42 | // TODO, replace parse_params with an EXPRESSION 43 | pub fn if_start(input: &str) -> IResult<&str, &str> { 44 | 45 | let (input, inside_if) = delimited( spacey(tag("<>"), tag(">>"))(input) ?; 46 | println!("inside_if {} ", inside_if); 47 | 48 | /*let (input , _) = tag("<>")(input)?; 50 | let (input, expression) = parse_params(input)?; 51 | let (input , _) = tag(" >>")(input)?;*/ 52 | Ok((input, inside_if)) 53 | } 54 | 55 | pub fn yarn_conditionals(input: &str) -> IResult<&str, Vec<&str>> { 56 | // FIXME: delimited here is wrong, we do not want to discard the content of if_start 57 | let (input, params) = delimited(spacey(if_start), take_until("<>"), tag("<>"))(input)?; 58 | println!("conditional body: {}", params); 59 | // let (input, params) = parse_params(params)?; 60 | Ok((input, vec![params])) 61 | } 62 | 63 | 64 | /// node tags are only allowed at the END of a line 65 | pub fn node_tags(input: &str) -> IResult<&str, &str> { 66 | // let (input, _) = tag("#")(input)?; 67 | let (input, aa) = take_until("#")(input)?;// separated_list0(tag("#"), identifier)(input) ?; 68 | println!("bla bla {:?}", aa); 69 | let (input, tag) = take_until(" ")(input)?; 70 | println!("tag result {:?}",tag); 71 | Ok((input, tag)) 72 | } 73 | 74 | pub fn till_end(input: &str) -> IResult<&str, &str> { 75 | terminated(not_line_ending, alt(( tag("\n"), eof ))) (input) //take_until("\n")(input)?; 76 | } 77 | 78 | 79 | pub fn rest (input: &str) -> IResult<&str, &str> { 80 | // CAREFULL ! swallows the whole input 81 | Ok(("", input)) 82 | } 83 | 84 | 85 | /// a bit more complex 86 | /// [wave size=2]Wavy![/wave] size=2 is an expression, an assignment expression 87 | pub fn attributes(input: &str) -> IResult<&str, (String, Vec<&str>)> { 88 | // this is a special one, as we want to extract the tags, but keep the rest of the text 89 | let mut withouth_attributes:Vec<&str>= vec![]; 90 | let mut attributes:Vec<&str>= vec![]; 91 | let (input , before_attribute) = take_until("[")(input)?; 92 | withouth_attributes.push(before_attribute); 93 | 94 | let (input, attribute_name) = delimited(tag("["), identifier, tag("]"))(input)?; 95 | attributes.push(attribute_name); 96 | //println!("ATTRIBUTES start input {}, attribute_name {}", input, attribute_name); 97 | let (input , inside) = take_until("[/")(input)?; 98 | withouth_attributes.push(inside); 99 | 100 | let (input, closing_attribute_name) = delimited(tag("[/"), identifier, tag("]"))(input)?; 101 | // println!("ATTRIBUTES end input {}, bla {}", input, closing_attribute_name); 102 | withouth_attributes.push(input); 103 | // TODO: detect un matching attribute names & throw an error ? 104 | println!("text withouth attributes {:?}",withouth_attributes.join(" ")); 105 | println!("attributes {:?}", attributes); 106 | 107 | let text_withouth_attributes = withouth_attributes.join(" "); 108 | 109 | Ok((input, (text_withouth_attributes, attributes))) 110 | } 111 | 112 | pub fn statement_dialogue_who_what(input: &str) -> IResult<&str, Statements> { 113 | let (input, (who, _, what)) = tuple((spacey(identifier), spacey(tag(":")), alt((till_end, rest))))(input)?; 114 | let result = Statements::Dialogue(Dialogue { who: who.to_string(), what: what.to_string(), ..Default::default() }); 115 | Ok((input, result)) 116 | } 117 | 118 | pub fn statement_dialogue_what(input: &str) -> IResult<&str, Statements> { 119 | let (input, what) = till_end(input)?; 120 | let result = Statements::Dialogue(Dialogue { who: "nobody".to_string(), what: what.to_string(), ..Default::default() }); 121 | Ok((input, result)) 122 | } 123 | 124 | // (identifier :) (optional) text \n 125 | pub fn statement_dialogue(input: &str) -> IResult<&str, Statements> { 126 | let (input, result) = 127 | alt( 128 | (statement_dialogue_who_what, statement_dialogue_what) 129 | )(input) ?; 130 | // here we have the who + what combo, so we can extract special character like tags etc 131 | Ok((input, result)) 132 | } 133 | 134 | 135 | // fixme: not sure 136 | fn statement_empty_line(input: &str) -> IResult<&str, Statements> { 137 | let (input, _) = empty_line(input)?; 138 | Ok((input, Statements::Empty)) 139 | } 140 | 141 | 142 | pub fn hashtags(input: &str) -> IResult<&str, Vec<&str>> { 143 | many0(spacey(tag_identifier))(input) 144 | } 145 | 146 | pub fn get_indentation(input: &str) -> IResult<&str, usize> { 147 | let mut identation = 0; 148 | // FIXME: damn, whitespace counting needs to include the choice's -> 149 | 150 | if input.contains("->") { 151 | let (bli, (pre_space, tag, post_space, _)) = tuple(( space0, tag("->"), space0, not_line_ending ))(input)?; 152 | //println!("WHITESPACE TEST2: {} {:?}", bli, (pre_space.len(), tag.len(), post_space.len())); 153 | identation = pre_space.len() + tag.len() + post_space.len(); 154 | }else { 155 | let (_, (white_spaces, rest_of_line)) = tuple(( space0, not_line_ending))(input)?; 156 | //println!("WHITESPACE TEST: {} {:?}", white_spaces.len(), rest_of_line); 157 | identation = white_spaces.len(); 158 | 159 | } 160 | 161 | Ok(("", identation)) 162 | } 163 | 164 | 165 | // see https://github.com/YarnSpinnerTool/YarnSpinner/blob/040a2436d98e5c0cc72e6a8bc04e6c3fa156399d/Documentation/Yarn-Spec.md#body 166 | pub fn statement_base_line(input: &str) -> IResult<&str, (&str, Vec<&str>, usize)> { 167 | let (input, content) = terminated(not_line_ending, alt(( tag("#"),tag("\n"))) )(input)?; 168 | //extract white spaces/indentation 169 | let (_, indentation) = get_indentation(content)?; 170 | let (hashtags_raw, con) = opt(take_until("#"))(content)?; 171 | let mut tags: Vec<&str> = vec![]; 172 | let mut result = content; // FIXME this whole thing is terrible 173 | if let Some(c) = con { 174 | if let(Ok(_tags)) = hashtags(hashtags_raw) { 175 | tags = _tags.1; 176 | } 177 | result = c; 178 | } 179 | Ok((input, (result, tags, indentation))) 180 | } 181 | 182 | 183 | // see https://github.com/YarnSpinnerTool/YarnSpinner/blob/040a2436d98e5c0cc72e6a8bc04e6c3fa156399d/Documentation/Yarn-Spec.md#body 184 | // returns a Vec<(content, Vec) 185 | // ie each line with its tags 186 | pub fn statement_base(input: &str) -> IResult<&str, Vec<(&str, Vec<&str>, usize)>> { 187 | many1(statement_base_line)(input) 188 | } 189 | 190 | pub fn state_pop(mut stack: Vec, mut current_branch : Branch, mut current_branches: Vec) -> Branch{ 191 | current_branches.push(current_branch.clone()); 192 | 193 | if stack.len() > 0 { 194 | current_branch = stack.pop().unwrap(); 195 | if current_branches.len() > 0 { 196 | current_branch.statements.push( // need to be pushed to the parent branch, so that is why we pop() first 197 | Statements::Choice(Choice { branches: current_branches.clone() , ..Default::default()} ) 198 | ); 199 | } 200 | } 201 | println!("nesting level {}", stack.len()); 202 | 203 | current_branches = vec![]; 204 | 205 | current_branch 206 | } 207 | 208 | /// wraps all the rest 209 | pub fn body(input: &str) -> IResult<&str, Branch> { 210 | let (input, lines) = statement_base(input)?; // TODO: use nom's map 211 | 212 | let mut current_branches: Vec = vec![]; // stores the data for the current choice node , if any 213 | let mut current_branch : Branch = Branch { statements: vec![], ..Default::default() }; // this is the root branch after the end of the parsing 214 | let mut stack: Vec = vec![]; 215 | 216 | let mut choices_stack: Vec = vec![]; 217 | // remember choice "groups" are delimited by : 218 | // - empty white line 219 | // - a different indentation 220 | 221 | let mut previous_indentation:usize = 0; 222 | let mut nesting_level= 0; 223 | 224 | let mut close_options = false; // signal we need a cleanup 225 | for (line, tags, indentation) in lines.iter() { 226 | // the order of these is important !! 227 | let (_, statement) = alt(( 228 | statement_empty_line, 229 | statement_command, 230 | statement_choice, 231 | statement_dialogue_who_what, 232 | statement_dialogue_what, 233 | ) 234 | )(line)?; 235 | let tags: Vec = tags.clone().iter().map(|x|x.to_string()).collect(); 236 | let indentation = indentation.clone(); 237 | 238 | // println!("statement {:?}, tags: {:?}" ,statement.clone(), tags); 239 | match statement.clone(){ 240 | Statements::ChoiceBranch(branch) => { 241 | println!("nesting level BEFORE BEFORE {}//{}", nesting_level, stack.len()); 242 | println!("indentation vs previous {} //{}", indentation, previous_indentation); 243 | // IF non nested, the branch is on the same level as previous branches 244 | if indentation > previous_indentation { 245 | println!("higher level, we need to nest !"); 246 | 247 | }else if indentation == previous_indentation { 248 | println!("same level , add another branch"); 249 | // push the previous choice branch to the list of branches in the choice 250 | current_branches.push(current_branch.clone()); 251 | if stack.len() > 0 { 252 | current_branch = stack.pop().unwrap(); 253 | } 254 | }else { 255 | // FIXME: we would need close_options // popping back BEFORE this, otherwise, current_branch is still the nested branch & not the root 256 | // state_pop(stack, current_branch, current_branches); 257 | println!("lower level leave this branch") 258 | } 259 | 260 | stack.push(current_branch); 261 | current_branch = branch; 262 | nesting_level = stack.len(); 263 | } 264 | Statements::Empty => { 265 | close_options = true; 266 | } 267 | _=> { 268 | // we push everything else to the current branch 269 | // FIXME: we would need close_options // popping back BEFORE this, otherwise, current_branch is still the nested branch & not the root 270 | 271 | if indentation < previous_indentation { 272 | println!("poping"); 273 | current_branches.push(current_branch.clone()); 274 | if stack.len() > 0 { 275 | current_branch = stack.pop().unwrap(); 276 | if current_branches.len() > 0 { 277 | current_branch.statements.push( // need to be pushed to the parent branch, so that is why we pop() first 278 | Statements::Choice(Choice { branches: current_branches.clone() , ..Default::default()} ) 279 | ); 280 | } 281 | } 282 | // println!("nesting level {}", stack.len()); 283 | current_branches = vec![]; 284 | } 285 | 286 | 287 | current_branch.statements.push(statement); 288 | println!("nesting level {}", stack.len()); 289 | } 290 | } 291 | 292 | // generic handling, outside of specific cases 293 | if indentation < previous_indentation { 294 | println!("lower level leave this branch"); 295 | // close_options = true; 296 | } 297 | 298 | previous_indentation = indentation.clone(); 299 | 300 | if close_options { 301 | println!("poping"); 302 | 303 | // IF we had an open CHOICE still gathering branches, add a choice with all current branches 304 | //If we have an empty line, OR if the IDENTATION IS LESS pop back to the previous level branch 305 | current_branches.push(current_branch.clone()); 306 | 307 | if stack.len() > 0 { 308 | current_branch = stack.pop().unwrap(); 309 | if current_branches.len() > 0 { 310 | current_branch.statements.push( // need to be pushed to the parent branch, so that is why we pop() first 311 | Statements::Choice(Choice { branches: current_branches.clone() , ..Default::default()} ) 312 | ); 313 | } 314 | } 315 | // println!("nesting level {}", stack.len()); 316 | current_branches = vec![]; 317 | close_options = false; 318 | } 319 | } 320 | // lines done 321 | // unstack & push the branches 322 | // current_branches.push(current_branch); 323 | // here current_branch should be the root branch 324 | Ok((input, current_branch)) 325 | } 326 | 327 | 328 | pub fn display_dialogue_tree(branch: &Branch, indentation_level: usize) { 329 | let identation_pattern = " "; 330 | let identation = format!(" {}", identation_pattern.repeat(indentation_level)); 331 | for statement in branch.statements.iter(){ 332 | match statement { 333 | Statements::Choice(choice) => { 334 | println!("{}statement choices ({}): tags:{:?}", identation, choice.branches.len(), choice.tags); 335 | for branch in choice.branches.iter() { 336 | println!("{}{}Branch:", identation, identation); 337 | display_dialogue_tree(branch, indentation_level +3 ); 338 | } 339 | } 340 | _ => { 341 | println!("{}statement {:?}",identation, statement); 342 | } 343 | } 344 | } 345 | } 346 | 347 | 348 | // should be empty line OR eof 349 | fn empty_line(input: &str) -> IResult<&str, &str> { 350 | recognize( 351 | many_till(space0, alt(( tag("\n"), eof )) ) 352 | )(input) 353 | } 354 | 355 | -------------------------------------------------------------------------------- /tests/parser_test.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use bevy_mod_yarn::prelude::*; 4 | 5 | #[test] 6 | fn test_parse_minimal() { 7 | let micro = "title: Test_node 8 | --- 9 | Dona: what is wrong ? 10 | Grumpy: ... 11 | === 12 | "; 13 | 14 | let mut expected: HashMap = HashMap::new(); 15 | expected.insert( 16 | "Test_node".into(), 17 | YarnNode { 18 | title: "Test_node".into(), 19 | branch: { 20 | Branch { 21 | statements: vec![ 22 | Statements::Dialogue(Dialogue { 23 | who: "Dona".into(), 24 | what: "what is wrong ?".into(), 25 | ..Default::default() 26 | }), 27 | Statements::Dialogue(Dialogue { 28 | who: "Grumpy".into(), 29 | what: "...".into(), 30 | ..Default::default() 31 | }), 32 | Statements::Exit, 33 | ], 34 | } 35 | }, 36 | ..Default::default() 37 | }, 38 | ); 39 | assert_eq!(parse_yarn_nodes(micro), expected); 40 | 41 | // assert_eq!(yarn_commands("<>"), Ok(("", vec!["stop"]))); 42 | // assert_eq!(yarn_commands("<>"), Ok(("", vec!["say", "hello"]))); 43 | // assert_eq!(yarn_commands("<>"), Ok(("", vec!["jump", "Other_node"]))); 44 | } 45 | 46 | #[test] 47 | fn test_branching_basic_whiteline_seperator() { 48 | let choices = "title: Test_node 49 | --- 50 | it was a beautiful day , said nobody 51 | Lamik: hi ! 52 | Dona: good morning , how are you ? 53 | -> Lamik: are you asking me ? 54 | Dona: yes 55 | -> Lamik: fine ! 56 | Dona: good to hear 57 | 58 | === 59 | "; 60 | let mut expected: HashMap = HashMap::new(); 61 | expected.insert( 62 | "Test_node".into(), 63 | YarnNode { 64 | title: "Test_node".into(), 65 | branch: { 66 | Branch { 67 | statements: vec![ 68 | Statements::Dialogue(Dialogue { 69 | who: "nobody".into(), 70 | what: "it was a beautiful day , said nobody".into(), 71 | ..Default::default() 72 | }), 73 | Statements::Dialogue(Dialogue { 74 | who: "Lamik".into(), 75 | what: "hi !".into(), 76 | ..Default::default() 77 | }), 78 | Statements::Dialogue(Dialogue { 79 | who: "Dona".into(), 80 | what: "good morning , how are you ?".into(), 81 | ..Default::default() 82 | }), 83 | Statements::Choice(Choice { 84 | branches: vec![ 85 | Branch { 86 | statements: vec![ 87 | Statements::Dialogue(Dialogue { 88 | who: "Lamik".into(), 89 | what: "are you asking me ?".into(), 90 | ..Default::default() 91 | }), 92 | Statements::Dialogue(Dialogue { 93 | who: "Dona".into(), 94 | what: "yes".into(), 95 | ..Default::default() 96 | }), 97 | ], 98 | }, 99 | Branch { 100 | statements: vec![ 101 | Statements::Dialogue(Dialogue { 102 | who: "Lamik".into(), 103 | what: "fine !".into(), 104 | ..Default::default() 105 | }), 106 | Statements::Dialogue(Dialogue { 107 | who: "Dona".into(), 108 | what: "good to hear".into(), 109 | ..Default::default() 110 | }), 111 | ], 112 | }, 113 | ], 114 | ..Default::default() 115 | }), 116 | Statements::Exit, 117 | ], 118 | } 119 | }, 120 | ..Default::default() 121 | }, 122 | ); 123 | assert_eq!(parse_yarn_nodes(choices), expected); 124 | } 125 | 126 | #[test] 127 | fn test_branching_basic_eof_seperator() { 128 | let choices = "title: Test_node 129 | --- 130 | it was a beautiful day , said nobody 131 | Lamik: hi ! 132 | Dona: good morning , how are you ? 133 | -> Lamik: are you asking me ? 134 | Dona: yes 135 | -> Lamik: fine ! 136 | Dona: good to hear 137 | === 138 | "; 139 | let mut expected: HashMap = HashMap::new(); 140 | expected.insert( 141 | "Test_node".into(), 142 | YarnNode { 143 | title: "Test_node".into(), 144 | branch: { 145 | Branch { 146 | statements: vec![ 147 | Statements::Dialogue(Dialogue { 148 | who: "nobody".into(), 149 | what: "it was a beautiful day , said nobody".into(), 150 | ..Default::default() 151 | }), 152 | Statements::Dialogue(Dialogue { 153 | who: "Lamik".into(), 154 | what: "hi !".into(), 155 | ..Default::default() 156 | }), 157 | Statements::Dialogue(Dialogue { 158 | who: "Dona".into(), 159 | what: "good morning , how are you ?".into(), 160 | ..Default::default() 161 | }), 162 | Statements::Choice(Choice { 163 | branches: vec![ 164 | Branch { 165 | statements: vec![ 166 | Statements::Dialogue(Dialogue { 167 | who: "Lamik".into(), 168 | what: "are you asking me ?".into(), 169 | ..Default::default() 170 | }), 171 | Statements::Dialogue(Dialogue { 172 | who: "Dona".into(), 173 | what: "yes".into(), 174 | ..Default::default() 175 | }), 176 | ], 177 | }, 178 | Branch { 179 | statements: vec![ 180 | Statements::Dialogue(Dialogue { 181 | who: "Lamik".into(), 182 | what: "fine !".into(), 183 | ..Default::default() 184 | }), 185 | Statements::Dialogue(Dialogue { 186 | who: "Dona".into(), 187 | what: "good to hear".into(), 188 | ..Default::default() 189 | }), 190 | ], 191 | }, 192 | ], 193 | ..Default::default() 194 | }), 195 | Statements::Exit, 196 | ], 197 | } 198 | }, 199 | ..Default::default() 200 | }, 201 | ); 202 | assert_eq!(parse_yarn_nodes(choices), expected); 203 | } 204 | 205 | #[test] 206 | fn test_branching_basic_eof_seperator_lines_at_root_and_commands() { 207 | let choices = "title: Test_node 208 | --- 209 | it was a beautiful day , said nobody 210 | Lamik: hi ! 211 | Dona: good morning , how are you ? 212 | -> Lamik: are you asking me ? 213 | Dona: yes 214 | <> 215 | -> Lamik: fine ! 216 | Dona: good to hear 217 | <> 218 | Lamik: some other stuff 219 | <> 220 | === 221 | "; 222 | let mut expected: HashMap = HashMap::new(); 223 | expected.insert( 224 | "Test_node".into(), 225 | YarnNode { 226 | title: "Test_node".into(), 227 | branch: { 228 | Branch { 229 | statements: vec![ 230 | Statements::Dialogue(Dialogue { 231 | who: "nobody".into(), 232 | what: "it was a beautiful day , said nobody".into(), 233 | ..Default::default() 234 | }), 235 | Statements::Dialogue(Dialogue { 236 | who: "Lamik".into(), 237 | what: "hi !".into(), 238 | ..Default::default() 239 | }), 240 | Statements::Dialogue(Dialogue { 241 | who: "Dona".into(), 242 | what: "good morning , how are you ?".into(), 243 | ..Default::default() 244 | }), 245 | Statements::Choice(Choice { 246 | branches: vec![ 247 | Branch { 248 | statements: vec![ 249 | Statements::Dialogue(Dialogue { 250 | who: "Lamik".into(), 251 | what: "are you asking me ?".into(), 252 | ..Default::default() 253 | }), 254 | Statements::Dialogue(Dialogue { 255 | who: "Dona".into(), 256 | what: "yes".into(), 257 | ..Default::default() 258 | }), 259 | Statements::Command(YarnCommand { 260 | name: "jump".into(), 261 | params: "node_a".to_string(), 262 | command_type: Commands::Jump, 263 | ..Default::default() 264 | }), 265 | ], 266 | }, 267 | Branch { 268 | statements: vec![ 269 | Statements::Dialogue(Dialogue { 270 | who: "Lamik".into(), 271 | what: "fine !".into(), 272 | ..Default::default() 273 | }), 274 | Statements::Dialogue(Dialogue { 275 | who: "Dona".into(), 276 | what: "good to hear".into(), 277 | ..Default::default() 278 | }), 279 | Statements::Command(YarnCommand { 280 | name: "jump".into(), 281 | params: "node_b".to_string(), 282 | command_type: Commands::Jump, 283 | ..Default::default() 284 | }), 285 | ], 286 | }, 287 | ], 288 | ..Default::default() 289 | }), 290 | Statements::Dialogue(Dialogue { 291 | who: "Lamik".into(), 292 | what: "some other stuff".into(), 293 | ..Default::default() 294 | }), 295 | Statements::Command(YarnCommand { 296 | name: "blowup".into(), 297 | params: "universe,now".to_string(), 298 | command_type: Commands::Generic, 299 | ..Default::default() 300 | }), 301 | Statements::Exit, 302 | ], 303 | } 304 | }, 305 | ..Default::default() 306 | }, 307 | ); 308 | assert_eq!(parse_yarn_nodes(choices), expected); 309 | } 310 | 311 | #[test] 312 | fn test_branching_nesting_eof_seperator() { 313 | let choices = "title: Test_node 314 | --- 315 | it was a beautiful day , said nobody 316 | -> A 317 | -> A1 318 | -> A2 319 | -> B 320 | === 321 | "; 322 | let mut expected: HashMap = HashMap::new(); 323 | expected.insert( 324 | "Test_node".into(), 325 | YarnNode { 326 | title: "Test_node".into(), 327 | branch: { 328 | Branch { 329 | statements: vec![ 330 | Statements::Dialogue(Dialogue { 331 | who: "nobody".into(), 332 | what: "it was a beautiful day , said nobody".into(), 333 | ..Default::default() 334 | }), 335 | Statements::Choice(Choice { 336 | branches: vec![ 337 | Branch { 338 | statements: vec![ 339 | Statements::Dialogue(Dialogue { 340 | who: "nobody".into(), 341 | what: "A".into(), 342 | ..Default::default() 343 | }), 344 | Statements::Choice(Choice { 345 | branches: vec![ 346 | Branch { 347 | statements: vec![Statements::Dialogue( 348 | Dialogue { 349 | who: "nobody".into(), 350 | what: "A1".into(), 351 | ..Default::default() 352 | }, 353 | )], 354 | }, 355 | Branch { 356 | statements: vec![Statements::Dialogue( 357 | Dialogue { 358 | who: "nobody".into(), 359 | what: "A2".into(), 360 | ..Default::default() 361 | }, 362 | )], 363 | }, 364 | ], 365 | ..Default::default() 366 | }), 367 | ], 368 | }, 369 | Branch { 370 | statements: vec![Statements::Dialogue(Dialogue { 371 | who: "nobody".into(), 372 | what: "B".into(), 373 | ..Default::default() 374 | })], 375 | }, 376 | ], 377 | ..Default::default() 378 | }), 379 | Statements::Exit, 380 | ], 381 | } 382 | }, 383 | ..Default::default() 384 | }, 385 | ); 386 | assert_eq!(parse_yarn_nodes(choices), expected); 387 | } 388 | -------------------------------------------------------------------------------- /src/parser/body.rs: -------------------------------------------------------------------------------- 1 | use nom::{ 2 | branch::alt, 3 | bytes::complete::{tag, take_until}, 4 | character::complete::{not_line_ending, space0}, 5 | combinator::{eof, map, opt, recognize}, 6 | multi::{many0, many1, many_till}, 7 | sequence::{delimited, terminated, tuple}, 8 | // error::ParseError, 9 | IResult, 10 | }; 11 | 12 | use super::{identifier, parse_params, spacey, tag_identifier}; 13 | use super::{variable_identifier, Branch, Choice, Commands, Dialogue, Statements, YarnCommand}; 14 | 15 | // TODO, replace parse_params with an EXPRESSION 16 | pub fn yarn_commands(input: &str) -> IResult<&str, YarnCommand> { 17 | let (input, params) = delimited(spacey(tag("<<")), take_until(">>"), spacey(tag(">>")))(input)?; 18 | let (_, params) = parse_params(params)?; 19 | let command_name = params[0]; 20 | let cmd = match command_name { 21 | "declare" => Commands::Declare, 22 | "set" => Commands::Set, 23 | "jump" => Commands::Jump, 24 | "stop" => Commands::Stop, 25 | _ => Commands::Generic, 26 | }; 27 | 28 | let command = YarnCommand { 29 | name: params[0].to_string(), 30 | params: params[1..].join(","), 31 | command_type: cmd, 32 | ..Default::default() 33 | }; 34 | Ok((input, command)) 35 | } 36 | 37 | pub fn statement_command(input: &str) -> IResult<&str, Statements> { 38 | map(spacey(yarn_commands), |command: YarnCommand| { 39 | Statements::Command(command) 40 | })(input) 41 | } 42 | 43 | pub fn statement_choice(input: &str) -> IResult<&str, Statements> { 44 | //(Statements, usize) 45 | let (input, _indentations) = take_until("->")(input)?; //tuple(( many0_count(space0), tag("->") ))(input)?; 46 | let (input, _) = tag("->")(input)?; 47 | let (_input, rest) = till_end(input)?; 48 | let (input, dialogue) = statement_dialogue(rest)?; 49 | let choice = Statements::ChoiceBranch(Branch { 50 | statements: vec![dialogue], 51 | ..Default::default() 52 | }); 53 | Ok((input, choice)) 54 | //Ok(( input, (dialogue, indentations.len()) )) 55 | } 56 | 57 | // TODO, replace parse_params with an EXPRESSION 58 | pub fn if_start(input: &str) -> IResult<&str, &str> { 59 | let (input, inside_if) = delimited(spacey(tag("<>"), tag(">>"))(input)?; 60 | println!("inside_if {} ", inside_if); 61 | 62 | /*let (input , _) = tag("<>")(input)?; 64 | let (input, expression) = parse_params(input)?; 65 | let (input , _) = tag(" >>")(input)?;*/ 66 | Ok((input, inside_if)) 67 | } 68 | 69 | pub fn yarn_conditionals(input: &str) -> IResult<&str, Vec<&str>> { 70 | // FIXME: delimited here is wrong, we do not want to discard the content of if_start 71 | let (input, params) = 72 | delimited(spacey(if_start), take_until("<>"), tag("<>"))(input)?; 73 | println!("conditional body: {}", params); 74 | // let (input, params) = parse_params(params)?; 75 | Ok((input, vec![params])) 76 | } 77 | 78 | /// node tags are only allowed at the END of a line 79 | pub fn node_tags(input: &str) -> IResult<&str, &str> { 80 | // let (input, _) = tag("#")(input)?; 81 | let (input, aa) = take_until("#")(input)?; // separated_list0(tag("#"), identifier)(input) ?; 82 | println!("bla bla {:?}", aa); 83 | let (input, tag) = take_until(" ")(input)?; 84 | println!("tag result {:?}", tag); 85 | Ok((input, tag)) 86 | } 87 | 88 | pub fn till_end(input: &str) -> IResult<&str, &str> { 89 | terminated(not_line_ending, alt((tag("\n"), eof)))(input) //take_until("\n")(input)?; 90 | } 91 | 92 | pub fn rest(input: &str) -> IResult<&str, &str> { 93 | // CAREFULL ! swallows the whole input 94 | Ok(("", input)) 95 | } 96 | 97 | /// a bit more complex 98 | /// [wave size=2]Wavy![/wave] size=2 is an expression, an assignment expression 99 | pub fn attributes(input: &str) -> IResult<&str, (String, Vec<&str>)> { 100 | // this is a special one, as we want to extract the tags, but keep the rest of the text 101 | let mut withouth_attributes: Vec<&str> = vec![]; 102 | let mut attributes: Vec<&str> = vec![]; 103 | let (input, before_attribute) = take_until("[")(input)?; 104 | withouth_attributes.push(before_attribute); 105 | 106 | let (input, attribute_name) = delimited(tag("["), identifier, tag("]"))(input)?; 107 | attributes.push(attribute_name); 108 | //println!("ATTRIBUTES start input {}, attribute_name {}", input, attribute_name); 109 | let (input, inside) = take_until("[/")(input)?; 110 | withouth_attributes.push(inside); 111 | 112 | let (input, _closing_attribute_name) = delimited(tag("[/"), identifier, tag("]"))(input)?; 113 | // println!("ATTRIBUTES end input {}, bla {}", input, closing_attribute_name); 114 | withouth_attributes.push(input); 115 | // TODO: detect un matching attribute names & throw an error ? 116 | println!( 117 | "text withouth attributes {:?}", 118 | withouth_attributes.join(" ") 119 | ); 120 | println!("attributes {:?}", attributes); 121 | 122 | let text_withouth_attributes = withouth_attributes.join(" "); 123 | 124 | Ok((input, (text_withouth_attributes, attributes))) 125 | } 126 | 127 | /// https://github.com/YarnSpinnerTool/YarnSpinner/blob/main/Documentation/Yarn-Spec.md#dialogue-statement 128 | /// we want to return the text BEFORE and AFTER the tagged part 129 | /// possible solutions: peek, success, consume 130 | /// alternate: line terminated((alt("{", "<<<", ) )) 131 | pub fn _interpolated_value(input: &str) -> IResult<&str, &str> { 132 | delimited(tag("{"), variable_identifier, tag("}"))(input) 133 | } 134 | 135 | pub fn statement_dialogue_who_what(input: &str) -> IResult<&str, Statements> { 136 | let (input, (who, _, what)) = 137 | tuple((spacey(identifier), spacey(tag(":")), alt((till_end, rest))))(input)?; 138 | let result = Statements::Dialogue(Dialogue { 139 | who: who.to_string(), 140 | what: what.to_string(), 141 | ..Default::default() 142 | }); 143 | Ok((input, result)) 144 | } 145 | 146 | pub fn statement_dialogue_what(input: &str) -> IResult<&str, Statements> { 147 | let (input, what) = till_end(input)?; 148 | let result = Statements::Dialogue(Dialogue { 149 | who: "nobody".to_string(), 150 | what: what.trim_start().to_string(), 151 | ..Default::default() 152 | }); 153 | Ok((input, result)) 154 | } 155 | 156 | // (identifier :) (optional) text \n 157 | pub fn statement_dialogue(input: &str) -> IResult<&str, Statements> { 158 | let (input, result) = alt((statement_dialogue_who_what, statement_dialogue_what))(input)?; 159 | // here we have the who + what combo, so we can extract special character like tags etc 160 | Ok((input, result)) 161 | } 162 | 163 | // fixme: not sure 164 | fn statement_empty_line(input: &str) -> IResult<&str, Statements> { 165 | let (input, _) = empty_line(input)?; 166 | Ok((input, Statements::Empty)) 167 | } 168 | 169 | pub fn hashtags(input: &str) -> IResult<&str, Vec<&str>> { 170 | many0(spacey(tag_identifier))(input) 171 | } 172 | 173 | pub fn get_indentation(input: &str) -> IResult<&str, usize> { 174 | let mut identation = 0; 175 | // FIXME: damn, whitespace counting needs to include the choice's -> 176 | 177 | if input.contains("->") { 178 | let (_, (pre_space, tag, post_space, _)) = 179 | tuple((space0, tag("->"), space0, not_line_ending))(input)?; 180 | identation = pre_space.len() + tag.len() + post_space.len(); 181 | } else { 182 | let (_, (white_spaces, _rest_of_line)) = tuple((space0, not_line_ending))(input)?; 183 | identation = white_spaces.len(); 184 | } 185 | 186 | Ok(("", identation)) 187 | } 188 | 189 | // see https://github.com/YarnSpinnerTool/YarnSpinner/blob/040a2436d98e5c0cc72e6a8bc04e6c3fa156399d/Documentation/Yarn-Spec.md#body 190 | pub fn statement_base_line(input: &str) -> IResult<&str, (&str, Vec<&str>, usize)> { 191 | let (input, content) = terminated(not_line_ending, alt((tag("#"), tag("\n"))))(input)?; 192 | //extract white spaces/indentation 193 | let (_, indentation) = get_indentation(content)?; 194 | let (hashtags_raw, con) = opt(take_until("#"))(content)?; 195 | let mut tags: Vec<&str> = vec![]; 196 | let mut result = content; // FIXME this whole thing is terrible 197 | if let Some(c) = con { 198 | if let Ok(_tags) = hashtags(hashtags_raw) { 199 | tags = _tags.1; 200 | } 201 | result = c; 202 | } 203 | Ok((input, (result, tags, indentation))) 204 | } 205 | 206 | // see https://github.com/YarnSpinnerTool/YarnSpinner/blob/040a2436d98e5c0cc72e6a8bc04e6c3fa156399d/Documentation/Yarn-Spec.md#body 207 | // returns a Vec<(content, Vec) 208 | // ie each line with its tags 209 | pub fn statement_base(input: &str) -> IResult<&str, Vec<(&str, Vec<&str>, usize)>> { 210 | many1(statement_base_line)(input) 211 | } 212 | 213 | /* 214 | Alternative impl 215 | do not directly add statements, go through an itermediary data structure 216 | - clear (blank_line /eof) => pops all items until it reaches 0 217 | - add (indentlevel(ie choice stack level), statement )*/ 218 | enum HelperOps { 219 | // Add((usize, Statements)), 220 | Clear, 221 | None, 222 | } 223 | 224 | // TODO: do not just pop arbitrarly on < identation 225 | // keep track of indentation levels !!! 226 | 227 | /// wraps all the rest 228 | pub fn body(input: &str) -> IResult<&str, Branch> { 229 | let (input, lines) = statement_base(input)?; // TODO: use nom's map ? 230 | 231 | let mut root_branch: Branch = Branch { 232 | statements: vec![], 233 | ..Default::default() 234 | }; // this is the root branch after the end of the parsing 235 | let mut choices_stack: Vec = vec![]; 236 | //let mut current_branch:& mut Branch; 237 | // current_branch = &mut root_branch; 238 | 239 | // remember choice "groups" are delimited by : 240 | // - empty white line 241 | // - a different indentation 242 | let mut previous_indentation: usize = 0; 243 | let mut previous_choice_indentation: usize = 0; 244 | let mut helper = HelperOps::None; 245 | 246 | for (index, (line, tags, indentation)) in lines.iter().enumerate() { 247 | // the order of these is important !! 248 | let (_, statement) = alt(( 249 | statement_empty_line, 250 | statement_command, 251 | statement_choice, 252 | statement_dialogue_who_what, 253 | statement_dialogue_what, 254 | ))(line)?; 255 | let _tags: Vec = tags.clone().iter().map(|x| x.to_string()).collect(); // TODO 256 | let indentation = indentation.clone(); 257 | 258 | match statement.clone() { 259 | Statements::ChoiceBranch(branch) => { 260 | // IF non nested, the branch is on the same level as previous branches 261 | if indentation > previous_choice_indentation { 262 | // println!("higher level, we need to nest !"); 263 | choices_stack.push(Choice { 264 | branches: vec![branch], 265 | ..Default::default() 266 | }); 267 | } else if indentation == previous_choice_indentation { 268 | // println!("same level , add another branch"); 269 | // push the previous choice branch to the list of branches in the choice 270 | choices_stack 271 | .last_mut() 272 | .expect("we should always have one item in the stack here") 273 | .branches 274 | .push(branch); 275 | } else { 276 | println!("lower level leave this branch"); 277 | let choice = choices_stack.pop().unwrap(); 278 | 279 | // FIXME: always the same stuff, of different "current branch" if we are at root level or nested in a Choice 280 | if choices_stack.len() > 0 { 281 | choices_stack 282 | .last_mut() 283 | .unwrap() 284 | .branches 285 | .last_mut() 286 | .unwrap() 287 | .statements 288 | .push(Statements::Choice(choice)); 289 | choices_stack.last_mut().unwrap().branches.push(branch); 290 | } else { 291 | root_branch.statements.push(Statements::Choice(choice)); 292 | } 293 | } 294 | previous_choice_indentation = indentation.clone(); 295 | } 296 | Statements::Empty => { 297 | // println!("blank line"); 298 | // Empty lines reset the whole stack to 0: ie we should pop everything one by one to close off the options 299 | // FIXME: how to dedupe this ? closures do not work , external functions do not work 300 | helper = HelperOps::Clear; 301 | } 302 | _ => { 303 | // we push everything else to the current branch 304 | if indentation < previous_indentation { 305 | previous_choice_indentation = indentation; 306 | if choices_stack.len() > 0 { 307 | let choice = choices_stack.pop().unwrap(); 308 | if choices_stack.len() > 0 { 309 | choices_stack 310 | .last_mut() 311 | .unwrap() 312 | .branches 313 | .last_mut() 314 | .unwrap() 315 | .statements 316 | .push(Statements::Choice(choice)); 317 | } else { 318 | root_branch.statements.push(Statements::Choice(choice)); 319 | } 320 | } 321 | } 322 | if choices_stack.len() > 0 { 323 | choices_stack 324 | .last_mut() 325 | .unwrap() 326 | .branches 327 | .last_mut() 328 | .unwrap() 329 | .statements 330 | .push(statement); 331 | } else { 332 | root_branch.statements.push(statement); 333 | } 334 | } 335 | } 336 | 337 | // generic handling, outside of specific cases 338 | if indentation < previous_indentation { 339 | //println!("lower level leave this branch"); 340 | } 341 | previous_indentation = indentation.clone(); 342 | 343 | if index == lines.len() - 1 { 344 | helper = HelperOps::Clear; 345 | } 346 | 347 | match helper { 348 | // we still have items on the stack after finishing everything, so close them off until there is none left 349 | HelperOps::Clear => { 350 | if choices_stack.len() > 0 { 351 | // println!("remaining {}, poping", choices_stack.len()); 352 | // FIXME: horrible implementation 353 | let mut child = choices_stack.pop().unwrap(); 354 | if choices_stack.len() > 0 { 355 | while let Some(mut parent_choice) = choices_stack.pop() { 356 | if choices_stack.len() > 0 { 357 | parent_choice 358 | .branches 359 | .last_mut() 360 | .unwrap() 361 | .statements 362 | .push(Statements::Choice(child.clone())); 363 | child = parent_choice; 364 | } else { 365 | parent_choice 366 | .branches 367 | .last_mut() 368 | .unwrap() 369 | .statements 370 | .push(Statements::Choice(child.clone())); 371 | root_branch 372 | .statements 373 | .push(Statements::Choice(parent_choice.clone())); 374 | } 375 | } 376 | } else { 377 | root_branch 378 | .statements 379 | .push(Statements::Choice(child.clone())); 380 | } 381 | } 382 | } 383 | _ => {} 384 | } 385 | } 386 | // lines done 387 | // here root_branch should be the root branch 388 | root_branch.statements.push(Statements::Exit); 389 | Ok((input, root_branch)) 390 | } 391 | 392 | pub fn display_dialogue_tree(branch: &Branch, indentation_level: usize) { 393 | let identation_pattern = " "; 394 | let identation = format!(" {}", identation_pattern.repeat(indentation_level)); 395 | for statement in branch.statements.iter() { 396 | match statement { 397 | Statements::Choice(choice) => { 398 | println!( 399 | "{}statement choices ({}): tags:{:?}", 400 | identation, 401 | choice.branches.len(), 402 | choice.tags 403 | ); 404 | for branch in choice.branches.iter() { 405 | println!("{}Branch:", identation); 406 | display_dialogue_tree(branch, indentation_level + 3); 407 | } 408 | } 409 | _ => { 410 | println!("{}statement {:?}", identation, statement); 411 | } 412 | } 413 | } 414 | } 415 | 416 | // should be empty line OR eof 417 | fn empty_line(input: &str) -> IResult<&str, &str> { 418 | recognize(many_till(space0, alt((tag("\n"), eof))))(input) 419 | } 420 | -------------------------------------------------------------------------------- /tests/runner_test.rs: -------------------------------------------------------------------------------- 1 | use bevy_mod_yarn::prelude::{ 2 | parse_yarn_nodes, Branch, Choice, Commands, Dialogue, DialogueRunner, Statements, YarnAsset, 3 | YarnCommand, 4 | }; 5 | 6 | #[test] 7 | fn test_evaluate_minimal() { 8 | let micro = "title: Test_node 9 | --- 10 | Dona: what is wrong ? 11 | Grumpy: ... 12 | === 13 | "; 14 | 15 | let parsed = parse_yarn_nodes(micro); 16 | let yarn_asset = YarnAsset { 17 | raw: micro.into(), 18 | nodes: parsed, 19 | }; 20 | 21 | let mut dialogue_runner = DialogueRunner::new(yarn_asset, "Test_node".into()); //{ current_node: "Test_node".into(), ..Default::default() }; 22 | // dialogue_runner.set_current_branch(yarn_asset); 23 | 24 | let current_statement = dialogue_runner.current_statement(); 25 | let expected = Statements::Dialogue(Dialogue { 26 | who: "Dona".into(), 27 | what: "what is wrong ?".into(), 28 | ..Default::default() 29 | }); 30 | assert_eq!(current_statement, expected); 31 | 32 | // go to next entry 33 | dialogue_runner.next_entry(); 34 | let current_statement = dialogue_runner.current_statement(); 35 | let expected = Statements::Dialogue(Dialogue { 36 | who: "Grumpy".into(), 37 | what: "...".into(), 38 | ..Default::default() 39 | }); 40 | assert_eq!(current_statement, expected); 41 | 42 | dialogue_runner.next_entry(); 43 | let current_statement = dialogue_runner.current_statement(); 44 | let expected = Statements::Exit; 45 | assert_eq!(current_statement, expected); 46 | } 47 | 48 | #[test] 49 | fn test_evaluate_branching_basic() { 50 | let choices = "title: Test_node 51 | --- 52 | it was a beautiful day , said nobody 53 | Lamik: hi ! 54 | Dona: good morning , how are you ? 55 | -> Lamik: are you asking me ? 56 | Dona: yes 57 | -> Lamik: fine ! 58 | Dona: good to hear 59 | 60 | === 61 | "; 62 | 63 | let parsed = parse_yarn_nodes(choices); 64 | let yarn_asset = YarnAsset { 65 | raw: choices.into(), 66 | nodes: parsed, 67 | }; 68 | 69 | let mut dialogue_runner = DialogueRunner::new(yarn_asset, "Test_node".into()); //{ current_node: "Test_node".into(), ..Default::default() }; 70 | // dialogue_runner.set_current_branch(yarn_asset); 71 | 72 | let current_statement = dialogue_runner.current_statement(); 73 | let expected = Statements::Dialogue(Dialogue { 74 | who: "nobody".into(), 75 | what: "it was a beautiful day , said nobody".into(), 76 | ..Default::default() 77 | }); 78 | assert_eq!(current_statement, expected); 79 | 80 | dialogue_runner.next_entry(); 81 | let current_statement = dialogue_runner.current_statement(); 82 | let expected = Statements::Dialogue(Dialogue { 83 | who: "Lamik".into(), 84 | what: "hi !".into(), 85 | ..Default::default() 86 | }); 87 | assert_eq!(current_statement, expected); 88 | 89 | dialogue_runner.next_entry(); 90 | let current_statement = dialogue_runner.current_statement(); 91 | let expected = Statements::Dialogue(Dialogue { 92 | who: "Dona".into(), 93 | what: "good morning , how are you ?".into(), 94 | ..Default::default() 95 | }); 96 | assert_eq!(current_statement, expected); 97 | 98 | dialogue_runner.next_entry(); 99 | let current_statement = dialogue_runner.current_statement(); 100 | let expected = Statements::Choice(Choice { 101 | branches: vec![ 102 | Branch { 103 | statements: vec![ 104 | Statements::Dialogue(Dialogue { 105 | who: "Lamik".into(), 106 | what: "are you asking me ?".into(), 107 | ..Default::default() 108 | }), 109 | Statements::Dialogue(Dialogue { 110 | who: "Dona".into(), 111 | what: "yes".into(), 112 | ..Default::default() 113 | }), 114 | ], 115 | }, 116 | Branch { 117 | statements: vec![ 118 | Statements::Dialogue(Dialogue { 119 | who: "Lamik".into(), 120 | what: "fine !".into(), 121 | ..Default::default() 122 | }), 123 | Statements::Dialogue(Dialogue { 124 | who: "Dona".into(), 125 | what: "good to hear".into(), 126 | ..Default::default() 127 | }), 128 | ], 129 | }, 130 | ], 131 | ..Default::default() 132 | }); 133 | assert_eq!(current_statement, expected); 134 | 135 | // we check our choices helper 136 | let (current_choices, _choice_index) = dialogue_runner.get_current_choices(); 137 | let expected = vec![ 138 | Dialogue { 139 | who: "Lamik".into(), 140 | what: "are you asking me ?".into(), 141 | ..Default::default() 142 | }, 143 | Dialogue { 144 | who: "Lamik".into(), 145 | what: "fine !".into(), 146 | ..Default::default() 147 | }, 148 | ]; 149 | assert_eq!(current_choices, expected); 150 | 151 | // choose the other choice 152 | dialogue_runner.next_choice(); 153 | dialogue_runner.next_entry(); // FIXME: still not sure about this way of validating choices 154 | let current_statement = dialogue_runner.current_statement(); 155 | let expected = Statements::Dialogue(Dialogue { 156 | who: "Lamik".into(), 157 | what: "fine !".into(), 158 | ..Default::default() 159 | }); 160 | assert_eq!(current_statement, expected); 161 | 162 | dialogue_runner.next_entry(); 163 | let current_statement = dialogue_runner.current_statement(); 164 | let expected = Statements::Dialogue(Dialogue { 165 | who: "Dona".into(), 166 | what: "good to hear".into(), 167 | ..Default::default() 168 | }); 169 | assert_eq!(current_statement, expected); 170 | 171 | dialogue_runner.next_entry(); 172 | let current_statement = dialogue_runner.current_statement(); 173 | let expected = Statements::Exit; 174 | assert_eq!(current_statement, expected); 175 | } 176 | 177 | #[test] 178 | fn test_evaluate_branching_nested_multinode() { 179 | let choices = "title: Test_node 180 | --- 181 | it was a beautiful day , said nobody 182 | Lamik: hi ! 183 | Dona: good morning , how are you ? 184 | -> Lamik: are you asking me ? 185 | Dona: yes 186 | -> Lamik: fine ! 187 | Dona: good to hear 188 | Dona: so... what have you been up to ? 189 | -> Lamik: hmmm... 190 | -> Lamik: i have started working on the most AMAZING project ever ! 191 | Dona: ohh cool , tell me more !! 192 | -> Lamik: short version then 193 | Lamik: a mechanical bunny of doom ! 194 | -> Lamik: the long version ? here goes 195 | <> 196 | -> Lamik: too early to tell 197 | Dona: oh ok, well, anyway, gotta go ! 198 | Lamik: ok, bye ! 199 | Dona: see you soon ! 200 | === 201 | title: project_long 202 | --- 203 | Lamik: so as I was saying, a mechanical bunny of doom ! 204 | Lamik: ... with floppy ears of course 205 | === 206 | "; 207 | 208 | let parsed = parse_yarn_nodes(choices); 209 | let yarn_asset = YarnAsset { 210 | raw: choices.into(), 211 | nodes: parsed, 212 | }; 213 | 214 | let mut dialogue_runner = DialogueRunner::new(yarn_asset, "Test_node".into()); //{ current_node: "Test_node".into(), ..Default::default() }; 215 | // dialogue_runner.set_current_branch(yarn_asset); 216 | 217 | let current_statement = dialogue_runner.current_statement(); 218 | let expected = Statements::Dialogue(Dialogue { 219 | who: "nobody".into(), 220 | what: "it was a beautiful day , said nobody".into(), 221 | ..Default::default() 222 | }); 223 | assert_eq!(current_statement, expected); 224 | 225 | dialogue_runner.next_entry(); 226 | let current_statement = dialogue_runner.current_statement(); 227 | let expected = Statements::Dialogue(Dialogue { 228 | who: "Lamik".into(), 229 | what: "hi !".into(), 230 | ..Default::default() 231 | }); 232 | assert_eq!(current_statement, expected); 233 | 234 | dialogue_runner.next_entry(); 235 | let current_statement = dialogue_runner.current_statement(); 236 | let expected = Statements::Dialogue(Dialogue { 237 | who: "Dona".into(), 238 | what: "good morning , how are you ?".into(), 239 | ..Default::default() 240 | }); 241 | assert_eq!(current_statement, expected); 242 | 243 | dialogue_runner.next_entry(); 244 | let current_statement = dialogue_runner.current_statement(); 245 | let expected = Statements::Choice(Choice { 246 | branches: vec![ 247 | Branch { 248 | statements: vec![ 249 | Statements::Dialogue(Dialogue { 250 | who: "Lamik".into(), 251 | what: "are you asking me ?".into(), 252 | ..Default::default() 253 | }), 254 | Statements::Dialogue(Dialogue { 255 | who: "Dona".into(), 256 | what: "yes".into(), 257 | ..Default::default() 258 | }), 259 | ], 260 | }, 261 | Branch { 262 | statements: vec![ 263 | Statements::Dialogue(Dialogue { 264 | who: "Lamik".into(), 265 | what: "fine !".into(), 266 | ..Default::default() 267 | }), 268 | Statements::Dialogue(Dialogue { 269 | who: "Dona".into(), 270 | what: "good to hear".into(), 271 | ..Default::default() 272 | }), 273 | ], 274 | }, 275 | ], 276 | ..Default::default() 277 | }); 278 | assert_eq!(current_statement, expected); 279 | 280 | // we check our choices helper 281 | let (current_choices, _choice_index) = dialogue_runner.get_current_choices(); 282 | let expected = vec![ 283 | Dialogue { 284 | who: "Lamik".into(), 285 | what: "are you asking me ?".into(), 286 | ..Default::default() 287 | }, 288 | Dialogue { 289 | who: "Lamik".into(), 290 | what: "fine !".into(), 291 | ..Default::default() 292 | }, 293 | ]; 294 | assert_eq!(current_choices, expected); 295 | 296 | // choose the other choice 297 | dialogue_runner.next_choice(); 298 | dialogue_runner.next_entry(); // FIXME: still not sure about this way of validating choices 299 | let current_statement = dialogue_runner.current_statement(); 300 | let expected = Statements::Dialogue(Dialogue { 301 | who: "Lamik".into(), 302 | what: "fine !".into(), 303 | ..Default::default() 304 | }); 305 | assert_eq!(current_statement, expected); 306 | 307 | dialogue_runner.next_entry(); 308 | let current_statement = dialogue_runner.current_statement(); 309 | let expected = Statements::Dialogue(Dialogue { 310 | who: "Dona".into(), 311 | what: "good to hear".into(), 312 | ..Default::default() 313 | }); 314 | assert_eq!(current_statement, expected); 315 | 316 | dialogue_runner.next_entry(); 317 | let current_statement = dialogue_runner.current_statement(); 318 | let expected = Statements::Dialogue(Dialogue { 319 | who: "Dona".into(), 320 | what: "so... what have you been up to ?".into(), 321 | ..Default::default() 322 | }); 323 | assert_eq!(current_statement, expected); 324 | 325 | dialogue_runner.next_entry(); 326 | let current_statement = dialogue_runner.current_statement(); 327 | let expected = Statements::Choice(Choice { 328 | branches: vec![ 329 | Branch { 330 | statements: vec![Statements::Dialogue(Dialogue { 331 | who: "Lamik".into(), 332 | what: "hmmm...".into(), 333 | ..Default::default() 334 | })], 335 | }, 336 | Branch { 337 | statements: vec![ 338 | Statements::Dialogue(Dialogue { 339 | who: "Lamik".into(), 340 | what: "i have started working on the most AMAZING project ever !".into(), 341 | ..Default::default() 342 | }), 343 | Statements::Dialogue(Dialogue { 344 | who: "Dona".into(), 345 | what: "ohh cool , tell me more !!".into(), 346 | ..Default::default() 347 | }), 348 | Statements::Choice(Choice { 349 | branches: vec![ 350 | Branch { 351 | statements: vec![ 352 | Statements::Dialogue(Dialogue { 353 | who: "Lamik".into(), 354 | what: "short version then".into(), 355 | ..Default::default() 356 | }), 357 | Statements::Dialogue(Dialogue { 358 | who: "Lamik".into(), 359 | what: "a mechanical bunny of doom !".into(), 360 | ..Default::default() 361 | }), 362 | ], 363 | }, 364 | Branch { 365 | statements: vec![ 366 | Statements::Dialogue(Dialogue { 367 | who: "Lamik".into(), 368 | what: "the long version ? here goes".into(), 369 | ..Default::default() 370 | }), 371 | Statements::Command(YarnCommand { 372 | name: "jump".into(), 373 | params: "project_long".to_string(), 374 | command_type: Commands::Jump, 375 | ..Default::default() 376 | }), 377 | ], 378 | }, 379 | ], 380 | ..Default::default() 381 | }), 382 | ], 383 | }, 384 | Branch { 385 | statements: vec![ 386 | Statements::Dialogue(Dialogue { 387 | who: "Lamik".into(), 388 | what: "too early to tell".into(), 389 | ..Default::default() 390 | }), 391 | Statements::Dialogue(Dialogue { 392 | who: "Dona".into(), 393 | what: "oh ok, well, anyway, gotta go !".into(), 394 | ..Default::default() 395 | }), 396 | ], 397 | }, 398 | ], 399 | ..Default::default() 400 | }); 401 | assert_eq!(current_statement, expected); 402 | 403 | // we check our choices helper 404 | let (current_choices, _choice_index) = dialogue_runner.get_current_choices(); 405 | let expected = vec![ 406 | Dialogue { 407 | who: "Lamik".into(), 408 | what: "hmmm...".into(), 409 | ..Default::default() 410 | }, 411 | Dialogue { 412 | who: "Lamik".into(), 413 | what: "i have started working on the most AMAZING project ever !".into(), 414 | ..Default::default() 415 | }, 416 | Dialogue { 417 | who: "Lamik".into(), 418 | what: "too early to tell".into(), 419 | ..Default::default() 420 | }, 421 | ]; 422 | assert_eq!(current_choices, expected); 423 | 424 | // choose a specific choice (by index) 425 | dialogue_runner.specific_choice(1); 426 | 427 | dialogue_runner.next_entry(); 428 | let current_statement = dialogue_runner.current_statement(); 429 | let expected = Statements::Dialogue(Dialogue { 430 | who: "Lamik".into(), 431 | what: "i have started working on the most AMAZING project ever !".into(), 432 | ..Default::default() 433 | }); 434 | assert_eq!(current_statement, expected); 435 | 436 | dialogue_runner.next_entry(); 437 | let current_statement = dialogue_runner.current_statement(); 438 | let expected = Statements::Dialogue(Dialogue { 439 | who: "Dona".into(), 440 | what: "ohh cool , tell me more !!".into(), 441 | ..Default::default() 442 | }); 443 | assert_eq!(current_statement, expected); 444 | 445 | dialogue_runner.next_entry(); 446 | let current_statement = dialogue_runner.current_statement(); 447 | let expected = Statements::Choice(Choice { 448 | branches: vec![ 449 | Branch { 450 | statements: vec![ 451 | Statements::Dialogue(Dialogue { 452 | who: "Lamik".into(), 453 | what: "short version then".into(), 454 | ..Default::default() 455 | }), 456 | Statements::Dialogue(Dialogue { 457 | who: "Lamik".into(), 458 | what: "a mechanical bunny of doom !".into(), 459 | ..Default::default() 460 | }), 461 | ], 462 | }, 463 | Branch { 464 | statements: vec![ 465 | Statements::Dialogue(Dialogue { 466 | who: "Lamik".into(), 467 | what: "the long version ? here goes".into(), 468 | ..Default::default() 469 | }), 470 | Statements::Command(YarnCommand { 471 | name: "jump".into(), 472 | params: "project_long".to_string(), 473 | command_type: Commands::Jump, 474 | ..Default::default() 475 | }), 476 | ], 477 | }, 478 | ], 479 | ..Default::default() 480 | }); 481 | assert_eq!(current_statement, expected); 482 | 483 | // we check our choices helper 484 | let (current_choices, _choice_index) = dialogue_runner.get_current_choices(); 485 | let expected = vec![ 486 | Dialogue { 487 | who: "Lamik".into(), 488 | what: "short version then".into(), 489 | ..Default::default() 490 | }, 491 | Dialogue { 492 | who: "Lamik".into(), 493 | what: "the long version ? here goes".into(), 494 | ..Default::default() 495 | }, 496 | ]; 497 | assert_eq!(current_choices, expected); 498 | 499 | // choose a specific choice (by index) 500 | dialogue_runner.specific_choice(1); 501 | 502 | // validate choice 503 | dialogue_runner.next_entry(); 504 | let current_statement = dialogue_runner.current_statement(); 505 | let expected = Statements::Dialogue(Dialogue { 506 | who: "Lamik".into(), 507 | what: "the long version ? here goes".into(), 508 | ..Default::default() 509 | }); 510 | assert_eq!(current_statement, expected); 511 | 512 | dialogue_runner.next_entry(); 513 | let current_statement = dialogue_runner.current_statement(); 514 | let expected = Statements::Dialogue(Dialogue { 515 | who: "Lamik".into(), 516 | what: "so as I was saying, a mechanical bunny of doom ! ".into(), 517 | ..Default::default() 518 | }); 519 | assert_eq!(current_statement, expected); 520 | 521 | dialogue_runner.next_entry(); 522 | let current_statement = dialogue_runner.current_statement(); 523 | let expected = Statements::Dialogue(Dialogue { 524 | who: "Lamik".into(), 525 | what: "... with floppy ears of course".into(), 526 | ..Default::default() 527 | }); 528 | assert_eq!(current_statement, expected); 529 | 530 | dialogue_runner.next_entry(); 531 | let current_statement = dialogue_runner.current_statement(); 532 | let expected = Statements::Exit; 533 | assert_eq!(current_statement, expected); 534 | } 535 | --------------------------------------------------------------------------------