1use std::{collections::HashMap, path::PathBuf};
5
6use bevy::{
7 asset::{AssetLoader, AssetPath, LoadedAsset},
8 prelude::{warn, Handle},
9 reflect::{TypePath, TypeUuid},
10};
11use chapter::{expand_format_functions, Line, LineInfo, MetadataInfo, Program};
12use csv::{Reader, ReaderBuilder};
13use prost::Message;
14use regex::Regex;
15
16#[derive(Debug, TypeUuid, TypePath)]
19#[uuid = "e0021061-5ff9-4134-992e-a9352d8854cd"]
20pub struct BevyYarnProgram {
21 pub program: Program,
23
24 pub string_table: Handle<BevyYarnStringTable>,
26
27 pub metadata_table: Handle<BevyYarnMetadataTable>,
29}
30
31pub(crate) fn get_table_pathbuf_from_yarnc_path<P>(yarnc_path: P, prefix: &str) -> PathBuf
32where
33 P: Into<PathBuf>,
34{
35 let mut pb: PathBuf = yarnc_path.into();
36 pb.set_file_name(format!(
37 "{}.{prefix}.csv",
38 pb.file_stem().unwrap().to_str().unwrap()
39 ));
40 pb
41}
42
43#[derive(Default)]
45pub struct BevyYarnProjectAssetLoader;
46
47impl AssetLoader for BevyYarnProjectAssetLoader {
48 fn load<'a>(
49 &'a self,
50 bytes: &'a [u8],
51 load_context: &'a mut bevy::asset::LoadContext,
52 ) -> bevy::utils::BoxedFuture<'a, Result<(), bevy::asset::Error>> {
53 Box::pin(async move {
54 let program = Program::decode(bytes)?;
56
57 let path = get_table_pathbuf_from_yarnc_path(load_context.path(), "lines");
59 let string_asset_path = AssetPath::new(path, None);
60 let string_table: Handle<BevyYarnStringTable> =
61 load_context.get_handle(string_asset_path.clone());
62
63 let path = get_table_pathbuf_from_yarnc_path(load_context.path(), "metadata");
65 let metadata_asset_path = AssetPath::new(path, None);
66 let metadata_table: Handle<BevyYarnMetadataTable> =
67 load_context.get_handle(metadata_asset_path.clone());
68
69 load_context.set_default_asset(
71 LoadedAsset::new(BevyYarnProgram {
72 program,
73 string_table,
74 metadata_table,
75 })
76 .with_dependencies(vec![string_asset_path, metadata_asset_path]),
77 );
78
79 Ok(())
80 })
81 }
82
83 fn extensions(&self) -> &[&str] {
84 &["yarnc"]
85 }
86}
87
88#[derive(Default, Debug, TypeUuid, TypePath)]
90#[uuid = "d11069b5-98c8-4db0-8616-58d86ee1deb3"]
91pub struct BevyYarnStringTable(pub HashMap<String, LineInfo>);
92
93impl BevyYarnStringTable {
94 fn find_string_in_table(&self, id: &String) -> String {
96 if let Some(text) = self.0.get(id).map(|line_info| line_info.text.clone()) {
97 text
98 } else {
99 warn!("Line id {id} missing from string table. Skipping");
100 format!("<missing_string: {id}>")
101 }
102 }
103
104 fn perform_variable_substitutions(initial: String, substitutions: &[String]) -> String {
106 substitutions
107 .iter()
108 .enumerate()
109 .fold(initial, |current, (idx, next_sub)| {
110 current.replace(&format!("{{{idx}}}"), next_sub)
111 })
112 }
113
114 fn extract_character(formatted_text: String) -> (Option<String>, String) {
117 let character_regex: Regex = Regex::new(r"([a-zA-Z0-9]+:)?\s*(.*)").unwrap();
118
119 match character_regex.captures(&formatted_text) {
120 Some(captures) => {
121 if captures.len() == 3 {
122 (
123 captures
124 .get(1)
125 .map(|val| val.as_str().to_owned().replace(':', "")),
126 captures.get(2).unwrap().as_str().to_owned(),
127 )
128 } else {
129 (None, formatted_text)
130 }
131 }
132 None => (None, formatted_text),
133 }
134 }
135
136 pub fn get_final_text(&self, line: &Line, local_code: &str) -> (Option<String>, String) {
138 let initial = self.find_string_in_table(&line.id);
139 let (character, initial) = Self::extract_character(initial);
140 let subbed_text = Self::perform_variable_substitutions(initial, &line.substitutions);
141 (character, expand_format_functions(&subbed_text, local_code))
142 }
143}
144
145#[derive(Default)]
147pub struct BevyYarnStringTableAssetLoader;
148
149impl AssetLoader for BevyYarnStringTableAssetLoader {
150 fn load<'a>(
151 &'a self,
152 bytes: &'a [u8],
153 load_context: &'a mut bevy::asset::LoadContext,
154 ) -> bevy::utils::BoxedFuture<'a, Result<(), bevy::asset::Error>> {
155 Box::pin(async move {
156 let string_table =
157 HashMap::from_iter(Reader::from_reader(bytes).deserialize().map(|result| {
158 let res: LineInfo = result.unwrap();
159 (res.id.clone(), res)
160 }));
161
162 load_context.set_default_asset(LoadedAsset::new(BevyYarnStringTable(string_table)));
163
164 Ok(())
165 })
166 }
167
168 fn extensions(&self) -> &[&str] {
169 &["lines.csv"]
170 }
171}
172
173#[derive(Default, Debug, TypeUuid, TypePath)]
175#[uuid = "42073437-7c2b-4526-859c-f1b059881c67"]
176pub struct BevyYarnMetadataTable(pub HashMap<String, MetadataInfo>);
177
178impl BevyYarnMetadataTable {
179 pub fn get_tags_for_line(&self, line: &Line) -> Vec<String> {
181 self.0
182 .get(&line.id)
183 .map(|metadata_info| &metadata_info.tags)
184 .cloned()
185 .unwrap_or_default()
186 }
187}
188
189#[derive(Default)]
191pub struct BevyYarnMetadataTableAssetLoader;
192
193impl AssetLoader for BevyYarnMetadataTableAssetLoader {
194 fn load<'a>(
195 &'a self,
196 bytes: &'a [u8],
197 load_context: &'a mut bevy::asset::LoadContext,
198 ) -> bevy::utils::BoxedFuture<'a, Result<(), bevy::asset::Error>> {
199 Box::pin(async move {
200 let metadata_table = HashMap::from_iter(
201 ReaderBuilder::new()
202 .flexible(true)
203 .from_reader(bytes)
204 .deserialize()
205 .map(|result| {
206 if result.is_err() {
207 warn!("[{:?}] {result:?}\n", load_context.path());
208 }
209
210 let res: MetadataInfo = result.unwrap();
211 (res.id.clone(), res)
212 }),
213 );
214
215 load_context.set_default_asset(LoadedAsset::new(BevyYarnMetadataTable(metadata_table)));
216
217 Ok(())
218 })
219 }
220
221 fn extensions(&self) -> &[&str] {
222 &["metadata.csv"]
223 }
224}