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, loader: Option, config_file: File, } impl Create { const LOADERS: [&str; 2] = ["Vanilla", "Fabric"]; pub async fn new(project_name: String) -> 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,*/ loader: None, config_file, }) } 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(&self) -> Result> { Ok(match self.loader.as_deref() { Some("Vanilla") => VanillaDownloader::download().await?, Some("Fabric") => FabricDownloader::download().await?, _ => return Err("Invalid selection".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() -> Result>; } struct VanillaDownloader {} impl LoaderDownloader for VanillaDownloader { fn name() -> String { "Vanilla".to_string() } async fn download() -> 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?; 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 {} {}", Self::name(), 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: Self::name(), loader_version: None, }, mods: Mods {}, }; Ok(config) } } struct FabricDownloader {} impl LoaderDownloader for FabricDownloader { fn name() -> String { "Fabric".to_string() } async fn download() -> 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?; 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 {} {}, {}", Self::name(), 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: Self::name(), loader_version: Some(loader_version), }, mods: Mods {}, }; Ok(config) } }