From a393e0a2f2c3678a3ea869dc1417fa269f2b1040 Mon Sep 17 00:00:00 2001 From: Filip Wandzio Date: Sat, 24 Jan 2026 08:29:14 +0100 Subject: Resolve audio not loading bug Ensure all assets are downloading for each version Temporarily disable minecraft versions older than 1.8 because of the asset/manifest loading issues Implement basic documentation of modules Implement basic async/multithreading for downloading assets --- src/minecraft/downloads.rs | 172 ++++++++++++++++++++++++++++++++++++++------ src/minecraft/extraction.rs | 116 ++++++++++++++++++++++++++++-- src/minecraft/launcher.rs | 91 ++++++++++++++++++----- src/minecraft/manifests.rs | 43 ++++++++++- src/minecraft/mod.rs | 15 ++++ 5 files changed, 391 insertions(+), 46 deletions(-) (limited to 'src/minecraft') diff --git a/src/minecraft/downloads.rs b/src/minecraft/downloads.rs index 5be5a05..a994146 100644 --- a/src/minecraft/downloads.rs +++ b/src/minecraft/downloads.rs @@ -1,5 +1,10 @@ use log::{debug, info}; -use tokio::{fs, io::AsyncWriteExt}; +use reqwest::get; +use serde::Deserialize; +use tokio::{ + fs::{self, File, create_dir_all}, + io::AsyncWriteExt, +}; use crate::{ config::Config, @@ -8,58 +13,181 @@ use crate::{ platform::paths, }; -/// Download everything required to launch: +#[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 -/// - libraries -pub async fn download_all(config: &Config, version: &Version) -> Result<(), McError> { +/// - 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> { +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"); + 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 lib in libraries { - let Some(artifact) = &lib.downloads.artifact else { - continue; - }; +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(()) +} - let lib_path = paths::library_file(config, &artifact.path)?; +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) +} - if lib_path.exists() { +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; } - info!("Downloading library {}", artifact.path); - download_file(&artifact.url, &lib_path).await?; + 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 ---------------- */ - -async fn download_file(url: &str, path: &std::path::Path) -> Result<(), McError> { +/// Helper do pobierania plików +async fn download_file( + url: &str, + path: &std::path::Path, +) -> Result<(), McError> { if let Some(parent) = path.parent() { - fs::create_dir_all(parent).await?; + create_dir_all(parent).await?; } - let response = reqwest::get(url).await?; + let response = get(url).await?; let bytes = response.bytes().await?; - let mut file = fs::File::create(path).await?; + let mut file = File::create(path).await?; file.write_all(&bytes).await?; Ok(()) diff --git a/src/minecraft/extraction.rs b/src/minecraft/extraction.rs index 5175ee0..b58fd2e 100644 --- a/src/minecraft/extraction.rs +++ b/src/minecraft/extraction.rs @@ -1,10 +1,118 @@ +use std::{fs, io, path::Path}; + use log::info; +use zip::ZipArchive; + +use crate::{ + errors::McError, + minecraft::manifests::{Library, Version}, +}; -use crate::errors::McError; pub fn extract_natives( - _cfg: &crate::config::Config, - version: &crate::minecraft::manifests::Version, + cfg: &crate::config::Config, + version: &Version, ) -> Result<(), McError> { - info!("Extracting natives for {}", version.id); + let natives_dir = cfg + .data_dir + .join("minecraft") + .join("versions") + .join(&version.id) + .join("natives"); + + info!("Extracting natives for {} into {:?}", version.id, natives_dir); + + if natives_dir.exists() { + fs::remove_dir_all(&natives_dir)?; + } + fs::create_dir_all(&natives_dir)?; + + for lib in &version.libraries { + if !library_allowed(lib) { + continue; + } + + let natives = match &lib.natives { + | Some(n) => n, + | None => continue, + }; + + let classifier = match natives.get("linux") { + | Some(c) => c, + | None => continue, + }; + + let classifiers = match &lib.downloads.classifiers { + | Some(c) => c, + | None => continue, + }; + + let artifact = match classifiers.get(classifier) { + | Some(a) => a, + | None => continue, + }; + + let jar_path = cfg + .data_dir + .join("minecraft") + .join("libraries") + .join(&artifact.path); + + info!("Extracting natives from {:?}", jar_path); + + extract_zip(&jar_path, &natives_dir)?; + } + + Ok(()) +} + +fn library_allowed(lib: &Library) -> bool { + let rules = match &lib.rules { + | Some(r) => r, + | None => return true, + }; + + let mut allowed = false; + + for rule in rules { + let os_match = match &rule.os { + | Some(os) => os.name == "linux", + | None => true, + }; + + if os_match { + allowed = rule.action == "allow"; + } + } + + allowed +} + +fn extract_zip(jar_path: &Path, out_dir: &Path) -> Result<(), McError> { + let file = fs::File::open(jar_path)?; + let mut zip = ZipArchive::new(file)?; + + for i in 0..zip.len() { + let mut entry = zip.by_index(i)?; + let name = entry.name(); + + if name.starts_with("META-INF/") { + continue; + } + + let out_path = out_dir.join(name); + + if entry.is_dir() { + fs::create_dir_all(&out_path)?; + continue; + } + + if let Some(parent) = out_path.parent() { + fs::create_dir_all(parent)?; + } + + let mut out_file = fs::File::create(&out_path)?; + io::copy(&mut entry, &mut out_file)?; + } + Ok(()) } diff --git a/src/minecraft/launcher.rs b/src/minecraft/launcher.rs index f7e3ecc..cfd6c85 100644 --- a/src/minecraft/launcher.rs +++ b/src/minecraft/launcher.rs @@ -2,26 +2,39 @@ use std::process::Command; use log::{debug, info}; -use crate::{config::Config, errors::McError, minecraft::manifests::Version, platform::paths}; +use crate::{ + config::Config, + errors::McError, + minecraft::manifests::{Library, Version}, + platform::paths, +}; -/// Build the full classpath -fn build_classpath(config: &Config, version: &Version) -> Result { +/// Buduje classpath dla danej wersji Minecrafta +fn build_classpath( + config: &Config, + version: &Version, +) -> Result { let sep = if cfg!(windows) { ";" } else { ":" }; let mut entries = Vec::new(); - for lib in &version.libraries { - if let Some(artifact) = &lib.downloads.artifact { + for library in &version.libraries { + if !library_allowed(library) { + continue; + } + if let Some(artifact) = &library.downloads.artifact { let path = paths::library_file(config, &artifact.path)?; entries.push(path.to_string_lossy().to_string()); } } + // client.jar zawsze na końcu classpath let client_jar = paths::client_jar(config, &version.id)?; entries.push(client_jar.to_string_lossy().to_string()); + Ok(entries.join(sep)) } -/// Launch Minecraft +/// Uruchamia Minecraft pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { let java = &config.java_path; let classpath = build_classpath(config, version)?; @@ -34,26 +47,46 @@ pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { ))); } + let asset_index_id = version + .asset_index + .as_ref() + .ok_or_else(|| { + McError::Runtime("Missing assetIndex in version.json".into()) + })? + .id + .clone(); + info!("Launching Minecraft {}", version.id); debug!("Classpath: {}", classpath); debug!("Natives: {}", natives_dir.display()); + debug!("Asset index: {}", asset_index_id); + + let mut cmd = Command::new(java); + + // ===== JVM ARGUMENTS (muszą być na początku) ===== + cmd.arg(format!("-Xmx{}M", config.max_memory_mb)) + .arg(format!("-Djava.library.path={}", natives_dir.display())); - let status = Command::new(java) - .arg(format!("-Xmx{}M", config.max_memory_mb)) - .arg(format!("-Djava.library.path={}", natives_dir.display())) - .arg("-cp") + for arg in &config.jvm_args { + cmd.arg(arg); + } + + // ===== CLASSPATH + MAIN CLASS ===== + cmd.arg("-cp") .arg(classpath) - .arg(&version.main_class) - .arg("--username") + .arg(&version.main_class); + + // ===== ARGUMENTY GRY ===== + cmd.arg("--username") .arg(&config.username) .arg("--version") .arg(&version.id) .arg("--gameDir") - .arg(paths::minecraft_root(config)) + .arg(paths::game_dir(config)) .arg("--assetsDir") - .arg(paths::minecraft_root(config).join("assets")) + .arg(paths::assets_dir(config)) .arg("--assetIndex") - .arg(&version.id) + .arg(&asset_index_id) .arg("--uuid") .arg(&config.uuid) .arg("--userProperties") @@ -61,9 +94,9 @@ pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { .arg("--accessToken") .arg("0") .arg("--userType") - .arg("legacy") - .args(&config.jvm_args) - .status()?; + .arg("legacy"); // legacy dla starych kont, można później zmienić + + let status = cmd.status()?; if !status.success() { return Err(McError::Process("Minecraft exited with error".into())); @@ -71,3 +104,25 @@ pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { Ok(()) } + +/// Sprawdza reguły bibliotek tak jak robi Mojang +fn library_allowed(lib: &Library) -> bool { + let rules = match &lib.rules { + | Some(r) => r, + | None => return true, + }; + + let mut allowed = false; + + for rule in rules { + let os_match = match &rule.os { + | Some(os) => os.name == "linux", + | None => true, + }; + if os_match { + allowed = rule.action == "allow"; + } + } + + allowed +} diff --git a/src/minecraft/manifests.rs b/src/minecraft/manifests.rs index 3cc59af..8bdec26 100644 --- a/src/minecraft/manifests.rs +++ b/src/minecraft/manifests.rs @@ -1,4 +1,7 @@ #![allow(dead_code)] + +use std::collections::HashMap; + use reqwest; use serde::Deserialize; @@ -13,6 +16,17 @@ pub struct Version { pub downloads: Downloads, pub libraries: Vec, + + #[serde(rename = "assetIndex")] + pub asset_index: Option, +} + +#[derive(Debug, Deserialize)] +pub struct AssetIndex { + pub id: String, + pub sha1: String, + pub size: u64, + pub url: String, } #[derive(Debug, Deserialize)] @@ -29,12 +43,22 @@ pub struct DownloadInfo { #[derive(Debug, Deserialize)] pub struct Library { + pub name: Option, pub downloads: LibraryDownloads, + + #[serde(default)] + pub natives: Option>, + + #[serde(default)] + pub rules: Option>, } #[derive(Debug, Deserialize)] pub struct LibraryDownloads { pub artifact: Option, + + #[serde(default)] + pub classifiers: Option>, } #[derive(Debug, Deserialize)] @@ -45,7 +69,20 @@ pub struct LibraryArtifact { pub size: u64, } -pub async fn load_version(cfg: &crate::config::Config) -> Result { +#[derive(Debug, Deserialize)] +pub struct Rule { + pub action: String, + pub os: Option, +} + +#[derive(Debug, Deserialize)] +pub struct OsRule { + pub name: String, +} + +pub async fn load_version( + cfg: &crate::config::Config, +) -> Result { let manifest_text = reqwest::get(VERSION_MANIFEST_URL) .await? .text() @@ -67,7 +104,9 @@ pub async fn load_version(cfg: &crate::config::Config) -> Result