diff options
| author | Filip Wandzio <contact@philw.dev> | 2026-01-24 08:29:14 +0100 |
|---|---|---|
| committer | Filip Wandzio <contact@philw.dev> | 2026-01-24 08:29:14 +0100 |
| commit | a393e0a2f2c3678a3ea869dc1417fa269f2b1040 (patch) | |
| tree | 606df6a9284b5bd2dbf84fa5e3d363b8e6a01322 | |
| parent | 72ddd7b7704f2087a52c9c0552446682918c513b (diff) | |
| download | dml-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
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | clippy.toml | 8 | ||||
| -rw-r--r-- | rustfmt.toml | 2 | ||||
| -rw-r--r-- | src/config/loader.rs | 23 | ||||
| -rw-r--r-- | src/config/mod.rs | 11 | ||||
| -rw-r--r-- | src/constants.rs | 2 | ||||
| -rw-r--r-- | src/errors.rs | 23 | ||||
| -rw-r--r-- | src/main.rs | 16 | ||||
| -rw-r--r-- | src/minecraft/downloads.rs | 172 | ||||
| -rw-r--r-- | src/minecraft/extraction.rs | 116 | ||||
| -rw-r--r-- | src/minecraft/launcher.rs | 91 | ||||
| -rw-r--r-- | src/minecraft/manifests.rs | 43 | ||||
| -rw-r--r-- | src/minecraft/mod.rs | 15 | ||||
| -rw-r--r-- | src/platform/mod.rs | 10 | ||||
| -rw-r--r-- | src/platform/paths.rs | 36 | ||||
| -rw-r--r-- | src/util/fs.rs | 4 | ||||
| -rw-r--r-- | src/util/mod.rs | 12 | ||||
| -rw-r--r-- | src/util/sha1.rs | 3 |
18 files changed, 488 insertions, 100 deletions
| @@ -6,3 +6,4 @@ Cargo.lock | |||
| 6 | **/*.rs.bk | 6 | **/*.rs.bk |
| 7 | *.pdb | 7 | *.pdb |
| 8 | rust-project.json | 8 | rust-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 @@ | |||
| 1 | stack-size-threshold = 393216 | 1 | stack-size-threshold = 393216 |
| 2 | future-size-threshold = 24576 | 2 | future-size-threshold = 24576 |
| 3 | array-size-threshold = 4096 | 3 | array-size-threshold = 4096 |
| 4 | large-error-threshold = 256 # TODO reduce me ALARA | 4 | large-error-threshold = 256 |
| 5 | too-many-lines-threshold = 100 # TODO reduce me to <= 100 | 5 | too-many-lines-threshold = 100 |
| 6 | excessive-nesting-threshold = 3 | 6 | excessive-nesting-threshold = 3 |
| 7 | type-complexity-threshold = 250 # reduce me to ~200 | 7 | type-complexity-threshold = 250 |
| 8 | cognitive-complexity-threshold = 100 # TODO reduce me ALARA | 8 | cognitive-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" | |||
| 16 | match_arm_blocks = false | 16 | match_arm_blocks = false |
| 17 | match_arm_leading_pipes = "Always" | 17 | match_arm_leading_pipes = "Always" |
| 18 | match_block_trailing_comma = true | 18 | match_block_trailing_comma = true |
| 19 | max_width = 98 | 19 | max_width = 80 |
| 20 | newline_style = "Unix" | 20 | newline_style = "Unix" |
| 21 | normalize_comments = false | 21 | normalize_comments = false |
| 22 | overflow_delimited_expr = true | 22 | overflow_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 @@ | |||
| 1 | use std::{env, path::PathBuf}; | 1 | use std::{env::var, fs::read_to_string, path::PathBuf}; |
| 2 | 2 | ||
| 3 | use directories::ProjectDirs; | ||
| 3 | use serde::Deserialize; | 4 | use serde::Deserialize; |
| 4 | 5 | ||
| 5 | use crate::{constants::*, errors::McError}; | 6 | use crate::{constants::*, errors::McError}; |
| 6 | |||
| 7 | #[allow(dead_code)] | 7 | #[allow(dead_code)] |
| 8 | #[derive(Debug, Deserialize)] | 8 | #[derive(Debug, Deserialize)] |
| 9 | pub struct Config { | 9 | pub 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 | |||
| 22 | impl Config { | 21 | impl 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 | |||
| 66 | fn default_config_path() -> Result<PathBuf, McError> { | 61 | fn 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 @@ | |||
| 1 | pub 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 | ||
| 11 | pub mod loader; | ||
| 3 | pub use loader::Config; | 12 | pub 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 = | |||
| 8 | pub const DOWNLOAD_RETRIES: usize = 3; | 8 | pub const DOWNLOAD_RETRIES: usize = 3; |
| 9 | pub const DOWNLOAD_BACKOFF: Duration = Duration::from_millis(400); | 9 | pub const DOWNLOAD_BACKOFF: Duration = Duration::from_millis(400); |
| 10 | 10 | ||
| 11 | pub const DEFAULT_MAX_MEMORY_MB: u32 = 2048; | 11 | pub const DEFAULT_MAX_MEMORY_MB: u32 = 4048; |
| 12 | pub const DEFAULT_JAVA_PATH: &str = "java"; | 12 | pub const DEFAULT_JAVA_PATH: &str = "java"; |
| 13 | pub const DEFAULT_VERSION: &str = "latest"; | 13 | pub 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 @@ | |||
| 1 | use std::{fmt, io}; | 1 | use std::{fmt, io}; |
| 2 | 2 | ||
| 3 | use fmt::{Display, Formatter, Result}; | ||
| 4 | use 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)] |
| 5 | pub enum McError { | 13 | pub 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 | ||
| 16 | impl fmt::Display for McError { | 24 | impl 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 | ||
| 20 | impl From<io::Error> for McError { | 31 | impl 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 | ||
| 32 | impl From<zip::result::ZipError> for McError { | 43 | impl 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 | ||
| 9 | use clap::Parser; | 9 | use clap::Parser; |
| 10 | use config::Config; | 10 | use config::Config; |
| 11 | use dotenvy::dotenv; | ||
| 11 | use errors::McError; | 12 | use errors::McError; |
| 12 | use log::{debug, info}; | 13 | use log::{debug, info}; |
| 13 | 14 | ||
| 15 | use 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)] |
| 16 | struct Cli { | 22 | struct Cli { |
| @@ -26,7 +32,7 @@ struct Cli { | |||
| 26 | 32 | ||
| 27 | #[tokio::main] | 33 | #[tokio::main] |
| 28 | async fn main() -> Result<(), McError> { | 34 | async 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 @@ | |||
| 1 | use log::{debug, info}; | 1 | use log::{debug, info}; |
| 2 | use tokio::{fs, io::AsyncWriteExt}; | 2 | use reqwest::get; |
| 3 | use serde::Deserialize; | ||
| 4 | use tokio::{ | ||
| 5 | fs::{self, File, create_dir_all}, | ||
| 6 | io::AsyncWriteExt, | ||
| 7 | }; | ||
| 3 | 8 | ||
| 4 | use crate::{ | 9 | use 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)] |
| 17 | struct AssetObject { | ||
| 18 | hash: String, | ||
| 19 | size: u64, | ||
| 20 | } | ||
| 21 | |||
| 22 | #[derive(Debug, Deserialize)] | ||
| 23 | struct 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) |
| 14 | pub async fn download_all(config: &Config, version: &Version) -> Result<(), McError> { | 30 | /// - assets (w tym textures, sounds) |
| 31 | pub 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 | ||
| 20 | async fn download_client(config: &Config, version: &Version) -> Result<(), McError> { | 41 | async 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 | ||
| 33 | async fn download_libraries(config: &Config, libraries: &[Library]) -> Result<(), McError> { | 56 | async 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)?; | 89 | async 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() { | 129 | async 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 | 179 | async fn download_file( | |
| 54 | async 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 @@ | |||
| 1 | use std::{fs, io, path::Path}; | ||
| 2 | |||
| 1 | use log::info; | 3 | use log::info; |
| 4 | use zip::ZipArchive; | ||
| 5 | |||
| 6 | use crate::{ | ||
| 7 | errors::McError, | ||
| 8 | minecraft::manifests::{Library, Version}, | ||
| 9 | }; | ||
| 2 | 10 | ||
| 3 | use crate::errors::McError; | ||
| 4 | pub fn extract_natives( | 11 | pub 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 | |||
| 68 | fn 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 | |||
| 90 | fn 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 | ||
| 3 | use log::{debug, info}; | 3 | use log::{debug, info}; |
| 4 | 4 | ||
| 5 | use crate::{config::Config, errors::McError, minecraft::manifests::Version, platform::paths}; | 5 | use 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 |
| 8 | fn build_classpath(config: &Config, version: &Version) -> Result<String, McError> { | 13 | fn 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 |
| 25 | pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { | 38 | pub 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 | ||
| 109 | fn 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 | |||
| 3 | use std::collections::HashMap; | ||
| 4 | |||
| 2 | use reqwest; | 5 | use reqwest; |
| 3 | use serde::Deserialize; | 6 | use 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)] | ||
| 25 | pub 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)] |
| 31 | pub struct Library { | 45 | pub 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)] |
| 36 | pub struct LibraryDownloads { | 57 | pub 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 | ||
| 48 | pub async fn load_version(cfg: &crate::config::Config) -> Result<Version, McError> { | 72 | #[derive(Debug, Deserialize)] |
| 73 | pub struct Rule { | ||
| 74 | pub action: String, | ||
| 75 | pub os: Option<OsRule>, | ||
| 76 | } | ||
| 77 | |||
| 78 | #[derive(Debug, Deserialize)] | ||
| 79 | pub struct OsRule { | ||
| 80 | pub name: String, | ||
| 81 | } | ||
| 82 | |||
| 83 | pub 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 | |||
| 1 | pub mod downloads; | 16 | pub mod downloads; |
| 2 | pub mod extraction; | 17 | pub mod extraction; |
| 3 | pub mod launcher; | 18 | pub 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 | |||
| 1 | pub mod paths; | 11 | pub 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 @@ | |||
| 1 | use std::{fs, path::PathBuf}; | 1 | use std::{fs::create_dir_all, path::PathBuf}; |
| 2 | |||
| 3 | use directories::ProjectDirs; | ||
| 4 | 2 | ||
| 5 | use crate::{config::Config, errors::McError}; | 3 | use crate::{config::Config, errors::McError}; |
| 6 | 4 | ||
| 7 | fn project_dirs() -> ProjectDirs { | 5 | /// ~/.local/share/dml/minecraft |
| 8 | ProjectDirs::from("com", "dml", "dml").expect("failed to determine project directories") | 6 | pub fn minecraft_root(cfg: &Config) -> PathBuf { |
| 7 | cfg.data_dir.join("minecraft") | ||
| 9 | } | 8 | } |
| 10 | 9 | ||
| 11 | /// Root Minecraft directory | 10 | pub fn assets_dir(cfg: &Config) -> PathBuf { |
| 12 | pub fn minecraft_root(_cfg: &Config) -> PathBuf { project_dirs().data_dir().join("minecraft") } | 11 | minecraft_root(cfg).join("assets") |
| 13 | 12 | } | |
| 14 | /* ---------------- setup ---------------- */ | ||
| 15 | 13 | ||
| 14 | pub fn game_dir(cfg: &Config) -> PathBuf { minecraft_root(cfg) } | ||
| 16 | pub fn ensure_dirs(cfg: &Config) -> Result<(), McError> { | 15 | pub 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 | |||
| 28 | pub fn version_dir(cfg: &Config, version: &str) -> PathBuf { | 28 | pub 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 | ||
| 32 | pub fn client_jar(cfg: &Config, version: &str) -> Result<PathBuf, McError> { | 32 | pub 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 | |||
| 38 | pub fn library_file(cfg: &Config, rel_path: &str) -> Result<PathBuf, McError> { | 36 | pub 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 | |||
| 46 | pub fn natives_dir(cfg: &Config, version: &str) -> PathBuf { | 42 | pub 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 | ||
| 3 | use std::path::Path; | 3 | use std::path::Path; |
| 4 | 4 | ||
| 5 | use tokio::fs::remove_file; | ||
| 6 | |||
| 5 | use crate::errors::McError; | 7 | use crate::errors::McError; |
| 6 | 8 | ||
| 7 | pub async fn remove_if_exists(path: &Path) -> Result<(), McError> { | 9 | pub 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 | |||
| 1 | pub mod fs; | 13 | pub mod fs; |
| 2 | pub mod sha1; | 14 | pub 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 @@ | |||
| 3 | use std::path::Path; | 3 | use std::path::Path; |
| 4 | 4 | ||
| 5 | use sha1::{Digest, Sha1}; | 5 | use sha1::{Digest, Sha1}; |
| 6 | use tokio::fs::read; | ||
| 6 | 7 | ||
| 7 | use crate::errors::McError; | 8 | use crate::errors::McError; |
| 8 | 9 | ||
| 9 | pub async fn sha1_hex(path: &Path) -> Result<String, McError> { | 10 | pub 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())) |
