diff options
| author | Filip Wandzio <contact@philw.dev> | 2026-01-22 23:14:08 +0100 |
|---|---|---|
| committer | Filip Wandzio <contact@philw.dev> | 2026-01-22 23:14:08 +0100 |
| commit | 72ddd7b7704f2087a52c9c0552446682918c513b (patch) | |
| tree | e5134f215ea82c1fc8eda17b34e426a7b1dfafc6 /src/minecraft | |
| download | dml-72ddd7b7704f2087a52c9c0552446682918c513b.tar.gz dml-72ddd7b7704f2087a52c9c0552446682918c513b.zip | |
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 <contact@philw.dev>
Diffstat (limited to 'src/minecraft')
| -rw-r--r-- | src/minecraft/downloads.rs | 66 | ||||
| -rw-r--r-- | src/minecraft/extraction.rs | 10 | ||||
| -rw-r--r-- | src/minecraft/launcher.rs | 73 | ||||
| -rw-r--r-- | src/minecraft/manifests.rs | 80 | ||||
| -rw-r--r-- | src/minecraft/mod.rs | 4 |
5 files changed, 233 insertions, 0 deletions
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 @@ | |||
| 1 | use log::{debug, info}; | ||
| 2 | use tokio::{fs, io::AsyncWriteExt}; | ||
| 3 | |||
| 4 | use crate::{ | ||
| 5 | config::Config, | ||
| 6 | errors::McError, | ||
| 7 | minecraft::manifests::{Library, Version}, | ||
| 8 | platform::paths, | ||
| 9 | }; | ||
| 10 | |||
| 11 | /// Download everything required to launch: | ||
| 12 | /// - client jar | ||
| 13 | /// - libraries | ||
| 14 | pub async fn download_all(config: &Config, version: &Version) -> Result<(), McError> { | ||
| 15 | download_client(config, version).await?; | ||
| 16 | download_libraries(config, &version.libraries).await?; | ||
| 17 | Ok(()) | ||
| 18 | } | ||
| 19 | |||
| 20 | async fn download_client(config: &Config, version: &Version) -> Result<(), McError> { | ||
| 21 | let jar_path = paths::client_jar(config, &version.id)?; | ||
| 22 | |||
| 23 | if jar_path.exists() { | ||
| 24 | debug!("Client jar already exists"); | ||
| 25 | return Ok(()); | ||
| 26 | } | ||
| 27 | |||
| 28 | info!("Downloading client {}", version.id); | ||
| 29 | |||
| 30 | download_file(&version.downloads.client.url, &jar_path).await | ||
| 31 | } | ||
| 32 | |||
| 33 | async fn download_libraries(config: &Config, libraries: &[Library]) -> Result<(), McError> { | ||
| 34 | for lib in libraries { | ||
| 35 | let Some(artifact) = &lib.downloads.artifact else { | ||
| 36 | continue; | ||
| 37 | }; | ||
| 38 | |||
| 39 | let lib_path = paths::library_file(config, &artifact.path)?; | ||
| 40 | |||
| 41 | if lib_path.exists() { | ||
| 42 | continue; | ||
| 43 | } | ||
| 44 | |||
| 45 | info!("Downloading library {}", artifact.path); | ||
| 46 | download_file(&artifact.url, &lib_path).await?; | ||
| 47 | } | ||
| 48 | |||
| 49 | Ok(()) | ||
| 50 | } | ||
| 51 | |||
| 52 | /* ---------------- helper ---------------- */ | ||
| 53 | |||
| 54 | async fn download_file(url: &str, path: &std::path::Path) -> Result<(), McError> { | ||
| 55 | if let Some(parent) = path.parent() { | ||
| 56 | fs::create_dir_all(parent).await?; | ||
| 57 | } | ||
| 58 | |||
| 59 | let response = reqwest::get(url).await?; | ||
| 60 | let bytes = response.bytes().await?; | ||
| 61 | |||
| 62 | let mut file = fs::File::create(path).await?; | ||
| 63 | file.write_all(&bytes).await?; | ||
| 64 | |||
| 65 | Ok(()) | ||
| 66 | } | ||
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 @@ | |||
| 1 | use log::info; | ||
| 2 | |||
| 3 | use crate::errors::McError; | ||
| 4 | pub fn extract_natives( | ||
| 5 | _cfg: &crate::config::Config, | ||
| 6 | version: &crate::minecraft::manifests::Version, | ||
| 7 | ) -> Result<(), McError> { | ||
| 8 | info!("Extracting natives for {}", version.id); | ||
| 9 | Ok(()) | ||
| 10 | } | ||
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 @@ | |||
| 1 | use std::process::Command; | ||
| 2 | |||
| 3 | use log::{debug, info}; | ||
| 4 | |||
| 5 | use crate::{config::Config, errors::McError, minecraft::manifests::Version, platform::paths}; | ||
| 6 | |||
| 7 | /// Build the full classpath | ||
| 8 | fn build_classpath(config: &Config, version: &Version) -> Result<String, McError> { | ||
| 9 | let sep = if cfg!(windows) { ";" } else { ":" }; | ||
| 10 | let mut entries = Vec::new(); | ||
| 11 | |||
| 12 | for lib in &version.libraries { | ||
| 13 | if let Some(artifact) = &lib.downloads.artifact { | ||
| 14 | let path = paths::library_file(config, &artifact.path)?; | ||
| 15 | entries.push(path.to_string_lossy().to_string()); | ||
| 16 | } | ||
| 17 | } | ||
| 18 | |||
| 19 | let client_jar = paths::client_jar(config, &version.id)?; | ||
| 20 | entries.push(client_jar.to_string_lossy().to_string()); | ||
| 21 | Ok(entries.join(sep)) | ||
| 22 | } | ||
| 23 | |||
| 24 | /// Launch Minecraft | ||
| 25 | pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { | ||
| 26 | let java = &config.java_path; | ||
| 27 | let classpath = build_classpath(config, version)?; | ||
| 28 | let natives_dir = paths::natives_dir(config, &version.id); | ||
| 29 | |||
| 30 | if !natives_dir.exists() { | ||
| 31 | return Err(McError::Runtime(format!( | ||
| 32 | "Natives folder does not exist: {}", | ||
| 33 | natives_dir.display() | ||
| 34 | ))); | ||
| 35 | } | ||
| 36 | |||
| 37 | info!("Launching Minecraft {}", version.id); | ||
| 38 | debug!("Classpath: {}", classpath); | ||
| 39 | debug!("Natives: {}", natives_dir.display()); | ||
| 40 | |||
| 41 | let status = Command::new(java) | ||
| 42 | .arg(format!("-Xmx{}M", config.max_memory_mb)) | ||
| 43 | .arg(format!("-Djava.library.path={}", natives_dir.display())) | ||
| 44 | .arg("-cp") | ||
| 45 | .arg(classpath) | ||
| 46 | .arg(&version.main_class) | ||
| 47 | .arg("--username") | ||
| 48 | .arg(&config.username) | ||
| 49 | .arg("--version") | ||
| 50 | .arg(&version.id) | ||
| 51 | .arg("--gameDir") | ||
| 52 | .arg(paths::minecraft_root(config)) | ||
| 53 | .arg("--assetsDir") | ||
| 54 | .arg(paths::minecraft_root(config).join("assets")) | ||
| 55 | .arg("--assetIndex") | ||
| 56 | .arg(&version.id) | ||
| 57 | .arg("--uuid") | ||
| 58 | .arg(&config.uuid) | ||
| 59 | .arg("--userProperties") | ||
| 60 | .arg("{}") | ||
| 61 | .arg("--accessToken") | ||
| 62 | .arg("0") | ||
| 63 | .arg("--userType") | ||
| 64 | .arg("legacy") | ||
| 65 | .args(&config.jvm_args) | ||
| 66 | .status()?; | ||
| 67 | |||
| 68 | if !status.success() { | ||
| 69 | return Err(McError::Process("Minecraft exited with error".into())); | ||
| 70 | } | ||
| 71 | |||
| 72 | Ok(()) | ||
| 73 | } | ||
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 @@ | |||
| 1 | #![allow(dead_code)] | ||
| 2 | use reqwest; | ||
| 3 | use serde::Deserialize; | ||
| 4 | |||
| 5 | use crate::{constants::VERSION_MANIFEST_URL, errors::McError}; | ||
| 6 | |||
| 7 | #[derive(Debug, Deserialize)] | ||
| 8 | pub struct Version { | ||
| 9 | pub id: String, | ||
| 10 | |||
| 11 | #[serde(rename = "mainClass")] | ||
| 12 | pub main_class: String, | ||
| 13 | |||
| 14 | pub downloads: Downloads, | ||
| 15 | pub libraries: Vec<Library>, | ||
| 16 | } | ||
| 17 | |||
| 18 | #[derive(Debug, Deserialize)] | ||
| 19 | pub struct Downloads { | ||
| 20 | pub client: DownloadInfo, | ||
| 21 | } | ||
| 22 | |||
| 23 | #[derive(Debug, Deserialize)] | ||
| 24 | pub struct DownloadInfo { | ||
| 25 | pub url: String, | ||
| 26 | pub sha1: String, | ||
| 27 | pub size: u64, | ||
| 28 | } | ||
| 29 | |||
| 30 | #[derive(Debug, Deserialize)] | ||
| 31 | pub struct Library { | ||
| 32 | pub downloads: LibraryDownloads, | ||
| 33 | } | ||
| 34 | |||
| 35 | #[derive(Debug, Deserialize)] | ||
| 36 | pub struct LibraryDownloads { | ||
| 37 | pub artifact: Option<LibraryArtifact>, | ||
| 38 | } | ||
| 39 | |||
| 40 | #[derive(Debug, Deserialize)] | ||
| 41 | pub struct LibraryArtifact { | ||
| 42 | pub path: String, | ||
| 43 | pub url: String, | ||
| 44 | pub sha1: String, | ||
| 45 | pub size: u64, | ||
| 46 | } | ||
| 47 | |||
| 48 | pub async fn load_version(cfg: &crate::config::Config) -> Result<Version, McError> { | ||
| 49 | let manifest_text = reqwest::get(VERSION_MANIFEST_URL) | ||
| 50 | .await? | ||
| 51 | .text() | ||
| 52 | .await?; | ||
| 53 | let root: serde_json::Value = serde_json::from_str(&manifest_text)?; | ||
| 54 | let version_id = if cfg.version == "latest" { | ||
| 55 | root["latest"]["release"] | ||
| 56 | .as_str() | ||
| 57 | .ok_or_else(|| McError::Config("missing latest.release".into()))? | ||
| 58 | .to_string() | ||
| 59 | } else { | ||
| 60 | cfg.version.clone() | ||
| 61 | }; | ||
| 62 | |||
| 63 | let versions = root["versions"] | ||
| 64 | .as_array() | ||
| 65 | .ok_or_else(|| McError::Config("missing versions array".into()))?; | ||
| 66 | |||
| 67 | let version_entry = versions | ||
| 68 | .iter() | ||
| 69 | .find(|v| v["id"].as_str() == Some(&version_id)) | ||
| 70 | .ok_or_else(|| McError::Config(format!("version '{}' not found", version_id)))?; | ||
| 71 | |||
| 72 | let url = version_entry["url"] | ||
| 73 | .as_str() | ||
| 74 | .ok_or_else(|| McError::Config("missing version url".into()))?; | ||
| 75 | |||
| 76 | let version_text = reqwest::get(url).await?.text().await?; | ||
| 77 | let version: Version = serde_json::from_str(&version_text)?; | ||
| 78 | |||
| 79 | Ok(version) | ||
| 80 | } | ||
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 @@ | |||
| 1 | pub mod downloads; | ||
| 2 | pub mod extraction; | ||
| 3 | pub mod launcher; | ||
| 4 | pub mod manifests; | ||
