aboutsummaryrefslogtreecommitdiffstats
path: root/src/minecraft
diff options
context:
space:
mode:
authorFilip Wandzio <contact@philw.dev>2026-01-24 08:29:14 +0100
committerFilip Wandzio <contact@philw.dev>2026-01-24 08:29:14 +0100
commita393e0a2f2c3678a3ea869dc1417fa269f2b1040 (patch)
tree606df6a9284b5bd2dbf84fa5e3d363b8e6a01322 /src/minecraft
parent72ddd7b7704f2087a52c9c0552446682918c513b (diff)
downloaddml-a393e0a2f2c3678a3ea869dc1417fa269f2b1040.tar.gz
dml-a393e0a2f2c3678a3ea869dc1417fa269f2b1040.zip
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
Diffstat (limited to '')
-rw-r--r--src/minecraft/downloads.rs172
-rw-r--r--src/minecraft/extraction.rs116
-rw-r--r--src/minecraft/launcher.rs91
-rw-r--r--src/minecraft/manifests.rs43
-rw-r--r--src/minecraft/mod.rs15
5 files changed, 391 insertions, 46 deletions
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 @@
1use log::{debug, info}; 1use log::{debug, info};
2use tokio::{fs, io::AsyncWriteExt}; 2use reqwest::get;
3use serde::Deserialize;
4use tokio::{
5 fs::{self, File, create_dir_all},
6 io::AsyncWriteExt,
7};
3 8
4use crate::{ 9use crate::{
5 config::Config, 10 config::Config,
@@ -8,58 +13,181 @@ use crate::{
8 platform::paths, 13 platform::paths,
9}; 14};
10 15
11/// Download everything required to launch: 16#[derive(Debug, Deserialize)]
17struct AssetObject {
18 hash: String,
19 size: u64,
20}
21
22#[derive(Debug, Deserialize)]
23struct AssetIndexManifest {
24 objects: std::collections::HashMap<String, AssetObject>,
25}
26
27/// Pobiera wszystko potrzebne do uruchomienia Minecraft:
12/// - client jar 28/// - client jar
13/// - libraries 29/// - biblioteki (artifact + natives)
14pub async fn download_all(config: &Config, version: &Version) -> Result<(), McError> { 30/// - assets (w tym textures, sounds)
31pub async fn download_all(
32 config: &Config,
33 version: &Version,
34) -> Result<(), McError> {
15 download_client(config, version).await?; 35 download_client(config, version).await?;
16 download_libraries(config, &version.libraries).await?; 36 download_libraries(config, &version.libraries).await?;
37 download_assets(config, version).await?;
17 Ok(()) 38 Ok(())
18} 39}
19 40
20async fn download_client(config: &Config, version: &Version) -> Result<(), McError> { 41async fn download_client(
42 config: &Config,
43 version: &Version,
44) -> Result<(), McError> {
21 let jar_path = paths::client_jar(config, &version.id)?; 45 let jar_path = paths::client_jar(config, &version.id)?;
22 46
23 if jar_path.exists() { 47 if jar_path.exists() {
24 debug!("Client jar already exists"); 48 debug!("Client jar already exists: {}", jar_path.display());
25 return Ok(()); 49 return Ok(());
26 } 50 }
27 51
28 info!("Downloading client {}", version.id); 52 info!("Downloading client {}", version.id);
29
30 download_file(&version.downloads.client.url, &jar_path).await 53 download_file(&version.downloads.client.url, &jar_path).await
31} 54}
32 55
33async fn download_libraries(config: &Config, libraries: &[Library]) -> Result<(), McError> { 56async fn download_libraries(
34 for lib in libraries { 57 config: &Config,
35 let Some(artifact) = &lib.downloads.artifact else { 58 libraries: &[Library],
36 continue; 59) -> Result<(), McError> {
37 }; 60 for library in libraries {
61 // ===== CLASSPATH LIBRARIES =====
62 if let Some(artifact) = &library.downloads.artifact {
63 let library_path = paths::library_file(config, &artifact.path)?;
64
65 if !library_path.exists() {
66 info!("Downloading library {}", artifact.path);
67 download_file(&artifact.url, &library_path).await?;
68 }
69 }
70
71 // ===== NATIVES =====
72 if let Some(classifiers) = &library.downloads.classifiers {
73 for (_, native) in classifiers {
74 let native_path = paths::library_file(config, &native.path)?;
75
76 if native_path.exists() {
77 continue;
78 }
79
80 info!("Downloading native library {}", native.path);
81 download_file(&native.url, &native_path).await?;
82 }
83 }
84 }
85
86 Ok(())
87}
38 88
39 let lib_path = paths::library_file(config, &artifact.path)?; 89async fn download_asset_index(
90 config: &Config,
91 version: &Version,
92) -> Result<AssetIndexManifest, McError> {
93 let assets_dir = paths::assets_dir(config);
94 create_dir_all(assets_dir.join("indexes")).await?;
95
96 let asset_index = version.asset_index.as_ref().ok_or_else(|| {
97 McError::Config("Missing asset_index in version.json".into())
98 })?;
99
100 // Nie pozwalamy na legacy dla nowoczesnych wersji
101 if asset_index.id == "legacy" {
102 return Err(McError::Config(
103 "Legacy assetIndex detected – pobierz właściwy version.json".into(),
104 ));
105 }
106
107 let index_path = assets_dir
108 .join("indexes")
109 .join(format!("{}.json", asset_index.id));
110
111 // Jeśli indeks istnieje lokalnie
112 if index_path.exists() {
113 let index_data = fs::read_to_string(&index_path).await?;
114 let manifest: AssetIndexManifest = serde_json::from_str(&index_data)?;
115 return Ok(manifest);
116 }
117
118 // Pobierz indeks z sieci
119 info!("Downloading asset index {}", asset_index.id);
120 let response = get(&asset_index.url).await?;
121 let manifest_text = response.text().await?;
122
123 fs::write(&index_path, &manifest_text).await?;
124
125 let manifest: AssetIndexManifest = serde_json::from_str(&manifest_text)?;
126 Ok(manifest)
127}
40 128
41 if lib_path.exists() { 129async fn download_assets(
130 config: &Config,
131 version: &Version,
132) -> Result<(), McError> {
133 let assets_dir = paths::assets_dir(config);
134
135 // Katalogi MUSZĄ istnieć
136 create_dir_all(assets_dir.join("objects")).await?;
137 create_dir_all(assets_dir.join("indexes")).await?;
138
139 let manifest = download_asset_index(config, version).await?;
140
141 // Pobieramy wszystkie obiekty
142 for (logical_path, asset) in &manifest.objects {
143 let subdir = &asset.hash[0..2];
144 let file_path = assets_dir
145 .join("objects")
146 .join(subdir)
147 .join(&asset.hash);
148
149 if file_path.exists() {
42 continue; 150 continue;
43 } 151 }
44 152
45 info!("Downloading library {}", artifact.path); 153 let url = format!(
46 download_file(&artifact.url, &lib_path).await?; 154 "https://resources.download.minecraft.net/{}/{}",
155 subdir, asset.hash
156 );
157 info!("Downloading asset {} -> {}", logical_path, file_path.display());
158 download_file(&url, &file_path).await?;
159 }
160
161 // Pobierz sounds.json jeśli istnieje
162 if let Some(asset) = manifest.objects.get("sounds.json") {
163 let file_path = assets_dir.join("indexes").join("sounds.json");
164 if !file_path.exists() {
165 let subdir = &asset.hash[0..2];
166 let url = format!(
167 "https://resources.download.minecraft.net/{}/{}",
168 subdir, asset.hash
169 );
170 info!("Downloading sounds.json");
171 download_file(&url, &file_path).await?;
172 }
47 } 173 }
48 174
49 Ok(()) 175 Ok(())
50} 176}
51 177
52/* ---------------- helper ---------------- */ 178/// Helper do pobierania plików
53 179async fn download_file(
54async fn download_file(url: &str, path: &std::path::Path) -> Result<(), McError> { 180 url: &str,
181 path: &std::path::Path,
182) -> Result<(), McError> {
55 if let Some(parent) = path.parent() { 183 if let Some(parent) = path.parent() {
56 fs::create_dir_all(parent).await?; 184 create_dir_all(parent).await?;
57 } 185 }
58 186
59 let response = reqwest::get(url).await?; 187 let response = get(url).await?;
60 let bytes = response.bytes().await?; 188 let bytes = response.bytes().await?;
61 189
62 let mut file = fs::File::create(path).await?; 190 let mut file = File::create(path).await?;
63 file.write_all(&bytes).await?; 191 file.write_all(&bytes).await?;
64 192
65 Ok(()) 193 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 @@
1use std::{fs, io, path::Path};
2
1use log::info; 3use log::info;
4use zip::ZipArchive;
5
6use crate::{
7 errors::McError,
8 minecraft::manifests::{Library, Version},
9};
2 10
3use crate::errors::McError;
4pub fn extract_natives( 11pub fn extract_natives(
5 _cfg: &crate::config::Config, 12 cfg: &crate::config::Config,
6 version: &crate::minecraft::manifests::Version, 13 version: &Version,
7) -> Result<(), McError> { 14) -> Result<(), McError> {
8 info!("Extracting natives for {}", version.id); 15 let natives_dir = cfg
16 .data_dir
17 .join("minecraft")
18 .join("versions")
19 .join(&version.id)
20 .join("natives");
21
22 info!("Extracting natives for {} into {:?}", version.id, natives_dir);
23
24 if natives_dir.exists() {
25 fs::remove_dir_all(&natives_dir)?;
26 }
27 fs::create_dir_all(&natives_dir)?;
28
29 for lib in &version.libraries {
30 if !library_allowed(lib) {
31 continue;
32 }
33
34 let natives = match &lib.natives {
35 | Some(n) => n,
36 | None => continue,
37 };
38
39 let classifier = match natives.get("linux") {
40 | Some(c) => c,
41 | None => continue,
42 };
43
44 let classifiers = match &lib.downloads.classifiers {
45 | Some(c) => c,
46 | None => continue,
47 };
48
49 let artifact = match classifiers.get(classifier) {
50 | Some(a) => a,
51 | None => continue,
52 };
53
54 let jar_path = cfg
55 .data_dir
56 .join("minecraft")
57 .join("libraries")
58 .join(&artifact.path);
59
60 info!("Extracting natives from {:?}", jar_path);
61
62 extract_zip(&jar_path, &natives_dir)?;
63 }
64
65 Ok(())
66}
67
68fn library_allowed(lib: &Library) -> bool {
69 let rules = match &lib.rules {
70 | Some(r) => r,
71 | None => return true,
72 };
73
74 let mut allowed = false;
75
76 for rule in rules {
77 let os_match = match &rule.os {
78 | Some(os) => os.name == "linux",
79 | None => true,
80 };
81
82 if os_match {
83 allowed = rule.action == "allow";
84 }
85 }
86
87 allowed
88}
89
90fn extract_zip(jar_path: &Path, out_dir: &Path) -> Result<(), McError> {
91 let file = fs::File::open(jar_path)?;
92 let mut zip = ZipArchive::new(file)?;
93
94 for i in 0..zip.len() {
95 let mut entry = zip.by_index(i)?;
96 let name = entry.name();
97
98 if name.starts_with("META-INF/") {
99 continue;
100 }
101
102 let out_path = out_dir.join(name);
103
104 if entry.is_dir() {
105 fs::create_dir_all(&out_path)?;
106 continue;
107 }
108
109 if let Some(parent) = out_path.parent() {
110 fs::create_dir_all(parent)?;
111 }
112
113 let mut out_file = fs::File::create(&out_path)?;
114 io::copy(&mut entry, &mut out_file)?;
115 }
116
9 Ok(()) 117 Ok(())
10} 118}
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;
2 2
3use log::{debug, info}; 3use log::{debug, info};
4 4
5use crate::{config::Config, errors::McError, minecraft::manifests::Version, platform::paths}; 5use crate::{
6 config::Config,
7 errors::McError,
8 minecraft::manifests::{Library, Version},
9 platform::paths,
10};
6 11
7/// Build the full classpath 12/// Buduje classpath dla danej wersji Minecrafta
8fn build_classpath(config: &Config, version: &Version) -> Result<String, McError> { 13fn build_classpath(
14 config: &Config,
15 version: &Version,
16) -> Result<String, McError> {
9 let sep = if cfg!(windows) { ";" } else { ":" }; 17 let sep = if cfg!(windows) { ";" } else { ":" };
10 let mut entries = Vec::new(); 18 let mut entries = Vec::new();
11 19
12 for lib in &version.libraries { 20 for library in &version.libraries {
13 if let Some(artifact) = &lib.downloads.artifact { 21 if !library_allowed(library) {
22 continue;
23 }
24 if let Some(artifact) = &library.downloads.artifact {
14 let path = paths::library_file(config, &artifact.path)?; 25 let path = paths::library_file(config, &artifact.path)?;
15 entries.push(path.to_string_lossy().to_string()); 26 entries.push(path.to_string_lossy().to_string());
16 } 27 }
17 } 28 }
18 29
30 // client.jar zawsze na końcu classpath
19 let client_jar = paths::client_jar(config, &version.id)?; 31 let client_jar = paths::client_jar(config, &version.id)?;
20 entries.push(client_jar.to_string_lossy().to_string()); 32 entries.push(client_jar.to_string_lossy().to_string());
33
21 Ok(entries.join(sep)) 34 Ok(entries.join(sep))
22} 35}
23 36
24/// Launch Minecraft 37/// Uruchamia Minecraft
25pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { 38pub fn launch(config: &Config, version: &Version) -> Result<(), McError> {
26 let java = &config.java_path; 39 let java = &config.java_path;
27 let classpath = build_classpath(config, version)?; 40 let classpath = build_classpath(config, version)?;
@@ -34,26 +47,46 @@ pub fn launch(config: &Config, version: &Version) -> Result<(), McError> {
34 ))); 47 )));
35 } 48 }
36 49
50 let asset_index_id = version
51 .asset_index
52 .as_ref()
53 .ok_or_else(|| {
54 McError::Runtime("Missing assetIndex in version.json".into())
55 })?
56 .id
57 .clone();
58
37 info!("Launching Minecraft {}", version.id); 59 info!("Launching Minecraft {}", version.id);
38 debug!("Classpath: {}", classpath); 60 debug!("Classpath: {}", classpath);
39 debug!("Natives: {}", natives_dir.display()); 61 debug!("Natives: {}", natives_dir.display());
62 debug!("Asset index: {}", asset_index_id);
63
64 let mut cmd = Command::new(java);
65
66 // ===== JVM ARGUMENTS (muszą być na początku) =====
67 cmd.arg(format!("-Xmx{}M", config.max_memory_mb))
68 .arg(format!("-Djava.library.path={}", natives_dir.display()));
40 69
41 let status = Command::new(java) 70 for arg in &config.jvm_args {
42 .arg(format!("-Xmx{}M", config.max_memory_mb)) 71 cmd.arg(arg);
43 .arg(format!("-Djava.library.path={}", natives_dir.display())) 72 }
44 .arg("-cp") 73
74 // ===== CLASSPATH + MAIN CLASS =====
75 cmd.arg("-cp")
45 .arg(classpath) 76 .arg(classpath)
46 .arg(&version.main_class) 77 .arg(&version.main_class);
47 .arg("--username") 78
79 // ===== ARGUMENTY GRY =====
80 cmd.arg("--username")
48 .arg(&config.username) 81 .arg(&config.username)
49 .arg("--version") 82 .arg("--version")
50 .arg(&version.id) 83 .arg(&version.id)
51 .arg("--gameDir") 84 .arg("--gameDir")
52 .arg(paths::minecraft_root(config)) 85 .arg(paths::game_dir(config))
53 .arg("--assetsDir") 86 .arg("--assetsDir")
54 .arg(paths::minecraft_root(config).join("assets")) 87 .arg(paths::assets_dir(config))
55 .arg("--assetIndex") 88 .arg("--assetIndex")
56 .arg(&version.id) 89 .arg(&asset_index_id)
57 .arg("--uuid") 90 .arg("--uuid")
58 .arg(&config.uuid) 91 .arg(&config.uuid)
59 .arg("--userProperties") 92 .arg("--userProperties")
@@ -61,9 +94,9 @@ pub fn launch(config: &Config, version: &Version) -> Result<(), McError> {
61 .arg("--accessToken") 94 .arg("--accessToken")
62 .arg("0") 95 .arg("0")
63 .arg("--userType") 96 .arg("--userType")
64 .arg("legacy") 97 .arg("legacy"); // legacy dla starych kont, można później zmienić
65 .args(&config.jvm_args) 98
66 .status()?; 99 let status = cmd.status()?;
67 100
68 if !status.success() { 101 if !status.success() {
69 return Err(McError::Process("Minecraft exited with error".into())); 102 return Err(McError::Process("Minecraft exited with error".into()));
@@ -71,3 +104,25 @@ pub fn launch(config: &Config, version: &Version) -> Result<(), McError> {
71 104
72 Ok(()) 105 Ok(())
73} 106}
107
108/// Sprawdza reguły bibliotek tak jak robi Mojang
109fn library_allowed(lib: &Library) -> bool {
110 let rules = match &lib.rules {
111 | Some(r) => r,
112 | None => return true,
113 };
114
115 let mut allowed = false;
116
117 for rule in rules {
118 let os_match = match &rule.os {
119 | Some(os) => os.name == "linux",
120 | None => true,
121 };
122 if os_match {
123 allowed = rule.action == "allow";
124 }
125 }
126
127 allowed
128}
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 @@
1#![allow(dead_code)] 1#![allow(dead_code)]
2
3use std::collections::HashMap;
4
2use reqwest; 5use reqwest;
3use serde::Deserialize; 6use serde::Deserialize;
4 7
@@ -13,6 +16,17 @@ pub struct Version {
13 16
14 pub downloads: Downloads, 17 pub downloads: Downloads,
15 pub libraries: Vec<Library>, 18 pub libraries: Vec<Library>,
19
20 #[serde(rename = "assetIndex")]
21 pub asset_index: Option<AssetIndex>,
22}
23
24#[derive(Debug, Deserialize)]
25pub struct AssetIndex {
26 pub id: String,
27 pub sha1: String,
28 pub size: u64,
29 pub url: String,
16} 30}
17 31
18#[derive(Debug, Deserialize)] 32#[derive(Debug, Deserialize)]
@@ -29,12 +43,22 @@ pub struct DownloadInfo {
29 43
30#[derive(Debug, Deserialize)] 44#[derive(Debug, Deserialize)]
31pub struct Library { 45pub struct Library {
46 pub name: Option<String>,
32 pub downloads: LibraryDownloads, 47 pub downloads: LibraryDownloads,
48
49 #[serde(default)]
50 pub natives: Option<HashMap<String, String>>,
51
52 #[serde(default)]
53 pub rules: Option<Vec<Rule>>,
33} 54}
34 55
35#[derive(Debug, Deserialize)] 56#[derive(Debug, Deserialize)]
36pub struct LibraryDownloads { 57pub struct LibraryDownloads {
37 pub artifact: Option<LibraryArtifact>, 58 pub artifact: Option<LibraryArtifact>,
59
60 #[serde(default)]
61 pub classifiers: Option<HashMap<String, LibraryArtifact>>,
38} 62}
39 63
40#[derive(Debug, Deserialize)] 64#[derive(Debug, Deserialize)]
@@ -45,7 +69,20 @@ pub struct LibraryArtifact {
45 pub size: u64, 69 pub size: u64,
46} 70}
47 71
48pub async fn load_version(cfg: &crate::config::Config) -> Result<Version, McError> { 72#[derive(Debug, Deserialize)]
73pub struct Rule {
74 pub action: String,
75 pub os: Option<OsRule>,
76}
77
78#[derive(Debug, Deserialize)]
79pub struct OsRule {
80 pub name: String,
81}
82
83pub async fn load_version(
84 cfg: &crate::config::Config,
85) -> Result<Version, McError> {
49 let manifest_text = reqwest::get(VERSION_MANIFEST_URL) 86 let manifest_text = reqwest::get(VERSION_MANIFEST_URL)
50 .await? 87 .await?
51 .text() 88 .text()
@@ -67,7 +104,9 @@ pub async fn load_version(cfg: &crate::config::Config) -> Result<Version, McErro
67 let version_entry = versions 104 let version_entry = versions
68 .iter() 105 .iter()
69 .find(|v| v["id"].as_str() == Some(&version_id)) 106 .find(|v| v["id"].as_str() == Some(&version_id))
70 .ok_or_else(|| McError::Config(format!("version '{}' not found", version_id)))?; 107 .ok_or_else(|| {
108 McError::Config(format!("version '{}' not found", version_id))
109 })?;
71 110
72 let url = version_entry["url"] 111 let url = version_entry["url"]
73 .as_str() 112 .as_str()
diff --git a/src/minecraft/mod.rs b/src/minecraft/mod.rs
index f1ce1f0..6dea71d 100644
--- a/src/minecraft/mod.rs
+++ b/src/minecraft/mod.rs
@@ -1,3 +1,18 @@
1//! Minecraft module for the DML launcher.
2//!
3//! This module provides the core functionality for interacting with
4//! Minecraft game files, including downloading assets, extracting
5//! game JARs, managing libraries, and launching the game.
6//!
7//! # Submodules
8//! - `downloads`: Async functions for downloading game files and assets.
9//! - `extraction`: Functions for extracting ZIP/JAR files to the correct
10//! locations.
11//! - `launcher`: Functions to launch the Minecraft process with the proper
12//! arguments.
13//! - `manifests`: Structures representing Minecraft version manifests,
14//! libraries, and rules.
15
1pub mod downloads; 16pub mod downloads;
2pub mod extraction; 17pub mod extraction;
3pub mod launcher; 18pub mod launcher;