use log::{debug, info}; use reqwest::get; use serde::Deserialize; use tokio::{ fs::{self, File, create_dir_all}, io::AsyncWriteExt, }; use crate::{ config::Config, errors::McError, minecraft::manifests::{Library, Version}, platform::paths, }; #[derive(Debug, Deserialize)] struct AssetObject { hash: String, size: u64, } #[derive(Debug, Deserialize)] struct AssetIndexManifest { objects: std::collections::HashMap, } /// Pobiera wszystko potrzebne do uruchomienia Minecraft: /// - client jar /// - biblioteki (artifact + natives) /// - assets (w tym textures, sounds) pub async fn download_all( config: &Config, version: &Version, ) -> Result<(), McError> { download_client(config, version).await?; download_libraries(config, &version.libraries).await?; download_assets(config, version).await?; Ok(()) } async fn download_client( config: &Config, version: &Version, ) -> Result<(), McError> { let jar_path = paths::client_jar(config, &version.id)?; if jar_path.exists() { debug!("Client jar already exists: {}", jar_path.display()); return Ok(()); } info!("Downloading client {}", version.id); download_file(&version.downloads.client.url, &jar_path).await } async fn download_libraries( config: &Config, libraries: &[Library], ) -> Result<(), McError> { for library in libraries { // ===== CLASSPATH LIBRARIES ===== if let Some(artifact) = &library.downloads.artifact { let library_path = paths::library_file(config, &artifact.path)?; if !library_path.exists() { info!("Downloading library {}", artifact.path); download_file(&artifact.url, &library_path).await?; } } // ===== NATIVES ===== if let Some(classifiers) = &library.downloads.classifiers { for (_, native) in classifiers { let native_path = paths::library_file(config, &native.path)?; if native_path.exists() { continue; } info!("Downloading native library {}", native.path); download_file(&native.url, &native_path).await?; } } } Ok(()) } async fn download_asset_index( config: &Config, version: &Version, ) -> Result { let assets_dir = paths::assets_dir(config); create_dir_all(assets_dir.join("indexes")).await?; let asset_index = version.asset_index.as_ref().ok_or_else(|| { McError::Config("Missing asset_index in version.json".into()) })?; // Nie pozwalamy na legacy dla nowoczesnych wersji if asset_index.id == "legacy" { return Err(McError::Config( "Legacy assetIndex detected – pobierz właściwy version.json".into(), )); } let index_path = assets_dir .join("indexes") .join(format!("{}.json", asset_index.id)); // Jeśli indeks istnieje lokalnie if index_path.exists() { let index_data = fs::read_to_string(&index_path).await?; let manifest: AssetIndexManifest = serde_json::from_str(&index_data)?; return Ok(manifest); } // Pobierz indeks z sieci info!("Downloading asset index {}", asset_index.id); let response = get(&asset_index.url).await?; let manifest_text = response.text().await?; fs::write(&index_path, &manifest_text).await?; let manifest: AssetIndexManifest = serde_json::from_str(&manifest_text)?; Ok(manifest) } async fn download_assets( config: &Config, version: &Version, ) -> Result<(), McError> { let assets_dir = paths::assets_dir(config); // Katalogi MUSZĄ istnieć create_dir_all(assets_dir.join("objects")).await?; create_dir_all(assets_dir.join("indexes")).await?; let manifest = download_asset_index(config, version).await?; // Pobieramy wszystkie obiekty for (logical_path, asset) in &manifest.objects { let subdir = &asset.hash[0..2]; let file_path = assets_dir .join("objects") .join(subdir) .join(&asset.hash); if file_path.exists() { continue; } let url = format!( "https://resources.download.minecraft.net/{}/{}", subdir, asset.hash ); info!("Downloading asset {} -> {}", logical_path, file_path.display()); download_file(&url, &file_path).await?; } // Pobierz sounds.json jeśli istnieje if let Some(asset) = manifest.objects.get("sounds.json") { let file_path = assets_dir.join("indexes").join("sounds.json"); if !file_path.exists() { let subdir = &asset.hash[0..2]; let url = format!( "https://resources.download.minecraft.net/{}/{}", subdir, asset.hash ); info!("Downloading sounds.json"); download_file(&url, &file_path).await?; } } Ok(()) } /// Helper do pobierania plików async fn download_file( url: &str, path: &std::path::Path, ) -> Result<(), McError> { if let Some(parent) = path.parent() { create_dir_all(parent).await?; } let response = get(url).await?; let bytes = response.bytes().await?; let mut file = File::create(path).await?; file.write_all(&bytes).await?; Ok(()) }