z_mirror/fmm
git clone https://git.raiguard.me/z_mirror/fmm
a95b21e
rewrite
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 )] 26config : Option < PathBuf >, 27# [ structopt ( long )] 28dir : Option < PathBuf >, 29# [ structopt ( short = "o" , long )] 30disable_all : bool , 31# [ structopt ( short , long )] 32disable : Vec < InputMod >, 33# [ structopt ( short = "a" , long )] 34enable_all : bool , 35# [ structopt ( short , long )] 36enable : Vec < InputMod >, 37} 38 39impl App { 40fn merge_config ( & mut self , config_file : ConfigFile ) { 41if let Some ( directory) = config_file. directory { 42self . 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 > { 54let contents =match ModEntryStructure :: parse ( entry) ?{ 55ModEntryStructure :: Directory |ModEntryStructure :: Symlink =>{ 56let mut path = entry. path (); 57 path. push ( "info.json" ); 58 fs:: read_to_string ( path). ok () ? 59} 60ModEntryStructure :: Zip =>{ 61let mut archive =ZipArchive :: new ( File :: open ( entry. path ()). ok () ?). ok () ?; 62let filename = archive 63. file_names () 64. find ( |name| name. contains ( "info.json" )) 65. map ( ToString :: to_string) ?; 66let mut file = archive. by_name ( & filename). ok () ?; 67let 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 >> { 77let mut app =App :: from_args (); 78 79let config =ConfigFile :: new ( & app. config ) ?; 80if let Some ( config) = config{ 81 app. merge_config ( config); 82} 83 84if app. dir . is_none () { 85return Err ( "Must specify a mods path via flag or via the configuration file." . into ()); 86} 87 88let dir = app. dir . unwrap (); 89 90// Get all mods in the directory 91let mod_entries = fs:: read_dir ( & dir) ? 92. filter_map ( |entry|{ 93let entry = entry. ok () ?; 94let file_name = entry. file_name (); 95 96if let Some (( mod_name, version)) = file_name. to_str () ?. rsplit_once ( "_" ) { 97let ( version, _) = version. rsplit_once ( "." ). unwrap_or (( version, "" )); // Strip file extension 98 99Some (( 100 mod_name. to_string (), 101ModVersion { 102 entry, 103version : Version :: parse ( version). ok () ?, 104}, 105)) 106} else { 107let info_json =read_info_json ( & entry) ?; 108 109Some (( 110 info_json. name , 111ModVersion { 112 entry, 113version : info_json. version , 114}, 115)) 116} 117}) 118. fold ( HashMap :: new (), |mut directory_mods, ( mod_name, version) |{ 119let versions = directory_mods. entry ( mod_name). or_insert_with ( Vec :: new); 120 121let 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 130let mut mlj_path = dir; 131 mlj_path. push ( "mod-list.json" ); 132let enabled_versions = std:: fs:: read_to_string ( & mlj_path) ?; 133let mut mod_list_json: ModListJson = serde_json:: from_str ( & enabled_versions) ?; 134 135// Disable all mods 136if app. disable_all { 137println! ( "Disabled all mods" ); 138for mod_datain 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 149if app. enable_all { 150println! ( "Enabled latest versions of all mods" ); 151for mod_datain mod_list_json. mods . iter_mut () { 152 mod_data. enabled =true ; 153 mod_data. version =None ; 154} 155} 156 157// Enable specified mods 158let mut to_enable = app. enable . clone (); 159while !to_enable. is_empty () { 160let mut to_enable_next =Vec :: new (); 161for mod_identin & to_enable{ 162if mod_ident. name !="base" { 163let mod_entry = mod_entries. get ( & mod_ident. name ). and_then ( |mod_versions|{ 164if 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 174if let Some ( mod_entry) = mod_entry{ 175let mod_state = mod_list_json 176. mods 177. iter_mut () 178. find ( |mod_state| mod_ident. name == mod_state. name ); 179 180let enabled = mod_state. is_some () && mod_state. as_ref (). unwrap (). enabled ; 181 182if !enabled{ 183println! ( "Enabled {} v{}" , mod_ident. name , mod_entry. version ); 184 185let version = mod_ident 186. version_req 187. as_ref () 188. map ( |_| mod_entry. version . clone ()); 189 190if 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 { 195name : mod_ident. name . to_string (), 196enabled : 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 , 210ModDependencyType :: NoLoadOrder 211 |ModDependencyType :: Required 212) 213}) 214. map ( |dependency|InputMod { 215name : dependency. name . clone (), 216version_req : dependency. version_req . clone (), 217}) 218. collect (), 219); 220} 221} else { 222println! ( "Could not find {}" , & mod_ident); 223} 224} 225} 226 227 to_enable = to_enable_next; 228} 229 230// Disable specified mods 231for mod_datain app. disable { 232if mod_data. name =="base" || mod_entries. contains_key ( & mod_data. name ) { 233let mod_state = mod_list_json 234. mods 235. iter_mut () 236. find ( |mod_state| mod_data. name == mod_state. name ); 237 238println! ( "Disabled {}" , & mod_data); 239 240if let Some ( mod_state) = mod_state{ 241 mod_state. enabled =false ; 242 mod_state. version =None ; 243} 244} else { 245println! ( "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 252Ok (()) 253}