diff options
| author | 2025-01-18 23:47:48 -0600 | |
|---|---|---|
| committer | 2025-01-18 23:47:48 -0600 | |
| commit | cd8bf1667494070c3a22ab5d63b559a9742b8a1a (patch) | |
| tree | 6f93f0c0fbdccfa18733499845a8bc7c298c402f /src/launcher/settings.rs | |
| parent | building classpath (diff) | |
more stuff
Diffstat (limited to 'src/launcher/settings.rs')
| -rw-r--r-- | src/launcher/settings.rs | 184 |
1 files changed, 184 insertions, 0 deletions
diff --git a/src/launcher/settings.rs b/src/launcher/settings.rs new file mode 100644 index 0000000..5a96589 --- /dev/null +++ b/src/launcher/settings.rs @@ -0,0 +1,184 @@ +use std::collections::HashMap; +use std::error::Error; +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<String, Profile>, + instances: HashMap<String, Instance> +} + +pub struct Settings { + path: Option<PathBuf>, + 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<Path>) -> Result<SettingsInner, SettingsError> { + 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<impl AsRef<Path>>) -> Result<Settings, SettingsError> { + 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<Path>) -> Result<Settings, SettingsError> { + 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<Path>) -> 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)] +pub struct Profile { + game_version: ProfileVersion, + java_runtime: Option<String>, + instance: String // ugly that this is a string instead of reference to an Instance but whatever I'm lazy +} + +impl<P: AsRef<Path>> From<P> for Instance { + fn from(path: P) -> Self { + Self { path: path.as_ref().into() } + } +} + +impl Instance { + pub async fn get_path(&self, home: impl AsRef<Path>) -> Result<PathBuf, io::Error> { + let path = self.path.as_path(); + + if path.is_relative() { + Ok([home.as_ref(), Path::new("instances"), path].iter().collect::<PathBuf>()) + } else { + fs::canonicalize(path).await + } + } +} + +impl Profile { + fn new(instance_name: &str) -> Self { + Self { + game_version: ProfileVersion::LatestRelease, + java_runtime: None, + instance: instance_name.into() + } + } + + pub fn get_version(&self) -> &ProfileVersion { + &self.game_version + } + + pub fn get_instance_name(&self) -> &str { + &self.instance + } +} |
