z_mirror/fmm

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

raiguardIgnore base in dependenciesa95b21e

rewrite
8.3 KiB253 linesraw
1#![feature(iter_intersperse)]
2
3use semver::{Version, VersionReq};
4use std::collections::HashMap;
5use std::error::Error;
6use std::fs;
7use std::fs::DirEntry;
8use std::fs::File;
9use std::io::Read;
10use std::path::PathBuf;
11use structopt::StructOpt;
12use zip::ZipArchive;
13
14mod config;
15mod dependency;
16mod types;
17
18use config::*;
19use dependency::*;
20use types::*;
21
22#[derive(StructOpt)]
23#[structopt(name = "fmm", about = "Manage your Factorio mods.")]
24struct App {
25    #[structopt(long)]
26    config: Option<PathBuf>,
27    #[structopt(long)]
28    dir: Option<PathBuf>,
29    #[structopt(short = "o", long)]
30    disable_all: bool,
31    #[structopt(short, long)]
32    disable: Vec<InputMod>,
33    #[structopt(short = "a", long)]
34    enable_all: bool,
35    #[structopt(short, long)]
36    enable: Vec<InputMod>,
37}
38
39impl App {
40    fn merge_config(&mut self, config_file: ConfigFile) {
41        if let Some(directory) = config_file.directory {
42            self.dir = Some(directory);
43        }
44    }
45}
46
47// DESIGN NOTES:
48// - Get a list of all mods + versions in the folder _without_ reading the ZIP files (use filenames)
49// - Only read ZIPs if we need to get dependencies or other info
50// - Cache will only be used once we have advanced features that would benefit from it
51
52// TODO: Use errors instead of an option
53fn read_info_json(entry: &DirEntry) -> Option<InfoJson> {
54    let contents = match ModEntryStructure::parse(entry)? {
55        ModEntryStructure::Directory | ModEntryStructure::Symlink => {
56            let mut path = entry.path();
57            path.push("info.json");
58            fs::read_to_string(path).ok()?
59        }
60        ModEntryStructure::Zip => {
61            let mut archive = ZipArchive::new(File::open(entry.path()).ok()?).ok()?;
62            let filename = archive
63                .file_names()
64                .find(|name| name.contains("info.json"))
65                .map(ToString::to_string)?;
66            let mut file = archive.by_name(&filename).ok()?;
67            let mut contents = String::new();
68            file.read_to_string(&mut contents).ok()?;
69            contents
70        }
71    };
72
73    serde_json::from_str::<InfoJson>(&contents).ok()
74}
75
76fn main() -> Result<(), Box<dyn Error>> {
77    let mut app = App::from_args();
78
79    let config = ConfigFile::new(&app.config)?;
80    if let Some(config) = config {
81        app.merge_config(config);
82    }
83
84    if app.dir.is_none() {
85        return Err("Must specify a mods path via flag or via the configuration file.".into());
86    }
87
88    let dir = app.dir.unwrap();
89
90    // Get all mods in the directory
91    let mod_entries = fs::read_dir(&dir)?
92        .filter_map(|entry| {
93            let entry = entry.ok()?;
94            let file_name = entry.file_name();
95
96            if let Some((mod_name, version)) = file_name.to_str()?.rsplit_once("_") {
97                let (version, _) = version.rsplit_once(".").unwrap_or((version, "")); // Strip file extension
98
99                Some((
100                    mod_name.to_string(),
101                    ModVersion {
102                        entry,
103                        version: Version::parse(version).ok()?,
104                    },
105                ))
106            } else {
107                let info_json = read_info_json(&entry)?;
108
109                Some((
110                    info_json.name,
111                    ModVersion {
112                        entry,
113                        version: info_json.version,
114                    },
115                ))
116            }
117        })
118        .fold(HashMap::new(), |mut directory_mods, (mod_name, version)| {
119            let versions = directory_mods.entry(mod_name).or_insert_with(Vec::new);
120
121            let index = versions
122                .binary_search(&version)
123                .unwrap_or_else(|index| index);
124            versions.insert(index, version);
125
126            directory_mods
127        });
128
129    // Parse mod-list.json
130    let mut mlj_path = dir;
131    mlj_path.push("mod-list.json");
132    let enabled_versions = std::fs::read_to_string(&mlj_path)?;
133    let mut mod_list_json: ModListJson = serde_json::from_str(&enabled_versions)?;
134
135    // Disable all mods
136    if app.disable_all {
137        println!("Disabled all mods");
138        for mod_data in mod_list_json
139            .mods
140            .iter_mut()
141            .filter(|mod_state| mod_state.name != "base")
142        {
143            mod_data.enabled = false;
144            mod_data.version = None;
145        }
146    }
147
148    // Enable all mods
149    if app.enable_all {
150        println!("Enabled latest versions of all mods");
151        for mod_data in mod_list_json.mods.iter_mut() {
152            mod_data.enabled = true;
153            mod_data.version = None;
154        }
155    }
156
157    // Enable specified mods
158    let mut to_enable = app.enable.clone();
159    while !to_enable.is_empty() {
160        let mut to_enable_next = Vec::new();
161        for mod_ident in &to_enable {
162            if mod_ident.name != "base" {
163                let mod_entry = mod_entries.get(&mod_ident.name).and_then(|mod_versions| {
164                    if let Some(version_req) = &mod_ident.version_req {
165                        mod_versions
166                            .iter()
167                            .rev()
168                            .find(|version| version_req.matches(&version.version))
169                    } else {
170                        mod_versions.last()
171                    }
172                });
173
174                if let Some(mod_entry) = mod_entry {
175                    let mod_state = mod_list_json
176                        .mods
177                        .iter_mut()
178                        .find(|mod_state| mod_ident.name == mod_state.name);
179
180                    let enabled = mod_state.is_some() && mod_state.as_ref().unwrap().enabled;
181
182                    if !enabled {
183                        println!("Enabled {} v{}", mod_ident.name, mod_entry.version);
184
185                        let version = mod_ident
186                            .version_req
187                            .as_ref()
188                            .map(|_| mod_entry.version.clone());
189
190                        if let Some(mod_state) = mod_state {
191                            mod_state.enabled = true;
192                            mod_state.version = version;
193                        } else {
194                            mod_list_json.mods.push(ModListJsonMod {
195                                name: mod_ident.name.to_string(),
196                                enabled: true,
197                                version,
198                            });
199                        }
200
201                        to_enable_next.append(
202                            &mut read_info_json(&mod_entry.entry)
203                                .and_then(|info_json| info_json.dependencies)
204                                .unwrap_or_default()
205                                .iter()
206                                .filter(|dependency| {
207                                    dependency.name != "base"
208                                        && matches!(
209                                            dependency.dep_type,
210                                            ModDependencyType::NoLoadOrder
211                                                | ModDependencyType::Required
212                                        )
213                                })
214                                .map(|dependency| InputMod {
215                                    name: dependency.name.clone(),
216                                    version_req: dependency.version_req.clone(),
217                                })
218                                .collect(),
219                        );
220                    }
221                } else {
222                    println!("Could not find {}", &mod_ident);
223                }
224            }
225        }
226
227        to_enable = to_enable_next;
228    }
229
230    // Disable specified mods
231    for mod_data in app.disable {
232        if mod_data.name == "base" || mod_entries.contains_key(&mod_data.name) {
233            let mod_state = mod_list_json
234                .mods
235                .iter_mut()
236                .find(|mod_state| mod_data.name == mod_state.name);
237
238            println!("Disabled {}", &mod_data);
239
240            if let Some(mod_state) = mod_state {
241                mod_state.enabled = false;
242                mod_state.version = None;
243            }
244        } else {
245            println!("Could not find {}", &mod_data);
246        }
247    }
248
249    // Write mod-list.json
250    fs::write(&mlj_path, serde_json::to_string_pretty(&mod_list_json)?)?;
251
252    Ok(())
253}