Skip to main content

thedes_settings/
lib.rs

1use std::{
2    borrow::Cow,
3    error::Error,
4    fs::File,
5    io::{BufReader, BufWriter},
6    path::{Path, PathBuf},
7};
8
9use serde::{Deserialize, Serialize};
10use thedes_tui_core::audio::Volume;
11use thiserror::Error;
12use tokio::{io, task};
13use tracing::Level;
14
15#[derive(Debug, Error)]
16pub enum LoadErrorSource {
17    #[error("I/O error happened")]
18    Io(#[from] io::Error),
19    #[error("Failed to deserialize")]
20    Deserialize(#[from] serde_json::Error),
21}
22
23#[derive(Debug, Error)]
24#[error("Failed to load game from {path}")]
25pub struct LoadError {
26    pub path: PathBuf,
27    #[source]
28    pub source: LoadErrorSource,
29}
30
31#[derive(Debug, Error)]
32pub enum SaveErrorSource {
33    #[error("I/O error happened")]
34    Io(#[from] io::Error),
35    #[error("Failed to serialize")]
36    Serialize(#[from] serde_json::Error),
37}
38
39#[derive(Debug, Error)]
40#[error("Failed to save game to {path}")]
41pub struct SaveError {
42    pub path: PathBuf,
43    #[source]
44    pub source: SaveErrorSource,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
48pub enum AudioSinkType {
49    Music,
50    Fx,
51}
52
53impl AudioSinkType {
54    pub const fn name(self) -> &'static str {
55        match self {
56            Self::Music => "Music",
57            Self::Fx => "FX",
58        }
59    }
60}
61
62impl From<AudioSinkType> for &'static str {
63    fn from(value: AudioSinkType) -> Self {
64        value.name()
65    }
66}
67
68impl From<AudioSinkType> for Cow<'static, str> {
69    fn from(value: AudioSinkType) -> Self {
70        <&'static str>::from(value).into()
71    }
72}
73
74#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75pub struct AudioSettings {
76    music: Volume,
77    fx: Volume,
78}
79
80impl Default for AudioSettings {
81    fn default() -> Self {
82        Self { music: 125, fx: 190 }
83    }
84}
85
86impl AudioSettings {
87    const VOLUME_STEP: u8 = 15;
88
89    pub fn volume(&self, controller_type: AudioSinkType) -> u8 {
90        match controller_type {
91            AudioSinkType::Music => self.music,
92            AudioSinkType::Fx => self.fx,
93        }
94    }
95
96    pub fn set_volume(&mut self, controller_type: AudioSinkType, value: u8) {
97        match controller_type {
98            AudioSinkType::Music => self.music = value,
99            AudioSinkType::Fx => self.fx = value,
100        }
101    }
102
103    pub fn increase_volume(&mut self, controller_type: AudioSinkType) {
104        self.set_volume(
105            controller_type,
106            self.volume(controller_type).saturating_add(Self::VOLUME_STEP),
107        );
108    }
109
110    pub fn decrease_volume(&mut self, controller_type: AudioSinkType) {
111        self.set_volume(
112            controller_type,
113            self.volume(controller_type).saturating_sub(Self::VOLUME_STEP),
114        );
115    }
116}
117
118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119pub struct Settings {
120    audio: AudioSettings,
121}
122
123impl Default for Settings {
124    fn default() -> Self {
125        Self { audio: AudioSettings::default() }
126    }
127}
128
129impl Settings {
130    pub async fn load(path: &Path) -> Result<Self, LoadError> {
131        if tracing::event_enabled!(Level::DEBUG) {
132            tracing::debug!(
133                path = path.display().to_string(),
134                "Loading settings"
135            );
136        }
137        task::block_in_place(|| {
138            let file =
139                File::open(&path).map_err(LoadErrorSource::from).map_err(
140                    |source| LoadError { path: path.to_owned(), source },
141                )?;
142            let mut file = BufReader::new(file);
143            serde_json::from_reader(&mut file)
144                .map_err(LoadErrorSource::from)
145                .map_err(|source| LoadError { path: path.to_owned(), source })
146        })
147    }
148
149    pub async fn load_or_default(path: &Path) -> Self {
150        match Self::load(path).await {
151            Ok(this) => this,
152            Err(e) => {
153                let mut chain = String::new();
154                let mut next = Some(&e as &(dyn Error + 'static));
155                while let Some(current) = next {
156                    chain.push_str(&current.to_string());
157                    chain.push_str("\n");
158                    next = current.source();
159                }
160                let path = path.display().to_string();
161                tracing::error!(
162                    %chain,
163                    %path,
164                    "Failed to load configuration",
165                );
166                Self::default()
167            },
168        }
169    }
170
171    pub async fn save(&self, path: &Path) -> Result<(), SaveError> {
172        if tracing::event_enabled!(Level::DEBUG) {
173            tracing::debug!(
174                path = path.display().to_string(),
175                "Saving settings"
176            );
177        }
178        task::block_in_place(|| {
179            let file =
180                File::create(&path).map_err(SaveErrorSource::from).map_err(
181                    |source| SaveError { path: path.to_owned(), source },
182                )?;
183            let mut file = BufWriter::new(file);
184            serde_json::to_writer(&mut file, self)
185                .map_err(SaveErrorSource::from)
186                .map_err(|source| SaveError {
187                    path: path.to_owned(),
188                    source,
189                })?;
190            Ok(())
191        })
192    }
193
194    pub fn audio(&self) -> &AudioSettings {
195        &self.audio
196    }
197
198    pub fn audio_mut(&mut self) -> &mut AudioSettings {
199        &mut self.audio
200    }
201}