1#![deny(missing_docs)]
2
3use header_parsing::parse_header;
6use thiserror::Error;
7
8use std::{
9 collections::{HashMap, HashSet},
10 fs::{File, read_dir},
11 hash::Hash,
12 io::{BufRead, BufReader},
13 path::{Path, PathBuf},
14};
15
16#[derive(Clone, Debug)]
18pub struct DialogLine<P> {
19 pub text: Box<str>,
21 pub actions: HashSet<P>,
23}
24
25#[derive(Clone, Debug)]
27pub struct DialogBlock<P> {
28 pub name: Box<str>,
30 pub lines: Vec<DialogLine<P>>,
32 pub final_actions: HashSet<P>,
34}
35
36impl<P> DialogBlock<P> {
37 fn new() -> Self {
38 Self {
39 name: "".into(),
40 lines: Vec::new(),
41 final_actions: HashSet::new(),
42 }
43 }
44
45 fn is_empty(&self) -> bool {
46 self.name.is_empty() && self.lines.is_empty() && self.final_actions.is_empty()
47 }
48
49 pub fn lines(&self) -> impl Iterator<Item = &str> {
51 self.lines.iter().map(|line| line.text.as_ref())
52 }
53}
54
55pub trait DialogParameter: Sized {
57 type Context;
59 fn create(name: &str, context: &mut Self::Context) -> Option<Self>;
61}
62
63pub trait DialogChange: Sized {
65 type Parameter: DialogParameter + Clone + Eq + Hash;
67
68 fn default_change(parameter: Self::Parameter) -> Self;
70
71 fn value_change(
73 parameter: Self::Parameter,
74 value: &str,
75 context: &mut <<Self as DialogChange>::Parameter as DialogParameter>::Context,
76 ) -> Self;
77}
78
79pub struct DialogSequence<C, P> {
81 pub blocks: Vec<DialogBlock<P>>,
83 pub changes: HashMap<P, Vec<C>>,
85}
86
87pub trait DialogMap<C: DialogChange>: Default {
89 fn add(&mut self, key: Vec<Box<str>>, value: DialogSequence<C, C::Parameter>);
91}
92
93impl<C: DialogChange> DialogMap<C> for HashMap<Vec<Box<str>>, DialogSequence<C, C::Parameter>> {
94 fn add(&mut self, key: Vec<Box<str>>, value: DialogSequence<C, C::Parameter>) {
95 self.insert(key, value);
96 }
97}
98
99impl<C: DialogChange> DialogMap<C> for Vec<DialogSequence<C, C::Parameter>> {
100 fn add(&mut self, _key: Vec<Box<str>>, value: DialogSequence<C, C::Parameter>) {
101 self.push(value);
102 }
103}
104
105#[derive(Debug, Error)]
107pub enum ParsingError {
108 #[error("Colon parameters are not allowed to have a value supplied")]
110 ColonParameterWithValues,
111 #[error("Error while opening story file: {0}")]
113 OpeningError(PathBuf),
114 #[error("Error while reading story file: {0}")]
116 ReadingError(PathBuf),
117 #[error("Subheader found without a matching header")]
119 SubheaderWithoutHeader,
120 #[error("Invalid dialog format")]
122 InvalidIndentation,
123 #[error("Invalid indentation level")]
125 IndentationTooHigh,
126 #[error("Default parameters cannot have a value supplied")]
128 DefaultParameterWithValue,
129 #[error("Duplicate definition of change: {0}")]
131 DuplicateDefinitionOfChange(Box<str>),
132}
133
134impl<C: DialogChange> DialogSequence<C, C::Parameter> {
135 fn new() -> Self {
136 Self {
137 blocks: Vec::new(),
138 changes: HashMap::new(),
139 }
140 }
141
142 pub fn map_from_path<M: DialogMap<C>>(
147 path: &Path,
148 context: &mut <C::Parameter as DialogParameter>::Context,
149 ) -> Result<M, ParsingError> {
150 let mut text_map = M::default();
151 Self::fill_map_from_path(path, &mut text_map, context)?;
152 Ok(text_map)
153 }
154
155 pub fn fill_map_from_path<M: DialogMap<C>>(
160 path: &Path,
161 text_map: &mut M,
162 context: &mut <C::Parameter as DialogParameter>::Context,
163 ) -> Result<(), ParsingError> {
164 Self::named_fill_map_from_path(path, text_map, Vec::new(), context)
165 }
166
167 fn named_fill_map_from_path<M: DialogMap<C>>(
168 path: &Path,
169 text_map: &mut M,
170 default_name: Vec<Box<str>>,
171 context: &mut <C::Parameter as DialogParameter>::Context,
172 ) -> Result<(), ParsingError> {
173 let Ok(dirs) = read_dir(path) else {
174 return Self::fill_map_from_file(path, default_name, text_map, context);
175 };
176
177 for dir in dirs.flatten() {
178 Self::try_fill_submap_from_path(&dir.path(), default_name.clone(), text_map, context)?;
179 }
180
181 Ok(())
182 }
183
184 fn try_fill_submap_from_path<M: DialogMap<C>>(
185 path: &Path,
186 mut relative_name: Vec<Box<str>>,
187 text_map: &mut M,
188 context: &mut <C::Parameter as DialogParameter>::Context,
189 ) -> Result<(), ParsingError> {
190 let Some(name) = path.file_stem() else {
191 return Ok(());
192 };
193
194 let Some(name) = name.to_str() else {
195 return Ok(());
196 };
197
198 relative_name.push(name.into());
199 Self::named_fill_map_from_path(path, text_map, relative_name, context)
200 }
201
202 fn handle_content_line(
203 &mut self,
204 line: &str,
205 mut current_block: DialogBlock<C::Parameter>,
206 path: &mut Vec<Box<str>>,
207 context: &mut <C::Parameter as DialogParameter>::Context,
208 ) -> Result<DialogBlock<C::Parameter>, ParsingError> {
209 if line.trim().is_empty() {
210 if !current_block.is_empty() {
211 self.blocks.push(current_block);
212 current_block = DialogBlock::new();
213 }
214
215 return Ok(current_block);
216 }
217
218 let mut spaces = 0;
219 let mut chars = line.chars();
220 let mut c = chars.next().unwrap();
221 while c == ' ' {
222 spaces += 1;
223 c = chars.next().unwrap();
224 }
225 let first = c;
226
227 if first == '-' {
228 if spaces % 2 != 0 {
229 return Err(ParsingError::InvalidIndentation);
230 }
231 let level = spaces / 2;
232 if level > path.len() {
233 return Err(ParsingError::IndentationTooHigh);
234 }
235 while path.len() > level {
236 path.pop();
237 }
238 let line = line[(spaces + 1)..].trim();
239 let (name_end, value) = line
240 .split_once(' ')
241 .map_or((line, ""), |(name, value)| (name.trim(), value.trim()));
242 let default = name_end.ends_with('!');
243
244 if default && !value.is_empty() {
245 return Err(ParsingError::DefaultParameterWithValue);
246 }
247
248 let colon_end = name_end.ends_with(':');
249
250 let name_end: Box<str> = if default || colon_end {
251 &name_end[0..(name_end.len() - 1)]
252 } else {
253 name_end
254 }
255 .into();
256
257 if colon_end {
258 if !value.is_empty() {
259 return Err(ParsingError::ColonParameterWithValues);
260 }
261
262 path.push(name_end);
263 return Ok(current_block);
264 }
265
266 let parameter_name = path.iter().rev().fold(name_end.clone(), |name, element| {
267 format!("{element}:{name}").into()
268 });
269
270 path.push(name_end);
271
272 let Some(parameter) = DialogParameter::create(¶meter_name, context) else {
273 return Ok(current_block);
274 };
275
276 if current_block.final_actions.contains(¶meter) {
277 return Err(ParsingError::DuplicateDefinitionOfChange(parameter_name));
278 }
279
280 let change = if default {
281 DialogChange::default_change(parameter.clone())
282 } else {
283 DialogChange::value_change(parameter.clone(), value, context)
284 };
285
286 if let Some(map) = self.changes.get_mut(¶meter) {
287 map.push(change);
288 } else {
289 self.changes.insert(parameter.clone(), vec![change]);
290 }
291
292 current_block.final_actions.insert(parameter);
293
294 return Ok(current_block);
295 }
296
297 path.clear();
298
299 let (Some((name, text)), 0) = (line.split_once(':'), spaces) else {
300 current_block.lines.push(DialogLine {
301 text: line.trim().into(),
302 actions: current_block.final_actions,
303 });
304 current_block.final_actions = HashSet::new();
305
306 return Ok(current_block);
307 };
308
309 let text = text.trim();
310
311 let parameters = if !current_block.is_empty() {
312 self.blocks.push(current_block);
313 HashSet::new()
314 } else if !current_block.final_actions.is_empty() {
315 current_block.final_actions
316 } else {
317 HashSet::new()
318 };
319
320 let (parameters, lines) = if text.is_empty() {
321 (parameters, Vec::new())
322 } else {
323 (
324 HashSet::new(),
325 vec![DialogLine {
326 text: text.into(),
327 actions: parameters,
328 }],
329 )
330 };
331
332 Ok(DialogBlock {
333 name: name.trim().into(),
334 lines,
335 final_actions: parameters,
336 })
337 }
338
339 fn fill_map_from_file<M: DialogMap<C>>(
340 path: &Path,
341 default_name: Vec<Box<str>>,
342 text_map: &mut M,
343 context: &mut <C::Parameter as DialogParameter>::Context,
344 ) -> Result<(), ParsingError> {
345 let valid_path = path.extension().is_some_and(|e| e == "pk");
346
347 if !valid_path {
348 return Ok(());
349 }
350
351 let Ok(story_file) = File::open(path) else {
352 return Err(ParsingError::OpeningError(path.to_path_buf()));
353 };
354 let mut current_block = DialogBlock::new();
355 let mut current_sequence = Self::new();
356 let mut name = Vec::new();
357 let mut parameter_path = Vec::new();
358
359 for line in BufReader::new(story_file).lines() {
360 let Ok(line) = line else {
361 return Err(ParsingError::ReadingError(path.to_path_buf()));
362 };
363
364 if let Some(success) = parse_header(&mut name, &line) {
365 let Ok(changes) = success else {
366 return Err(ParsingError::SubheaderWithoutHeader);
367 };
368
369 if !current_block.is_empty() {
370 current_sequence.blocks.push(current_block);
371 current_block = DialogBlock::new();
372 }
373
374 if !current_sequence.blocks.is_empty() {
375 let mut new_name = default_name.clone();
376 new_name.extend(changes.path.clone());
377 text_map.add(new_name, current_sequence);
378 }
379 current_sequence = Self::new();
380
381 changes.apply();
382
383 continue;
384 }
385
386 current_block = current_sequence.handle_content_line(
387 &line,
388 current_block,
389 &mut parameter_path,
390 context,
391 )?;
392 }
393
394 if !current_block.is_empty() {
395 current_sequence.blocks.push(current_block);
396 }
397
398 if !current_sequence.blocks.is_empty() {
399 let mut new_name = default_name;
400 new_name.extend(name);
401 text_map.add(new_name, current_sequence);
402 }
403
404 Ok(())
405 }
406}