mod constants; mod version; mod profile; mod strsub; mod download; mod request; use std::borrow::Cow; use std::collections::HashMap; use std::env::consts::{ARCH, OS}; use std::error::Error; use std::fmt::{Display, Formatter}; use std::io::ErrorKind; use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use sha1_smol::Sha1; use sysinfo::System; use tokio::fs::File; use tokio::io::AsyncReadExt; use version::VersionList; use profile::{Instance, Profile}; use crate::launcher::version::{VersionResolveError, VersionResult}; use crate::version::{Library, OSRestriction, OperatingSystem}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Settings { profiles: HashMap, instances: HashMap } struct SystemInfo { os: OperatingSystem, os_version: String, arch: String, arch_bits: usize } struct LibraryRepository { home: PathBuf } pub struct Launcher { online: bool, home: PathBuf, versions: VersionList, settings_path: PathBuf, // maybe redundant but idc >:3 settings: Settings, system_info: SystemInfo, libraries: LibraryRepository } #[derive(Debug)] pub enum LaunchError { UnknownVersion(String), LoadVersion(Box), ResolveVersion(VersionResolveError) } impl Display for LaunchError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match &self { LaunchError::UnknownVersion(id) => write!(f, "unknown version id: {id}"), LaunchError::LoadVersion(e) => write!(f, "error loading remote version: {e}"), LaunchError::ResolveVersion(e) => write!(f, "error resolving remote version: {e}") } } } impl Error for LaunchError { fn cause(&self) -> Option<&dyn Error> { match &self { LaunchError::LoadVersion(e) => Some(e.as_ref()), LaunchError::ResolveVersion(e) => Some(e), _ => None } } } impl Launcher { // FIXME: more descriptive error type por favor pub async fn new(home: &Path, online: bool) -> Result> { let home = home.to_owned(); let versions_home = home.join("versions"); let versions; if online { versions = VersionList::online(versions_home.as_ref()).await?; } else { versions = VersionList::offline(versions_home.as_ref()).await?; } let settings_path = home.join("ozone.json"); let settings = serde_json::from_str(tokio::fs::read_to_string(&settings_path).await?.as_str())?; Ok(Launcher { online, home: home.to_owned(), versions, settings_path, settings, system_info: SystemInfo::new(), libraries: LibraryRepository { home: home.join("libraries") } }) } pub async fn prepare_launch(&self, profile: &Profile) -> Result<(), LaunchError> { /* tasks 2 l;aunch the gayme!!!! :3 * - java runtime * - normal process (good research, past figboot :3) * - libraries * - check which libraries we actually need (some have classifiers that don't apply to us) * - of the libraries we need, check which have correct size and sha1 * - redownload necessary libraries * - (if offline mode and there are libraries to download, then explode violently) * - extract natives * - logging * - download the config if present and necessary * - (explode if offline mode and we need to download stuff) * - assets * - get asset index (check if our local copy is good and redownload if not) * - check what ones are good and what needs to be downloaded * - download them * - (if offline mode, explode) * - if virtual or resource-mapped, copy (or perhaps hardlink? that would be cool) * - the actual client jar * - check integriddy and download if needed * - (explode if offline mode) * - launch the game * - build argument list and whatnot also */ let ver_res = self.versions.get_version_lazy(&profile.version_id); let ver = match ver_res { VersionResult::Remote(mv) => Cow::Owned(self.versions.load_remote_version(mv).await.map_err(|e| LaunchError::LoadVersion(e))?), VersionResult::Complete(cv) => Cow::Borrowed(cv), VersionResult::None => { return Err(LaunchError::UnknownVersion(profile.version_id.clone()).into()) } }; let ver = self.versions.resolve_version(ver.as_ref()).await.map_err(|e| LaunchError::ResolveVersion(e))?; todo!() } } #[derive(Debug, Clone)] enum LibraryError { InvalidName(String), IOError(ErrorKind) } impl Display for LibraryError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { LibraryError::InvalidName(name) => write!(f, "invalid name: {name}"), LibraryError::IOError(e) => write!(f, "io error reading library: {e}"), } } } impl Error for LibraryError {} impl LibraryRepository { fn get_artifact_base_dir(name: &str) -> Option { let end_of_gid = name.find(':')?; Some(name[..end_of_gid].split('.').chain(name.split(':').skip(1).take(2)).collect()) } fn get_artifact_filename(name: &str, classifier: Option<&str>) -> Option { let n: Vec<&str> = name.splitn(4, ':').skip(1).collect(); if let Some(classifier) = classifier { match n.len() { 3 => Some(PathBuf::from(format!("{}-{}-{}.jar", n[1], n[2], classifier))), 4 => Some(PathBuf::from(format!("{}-{}-{}-{}.jar", n[1], n[2], classifier, n[3]))), _ => None } } else { match n.len() { 3 => Some(PathBuf::from(format!("{}-{}.jar", n[1], n[2]))), 4 => Some(PathBuf::from(format!("{}-{}-{}.jar", n[1], n[2], n[3]))), _ => None } } } fn get_artifact_path(name: &str, classifier: Option<&str>) -> Option { let Some(mut p) = Self::get_artifact_base_dir(name) else { return None; }; p.push(Self::get_artifact_filename(name, classifier)?); Some(p) } async fn should_redownload(&self, lib: &Library, classifier: Option<&str>) -> Result { let path = Self::get_artifact_path(lib.name.as_str(), classifier) .map_or_else(|| Err(LibraryError::InvalidName(lib.name.clone())), |p| Ok(p))?; let mut f = match File::open(path).await { Ok(f) => f, Err(e) => return match e.kind() { ErrorKind::NotFound => Ok(true), e => Err(LibraryError::IOError(e)) } }; let mut data = [0u8; 4096]; let mut sha1 = Sha1::new(); loop { let n = match f.read(&mut data).await { Ok(n) => n, Err(e) => return match e.kind() { ErrorKind::Interrupted => continue, kind => Err(LibraryError::IOError(kind)) } }; if n == 0 { break; // we reached the end of the file } sha1.update(&data[..n]); } todo!() } } impl SystemInfo { fn new() -> SystemInfo { let os = match OS { "windows" => OperatingSystem::Windows, "macos" => OperatingSystem::MacOS, "linux" => OperatingSystem::Linux, _ => OperatingSystem::Unknown // could probably consider "hurd" and "*bsd" to be linux... }; let mut os_version = System::os_version().unwrap_or_default(); if os == OperatingSystem::Windows && (os_version.starts_with("10") || os_version.starts_with("11")) { os_version.replace_range(..2, "10.0"); // minecraft expects this funny business... } let mut arch = ARCH.to_owned(); if arch == "x86_64" { // this nomenclature is preferred, since some versions expect the arch containing "x86" to mean 32-bit. arch.replace_range(.., "amd64"); } SystemInfo { os, os_version, arch, arch_bits: size_of::<*const i32>() * 8 } } fn is_our_os(&self, os: OperatingSystem) -> bool { if self.os == OperatingSystem::Unknown { return false; } self.os == os } fn applies(&self, restriction: &OSRestriction) -> bool { restriction.os.is_none_or(|os| self.is_our_os(os)) && restriction.version.as_deref().is_none_or(|pat| pat.is_match(&self.os_version)) && restriction.arch.as_deref().is_none_or(|pat| pat.is_match(&self.arch)) } }