use std::error::Error; use dialoguer::{FuzzySelect, Select, theme::ColorfulTheme}; use futures_util::StreamExt; use sha1::{Digest, Sha1}; use tokio::{fs::File, io::AsyncWriteExt}; use crate::{ models::{ config::{Config, Mods, Server}, fabric::FabricVersions, vanilla::{VanillaVersionDetail, VanillaVersionType, VanillaVersions}, }, utils::{with_progressbar, with_spinner}, }; pub struct Create { // project_name: String, config_file: File, version: Option, loader: Option, loader_version: Option, show_snapshots: bool, show_betas: bool, show_alphas: bool, } impl Create { const LOADERS: [&str; 2] = ["Vanilla", "Fabric"]; pub async fn new( project_name: String, version: Option, loader: Option, loader_version: Option, show_snapshots: bool, show_betas: bool, show_alphas: bool, ) -> Result> { tokio::fs::create_dir_all(&project_name).await?; std::env::set_current_dir(&project_name)?; let config_file = tokio::fs::File::create("mcsm.toml").await?; Ok(Create { /*project_name,*/ config_file, version, loader, loader_version, show_snapshots, show_betas, show_alphas, }) } pub async fn init_loader(&mut self) -> Result<(), Box> { let loader_index = Select::with_theme(&ColorfulTheme::default()) .with_prompt("Select Loader") .items(Self::LOADERS) .default(0) .interact()?; self.loader = Some(Self::LOADERS[loader_index].to_string()); Ok(()) } pub async fn download_server(&mut self) -> Result> { Ok(match self.loader.as_deref() { Some("Vanilla") => VanillaDownloader::download(self).await?, Some("Fabric") => FabricDownloader::download(self).await?, Some(l) => return Err(format!("Invalid loader specified: {}", l).into()), None => return Err("No loader specified".into()), }) } pub async fn save_config(&mut self, config: Config) -> Result<(), Box> { self.config_file .write_all(toml::to_string(&config)?.as_bytes()) .await?; Ok(()) } } trait LoaderDownloader { fn name() -> String; async fn download(create: &mut Create) -> Result>; } struct VanillaDownloader {} impl LoaderDownloader for VanillaDownloader { fn name() -> String { "Vanilla".to_string() } async fn download(create: &mut Create) -> Result> { 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?; if let Some(version) = create.version.clone() { let version_exists = res.versions.iter().any(|v| v.id == version); if !version_exists { return Err("Invalid minecraft version specified".into()); } } else { let versions = res .versions .iter() .filter(|version| { version.version_type == VanillaVersionType::Release || (create.show_snapshots && version.version_type == VanillaVersionType::Snapshot) || (create.show_betas && version.version_type == VanillaVersionType::OldBeta) || (create.show_alphas && version.version_type == VanillaVersionType::OldAlpha) }) .collect::>(); let version_names = versions .iter() .map(|version| version.id.as_str()) .collect::>(); let version_index = FuzzySelect::with_theme(&ColorfulTheme::default()) .with_prompt("Select Version") .items(&version_names) .default(0) .interact()?; create.version = Some(version_names[version_index].to_string()); } let version = res .versions .iter() .find(|v| &v.id == create.version.as_ref().unwrap()) .unwrap(); let version_detail: VanillaVersionDetail = with_spinner("", "", || async { let version_detail: VanillaVersionDetail = reqwest::get(version.url.clone()).await?.json().await?; Ok(version_detail) }) .await?; let start_msg = format!( "downloading server.jar for {} {}", Self::name(), create.version.as_ref().unwrap(), ); 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: create.version.clone().unwrap(), loader: Self::name(), loader_version: None, }, mods: Mods {}, }; Ok(config) } } struct FabricDownloader {} impl LoaderDownloader for FabricDownloader { fn name() -> String { "Fabric".to_string() } async fn download(create: &mut Create) -> Result> { 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?; if let Some(version) = create.version.clone() { let exists = res.game.iter().any(|game| game.version == version); if !exists { return Err("Invalid minecraft version specified".into()); } } else { let game_version_names = res .game .iter() .filter(|game| create.show_snapshots || game.stable) .map(|game| game.version.as_str()) .collect::>(); let game_version_index = FuzzySelect::with_theme(&ColorfulTheme::default()) .with_prompt("Select Game Version") .items(&game_version_names) .default(0) .interact()?; create.version = Some(game_version_names[game_version_index].to_string()); } if let Some(loader_version) = create.loader_version.clone() { let exists = res .loader .iter() .any(|loader| loader.version == loader_version); if !exists { return Err(format!("Invalid {} loader version specified", Self::name()).into()); } } else { 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()?; create.loader_version = Some(loader_version_names[loader_version_index].to_string()); } 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 {} {}, {}", Self::name(), create.loader_version.as_ref().unwrap(), create.version.as_ref().unwrap(), ); with_spinner(&start_msg, "download finished", || async { let download_url = format!( "{}/loader/{}/{}/{}/server/jar", URL, create.version.as_ref().unwrap(), create.loader_version.as_ref().unwrap(), 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: create.version.clone().unwrap(), loader: Self::name(), loader_version: create.loader_version.clone(), }, mods: Mods {}, }; Ok(config) } }