z_mirror/fmm

git clone https://git.raiguard.me/z_mirror/fmm

raiguardEnable dependencies3e7423d

refactor
8.6 KiB278 linesraw
1use anyhow::Result;
2use std::cmp::Ordering;
3use std::collections::HashMap;
4use std::ffi::OsString;
5use std::fs;
6use std::fs::{DirEntry, File};
7use std::io::Read;
8use std::path::{Path, PathBuf};
9
10use semver::{Version, VersionReq};
11use zip::ZipArchive;
12
13use crate::dependency::ModDependencyType;
14use crate::types::*;
15
16pub struct Directory {
17    pub mods: HashMap<String, Vec<ModVersion>>,
18    pub mod_list: Vec<ModListJsonMod>,
19    pub mod_list_path: PathBuf,
20}
21
22impl Directory {
23    pub fn new(dir: &Path) -> Result<Self> {
24        // Get all mods in the directory
25        let mod_entries = fs::read_dir(&dir)?
26            .filter_map(|entry| {
27                let entry = entry.ok()?;
28
29                if let Some((mod_name, version)) = parse_file_name(&entry.file_name()) {
30                    Some((
31                        mod_name,
32                        ModVersion {
33                            entry,
34                            version,
35                            info_json: None,
36                        },
37                    ))
38                } else {
39                    let info_json = read_info_json(&entry)?;
40                    Some((
41                        info_json.name.clone(),
42                        ModVersion {
43                            entry,
44                            version: info_json.version.clone(),
45                            info_json: Some(info_json),
46                        },
47                    ))
48                }
49            })
50            .fold(HashMap::new(), |mut directory_mods, (mod_name, version)| {
51                let versions = directory_mods.entry(mod_name).or_insert_with(Vec::new);
52
53                let index = versions
54                    .binary_search(&version)
55                    .unwrap_or_else(|index| index);
56                versions.insert(index, version);
57
58                directory_mods
59            });
60
61        // Parse mod-list.json
62        let mut mlj_path = dir.to_owned();
63        mlj_path.push("mod-list.json");
64        let enabled_versions = fs::read_to_string(&mlj_path)?;
65        let mod_list_json: ModListJson = serde_json::from_str(&enabled_versions)?;
66
67        Ok(Self {
68            mods: mod_entries,
69            mod_list: mod_list_json.mods,
70            mod_list_path: mlj_path.clone(),
71        })
72    }
73
74    pub fn get_mut(&mut self, mod_ident: &ModIdent) -> Option<&mut ModVersion> {
75        self.mods.get_mut(&mod_ident.name).and_then(|mod_versions| {
76            if let Some(version_req) = &mod_ident.version_req {
77                mod_versions
78                    .iter_mut()
79                    .rev()
80                    .find(|version| version_req.matches(&version.version))
81            } else {
82                mod_versions.last_mut()
83            }
84        })
85    }
86
87    pub fn disable(&mut self, mod_ident: &ModIdent) {
88        if mod_ident.name == "base" || self.mods.contains_key(&mod_ident.name) {
89            let mod_state = self
90                .mod_list
91                .iter_mut()
92                .find(|mod_state| mod_ident.name == mod_state.name);
93
94            println!("Disabled {}", &mod_ident);
95
96            if let Some(mod_state) = mod_state {
97                mod_state.enabled = false;
98                mod_state.version = None;
99            }
100        } else {
101            println!("Could not find {}", &mod_ident);
102        }
103    }
104
105    pub fn disable_all(&mut self) {
106        println!("Disabled all mods");
107        for mod_data in self
108            .mod_list
109            .iter_mut()
110            .filter(|mod_state| mod_state.name != "base")
111        {
112            mod_data.enabled = false;
113            mod_data.version = None;
114        }
115    }
116
117    pub fn enable(&mut self, mod_ident: &ModIdent) {
118        let mod_entry = self.mods.get(&mod_ident.name).and_then(|mod_versions| {
119            if let Some(version_req) = &mod_ident.version_req {
120                mod_versions
121                    .iter()
122                    .rev()
123                    .find(|version| version_req.matches(&version.version))
124            } else {
125                mod_versions.last()
126            }
127        });
128
129        if let Some(mod_entry) = mod_entry {
130            let mod_state = self
131                .mod_list
132                .iter_mut()
133                .find(|mod_state| mod_ident.name == mod_state.name);
134
135            let enabled = mod_state.is_some() && mod_state.as_ref().unwrap().enabled;
136
137            if !enabled {
138                println!("Enabled {} v{}", mod_ident.name, mod_entry.version);
139
140                let version = mod_ident
141                    .version_req
142                    .as_ref()
143                    .map(|_| mod_entry.version.clone());
144
145                if let Some(mod_state) = mod_state {
146                    mod_state.enabled = true;
147                    mod_state.version = version;
148                } else {
149                    self.mod_list.push(ModListJsonMod {
150                        name: mod_ident.name.to_string(),
151                        enabled: true,
152                        version,
153                    });
154                }
155            }
156        } else {
157            println!("Could not find or read {}", &mod_ident);
158        }
159    }
160
161    pub fn enable_all(&mut self) {
162        println!("Enabled latest versions of all mods");
163        for mod_data in self.mod_list.iter_mut() {
164            mod_data.enabled = true;
165            mod_data.version = None;
166        }
167    }
168
169    pub fn remove(&mut self, mod_ident: &ModIdent) {
170        let version_req = mod_ident
171            .version_req
172            .as_ref()
173            .cloned()
174            .unwrap_or_else(VersionReq::any);
175        if let Some(mod_versions) = self.mods.get(&mod_ident.name) {
176            mod_versions
177                .iter()
178                .filter(|version| version_req.matches(&version.version))
179                .for_each(|version| {
180                    let result = version.entry.metadata().and_then(|metadata| {
181                        if metadata.is_dir() {
182                            fs::remove_dir_all(version.entry.path())
183                        } else {
184                            fs::remove_file(version.entry.path())
185                        }
186                    });
187                    if result.is_ok() {
188                        println!("Removed {} v{}", &mod_ident.name, version.version);
189                    } else {
190                        println!("Could not remove {} v{}", &mod_ident.name, version.version);
191                    }
192                });
193            self.mods.remove(&mod_ident.name);
194        }
195
196        if let Some((index, _)) = self
197            .mod_list
198            .iter()
199            .enumerate()
200            .find(|(_, mod_state)| mod_ident.name == mod_state.name)
201        {
202            self.mod_list.remove(index);
203        }
204    }
205}
206
207pub struct ModVersion {
208    pub entry: DirEntry,
209    pub version: Version,
210    pub info_json: Option<InfoJson>,
211}
212
213impl ModVersion {
214    pub fn get_info_json(&mut self) -> Option<&InfoJson> {
215        if self.info_json.is_none() {
216            let info_json = read_info_json(&self.entry);
217            self.info_json = info_json;
218        }
219        self.info_json.as_ref()
220    }
221}
222
223impl PartialOrd for ModVersion {
224    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
225        self.version.partial_cmp(&other.version)
226    }
227}
228
229impl Ord for ModVersion {
230    fn cmp(&self, other: &Self) -> Ordering {
231        self.version.cmp(&other.version)
232    }
233}
234
235impl PartialEq for ModVersion {
236    fn eq(&self, other: &Self) -> bool {
237        self.version == other.version
238    }
239}
240
241impl Eq for ModVersion {}
242
243fn parse_file_name(file_name: &OsString) -> Option<(String, Version)> {
244    let (name, version) = file_name
245        .to_str()?
246        .trim_end_matches(".zip")
247        .rsplit_once("_")?;
248
249    if let Ok(version) = Version::parse(version) {
250        Some((name.to_string(), version))
251    } else {
252        None
253    }
254}
255
256// TODO: Use errors instead of an option
257fn read_info_json(entry: &DirEntry) -> Option<InfoJson> {
258    let contents = match ModEntryStructure::parse(entry)? {
259        ModEntryStructure::Directory | ModEntryStructure::Symlink => {
260            let mut path = entry.path();
261            path.push("info.json");
262            fs::read_to_string(path).ok()?
263        }
264        ModEntryStructure::Zip => {
265            let mut archive = ZipArchive::new(File::open(entry.path()).ok()?).ok()?;
266            let filename = archive
267                .file_names()
268                .find(|name| name.contains("info.json"))
269                .map(ToString::to_string)?;
270            let mut file = archive.by_name(&filename).ok()?;
271            let mut contents = String::new();
272            file.read_to_string(&mut contents).ok()?;
273            contents
274        }
275    };
276
277    serde_json::from_str::<InfoJson>(&contents).ok()
278}