mod constants; mod version; mod profile; mod strsub; mod download; 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 const_format::formatcp; use futures::TryFutureExt; use log::{debug, warn}; use serde::{Deserialize, Serialize}; use sha1_smol::Sha1; use sysinfo::System; use tokio::fs::File; use tokio::{fs, io}; use tokio::io::AsyncReadExt; use version::VersionList; use profile::{Instance, Profile}; use crate::launcher::download::{Download, VerifiedDownload}; use crate::launcher::version::{VersionResolveError, VersionResult}; use crate::version::{DownloadInfo, Library, OSRestriction, OperatingSystem}; #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Settings { profiles: HashMap, instances: HashMap } #[derive(Debug)] enum SettingsLoadError { IO(io::Error), Format(serde_json::Error) } impl Display for SettingsLoadError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { SettingsLoadError::IO(err) => write!(f, "I/O error loading settings: {}", err), SettingsLoadError::Format(err) => write!(f, "settings format error: {}", err), } } } impl Error for SettingsLoadError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { SettingsLoadError::IO(err) => Some(err), SettingsLoadError::Format(err) => Some(err), } } } impl Settings { async fn load(path: impl AsRef) -> Result { let data = match fs::read_to_string(&path).await { Ok(data) => data, Err(e) => return match e.kind() { ErrorKind::NotFound => Ok(Settings::default()), _ => Err(SettingsLoadError::IO(e)) } }; serde_json::from_str(data.as_str()).map_err(SettingsLoadError::Format) } } struct SystemInfo { os: OperatingSystem, os_version: String, arch: String } 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; match tokio::fs::create_dir_all(&home).await { Ok(_) => (), Err(e) => match e.kind() { ErrorKind::AlreadyExists => (), _ => { warn!("Failed to create launcher home directory: {}", e); return Err(e.into()); } } } debug!("Version list online?: {online}"); 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 = Settings::load(&settings_path).await?; 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: make a list of all the required libraries 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 {} const ARCH_BITS: &'static str = formatcp!("{}", usize::BITS); impl LibraryRepository { fn lib_replace(key: &str) -> Option> { match key { "arch" => Some(Cow::Borrowed(ARCH_BITS)), _ => None } } 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(strsub::replace_thru(format!("{}-{}-{}.jar", n[1], n[2], classifier), Self::lib_replace))), 4 => Some(PathBuf::from(strsub::replace_thru(format!("{}-{}-{}-{}.jar", n[1], n[2], classifier, n[3]), Self::lib_replace))), _ => None } } else { match n.len() { 3 => Some(PathBuf::from(strsub::replace_thru(format!("{}-{}.jar", n[1], n[2]), Self::lib_replace))), 4 => Some(PathBuf::from(strsub::replace_thru(format!("{}-{}-{}.jar", n[1], n[2], n[3]), Self::lib_replace))), _ => 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) } fn create_download(&self, lib: &Library, os: OperatingSystem) -> Option { let classifier = lib.natives.as_ref()?.get(&os).map(|s| s.as_str()); if lib.url.is_some() || lib.downloads.is_none() { // TODO: derive download URL in this situation? warn!("BUG: Deprecated case for library {}: url present or downloads missing. The launcher does not support out-of-line checksums at this time. Not downloading this library.", lib.name); return None; } let dlinfo = lib.downloads.as_ref()?.get_download_info(classifier)?; // drinking game: take a shot once per heap allocation let path = self.home.join(dlinfo.path.as_ref().map(PathBuf::from).or_else(|| Self::get_artifact_path(lib.name.as_str(), classifier))?); Some(VerifiedDownload::new(dlinfo.url.as_ref()?, path.as_path(), dlinfo.size, dlinfo.sha1)) } } 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 } } 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)) } }