1use std::collections::BTreeSet;
2
3use serde_derive::{Deserialize, Serialize};
4
5use crate::{meta::Metadata, 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 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}