use std::collections::HashMap; use std::error::Error; use std::ffi::{OsStr, OsString}; use std::fmt::{Display, Formatter}; use std::io::ErrorKind; use std::path::{Path, PathBuf}; use log::warn; use serde::{Deserialize, Serialize}; use tokio::{fs, io}; use tokio::fs::File; use tokio::io::AsyncWriteExt; use super::constants; #[derive(Debug, Clone, Serialize, Deserialize)] struct SettingsInner { profiles: HashMap, instances: HashMap } pub struct Settings { path: Option, inner: SettingsInner } #[derive(Debug)] pub enum SettingsError { IO { what: &'static str, error: io::Error }, Format(serde_json::Error), Inconsistent(String) } impl Display for SettingsError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { SettingsError::IO { what, error } => write!(f, "settings i/o error ({}): {}", what, error), SettingsError::Format(err) => write!(f, "settings format error: {}", err), SettingsError::Inconsistent(err) => write!(f, "inconsistent settings: {}", err), } } } impl Error for SettingsError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { SettingsError::IO { error: err, .. } => Some(err), SettingsError::Format(err) => Some(err), _ => None } } } impl Default for SettingsInner { fn default() -> Self { SettingsInner { instances: [(String::from(constants::DEF_INSTANCE_NAME), PathBuf::from(constants::DEF_INSTANCE_NAME).into())].into_iter().collect(), profiles: [(String::from(constants::DEF_PROFILE_NAME), Profile::new(constants::DEF_INSTANCE_NAME))].into_iter().collect() } } } impl Settings { async fn load_inner(path: impl AsRef) -> Result { match fs::read_to_string(&path).await { Ok(data) => serde_json::from_str(data.as_str()).map_err(SettingsError::Format), Err(e) if e.kind() == ErrorKind::NotFound => Ok(SettingsInner::default()), Err(e) => Err(SettingsError::IO { what: "loading settings", error: e }) } } fn check_consistent(mut inner: SettingsInner, path: Option>) -> Result { inner.profiles.retain(|name, profile| { if !inner.instances.contains_key(&profile.instance) { warn!("Settings inconsistency: profile {} refers to instance {}, which does not exist. Ignoring this profile.", name, profile.instance); false } else { true } }); // there will be more checks later maybe Ok(Settings { path: path.map(|p| p.as_ref().to_owned()), inner }) } pub async fn load(path: impl AsRef) -> Result { Self::check_consistent(Self::load_inner(&path).await?, Some(path)) } pub fn get_path(&self) -> Option<&Path> { self.path.as_ref().map(|p| p.as_path()) } pub async fn save_to(&self, path: impl AsRef) -> Result<(), SettingsError> { let path = path.as_ref(); if let Some(parent) = path.parent() { fs::create_dir_all(parent).await .map_err(|e| SettingsError::IO { what: "saving settings (creating directory)", error: e })?; } let mut file = File::create(path).await .map_err(|e| SettingsError::IO { what: "saving settings (open)", error: e })?; file.write_all(serde_json::to_string_pretty(&self.inner).map_err(SettingsError::Format)?.as_bytes()).await .map_err(|e| SettingsError::IO { what: "saving settings (write)", error: e })?; Ok(()) } pub async fn save(&self) -> Result<(), SettingsError> { self.save_to(self.path.as_ref().expect("save() called on Settings instance not loaded from file")).await } pub fn get_instance(&self, name: &str) -> Option<&Instance> { self.inner.instances.get(name) } pub fn get_profile(&self, name: &str) -> Option<&Profile> { self.inner.profiles.get(name) } pub fn get_instance_for(&self, profile: &Profile) -> &Instance { self.inner.instances.get(&profile.instance).unwrap() } } #[derive(Deserialize, Serialize, Debug, Clone)] pub struct Instance { path: PathBuf // relative to launcher home (or absolute) } #[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "snake_case")] pub enum ProfileVersion { LatestSnapshot, LatestRelease, #[serde(untagged)] Specific(String) } #[derive(Deserialize, Serialize, Debug, Clone, Copy)] pub struct Resolution { width: u32, height: u32 } impl Default for Resolution { fn default() -> Self { Resolution { width: 864, height: 480 } } } #[derive(Deserialize, Serialize, Debug, Clone)] pub struct Profile { game_version: ProfileVersion, java_runtime: Option, instance: String, #[serde(default)] jvm_arguments: Vec, #[serde(default)] legacy_launch: bool, resolution: Option } impl> From

for Instance { fn from(path: P) -> Self { Self { path: path.as_ref().into() } } } impl Instance { pub async fn get_path(&self, home: impl AsRef) -> Result { let path = self.path.as_path(); if path.is_relative() { Ok([home.as_ref(), Path::new("instances"), path].iter().collect::()) } else { fs::canonicalize(path).await } } } const DEF_JVM_ARGUMENTS: [&'static str; 7] = [ "-Xmx2G", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseG1GC", "-XX:G1NewSizePercent=20", "-XX:G1ReservePercent=20", "-XX:MaxGCPauseMillis=50", "-XX:G1HeapRegionSize=32M" ]; impl Profile { fn new(instance_name: &str) -> Self { Self { game_version: ProfileVersion::LatestRelease, java_runtime: None, instance: instance_name.into(), jvm_arguments: DEF_JVM_ARGUMENTS.iter().map(|s| String::from(*s)).collect(), legacy_launch: false, resolution: None } } pub fn get_version(&self) -> &ProfileVersion { &self.game_version } pub fn get_instance_name(&self) -> &str { &self.instance } pub fn iter_arguments(&self) -> impl Iterator { self.jvm_arguments.iter() } pub fn get_resolution(&self) -> Option { self.resolution } pub fn get_java_runtime(&self) -> Option<&String> { self.java_runtime.as_ref() } pub fn is_legacy_launch(&self) -> bool { self.legacy_launch } }