use super::*; /// A monologue containing a list of optional lines #[derive(Asset, TypePath, Debug, Deserialize, Default, Clone, PartialEq)] pub(crate) struct Monologue { pub batches: Vec, } impl Monologue { pub fn get(&self, idx: usize) -> Option<&MonologueLineBatch> { self.batches.get(idx) } pub fn add_batch(&mut self, group: MonologueLineBatch) { self.batches.push(group); } } #[derive(Debug, Error)] pub struct MonologueParseError; impl std::fmt::Display for MonologueParseError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "Could not parse monologue file") } } impl TryFrom<&str> for Monologue { type Error = MonologueParseError; fn try_from(value: &str) -> Result { // Create a blank monologue to populate let mut monologue = Monologue::default(); // Add an initial batch that may end up being empty monologue.add_batch(MonologueLineBatch::default()); // Iterate over raw string lines in the .mono file for line in value.lines() { // Break up into batches by --- separator if line.starts_with("---") { monologue.add_batch(MonologueLineBatch::default()); // Skip any empty lines or comments } else if line.starts_with("#") || line.is_empty() { // Skip comments and blank lines // everything else we read as a monologue line } else { monologue.batches.last_mut().unwrap().add_line(line.into()); } } // Clear empty batches monologue.batches.retain(|batch| !batch.lines.is_empty()); Ok(monologue) } } /// A set of possible lines in a monologue #[derive(Debug, Deserialize, Default, Clone, PartialEq)] pub(crate) struct MonologueLineBatch { pub lines: Vec, } impl MonologueLineBatch { fn add_line(&mut self, line: MonologueLine) { self.lines.push(line); } } /// A single monologue line #[derive(Debug, Deserialize, Default, Clone, PartialEq)] pub(crate) struct MonologueLine { pub value: String, } impl Display for MonologueLine { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", self.value) } } impl From for MonologueLine { fn from(value: String) -> Self { MonologueLine { value } } } impl From for String { fn from(val: MonologueLine) -> Self { val.value } } impl From<&str> for MonologueLine { fn from(value: &str) -> Self { MonologueLine { value: value.into(), } } } #[cfg(test)] mod tests { use super::*; use indoc::*; #[test] fn empty_monologue() { const MONO: &str = ""; let parsed: Monologue = MONO.try_into().unwrap(); let expected = Monologue::default(); assert_eq!(parsed, expected); } #[test] fn complicated_empty_monologue() { const MONO: &str = indoc! {" --- --- # a comment --- --- # more stuff # another comment --- --- "}; let parsed: Monologue = MONO.try_into().unwrap(); let expected = Monologue::default(); assert_eq!(parsed, expected); } #[test] fn basic_monologue() { const MONO: &str = indoc! {" --- hello --- world --- "}; let parsed: Monologue = MONO.try_into().unwrap(); let expected = Monologue { batches: vec![ MonologueLineBatch { lines: vec!["hello".into()], }, MonologueLineBatch { lines: vec!["world".into()], }, ], }; assert_eq!(parsed, expected); } #[test] fn basic_with_comments_monologue() { const MONO: &str = indoc! {" # Some stuff before we get started --- # I really like this line hello --- # I'm skeptical about this one... world # This line needs work --- # More notes after the lines "}; let parsed: Monologue = MONO.try_into().unwrap(); let expected = Monologue { batches: vec![ MonologueLineBatch { lines: vec!["hello".into()], }, MonologueLineBatch { lines: vec!["world".into()], }, ], }; assert_eq!(parsed, expected); } #[test] fn batches_monologue() { const MONO: &str = indoc! {" a b c --- d e --- # with comment before... f # ...and after "}; let parsed: Monologue = MONO.try_into().unwrap(); let expected = Monologue { batches: vec![ MonologueLineBatch { lines: vec!["a".into(), "b".into(), "c".into()], }, MonologueLineBatch { lines: vec!["d".into(), "e".into()], }, MonologueLineBatch { lines: vec!["f".into()], }, ], }; assert_eq!(parsed, expected); } } #[derive(Default)] struct MonologueLoader; #[derive(Debug, Error)] enum MonologueLoaderError { #[error("Could not load asset: {0}")] Io(#[from] std::io::Error), #[error("Could not parse utf8")] Utf8(#[from] std::string::FromUtf8Error), // TODO: Real errors #[error("Could not as monologue")] Parse(#[from] MonologueParseError), } impl AssetLoader for MonologueLoader { type Asset = Monologue; type Settings = (); type Error = MonologueLoaderError; async fn load( &self, reader: &mut dyn Reader, _settings: &(), _load_context: &mut LoadContext<'_>, ) -> Result { let mut bytes: Vec = Vec::new(); reader.read_to_end(&mut bytes).await?; let raw_string = String::from_utf8(bytes)?; Ok(Monologue::try_from(raw_string.as_str())?) } fn extensions(&self) -> &[&str] { &["mono"] } } pub struct MonologueAssetsPlugin; impl Plugin for MonologueAssetsPlugin { fn build(&self, app: &mut App) { app.init_asset::() .init_asset_loader::(); } }