summaryrefslogtreecommitdiffstats
path: root/src/launcher/settings.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/launcher/settings.rs')
-rw-r--r--src/launcher/settings.rs184
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
+ }
+}