aboutsummaryrefslogtreecommitdiffstats
path: root/src/minecraft
diff options
context:
space:
mode:
Diffstat (limited to 'src/minecraft')
-rw-r--r--src/minecraft/downloads.rs423
-rw-r--r--src/minecraft/extraction.rs70
-rw-r--r--src/minecraft/launcher.rs262
-rw-r--r--src/minecraft/manifests.rs41
4 files changed, 549 insertions, 247 deletions
diff --git a/src/minecraft/downloads.rs b/src/minecraft/downloads.rs
index 7017d3f..8df994e 100644
--- a/src/minecraft/downloads.rs
+++ b/src/minecraft/downloads.rs
@@ -1,181 +1,360 @@
1use log::{debug, info}; 1use std::{
2use reqwest::get; 2 collections::HashMap,
3 io::{Write, stdout},
4 path::{Path, PathBuf},
5 sync::{
6 Arc,
7 atomic::{AtomicUsize, Ordering},
8 },
9};
10
11use McError::Config;
12use Ordering::SeqCst;
13use futures::stream::{FuturesUnordered, StreamExt};
14use reqwest::Response;
3use serde::Deserialize; 15use serde::Deserialize;
4use tokio::{ 16use tokio::{
5 fs::{self, File, create_dir_all}, 17 fs::{File, create_dir_all, read_to_string, write},
6 io::AsyncWriteExt, 18 io::AsyncWriteExt,
19 spawn,
20 sync::Semaphore,
21 task::{JoinError, JoinHandle},
7}; 22};
8 23
9use crate::{ 24use crate::{
10 config::Config, 25 config::RuntimeConfig,
11 errors::McError, 26 errors::McError,
12 minecraft::manifests::{Library, Version}, 27 minecraft::manifests::{AssetIndex, Library, LibraryArtifact, Version},
13 platform::paths, 28 platform::paths::{assets_directory, client_jar, library_file},
14}; 29};
15 30
31const MAX_CONCURRENT_DOWNLOADS: usize = 100;
32const PROGRESS_INITIAL_VALUE: usize = 0;
33const ASSET_URL_BASE: &str = "https://resources.download.minecraft.net/";
34const MAX_PROGRESS_PERCENT: f64 = 100.0;
35const PROGRESS_PRINT_WIDTH: usize = 3;
36const ATOMIC_ORDERING: Ordering = SeqCst;
37const DOWNLOAD_COMPLETE_MESSAGE: &str = "All downloads completed successfully!";
38const ASSET_INDEX_MISSING_ERROR: &str = "Missing asset_index for the version";
39
40impl From<JoinError> for McError {
41 fn from(error: JoinError) -> Self {
42 Config(format!("Task panicked: {}", error))
43 }
44}
45
46/// Represents a single file to download, including the URL and destination
47/// path.
48#[derive(Debug, Clone)]
49struct DownloadJob {
50 url: String,
51 destination_path: PathBuf,
52}
53
54impl DownloadJob {
55 fn already_exists(&self) -> bool { self.destination_path.exists() }
56}
57
58/// Represents a single asset entry in the Minecraft asset index.
16#[derive(Debug, Deserialize)] 59#[derive(Debug, Deserialize)]
17struct AssetObject { 60struct AssetObject {
18 hash: String, 61 hash: String,
19 // size: u64,
20} 62}
21 63
64/// The Minecraft asset index manifest.
22#[derive(Debug, Deserialize)] 65#[derive(Debug, Deserialize)]
23struct AssetIndexManifest { 66struct AssetIndexManifest {
24 objects: std::collections::HashMap<String, AssetObject>, 67 objects: HashMap<String, AssetObject>,
25} 68}
26 69
27pub async fn download_all( 70/// Download all files required to run a specific Minecraft version.
28 config: &Config, 71pub async fn download_all_files(
29 version: &Version, 72 http_client: &reqwest::Client,
73 runtime_config: &RuntimeConfig,
74 version_info: &Version,
30) -> Result<(), McError> { 75) -> Result<(), McError> {
31 download_client(config, version).await?; 76 let assets_directory_path: PathBuf =
32 download_libraries(config, &version.libraries).await?; 77 ensure_assets_directories_exist(runtime_config).await?;
33 download_assets(config, version).await?; 78 let asset_manifest: AssetIndexManifest = load_asset_index_manifest(
79 http_client,
80 &assets_directory_path,
81 version_info,
82 )
83 .await?;
84
85 let download_jobs: Vec<DownloadJob> = build_download_jobs(
86 runtime_config,
87 version_info,
88 &assets_directory_path,
89 &asset_manifest,
90 );
91
92 execute_download_jobs(http_client, download_jobs).await?;
93 println!("\n{}", DOWNLOAD_COMPLETE_MESSAGE);
94
34 Ok(()) 95 Ok(())
35} 96}
36 97
37async fn download_client( 98/// Ensure the essential assets directories exist.
38 config: &Config, 99async fn ensure_assets_directories_exist(
39 version: &Version, 100 config: &RuntimeConfig,
40) -> Result<(), McError> { 101) -> Result<PathBuf, McError> {
41 let jar_path = paths::client_jar(config, &version.id)?; 102 let assets_dir = assets_directory(config);
42 103 create_dir_all(assets_dir.join("objects")).await?;
43 if jar_path.exists() { 104 create_dir_all(assets_dir.join("indexes")).await?;
44 debug!("Client jar already exists: {}", jar_path.display()); 105 Ok(assets_dir)
45 return Ok(());
46 }
47
48 info!("Downloading client {}", version.id);
49 download_file(&version.downloads.client.url, &jar_path).await
50} 106}
51 107
52async fn download_libraries( 108/// Load the asset index manifest for the given Minecraft version.
53 config: &Config, 109async fn load_asset_index_manifest(
54 libraries: &[Library], 110 http_client: &reqwest::Client,
55) -> Result<(), McError> { 111 assets_dir: &Path,
56 for library in libraries { 112 version_info: &Version,
57 if let Some(artifact) = &library.downloads.artifact { 113) -> Result<AssetIndexManifest, McError> {
58 let library_path = paths::library_file(config, &artifact.path)?; 114 let asset_index: &AssetIndex = version_info
59 115 .asset_index
60 if !library_path.exists() { 116 .as_ref()
61 info!("Downloading library {}", artifact.path); 117 .ok_or_else(|| Config(ASSET_INDEX_MISSING_ERROR.into()))?;
62 download_file(&artifact.url, &library_path).await?;
63 }
64 }
65
66 if let Some(classifiers) = &library.downloads.classifiers {
67 for (_, native) in classifiers {
68 let native_path = paths::library_file(config, &native.path)?;
69 118
70 if native_path.exists() { 119 let index_file_path: PathBuf = assets_dir
71 continue; 120 .join("indexes")
72 } 121 .join(format!("{}.json", asset_index.id));
73 122
74 info!("Downloading native library {}", native.path); 123 if !index_file_path.exists() {
75 download_file(&native.url, &native_path).await?; 124 download_text_file(http_client, &asset_index.url, &index_file_path)
76 } 125 .await?;
77 }
78 } 126 }
79 127
80 Ok(()) 128 let json_string: String = read_to_string(index_file_path).await?;
129 Ok(serde_json::from_str(&json_string)?)
81} 130}
82 131
83async fn download_asset_index( 132fn build_download_jobs(
84 config: &Config, 133 config: &RuntimeConfig,
85 version: &Version, 134 version_info: &Version,
86) -> Result<AssetIndexManifest, McError> { 135 assets_dir: &Path,
87 let assets_dir = paths::assets_dir(config); 136 asset_manifest: &AssetIndexManifest,
88 create_dir_all(assets_dir.join("indexes")).await?; 137) -> Vec<DownloadJob> {
138 let mut jobs: Vec<DownloadJob> = Vec::new();
89 139
90 let asset_index = version.asset_index.as_ref().ok_or_else(|| { 140 add_client_download_job(&mut jobs, config, version_info);
91 McError::Config("Missing asset_index in version.json".into()) 141 add_library_download_jobs(&mut jobs, config, version_info);
92 })?; 142 add_asset_download_jobs(&mut jobs, assets_dir, asset_manifest);
93 143
94 if asset_index.id == "legacy" { 144 jobs
95 return Err(McError::Config( 145}
96 "Legacy assetIndex detected – pobierz właściwy version.json".into(),
97 ));
98 }
99 146
100 let index_path = assets_dir 147fn add_client_download_job(
101 .join("indexes") 148 jobs: &mut Vec<DownloadJob>,
102 .join(format!("{}.json", asset_index.id)); 149 config: &RuntimeConfig,
150 version_info: &Version,
151) {
152 jobs.push(DownloadJob {
153 url: version_info.downloads.client.url.clone(),
154 destination_path: client_jar(config, &version_info.id),
155 });
156}
103 157
104 if index_path.exists() { 158fn add_library_download_jobs(
105 let index_data = fs::read_to_string(&index_path).await?; 159 jobs: &mut Vec<DownloadJob>,
106 let manifest: AssetIndexManifest = serde_json::from_str(&index_data)?; 160 config: &RuntimeConfig,
107 return Ok(manifest); 161 version_info: &Version,
162) {
163 for library in &version_info.libraries {
164 add_library_artifact_job(jobs, config, library);
165 add_library_classifier_jobs(jobs, config, library);
108 } 166 }
167}
168fn add_library_artifact_job(
169 jobs: &mut Vec<DownloadJob>,
170 config: &RuntimeConfig,
171 library: &Library,
172) {
173 let artifact: &LibraryArtifact = match &library.downloads.artifact {
174 | Some(a) => a,
175 | None => return,
176 };
177
178 jobs.push(DownloadJob {
179 url: artifact.url.clone(),
180 destination_path: library_file(config, &artifact.path),
181 });
182}
109 183
110 info!("Downloading asset index {}", asset_index.id); 184fn add_library_classifier_jobs(
111 let response = get(&asset_index.url).await?; 185 jobs: &mut Vec<DownloadJob>,
112 let manifest_text = response.text().await?; 186 config: &RuntimeConfig,
113 187 library: &Library,
114 fs::write(&index_path, &manifest_text).await?; 188) {
189 let classifiers: &HashMap<String, LibraryArtifact> =
190 match &library.downloads.classifiers {
191 | Some(values) => values,
192 | None => return,
193 };
194
195 for classifier_entry in classifiers.values() {
196 jobs.push(DownloadJob {
197 url: classifier_entry.url.clone(),
198 destination_path: library_file(config, &classifier_entry.path),
199 });
200 }
201}
115 202
116 let manifest: AssetIndexManifest = serde_json::from_str(&manifest_text)?; 203fn add_asset_download_jobs(
117 Ok(manifest) 204 jobs: &mut Vec<DownloadJob>,
205 assets_dir: &Path,
206 asset_manifest: &AssetIndexManifest,
207) {
208 for asset_object in asset_manifest.objects.values() {
209 let prefix: &str = &asset_object.hash[0..2];
210 jobs.push(DownloadJob {
211 url: format!("{}{}/{}", ASSET_URL_BASE, prefix, asset_object.hash),
212 destination_path: assets_dir
213 .join("objects")
214 .join(prefix)
215 .join(&asset_object.hash),
216 });
217 }
118} 218}
119 219
120async fn download_assets( 220async fn execute_download_jobs(
121 config: &Config, 221 http_client: &reqwest::Client,
122 version: &Version, 222 download_jobs: Vec<DownloadJob>,
123) -> Result<(), McError> { 223) -> Result<(), McError> {
124 let assets_dir = paths::assets_dir(config); 224 let total_jobs_count: usize = download_jobs.len();
125 225 let completed_jobs_count: Arc<AtomicUsize> =
126 create_dir_all(assets_dir.join("objects")).await?; 226 Arc::new(AtomicUsize::new(PROGRESS_INITIAL_VALUE));
127 create_dir_all(assets_dir.join("indexes")).await?; 227 let concurrent_download_semaphore: Arc<Semaphore> =
128 228 Arc::new(Semaphore::new(MAX_CONCURRENT_DOWNLOADS));
129 let manifest = download_asset_index(config, version).await?; 229
230 let mut tasks: FuturesUnordered<JoinHandle<Result<(), McError>>> =
231 spawn_missing_download_jobs(
232 http_client,
233 download_jobs,
234 &completed_jobs_count,
235 total_jobs_count,
236 concurrent_download_semaphore,
237 );
130 238
131 for (logical_path, asset) in &manifest.objects { 239 await_download_tasks(&mut tasks).await
132 let subdir = &asset.hash[0..2]; 240}
133 let file_path = assets_dir
134 .join("objects")
135 .join(subdir)
136 .join(&asset.hash);
137 241
138 if file_path.exists() { 242fn spawn_missing_download_jobs(
243 http_client: &reqwest::Client,
244 download_jobs: Vec<DownloadJob>,
245 completed_jobs_counter: &Arc<AtomicUsize>,
246 total_jobs_count: usize,
247 concurrent_download_semaphore: Arc<Semaphore>,
248) -> FuturesUnordered<JoinHandle<Result<(), McError>>> {
249 let download_tasks: FuturesUnordered<JoinHandle<Result<(), McError>>> =
250 FuturesUnordered::new();
251
252 for job in download_jobs {
253 if job.already_exists() {
254 completed_jobs_counter.fetch_add(1, SeqCst);
255 print_download_progress(completed_jobs_counter, total_jobs_count);
139 continue; 256 continue;
140 } 257 }
141 258
142 let url = format!( 259 download_tasks.push(spawn_download_job(
143 "https://resources.download.minecraft.net/{}/{}", 260 http_client.clone(),
144 subdir, asset.hash 261 job,
145 ); 262 concurrent_download_semaphore.clone(),
146 info!("Downloading asset {} -> {}", logical_path, file_path.display()); 263 completed_jobs_counter.clone(),
147 download_file(&url, &file_path).await?; 264 total_jobs_count,
265 ));
148 } 266 }
149 267
150 if let Some(asset) = manifest.objects.get("sounds.json") { 268 download_tasks
151 let file_path = assets_dir.join("indexes").join("sounds.json"); 269}
152 if !file_path.exists() { 270
153 let subdir = &asset.hash[0..2]; 271async fn await_download_tasks(
154 let url = format!( 272 tasks: &mut FuturesUnordered<JoinHandle<Result<(), McError>>>,
155 "https://resources.download.minecraft.net/{}/{}", 273) -> Result<(), McError> {
156 subdir, asset.hash 274 while let Some(task_result) = tasks.next().await {
157 ); 275 task_result.map_err(McError::from)??;
158 info!("Downloading sounds.json");
159 download_file(&url, &file_path).await?;
160 }
161 } 276 }
277 Ok(())
278}
162 279
280fn spawn_download_job(
281 http_client: reqwest::Client,
282 download_job: DownloadJob,
283 concurrent_download_semaphore: Arc<Semaphore>,
284 completed_jobs_count: Arc<AtomicUsize>,
285 total_jobs_count: usize,
286) -> JoinHandle<Result<(), McError>> {
287 spawn(async move {
288 let _permit = concurrent_download_semaphore
289 .acquire_owned()
290 .await
291 .unwrap();
292 download_file(&http_client, &download_job).await?;
293 completed_jobs_count.fetch_add(1, SeqCst);
294 print_download_progress(&completed_jobs_count, total_jobs_count);
295 Ok(())
296 })
297}
298
299/// Print the current progress of downloads to stdout.
300///
301/// # Parameters
302/// - `completed_jobs_count`: Atomic counter of completed download jobs.
303/// - `total_jobs_count`: Total number of jobs being processed.
304fn print_download_progress(
305 completed_jobs_count: &AtomicUsize,
306 total_jobs_count: usize,
307) {
308 let completed_jobs: usize = completed_jobs_count.load(ATOMIC_ORDERING);
309 let progress_percentage: f64 = ((completed_jobs as f64
310 / total_jobs_count as f64)
311 * MAX_PROGRESS_PERCENT)
312 .min(MAX_PROGRESS_PERCENT);
313
314 print!(
315 "\rDownloading game files: {:>width$.0}%",
316 progress_percentage,
317 width = PROGRESS_PRINT_WIDTH
318 );
319 stdout().flush().unwrap();
320}
321
322async fn download_text_file(
323 http_client: &reqwest::Client,
324 file_url: &str,
325 destination_path: &PathBuf,
326) -> Result<(), McError> {
327 let text_content = http_client
328 .get(file_url)
329 .send()
330 .await?
331 .error_for_status()?
332 .text()
333 .await?;
334 write(destination_path, text_content).await?;
163 Ok(()) 335 Ok(())
164} 336}
165 337
166async fn download_file( 338async fn download_file(
167 url: &str, 339 http_client: &reqwest::Client,
168 path: &std::path::Path, 340 download_job: &DownloadJob,
169) -> Result<(), McError> { 341) -> Result<(), McError> {
170 if let Some(parent) = path.parent() { 342 if let Some(parent_dir) = download_job.destination_path.parent() {
171 create_dir_all(parent).await?; 343 create_dir_all(parent_dir).await?;
172 } 344 }
173 345
174 let response = get(url).await?; 346 let response: Response = http_client
175 let bytes = response.bytes().await?; 347 .get(&download_job.url)
176 348 .send()
177 let mut file = File::create(path).await?; 349 .await?
178 file.write_all(&bytes).await?; 350 .error_for_status()?;
351 let mut byte_stream = response.bytes_stream();
352 let mut file_handle: File =
353 File::create(&download_job.destination_path).await?;
354
355 while let Some(chunk) = byte_stream.next().await {
356 file_handle.write_all(&chunk?).await?;
357 }
179 358
180 Ok(()) 359 Ok(())
181} 360}
diff --git a/src/minecraft/extraction.rs b/src/minecraft/extraction.rs
index b58fd2e..292566f 100644
--- a/src/minecraft/extraction.rs
+++ b/src/minecraft/extraction.rs
@@ -1,11 +1,17 @@
1use std::{fs, io, path::Path}; 1use std::{
2 collections::HashMap,
3 fs,
4 fs::File,
5 io,
6 path::{Path, PathBuf},
7};
2 8
3use log::info; 9use zip::{read::ZipFile, ZipArchive};
4use zip::ZipArchive;
5 10
6use crate::{ 11use crate::{
7 errors::McError, 12 errors::McError,
8 minecraft::manifests::{Library, Version}, 13 minecraft::manifests::{LibraryArtifact, Version},
14 util::fs::library_allowed,
9}; 15};
10 16
11pub fn extract_natives( 17pub fn extract_natives(
@@ -19,8 +25,6 @@ pub fn extract_natives(
19 .join(&version.id) 25 .join(&version.id)
20 .join("natives"); 26 .join("natives");
21 27
22 info!("Extracting natives for {} into {:?}", version.id, natives_dir);
23
24 if natives_dir.exists() { 28 if natives_dir.exists() {
25 fs::remove_dir_all(&natives_dir)?; 29 fs::remove_dir_all(&natives_dir)?;
26 } 30 }
@@ -31,75 +35,51 @@ pub fn extract_natives(
31 continue; 35 continue;
32 } 36 }
33 37
34 let natives = match &lib.natives { 38 let natives: &HashMap<String, String> = match &lib.natives {
35 | Some(n) => n, 39 | Some(n) => n,
36 | None => continue, 40 | None => continue,
37 }; 41 };
38 42
39 let classifier = match natives.get("linux") { 43 let classifier: &String = match natives.get("linux") {
40 | Some(c) => c, 44 | Some(c) => c,
41 | None => continue, 45 | None => continue,
42 }; 46 };
43 47
44 let classifiers = match &lib.downloads.classifiers { 48 let classifiers: &HashMap<String, LibraryArtifact> =
45 | Some(c) => c, 49 match &lib.downloads.classifiers {
46 | None => continue, 50 | Some(c) => c,
47 }; 51 | None => continue,
52 };
48 53
49 let artifact = match classifiers.get(classifier) { 54 let artifact: &LibraryArtifact = match classifiers.get(classifier) {
50 | Some(a) => a, 55 | Some(a) => a,
51 | None => continue, 56 | None => continue,
52 }; 57 };
53 58
54 let jar_path = cfg 59 let jar_path: PathBuf = cfg
55 .data_dir 60 .data_dir
56 .join("minecraft") 61 .join("minecraft")
57 .join("libraries") 62 .join("libraries")
58 .join(&artifact.path); 63 .join(&artifact.path);
59 64
60 info!("Extracting natives from {:?}", jar_path);
61
62 extract_zip(&jar_path, &natives_dir)?; 65 extract_zip(&jar_path, &natives_dir)?;
63 } 66 }
64 67
65 Ok(()) 68 Ok(())
66} 69}
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> { 70fn extract_zip(jar_path: &Path, out_dir: &Path) -> Result<(), McError> {
91 let file = fs::File::open(jar_path)?; 71 let file: File = File::open(jar_path)?;
92 let mut zip = ZipArchive::new(file)?; 72 let mut zip: ZipArchive<File> = ZipArchive::new(file)?;
93 73
94 for i in 0..zip.len() { 74 for i in 0..zip.len() {
95 let mut entry = zip.by_index(i)?; 75 let mut entry: ZipFile<File> = zip.by_index(i)?;
96 let name = entry.name(); 76 let name: &str = entry.name();
97 77
98 if name.starts_with("META-INF/") { 78 if name.starts_with("META-INF/") {
99 continue; 79 continue;
100 } 80 }
101 81
102 let out_path = out_dir.join(name); 82 let out_path: PathBuf = out_dir.join(name);
103 83
104 if entry.is_dir() { 84 if entry.is_dir() {
105 fs::create_dir_all(&out_path)?; 85 fs::create_dir_all(&out_path)?;
@@ -110,7 +90,7 @@ fn extract_zip(jar_path: &Path, out_dir: &Path) -> Result<(), McError> {
110 fs::create_dir_all(parent)?; 90 fs::create_dir_all(parent)?;
111 } 91 }
112 92
113 let mut out_file = fs::File::create(&out_path)?; 93 let mut out_file: File = File::create(&out_path)?;
114 io::copy(&mut entry, &mut out_file)?; 94 io::copy(&mut entry, &mut out_file)?;
115 } 95 }
116 96
diff --git a/src/minecraft/launcher.rs b/src/minecraft/launcher.rs
index 2eeaf21..3cca948 100644
--- a/src/minecraft/launcher.rs
+++ b/src/minecraft/launcher.rs
@@ -1,64 +1,104 @@
1use std::process::Command; 1use paths::{client_jar, library_file, natives_dir};
2 2use serde::Deserialize;
3use log::{debug, info}; 3use std::path::Path;
4use std::process::Output;
5use std::{
6 path::PathBuf,
7 process::{Command, ExitStatus},
8};
9use McError::Process;
4 10
5use crate::{ 11use crate::{
6 config::Config, 12 config::Config, errors::McError, minecraft::manifests::Version,
7 errors::McError, 13 platform::paths, util::fs::library_allowed,
8 minecraft::manifests::{Library, Version},
9 platform::paths,
10}; 14};
11 15
12fn build_classpath( 16#[derive(Debug, Clone, Deserialize)]
13 config: &Config, 17pub struct JavaRuntime {
14 version: &Version, 18 pub major: u8,
15) -> Result<String, McError> { 19 pub path: PathBuf,
16 let sep = if cfg!(windows) { ";" } else { ":" }; 20}
17 let mut entries = Vec::new(); 21
22impl JavaRuntime {
23 fn validate(&self) -> Result<(), McError> {
24 let output: Output = Command::new(&self.path)
25 .arg("-version")
26 .output()
27 .map_err(|e| {
28 McError::Runtime(format!(
29 "Failed to execute Java at {}: {}",
30 self.path.display(),
31 e
32 ))
33 })?;
34
35 if !output.status.success() {
36 return Err(McError::Runtime(format!(
37 "Invalid Java binary: {}",
38 self.path.display()
39 )));
40 }
41
42 Ok(())
43 }
44}
45
46fn required_java_major(java_major: Option<u8>) -> u8 {
47 java_major.unwrap_or(8)
48}
49
50fn resolve_runtime(
51 required: u8,
52 runtimes: &[JavaRuntime],
53) -> Result<PathBuf, McError> {
54 let mut candidates: Vec<&JavaRuntime> = runtimes
55 .iter()
56 .filter(|r| r.major >= required)
57 .collect();
58
59 candidates.sort_by_key(|r| r.major);
60
61 let runtime = candidates.first().ok_or_else(|| {
62 McError::Runtime(format!(
63 "No suitable Java runtime found (required: Java {})",
64 required
65 ))
66 })?;
67
68 runtime.validate()?;
69 Ok(runtime.path.clone())
70}
71
72fn build_classpath(config: &Config, version: &Version) -> String {
73 let system_separator: &str = if cfg!(windows) { ";" } else { ":" };
74 let mut entries: Vec<String> = Vec::new();
18 75
19 for library in &version.libraries { 76 for library in &version.libraries {
20 if !library_allowed(library) { 77 if !library_allowed(library) {
21 continue; 78 continue;
22 } 79 }
80
23 if let Some(artifact) = &library.downloads.artifact { 81 if let Some(artifact) = &library.downloads.artifact {
24 let path = paths::library_file(config, &artifact.path)?; 82 let path = library_file(config, &artifact.path);
25 entries.push(path.to_string_lossy().to_string()); 83 entries.push(path.to_string_lossy().to_string());
26 } 84 }
27 } 85 }
28 86
29 let client_jar = paths::client_jar(config, &version.id)?; 87 let client: PathBuf = client_jar(config, &version.id);
30 entries.push(client_jar.to_string_lossy().to_string()); 88 entries.push(client.to_string_lossy().to_string());
31 89
32 Ok(entries.join(sep)) 90 entries.join(system_separator)
33} 91}
34 92
35pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { 93fn build_command(
36 let java = &config.java_path; 94 java_path: PathBuf,
37 let classpath = build_classpath(config, version)?; 95 config: &Config,
38 let natives_dir = paths::natives_dir(config, &version.id); 96 version: &Version,
39 97 natives_dir: &Path,
40 if !natives_dir.exists() { 98 classpath: String,
41 return Err(McError::Runtime(format!( 99 asset_index_id: String,
42 "Natives folder does not exist: {}", 100) -> Command {
43 natives_dir.display() 101 let mut cmd: Command = Command::new(java_path);
44 )));
45 }
46
47 let asset_index_id = version
48 .asset_index
49 .as_ref()
50 .ok_or_else(|| {
51 McError::Runtime("Missing assetIndex in version.json".into())
52 })?
53 .id
54 .clone();
55
56 info!("Launching Minecraft {}", version.id);
57 debug!("Classpath: {}", classpath);
58 debug!("Natives: {}", natives_dir.display());
59 debug!("Asset index: {}", asset_index_id);
60
61 let mut cmd = Command::new(java);
62 102
63 cmd.arg(format!("-Xmx{}M", config.max_memory_mb)) 103 cmd.arg(format!("-Xmx{}M", config.max_memory_mb))
64 .arg(format!("-Djava.library.path={}", natives_dir.display())); 104 .arg(format!("-Djava.library.path={}", natives_dir.display()));
@@ -76,11 +116,11 @@ pub fn launch(config: &Config, version: &Version) -> Result<(), McError> {
76 .arg("--version") 116 .arg("--version")
77 .arg(&version.id) 117 .arg(&version.id)
78 .arg("--gameDir") 118 .arg("--gameDir")
79 .arg(paths::game_dir(config)) 119 .arg(paths::game_directory(config))
80 .arg("--assetsDir") 120 .arg("--assetsDir")
81 .arg(paths::assets_dir(config)) 121 .arg(paths::assets_directory(config))
82 .arg("--assetIndex") 122 .arg("--assetIndex")
83 .arg(&asset_index_id) 123 .arg(asset_index_id)
84 .arg("--uuid") 124 .arg("--uuid")
85 .arg(&config.uuid) 125 .arg(&config.uuid)
86 .arg("--userProperties") 126 .arg("--userProperties")
@@ -90,32 +130,128 @@ pub fn launch(config: &Config, version: &Version) -> Result<(), McError> {
90 .arg("--userType") 130 .arg("--userType")
91 .arg("legacy"); 131 .arg("legacy");
92 132
93 let status = cmd.status()?; 133 cmd
134}
135
136pub fn launch(config: &Config, version: &Version) -> Result<(), McError> {
137 let required: u8 = required_java_major(
138 version
139 .java_version
140 .as_ref()
141 .map(|j| j.major_version),
142 );
143
144 let java_path: PathBuf = resolve_runtime(required, &config.runtimes)?;
145
146 let classpath: String = build_classpath(config, version);
147 let natives_dir: PathBuf = natives_dir(config, &version.id);
148
149 if !natives_dir.exists() {
150 return Err(McError::Runtime(format!(
151 "Natives folder does not exist: {}",
152 natives_dir.display()
153 )));
154 }
155
156 let asset_index_id = version
157 .asset_index
158 .as_ref()
159 .ok_or_else(|| {
160 McError::Runtime("Missing assetIndex in version.json".into())
161 })?
162 .id
163 .clone();
164
165 let mut cmd: Command = build_command(
166 java_path,
167 config,
168 version,
169 &natives_dir,
170 classpath,
171 asset_index_id,
172 );
173
174 let status: ExitStatus = cmd.status()?;
94 175
95 if !status.success() { 176 if !status.success() {
96 return Err(McError::Process("Minecraft exited with error".into())); 177 return Err(Process("Minecraft exited with error".into()));
97 } 178 }
98 179
99 Ok(()) 180 Ok(())
100} 181}
101 182
102fn library_allowed(lib: &Library) -> bool { 183#[cfg(test)]
103 let rules = match &lib.rules { 184mod tests {
104 | Some(r) => r, 185 use super::*;
105 | None => return true, 186 use std::path::PathBuf;
106 }; 187
188 #[test]
189 fn required_java_major_defaults_to_8() {
190 assert_eq!(required_java_major(None), 8);
191 }
192
193 #[test]
194 fn required_java_major_uses_given_value() {
195 assert_eq!(required_java_major(Some(17)), 17);
196 }
197
198 #[test]
199 fn resolve_runtime_errors_when_no_runtimes() {
200 let runtimes: Vec<JavaRuntime> = vec![];
201 assert!(resolve_runtime(17, &runtimes).is_err());
202 }
203
204 #[test]
205 fn resolve_runtime_picks_lowest_matching_major() {
206 let runtimes: Vec<JavaRuntime> = vec![
207 JavaRuntime {
208 major: 21,
209 path: PathBuf::from("/bin/true"),
210 },
211 JavaRuntime {
212 major: 17,
213 path: PathBuf::from("/bin/true"),
214 },
215 ];
216
217 let result: PathBuf = resolve_runtime(8, &runtimes).unwrap();
218 assert_eq!(result, PathBuf::from("/bin/true"));
219 }
107 220
108 let mut allowed = false; 221 #[test]
222 fn resolve_runtime_respects_required_version() {
223 let runtimes: Vec<JavaRuntime> = vec![
224 JavaRuntime {
225 major: 8,
226 path: PathBuf::from("/bin/true"),
227 },
228 JavaRuntime {
229 major: 17,
230 path: PathBuf::from("/bin/true"),
231 },
232 ];
233
234 let result: PathBuf = resolve_runtime(17, &runtimes).unwrap();
235 assert_eq!(result, PathBuf::from("/bin/true"));
236 }
109 237
110 for rule in rules { 238 #[test]
111 let os_match = match &rule.os { 239 fn validate_succeeds_for_true_binary() {
112 | Some(os) => os.name == "linux", 240 let runtime = JavaRuntime {
113 | None => true, 241 major: 17,
242 path: PathBuf::from("/bin/true"),
114 }; 243 };
115 if os_match { 244
116 allowed = rule.action == "allow"; 245 assert!(runtime.validate().is_ok());
117 }
118 } 246 }
119 247
120 allowed 248 #[test]
249 fn validate_fails_for_false_binary() {
250 let runtime = JavaRuntime {
251 major: 17,
252 path: PathBuf::from("/bin/false"),
253 };
254
255 assert!(runtime.validate().is_err());
256 }
121} 257}
diff --git a/src/minecraft/manifests.rs b/src/minecraft/manifests.rs
index 8bdec26..64e38da 100644
--- a/src/minecraft/manifests.rs
+++ b/src/minecraft/manifests.rs
@@ -1,11 +1,18 @@
1#![allow(dead_code)] 1#![allow(dead_code)]
2 2
3use std::collections::HashMap; 3use crate::{constants::VERSION_MANIFEST_URL, errors::McError};
4 4use reqwest::get;
5use reqwest;
6use serde::Deserialize; 5use serde::Deserialize;
6use serde_json::{from_str, Value};
7use std::collections::HashMap;
8use McError::Config;
7 9
8use crate::{constants::VERSION_MANIFEST_URL, errors::McError}; 10#[derive(Debug, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct JavaVersionInfo {
13 pub component: String,
14 pub major_version: u8,
15}
9 16
10#[derive(Debug, Deserialize)] 17#[derive(Debug, Deserialize)]
11pub struct Version { 18pub struct Version {
@@ -19,6 +26,10 @@ pub struct Version {
19 26
20 #[serde(rename = "assetIndex")] 27 #[serde(rename = "assetIndex")]
21 pub asset_index: Option<AssetIndex>, 28 pub asset_index: Option<AssetIndex>,
29
30 #[serde(default)]
31 #[serde(rename = "javaVersion")]
32 pub java_version: Option<JavaVersionInfo>,
22} 33}
23 34
24#[derive(Debug, Deserialize)] 35#[derive(Debug, Deserialize)]
@@ -83,15 +94,13 @@ pub struct OsRule {
83pub async fn load_version( 94pub async fn load_version(
84 cfg: &crate::config::Config, 95 cfg: &crate::config::Config,
85) -> Result<Version, McError> { 96) -> Result<Version, McError> {
86 let manifest_text = reqwest::get(VERSION_MANIFEST_URL) 97 let manifest_text = get(VERSION_MANIFEST_URL).await?.text().await?;
87 .await? 98 let root: Value = from_str(&manifest_text)?;
88 .text() 99
89 .await?;
90 let root: serde_json::Value = serde_json::from_str(&manifest_text)?;
91 let version_id = if cfg.version == "latest" { 100 let version_id = if cfg.version == "latest" {
92 root["latest"]["release"] 101 root["latest"]["release"]
93 .as_str() 102 .as_str()
94 .ok_or_else(|| McError::Config("missing latest.release".into()))? 103 .ok_or_else(|| Config("missing latest.release".into()))?
95 .to_string() 104 .to_string()
96 } else { 105 } else {
97 cfg.version.clone() 106 cfg.version.clone()
@@ -99,21 +108,19 @@ pub async fn load_version(
99 108
100 let versions = root["versions"] 109 let versions = root["versions"]
101 .as_array() 110 .as_array()
102 .ok_or_else(|| McError::Config("missing versions array".into()))?; 111 .ok_or_else(|| Config("missing versions array".into()))?;
103 112
104 let version_entry = versions 113 let version_entry = versions
105 .iter() 114 .iter()
106 .find(|v| v["id"].as_str() == Some(&version_id)) 115 .find(|v| v["id"].as_str() == Some(&version_id))
107 .ok_or_else(|| { 116 .ok_or_else(|| Config(format!("version '{}' not found", version_id)))?;
108 McError::Config(format!("version '{}' not found", version_id))
109 })?;
110 117
111 let url = version_entry["url"] 118 let url = version_entry["url"]
112 .as_str() 119 .as_str()
113 .ok_or_else(|| McError::Config("missing version url".into()))?; 120 .ok_or_else(|| Config("missing version url".into()))?;
114 121
115 let version_text = reqwest::get(url).await?.text().await?; 122 let version_text = get(url).await?.text().await?;
116 let version: Version = serde_json::from_str(&version_text)?; 123 let version: Version = from_str(&version_text)?;
117 124
118 Ok(version) 125 Ok(version)
119} 126}