z_mirror/fmm

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

raiguardSync with save filea557023

refactor
9.1 KiB278 linesraw
1use anyhow::anyhow;
2use anyhow::Result;
3use directories::BaseDirs;
4use serde::Deserialize;
5use serde_with::{serde_as, DisplayFromStr};
6use std::collections::HashMap;
7use std::fs;
8use std::path::PathBuf;
9use structopt::StructOpt;
10use thiserror::Error;
11
12use crate::dependency::ModDependencyType;
13use crate::directory::Directory;
14use crate::sync;
15use crate::types::*;
16
17#[derive(Debug, Default)]
18pub struct Actions {
19    pub remove: Vec<ModIdent>,
20    pub disable: ModifyType,
21    pub download: Vec<ModIdent>,
22    pub enable: ModifyType,
23}
24
25#[derive(Debug)]
26pub enum ModifyType {
27    All,
28    Some(Vec<ModIdent>),
29    None,
30}
31
32impl Default for ModifyType {
33    fn default() -> Self {
34        Self::None
35    }
36}
37
38#[derive(Debug)]
39pub struct Config {
40    pub game_dir: Option<PathBuf>,
41    pub mods_dir: PathBuf,
42    pub portal_auth: Option<PortalAuth>,
43}
44
45#[derive(Debug, Deserialize)]
46pub struct PortalAuth {
47    pub username: String,
48    pub token: String,
49}
50
51pub fn proc_input() -> Result<(Actions, Config, Directory)> {
52    // Input
53    let args = Args::from_args();
54    let config_file = ConfigFile::new(&args.config)?.unwrap_or_default();
55
56    // Merge config options
57    let config = {
58        let game_dir = args.game_dir.or(config_file.game_dir);
59        Config {
60            game_dir: game_dir.clone(),
61            mods_dir: match [game_dir.clone(), args.mods_dir.or(config_file.mods_dir)] {
62                [_, Some(mods_dir)] if mods_dir.exists() => mods_dir,
63                [Some(game_dir), None] if game_dir.exists() => {
64                    let mut mods_dir = game_dir;
65                    mods_dir.push("mods");
66                    if !mods_dir.exists() {
67                        return Err(anyhow!("Could not find mods directory"));
68                    }
69                    mods_dir
70                }
71                _ => return Err(anyhow!("Invalid game or mods directories")),
72            },
73            portal_auth: config_file.portal.or_else(|| {
74                if let Some(game_dir) = &game_dir {
75                    let mut player_data_path = game_dir.clone();
76                    player_data_path.push("player-data.json");
77                    if player_data_path.exists() {
78                        let player_data_json = fs::read_to_string(&player_data_path).ok()?;
79                        if let PlayerDataJson {
80                            service_token: Some(token),
81                            service_username: Some(username),
82                        } = serde_json::from_str(&player_data_json).ok()?
83                        {
84                            return Some(PortalAuth { token, username });
85                        }
86                    }
87                }
88                None
89            }),
90        }
91    };
92
93    // Process actions
94    let mut directory = Directory::new(&config.mods_dir)?;
95    let mut actions = Actions {
96        remove: args.remove,
97        ..Default::default()
98    };
99
100    // Disabling
101    if args.disable_all {
102        actions.disable = ModifyType::All;
103    } else if !args.disable.is_empty() {
104        actions.disable = ModifyType::Some(args.disable);
105    }
106
107    // TODO: Downloading
108
109    // Enabling
110    // TODO: This whole thing needs to be cleaned up
111    if args.enable_all {
112        actions.enable = ModifyType::All;
113    } else {
114        if let Some(path) = args.sync {
115            if let Ok(save_file) = sync::SaveFile::from(path) {
116                actions.enable = match actions.enable {
117                    ModifyType::None => ModifyType::Some(save_file.mods),
118                    ModifyType::Some(mut mods) => {
119                        mods.extend(save_file.mods);
120                        ModifyType::Some(mods)
121                    }
122                    ModifyType::All => ModifyType::All,
123                }
124            }
125        }
126        if let Some(set_name) = args.enable_set {
127            if let Some(set) = config_file.sets.and_then(|mut sets| sets.remove(&set_name)) {
128                actions.enable = match actions.enable {
129                    ModifyType::None => ModifyType::Some(set),
130                    ModifyType::Some(mut mods) => {
131                        mods.extend(set);
132                        ModifyType::Some(mods)
133                    }
134                    ModifyType::All => todo!(),
135                };
136            }
137        }
138        if !args.enable.is_empty() {
139            actions.enable = match actions.enable {
140                ModifyType::None => ModifyType::Some(args.enable),
141                ModifyType::Some(mut mods) => {
142                    mods.extend(args.enable);
143
144                    ModifyType::Some(mods)
145                }
146                ModifyType::All => ModifyType::All,
147            };
148        }
149
150        // Add dependencies to the list
151        if let ModifyType::Some(ref mut mods) = actions.enable {
152            let mut cycle = mods.clone();
153            while !cycle.is_empty() {
154                cycle = cycle
155                    .iter()
156                    .filter(|mod_ident| mod_ident.name != "base")
157                    .filter_map(|mod_ident| {
158                        directory
159                            .get_mut(mod_ident)
160                            .and_then(|mod_version| {
161                                mod_version.get_info_json()?.dependencies.as_ref()
162                            })
163                            .map(|dependencies| {
164                                dependencies
165                                    .iter()
166                                    .filter(|dependency| {
167                                        dependency.name != "base"
168                                            && matches!(
169                                                dependency.dep_type,
170                                                ModDependencyType::NoLoadOrder
171                                                    | ModDependencyType::Required
172                                            )
173                                    })
174                                    .map(|dependency| ModIdent {
175                                        name: dependency.name.clone(),
176                                        version_req: dependency.version_req.clone(),
177                                    })
178                                    .collect::<Vec<ModIdent>>()
179                            })
180                    })
181                    .flatten()
182                    .collect();
183
184                mods.extend(cycle.clone());
185            }
186        }
187    }
188
189    Ok((actions, config, directory))
190}
191
192#[derive(StructOpt)]
193#[structopt(name = "fmm", about = "Manage your Factorio mods.")]
194struct Args {
195    /// The path to the configuration file
196    #[structopt(long)]
197    config: Option<PathBuf>,
198    /// The directory of the Factorio installation
199    #[structopt(long)]
200    game_dir: Option<PathBuf>,
201    /// The directory where mods are kept. Defaults to factorio-dir/mods
202    #[structopt(long)]
203    mods_dir: Option<PathBuf>,
204    /// Disables all mods in the directory
205    #[structopt(short = "o", long)]
206    disable_all: bool,
207    /// Disables the given mods. Mods are formatted as `Name`
208    #[structopt(short, long)]
209    disable: Vec<ModIdent>,
210    /// Enables all mods in the directory
211    #[structopt(short = "a", long)]
212    enable_all: bool,
213    /// Enables the mods in the given set
214    #[structopt(short = "E", long)]
215    enable_set: Option<String>,
216    /// Enables the given mods. Mods are formatted as `Name` or `Name@Version`
217    #[structopt(short, long)]
218    enable: Vec<ModIdent>,
219    /// Lists all mods in the directory
220    #[structopt(short, long)]
221    list: bool,
222    /// Removes the given mods from the mods directory. Mods are formatted as `Name` or `Name@Version`
223    #[structopt(short, long)]
224    remove: Vec<ModIdent>,
225    /// A path to a save file to sync with
226    #[structopt(short, long)]
227    sync: Option<PathBuf>,
228}
229
230#[serde_as]
231#[derive(Deserialize, Default)]
232struct ConfigFile {
233    game_dir: Option<PathBuf>,
234    mods_dir: Option<PathBuf>,
235    portal: Option<PortalAuth>,
236    #[serde_as(as = "Option<HashMap<_, Vec<DisplayFromStr>>>")]
237    sets: Option<HashMap<String, Vec<ModIdent>>>,
238}
239
240impl ConfigFile {
241    fn new(path: &Option<PathBuf>) -> Result<Option<Self>, ConfigFileErr> {
242        let config_path: Option<PathBuf> = path
243            .clone()
244            .or({
245                BaseDirs::new().map(|base_dirs| {
246                    let mut config_path: PathBuf = base_dirs.config_dir().into();
247                    config_path.push("fmm");
248                    config_path.push("fmm.toml");
249                    config_path
250                })
251            })
252            .filter(|config_path| config_path.exists());
253
254        if config_path.is_none() {
255            return Ok(None);
256        }
257
258        let file = fs::read_to_string(config_path.unwrap()).map_err(|_| ConfigFileErr::Open)?;
259
260        let config: ConfigFile = toml::from_str(&file).map_err(|_| ConfigFileErr::ParseFile)?;
261        Ok(Some(config))
262    }
263}
264
265#[derive(Debug, Error)]
266enum ConfigFileErr {
267    #[error("Could not open config file.")]
268    Open,
269    #[error("Could not parse config file.")]
270    ParseFile,
271}
272
273#[derive(Deserialize)]
274#[serde(rename_all = "kebab-case")]
275struct PlayerDataJson {
276    service_username: Option<String>,
277    service_token: Option<String>,
278}