use std::io::Write; use anyhow::{anyhow, bail, Context, Result}; use clap::{crate_version, App, AppSettings, Arg, ArgMatches, SubCommand}; use itertools::Itertools; use log::debug; use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; use modderbaas::{baas::Baas, MineMod, Snapshot, World}; mod fancyfmt; mod uiutil; use uiutil::{ask_continue, user_choice}; mod install; use install::install_mods; fn main() -> Result<()> { stderrlog::new() .module(module_path!()) //.verbosity(1) .verbosity(5) .init() .unwrap(); let matches = App::new("ModderBaas") .version(crate_version!()) .arg( Arg::with_name("world") .long("world") .short("w") .required(false) .help("Select the world that we should operate on") .takes_value(true), ) .subcommand( SubCommand::with_name("enable") .about("Enables a mod and its dependencies") .arg( Arg::with_name("mod") .multiple(true) .required(true) .help("The mod to enable"), ), ) .subcommand( SubCommand::with_name("install") .about("Installs a mod and its dependencies") .arg( Arg::with_name("mod") .multiple(true) .required(true) .help("The mod to install"), ) .arg( Arg::with_name("target") .short("t") .long("target-dir") .help("The mod target directory") .default_value("."), ) .arg( Arg::with_name("dry-run") .short("n") .long("dry-run") .help("Only resolve dependencies, don't actually install any mods") .required(false), ) .arg( Arg::with_name("chown") .short("o") .long("chown") .help("Change the owner of the installed mod to match target-dir's") .required(false), ), ) .settings(&[AppSettings::SubcommandRequired, AppSettings::ColoredHelp]) .get_matches(); let mut stdout = StandardStream::stdout(ColorChoice::Auto); let baas = Baas::with_standard_dirs()?; let snapshot = baas .snapshot() .context("Creating the initial snapshot failed")?; let world = select_world(&mut stdout, &matches, &snapshot)?; write!(stdout, "Using world ")?; stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?; writeln!(stdout, "{}", world.world_name()?)?; stdout.reset()?; if let Some(enable) = matches.subcommand_matches("enable") { let mods = enable.values_of("mod").unwrap().collect::>(); enable_mods(&mut stdout, &snapshot, &world, &mods)?; } if let Some(install) = matches.subcommand_matches("install") { let mods = install.values_of("mod").unwrap().collect::>(); install_mods(&mut stdout, &baas, &world, &mods, install)?; } Ok(()) } /// Select the world that we want to work on. /// /// If there is only one world available, we use it. /// If there are more worlds, we ask the user for a choice. /// /// If the command line argument is given, it overrides the previous rules. fn select_world( output: &mut StandardStream, cli: &ArgMatches, snapshot: &Snapshot, ) -> Result { debug!("Starting world selection"); let worlds = snapshot.worlds(); if worlds.is_empty() { bail!("No world found"); } if let Some(world_name) = cli.value_of("world") { if let Some(world) = worlds.get(world_name) { return Ok(world.clone()); } else { bail!("Invalid world name given: {}", world_name); } } if worlds.len() == 1 { return Ok(worlds .values() .next() .expect("We just checked the length!") .clone()); } // Here, we cannot do an automatic selection, so ask the user: let worlds = worlds .iter() .sorted_by_key(|i| i.0) .map(|i| i.1) .collect::>(); writeln!( output, "The following worlds were found, please select one:" )?; user_choice(&worlds, output).map(|&i| i.clone()) } /// Enables the given list of mods and their dependencies. /// /// Fails if any mod has a dependency that can not be satisfied with the locally available mods. fn enable_mods( output: &mut StandardStream, snapshot: &Snapshot, world: &World, mods: &[&str], ) -> Result<()> { let mut wanted = mods.iter().map(|&s| s.to_owned()).collect::>(); let mut to_enable = Vec::::new(); let game = snapshot .games() .get(&world.game_id()?) .ok_or_else(|| anyhow!("The game definition was not found"))?; let game_mods = game .mods()? .into_iter() .map(|m| m.mod_id()) .collect::, _>>()?; while !wanted.is_empty() { let next_mod = wanted.remove(0); // Do we already have the mod enabled, somehow? if world.mods()?.contains(&next_mod) || game_mods.contains(&next_mod) { continue; } match snapshot.mods().get(&next_mod as &str) { Some(m) => { to_enable.push(m.clone()); wanted.extend(m.dependencies()?); } None => bail!("Mod {} could not be found", next_mod), } } if to_enable.is_empty() { writeln!(output, "Done!")?; return Ok(()); } writeln!(output, "Enabling {} mods:", to_enable.len())?; writeln!(output, "{}", to_enable.iter().join(", "))?; ask_continue(output)?; for m in to_enable { let mod_id = m.mod_id()?; world .enable_mod(&mod_id) .context(format!("Error enabling '{}'", mod_id))?; } writeln!(output, "Done!")?; Ok(()) }