mod cli; use std::error::Error; use std::path::Path; use std::process::ExitCode; use log::{error, info, trace, LevelFilter}; use clap::Parser; use ozone::launcher::{Instance, JavaRuntimeSetting, Launcher, Settings, ALT_CLIENT_ID, MAIN_CLIENT_ID}; use ozone::launcher::version::{VersionList, VersionResult}; use uuid::Uuid; use ozone::auth::{Account, AccountStorage, MsaAccount}; use ozone::auth::error::AuthErrorKind; use crate::cli::{AccountCommand, Cli, InstanceCommand, ProfileSelectArgs, RootCommand}; const ACCOUNT_DB_PATH: &str = "ozone_accounts.json"; fn find_account<'a>(auth: &'a AccountStorage, input: &ProfileSelectArgs) -> Result, ()> { if let Some(uuid) = input.uuid { Ok(auth.iter_accounts().filter(|(_, a)| match *a { Account::Dummy(p) => p.id == uuid, Account::MSA(account) => account.player_profile.as_ref().is_some_and(|p| p.id == uuid) }).collect()) } else if let Some(ref name) = input.name { let name = name.to_ascii_lowercase(); Ok(auth.iter_accounts().filter(|(_, a)| match *a { Account::Dummy(profile) => profile.name.to_ascii_lowercase().starts_with(&name), Account::MSA(account) => account.player_profile.as_ref().is_some_and(|p| p.name.to_ascii_lowercase().starts_with(&name)) }).collect()) } else if let Some(ref gt) = input.gamertag { let gt = gt.to_ascii_lowercase(); Ok(auth.iter_accounts().filter(|(_, a)| match *a { Account::MSA(account) => account.gamertag.as_ref().is_some_and(|g| g.to_ascii_lowercase().starts_with(>)), _ => false }).collect()) } else if let Some(ref xuid) = input.xuid { Ok(auth.iter_accounts().filter(|(_, a)| match *a { Account::MSA(account) => account.xuid.as_ref().is_some_and(|x| x == xuid), _ => false }).collect()) } else { eprintln!("No account specified."); Err(()) } } fn display_instance(instance: &Instance, id: Uuid, home: impl AsRef, selected: bool, verbose: bool) { println!("Instance `{}':{}", instance.name, if selected { " (selected)" } else { "" }); println!(" UUID: {}", id); println!(" Version: {}", instance.game_version); println!(" Location: {}", home.as_ref().join(Settings::get_instance_path(id)).display()); if !verbose { return; } if let Some(ref args) = instance.jvm_arguments { println!(" JVM arguments: <{} argument{}>", args.len(), if args.len() == 1 { "" } else { "s" }) } if let Some(res) = instance.resolution { println!(" Resolution: {}x{}", res.width, res.height); } match &instance.java_runtime { Some(JavaRuntimeSetting::Component(c)) => println!(" Java runtime component: {c}"), Some(JavaRuntimeSetting::Path(p)) => println!(" Java runtime path: {}", p.display()), _ => () } } fn display_account(account: &Account, selected: bool, verbose: bool) { let selected = if selected { " (selected)" } else { "" }; match *account { Account::Dummy(ref profile) => { println!("Dummy account:{selected}"); println!(" Username: {}", profile.name); println!(" UUID: {}", profile.id); if verbose { println!(" Properties: <{} propert{}>", profile.properties.len(), if profile.properties.len() == 1 { "y" } else { "ies" }); } }, Account::MSA(ref msa_acct) => { println!("Microsoft account:{selected}"); if let Some(ref profile) = msa_acct.player_profile { println!(" Username: {}", profile.name); println!(" UUID: {}", profile.id); if verbose { println!(" Properties: <{} propert{}>", profile.properties.len(), if profile.properties.len() == 1 { "y" } else { "ies" }); } } else { println!(" Username: "); println!(" UUID: "); if verbose { println!(" Properties: "); } } println!(" Xbox Gamertag: {}", msa_acct.gamertag.as_deref().unwrap_or("")); if verbose { println!(" XUID: {}", msa_acct.xuid.as_deref().unwrap_or("")); } } } } async fn main_inner(cli: Cli) -> Result> { let Some(home) = cli.home.or_else(Launcher::sensible_home) else { error!("Could not choose a launcher home directory. Please choose one with `--home'."); return Ok(ExitCode::FAILURE); // we print our own error message }; trace!("Sensible home could be {home:?}"); let mut settings = Settings::load(home.join("ozone.json")).await?; match &cli.subcmd { RootCommand::Instance(inst_args) => match inst_args.command() { InstanceCommand::List => { let mut first = true; if settings.instances.is_empty() { eprintln!("There are no instances. Create one with `instance create '."); return Ok(ExitCode::SUCCESS); } for (cur_id, instance) in settings.instances.iter() { if !first { println!(); } first = false; let cur_id = *cur_id; let sel = settings.selected_instance.is_some_and(|id| id == cur_id); display_instance(instance, cur_id, &home, sel, false); } }, InstanceCommand::Create(args) => { if args.name.is_empty() { eprintln!("The instance must not have an empty name."); return Ok(ExitCode::FAILURE); } let mut inst = if args.clone { if let Some(selected_inst) = settings.get_selected_instance() { let mut inst = selected_inst.clone(); inst.name.replace_range(.., &args.name); inst } else { eprintln!("You do not have an instance selected."); return Ok(ExitCode::FAILURE); } } else { Instance::new(&args.name) }; if let Some(ref ver_name) = args.settings.version { // FIXME: don't hardcode "versions" path let versions = VersionList::new(home.join("versions"), !cli.offline).await?; if matches!(versions.get_version_lazy(ver_name), VersionResult::None) { eprintln!("The version `{}' could not be found.", ver_name); return Ok(ExitCode::FAILURE); } } args.settings.apply_to(&mut inst); let new_id = Uuid::new_v4(); settings.instances.insert(new_id, inst); if !args.no_select { settings.selected_instance = Some(new_id); } settings.save().await?; }, InstanceCommand::Select(args) => { if let Ok(uuid) = args.instance.parse::() { if !settings.instances.contains_key(&uuid) { eprintln!("No instances were found by that UUID."); return Ok(ExitCode::FAILURE); } settings.selected_instance = Some(uuid); settings.save().await?; return Ok(ExitCode::SUCCESS); } let search_norm = args.instance.to_lowercase(); let found: Vec<_> = settings.instances.iter() .filter(|(_, inst)| { // FIXME: find a better way of doing this matching inst.name.to_lowercase().starts_with(&search_norm) }).collect(); if found.is_empty() { eprintln!("No instances were found."); return Ok(ExitCode::FAILURE); } if found.len() > 1 { eprintln!("Ambiguous argument. Found {} instances:", found.len()); for (id, inst) in found { eprintln!("- {} ({id})", inst.name); } return Ok(ExitCode::FAILURE); } let (found_id, found_inst) = found.first().unwrap(); println!("Selected instance {} ({found_id}).", found_inst.name); settings.selected_instance = Some(**found_id); settings.save().await?; }, InstanceCommand::Set(args) => { let Some(inst) = settings.get_selected_instance_mut() else { eprintln!("No instance selected."); return Ok(ExitCode::FAILURE); }; args.apply_to(inst); settings.save().await?; }, InstanceCommand::Delete => { let Some(inst) = settings.selected_instance else { eprintln!("No instance selected."); return Ok(ExitCode::FAILURE); }; settings.instances.remove(&inst); settings.selected_instance = None; settings.save().await?; }, InstanceCommand::Info => { let Some(inst) = settings.get_selected_instance() else { eprintln!("No instance selected."); return Ok(ExitCode::FAILURE); }; display_instance(inst, settings.selected_instance.unwrap(), &home, false, true); }, InstanceCommand::Rename { name } => { if name.is_empty() { eprintln!("The instance must not have an empty name."); return Ok(ExitCode::FAILURE); } let Some(inst) = settings.get_selected_instance_mut() else { eprintln!("No instance selected."); return Ok(ExitCode::FAILURE); }; inst.name.replace_range(.., name); settings.save().await?; } }, RootCommand::Account(account_args) => { let accounts_path = home.join(ACCOUNT_DB_PATH); let mut accounts = AccountStorage::load(&accounts_path).await?; match &account_args.command { AccountCommand::Select(args) => { let Ok(results) = find_account(&accounts, args) else { return Ok(ExitCode::FAILURE); }; if results.is_empty() { eprintln!("No account was found."); return Ok(ExitCode::FAILURE); } if results.len() > 1 { eprintln!("Ambiguous argument. Found {} accounts:", results.len()); for (_, account) in results { eprintln!("- {account}"); } return Ok(ExitCode::FAILURE); } let (key, account) = results.into_iter().next().unwrap(); println!("Selected account: {account}"); let key = key.clone(); accounts.set_selected_account(key); accounts.save(&accounts_path).await?; }, AccountCommand::Forget => { if accounts.pop_selected_account().is_none() { eprintln!("No account selected."); return Ok(ExitCode::FAILURE); } accounts.save(&accounts_path).await?; }, AccountCommand::SignIn(args) => { let (client_id, azure) = if args.use_alt_client_id { (ALT_CLIENT_ID, false) } else { (MAIN_CLIENT_ID, true) }; let client = MsaAccount::create_client(); let mut acct = MsaAccount::with_client_id(client_id, azure); acct.xbl_login_device(&client, |d| async move { if let Some(uri_complete) = d.verification_uri_complete() { println!("In a browser, please navigate to the following URL:"); println!("{}", uri_complete.secret()); } else { println!("In a browser, please navigate to the following URL: {}", d.verification_uri()); println!("Use the following device code: {}", d.user_code().secret()) } }).await?; println!("Authentication success! Logging in..."); match acct.log_in_silent(&client).await { Ok(_) => (), Err(e) => match e.kind() { AuthErrorKind::NotOnXbox => { eprintln!("This Microsoft account is not on Xbox. Please make sure you are using the correct Microsoft account."); return Ok(ExitCode::FAILURE); }, AuthErrorKind::TooYoung => { eprintln!("Currently, Microsoft accounts held by minors (under 18 years old) cannot sign into third party applications (such as olauncher) unless they are in a family."); eprintln!("If you do not wish to configure a family, try running this command with the `--use-alt-client-id' flag."); return Ok(ExitCode::FAILURE); }, AuthErrorKind::NotEntitled => { eprintln!("Warning: This Microsoft account does not seem to own the game."); eprintln!("This account will only be able to play the demo version of the game."); }, AuthErrorKind::NoProfile => { eprintln!("Warning: It appears that you own the game but have not yet created a profile."); eprintln!("Visit https://minecraft.net to choose a name. Until then, you will only be able to play the demo version of the game."); }, _ => { eprintln!("An unknown error occurred while signing into the account:"); eprintln!("{e}"); return Ok(ExitCode::FAILURE); } } } let key = accounts.add_account(acct.into()).expect("authentication succeeded but xuid missing????"); if !args.no_select { accounts.set_selected_account(key); } accounts.save(&accounts_path).await?; println!("Success! Account added."); }, AccountCommand::List => { let iter = accounts.iter_accounts(); if iter.len() == 0 { eprintln!("There are no accounts."); return Ok(ExitCode::FAILURE); } let sel_id = accounts.get_selected_account().map(|(id, _)| id); let mut first = true; for (id, account) in iter { if !first { println!(); } first = false; display_account(account, sel_id.is_some_and(|s| s == id), false); } }, AccountCommand::Info => { let Some((_, account)) = accounts.get_selected_account() else { eprintln!("No account selected."); return Ok(ExitCode::FAILURE); }; display_account(account, false, true); }, AccountCommand::Refresh => { let Some(account) = accounts.get_selected_account_mut() else { eprintln!("No account selected."); return Ok(ExitCode::FAILURE); }; let client = MsaAccount::create_client(); match account { Account::MSA(msa_acct) => { msa_acct.log_in_silent(&client).await?; println!("Successfully refreshed account: {}", account); }, _ => { eprintln!("Cannot refresh non-MSA account."); return Ok(ExitCode::FAILURE); } } accounts.save(&accounts_path).await?; } } } RootCommand::Launch => { let Some(selection) = settings.selected_instance else { eprintln!("No instance selected."); return Ok(ExitCode::FAILURE); }; let inst = settings.instances.get(&selection).expect("settings inconsistency"); settings.save().await?; let accounts_path = home.join(ACCOUNT_DB_PATH); let mut accounts = AccountStorage::load(&accounts_path).await?; let Some(account) = accounts.get_selected_account_mut() else { eprintln!("No account selected."); return Ok(ExitCode::FAILURE); }; match account { Account::MSA(msa_acct) => { let client = MsaAccount::create_client(); println!("Looking up account information..."); match msa_acct.log_in_silent(&client).await { Ok(_) => (), Err(e) if e.kind() == AuthErrorKind::InteractionRequired => { eprintln!("This account requires interactive authentication: {}", account); eprintln!("Details: {e}"); return Ok(ExitCode::FAILURE); }, Err(e) => { eprintln!("Error refreshing account: {e}"); return Ok(ExitCode::FAILURE); } } accounts.save(&accounts_path).await?; } _ => () // nothing to be done } println!("Preparing the game files..."); let launcher = Launcher::new(&home, !cli.offline).await?; let launch = launcher.prepare_launch(inst, Settings::get_instance_path(selection), settings.client_id).await.map_err(|e| { error!("error launching: {e}"); e })?; dbg!(&launch); info!("ok!"); println!("Launching the game!"); ozone::launcher::run_the_game(&launch)?; } } Ok(ExitCode::SUCCESS) } #[tokio::main] async fn main() -> ExitCode { // use Warn as the default level to minimize noise on the command line simple_logger::SimpleLogger::new().env().init().unwrap(); let arg = Cli::parse(); main_inner(arg).await.unwrap_or_else(|e| { error!("Launcher initialization error:"); error!("{e}"); ExitCode::FAILURE }) }