From a393e0a2f2c3678a3ea869dc1417fa269f2b1040 Mon Sep 17 00:00:00 2001 From: Filip Wandzio Date: Sat, 24 Jan 2026 08:29:14 +0100 Subject: 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 --- .gitignore | 1 + clippy.toml | 8 +-- rustfmt.toml | 2 +- src/config/loader.rs | 23 +++--- src/config/mod.rs | 11 ++- src/constants.rs | 2 +- src/errors.rs | 23 ++++-- src/main.rs | 16 +++-- src/minecraft/downloads.rs | 172 ++++++++++++++++++++++++++++++++++++++------ src/minecraft/extraction.rs | 116 ++++++++++++++++++++++++++++-- src/minecraft/launcher.rs | 91 ++++++++++++++++++----- src/minecraft/manifests.rs | 43 ++++++++++- src/minecraft/mod.rs | 15 ++++ src/platform/mod.rs | 10 +++ src/platform/paths.rs | 36 +++++----- src/util/fs.rs | 4 +- src/util/mod.rs | 12 ++++ src/util/sha1.rs | 3 +- 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 **/*.rs.bk *.pdb rust-project.json +*.png diff --git a/clippy.toml b/clippy.toml index f688e0a..6b15c5a 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,8 +1,8 @@ stack-size-threshold = 393216 future-size-threshold = 24576 array-size-threshold = 4096 -large-error-threshold = 256 # TODO reduce me ALARA -too-many-lines-threshold = 100 # TODO reduce me to <= 100 +large-error-threshold = 256 +too-many-lines-threshold = 100 excessive-nesting-threshold = 3 -type-complexity-threshold = 250 # reduce me to ~200 -cognitive-complexity-threshold = 100 # TODO reduce me ALARA +type-complexity-threshold = 250 +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" match_arm_blocks = false match_arm_leading_pipes = "Always" match_block_trailing_comma = true -max_width = 98 +max_width = 80 newline_style = "Unix" normalize_comments = false 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 @@ -use std::{env, path::PathBuf}; +use std::{env::var, fs::read_to_string, path::PathBuf}; +use directories::ProjectDirs; use serde::Deserialize; use crate::{constants::*, errors::McError}; - #[allow(dead_code)] #[derive(Debug, Deserialize)] pub struct Config { @@ -18,37 +18,33 @@ pub struct Config { #[serde(default)] pub jvm_args: Vec, } - impl Config { pub fn load() -> Result { let cfg_path = default_config_path()?; let mut cfg: Config = if cfg_path.exists() { - let txt = std::fs::read_to_string(&cfg_path)?; + let txt = read_to_string(&cfg_path)?; toml::from_str(&txt).map_err(|e| McError::Config(e.to_string()))? } else { Self::default() }; - - if let Ok(v) = env::var("MC_USERNAME") { + if let Ok(v) = var("MC_USERNAME") { cfg.username = v; } - if let Ok(v) = env::var("MC_VERSION") { + if let Ok(v) = var("MC_VERSION") { cfg.version = v; } - if let Ok(v) = env::var("MC_JAVA_PATH") { + if let Ok(v) = var("MC_JAVA_PATH") { cfg.java_path = v; } - if let Ok(v) = env::var("MC_MAX_MEMORY_MB") { + if let Ok(v) = var("MC_MAX_MEMORY_MB") { cfg.max_memory_mb = v.parse().unwrap_or(cfg.max_memory_mb); } - Ok(cfg) } fn default() -> Self { let base = - directories::ProjectDirs::from("com", "example", "mccl").expect("platform dirs"); - + ProjectDirs::from("com", "example", "dml").expect("platform dirs"); Self { username: "Player".into(), uuid: uuid::Uuid::new_v4().to_string(), @@ -62,9 +58,8 @@ impl Config { } } } - fn default_config_path() -> Result { - let base = directories::ProjectDirs::from("com", "example", "mccl") + let base = ProjectDirs::from("com", "example", "dml") .ok_or_else(|| McError::Config("cannot determine config dir".into()))?; Ok(base.config_dir().join("config.toml")) } 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 @@ -pub mod loader; +//! Configuration module for the DML launcher. +//! +//! This module contains submodules and helpers for loading, parsing, and +//! managing configuration files and settings. It abstracts the details +//! of where configuration is stored and how it is represented in memory. +//! +//! # Submodules +//! - `loader`: Functions to load and parse configuration from disk or +//! environment variables. +pub mod loader; 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 = pub const DOWNLOAD_RETRIES: usize = 3; pub const DOWNLOAD_BACKOFF: Duration = Duration::from_millis(400); -pub const DEFAULT_MAX_MEMORY_MB: u32 = 2048; +pub const DEFAULT_MAX_MEMORY_MB: u32 = 4048; pub const DEFAULT_JAVA_PATH: &str = "java"; 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 @@ use std::{fmt, io}; +use fmt::{Display, Formatter, Result}; +use zip::result::ZipError; + +/// Represents all possible errors that can occur in the DML launcher. +/// +/// This enum centralizes error handling for the entire application, +/// wrapping various underlying error types from I/O, HTTP requests, +/// JSON parsing, ZIP extraction, configuration issues, and runtime errors. #[allow(dead_code)] #[derive(Debug)] pub enum McError { Io(io::Error), Http(reqwest::Error), Json(serde_json::Error), - Zip(zip::result::ZipError), + Zip(ZipError), Config(String), ShaMismatch(String), Process(String), - Runtime(String), // ← NEW + Runtime(String), } -impl fmt::Display for McError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?}", self) } +impl Display for McError { + /// Formats the error for user-friendly display. + /// + /// Currently, it uses the `Debug` format for simplicity. + fn fmt(&self, f: &mut Formatter<'_>) -> Result { write!(f, "{:?}", self) } } impl From for McError { @@ -29,6 +40,6 @@ impl From for McError { fn from(e: serde_json::Error) -> Self { Self::Json(e) } } -impl From for McError { - fn from(e: zip::result::ZipError) -> Self { Self::Zip(e) } +impl From for McError { + fn from(e: ZipError) -> Self { Self::Zip(e) } } 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; use clap::Parser; use config::Config; +use dotenvy::dotenv; use errors::McError; use log::{debug, info}; +use crate::minecraft::{ + downloads::download_all, extraction::extract_natives, launcher::launch, + manifests, +}; + #[derive(Parser, Debug)] #[command(author, about, disable_version_flag = true)] struct Cli { @@ -26,7 +32,7 @@ struct Cli { #[tokio::main] async fn main() -> Result<(), McError> { - dotenvy::dotenv().ok(); + dotenv().ok(); env_logger::init(); let cli = Cli::parse(); @@ -48,13 +54,13 @@ async fn main() -> Result<(), McError> { platform::paths::ensure_dirs(&config)?; info!("Using Minecraft version {}", config.version); - let version = minecraft::manifests::load_version(&config).await?; + let version = manifests::load_version(&config).await?; info!("Loaded version manifest for: {}", version.id); debug!("Main class: {}", version.main_class); - minecraft::downloads::download_all(&config, &version).await?; - minecraft::extraction::extract_natives(&config, &version)?; - minecraft::launcher::launch(&config, &version)?; + download_all(&config, &version).await?; + extract_natives(&config, &version)?; + launch(&config, &version)?; Ok(()) } 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 @@ use log::{debug, info}; -use tokio::{fs, io::AsyncWriteExt}; +use reqwest::get; +use serde::Deserialize; +use tokio::{ + fs::{self, File, create_dir_all}, + io::AsyncWriteExt, +}; use crate::{ config::Config, @@ -8,58 +13,181 @@ use crate::{ platform::paths, }; -/// Download everything required to launch: +#[derive(Debug, Deserialize)] +struct AssetObject { + hash: String, + size: u64, +} + +#[derive(Debug, Deserialize)] +struct AssetIndexManifest { + objects: std::collections::HashMap, +} + +/// Pobiera wszystko potrzebne do uruchomienia Minecraft: /// - client jar -/// - libraries -pub async fn download_all(config: &Config, version: &Version) -> Result<(), McError> { +/// - biblioteki (artifact + natives) +/// - assets (w tym textures, sounds) +pub async fn download_all( + config: &Config, + version: &Version, +) -> Result<(), McError> { download_client(config, version).await?; download_libraries(config, &version.libraries).await?; + download_assets(config, version).await?; Ok(()) } -async fn download_client(config: &Config, version: &Version) -> Result<(), McError> { +async fn download_client( + config: &Config, + version: &Version, +) -> Result<(), McError> { let jar_path = paths::client_jar(config, &version.id)?; if jar_path.exists() { - debug!("Client jar already exists"); + debug!("Client jar already exists: {}", jar_path.display()); return Ok(()); } info!("Downloading client {}", version.id); - download_file(&version.downloads.client.url, &jar_path).await } -async fn download_libraries(config: &Config, libraries: &[Library]) -> Result<(), McError> { - for lib in libraries { - let Some(artifact) = &lib.downloads.artifact else { - continue; - }; +async fn download_libraries( + config: &Config, + libraries: &[Library], +) -> Result<(), McError> { + for library in libraries { + // ===== CLASSPATH LIBRARIES ===== + if let Some(artifact) = &library.downloads.artifact { + let library_path = paths::library_file(config, &artifact.path)?; + + if !library_path.exists() { + info!("Downloading library {}", artifact.path); + download_file(&artifact.url, &library_path).await?; + } + } + + // ===== NATIVES ===== + if let Some(classifiers) = &library.downloads.classifiers { + for (_, native) in classifiers { + let native_path = paths::library_file(config, &native.path)?; + + if native_path.exists() { + continue; + } + + info!("Downloading native library {}", native.path); + download_file(&native.url, &native_path).await?; + } + } + } + + Ok(()) +} - let lib_path = paths::library_file(config, &artifact.path)?; +async fn download_asset_index( + config: &Config, + version: &Version, +) -> Result { + let assets_dir = paths::assets_dir(config); + create_dir_all(assets_dir.join("indexes")).await?; + + let asset_index = version.asset_index.as_ref().ok_or_else(|| { + McError::Config("Missing asset_index in version.json".into()) + })?; + + // Nie pozwalamy na legacy dla nowoczesnych wersji + if asset_index.id == "legacy" { + return Err(McError::Config( + "Legacy assetIndex detected – pobierz właściwy version.json".into(), + )); + } + + let index_path = assets_dir + .join("indexes") + .join(format!("{}.json", asset_index.id)); + + // Jeśli indeks istnieje lokalnie + if index_path.exists() { + let index_data = fs::read_to_string(&index_path).await?; + let manifest: AssetIndexManifest = serde_json::from_str(&index_data)?; + return Ok(manifest); + } + + // Pobierz indeks z sieci + info!("Downloading asset index {}", asset_index.id); + let response = get(&asset_index.url).await?; + let manifest_text = response.text().await?; + + fs::write(&index_path, &manifest_text).await?; + + let manifest: AssetIndexManifest = serde_json::from_str(&manifest_text)?; + Ok(manifest) +} - if lib_path.exists() { +async fn download_assets( + config: &Config, + version: &Version, +) -> Result<(), McError> { + let assets_dir = paths::assets_dir(config); + + // Katalogi MUSZĄ istnieć + create_dir_all(assets_dir.join("objects")).await?; + create_dir_all(assets_dir.join("indexes")).await?; + + let manifest = download_asset_index(config, version).await?; + + // Pobieramy wszystkie obiekty + for (logical_path, asset) in &manifest.objects { + let subdir = &asset.hash[0..2]; + let file_path = assets_dir + .join("objects") + .join(subdir) + .join(&asset.hash); + + if file_path.exists() { continue; } - info!("Downloading library {}", artifact.path); - download_file(&artifact.url, &lib_path).await?; + let url = format!( + "https://resources.download.minecraft.net/{}/{}", + subdir, asset.hash + ); + info!("Downloading asset {} -> {}", logical_path, file_path.display()); + download_file(&url, &file_path).await?; + } + + // Pobierz sounds.json jeśli istnieje + if let Some(asset) = manifest.objects.get("sounds.json") { + let file_path = assets_dir.join("indexes").join("sounds.json"); + if !file_path.exists() { + let subdir = &asset.hash[0..2]; + let url = format!( + "https://resources.download.minecraft.net/{}/{}", + subdir, asset.hash + ); + info!("Downloading sounds.json"); + download_file(&url, &file_path).await?; + } } Ok(()) } -/* ---------------- helper ---------------- */ - -async fn download_file(url: &str, path: &std::path::Path) -> Result<(), McError> { +/// Helper do pobierania plików +async fn download_file( + url: &str, + path: &std::path::Path, +) -> Result<(), McError> { if let Some(parent) = path.parent() { - fs::create_dir_all(parent).await?; + create_dir_all(parent).await?; } - let response = reqwest::get(url).await?; + let response = get(url).await?; let bytes = response.bytes().await?; - let mut file = fs::File::create(path).await?; + let mut file = File::create(path).await?; file.write_all(&bytes).await?; 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 @@ +use std::{fs, io, path::Path}; + use log::info; +use zip::ZipArchive; + +use crate::{ + errors::McError, + minecraft::manifests::{Library, Version}, +}; -use crate::errors::McError; pub fn extract_natives( - _cfg: &crate::config::Config, - version: &crate::minecraft::manifests::Version, + cfg: &crate::config::Config, + version: &Version, ) -> Result<(), McError> { - info!("Extracting natives for {}", version.id); + let natives_dir = cfg + .data_dir + .join("minecraft") + .join("versions") + .join(&version.id) + .join("natives"); + + info!("Extracting natives for {} into {:?}", version.id, natives_dir); + + if natives_dir.exists() { + fs::remove_dir_all(&natives_dir)?; + } + fs::create_dir_all(&natives_dir)?; + + for lib in &version.libraries { + if !library_allowed(lib) { + continue; + } + + let natives = match &lib.natives { + | Some(n) => n, + | None => continue, + }; + + let classifier = match natives.get("linux") { + | Some(c) => c, + | None => continue, + }; + + let classifiers = match &lib.downloads.classifiers { + | Some(c) => c, + | None => continue, + }; + + let artifact = match classifiers.get(classifier) { + | Some(a) => a, + | None => continue, + }; + + let jar_path = cfg + .data_dir + .join("minecraft") + .join("libraries") + .join(&artifact.path); + + info!("Extracting natives from {:?}", jar_path); + + extract_zip(&jar_path, &natives_dir)?; + } + + Ok(()) +} + +fn library_allowed(lib: &Library) -> bool { + let rules = match &lib.rules { + | Some(r) => r, + | None => return true, + }; + + let mut allowed = false; + + for rule in rules { + let os_match = match &rule.os { + | Some(os) => os.name == "linux", + | None => true, + }; + + if os_match { + allowed = rule.action == "allow"; + } + } + + allowed +} + +fn extract_zip(jar_path: &Path, out_dir: &Path) -> Result<(), McError> { + let file = fs::File::open(jar_path)?; + let mut zip = ZipArchive::new(file)?; + + for i in 0..zip.len() { + let mut entry = zip.by_index(i)?; + let name = entry.name(); + + if name.starts_with("META-INF/") { + continue; + } + + let out_path = out_dir.join(name); + + if entry.is_dir() { + fs::create_dir_all(&out_path)?; + continue; + } + + if let Some(parent) = out_path.parent() { + fs::create_dir_all(parent)?; + } + + let mut out_file = fs::File::create(&out_path)?; + io::copy(&mut entry, &mut out_file)?; + } + Ok(()) } 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; use log::{debug, info}; -use crate::{config::Config, errors::McError, minecraft::manifests::Version, platform::paths}; +use crate::{ + config::Config, + errors::McError, + minecraft::manifests::{Library, Version}, + platform::paths, +}; -/// Build the full classpath -fn build_classpath(config: &Config, version: &Version) -> Result { +/// Buduje classpath dla danej wersji Minecrafta +fn build_classpath( + config: &Config, + version: &Version, +) -> Result { let sep = if cfg!(windows) { ";" } else { ":" }; let mut entries = Vec::new(); - for lib in &version.libraries { - if let Some(artifact) = &lib.downloads.artifact { + for library in &version.libraries { + if !library_allowed(library) { + continue; + } + if let Some(artifact) = &library.downloads.artifact { let path = paths::library_file(config, &artifact.path)?; entries.push(path.to_string_lossy().to_string()); } } + // client.jar zawsze na końcu classpath let client_jar = paths::client_jar(config, &version.id)?; entries.push(client_jar.to_string_lossy().to_string()); + Ok(entries.join(sep)) } -/// Launch Minecraft +/// Uruchamia Minecraft pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { let java = &config.java_path; let classpath = build_classpath(config, version)?; @@ -34,26 +47,46 @@ pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { ))); } + let asset_index_id = version + .asset_index + .as_ref() + .ok_or_else(|| { + McError::Runtime("Missing assetIndex in version.json".into()) + })? + .id + .clone(); + info!("Launching Minecraft {}", version.id); debug!("Classpath: {}", classpath); debug!("Natives: {}", natives_dir.display()); + debug!("Asset index: {}", asset_index_id); + + let mut cmd = Command::new(java); + + // ===== JVM ARGUMENTS (muszą być na początku) ===== + cmd.arg(format!("-Xmx{}M", config.max_memory_mb)) + .arg(format!("-Djava.library.path={}", natives_dir.display())); - let status = Command::new(java) - .arg(format!("-Xmx{}M", config.max_memory_mb)) - .arg(format!("-Djava.library.path={}", natives_dir.display())) - .arg("-cp") + for arg in &config.jvm_args { + cmd.arg(arg); + } + + // ===== CLASSPATH + MAIN CLASS ===== + cmd.arg("-cp") .arg(classpath) - .arg(&version.main_class) - .arg("--username") + .arg(&version.main_class); + + // ===== ARGUMENTY GRY ===== + cmd.arg("--username") .arg(&config.username) .arg("--version") .arg(&version.id) .arg("--gameDir") - .arg(paths::minecraft_root(config)) + .arg(paths::game_dir(config)) .arg("--assetsDir") - .arg(paths::minecraft_root(config).join("assets")) + .arg(paths::assets_dir(config)) .arg("--assetIndex") - .arg(&version.id) + .arg(&asset_index_id) .arg("--uuid") .arg(&config.uuid) .arg("--userProperties") @@ -61,9 +94,9 @@ pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { .arg("--accessToken") .arg("0") .arg("--userType") - .arg("legacy") - .args(&config.jvm_args) - .status()?; + .arg("legacy"); // legacy dla starych kont, można później zmienić + + let status = cmd.status()?; if !status.success() { return Err(McError::Process("Minecraft exited with error".into())); @@ -71,3 +104,25 @@ pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { Ok(()) } + +/// Sprawdza reguły bibliotek tak jak robi Mojang +fn library_allowed(lib: &Library) -> bool { + let rules = match &lib.rules { + | Some(r) => r, + | None => return true, + }; + + let mut allowed = false; + + for rule in rules { + let os_match = match &rule.os { + | Some(os) => os.name == "linux", + | None => true, + }; + if os_match { + allowed = rule.action == "allow"; + } + } + + allowed +} 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 @@ #![allow(dead_code)] + +use std::collections::HashMap; + use reqwest; use serde::Deserialize; @@ -13,6 +16,17 @@ pub struct Version { pub downloads: Downloads, pub libraries: Vec, + + #[serde(rename = "assetIndex")] + pub asset_index: Option, +} + +#[derive(Debug, Deserialize)] +pub struct AssetIndex { + pub id: String, + pub sha1: String, + pub size: u64, + pub url: String, } #[derive(Debug, Deserialize)] @@ -29,12 +43,22 @@ pub struct DownloadInfo { #[derive(Debug, Deserialize)] pub struct Library { + pub name: Option, pub downloads: LibraryDownloads, + + #[serde(default)] + pub natives: Option>, + + #[serde(default)] + pub rules: Option>, } #[derive(Debug, Deserialize)] pub struct LibraryDownloads { pub artifact: Option, + + #[serde(default)] + pub classifiers: Option>, } #[derive(Debug, Deserialize)] @@ -45,7 +69,20 @@ pub struct LibraryArtifact { pub size: u64, } -pub async fn load_version(cfg: &crate::config::Config) -> Result { +#[derive(Debug, Deserialize)] +pub struct Rule { + pub action: String, + pub os: Option, +} + +#[derive(Debug, Deserialize)] +pub struct OsRule { + pub name: String, +} + +pub async fn load_version( + cfg: &crate::config::Config, +) -> Result { let manifest_text = reqwest::get(VERSION_MANIFEST_URL) .await? .text() @@ -67,7 +104,9 @@ pub async fn load_version(cfg: &crate::config::Config) -> Result ProjectDirs { - ProjectDirs::from("com", "dml", "dml").expect("failed to determine project directories") +/// ~/.local/share/dml/minecraft +pub fn minecraft_root(cfg: &Config) -> PathBuf { + cfg.data_dir.join("minecraft") } -/// Root Minecraft directory -pub fn minecraft_root(_cfg: &Config) -> PathBuf { project_dirs().data_dir().join("minecraft") } - -/* ---------------- setup ---------------- */ +pub fn assets_dir(cfg: &Config) -> PathBuf { + minecraft_root(cfg).join("assets") +} +pub fn game_dir(cfg: &Config) -> PathBuf { minecraft_root(cfg) } pub fn ensure_dirs(cfg: &Config) -> Result<(), McError> { let root = minecraft_root(cfg); - - fs::create_dir_all(root.join("versions"))?; - fs::create_dir_all(root.join("libraries"))?; - fs::create_dir_all(root.join("assets"))?; + create_dir_all(&root)?; + create_dir_all(root.join("versions"))?; + create_dir_all(root.join("libraries"))?; + create_dir_all(assets_dir(cfg))?; + create_dir_all(assets_dir(cfg).join("indexes"))?; + create_dir_all(assets_dir(cfg).join("objects"))?; + create_dir_all(root.join("saves"))?; Ok(()) } -/* ---------------- versions ---------------- */ - pub fn version_dir(cfg: &Config, version: &str) -> PathBuf { minecraft_root(cfg).join("versions").join(version) } pub fn client_jar(cfg: &Config, version: &str) -> Result { - Ok(version_dir(cfg, version).join(format!("{}.jar", version))) + Ok(version_dir(cfg, version).join(format!("{version}.jar"))) } -/* ---------------- libraries ---------------- */ - pub fn library_file(cfg: &Config, rel_path: &str) -> Result { Ok(minecraft_root(cfg) .join("libraries") .join(rel_path)) } -/* ---------------- natives ---------------- */ - pub fn natives_dir(cfg: &Config, version: &str) -> PathBuf { version_dir(cfg, version).join("natives") } 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 @@ use std::path::Path; +use tokio::fs::remove_file; + use crate::errors::McError; pub async fn remove_if_exists(path: &Path) -> Result<(), McError> { if path.exists() { - tokio::fs::remove_file(path).await?; + remove_file(path).await?; } Ok(()) } 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 @@ +//! Utility module for the DML launcher. +//! +//! This module contains general-purpose helper functions used throughout +//! the project. It is designed to provide reusable functionality without +//! being specific to Minecraft or configuration. +//! +//! # Submodules +//! - `fs`: File system utilities such as safe file creation, reading, and +//! writing. +//! - `sha1`: Functions to compute SHA-1 hashes for files and data integrity +//! checks. + pub mod fs; 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 @@ use std::path::Path; use sha1::{Digest, Sha1}; +use tokio::fs::read; use crate::errors::McError; pub async fn sha1_hex(path: &Path) -> Result { - let data = tokio::fs::read(path).await?; + let data = read(path).await?; let mut hasher = Sha1::new(); hasher.update(&data); Ok(format!("{:x}", hasher.finalize())) -- cgit v1.2.3