dialogi/
lib.rs

1#![deny(missing_docs)]
2
3//! A crate to parse dialog lines from a simple markdown inspired format.
4
5use 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/// A single dialog line consisting of actions to be advanced when the line is displayed, and the text itself.
17#[derive(Clone, Debug)]
18pub struct DialogLine<P> {
19    /// The text of the dialog line.
20    pub text: Box<str>,
21    /// The actions advanced by the dialog line, identified by parameters of type `P`.
22    pub actions: HashSet<P>,
23}
24
25/// A full dialog block consisting of the name of the talker, the lines of the dialog, and some final actions to be advanced when the whole text box is fully displayed.
26#[derive(Clone, Debug)]
27pub struct DialogBlock<P> {
28    /// The speaker name of the dialog block.
29    pub name: Box<str>,
30    /// The text lines of the dialog block.
31    pub lines: Vec<DialogLine<P>>,
32    /// The actions advanced by the dialog block, identified by parameters of type `P`.
33    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    /// The text lines of a dialog block as string references.
50    pub fn lines(&self) -> impl Iterator<Item = &str> {
51        self.lines.iter().map(|line| line.text.as_ref())
52    }
53}
54
55/// A parameter to define an action of the dialog.
56pub trait DialogParameter: Sized {
57    /// The parameter context, which might be important for creation by name.
58    type Context;
59    /// The method to create a parameter. It might access some context and returns some new dialog parameter or none.
60    fn create(name: &str, context: &mut Self::Context) -> Option<Self>;
61}
62
63/// A change to define an how actions are applied.
64pub trait DialogChange: Sized {
65    /// The parameter to apply this change.
66    type Parameter: DialogParameter + Clone + Eq + Hash;
67
68    /// Creates the change of the parameter back to the default value.
69    fn default_change(parameter: Self::Parameter) -> Self;
70
71    /// Creates a change of the parameter to the specified value.
72    fn value_change(
73        parameter: Self::Parameter,
74        value: &str,
75        context: &mut <<Self as DialogChange>::Parameter as DialogParameter>::Context,
76    ) -> Self;
77}
78
79/// Defines a full sequential dialog as a sequence of dialog blocks.
80pub struct DialogSequence<C, P> {
81    /// The sequence of dialog blocks.
82    pub blocks: Vec<DialogBlock<P>>,
83    /// The changes to be applied by the dialog parameters.
84    pub changes: HashMap<P, Vec<C>>,
85}
86
87/// A trait to parse dialog sequences into, identified by a name.
88pub trait DialogMap<C: DialogChange>: Default {
89    /// Adds a single sequential dialog into the dialog map.
90    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/// An error type returned when parsing a the dialog structure fails.
106#[derive(Debug, Error)]
107pub enum ParsingError {
108    /// Colon parameters are not allowed to have a value supplied.
109    #[error("Colon parameters are not allowed to have a value supplied")]
110    ColonParameterWithValues,
111    /// Error while opening story file.
112    #[error("Error while opening story file: {0}")]
113    OpeningError(PathBuf),
114    /// Error while reading story file.
115    #[error("Error while reading story file: {0}")]
116    ReadingError(PathBuf),
117    /// Subheader found without a matching header.
118    #[error("Subheader found without a matching header")]
119    SubheaderWithoutHeader,
120    /// Invalid dialog format.
121    #[error("Invalid dialog format")]
122    InvalidIndentation,
123    /// Invalid indentation level.
124    #[error("Invalid indentation level")]
125    IndentationTooHigh,
126    /// Default parameters cannot have a value supplied.
127    #[error("Default parameters cannot have a value supplied")]
128    DefaultParameterWithValue,
129    /// Duplicate definition of change.
130    #[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    /// Loads a new dialog file from a specified path and returns a new text map.
143    ///
144    /// If the path is a folder, it will be scanned for `.pk` files and sub folders.
145    /// If the path is a file, it will be parsed as directly using the `.pk` parsing format.
146    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    /// Loads a new dialog file from a specified path into the supplied text map.
156    ///
157    /// If the path is a folder, it will be scanned for `.pk` files and sub folders.
158    /// If the path is a file, it will be parsed as directly using the `.pk` parsing format.
159    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(&parameter_name, context) else {
273                return Ok(current_block);
274            };
275
276            if current_block.final_actions.contains(&parameter) {
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(&parameter) {
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}