Skip to main content

pacenotes_installer\tree/
build.rs

1impl super::Tree {
2    /// The tree for ONE game: what the server listed, what the cache holds and
3    /// what the game folder's ledger has `installed`. A leaf starts checked when
4    /// it is installed or cached, because Apply uninstalls what is unchecked.
5    pub fn build(
6        game: &str,
7        packages: &[pacenotes_shared::PackageInfo],
8        cached: &[super::CachedPackage],
9        installed: &[String],
10    ) -> super::Tree {
11        let mut rows = listed_rows(game, packages, cached, installed);
12        rows.extend(local_only_rows(game, cached, installed, &rows));
13        rows.extend(unavailable_rows(game, installed, &rows));
14        rows.sort_by(|a, b| {
15            category_rank(game, &a.segments[0])
16                .cmp(&category_rank(game, &b.segments[0]))
17                .then_with(|| a.segments.cmp(&b.segments))
18        });
19
20        let mut tree = super::Tree::default();
21        for row in rows {
22            tree.insert(row);
23        }
24        tree
25    }
26
27    fn insert(&mut self, row: Row) {
28        let mut parent: Option<usize> = None;
29        let levels = &row.segments[..row.segments.len().saturating_sub(1)];
30        for level in levels {
31            parent = Some(self.child_named(parent, level));
32        }
33        let node = super::Node {
34            label: row.label,
35            parent,
36            children: Vec::new(),
37            key: Some(row.key),
38            state: Some(row.state),
39            installed: row.installed,
40            checked: row.installed || row.state.has_payload(),
41        };
42        self.push(node, parent);
43    }
44
45    /// The grouping child of `parent` with this label, created when missing.
46    fn child_named(&mut self, parent: Option<usize>, label: &str) -> usize {
47        let siblings = match parent {
48            Some(parent) => &self.nodes[parent].children,
49            None => &self.roots,
50        };
51        let existing = siblings
52            .iter()
53            .copied()
54            .find(|&index| !self.nodes[index].is_leaf() && self.nodes[index].label == label);
55        if let Some(existing) = existing {
56            return existing;
57        }
58        let node = super::Node {
59            label: label.to_string(),
60            parent,
61            children: Vec::new(),
62            key: None,
63            state: None,
64            installed: false,
65            checked: false,
66        };
67        self.push(node, parent)
68    }
69}
70
71/// A leaf on its way into the tree.
72struct Row {
73    segments: Vec<String>,
74    label: String,
75    key: String,
76    state: super::LeafState,
77    installed: bool,
78}
79
80/// The server's packages for `game`, with the cache deciding each one's state;
81/// a name that could widen into a path is dropped, since a manifest can come
82/// from a server the user does not run.
83fn listed_rows(
84    game: &str,
85    packages: &[pacenotes_shared::PackageInfo],
86    cached: &[super::CachedPackage],
87    installed: &[String],
88) -> Vec<Row> {
89    packages
90        .iter()
91        .filter(|package| package.game == game && pacenotes_shared::valid_name(&package.name))
92        .map(|package| {
93            let key = package.download_path();
94            let state = match cached.iter().find(|entry| entry.key() == key) {
95                Some(entry) if entry.sha256 == package.sha256 => super::LeafState::Downloaded,
96                Some(_) => super::LeafState::UpdateAvailable,
97                None => super::LeafState::NotCached,
98            };
99            Row {
100                segments: package.segments().map(str::to_string).collect(),
101                label: package.label().to_string(),
102                installed: installed.contains(&key),
103                key,
104                state,
105            }
106        })
107        .collect()
108}
109
110/// Cached packages for `game` that the server did not list.
111fn local_only_rows(
112    game: &str,
113    cached: &[super::CachedPackage],
114    installed: &[String],
115    listed: &[Row],
116) -> Vec<Row> {
117    cached
118        .iter()
119        .filter(|entry| entry.game == game && pacenotes_shared::valid_name(&entry.name))
120        .filter(|entry| !listed.iter().any(|row| row.key == entry.key()))
121        .map(|entry| {
122            let key = entry.key();
123            Row {
124                segments: entry.name.split('/').map(str::to_string).collect(),
125                label: entry
126                    .display_name
127                    .clone()
128                    .unwrap_or_else(|| leaf_of(&entry.name).to_string()),
129                installed: installed.contains(&key),
130                key,
131                state: super::LeafState::LocalOnly,
132            }
133        })
134        .collect()
135}
136
137/// Installed packages for `game` that neither the server nor the cache knows.
138fn unavailable_rows(game: &str, installed: &[String], known: &[Row]) -> Vec<Row> {
139    installed
140        .iter()
141        .filter(|key| !known.iter().any(|row| &row.key == *key))
142        .filter_map(|key| {
143            let (package_game, _, name) = pacenotes_shared::split_download_path(key)?;
144            (package_game == game).then(|| Row {
145                segments: name.split('/').map(str::to_string).collect(),
146                label: leaf_of(name).to_string(),
147                key: key.clone(),
148                state: super::LeafState::Unavailable,
149                installed: true,
150            })
151        })
152        .collect()
153}
154
155fn leaf_of(name: &str) -> &str {
156    name.rsplit('/').next().unwrap_or(name)
157}
158
159/// The game schema's order, then any undeclared category alphabetically, so the
160/// tree, scene 5 and the server's folders agree and a refetch never reorders.
161fn category_rank(game: &str, category: &str) -> usize {
162    let declared = crate::schema::categories(game);
163    declared
164        .iter()
165        .position(|declared| declared.id == category)
166        .unwrap_or(declared.len())
167}