From 72ddd7b7704f2087a52c9c0552446682918c513b Mon Sep 17 00:00:00 2001 From: Filip Wandzio Date: Thu, 22 Jan 2026 23:14:08 +0100 Subject: Implement basic game files download logic Implement core clap arguments Respect XDG_BASE_DIR Currently library extraction is broken because it assumes every instace has it's own library folder. This should be refactored so instances share libraries Signed-off-by: Filip Wandzio --- src/minecraft/downloads.rs | 66 +++++++++++++++++++++++++++++++++++++ src/minecraft/extraction.rs | 10 ++++++ src/minecraft/launcher.rs | 73 +++++++++++++++++++++++++++++++++++++++++ src/minecraft/manifests.rs | 80 +++++++++++++++++++++++++++++++++++++++++++++ src/minecraft/mod.rs | 4 +++ 5 files changed, 233 insertions(+) create mode 100644 src/minecraft/downloads.rs create mode 100644 src/minecraft/extraction.rs create mode 100644 src/minecraft/launcher.rs create mode 100644 src/minecraft/manifests.rs create mode 100644 src/minecraft/mod.rs (limited to 'src/minecraft') diff --git a/src/minecraft/downloads.rs b/src/minecraft/downloads.rs new file mode 100644 index 0000000..5be5a05 --- /dev/null +++ b/src/minecraft/downloads.rs @@ -0,0 +1,66 @@ +use log::{debug, info}; +use tokio::{fs, io::AsyncWriteExt}; + +use crate::{ + config::Config, + errors::McError, + minecraft::manifests::{Library, Version}, + platform::paths, +}; + +/// Download everything required to launch: +/// - client jar +/// - libraries +pub async fn download_all(config: &Config, version: &Version) -> Result<(), McError> { + download_client(config, version).await?; + download_libraries(config, &version.libraries).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"); + 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; + }; + + let lib_path = paths::library_file(config, &artifact.path)?; + + if lib_path.exists() { + continue; + } + + info!("Downloading library {}", artifact.path); + download_file(&artifact.url, &lib_path).await?; + } + + Ok(()) +} + +/* ---------------- helper ---------------- */ + +async fn download_file(url: &str, path: &std::path::Path) -> Result<(), McError> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).await?; + } + + let response = reqwest::get(url).await?; + let bytes = response.bytes().await?; + + let mut file = fs::File::create(path).await?; + file.write_all(&bytes).await?; + + Ok(()) +} diff --git a/src/minecraft/extraction.rs b/src/minecraft/extraction.rs new file mode 100644 index 0000000..5175ee0 --- /dev/null +++ b/src/minecraft/extraction.rs @@ -0,0 +1,10 @@ +use log::info; + +use crate::errors::McError; +pub fn extract_natives( + _cfg: &crate::config::Config, + version: &crate::minecraft::manifests::Version, +) -> Result<(), McError> { + info!("Extracting natives for {}", version.id); + Ok(()) +} diff --git a/src/minecraft/launcher.rs b/src/minecraft/launcher.rs new file mode 100644 index 0000000..f7e3ecc --- /dev/null +++ b/src/minecraft/launcher.rs @@ -0,0 +1,73 @@ +use std::process::Command; + +use log::{debug, info}; + +use crate::{config::Config, errors::McError, minecraft::manifests::Version, platform::paths}; + +/// Build the full classpath +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 { + let path = paths::library_file(config, &artifact.path)?; + entries.push(path.to_string_lossy().to_string()); + } + } + + let client_jar = paths::client_jar(config, &version.id)?; + entries.push(client_jar.to_string_lossy().to_string()); + Ok(entries.join(sep)) +} + +/// Launch Minecraft +pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { + let java = &config.java_path; + let classpath = build_classpath(config, version)?; + let natives_dir = paths::natives_dir(config, &version.id); + + if !natives_dir.exists() { + return Err(McError::Runtime(format!( + "Natives folder does not exist: {}", + natives_dir.display() + ))); + } + + info!("Launching Minecraft {}", version.id); + debug!("Classpath: {}", classpath); + debug!("Natives: {}", 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") + .arg(classpath) + .arg(&version.main_class) + .arg("--username") + .arg(&config.username) + .arg("--version") + .arg(&version.id) + .arg("--gameDir") + .arg(paths::minecraft_root(config)) + .arg("--assetsDir") + .arg(paths::minecraft_root(config).join("assets")) + .arg("--assetIndex") + .arg(&version.id) + .arg("--uuid") + .arg(&config.uuid) + .arg("--userProperties") + .arg("{}") + .arg("--accessToken") + .arg("0") + .arg("--userType") + .arg("legacy") + .args(&config.jvm_args) + .status()?; + + if !status.success() { + return Err(McError::Process("Minecraft exited with error".into())); + } + + Ok(()) +} diff --git a/src/minecraft/manifests.rs b/src/minecraft/manifests.rs new file mode 100644 index 0000000..3cc59af --- /dev/null +++ b/src/minecraft/manifests.rs @@ -0,0 +1,80 @@ +#![allow(dead_code)] +use reqwest; +use serde::Deserialize; + +use crate::{constants::VERSION_MANIFEST_URL, errors::McError}; + +#[derive(Debug, Deserialize)] +pub struct Version { + pub id: String, + + #[serde(rename = "mainClass")] + pub main_class: String, + + pub downloads: Downloads, + pub libraries: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct Downloads { + pub client: DownloadInfo, +} + +#[derive(Debug, Deserialize)] +pub struct DownloadInfo { + pub url: String, + pub sha1: String, + pub size: u64, +} + +#[derive(Debug, Deserialize)] +pub struct Library { + pub downloads: LibraryDownloads, +} + +#[derive(Debug, Deserialize)] +pub struct LibraryDownloads { + pub artifact: Option, +} + +#[derive(Debug, Deserialize)] +pub struct LibraryArtifact { + pub path: String, + pub url: String, + pub sha1: String, + pub size: u64, +} + +pub async fn load_version(cfg: &crate::config::Config) -> Result { + let manifest_text = reqwest::get(VERSION_MANIFEST_URL) + .await? + .text() + .await?; + let root: serde_json::Value = serde_json::from_str(&manifest_text)?; + let version_id = if cfg.version == "latest" { + root["latest"]["release"] + .as_str() + .ok_or_else(|| McError::Config("missing latest.release".into()))? + .to_string() + } else { + cfg.version.clone() + }; + + let versions = root["versions"] + .as_array() + .ok_or_else(|| McError::Config("missing versions array".into()))?; + + let version_entry = versions + .iter() + .find(|v| v["id"].as_str() == Some(&version_id)) + .ok_or_else(|| McError::Config(format!("version '{}' not found", version_id)))?; + + let url = version_entry["url"] + .as_str() + .ok_or_else(|| McError::Config("missing version url".into()))?; + + let version_text = reqwest::get(url).await?.text().await?; + let version: Version = serde_json::from_str(&version_text)?; + + Ok(version) +} diff --git a/src/minecraft/mod.rs b/src/minecraft/mod.rs new file mode 100644 index 0000000..f1ce1f0 --- /dev/null +++ b/src/minecraft/mod.rs @@ -0,0 +1,4 @@ +pub mod downloads; +pub mod extraction; +pub mod launcher; +pub mod manifests; -- cgit v1.2.3