Skip to main content

pacenotes_installer\choices/
configuration.rs

1#[derive(Clone, Debug, Default, PartialEq, Eq)]
2pub struct Configuration {
3    /// In the game schema's order, which is the order the page asks them in.
4    pub lists: Vec<super::ChoiceList>,
5    /// Checked packages that take no choice: `pacenotes` and any other category.
6    pub extras: Vec<String>,
7}
8
9impl Configuration {
10    /// The lists for the checked leaves of `tree`. A key in `keep` that is
11    /// still offered stays picked across a trip back to scene 4, failing that
12    /// what is installed, and a list with a single option comes preselected.
13    /// `choice_categories` comes from the game's compiled-in schema, since
14    /// which questions a game asks is its own.
15    pub fn from_tree(
16        tree: &crate::tree::Tree,
17        keep: &[String],
18        choice_categories: &[&'static str],
19    ) -> Configuration {
20        let checked = tree.checked_leaves();
21        let lists = choice_categories
22            .iter()
23            .map(|&category| {
24                let options: Vec<super::ChoiceOption> = checked
25                    .iter()
26                    .copied()
27                    .filter(|&leaf| tree.category_of(leaf) == category)
28                    .filter_map(|leaf| {
29                        let key = tree.node(leaf).key.clone()?;
30                        Some(super::ChoiceOption {
31                            key,
32                            label: option_label(&tree.path_of(leaf)),
33                        })
34                    })
35                    .collect();
36                // Keeping what is installed is the safe default, since a
37                // selection that omitted it would uninstall it.
38                let installed = checked
39                    .iter()
40                    .copied()
41                    .filter(|&leaf| tree.category_of(leaf) == category)
42                    .find(|&leaf| tree.node(leaf).installed)
43                    .and_then(|leaf| tree.node(leaf).key.clone());
44                let position_of =
45                    |key: &String| options.iter().position(|option| &option.key == key);
46                let selected = keep
47                    .iter()
48                    .find_map(position_of)
49                    .or_else(|| installed.as_ref().and_then(position_of))
50                    .or(if options.len() == 1 { Some(0) } else { None });
51                super::ChoiceList {
52                    category,
53                    options,
54                    selected,
55                }
56            })
57            .collect();
58
59        let extras = checked
60            .iter()
61            .copied()
62            .filter(|&leaf| !choice_categories.contains(&tree.category_of(leaf)))
63            .filter_map(|leaf| tree.node(leaf).key.clone())
64            .collect();
65
66        Configuration { lists, extras }
67    }
68
69    pub fn is_complete(&self) -> bool {
70        self.lists.iter().all(super::ChoiceList::is_satisfied)
71    }
72
73    /// The categories still waiting for a pick, as the warning names them.
74    pub fn missing(&self) -> Vec<&'static str> {
75        self.lists
76            .iter()
77            .filter(|list| !list.is_satisfied())
78            .map(|list| list.category)
79            .collect()
80    }
81
82    pub fn select(&mut self, list: usize, option: Option<usize>) {
83        if let Some(list) = self.lists.get_mut(list) {
84            list.selected = option.filter(|&index| index < list.options.len());
85        }
86    }
87
88    /// The pick for one category, by name, so a reordered schema cannot misdirect it.
89    pub fn selected_key(&self, category: &str) -> Option<&str> {
90        self.lists
91            .iter()
92            .find(|list| list.category == category)
93            .and_then(super::ChoiceList::selected_key)
94    }
95
96    /// In list order, skipping empty lists.
97    pub fn selected_keys(&self) -> Vec<String> {
98        self.lists
99            .iter()
100            .filter_map(|list| list.selected_key().map(str::to_string))
101            .collect()
102    }
103
104    /// Category to picked key, for the game's configuration step to take what it understands.
105    pub fn picks_by_category(&self) -> std::collections::BTreeMap<String, String> {
106        self.lists
107            .iter()
108            .filter_map(|list| {
109                let key = list.selected_key()?;
110                Some((list.category.to_string(), key.to_string()))
111            })
112            .collect()
113    }
114
115    /// The picks, then the extras, each key once.
116    pub fn install_keys(&self) -> Vec<String> {
117        let mut keys = self.selected_keys();
118        for extra in &self.extras {
119            if !keys.contains(extra) {
120                keys.push(extra.clone());
121            }
122        }
123        keys
124    }
125}
126
127/// The path below the category, which the list caption already names; a
128/// package that is its own category keeps its label.
129fn option_label(path: &[&str]) -> String {
130    match path {
131        [_category, below @ ..] if !below.is_empty() => below.join(" / "),
132        _ => path.join(" / "),
133    }
134}