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, Error as IoError},
13 mem,
14 path::{Path, PathBuf},
15};
16
17#[derive(Clone, Debug)]
19pub struct DialogLine<P> {
20 pub text: Box<str>,
22 pub actions: HashSet<P>,
24}
25
26#[derive(Clone, Debug)]
28pub struct DialogBlock<P> {
29 pub name: Box<str>,
31 pub lines: Vec<DialogLine<P>>,
33 pub final_actions: HashSet<P>,
35}
36
37impl<P> DialogBlock<P> {
38 fn new() -> Self {
39 Self {
40 name: "".into(),
41 lines: Vec::new(),
42 final_actions: HashSet::new(),
43 }
44 }
45
46 fn is_empty(&self) -> bool {
47 self.name.is_empty() && self.lines.is_empty() && self.final_actions.is_empty()
48 }
49
50 pub fn lines(&self) -> impl Iterator<Item = &str> {
52 self.lines.iter().map(|line| line.text.as_ref())
53 }
54}
55
56pub trait DialogParameter: Sized {
58 type Context;
60 fn create(name: &str, context: &mut Self::Context) -> Option<Self>;
62}
63
64pub trait DialogChange: Sized {
66 type Parameter: DialogParameter + Clone + Eq + Hash;
68
69 fn default_change(parameter: Self::Parameter) -> Self;
71
72 fn value_change(
74 parameter: Self::Parameter,
75 value: &str,
76 context: &mut <<Self as DialogChange>::Parameter as DialogParameter>::Context,
77 ) -> Self;
78}
79
80pub struct DialogSequence<C, P> {
82 pub blocks: Vec<DialogBlock<P>>,
84 pub changes: HashMap<P, Vec<C>>,
86}
87
88pub trait DialogMap<C: DialogChange>: Default {
90 fn add(&mut self, key: Vec<Box<str>>, value: DialogSequence<C, C::Parameter>);
92}
93
94impl<C: DialogChange> DialogMap<C> for HashMap<Vec<Box<str>>, DialogSequence<C, C::Parameter>> {
95 fn add(&mut self, key: Vec<Box<str>>, value: DialogSequence<C, C::Parameter>) {
96 self.insert(key, value);
97 }
98}
99
100impl<C: DialogChange> DialogMap<C> for Vec<DialogSequence<C, C::Parameter>> {
101 fn add(&mut self, _key: Vec<Box<str>>, value: DialogSequence<C, C::Parameter>) {
102 self.push(value);
103 }
104}
105
106#[derive(Debug, Error)]
108pub enum ParsingError {
109 #[error("Colon parameters are not allowed to have a value supplied")]
111 ColonParameterWithValues,
112 #[error("Error while opening story file {path}: {source}")]
114 OpeningError {
115 path: PathBuf,
117 source: IoError,
119 },
120 #[error("Error while reading story file {path}: {source}")]
122 ReadingError {
123 path: PathBuf,
125 source: IoError,
127 },
128 #[error("Subheader found without a matching header")]
130 SubheaderWithoutHeader,
131 #[error("Invalid dialog format")]
133 InvalidIndentation,
134 #[error("Invalid indentation level")]
136 IndentationTooHigh,
137 #[error("Default parameters cannot have a value supplied")]
139 DefaultParameterWithValue,
140 #[error("Duplicate definition of change: {0}")]
142 DuplicateDefinitionOfChange(Box<str>),
143}
144
145impl<C: DialogChange> DialogSequence<C, C::Parameter> {
146 fn new() -> Self {
147 Self {
148 blocks: Vec::new(),
149 changes: HashMap::new(),
150 }
151 }
152
153 pub fn map_from_path<M: DialogMap<C>>(
158 path: &Path,
159 context: &mut <C::Parameter as DialogParameter>::Context,
160 ) -> Result<M, ParsingError> {
161 let mut text_map = M::default();
162 Self::fill_map_from_path(path, &mut text_map, context)?;
163 Ok(text_map)
164 }
165
166 pub fn fill_map_from_path<M: DialogMap<C>>(
171 path: &Path,
172 text_map: &mut M,
173 context: &mut <C::Parameter as DialogParameter>::Context,
174 ) -> Result<(), ParsingError> {
175 Self::named_fill_map_from_path(path, text_map, Vec::new(), context)
176 }
177
178 fn named_fill_map_from_path<M: DialogMap<C>>(
179 path: &Path,
180 text_map: &mut M,
181 default_name: Vec<Box<str>>,
182 context: &mut <C::Parameter as DialogParameter>::Context,
183 ) -> Result<(), ParsingError> {
184 let Ok(dirs) = read_dir(path) else {
185 return Self::fill_map_from_file(path, default_name, text_map, context);
186 };
187
188 for entry in dirs {
189 let Ok(dir) = entry else {
190 eprintln!("Warning: failed to read entry in {}", path.display());
191 continue;
192 };
193 Self::try_fill_submap_from_path(&dir.path(), default_name.clone(), text_map, context)?;
194 }
195
196 Ok(())
197 }
198
199 fn try_fill_submap_from_path<M: DialogMap<C>>(
200 path: &Path,
201 mut relative_name: Vec<Box<str>>,
202 text_map: &mut M,
203 context: &mut <C::Parameter as DialogParameter>::Context,
204 ) -> Result<(), ParsingError> {
205 let Some(name) = path.file_stem() else {
206 return Ok(());
207 };
208
209 let Some(name) = name.to_str() else {
210 return Ok(());
211 };
212
213 relative_name.push(name.into());
214 Self::named_fill_map_from_path(path, text_map, relative_name, context)
215 }
216
217 fn handle_content_line(
218 &mut self,
219 line: &str,
220 current_block: &mut DialogBlock<C::Parameter>,
221 path: &mut Vec<Box<str>>,
222 context: &mut <C::Parameter as DialogParameter>::Context,
223 ) -> Result<(), ParsingError> {
224 if line.trim().is_empty() {
225 if !current_block.is_empty() {
226 self.blocks
227 .push(mem::replace(current_block, DialogBlock::new()));
228 }
229
230 return Ok(());
231 }
232
233 let mut spaces = 0;
234 let mut chars = line.chars();
235 let mut c = chars.next().unwrap();
236 while c == ' ' {
237 spaces += 1;
238 c = chars.next().unwrap();
239 }
240 let first = c;
241
242 if first == '-' {
243 if spaces % 2 != 0 {
244 return Err(ParsingError::InvalidIndentation);
245 }
246 let level = spaces / 2;
247 if level > path.len() {
248 return Err(ParsingError::IndentationTooHigh);
249 }
250 while path.len() > level {
251 path.pop();
252 }
253 let line = line[(spaces + 1)..].trim();
254 let (name_end, value) = line
255 .split_once(' ')
256 .map_or((line, ""), |(name, value)| (name.trim(), value.trim()));
257 let default = name_end.ends_with('!');
258
259 if default && !value.is_empty() {
260 return Err(ParsingError::DefaultParameterWithValue);
261 }
262
263 let colon_end = name_end.ends_with(':');
264
265 let name_end: Box<str> = if default || colon_end {
266 &name_end[0..(name_end.len() - 1)]
267 } else {
268 name_end
269 }
270 .into();
271
272 if colon_end {
273 if !value.is_empty() {
274 return Err(ParsingError::ColonParameterWithValues);
275 }
276
277 path.push(name_end);
278 return Ok(());
279 }
280
281 let parameter_name = path.iter().rev().fold(name_end.clone(), |name, element| {
282 format!("{element}:{name}").into()
283 });
284
285 path.push(name_end);
286
287 let Some(parameter) = DialogParameter::create(¶meter_name, context) else {
288 return Ok(());
289 };
290
291 if current_block.final_actions.contains(¶meter) {
292 return Err(ParsingError::DuplicateDefinitionOfChange(parameter_name));
293 }
294
295 let change = if default {
296 DialogChange::default_change(parameter.clone())
297 } else {
298 DialogChange::value_change(parameter.clone(), value, context)
299 };
300
301 if let Some(map) = self.changes.get_mut(¶meter) {
302 map.push(change);
303 } else {
304 self.changes.insert(parameter.clone(), vec![change]);
305 }
306
307 current_block.final_actions.insert(parameter);
308
309 return Ok(());
310 }
311
312 path.clear();
313
314 let (Some((name, text)), 0) = (line.split_once(':'), spaces) else {
315 current_block.lines.push(DialogLine {
316 text: line.trim().into(),
317 actions: mem::take(&mut current_block.final_actions),
318 });
319
320 return Ok(());
321 };
322
323 let text = text.trim();
324
325 let parameters = if current_block.is_empty() {
326 mem::take(&mut current_block.final_actions)
327 } else {
328 let old = mem::replace(current_block, DialogBlock::new());
329 self.blocks.push(old);
330 HashSet::new()
331 };
332
333 current_block.name = name.trim().into();
334 if text.is_empty() {
335 current_block.final_actions = parameters;
336 } else {
337 current_block.lines = vec![DialogLine {
338 text: text.into(),
339 actions: parameters,
340 }];
341 }
342
343 Ok(())
344 }
345
346 fn fill_map_from_file<M: DialogMap<C>>(
347 path: &Path,
348 default_name: Vec<Box<str>>,
349 text_map: &mut M,
350 context: &mut <C::Parameter as DialogParameter>::Context,
351 ) -> Result<(), ParsingError> {
352 let valid_path = path.extension().is_some_and(|e| e == "pk");
353
354 if !valid_path {
355 return Ok(());
356 }
357
358 let story_file = File::open(path).map_err(|source| ParsingError::OpeningError {
359 path: path.to_path_buf(),
360 source,
361 })?;
362 let mut current_block = DialogBlock::new();
363 let mut current_sequence = Self::new();
364 let mut name = Vec::new();
365 let mut parameter_path = Vec::new();
366
367 for line in BufReader::new(story_file).lines() {
368 let line = line.map_err(|source| ParsingError::ReadingError {
369 path: path.to_path_buf(),
370 source,
371 })?;
372
373 if let Some(success) = parse_header(&mut name, &line) {
374 let Ok(changes) = success else {
375 return Err(ParsingError::SubheaderWithoutHeader);
376 };
377
378 if !current_block.is_empty() {
379 current_sequence.blocks.push(current_block);
380 current_block = DialogBlock::new();
381 }
382
383 if !current_sequence.blocks.is_empty() {
384 let mut new_name = default_name.clone();
385 new_name.extend(changes.path.clone());
386 text_map.add(new_name, current_sequence);
387 }
388 current_sequence = Self::new();
389
390 changes.apply();
391
392 continue;
393 }
394
395 current_sequence.handle_content_line(
396 &line,
397 &mut current_block,
398 &mut parameter_path,
399 context,
400 )?;
401 }
402
403 if !current_block.is_empty() {
404 current_sequence.blocks.push(current_block);
405 }
406
407 if !current_sequence.blocks.is_empty() {
408 let mut new_name = default_name;
409 new_name.extend(name);
410 text_map.add(new_name, current_sequence);
411 }
412
413 Ok(())
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420 use std::io::Write as _;
421
422 #[derive(Clone, Debug, PartialEq, Eq, Hash)]
423 struct TestParameter(Box<str>);
424
425 impl DialogParameter for TestParameter {
426 type Context = ();
427 fn create(name: &str, _context: &mut ()) -> Option<Self> {
428 Some(TestParameter(name.into()))
429 }
430 }
431
432 #[derive(Debug)]
433 #[allow(dead_code)]
434 enum TestChange {
435 Default(TestParameter),
436 Value(TestParameter, Box<str>),
437 }
438
439 impl DialogChange for TestChange {
440 type Parameter = TestParameter;
441
442 fn default_change(parameter: TestParameter) -> Self {
443 TestChange::Default(parameter)
444 }
445
446 fn value_change(parameter: TestParameter, value: &str, _context: &mut ()) -> Self {
447 TestChange::Value(parameter, value.into())
448 }
449 }
450
451 type TestSequence = DialogSequence<TestChange, TestParameter>;
452 type TestMap = Vec<TestSequence>;
453
454 use std::sync::atomic::{AtomicU32, Ordering};
455
456 static TEST_COUNTER: AtomicU32 = AtomicU32::new(0);
457
458 fn parse_file(content: &str) -> Result<TestMap, ParsingError> {
459 let id = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
460 let dir = std::env::temp_dir().join(format!("dialogi_test_{id}"));
461 std::fs::create_dir_all(&dir).unwrap();
462 let path = dir.join("test.pk");
463 let mut file = File::create(&path).unwrap();
464 file.write_all(content.as_bytes()).unwrap();
465 let result = TestSequence::map_from_path::<TestMap>(&path, &mut ());
466 std::fs::remove_file(&path).unwrap();
467 let _ = std::fs::remove_dir(&dir);
468 result
469 }
470
471 #[test]
472 fn simple_text_blocks() {
473 let sequences = parse_file("# Scene\n\nHello world\n\nSecond block").unwrap();
474 assert_eq!(sequences.len(), 1);
475 assert_eq!(sequences[0].blocks.len(), 2);
476 assert_eq!(sequences[0].blocks[0].name.as_ref(), "");
477 assert_eq!(sequences[0].blocks[0].lines[0].text.as_ref(), "Hello world");
478 assert_eq!(
479 sequences[0].blocks[1].lines[0].text.as_ref(),
480 "Second block"
481 );
482 }
483
484 #[test]
485 fn talker_with_text() {
486 let sequences = parse_file("# Scene\n\nAlice: Hi!\n\nBob: Hello").unwrap();
487 assert_eq!(sequences[0].blocks.len(), 2);
488 assert_eq!(sequences[0].blocks[0].name.as_ref(), "Alice");
489 assert_eq!(sequences[0].blocks[0].lines[0].text.as_ref(), "Hi!");
490 assert_eq!(sequences[0].blocks[1].name.as_ref(), "Bob");
491 }
492
493 #[test]
494 fn talker_multiline() {
495 let sequences = parse_file("# Scene\n\nAlice:\nLine 1\nLine 2").unwrap();
496 assert_eq!(sequences[0].blocks[0].name.as_ref(), "Alice");
497 assert_eq!(sequences[0].blocks[0].lines.len(), 2);
498 assert_eq!(sequences[0].blocks[0].lines[0].text.as_ref(), "Line 1");
499 assert_eq!(sequences[0].blocks[0].lines[1].text.as_ref(), "Line 2");
500 }
501
502 #[test]
503 fn events_with_values() {
504 let sequences = parse_file("# Scene\n\n- Mood happy\nAlice: Hi!").unwrap();
505 assert!(
506 sequences[0]
507 .changes
508 .contains_key(&TestParameter("Mood".into()))
509 );
510 assert!(
511 sequences[0].blocks[0]
512 .final_actions
513 .contains(&TestParameter("Mood".into()))
514 );
515 assert_eq!(sequences[0].blocks[1].name.as_ref(), "Alice");
516 }
517
518 #[test]
519 fn default_event() {
520 let sequences = parse_file("# Scene\n\n- Mood!\nSome text").unwrap();
521 assert!(
522 sequences[0]
523 .changes
524 .contains_key(&TestParameter("Mood".into()))
525 );
526 }
527
528 #[test]
529 fn hierarchical_event_path() {
530 let sequences =
531 parse_file("# Scene\n\n- Path:\n - To:\n - Param Value\nSome text").unwrap();
532 assert!(
533 sequences[0]
534 .changes
535 .contains_key(&TestParameter("Path:To:Param".into()))
536 );
537 }
538
539 #[test]
540 fn multiple_headers() {
541 let sequences = parse_file("# Scene 1\n\nText 1\n\n# Scene 2\n\nText 2").unwrap();
542 assert_eq!(sequences.len(), 2);
543 }
544
545 #[test]
546 fn invalid_indentation() {
547 let result = parse_file("# Scene\n\n - Param Value");
548 assert!(matches!(result, Err(ParsingError::InvalidIndentation)));
549 }
550
551 #[test]
552 fn indentation_too_high() {
553 let result = parse_file("# Scene\n\n - Param Value");
554 assert!(matches!(result, Err(ParsingError::IndentationTooHigh)));
555 }
556
557 #[test]
558 fn default_parameter_with_value() {
559 let result = parse_file("# Scene\n\n- Param! Value");
560 assert!(matches!(
561 result,
562 Err(ParsingError::DefaultParameterWithValue)
563 ));
564 }
565
566 #[test]
567 fn empty_lines_separate_blocks() {
568 let sequences = parse_file("# Scene\n\nLine 1\n\nLine 2\n\nLine 3").unwrap();
569 assert_eq!(sequences[0].blocks.len(), 3);
570 }
571
572 #[test]
573 fn narrator_text_has_empty_name() {
574 let sequences = parse_file("# Scene\n\nNarrator text here").unwrap();
575 assert_eq!(sequences[0].blocks[0].name.as_ref(), "");
576 }
577
578 #[test]
579 fn colon_parameter_with_value_rejected() {
580 let result = parse_file("# Scene\n\n- Path: Value");
581 assert!(matches!(
582 result,
583 Err(ParsingError::ColonParameterWithValues)
584 ));
585 }
586
587 #[test]
588 fn nonexistent_file() {
589 let result =
590 TestSequence::map_from_path::<TestMap>(Path::new("/nonexistent/test.pk"), &mut ());
591 assert!(matches!(result, Err(ParsingError::OpeningError { .. })));
592 }
593
594 #[test]
595 fn non_pk_file_ignored() {
596 let id = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
597 let dir = std::env::temp_dir().join(format!("dialogi_test_ext_{id}"));
598 std::fs::create_dir_all(&dir).unwrap();
599 let path = dir.join("test.txt");
600 std::fs::write(&path, "# Scene\n\nHello").unwrap();
601 let result = TestSequence::map_from_path::<TestMap>(&path, &mut ()).unwrap();
602 assert!(result.is_empty());
603 std::fs::remove_file(&path).unwrap();
604 let _ = std::fs::remove_dir(&dir);
605 }
606}