diff --git a/crates/bevy_plugin/src/development_file_generation.rs b/crates/bevy_plugin/src/development_file_generation.rs index d900fe62..21315af3 100644 --- a/crates/bevy_plugin/src/development_file_generation.rs +++ b/crates/bevy_plugin/src/development_file_generation.rs @@ -5,14 +5,14 @@ pub(crate) fn development_file_generation_plugin(app: &mut App) { app.register_type::(); } -/// The kind of development experience you wish when creating yarn files and dealing with missing localizations. +/// The kind of development experience you wish when creating Yarn files and dealing with missing localizations. /// Defaults to [`DevelopmentFileGeneration::TRY_FULL`] in debug builds, [`DevelopmentFileGeneration::None`] otherwise. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[reflect(Debug, Default, PartialEq, Hash, Serialize, Deserialize)] #[non_exhaustive] pub enum DevelopmentFileGeneration { /// The recommended setting for a development environment: - /// - Generates line IDs for all lines in loaded yarn files and writes them back to disk. + /// - Generates line IDs for all lines in loaded Yarn files and writes them back to disk. /// - Generates new strings files for all languages that are missing them, filling them with the lines found in the Yarn files. /// - Adds new lines to strings files when they have been added to a loaded Yarn file. /// - Marks lines in strings files that have been changed since they were translated by appending "NEEDS UPDATE" to the respective line texts. diff --git a/crates/bevy_plugin/src/dialogue_runner.rs b/crates/bevy_plugin/src/dialogue_runner.rs index 36a686b7..c170c69b 100644 --- a/crates/bevy_plugin/src/dialogue_runner.rs +++ b/crates/bevy_plugin/src/dialogue_runner.rs @@ -320,12 +320,13 @@ impl DialogueRunner { self.text_provider.as_ref() } - /// Returns the registered [`AssetProvider`] for the given type. + /// Returns the registered [`AssetProvider`] of the given type if it was previously registered with [`DialogueRunnerBuilder::add_asset_provider`]. #[must_use] - pub fn asset_provider(&self) -> Option<&dyn AssetProvider> { + pub fn asset_provider(&self) -> Option<&T> { self.asset_providers - .get(&TypeId::of::()) - .map(|p| p.as_ref()) + .values() + .filter_map(|p| p.as_any().downcast_ref()) + .next() } /// Iterates over all registered [`AssetProvider`]s. diff --git a/crates/bevy_plugin/src/dialogue_runner/builder.rs b/crates/bevy_plugin/src/dialogue_runner/builder.rs index 2b5963f4..cb69b6e3 100644 --- a/crates/bevy_plugin/src/dialogue_runner/builder.rs +++ b/crates/bevy_plugin/src/dialogue_runner/builder.rs @@ -1,4 +1,4 @@ -use crate::default_impl::{MemoryVariableStore, StringsFileTextProvider}; +use crate::default_impl::{MemoryVariableStorage, StringsFileTextProvider}; use crate::line_provider::SharedTextProvider; use crate::prelude::*; use bevy::prelude::*; @@ -41,7 +41,7 @@ impl DialogueRunnerBuilder { #[must_use] pub(crate) fn from_yarn_project(yarn_project: &YarnProject) -> Self { Self { - variable_storage: Box::new(MemoryVariableStore::new()), + variable_storage: Box::new(MemoryVariableStorage::new()), text_provider: SharedTextProvider::new(StringsFileTextProvider::from_yarn_project( yarn_project, )), @@ -54,7 +54,7 @@ impl DialogueRunnerBuilder { } } - /// Replaces the [`VariableStorage`] used by the [`DialogueRunner`]. By default, this is a [`MemoryVariableStore`]. + /// Replaces the [`VariableStorage`] used by the [`DialogueRunner`]. By default, this is a [`MemoryVariableStorage`]. #[must_use] pub fn with_variable_storage(mut self, storage: Box) -> Self { self.variable_storage = storage; diff --git a/crates/bevy_plugin/src/lib.rs b/crates/bevy_plugin/src/lib.rs index 49dd2be8..f21273ab 100644 --- a/crates/bevy_plugin/src/lib.rs +++ b/crates/bevy_plugin/src/lib.rs @@ -53,29 +53,34 @@ //! //! ```no_run //! // src/main.rs -//! use bevy::prelude::*; +//! use bevy::{prelude::*, asset::ChangeWatcher, utils::Duration}; //! use bevy_yarn_slinger::prelude::*; //! // Use the example dialogue view to see the dialogue in action. Requires the `bevy_yarn_slinger_example_dialogue_view` crate. //! // use bevy_yarn_slinger_example_dialogue_view::prelude::*; //! //! fn main() { //! let mut app = App::new(); -//! app.add_plugins(DefaultPlugins) -//! // Register the Yarn Slinger plugin using its default settings, which will look for Yarn files in the "dialogue" folder -//! // If this app should support Wasm or Android, we cannot load files without specifying them, so use the following instead. -//! // .add_plugins(YarnSlingerPlugin::with_yarn_source(YarnFileSource::file("dialogue/hello_world.yarn"))) -//! .add_plugins(YarnSlingerPlugin::new()) +//! app.add_plugins(( +//! DefaultPlugins.set(AssetPlugin { +//! // Activate hot reloading +//! watch_for_changes: ChangeWatcher::with_delay(Duration::from_millis(200)), +//! ..default() +//! }), +//! // Add the Yarn Slinger plugin. +//! // As soon as this plugin is built, a Yarn project will be compiled +//! // from all Yarn files found under assets/dialog/*.yarn +//! YarnSlingerPlugin::new(), //! // Initialize the bundled example UI. Requires the `bevy_yarn_slinger_example_dialogue_view` crate. -//! // .add_plugins(ExampleYarnSlingerDialogueViewPlugin::new()) -//! .add_systems( -//! Update, -//! ( -//! setup_camera.on_startup(), -//! // Spawn dialogue runner once the Yarn project has finished compiling -//! spawn_dialogue_runner.run_if(resource_added::()), -//! ) -//! ) -//! .run(); +//! // ExampleYarnSlingerDialogueViewPlugin::new(), +//! )) +//! // Setup a 2D camera so we can see the text +//! .add_systems(Startup, setup_camera) +//! // Spawn the dialog as soon as the Yarn project finished compiling +//! .add_systems( +//! Update, +//! spawn_dialogue_runner.run_if(resource_added::()), +//! ) +//! .run(); //! } //! //! fn setup_camera(mut commands: Commands) { @@ -83,9 +88,8 @@ //! } //! //! fn spawn_dialogue_runner(mut commands: Commands, project: Res) { -//! // Create a dialogue runner from the project //! let mut dialogue_runner = project.create_dialogue_runner(); -//! // Immediately show the dialogue to the player by starting at the "Start" node +//! // Start the dialog at the node with the title "Start" //! dialogue_runner.start_node("Start"); //! commands.spawn(dialogue_runner); //! } @@ -116,7 +120,7 @@ pub mod default_impl { pub use crate::line_provider::{ file_extensions, FileExtensionAssetProvider, StringsFileTextProvider, }; - pub use yarn_slinger::runtime::{MemoryVariableStore, StringTableTextProvider}; + pub use yarn_slinger::runtime::{MemoryVariableStorage, StringTableTextProvider}; } pub mod events { @@ -164,7 +168,7 @@ pub use yarn_slinger::prelude::{ }; pub mod deferred_loading { - //! Contains types needed for the deferred loading functionality, which is used when the list of yarn files is not immediately available at startup. + //! Contains types needed for the deferred loading functionality, which is used when the list of Yarn files is not immediately available at startup. pub use crate::plugin::DeferredYarnSlingerPlugin; pub use crate::project::LoadYarnProjectEvent; } diff --git a/crates/bevy_plugin/src/localization/strings_file/asset.rs b/crates/bevy_plugin/src/localization/strings_file/asset.rs index 4d21a19a..3c89e07a 100644 --- a/crates/bevy_plugin/src/localization/strings_file/asset.rs +++ b/crates/bevy_plugin/src/localization/strings_file/asset.rs @@ -110,10 +110,14 @@ impl StringsFile { // This record's text was not translated, so we can safely overwrite it with the new text other_record.text.clone() }; + let comment = combine_comments(&record.comment, &other_record.comment); changed = true; - *record = other_record; - record.text = text; + *record = StringsFileRecord { + text, + comment, + ..other_record + }; } else if single_yarn_file { removed_lines.push(id.clone()); changed = true; @@ -223,6 +227,27 @@ fn records_equal_except_for_text(lhs: &StringsFileRecord, rhs: &StringsFileRecor } const UPDATE_PREFIX: &str = "(NEEDS UPDATE) "; +fn combine_comments(full_old_comment: &str, new_metadata: &str) -> String { + let translator_comment = extract_translator_comment(full_old_comment); + let new_metadata = (!new_metadata.is_empty()).then_some(new_metadata); + [translator_comment, new_metadata] + .into_iter() + .filter_map(|s| s) + .collect::>() + .join(LINE_METADATA_PREFIX_SEPARATOR) +} + +fn extract_translator_comment(comment: &str) -> Option<&str> { + let mut split = comment.split(LINE_METADATA_PREFIX); + split + .next() + .filter(|s| !s.is_empty()) + .map(|s| s.trim_end_matches(LINE_METADATA_PREFIX_SEPARATOR)) +} + +const LINE_METADATA_PREFIX: &str = "Line metadata: "; +const LINE_METADATA_PREFIX_SEPARATOR: &str = ", "; + #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] pub(crate) struct StringsFileRecord { /// The language that the line is written in. @@ -248,11 +273,11 @@ pub(crate) struct StringsFileRecord { /// the line's text as it appeared in the base localization CSV file. /// /// When a new StringTableEntry is created in a localized CSV file for a - /// .yarn file, the Lock value is copied over from the base CSV file, + /// .Yarn file, the Lock value is copied over from the base CSV file, /// and used for the translated entry. /// /// Because the base localization CSV is regenerated every time the - /// .yarn file is imported, the base localization Lock value will change + /// .Yarn file is imported, the base localization Lock value will change /// if a line's text changes. This means that if the base lock and /// translated lock differ, the translated line is out of date, and /// needs to be updated. @@ -295,6 +320,91 @@ fn read_comments(metadata: impl IntoIterator) -> String { if cleaned_metadata.is_empty() { String::new() } else { - format!("Line metadata: {}", cleaned_metadata.join(" ")) + format!("{LINE_METADATA_PREFIX}{}", cleaned_metadata.join(" ")) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn combines_comments_without_change() { + let old = "Foo, Line metadata: Bar"; + let new = "Line metadata: Bar"; + let combined = combine_comments(old, new); + assert_eq!(old, &combined) + } + + #[test] + fn combines_comments_with_deletion() { + let old = "Foo, Line metadata: Bar"; + let new = ""; + let combined = combine_comments(old, new); + assert_eq!("Foo", &combined) + } + + #[test] + fn combines_comments_with_insertion() { + let old = "Foo, Line metadata: Bar"; + let new = "Line metadata: Bar, Baz"; + let combined = combine_comments(old, new); + assert_eq!("Foo, Line metadata: Bar, Baz", &combined) + } + + #[test] + fn combines_comments_with_change() { + let old = "Foo, Line metadata: Bar"; + let new = "Line metadata: Baz"; + let combined = combine_comments(old, new); + assert_eq!("Foo, Line metadata: Baz", &combined) + } + + #[test] + fn combines_comments_without_meta() { + let old = "Foo"; + let new = ""; + let combined = combine_comments(old, new); + assert_eq!(old, &combined) + } + + #[test] + fn combines_comments_with_new_meta() { + let old = "Foo"; + let new = "Line metadata: Bar"; + let combined = combine_comments(old, new); + assert_eq!("Foo, Line metadata: Bar", &combined) + } + + #[test] + fn combines_comments_with_only_same_meta() { + let old = "Line metadata: Bar"; + let new = "Line metadata: Bar"; + let combined = combine_comments(old, new); + assert_eq!(old, &combined) + } + + #[test] + fn combines_empty_comments() { + let old = ""; + let new = ""; + let combined = combine_comments(old, new); + assert_eq!(old, &combined) + } + + #[test] + fn combines_comments_with_only_new_meta() { + let old = ""; + let new = "Line metadata: Bar"; + let combined = combine_comments(old, new); + assert_eq!(new, &combined) + } + + #[test] + fn combines_comments_with_only_changed_meta() { + let old = "Line metadata: Bar"; + let new = "Line metadata: Baz"; + let combined = combine_comments(old, new); + assert_eq!(new, &combined) } } diff --git a/crates/bevy_plugin/src/localization/strings_file/updating.rs b/crates/bevy_plugin/src/localization/strings_file/updating.rs index d84435a8..6affb6e0 100644 --- a/crates/bevy_plugin/src/localization/strings_file/updating.rs +++ b/crates/bevy_plugin/src/localization/strings_file/updating.rs @@ -96,11 +96,11 @@ fn update_all_strings_files_for_string_table( Ok(new_strings_file) => new_strings_file, Err(e) => { if project.development_file_generation == DevelopmentFileGeneration::Full { - debug!("Updating \"{}\" soon (lang: {language}) because the following yarn files were changed or loaded but do not have full line IDs yet: {file_names}", + debug!("Updating \"{}\" soon (lang: {language}) because the following Yarn files were changed or loaded but do not have full line IDs yet: {file_names}", strings_file_path.display()) } else { error!( - "Tried to update \"{}\" (lang: {language}) because the following yarn files were changed or loaded: {file_names}, but couldn't because: {e}", + "Tried to update \"{}\" (lang: {language}) because the following Yarn files were changed or loaded: {file_names}, but couldn't because: {e}", strings_file_path.display(), ); } @@ -111,7 +111,7 @@ fn update_all_strings_files_for_string_table( dirty_paths.insert((strings_file_handle, strings_file_path)); info!( - "Updated \"{}\" (lang: {language}) because the following yarn files were changed or loaded: {file_names}", + "Updated \"{}\" (lang: {language}) because the following Yarn files were changed or loaded: {file_names}", strings_file_path.display(), ); } @@ -144,7 +144,7 @@ fn lint_strings_file( .map(|asset_path| format!("at {}", asset_path.path().display())) .unwrap_or_else(|| "created at runtime".to_owned()); warn!( - "Strings file {source} contains the following strings for yarn files were not found in the project: {superfluous_file_names}. \ + "Strings file {source} contains the following strings for Yarn files were not found in the project: {superfluous_file_names}. \ Either you forgot to add these files to the project or the strings belonged to files that were deleted. \ You may want to delete these entries from the strings file manually. Yarn Slinger will not do this for you because it may lead to loss of work.", ); diff --git a/crates/bevy_plugin/src/plugin.rs b/crates/bevy_plugin/src/plugin.rs index 9962f40c..4a80c3be 100644 --- a/crates/bevy_plugin/src/plugin.rs +++ b/crates/bevy_plugin/src/plugin.rs @@ -7,7 +7,7 @@ mod yarn_file_source; /// The plugin that provides all Yarn Slinger functionality. /// In general, you'll want to create this by searching for Yarn files in "assets/dialogue", which [`YarnSlingerPlugin::new`] does under the hood. -/// You can also provide a list of yarn files to load via [`YarnSlingerPlugin::with_yarn_sources`]. +/// You can also provide a list of Yarn files to load via [`YarnSlingerPlugin::with_yarn_sources`]. /// If you however do not know the paths to any files nor have them in-memory at the start of the program, /// use [`YarnSlingerPlugin::deferred`] instead to later load the files by sending a [`LoadYarnProjectEvent`]. /// @@ -42,9 +42,9 @@ impl YarnSlingerPlugin { /// Otherwise this panics since Bevy cannot query folders on these platforms. /// Use [`YarnSlingerPlugin::with_yarn_source`] or [`YarnSlingerPlugin::with_yarn_sources`] there instead. /// - /// All yarn files will be shared across [`DialogueRunner`]s. + /// All Yarn files will be shared across [`DialogueRunner`]s. /// If [hot reloading](https://bevy-cheatbook.github.io/assets/hot-reload.html) is turned on, - /// these yarn files will be recompiled if they change during runtime. + /// these Yarn files will be recompiled if they change during runtime. /// /// Calling this is equivalent to calling [`YarnSlingerPlugin::with_yarn_source`] with a [`YarnFileSource::folder`] of `"dialogue"`. #[must_use] @@ -63,9 +63,9 @@ impl YarnSlingerPlugin { } /// Creates a new plugin that loads Yarn files from the given sources. - /// All yarn files will be shared across [`DialogueRunner`]s. + /// All Yarn files will be shared across [`DialogueRunner`]s. /// If [hot reloading](https://bevy-cheatbook.github.io/assets/hot-reload.html) is turned on, - /// these yarn files will be recompiled if they change during runtime. + /// these Yarn files will be recompiled if they change during runtime. /// /// See [`YarnFileSource`] for more information on where Yarn files can be loaded from. /// @@ -90,9 +90,9 @@ impl YarnSlingerPlugin { } /// Creates a new plugin that loads Yarn files from the given source. - /// All yarn files will be shared across [`DialogueRunner`]s. + /// All Yarn files will be shared across [`DialogueRunner`]s. /// If [hot reloading](https://bevy-cheatbook.github.io/assets/hot-reload.html) is turned on, - /// these yarn files will be recompiled if they change during runtime. + /// these Yarn files will be recompiled if they change during runtime. /// /// See [`YarnFileSource`] for more information on where Yarn files can be loaded from. /// @@ -169,7 +169,7 @@ impl Plugin for YarnSlingerPlugin { } /// The deferred version of [`YarnSlingerPlugin`]. Created by [`YarnSlingerPlugin::deferred`]. -/// Will not load any yarn files until a [`LoadYarnProjectEvent`] is sent. +/// Will not load any Yarn files until a [`LoadYarnProjectEvent`] is sent. #[derive(Debug)] #[non_exhaustive] pub struct DeferredYarnSlingerPlugin; diff --git a/crates/bevy_plugin/src/plugin/yarn_file_source.rs b/crates/bevy_plugin/src/plugin/yarn_file_source.rs index e24f2322..10f61f55 100644 --- a/crates/bevy_plugin/src/plugin/yarn_file_source.rs +++ b/crates/bevy_plugin/src/plugin/yarn_file_source.rs @@ -80,7 +80,7 @@ impl YarnFileSource { .load_folder(path) .unwrap_or_else(|e| { panic!( - "Failed to load Yarn file folder {path}: {e}", + "Failed to load Yarn file folder {path}: {e}.\nHelp: Does the folder exist under the assets directory?", path = path.display() ) }) @@ -97,7 +97,7 @@ impl YarnFileSource { .collect(); if handles.is_empty() { warn!("No Yarn files found in the assets subdirectory {path}, so Yarn Slinger won't be able to do anything this run. \ - Help: Add some yarn files to get started.", path = path.display()); + Help: Add some Yarn files to get started.", path = path.display()); } handles } diff --git a/crates/bevy_plugin/src/project.rs b/crates/bevy_plugin/src/project.rs index 61bead21..c4540c68 100644 --- a/crates/bevy_plugin/src/project.rs +++ b/crates/bevy_plugin/src/project.rs @@ -106,7 +106,7 @@ impl YarnProject { } /// Used to late initialize a [`YarnProject`] with a set of Yarn files when using [`YarnSlingerPlugin::deferred`]. -/// If you know the yarn files at the start of the game, you should use [`YarnSlingerPlugin::with_yarn_sources`] instead. +/// If you know the Yarn files at the start of the game, you should use [`YarnSlingerPlugin::with_yarn_sources`] instead. #[derive(Debug, Clone, PartialEq, Eq, Event)] pub struct LoadYarnProjectEvent { pub(crate) localizations: Option, diff --git a/crates/bevy_plugin/src/project/compilation.rs b/crates/bevy_plugin/src/project/compilation.rs index 1b51aa75..ad585893 100644 --- a/crates/bevy_plugin/src/project/compilation.rs +++ b/crates/bevy_plugin/src/project/compilation.rs @@ -58,15 +58,16 @@ fn load_project( bail!("Yarn project already loaded. Sending multiple LoadYarnProjectEvent is not allowed."); } assert!(!event.yarn_files.is_empty(), - "Failed to load Yarn project in deferred mode: no yarn files were specified. \ - Did run `LoadYarnProjectEvent::empty()` without adding any yarn files with `LoadYarnProjectEvent::add_yarn_file` and `LoadYarnProjectEvent::add_yarn_files`? \ + "Failed to load Yarn project in deferred mode: no Yarn files were specified. \ + Did run `LoadYarnProjectEvent::empty()` without adding any Yarn files with `LoadYarnProjectEvent::add_yarn_file` and `LoadYarnProjectEvent::add_yarn_files`? \ If you wanted to load from the default directory instead, use `LoadYarnProjectEvent::default()`."); if event.development_file_generation == DevelopmentFileGeneration::Full && !is_watching_for_changes.0 { warn!("Development file generation mode is set to `Full`, but hot reloading is not turned on. \ - For an optimal development experience, we recommend turning on hot reloading by setting the `watch_for_changes` field of the `AssetPlugin` to `true`. \ - You can see an example of how to do this in at "); + For an optimal development experience, we recommend turning on hot reloading by setting the `watch_for_changes` field of the `AssetPlugin` to `Some`, \ + e.g. via `watch_for_changes: ChangeWatcher::with_delay(Duration::from_millis(200))`. \ + You can see an example of how to do this in at "); } commands.insert_resource(YarnProjectConfigToLoad { @@ -266,7 +267,7 @@ fn compile_yarn_files( ); return Ok(None); } else { - bail!("Failed to compile yarn files: Localization mode is on, but \"{}\" is not does not have full line IDs. \ + bail!("Failed to compile Yarn files: Localization mode is on, but \"{}\" is not does not have full line IDs. \ Cannot generate the line IDs automatically either because we are not in `DevelopmentFileGeneration::Full`", untagged_file.file.file_name); } diff --git a/crates/runtime/src/command.rs b/crates/runtime/src/command.rs index e52733fb..d1315fcd 100644 --- a/crates/runtime/src/command.rs +++ b/crates/runtime/src/command.rs @@ -34,7 +34,7 @@ pub struct Command { /// The parameters are returned without underlying type information, so you will have to convert them using `YarnValue::try_into`. pub parameters: Vec, - /// The raw, unprocessed command as it appeared in the yarn file between the `<<` and `>>` characters. + /// The raw, unprocessed command as it appeared in the Yarn file between the `<<` and `>>` characters. pub raw: String, } diff --git a/crates/runtime/src/dialogue.rs b/crates/runtime/src/dialogue.rs index cc8353f5..feb463c3 100644 --- a/crates/runtime/src/dialogue.rs +++ b/crates/runtime/src/dialogue.rs @@ -52,7 +52,7 @@ impl Dialogue { /// - The [`TextProvider`] is used to retrieve the text of lines and options. /// - The [`VariableStorage`] is used to store and retrieve variables. /// - /// If you don't need any fancy behavior, you can use [`StringTableTextProvider`] and [`MemoryVariableStore`]. + /// If you don't need any fancy behavior, you can use [`StringTableTextProvider`] and [`MemoryVariableStorage`]. #[must_use] pub fn new( variable_storage: Box, @@ -391,7 +391,7 @@ mod tests { #[test] fn is_send_sync() { - let variable_storage = Box::new(MemoryVariableStore::new()); + let variable_storage = Box::new(MemoryVariableStorage::new()); let text_provider = Box::new(StringTableTextProvider::new()); let dialogue = Dialogue::new(variable_storage, text_provider); accept_send_sync(dialogue); diff --git a/crates/runtime/src/variable_storage.rs b/crates/runtime/src/variable_storage.rs index 160f4bef..e735dfcb 100644 --- a/crates/runtime/src/variable_storage.rs +++ b/crates/runtime/src/variable_storage.rs @@ -68,16 +68,16 @@ impl Clone for Box { /// A simple concrete implementation of [`VariableStorage`] that keeps all variables in memory. #[derive(Debug, Clone, Default)] -pub struct MemoryVariableStore(Arc>>); +pub struct MemoryVariableStorage(Arc>>); -impl MemoryVariableStore { - /// Creates a new empty `MemoryVariableStore`. +impl MemoryVariableStorage { + /// Creates a new empty `MemoryVariableStorage`. pub fn new() -> Self { Self::default() } } -impl VariableStorage for MemoryVariableStore { +impl VariableStorage for MemoryVariableStorage { fn clone_shallow(&self) -> Box { Box::new(self.clone()) } @@ -114,7 +114,7 @@ impl VariableStorage for MemoryVariableStore { } } -impl MemoryVariableStore { +impl MemoryVariableStorage { fn validate_name(name: impl AsRef) -> Result<()> { let name = name.as_ref(); if name.starts_with('$') { diff --git a/crates/yarn_slinger/tests/dialogue_tests.rs b/crates/yarn_slinger/tests/dialogue_tests.rs index 2408c9be..9b041e04 100644 --- a/crates/yarn_slinger/tests/dialogue_tests.rs +++ b/crates/yarn_slinger/tests/dialogue_tests.rs @@ -228,7 +228,7 @@ fn test_function_argument_type_inference() { let storage = test_base .with_compilation(result) .run_standard_testcase() - .variable_store + .variable_storage .clone_shallow(); // The values should be of the right type and value diff --git a/crates/yarn_slinger/tests/test_base/mod.rs b/crates/yarn_slinger/tests/test_base/mod.rs index 1381d8fe..7035808d 100644 --- a/crates/yarn_slinger/tests/test_base/mod.rs +++ b/crates/yarn_slinger/tests/test_base/mod.rs @@ -47,7 +47,7 @@ pub struct TestBase { pub dialogue: Dialogue, pub test_plan: Option, pub string_table: SharedTextProvider, - pub variable_store: MemoryVariableStore, + pub variable_storage: MemoryVariableStorage, runtime_errors_cause_failure: Arc, } @@ -57,11 +57,11 @@ impl Default for TestBase { if let Err(_e) = init_logger(runtime_errors_cause_failure.clone()) { // We've set the logger twice, that's alright for the tests. } - let variable_store = MemoryVariableStore::new(); + let variable_storage = MemoryVariableStorage::new(); let string_table = SharedTextProvider::new(StringTableTextProvider::new()); let mut dialogue = Dialogue::new( - Box::new(variable_store.clone()), + Box::new(variable_storage.clone()), Box::new(string_table.clone()), ); dialogue @@ -75,7 +75,7 @@ impl Default for TestBase { Self { dialogue, runtime_errors_cause_failure, - variable_store, + variable_storage, string_table, test_plan: Default::default(), } @@ -252,7 +252,7 @@ impl TestBase { self } - /// Returns the list of .node and.yarn files in the third-party/YarnSpinner/Tests/ directory. + /// Returns the list of .node and.Yarn files in the third-party/YarnSpinner/Tests/ directory. pub fn file_sources(subdir: impl AsRef) -> impl Iterator { let subdir: PathBuf = PathBuf::from(subdir.as_ref()); let path = test_data_path().join(&subdir); diff --git a/crates/yarn_slinger/tests/type_tests.rs b/crates/yarn_slinger/tests/type_tests.rs index de5abfe3..93cbc027 100644 --- a/crates/yarn_slinger/tests/type_tests.rs +++ b/crates/yarn_slinger/tests/type_tests.rs @@ -417,7 +417,7 @@ fn test_initial_values() -> anyhow::Result<()> { .declare_variable(Declaration::new("$external_bool", Type::Number).with_default_value(42)) .compile()?; - let mut variable_storage = test_base.variable_store.clone_shallow(); + let mut variable_storage = test_base.variable_storage.clone_shallow(); variable_storage.set("$external_str".to_string(), "Hello".into())?; variable_storage.set("$external_int".to_string(), 42.into())?; variable_storage.set("$external_bool".to_string(), true.into())?; diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 8b405d97..9fba0e16 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -1,17 +1,22 @@ - [Introduction](./introduction.md) -- [Working with Yarn Slinger](./working_with_yarn_slinger.md) - - [Yarn Files](./working_with_yarn_slinger/yarn_files.md) - - [Basics](./working_with_yarn_slinger/yarn_files/basics.md) - - [Running Examples](./working_with_yarn_slinger/yarn_files/running_examples.md) - - [Lines](./working_with_yarn_slinger/yarn_files/lines.md) - - [Variables](./working_with_yarn_slinger/yarn_files/variables.md) - - [Options](./working_with_yarn_slinger/yarn_files/options.md) - - [Nodes](./working_with_yarn_slinger/yarn_files/nodes.md) - - [Markup](./working_with_yarn_slinger/yarn_files/markup.md) - - [Functions](./working_with_yarn_slinger/yarn_files/functions.md) - - [Commands](./working_with_yarn_slinger/yarn_files/commands.md) - - [Bevy Plugin](./working_with_yarn_slinger/bevy_plugin.md) - - [Custom Functions](./working_with_yarn_slinger/bevy_plugin/custom_functions.md) - - [Custom Commands](./working_with_yarn_slinger/bevy_plugin/custom_commands.md) -- [Differences to Yarn Spinner for Unity](./differences_to_yarn_spinner_for_unity.md) +- [Yarn Files](yarn_files.md) + - [Basics](yarn_files/basics.md) + - [Running Examples](yarn_files/running_examples.md) + - [Lines](yarn_files/lines.md) + - [Variables](yarn_files/variables.md) + - [Options](yarn_files/options.md) + - [Nodes](yarn_files/nodes.md) + - [Markup](yarn_files/markup.md) + - [Functions](yarn_files/functions.md) + - [Commands](yarn_files/commands.md) +- [Bevy Plugin](bevy_plugin.md) + - [Setup](bevy_plugin/setup.md) + - [Compiling Yarn Files](bevy_plugin/compiling_yarn_files.md) + - [`DialogRunner` and a High Level Overview](bevy_plugin/dialog_runner.md) + - [Localization](bevy_plugin/localization.md) + - [Assets](bevy_plugin/assets.md) + - [Variable Storage](bevy_plugin/variable_storage.md) + - [Custom Functions](bevy_plugin/custom_functions.md) + - [Custom Commands](bevy_plugin/custom_commands.md) + - [Dialog Views](bevy_plugin/dialog_views.md) - [Porting Yarn Slinger](./porting_yarn_slinger.md) diff --git a/docs/src/bevy_plugin.md b/docs/src/bevy_plugin.md new file mode 100644 index 00000000..068544fa --- /dev/null +++ b/docs/src/bevy_plugin.md @@ -0,0 +1,10 @@ +# Bevy Plugin + +While Yarn Slinger is built to be engine-agnostic, the intended way to use it +is through an engine-specific wrapper. The currently only supported engine is [Bevy](https://bevyengine.org/). +It is a data-oriented game engine using an ECS, which broadly means that you don't look at your game world +through the traditional lens of objects mutating the world and each other, but instead see the game as a collection +of data attached to various entities that can be queried and manipulated through systems. + +This chapter will assume that you are familiar with the basics of Bevy. If you're not there not, +try to come back after you've gone through the [Bevy Book](https://bevyengine.org/learn/book/introduction/). diff --git a/docs/src/bevy_plugin/assets.md b/docs/src/bevy_plugin/assets.md new file mode 100644 index 00000000..f66bb4fb --- /dev/null +++ b/docs/src/bevy_plugin/assets.md @@ -0,0 +1,26 @@ +# Assets + +While Bevy as a whole has assets, Yarn Slinger can associate specific assets with lines. +These are always [localized](./localization.md), such as voiceovers. + +## Using Metadata Instead of Assets + +Before we jump into assets, let's first help you out if you don't care about localization. +The mechanism in place for this is *line metadata*, which are strings you can add to Yarn lines after a hashtag: + +```text +title: Start +--- +Granny: It's hard to believe that it's over, isn't it? #smiling +Granny: Funny how we get attached to the struggle. #laughing +Granny: Promise me tat you'll take care of yourself, okay? #smiling +=== +``` + +A [dialog view](./dialog_views.md) will be able to read the metadata "smiling", "laughing", and "smiling" again from `LocalizedLine::metadata` and accordingly load things like character portraits. +These annotations will also be written into the "comment" field of strings files, which are explained in the chapter [Localization](./localization.md). + +## Asset Providers + +TODO + diff --git a/docs/src/bevy_plugin/compiling_yarn_files.md b/docs/src/bevy_plugin/compiling_yarn_files.md new file mode 100644 index 00000000..5f1d8fb4 --- /dev/null +++ b/docs/src/bevy_plugin/compiling_yarn_files.md @@ -0,0 +1,61 @@ +# Compiling Yarn Files + +The `YarnProject` resource represents the set of all compiled Yarn files of +your game. You cannot construct it yourself. Instead, it is inserted into the Bevy world for +you when the compilation is finished. You can however steer how and when this is done. + +## Starting the Compilation Process + +Generally, you'll want your game to compile the Yarn files as soon as possible. This +is why the [`YarnSpinnerPlugin`](setup.md) will start doing so by default when it is added to the app. + +If for some reason you do not wish to start compilation right away, you can *defer* this process. To do this, +construct the `YarnSpinnerPlugin` with `YarnSpinnerPlugin::deferred()` when adding it. Then, whenever you are ready +to start the compilation, you can send a `LoadYarnProjectEvent`. Its construction methods are identical to the `YarnSpinnerPlugin`. +In fact, when not running in deferred mode, the `YarnSpinnerPlugin` simply relays its setting to a `LoadYarnProjectEvent` and sends it. + +## Settings + +If you look through the documentation of the [`YarnSpinnerPlugin`], you'll notice a few methods to modify +its settings. The first few deal with where our Yarn files are coming from. + +### Yarn File Sources + +By default, Yarn Slinger will look +in `/assets/dialog`. Yarn Slinger can only read files from the `assets` directory +— or its equivalent, if you have changed this default in the `AssetPlugin` on platforms which support it— +but you can change how the `assets` will be looked through. + +The way to specify this is via `YarnFileSource`s. This enum tells Yarn Slinger where one or more Yarn files +come from and can be added to an `AssetPlugin` with `AssetPlugin::add_yarn_source()`. +The enum variants should be self explanatory, but the two most common use-cases come with their own convenience constructors: +- `YarnFileSource::file()`: looks for a Yarn file at a path inside under the `assets` directory. +- `YarnFileSource::folder()`: recursively looks through a given subdirectory for Yarn files. + +Since the Wasm and Android builds of Bevy have restrictions on their filesystem access, +they cannot use `YarnFileSource::folder()` and must have all their Yarn files listed explicitly with `YarnFileSource::file()`. +As such, the default behavior provided by `YarnSlingerPlugin::new()` is not suitable for these platforms. +To avoid it, use the `AssetPlugin::with_yarn_source()` constructor instead. + +As you might have guessed by now, `YarnSlingerPlugin::new()` is simply a shorthand for `AssetPlugin::with_yarn_source(YarnFileSource::folder("dialog"))`. + +### Development File Generation + +`YarnSlingerPlugin::with_development_file_generation()` accepts a `DevelopmentFileGeneration`, which tells Yarn Slinger how aggressively to generate useful files on runtime. +"Useful" refers to the developer and not the user. The default is `DevelopmentFileGeneration::TRY_FULL`, which will be `DevelopmentFileGeneration::Full` on platforms which support filesystem access, +i.e. all except Wasm and Android. See the documentation for the full list of effects. Suffice it to say +that this is not very important when developing without localization, but becomes vital otherwise. See the [Localization](localization.md) chapter for more. + +Since these settings are intended for development, you can use `YarnSlingerPlugin::with_development_file_generation(DevelopmentFileGeneration::None)` when shipping your game to optimize the runtime costs and +avoid generating files that are useless to the player. + +### Localization + +The settings accessed by `YarnSlingerPlugin::with_localizatons` are important enough to warrant their own chapter. See [Localization](localization.md). + +## After the Compilation + +Whether you used `YarnSlingerPlugin` or `LoadYarnProjectEvent`, as soon as the compilation finished, a `YarnProject` resource will be inserted into the Bevy world. +You can react to its creation by guarding your systems with `.run_if(resource_added::())`, as seen in the [setup](setup.md). + +Once you have the `YarnProject`, you can use it to spawn a `DialogRunner` which in turn can, well, [run dialogs](dialog_runner.md) diff --git a/docs/src/working_with_yarn_slinger/bevy_plugin/custom_commands.md b/docs/src/bevy_plugin/custom_commands.md similarity index 100% rename from docs/src/working_with_yarn_slinger/bevy_plugin/custom_commands.md rename to docs/src/bevy_plugin/custom_commands.md diff --git a/docs/src/working_with_yarn_slinger/bevy_plugin/custom_functions.md b/docs/src/bevy_plugin/custom_functions.md similarity index 100% rename from docs/src/working_with_yarn_slinger/bevy_plugin/custom_functions.md rename to docs/src/bevy_plugin/custom_functions.md diff --git a/docs/src/bevy_plugin/dialog_runner.md b/docs/src/bevy_plugin/dialog_runner.md new file mode 100644 index 00000000..dcc93ed9 --- /dev/null +++ b/docs/src/bevy_plugin/dialog_runner.md @@ -0,0 +1,65 @@ +# `DialogRunner` and a High Level Overview + +The main way to actually manipulate the state of your dialog is through a `DialogRunner`. +You create it from a `YarnProject` (see [Compiling Yarn Files](compiling_yarn_files.md)) with either +`YarnProject::create_dialog_runner()` or `YarnProject::build_dialog_runner()`. +The first uses default configurations which should be alright for many use-cases, +while the latter allows you to add or change functionality. + +## Dialog Flow + +The actual navigation through a dialog is handled by a [dialog view](dialog_views.md), +which is responsible for back-and-forth interaction with the player. +As such, most of the methods provided by a `DialogRunner` are to be called by such a view. +The one you will want to call yourself, as seen in the [setup](setup.md), is `DialogRunner::start_node`, +which will tell the `DialogRunner` to start running from the provided [node](../yarn_files/nodes.md). + +## Variable Storage + +[Variables](../yarn_files/variables.md) need to be stored in some place. By default, they are kept in memory through the `InMemoryVariableStorage`. +This means that when you quit and reopen the game, all variables used in Yarn files will be empty again. Of course, this is suboptimal when you want to allow +the player saving and loading their game state. To accomplish this, you can go one of two routes: + +- Manipulate the variables in the variable store. Read then when saving and write them when loading. +You can access the variable storage through `DialogRunner::variable_storage()`. +- Directory use a variable storage that stores its variables in a persistent way, such as a database or a file. +You can change the underlying variable storage through the builder API discussed later in this chapter. + +For information on how to create your own variable storage, see the chapter [Variable Storage](./variable_storage) + +## Functions and Commands + +Yarn files can contain user-defined functions and commands. These can be accessed with +`DialogRunner::library()` and `DialogRunner::commands()`. For more information, see the chapters [Custom Functions](custom_functions.md) +and [Custom Commands](custom_commands.md). + +## Text and Assets + +We make a distinction between *text*, which are the written words organized into *lines* contained in Yarn files or in +[localization files](localization.md), and *assets*, which are supplemental data associated with a line. +Assets are referenced over a Bevy `Handle` and can be used for things such as voiceover sound files or images that might need translation. + +Of note is that using assets **requires** using [localization](localization.md), or at least thinking about it. +As a consequence, language settings are split between text and assets. After all, a player might want to hear lines delivered in the original recorded language but read the text translated into their own language. + +You can read more about how current language can be set for a `DialogRunner` in the [localization](localization.md) chapter. + +Text is provided by a `TextProvider`. While it can be overwritten, the default `StringsFileTextProvider` will be a good choice for +nearly all users. The only reason you might have to create an own `TextProvider` is if you want a very custom localization strategy, such as +translating text automatically through AI. + +Assets are provided by `AssetProvider`s. In contrast to the `TextProvider`, you might very well create your own `AssetProvider`. +For your convenience, Yarn Slinger already ships with an `AudioAssetProvider` that you can use for voice lines and a `FileExtensionAssetProvider` +that can load any asset based on naming conventions and file extensions. See the chapter [Assets](assets.md). + +Text and asset providers can be set through the builder API and accessed later with `DialogRunner::text_provider()` and `DialogRunner::asset_providers()`. If you know the exact type `T` of `AssetProvider` you +want, you can call `DialogRunner::asset_provider::()` instead. + +## Builder API + +As mentioned in the beginning of this chapter, a `DialogRunner` can be modified or extended on creation +by using `YarnProject::build_dialog_runner()`. In fact, `YarnProject::create_dialog_runner()` is nothing but a shorthand for `YarnProject::build_dialog_runner().build()`. + +You can use the builder API to inject your own implementations of traits used for the features presented in this chapter. +`DialogueRunnerBuilder::with_variable_storage` changes the underlying `VariableStorage` and `DialogueRunnerBuilder::with_text_provider` the `TextProvider`. +`DialogueRunnerBuilder::add_asset_provider` adds an `AssetProvider` to the set of asset providers called for each line presented to the player. diff --git a/docs/src/bevy_plugin/dialog_views.md b/docs/src/bevy_plugin/dialog_views.md new file mode 100644 index 00000000..262383ec --- /dev/null +++ b/docs/src/bevy_plugin/dialog_views.md @@ -0,0 +1,3 @@ +# Dialog Views + +TODO diff --git a/docs/src/bevy_plugin/localization.md b/docs/src/bevy_plugin/localization.md new file mode 100644 index 00000000..bf5767a5 --- /dev/null +++ b/docs/src/bevy_plugin/localization.md @@ -0,0 +1,169 @@ +# Localization + +If you only want to support a single language, you can safely ignore localization features. +As soon as you want to support [assets](assets.md) or multiple languages however, you will need to use localization. +Fortunately Yarn Slinger makes this quite easy! + +Let's first look at how to use localization and then explain what's going on under the hood. + +## Using Localization the Easy Way + +We specify our supported localizations when creating the [`YarnSlingerPlugin` (or using deferred compilation)](compiling_yarn_files.md): + +```rust +YarnSlingerPlugin::new().with_localizations(Localizations { + base_localization: "en-US".into(), + translations: vec!["de-CH".into()], +}) +``` + +The *base localization* is the language in which your Yarn files are already written. +In this case, we specified that our Yarn file was written in English as spoken in the USA. +The *translations* are all languages you want to support. Here, we want to support German as spoken in Switzerland. + +Put the code shown above into the example used in the [setup](setup.md) and run the game. + +Now take a look at your Yarn file at `assets/dialog/example.yarn`. +You will see that your line of dialog will contain an autogenerated ID, for example: +```diff +# assets/dialog/example.yarn +title: Start +--- +- Hello World! ++ Hello World! #line:13032079 +=== +``` +This ID uniquely references this line across translations. +For the sake of clarity, we will use diff highlighting throughout this chapter. +In case you're not familiar with this look, for our purposes the red line started by "- " shows how the line looked like before a change, +while the green line started by "+ " shows how the line looks like after the change. The "- " and "+ " are just visual indicators and not actually part +of the files, so don't let that confuse you! + +You will probably also have noticed a new file in your assets that was not there before, namely "de-CH.strings.csv": + +![strings_file_generated.png](strings_file_generated.png) + +This file is called a *strings file*, because it contains translations of each *string* of text of your Yarn files. +Let's see what it contains: + +```csv +language,id,text,file,node,line_number,lock,comment +de-CH,line:13032079,Hello World!,example.yarn,Start,4,7f83b165, +``` + +Since this is a CSV, let's open it in an application that renders the content as a table: +![strings_file_new.png](strings_file_new.png) + +You can see that our line from before is in there! Notice how the `id` matches across the files. + +This file will be populated with new entries as soon you change the Yarn files. Assuming that you +are using hot reloading as described in the [setup](setup.md), run your app again in case you closed it or advanced the dialog. +While you are greeted with the "Hello World!" message on screen, open the Yarn file and edit it. Let's add a new line: + +```diff +# assets/dialog/example.yarn +title: Start +--- +Hello World! #line:13032079 ++ This is a brand new line of text +=== +``` + +Save the file while the game is still running. You should see that our new line just got assigned an own line ID: + +```diff +# assets/dialogue/example.yarn +title: Start +--- +Hello World! #line:13032079 +- This is a brand new line of text ++ This is a brand new line of text #line:10414042 +=== +``` +In case you can't see this, your editor might still have the old state of the file cached. It usually helps to change focus, tab out to another window, or closing and reopening the editor. +The strings file should now also contain a new entry: + +![strings_file_another_line.png](strings_file_another_line.png) + +Let's translate some of this. Change the string "Hello World!" in this file to "Hallo Welt!", which is German, and save it: + +![strings_file_translated.png](strings_file_translated.png) + +The game will currently happily ignore this as by default it uses the base language, which means it will take +its text straight from the Yarn files. But we can easily switch the language: + +```rust +fn spawn_dialogue_runner(mut commands: Commands, project: Res) { + let mut dialogue_runner = project.create_dialogue_runner(); + dialogue_runner.start_node("Start"); + dialogue_runner.set_language("de-CH"); // Use our translation + commands.spawn(dialogue_runner); +} +``` + +Run the game again and you should be greeted by this text: +![translated_line.png](translated_line.png) + +Hurray! See how painless localization can be? + +## Languages + +Languages are specified according to IETF BCP 47. +You can add as many translations as you want. Each will receive an own strings file. + +To switch languages at runtime, simply retrieve a `DialogRunner` through a Bevy query inside a system. +When you use `DialogRunner::set_language()` as shown above, you will set the language for both text and assets. +You can be more granular by using `DialogRunner::set_text_language()` and `DialogRunner::set_asset_language()` separately instead. +This allows you to support use cases such as showing the text in the player's native language and play voiceover sound in the original recorded language, which might be a different one. + +## Assets + +Since assets require using localization, they are searched for in folders named after the language they support. +For the example used throughout this chapter, the assets for the base localization would be searched for in `assets/dialog/en-US/`, while the assets for the `de-CH` +translation will be searched at `assets/dialog/de-CH/`. This is however more a convention than a rule, as a given `AssetProvider` is allowed to look for its assets wherever. +The asset providers shipped by Yarn Slinger will additionally expect assets to be named after the line ID they belong to. For example, the `AudioAssetProvider` would look for the +voice line reading our "Hello World!" line at `assets/dialog/en-US/13032079.mp3` for the base localization. + +To read more about how to use assets, read the chapter [Assets](./assets.md). + +## File Editing Workflow + +The strings file can be freely edited by a translator in the *text* and *comment* fields. +While you can translate the texts yourself, the format being straightforward allows the translator to also be someone else that is not involved with the coding part of the game at all. + +You might have some questions regarding what happens when one person edits a Yarn file while another edits the strings file. As a general rule, +the strings file will try to "keep up" with the Yarn file without ever destroying anything that was already translated. + +As you've seen, new lines will be amended. If the Yarn file has a line edited, it will be changed in the strings file as well if it was not yet translated. +If there is already a translation, it will be marked by a "NEEDS UPDATE" prefix in the text. If a line was deleted in the Yarn file, it will also be deleted +in the strings file if it was untranslated. Otherwise, it will be left untouched. + +Bottom line: if there's a translation, it will **never** be removed. + +## Shipping the Game + +Once you want to build your game for a release, you should disable the automatic file creation and editing. +To do this, add the following line to the plugin creation: +```rust +YarnSlingerPlugin::new() +// ... +.with_development_file_generation(DevelopmentFileGeneration::None) +``` + +This will change the behavior of missing translations to simply fall back to the base localization. + +While you're on it, you might also want to disable Bevy's hot reloading. + +## Customization + +You may have wondered what the `.into()`s were for in the lines at the beginning of the chapter: + +```rust +YarnSlingerPlugin::new().with_localizations(Localizations { + base_localization: "en-US".into(), + translations: vec!["de-CH".into()], +}) +``` + +They're here because a localization is not just a string with a language code, but an entire struct, namely `Localization`. +You can construct this struct directly the path to the strings file and where assets are searched for. diff --git a/docs/src/bevy_plugin/setup.md b/docs/src/bevy_plugin/setup.md new file mode 100644 index 00000000..c06736a3 --- /dev/null +++ b/docs/src/bevy_plugin/setup.md @@ -0,0 +1,143 @@ +# Setup + +We will now go through the steps to setup a new Bevy project running Yarn Slinger dialogs. +This is the same setup as in the chapter [Running Examples](../yarn_files/running_examples.md), but with explanations this time. +If you've followed along in the linked chapter already, you can just read this part without executing anything. + +## Setting up the crate + +Run the following in your terminal to create a new crate with the required dependencies: + +```bash +cargo new yarn_slinger_playground +cd yarn_slinger_playground +cargo add bevy --features filesystem_watcher +cargo add bevy_yarn_slinger bevy_yarn_slinger_example_dialogue_view +``` + +The line `cargo add bevy --features filesystem_watcher` ensures that we can use *hot reloading* in our project, which means that we can edit the Yarn files +while the game is running and it will reload them automatically on change. + +The dependency `bevy_yarn_slinger` is for the Yarn Slinger Bevy plugin proper, while `bevy_yarn_slinger_example_dialogue_view` +gives us a nice default [dialog view](dialog_views.md), so we can actually see the text we've written and have options to click on. + +## Adding the Yarn Files + +We'll use a single Yarn file for this example. Inside the folder `assets/dialog`, add a file named `example.yarn` with the following content: +```text +# assets/dialogue/example.yarn +title: Start +--- +Hello World! +=== +``` + +## The main code + +Add the following code to your `src/main.rs`. + +```rust +// src/main.rs +use bevy::{prelude::*, asset::ChangeWatcher, utils::Duration}; +use bevy_yarn_slinger::prelude::*; +use bevy_yarn_slinger_example_dialogue_view::prelude::*; + +fn main() { + let mut app = App::new(); + app.add_plugins(( + DefaultPlugins.set(AssetPlugin { + // Activate hot reloading + watch_for_changes: ChangeWatcher::with_delay(Duration::from_millis(200)), + ..default() + }), + // Add the Yarn Slinger plugin. + // As soon as this plugin is built, a Yarn project will be compiled + // from all Yarn files found under assets/dialog/*.yarn + YarnSlingerPlugin::new(), + // Add the example dialogue view plugin + ExampleYarnSlingerDialogueViewPlugin::new(), + )) + // Setup a 2D camera so we can see the text + .add_systems(Startup, setup_camera) + // Spawn the dialog as soon as the Yarn project finished compiling + .add_systems( + Update, + spawn_dialogue_runner.run_if(resource_added::()), + ) + .run(); +} + +fn setup_camera(mut commands: Commands) { + commands.spawn(Camera2dBundle::default()); +} + +fn spawn_dialogue_runner(mut commands: Commands, project: Res) { + let mut dialogue_runner = project.create_dialogue_runner(); + // Start the dialog at the node with the title "Start" + dialogue_runner.start_node("Start"); + commands.spawn(dialogue_runner); +} +``` + +Reiterating the comments in the code, let's take a look at some snippets. + +```rust +DefaultPlugins.set(AssetPlugin { + // Activate hot reloading + watch_for_changes: ChangeWatcher::with_delay(Duration::from_millis(200)), + ..default() +}), +``` + +This setting for the `AssetPlugin` enables you to edit the Yarn files on the fly while your game is running and +see the effects instantaneously. We recommend using this workflow on all platforms which support it, which is to say all except Wasm and Android. + + +```rust +YarnSlingerPlugin::new(), +``` + +This self-explanatory line initializes the plugin. When using the standard constructor with no options, Yarn files will be searched for in the directory `/assets/dialog/`, where all +files ending in `.yarn` will be compiled as soon as the game starts. + +The plugin makes sure all components of Yarn Slinger work except for any actual graphics. You need to +instantiate a [dialog view](dialog_views.md) for that: + +```rust +ExampleYarnSlingerDialogueViewPlugin::new(), +``` + +Here we initialize the dialogue view shipped by the `bevy_yarn_slinger_example_dialogue_view` crate. It +offers some sensible defaults which you can see in the screenshots used throughout this guide. You can of course skip this +and use your own dialogue view instead. + +```rust +spawn_dialogue_runner.run_if(resource_added::()), +``` +The method `.run_if(resource_added::()` is our way of saying "run this system once as soon as our Yarn files are done compiling". +Let's look at what will actually be run in that moment: + +```rust +fn spawn_dialogue_runner(mut commands: Commands, project: Res) { + let mut dialogue_runner = project.create_dialogue_runner(); + // Start the dialog at the node with the title "Start" + dialogue_runner.start_node("Start"); + commands.spawn(dialogue_runner); +} +``` + +The main way of interacting with Yarn files during runtime and managing the flow of a dialog is through a +[`DialogRunner`](dialog_runner.md). To do this, we use the [`YarnProject`](compiling_yarn_files.md) resource we referenced in the `run_if` section above. +It represents our compiled Yarn files, which we use to create a new dialog runner. +We then point it to the [node](../yarn_files/nodes.md) named "Start" of our Yarn file. +We use `start_node` for this, which will "move" the dialog runner to the provided node and start executing the dialog in the next frame, +using the registered dialog view to actually present it on the screen. +Finally, we spawn the dialog runner on an own entity into the Bevy world. + +In the end, your file structure should look like this: + +![file_system.png](../yarn_files/file_system.png) + +Run your game with `cargo run` and you should see the following: + +![hello_world.png](../yarn_files/hello_world.png) diff --git a/docs/src/bevy_plugin/strings_file_another_line.png b/docs/src/bevy_plugin/strings_file_another_line.png new file mode 100644 index 00000000..cfa6441c Binary files /dev/null and b/docs/src/bevy_plugin/strings_file_another_line.png differ diff --git a/docs/src/bevy_plugin/strings_file_generated.png b/docs/src/bevy_plugin/strings_file_generated.png new file mode 100644 index 00000000..e1fe47be Binary files /dev/null and b/docs/src/bevy_plugin/strings_file_generated.png differ diff --git a/docs/src/bevy_plugin/strings_file_new.png b/docs/src/bevy_plugin/strings_file_new.png new file mode 100644 index 00000000..f57f01ee Binary files /dev/null and b/docs/src/bevy_plugin/strings_file_new.png differ diff --git a/docs/src/bevy_plugin/strings_file_translated.png b/docs/src/bevy_plugin/strings_file_translated.png new file mode 100644 index 00000000..4f636bf4 Binary files /dev/null and b/docs/src/bevy_plugin/strings_file_translated.png differ diff --git a/docs/src/bevy_plugin/translated_line.png b/docs/src/bevy_plugin/translated_line.png new file mode 100644 index 00000000..e74c965e Binary files /dev/null and b/docs/src/bevy_plugin/translated_line.png differ diff --git a/docs/src/bevy_plugin/variable_storage.md b/docs/src/bevy_plugin/variable_storage.md new file mode 100644 index 00000000..d9e545c4 --- /dev/null +++ b/docs/src/bevy_plugin/variable_storage.md @@ -0,0 +1,3 @@ +# Variable Storage + +TODO diff --git a/docs/src/differences_to_yarn_spinner_for_unity.md b/docs/src/differences_to_yarn_spinner_for_unity.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/src/introduction.md b/docs/src/introduction.md index 174410ad..a2f2c354 100644 --- a/docs/src/introduction.md +++ b/docs/src/introduction.md @@ -1,8 +1,10 @@ # Yarn Slinger Book Welcome to Yarn Slinger, the friendly dialogue tool for Rust! +Yarn Slinger is a port of the widely used dialogue tool [Yarn Spinner](https://yarnspinner.dev). + Please click on the link that describes your situation the best. - [I'm new to all of this](./working_with_yarn_slinger.md) -- [I have used Yarn Spinner for Unity before](./differences_to_yarn_spinner_for_unity.md) +- [I have used Yarn Spinner for Unity before](./bevy_plugin.md) - [I want to port Yarn Slinger to another language than Rust or another engine than Bevy](./porting_yarn_slinger.md) diff --git a/docs/src/working_with_yarn_slinger.md b/docs/src/working_with_yarn_slinger.md index ab130f74..bb488396 100644 --- a/docs/src/working_with_yarn_slinger.md +++ b/docs/src/working_with_yarn_slinger.md @@ -1,5 +1,5 @@ # Working with Yarn Slinger -Yarn Slinger is a port of the widely used dialogue tool [Yarn Spinner](https://yarnspinner.dev). + They both share the idea of writing dialog in writer-friendly text files called *Yarn files*. We -will take a look at how to write these in the next chapter, [Yarn Files](./working_with_yarn_slinger/yarn_files.md). +will take a look at how to write these in the next chapter, [Yarn Files](yarn_files.md). diff --git a/docs/src/working_with_yarn_slinger/bevy_plugin.md b/docs/src/working_with_yarn_slinger/bevy_plugin.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/src/working_with_yarn_slinger/yarn_files/commands.md b/docs/src/working_with_yarn_slinger/yarn_files/commands.md deleted file mode 100644 index 0c2aa5de..00000000 --- a/docs/src/working_with_yarn_slinger/yarn_files/commands.md +++ /dev/null @@ -1,3 +0,0 @@ -# Commands - -TODO diff --git a/docs/src/working_with_yarn_slinger/yarn_files/nodes.md b/docs/src/working_with_yarn_slinger/yarn_files/nodes.md deleted file mode 100644 index 5d766914..00000000 --- a/docs/src/working_with_yarn_slinger/yarn_files/nodes.md +++ /dev/null @@ -1,3 +0,0 @@ -# Nodes - -TODO diff --git a/docs/src/working_with_yarn_slinger/yarn_files.md b/docs/src/yarn_files.md similarity index 100% rename from docs/src/working_with_yarn_slinger/yarn_files.md rename to docs/src/yarn_files.md diff --git a/docs/src/yarn_files/apples.png b/docs/src/yarn_files/apples.png new file mode 100644 index 00000000..7dca3343 Binary files /dev/null and b/docs/src/yarn_files/apples.png differ diff --git a/docs/src/working_with_yarn_slinger/yarn_files/basics.md b/docs/src/yarn_files/basics.md similarity index 100% rename from docs/src/working_with_yarn_slinger/yarn_files/basics.md rename to docs/src/yarn_files/basics.md diff --git a/docs/src/yarn_files/commands.md b/docs/src/yarn_files/commands.md new file mode 100644 index 00000000..759b2f7c --- /dev/null +++ b/docs/src/yarn_files/commands.md @@ -0,0 +1,25 @@ +# Commands + +You've seen the `<>` syntax a couple of times now. +Everything that happens between the double angle brackets (`<<` & `>>`) is called a *command*. + +Commands serve either fundamental operations such as declaring new [variables](variables.md) or instructing the game engine to +manipulate the world somehow. A command takes up an entire line. +In contrast to [functions](functions.md), commands return no value and can thus not be used within lines via interpolation. + +Defining your own commands is specific to the game engine used. +For Bevy, see the chapter [Custom Commands](../bevy_plugin/custom_commands.md). + +The following commands are available by default in all game engines: + +## Variables +- `<>`: Creates a new variable and initializes it with a value. +- `<>`: Assigns a new value to an existing variable. + +## Flow control +- `<>` / `<>` / `<>` / `<>`: Executes lines conditionally. In [options](options.md), place `<>` at the end of the line. +- `<>`: Immediately ends the dialog as if it ran out of lines. + +## Engine communication +- `<>`: Waits for `$seconds` seconds before continuing the dialog, e.g. `wait 3.5` will wait for 3.5 seconds. +This will not block the game engine, so the rest of the game can continue running in the meantime, presumably without the player gaining control. diff --git a/docs/src/working_with_yarn_slinger/yarn_files/cond_options.png b/docs/src/yarn_files/cond_options.png similarity index 100% rename from docs/src/working_with_yarn_slinger/yarn_files/cond_options.png rename to docs/src/yarn_files/cond_options.png diff --git a/docs/src/working_with_yarn_slinger/yarn_files/dice.png b/docs/src/yarn_files/dice.png similarity index 100% rename from docs/src/working_with_yarn_slinger/yarn_files/dice.png rename to docs/src/yarn_files/dice.png diff --git a/docs/src/working_with_yarn_slinger/yarn_files/file_system.png b/docs/src/yarn_files/file_system.png similarity index 100% rename from docs/src/working_with_yarn_slinger/yarn_files/file_system.png rename to docs/src/yarn_files/file_system.png diff --git a/docs/src/working_with_yarn_slinger/yarn_files/functions.md b/docs/src/yarn_files/functions.md similarity index 88% rename from docs/src/working_with_yarn_slinger/yarn_files/functions.md rename to docs/src/yarn_files/functions.md index 311194b7..986759d7 100644 --- a/docs/src/working_with_yarn_slinger/yarn_files/functions.md +++ b/docs/src/yarn_files/functions.md @@ -14,6 +14,22 @@ Which will result in something like this: The curly braces (`{}`) are not part of the function call, but are used to interpolate the result of the function into the text, as seen previously in the chapter [Variables](variables.md). +Speaking about variables, you can also use them as parameters: +```text +title: Start +--- +<> +How many sides does your die have? +-> One + <> +-> Six + <> +-> Six thousand + <> +Rolling a die with {$sides} sides and got a {dice($sides)}! +=== +``` + There are a number of built-in functions available, such as the `dice` function used above. Defining your own functions is specific to the game engine used. For Bevy, see the chapter [Custom Functions](../bevy_plugin/custom_functions.md). diff --git a/docs/src/yarn_files/graph.png b/docs/src/yarn_files/graph.png new file mode 100644 index 00000000..85c8d24b Binary files /dev/null and b/docs/src/yarn_files/graph.png differ diff --git a/docs/src/working_with_yarn_slinger/yarn_files/hello_world.png b/docs/src/yarn_files/hello_world.png similarity index 100% rename from docs/src/working_with_yarn_slinger/yarn_files/hello_world.png rename to docs/src/yarn_files/hello_world.png diff --git a/docs/src/working_with_yarn_slinger/yarn_files/lines.md b/docs/src/yarn_files/lines.md similarity index 93% rename from docs/src/working_with_yarn_slinger/yarn_files/lines.md rename to docs/src/yarn_files/lines.md index 69676b64..8867347f 100644 --- a/docs/src/working_with_yarn_slinger/yarn_files/lines.md +++ b/docs/src/yarn_files/lines.md @@ -1,7 +1,7 @@ # Lines As you might have figured out by playing around with the file from last chapter, a *line* -can be spoken by a character. The following yarn file... +can be spoken by a character. The following Yarn file... ```text title: Start --- diff --git a/docs/src/working_with_yarn_slinger/yarn_files/markup.md b/docs/src/yarn_files/markup.md similarity index 100% rename from docs/src/working_with_yarn_slinger/yarn_files/markup.md rename to docs/src/yarn_files/markup.md diff --git a/docs/src/working_with_yarn_slinger/yarn_files/narrator.png b/docs/src/yarn_files/narrator.png similarity index 100% rename from docs/src/working_with_yarn_slinger/yarn_files/narrator.png rename to docs/src/yarn_files/narrator.png diff --git a/docs/src/yarn_files/nodes.md b/docs/src/yarn_files/nodes.md new file mode 100644 index 00000000..76c658f3 --- /dev/null +++ b/docs/src/yarn_files/nodes.md @@ -0,0 +1,49 @@ +# Nodes + +In the last chapters, we have so far only used a single node named "Start". +We will now use multiple nodes and `jump` between them to compose a more complex dialogue: + +```text +title: Start +--- +Ferris: Say, do you want to go on an adventure? +-> Aye aye! + Ferris: Great, let's go! + <> +-> No thanks. + Ferris: Okay, that's fine. + <> +=== + +title: Adventure +--- +Narrator: And so, the two friends went on an adventure. +Dictionary: timeskip (pl. timeskips)(fandom slang): An instance of fast-forwarding a substantial amount of time, such as years or decades, as a narrative device in a story, quickly aging characters and developing events. +Ferris: Wow, that was a great adventure! +<> +=== + +title: GoodBye +--- +Narrator: And everyone lived happily ever after. +=== +``` + +Here we've got three nodes: "Start", "Adventure", and "GoodBye". We jump between them using the `jump` command. +You can see that we always arrive at the node "GoodBye", but optionally go through the node "Adventure" first. +If you're editing your Yarn file using Visual Studio Code and have the Yarn Spinner extension installed, +you can display this flow in a graph by clicking the "Show Graph" button in the upper right corner, which will show you something like this: +![graph.png](graph.png) + +It is allowed to jump to your current node: +```text +title: Start +--- +Ferris: Say, do you want to go on an adventure? +-> Aye aye! + Ferris: Great, let's go! +-> No thanks. + Ferris: Pretty please? + <> +=== +``` diff --git a/docs/src/working_with_yarn_slinger/yarn_files/options.md b/docs/src/yarn_files/options.md similarity index 71% rename from docs/src/working_with_yarn_slinger/yarn_files/options.md rename to docs/src/yarn_files/options.md index 6e36c408..714d9b2d 100644 --- a/docs/src/working_with_yarn_slinger/yarn_files/options.md +++ b/docs/src/yarn_files/options.md @@ -25,7 +25,22 @@ Ferris: So, how's life these days? === ``` In this example, the character "Ferris" will only answer with "That's great to hear!" if the player chooses the first option. -You can also next options inside of each other: +This can also be used to conditionally set variables: + +```text +title: Start +--- +Ferris: So, how's life these days? +<> +-> Pretty good, actually. + <> +-> Could be better. + <> +Ferris: I see. So you're feeling {$mood}? +=== +``` + +You can also nest options within options: ```text title: Start --- @@ -55,7 +70,7 @@ Shopkeeper: Here you go! === ``` -Finally, boolean variables can be used to determine whether an option should be available or not: +Finally, boolean [variables](variables.md) can be used to determine whether an option should be available or not: ```text title: Start --- @@ -70,3 +85,7 @@ Shopkeeper: Welcome to my shop! What can I do for you? The above file will result in the following dialogue window: ![cond_options.png](cond_options.png) + +Keeping the disabled options hidden is the behavior of the [dialogue view](../bevy_plugin/dialog_views.md) used here, +but these options are delivered to the view, which means you could still show them to the user +in e.g. a greyed-out state. diff --git a/docs/src/working_with_yarn_slinger/yarn_files/options.png b/docs/src/yarn_files/options.png similarity index 100% rename from docs/src/working_with_yarn_slinger/yarn_files/options.png rename to docs/src/yarn_files/options.png diff --git a/docs/src/working_with_yarn_slinger/yarn_files/running_examples.md b/docs/src/yarn_files/running_examples.md similarity index 78% rename from docs/src/working_with_yarn_slinger/yarn_files/running_examples.md rename to docs/src/yarn_files/running_examples.md index b866b308..cea28ed7 100644 --- a/docs/src/working_with_yarn_slinger/yarn_files/running_examples.md +++ b/docs/src/yarn_files/running_examples.md @@ -1,6 +1,6 @@ # Running Examples -You can run yarn files by copy-pasting them into [Try Yarn Spinner](https://try.yarnspinner.dev). +You can run Yarn files by copy-pasting them into [Try Yarn Spinner](https://try.yarnspinner.dev). This is nice because it runs directly in your browser and so doesn't require any setup. Since Yarn Slinger and Yarn Spinner read Yarn files the same way, the behavior will be identical to how it would be in your game. The only thing to look out for is that Try Yarn Spinner will only start at a node named "Start". @@ -21,23 +21,27 @@ Then, in your `src/main.rs`, add the following code: ```rust // src/main.rs -use bevy::prelude::*; +use bevy::{prelude::*, asset::ChangeWatcher, utils::Duration}; use bevy_yarn_slinger::prelude::*; use bevy_yarn_slinger_example_dialogue_view::prelude::*; fn main() { let mut app = App::new(); - app.add_plugins(DefaultPlugins.set(AssetPlugin { - watch_for_changes: true, + + app.add_plugins(( + DefaultPlugins.set(AssetPlugin { + watch_for_changes: ChangeWatcher::with_delay(Duration::from_millis(200)), ..default() - })) - .add_plugins(YarnSlingerPlugin::new()) - .add_plugins(ExampleYarnSlingerDialogueViewPlugin::new()) - .add_systems(Update, ( - setup_camera.on_startup(), - spawn_dialogue_runner.run_if(resource_added::()), - )) - .run(); + }), + YarnSlingerPlugin::new(), + ExampleYarnSlingerDialogueViewPlugin::new(), + )) + .add_systems(Startup, setup_camera) + .add_systems( + Update, + spawn_dialogue_runner.run_if(resource_added::()), + ) + .run(); } fn setup_camera(mut commands: Commands) { @@ -51,8 +55,8 @@ fn spawn_dialogue_runner(mut commands: Commands, project: Res) { } ``` -Don't worry, we will look at what this code does in detail later. For now, just treat it as something -that runs your Yarn files. +Don't worry, we will look at what this code does in detail later, in the chapter [Bevy Plugin / Setup](../bevy_plugin/setup.md). +For now, just treat it as something that runs your Yarn files. Finally, add your Yarn files to the assets. Inside the folder `assets/dialogue`, add a file named `example.yarn` with the content you want to run. Let's use the example from the last chapter: diff --git a/docs/src/working_with_yarn_slinger/yarn_files/variables.md b/docs/src/yarn_files/variables.md similarity index 68% rename from docs/src/working_with_yarn_slinger/yarn_files/variables.md rename to docs/src/yarn_files/variables.md index cc123100..ec1305dc 100644 --- a/docs/src/working_with_yarn_slinger/yarn_files/variables.md +++ b/docs/src/yarn_files/variables.md @@ -37,9 +37,32 @@ Ferris: Hello, {name}! Wow, you're {$age} years old now! Time sure flies, eh? === ``` +## Types + Variables can have the following types: - `string`: A string of characters, like `"Hello World!"`. - `number`: A number, like `42`, `0`, `-99999`, `3.1415`, or `6.0`. - `boolean`: Either `true` or `false`. All variable names must start with a `$` and can only contain letters, numbers, and underscores (`_`). + +## Conditional lines + +Boolean variables or conditions can be used to only show lines according to a condition: + +```text +title: Start +--- +<> +< 2>> +Apple Aficionado: Woah, that's a lot of apples! +< 1>> +Apple Aficionado: Congrats, that's an appropriate amount of apples. +<> +Apple Aficionado: You should get more apples. +<> +=== +``` + +This Yarn file will result in dialog that only prints the first line: +![apples.png](apples.png) diff --git a/docs/src/working_with_yarn_slinger/yarn_files/variables.png b/docs/src/yarn_files/variables.png similarity index 100% rename from docs/src/working_with_yarn_slinger/yarn_files/variables.png rename to docs/src/yarn_files/variables.png