use bevy::prelude::*; use serde_yaml::Value; use serde::Deserialize; use crate::{audio, hud, settings, world}; pub const CHATS: &[&str] = &[ include_str!("chats/serenity.yaml"), include_str!("chats/startrans.yaml"), ]; pub const TOKEN_CHAT: &str = "chat"; pub const TOKEN_SYSTEM: &str = "system"; pub const TOKEN_WARN: &str = "warn"; pub const TOKEN_SET: &str = "set"; pub const TOKEN_IF: &str = "if"; pub const TOKEN_GOTO: &str = "goto"; pub const TOKEN_LABEL: &str = "label"; pub const TOKEN_SCRIPT: &str = "script"; pub const NAME_FALLBACK: &str = "Unknown"; pub const CHOICE_TIMER: f64 = 40.0 * settings::DEFAULT_CHAT_SPEED as f64; pub const LETTERS_PER_SECOND: f32 = 17.0; pub const TALKER_SPEED_FACTOR: f32 = settings::DEFAULT_CHAT_SPEED / LETTERS_PER_SECOND; pub const CHAT_SPEED_MIN_LEN: f32 = 40.0; pub const NON_CHOICE_TOKENS: &[&str] = &[ TOKEN_CHAT, TOKEN_SYSTEM, TOKEN_WARN, TOKEN_SET, TOKEN_IF, TOKEN_GOTO, TOKEN_LABEL, TOKEN_SCRIPT, ]; pub struct ChatPlugin; impl Plugin for ChatPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, load_chats); app.add_systems(Update, ( handle_reply_keys.before(handle_chat_timer), handle_chat_timer.before(handle_chat_events), handle_new_conversations.before(handle_chat_events), handle_chat_events, )); app.add_event::(); app.add_event::(); app.insert_resource(ChatDB(Vec::new())); } } type ChatPos = Vec; #[derive(Component)] pub struct Chat { pub id: usize, pub position: ChatPos, pub timer: f64, pub talker: Talker, } #[derive(Component)] pub struct Choice { pub text: String, pub key: usize, pub goto: ChatPos, } // This is the only place where any YAML interaction should be happening. #[derive(Resource)] pub struct ChatDB(Vec); impl ChatDB { pub fn load_from_str(&mut self, yaml_string: &str) -> Result<(), ()> { let mut count = 0; for document in serde_yaml::Deserializer::from_str(yaml_string) { match Value::deserialize(document) { Ok(yaml_data) => { if let Value::Sequence(yaml_sequence) = yaml_data { self.0.push(Value::Sequence(yaml_sequence)); count += 1; } else { error!("Could not load YAML: {:?}", yaml_data); } } Err(error) => { dbg!(error); return Err(()); } } } info!("Loaded {count} conversations"); return Ok(()); } pub fn get_chat_by_id(&self, id: &String) -> Result { let mut found: Option = None; for (index, object_yaml) in self.0.iter().enumerate() { if let Some(chat_id) = object_yaml[0][TOKEN_CHAT].as_str() { if chat_id == id { if found.is_some() { return Err("Found multiple chats with the same id!".to_string()); } found = Some(index); } } } if let Some(result) = found { return Ok(result); } return Err(format!("No chat with the conversation ID `{id}` was found.")); } // For a given Value, check whether it's a Value::Mapping and whether it // contains a choice. Mappings that will be detected as choices: // - `{"What's up?": [...]}` // - `{"What's up?": [...], "if": "value > 3"}` // Not acceptable: // - `"What's up?"` // - `{"goto": "foo"}` fn search_choice(&self, yaml: Option<&Value>) -> Option<(String, Value)> { let non_choice_tokens = NON_CHOICE_TOKENS.to_vec(); if let Some(Value::Mapping(hash)) = yaml { for key in hash.keys() { if let Value::String(key) = key { if non_choice_tokens.contains(&key.as_str()) { continue; } return Some((key.into(), hash[key].clone())); } } } return None; } // returns false if the advanced pointer is out of bounds fn advance_pointer(&self, chat: &mut Chat) -> bool { let len = chat.position.len(); if len == 0 { return false; } chat.position[len - 1] += 1; while chat.position.len() > 0 { dbg!(&chat.position); dbg!(self.at(chat.id, &chat.position)); match self.at(chat.id, &chat.position) { None => { dbg!("Pop."); chat.position.pop(); if chat.position.len() > 0 { let index = chat.position.len() - 1; chat.position[index] += 1; } }, Some(_) => { break; } } } if chat.position.len() == 0 { // out of bounds, return false. return false; } return true; } // Note that this (intentionally) may result in a pointer that's out of bounds. fn pointer_lookahead(&self, position: &mut Vec) { let index = position.len() - 1; position[index] += 1; } // Returns the Value at the given ID/position, as-is. // If it's a choice, it returns the entire {"choice text": [...]} mapping. fn at(&self, id: usize, position: &Vec) -> Option { if position.len() == 0 { return None; } let mut result: Option = None; let mut pointer: Option = Some(self.0[id].clone()); let mut next_pointer: Option = None; for index in position { if let Some(Value::Sequence(seq)) = &pointer { let value = seq.get(*index); match value { Some(Value::String(value_string)) => { result = Some(Value::String(value_string.into())); } Some(Value::Mapping(mapping)) => { if let Some((_choicetext, subconversation)) = self.search_choice(value) { result = Some(Value::Mapping(mapping.clone())); next_pointer = Some(subconversation); } else { result = Some(Value::Mapping(mapping.clone())); } } None => { // Out of bounds. return None; } _ => { error!("Could not handle YAML value {value:?}"); return None; } } pointer = next_pointer; next_pointer = None; } else { return None; } } return result; } pub fn advance_chat(&self, chat: &mut Chat, event: &mut EventWriter) { event.send(ChatEvent::DespawnAllChoices); let conv = &self.0.get(chat.id); if conv.is_none() { return; } // Handle next entry in the chat list let mut is_skipping_through = false; if !self.advance_pointer(chat) { event.send(ChatEvent::SpawnMessage("Disconnected.".to_string())); event.send(ChatEvent::DespawnAllChats); return; } else if let Some(_) = self.search_choice(self.at(chat.id, &chat.position).as_ref()) { is_skipping_through = true; } else if let Some(Value::String(message)) = self.at(chat.id, &chat.position) { event.send(ChatEvent::SpawnMessage(message.to_string())); } // Check if the immediately following entries are choices let mut pos = chat.position.clone(); self.pointer_lookahead(&mut pos); let mut key: usize = 0; loop { if is_skipping_through /*|| pos[0] >= conv.len()*/ { // TODO: out of bounds checking break; } dbg!(&pos); dbg!(self.at(chat.id, &pos)); let choice = self.search_choice(self.at(chat.id, &pos).as_ref()); dbg!(&choice); if let Some((choice, _)) = choice { let mut goto: Vec = pos.clone(); goto.push(0); event.send(ChatEvent::SpawnChoice(choice, key, goto)); key += 1; } else { break; } self.pointer_lookahead(&mut pos); } } } #[derive(Component)] #[derive(Clone)] pub struct Talker { pub conv_id: String, pub name: Option, pub pronoun: Option, pub talking_speed: f32, } #[derive(Event)] pub struct StartConversationEvent { pub talker: Talker, } #[derive(Event)] pub enum ChatEvent { DespawnAllChoices, DespawnAllChats, SpawnMessage(String), SpawnChoice(String, usize, ChatPos), //Script(String, String, String), } pub fn load_chats(mut chatdb: ResMut) { for chat_yaml in CHATS { if chatdb.load_from_str(chat_yaml).is_err() { error!("Could not load chat definitions. Validate files in `src/chats/` path."); } } // let mut chat = Chat { // id: 2, // position: vec![0], // timer: 0.0, // talker: Talker { // conv_id: "Icarus".to_string(), // name: None, // pronoun: None, // talking_speed: 1.0, // } // }; dbg!(chatdb.at(2, &vec![0])); dbg!(chatdb.at(2, &vec![1])); dbg!(chatdb.at(2, &vec![2])); dbg!(chatdb.at(2, &vec![3])); } pub fn handle_new_conversations( mut commands: Commands, mut er_conv: EventReader, mut ew_sfx: EventWriter, mut ew_chatevent: EventWriter, chatdb: Res, q_chats: Query<&Chat>, time: Res