mod models; mod utils; use crate::models::config::{Config, Mods, Server}; use crate::models::fabric::FabricVersions; use crate::{ models::vanilla::{VanillaVersionDetail, VanillaVersionType, VanillaVersions}, utils::{with_progressbar, with_spinner}, }; use clap::{ Parser, Subcommand, builder::styling::{self}, }; use dialoguer::{FuzzySelect, Select, theme::ColorfulTheme}; use futures_util::StreamExt; use sha1::{Digest, Sha1}; use std::error::Error; use tokio::io::AsyncWriteExt; fn styles() -> styling::Styles { styling::Styles::styled() .header(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD) .usage(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD) .literal(styling::AnsiColor::Cyan.on_default() | styling::Effects::BOLD) } #[derive(Parser)] #[command(name = "mcsm", version, about, styles = styles())] struct Mcsm { #[command(subcommand)] command: Commands, } #[derive(Subcommand)] enum Commands { Init { name: String }, } #[tokio::main] async fn main() -> Result<(), Box> { let mcsm = Mcsm::parse(); match mcsm.command { Commands::Init { name: project_name } => { tokio::fs::create_dir(&project_name).await?; std::env::set_current_dir(&project_name)?; let mut config_file = tokio::fs::File::create("mcsm.toml").await?; let loaders = vec!["Vanilla", "Fabric"]; let loader_index = Select::with_theme(&ColorfulTheme::default()) .with_prompt("Select Loader") .items(&loaders) .default(0) .interact()?; match loader_index { 0 => { const URL: &str = "https://launchermeta.mojang.com/mc/game/version_manifest.json"; let res = with_spinner("", "", || async { let res: VanillaVersions = reqwest::get(URL).await?.json().await?; Ok(res) }) .await?; let release_versions = res .versions .iter() .filter(|version| version.version_type == VanillaVersionType::Release) .collect::>(); let release_version_names = release_versions .iter() .map(|version| version.id.clone()) .collect::>(); let version_index = FuzzySelect::with_theme(&ColorfulTheme::default()) .with_prompt("Select Version") .items(&release_version_names) .default(0) .interact()?; let version_detail: VanillaVersionDetail = with_spinner("", "", || async { let version_detail: VanillaVersionDetail = reqwest::get(release_versions[version_index].url.clone()) .await? .json() .await?; Ok(version_detail) }) .await?; let start_msg = format!( "downloading server.jar for {} {}", loaders[loader_index], release_version_names[version_index] ); with_progressbar( &start_msg, "download finished", version_detail.downloads.server.size, |pb| async move { let server_bytes = reqwest::get(version_detail.downloads.server.url).await?; let mut file = tokio::fs::File::create("server.jar").await?; let mut stream = server_bytes.bytes_stream(); while let Some(chunk) = stream.next().await { let chunk = chunk?; file.write_all(&chunk).await?; pb.inc(chunk.len() as u64); } Ok(()) }, ) .await?; with_spinner("verifying sha1 hash", "sha1 hash verified", || async { let data = tokio::fs::read("server.jar").await?; let mut hasher = Sha1::new(); hasher.update(&data); let hash = hasher.finalize(); let hex: String = hash.iter().map(|b| format!("{:02x}", b)).collect(); if hex != version_detail.downloads.server.sha1 { tokio::fs::remove_file("server.jar").await?; return Err("sha1 verifaction failed for downloaded file".into()); } Ok(()) }) .await?; let config = Config { server: Server { version: release_version_names[version_index].to_string(), loader: loaders[loader_index].to_string(), loader_version: None, }, mods: Mods {}, }; config_file .write_all(toml::to_string(&config)?.as_bytes()) .await?; } 1 => { const URL: &str = "https://meta.fabricmc.net/v2/versions"; let res = with_spinner("", "", || async { let res: FabricVersions = reqwest::get(URL).await?.json().await?; Ok(res) }) .await?; let game_version_names = res .game .iter() .filter(|game| game.stable) .map(|game| game.version.clone()) .collect::>(); let game_version_index = FuzzySelect::with_theme(&ColorfulTheme::default()) .with_prompt("Select Game Version") .items(&game_version_names) .default(0) .interact()?; let loader_version_names = res .loader .iter() .filter(|loader| loader.stable) .map(|loader| loader.version.clone()) .collect::>(); let loader_version_index = FuzzySelect::with_theme(&ColorfulTheme::default()) .with_prompt("Select Loader Version") .items(&loader_version_names) .default(0) .interact()?; let game_version = game_version_names[game_version_index].clone(); let loader_version = loader_version_names[loader_version_index].clone(); let installer_version = res .installer .iter() .find_map(|installer| { if installer.stable { Some(installer.version.clone()) } else { None } }) .ok_or("No Installer Version Found")?; let start_msg = format!( "downloading server.jar for {} {}, {}", loaders[loader_index], loader_version_names[loader_version_index], game_version_names[game_version_index], ); with_spinner(&start_msg, "download finished", || async { let download_url = format!( "{}/loader/{}/{}/{}/server/jar", URL, game_version, loader_version, installer_version ); let res = reqwest::get(download_url).await?; let mut file = tokio::fs::File::create("server.jar").await?; let mut stream = res.bytes_stream(); while let Some(chunk) = stream.next().await { file.write_all(&chunk?).await?; } Ok(()) }) .await?; let config = Config { server: Server { version: game_version, loader: loaders[loader_index].to_string(), loader_version: Some(loader_version), }, mods: Mods {}, }; config_file .write_all(toml::to_string(&config)?.as_bytes()) .await?; } _ => return Err("Invalid selection".into()), } } } Ok(()) }