Skip to main content

mod_installer/config/
parser_config.rs

1use std::collections::BTreeSet;
2
3use serde_derive::{Deserialize, Serialize};
4
5use crate::{config::meta::Metadata, config::state::State};
6
7#[derive(Debug, PartialEq, Serialize, Deserialize)]
8pub struct ParserConfig {
9  pub in_progress_words: BTreeSet<String>,
10  pub useful_status_words: BTreeSet<String>,
11  pub choice_words: BTreeSet<String>,
12  pub choice_phrase: BTreeSet<String>,
13  pub completed_with_warnings: BTreeSet<String>,
14  pub failed_with_error: BTreeSet<String>,
15  pub finished: BTreeSet<String>,
16  pub eet_auto_fill: String,
17  pub bell_character: Option<char>,
18  pub metadata: Metadata,
19}
20
21impl Default for ParserConfig {
22  fn default() -> Self {
23    Self {
24      in_progress_words: vec!["installing".to_string(), "creating".to_string()]
25        .into_iter()
26        .collect(),
27      useful_status_words: vec![
28        "copied".to_string(),
29        "copying".to_string(),
30        "creating".to_string(),
31        "installed".to_string(),
32        "installing".to_string(),
33        "patched".to_string(),
34        "patching".to_string(),
35        "processed".to_string(),
36        "processing".to_string(),
37      ]
38      .into_iter()
39      .collect(),
40      choice_words: vec![
41        "choice".to_string(),
42        "choose".to_string(),
43        "select".to_string(),
44        "enter".to_string(),
45      ]
46      .into_iter()
47      .collect(),
48      choice_phrase: vec![
49        "do you want".to_string(),
50        "would you like".to_string(),
51        "answer [y]es or [n]o.".to_string(),
52        "is this correct?".to_string(),
53        "[y]es or [n]o".to_string(),
54        "please select".to_string(),
55        "please enter".to_string(),
56        "enter a new".to_string(),
57        "leave blank".to_string(),
58        "[a]ccept, [r]etry, [c]ancel".to_string(),
59        "[y]es, [n]o, [c]ancel".to_string(),
60      ]
61      .into_iter()
62      .collect(),
63      completed_with_warnings: vec!["installed with warnings".to_string()]
64        .into_iter()
65        .collect(),
66      failed_with_error: vec![
67        "not installed due to errors".to_string(),
68        "installation aborted".to_string(),
69      ]
70      .into_iter()
71      .collect(),
72      finished: vec![
73        "successfully installed      ".to_string(),
74        "process ended".to_string(),
75      ]
76      .into_iter()
77      .collect(),
78      eet_auto_fill: "Enter the full path to your BG:EE+SoD installation then press Enter."
79        .to_string(),
80      bell_character: Some('\x07'),
81      metadata: Metadata::default(),
82    }
83  }
84}
85
86impl ParserConfig {
87  pub fn string_looks_like_question(&self, weidu_output: &str) -> bool {
88    let comparable_output = weidu_output.trim().to_ascii_lowercase();
89    if let Some(bell) = self.bell_character
90      && comparable_output.contains(bell)
91    {
92      return true;
93    }
94    // installing|creating
95    for progress_word in self.in_progress_words.iter() {
96      if comparable_output.contains(progress_word) {
97        return false;
98      }
99    }
100    for question in self.choice_phrase.iter() {
101      if comparable_output.contains(question) {
102        return true;
103      }
104    }
105
106    for question in self.choice_words.iter() {
107      for word in comparable_output.split_whitespace() {
108        if word
109          .chars()
110          .filter(|c| c.is_alphabetic())
111          .collect::<String>()
112          .eq(question)
113        {
114          return true;
115        }
116      }
117    }
118
119    false
120  }
121
122  pub fn detect_weidu_finished_state(&self, weidu_output: &str) -> State {
123    let comparable_output = weidu_output.trim().to_lowercase();
124    let failure = self.failed_with_error.iter().fold(false, |acc, fail_case| {
125      comparable_output.contains(fail_case) || acc
126    });
127    if failure {
128      return State::CompletedWithErrors {
129        error_details: comparable_output,
130      };
131    }
132    let warning = self
133      .completed_with_warnings
134      .iter()
135      .fold(false, |acc, warn_case| {
136        comparable_output.contains(warn_case) || acc
137      });
138    if warning {
139      return State::CompletedWithWarnings;
140    }
141    let finished = self.finished.iter().fold(false, |acc, success_case| {
142      comparable_output.contains(success_case) || acc
143    });
144    if finished {
145      return State::Completed;
146    }
147    State::InProgress
148  }
149}
150
151#[cfg(test)]
152mod tests {
153
154  use super::*;
155  use pretty_assertions::assert_eq;
156  use std::{error::Error, path::Path, result::Result};
157
158  #[test]
159  fn test_exit_warnings() -> Result<(), Box<dyn Error>> {
160    let config = ParserConfig::default();
161    let test = "INSTALLED WITH WARNINGS     Additional equipment for Thieves and Bards";
162    assert_eq!(config.string_looks_like_question(test), false);
163    assert_eq!(
164      config.detect_weidu_finished_state(test),
165      State::CompletedWithWarnings
166    );
167    Ok(())
168  }
169
170  #[test]
171  fn test_exit_success() -> Result<(), Box<dyn Error>> {
172    let config = ParserConfig::default();
173    let test = "SUCCESSFULLY INSTALLED      Jan's Extended Quest";
174    assert_eq!(config.string_looks_like_question(test), false);
175    assert_eq!(config.detect_weidu_finished_state(test), State::Completed);
176    Ok(())
177  }
178
179  #[test]
180  fn is_a_question() -> Result<(), Box<dyn Error>> {
181    let config = ParserConfig::default();
182    let tests = vec![
183      "Enter the full path to your Baldur's Gate installation then press Enter.",
184      "Enter the full path to your BG:EE+SoD installation then press Enter.\
185Example: C:\\Program Files (x86)\\BeamDog\\Games\\00806",
186      "[N]o, [Q]uit or choose one:",
187      "Please enter the chance for items to randomly not be randomised as a integet number (e.g. 10 for 10%)",
188      "Is this correct? [Y]es or [N]o",
189      "Please enter number of the kit to select (leave blank to proceed with the installation):",
190      "Please enter a new title for the selected kit (leave blank to keep current):",
191      r#"accept kit title "354" ([a]ccept, [r]etry, [c]ancel)?"#,
192      "There is no question, only bell\x07",
193    ];
194    for test in tests {
195      assert_eq!(
196        config.string_looks_like_question(test),
197        true,
198        "String {} doesn't look like a question",
199        test
200      );
201      assert_eq!(config.detect_weidu_finished_state(test), State::InProgress);
202      assert_eq!(
203        config.useful_status_words.contains(&test.to_string()),
204        false,
205        "String {} looks like useful status words, it should only look like a question",
206        test
207      )
208    }
209    Ok(())
210  }
211
212  #[test]
213  fn is_not_a_question() -> Result<(), Box<dyn Error>> {
214    let config = ParserConfig::default();
215    let tests = vec![
216      "FAILURE:",
217      "NOT INSTALLED DUE TO ERRORS The BG1 NPC Project: Required Modifications",
218      "Creating epilogues. Too many epilogues... Why are there so many options here?",
219      "Including file(s) spellchoices_defensive/vanilla/ENCHANTER.TPH",
220    ];
221    for test in tests {
222      assert_eq!(
223        config.string_looks_like_question(test),
224        false,
225        "String {} does look like a question",
226        test
227      );
228    }
229    Ok(())
230  }
231
232  #[test]
233  fn load_config() -> Result<(), Box<dyn Error>> {
234    let config_root = std::env::current_dir()?;
235    let root = config_root.parent().ok_or("Could not get workspace root")?;
236    let config_path = Path::join(&root, Path::new("example_config.toml"));
237    let config: ParserConfig = confy::load_path(config_path)?;
238    let mut expected = ParserConfig::default();
239    expected.metadata = config.metadata.clone();
240    assert_eq!(expected, config);
241    Ok(())
242  }
243
244  #[test]
245  fn failure() -> Result<(), Box<dyn Error>> {
246    let config = ParserConfig::default();
247    let tests = vec![
248      "not installed due to errors the bg1 npc project: required modifications",
249      "installation aborted merge dlc into game -> merge all available dlcs",
250    ];
251    for input in tests {
252      assert_eq!(
253        config.detect_weidu_finished_state(input),
254        State::CompletedWithErrors {
255          error_details: input.to_string(),
256        },
257        "Input {} did not fail",
258        input
259      );
260    }
261    Ok(())
262  }
263}