aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--clippy.toml8
-rw-r--r--rustfmt.toml2
-rw-r--r--src/config/loader.rs23
-rw-r--r--src/config/mod.rs11
-rw-r--r--src/constants.rs2
-rw-r--r--src/errors.rs23
-rw-r--r--src/main.rs16
-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
-rw-r--r--src/platform/mod.rs10
-rw-r--r--src/platform/paths.rs36
-rw-r--r--src/util/fs.rs4
-rw-r--r--src/util/mod.rs12
-rw-r--r--src/util/sha1.rs3
18 files changed, 488 insertions, 100 deletions
diff --git a/.gitignore b/.gitignore
index 6aedc81..d0f7565 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@ Cargo.lock
6**/*.rs.bk 6**/*.rs.bk
7*.pdb 7*.pdb
8rust-project.json 8rust-project.json
9*.png
diff --git a/clippy.toml b/clippy.toml
index f688e0a..6b15c5a 100644
--- a/clippy.toml
+++ b/clippy.toml
@@ -1,8 +1,8 @@
1stack-size-threshold = 393216 1stack-size-threshold = 393216
2future-size-threshold = 24576 2future-size-threshold = 24576
3array-size-threshold = 4096 3array-size-threshold = 4096
4large-error-threshold = 256 # TODO reduce me ALARA 4large-error-threshold = 256
5too-many-lines-threshold = 100 # TODO reduce me to <= 100 5too-many-lines-threshold = 100
6excessive-nesting-threshold = 3 6excessive-nesting-threshold = 3
7type-complexity-threshold = 250 # reduce me to ~200 7type-complexity-threshold = 250
8cognitive-complexity-threshold = 100 # TODO reduce me ALARA 8cognitive-complexity-threshold = 100
diff --git a/rustfmt.toml b/rustfmt.toml
index 32fb2b2..68c25df 100644
--- a/rustfmt.toml
+++ b/rustfmt.toml
@@ -16,7 +16,7 @@ imports_granularity = "Crate"
16match_arm_blocks = false 16match_arm_blocks = false
17match_arm_leading_pipes = "Always" 17match_arm_leading_pipes = "Always"
18match_block_trailing_comma = true 18match_block_trailing_comma = true
19max_width = 98 19max_width = 80
20newline_style = "Unix" 20newline_style = "Unix"
21normalize_comments = false 21normalize_comments = false
22overflow_delimited_expr = true 22overflow_delimited_expr = true
diff --git a/src/config/loader.rs b/src/config/loader.rs
index 81a4351..d4b142e 100644
--- a/src/config/loader.rs
+++ b/src/config/loader.rs
@@ -1,9 +1,9 @@
1use std::{env, path::PathBuf}; 1use std::{env::var, fs::read_to_string, path::PathBuf};
2 2
3use directories::ProjectDirs;
3use serde::Deserialize; 4use serde::Deserialize;
4 5
5use crate::{constants::*, errors::McError}; 6use crate::{constants::*, errors::McError};
6
7#[allow(dead_code)] 7#[allow(dead_code)]
8#[derive(Debug, Deserialize)] 8#[derive(Debug, Deserialize)]
9pub struct Config { 9pub struct Config {
@@ -18,37 +18,33 @@ pub struct Config {
18 #[serde(default)] 18 #[serde(default)]
19 pub jvm_args: Vec<String>, 19 pub jvm_args: Vec<String>,
20} 20}
21
22impl Config { 21impl Config {
23 pub fn load() -> Result<Self, McError> { 22 pub fn load() -> Result<Self, McError> {
24 let cfg_path = default_config_path()?; 23 let cfg_path = default_config_path()?;
25 let mut cfg: Config = if cfg_path.exists() { 24 let mut cfg: Config = if cfg_path.exists() {
26 let txt = std::fs::read_to_string(&cfg_path)?; 25 let txt = read_to_string(&cfg_path)?;
27 toml::from_str(&txt).map_err(|e| McError::Config(e.to_string()))? 26 toml::from_str(&txt).map_err(|e| McError::Config(e.to_string()))?
28 } else { 27 } else {
29 Self::default() 28 Self::default()
30 }; 29 };
31 30 if let Ok(v) = var("MC_USERNAME") {
32 if let Ok(v) = env::var("MC_USERNAME") {
33 cfg.username = v; 31 cfg.username = v;
34 } 32 }
35 if let Ok(v) = env::var("MC_VERSION") { 33 if let Ok(v) = var("MC_VERSION") {
36 cfg.version = v; 34 cfg.version = v;
37 } 35 }
38 if let Ok(v) = env::var("MC_JAVA_PATH") { 36 if let Ok(v) = var("MC_JAVA_PATH") {
39 cfg.java_path = v; 37 cfg.java_path = v;
40 } 38 }
41 if let Ok(v) = env::var("MC_MAX_MEMORY_MB") { 39 if let Ok(v) = var("MC_MAX_MEMORY_MB") {
42 cfg.max_memory_mb = v.parse().unwrap_or(cfg.max_memory_mb); 40 cfg.max_memory_mb = v.parse().unwrap_or(cfg.max_memory_mb);
43 } 41 }
44
45 Ok(cfg) 42 Ok(cfg)
46 } 43 }
47 44
48 fn default() -> Self { 45 fn default() -> Self {
49 let base = 46 let base =
50 directories::ProjectDirs::from("com", "example", "mccl").expect("platform dirs"); 47 ProjectDirs::from("com", "example", "dml").expect("platform dirs");
51
52 Self { 48 Self {
53 username: "Player".into(), 49 username: "Player".into(),
54 uuid: uuid::Uuid::new_v4().to_string(), 50 uuid: uuid::Uuid::new_v4().to_string(),
@@ -62,9 +58,8 @@ impl Config {
62 } 58 }
63 } 59 }
64} 60}
65
66fn default_config_path() -> Result<PathBuf, McError> { 61fn default_config_path() -> Result<PathBuf, McError> {
67 let base = directories::ProjectDirs::from("com", "example", "mccl") 62 let base = ProjectDirs::from("com", "example", "dml")
68 .ok_or_else(|| McError::Config("cannot determine config dir".into()))?; 63 .ok_or_else(|| McError::Config("cannot determine config dir".into()))?;
69 Ok(base.config_dir().join("config.toml")) 64 Ok(base.config_dir().join("config.toml"))
70} 65}
diff --git a/src/config/mod.rs b/src/config/mod.rs
index c5fc004..066154a 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -1,3 +1,12 @@
1pub mod loader; 1//! Configuration module for the DML launcher.
2//!
3//! This module contains submodules and helpers for loading, parsing, and
4//! managing configuration files and settings. It abstracts the details
5//! of where configuration is stored and how it is represented in memory.
6//!
7//! # Submodules
8//! - `loader`: Functions to load and parse configuration from disk or
9//! environment variables.
2 10
11pub mod loader;
3pub use loader::Config; 12pub use loader::Config;
diff --git a/src/constants.rs b/src/constants.rs
index 52833b2..3234fe6 100644
--- a/src/constants.rs
+++ b/src/constants.rs
@@ -8,6 +8,6 @@ pub const VERSION_MANIFEST_URL: &str =
8pub const DOWNLOAD_RETRIES: usize = 3; 8pub const DOWNLOAD_RETRIES: usize = 3;
9pub const DOWNLOAD_BACKOFF: Duration = Duration::from_millis(400); 9pub const DOWNLOAD_BACKOFF: Duration = Duration::from_millis(400);
10 10
11pub const DEFAULT_MAX_MEMORY_MB: u32 = 2048; 11pub const DEFAULT_MAX_MEMORY_MB: u32 = 4048;
12pub const DEFAULT_JAVA_PATH: &str = "java"; 12pub const DEFAULT_JAVA_PATH: &str = "java";
13pub const DEFAULT_VERSION: &str = "latest"; 13pub const DEFAULT_VERSION: &str = "latest";
diff --git a/src/errors.rs b/src/errors.rs
index c167733..b98ae2d 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -1,20 +1,31 @@
1use std::{fmt, io}; 1use std::{fmt, io};
2 2
3use fmt::{Display, Formatter, Result};
4use zip::result::ZipError;
5
6/// Represents all possible errors that can occur in the DML launcher.
7///
8/// This enum centralizes error handling for the entire application,
9/// wrapping various underlying error types from I/O, HTTP requests,
10/// JSON parsing, ZIP extraction, configuration issues, and runtime errors.
3#[allow(dead_code)] 11#[allow(dead_code)]
4#[derive(Debug)] 12#[derive(Debug)]
5pub enum McError { 13pub enum McError {
6 Io(io::Error), 14 Io(io::Error),
7 Http(reqwest::Error), 15 Http(reqwest::Error),
8 Json(serde_json::Error), 16 Json(serde_json::Error),
9 Zip(zip::result::ZipError), 17 Zip(ZipError),
10 Config(String), 18 Config(String),
11 ShaMismatch(String), 19 ShaMismatch(String),
12 Process(String), 20 Process(String),
13 Runtime(String), // ← NEW 21 Runtime(String),
14} 22}
15 23
16impl fmt::Display for McError { 24impl Display for McError {
17 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?}", self) } 25 /// Formats the error for user-friendly display.
26 ///
27 /// Currently, it uses the `Debug` format for simplicity.
28 fn fmt(&self, f: &mut Formatter<'_>) -> Result { write!(f, "{:?}", self) }
18} 29}
19 30
20impl From<io::Error> for McError { 31impl From<io::Error> for McError {
@@ -29,6 +40,6 @@ impl From<serde_json::Error> for McError {
29 fn from(e: serde_json::Error) -> Self { Self::Json(e) } 40 fn from(e: serde_json::Error) -> Self { Self::Json(e) }
30} 41}
31 42
32impl From<zip::result::ZipError> for McError { 43impl From<ZipError> for McError {
33 fn from(e: zip::result::ZipError) -> Self { Self::Zip(e) } 44 fn from(e: ZipError) -> Self { Self::Zip(e) }
34} 45}
diff --git a/src/main.rs b/src/main.rs
index 8a01b9f..e229a3e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -8,9 +8,15 @@ mod util;
8 8
9use clap::Parser; 9use clap::Parser;
10use config::Config; 10use config::Config;
11use dotenvy::dotenv;
11use errors::McError; 12use errors::McError;
12use log::{debug, info}; 13use log::{debug, info};
13 14
15use crate::minecraft::{
16 downloads::download_all, extraction::extract_natives, launcher::launch,
17 manifests,
18};
19
14#[derive(Parser, Debug)] 20#[derive(Parser, Debug)]
15#[command(author, about, disable_version_flag = true)] 21#[command(author, about, disable_version_flag = true)]
16struct Cli { 22struct Cli {
@@ -26,7 +32,7 @@ struct Cli {
26 32
27#[tokio::main] 33#[tokio::main]
28async fn main() -> Result<(), McError> { 34async fn main() -> Result<(), McError> {
29 dotenvy::dotenv().ok(); 35 dotenv().ok();
30 env_logger::init(); 36 env_logger::init();
31 37
32 let cli = Cli::parse(); 38 let cli = Cli::parse();
@@ -48,13 +54,13 @@ async fn main() -> Result<(), McError> {
48 platform::paths::ensure_dirs(&config)?; 54 platform::paths::ensure_dirs(&config)?;
49 info!("Using Minecraft version {}", config.version); 55 info!("Using Minecraft version {}", config.version);
50 56
51 let version = minecraft::manifests::load_version(&config).await?; 57 let version = manifests::load_version(&config).await?;
52 info!("Loaded version manifest for: {}", version.id); 58 info!("Loaded version manifest for: {}", version.id);
53 debug!("Main class: {}", version.main_class); 59 debug!("Main class: {}", version.main_class);
54 60
55 minecraft::downloads::download_all(&config, &version).await?; 61 download_all(&config, &version).await?;
56 minecraft::extraction::extract_natives(&config, &version)?; 62 extract_natives(&config, &version)?;
57 minecraft::launcher::launch(&config, &version)?; 63 launch(&config, &version)?;
58 64
59 Ok(()) 65 Ok(())
60} 66}
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;
diff --git a/src/platform/mod.rs b/src/platform/mod.rs
index 8118b29..24619ff 100644
--- a/src/platform/mod.rs
+++ b/src/platform/mod.rs
@@ -1 +1,11 @@
1//! Platform-specific helpers for the DML launcher.
2//!
3//! This module provides utilities for handling file paths and directories
4//! across operating systems. It includes functions to get standard locations
5//! for Minecraft game data, assets, libraries, and version directories.
6//!
7//! # Submodules
8//! - `paths`: Functions to construct paths for Minecraft data, assets,
9//! libraries, and versions.
10
1pub mod paths; 11pub mod paths;
diff --git a/src/platform/paths.rs b/src/platform/paths.rs
index 47aae9a..b430f09 100644
--- a/src/platform/paths.rs
+++ b/src/platform/paths.rs
@@ -1,48 +1,44 @@
1use std::{fs, path::PathBuf}; 1use std::{fs::create_dir_all, path::PathBuf};
2
3use directories::ProjectDirs;
4 2
5use crate::{config::Config, errors::McError}; 3use crate::{config::Config, errors::McError};
6 4
7fn project_dirs() -> ProjectDirs { 5/// ~/.local/share/dml/minecraft
8 ProjectDirs::from("com", "dml", "dml").expect("failed to determine project directories") 6pub fn minecraft_root(cfg: &Config) -> PathBuf {
7 cfg.data_dir.join("minecraft")
9} 8}
10 9
11/// Root Minecraft directory 10pub fn assets_dir(cfg: &Config) -> PathBuf {
12pub fn minecraft_root(_cfg: &Config) -> PathBuf { project_dirs().data_dir().join("minecraft") } 11 minecraft_root(cfg).join("assets")
13 12}
14/* ---------------- setup ---------------- */
15 13
14pub fn game_dir(cfg: &Config) -> PathBuf { minecraft_root(cfg) }
16pub fn ensure_dirs(cfg: &Config) -> Result<(), McError> { 15pub fn ensure_dirs(cfg: &Config) -> Result<(), McError> {
17 let root = minecraft_root(cfg); 16 let root = minecraft_root(cfg);
18 17 create_dir_all(&root)?;
19 fs::create_dir_all(root.join("versions"))?; 18 create_dir_all(root.join("versions"))?;
20 fs::create_dir_all(root.join("libraries"))?; 19 create_dir_all(root.join("libraries"))?;
21 fs::create_dir_all(root.join("assets"))?; 20 create_dir_all(assets_dir(cfg))?;
21 create_dir_all(assets_dir(cfg).join("indexes"))?;
22 create_dir_all(assets_dir(cfg).join("objects"))?;
23 create_dir_all(root.join("saves"))?;
22 24
23 Ok(()) 25 Ok(())
24} 26}
25 27
26/* ---------------- versions ---------------- */
27
28pub fn version_dir(cfg: &Config, version: &str) -> PathBuf { 28pub fn version_dir(cfg: &Config, version: &str) -> PathBuf {
29 minecraft_root(cfg).join("versions").join(version) 29 minecraft_root(cfg).join("versions").join(version)
30} 30}
31 31
32pub fn client_jar(cfg: &Config, version: &str) -> Result<PathBuf, McError> { 32pub fn client_jar(cfg: &Config, version: &str) -> Result<PathBuf, McError> {
33 Ok(version_dir(cfg, version).join(format!("{}.jar", version))) 33 Ok(version_dir(cfg, version).join(format!("{version}.jar")))
34} 34}
35 35
36/* ---------------- libraries ---------------- */
37
38pub fn library_file(cfg: &Config, rel_path: &str) -> Result<PathBuf, McError> { 36pub fn library_file(cfg: &Config, rel_path: &str) -> Result<PathBuf, McError> {
39 Ok(minecraft_root(cfg) 37 Ok(minecraft_root(cfg)
40 .join("libraries") 38 .join("libraries")
41 .join(rel_path)) 39 .join(rel_path))
42} 40}
43 41
44/* ---------------- natives ---------------- */
45
46pub fn natives_dir(cfg: &Config, version: &str) -> PathBuf { 42pub fn natives_dir(cfg: &Config, version: &str) -> PathBuf {
47 version_dir(cfg, version).join("natives") 43 version_dir(cfg, version).join("natives")
48} 44}
diff --git a/src/util/fs.rs b/src/util/fs.rs
index b86c0d7..8ecd0d0 100644
--- a/src/util/fs.rs
+++ b/src/util/fs.rs
@@ -2,11 +2,13 @@
2 2
3use std::path::Path; 3use std::path::Path;
4 4
5use tokio::fs::remove_file;
6
5use crate::errors::McError; 7use crate::errors::McError;
6 8
7pub async fn remove_if_exists(path: &Path) -> Result<(), McError> { 9pub async fn remove_if_exists(path: &Path) -> Result<(), McError> {
8 if path.exists() { 10 if path.exists() {
9 tokio::fs::remove_file(path).await?; 11 remove_file(path).await?;
10 } 12 }
11 Ok(()) 13 Ok(())
12} 14}
diff --git a/src/util/mod.rs b/src/util/mod.rs
index 8176b9b..b8531c6 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -1,2 +1,14 @@
1//! Utility module for the DML launcher.
2//!
3//! This module contains general-purpose helper functions used throughout
4//! the project. It is designed to provide reusable functionality without
5//! being specific to Minecraft or configuration.
6//!
7//! # Submodules
8//! - `fs`: File system utilities such as safe file creation, reading, and
9//! writing.
10//! - `sha1`: Functions to compute SHA-1 hashes for files and data integrity
11//! checks.
12
1pub mod fs; 13pub mod fs;
2pub mod sha1; 14pub mod sha1;
diff --git a/src/util/sha1.rs b/src/util/sha1.rs
index c5f1021..6684963 100644
--- a/src/util/sha1.rs
+++ b/src/util/sha1.rs
@@ -3,11 +3,12 @@
3use std::path::Path; 3use std::path::Path;
4 4
5use sha1::{Digest, Sha1}; 5use sha1::{Digest, Sha1};
6use tokio::fs::read;
6 7
7use crate::errors::McError; 8use crate::errors::McError;
8 9
9pub async fn sha1_hex(path: &Path) -> Result<String, McError> { 10pub async fn sha1_hex(path: &Path) -> Result<String, McError> {
10 let data = tokio::fs::read(path).await?; 11 let data = read(path).await?;
11 let mut hasher = Sha1::new(); 12 let mut hasher = Sha1::new();
12 hasher.update(&data); 13 hasher.update(&data);
13 Ok(format!("{:x}", hasher.finalize())) 14 Ok(format!("{:x}", hasher.finalize()))