1use crate::prelude::*;
13use core::fmt::Debug;
14
15use shuyi_assets::keymap::Layout;
16
17use crate::frame_cap::{self, FrameCap};
18
19pub const SETTINGS_KEY: &str = "settings";
22
23pub const MAX_VOLUME: u8 = 100;
25
26pub const DEFAULT_VOLUME: u8 = 80;
28
29#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
31pub enum TextSpeed {
32 Slow,
34 #[default]
36 Normal,
37 Fast,
39 Instant,
41}
42
43impl TextSpeed {
44 pub const ALL: [Self; 4] = [Self::Slow, Self::Normal, Self::Fast, Self::Instant];
46
47 pub const fn chars_per_s(self) -> Option<f32> {
50 match self {
51 Self::Slow => Some(30.0),
52 Self::Normal => Some(60.0),
53 Self::Fast => Some(120.0),
54 Self::Instant => None,
55 }
56 }
57
58 pub const fn key(self) -> &'static str {
61 match self {
62 Self::Slow => "options.value.slow",
63 Self::Normal => "options.value.normal",
64 Self::Fast => "options.value.fast",
65 Self::Instant => "options.value.instant",
66 }
67 }
68}
69
70#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
72#[non_exhaustive]
73pub enum BackgroundAudio {
74 #[default]
76 Keep,
77 Mute,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87#[non_exhaustive]
88pub struct Background {
89 pub pause: bool,
92 pub audio: BackgroundAudio,
94}
95
96impl Background {
97 pub const KEEP_RUNNING: Self = Self {
99 pause: false,
100 audio: BackgroundAudio::Keep,
101 };
102
103 #[must_use]
105 pub const fn paused(self) -> Self {
106 Self {
107 pause: true,
108 ..self
109 }
110 }
111
112 #[must_use]
114 pub const fn muted(self) -> Self {
115 Self {
116 audio: BackgroundAudio::Mute,
117 ..self
118 }
119 }
120}
121
122impl Default for Background {
123 fn default() -> Self {
124 Self::KEEP_RUNNING
125 }
126}
127
128pub trait GameSettings: Debug + Clone + PartialEq + Eq + Default + 'static {
132 type Theme: Debug + Copy + PartialEq + Eq + Default + 'static;
136
137 const DEFAULT_FRAME_CAP: FrameCap = FrameCap::DEFAULT;
141
142 const FRAME_CAP_CHOICES: &'static [FrameCap] = &frame_cap::CHOICES;
147
148 const BACKGROUND: Background = Background::KEEP_RUNNING;
152
153 fn to_ron(settings: &Settings<Self>) -> String;
155
156 fn from_ron(text: &str) -> Result<Settings<Self>, String>;
160}
161
162#[derive(serde::Serialize, serde::Deserialize)]
166#[serde(default, rename = "Settings")]
167struct EngineOnly {
168 text_speed: TextSpeed,
169 fullscreen: bool,
170 music_volume: u8,
171 sound_volume: u8,
172 voices: bool,
173 voice_volume: u8,
174 frame_cap: FrameCap,
175 layout: Option<String>,
176}
177
178impl Default for EngineOnly {
179 fn default() -> Self {
180 Self::of(&Settings::default())
181 }
182}
183
184impl EngineOnly {
185 fn of(s: &Settings<()>) -> Self {
186 Self {
187 text_speed: s.text_speed,
188 fullscreen: s.fullscreen,
189 music_volume: s.music_volume,
190 sound_volume: s.sound_volume,
191 voices: s.voices,
192 voice_volume: s.voice_volume,
193 frame_cap: s.frame_cap,
194 layout: s.layout.map(|l| l.name().to_owned()),
195 }
196 }
197}
198
199impl GameSettings for () {
202 type Theme = ();
203
204 fn to_ron(s: &Settings<Self>) -> String {
205 ron::to_string(&EngineOnly::of(s)).unwrap_or_default()
206 }
207
208 fn from_ron(text: &str) -> Result<Settings<Self>, String> {
209 let file: EngineOnly = ron::from_str(text).map_err(|e| format!("unreadable: {e}"))?;
210 Ok(Settings {
211 text_speed: file.text_speed,
212 fullscreen: file.fullscreen,
213 music_volume: file.music_volume,
214 sound_volume: file.sound_volume,
215 voices: file.voices,
216 voice_volume: file.voice_volume,
217 frame_cap: file.frame_cap,
218 layout: file.layout.as_deref().and_then(Layout::from_name),
219 ..Settings::default()
220 })
221 }
222}
223
224#[derive(Debug, Clone, PartialEq, Eq)]
227pub struct Settings<S: GameSettings> {
228 pub text_speed: TextSpeed,
230 pub fullscreen: bool,
232 pub theme: S::Theme,
234 pub music_volume: u8,
236 pub sound_volume: u8,
238 pub voices: bool,
242 pub voice_volume: u8,
244 pub frame_cap: FrameCap,
250 pub(crate) layout: Option<Layout>,
254 pub game: S,
256}
257
258impl<S: GameSettings> Default for Settings<S> {
259 fn default() -> Self {
260 Self {
261 text_speed: TextSpeed::default(),
262 fullscreen: false,
263 theme: S::Theme::default(),
264 music_volume: DEFAULT_VOLUME,
265 sound_volume: DEFAULT_VOLUME,
266 voices: true,
267 voice_volume: DEFAULT_VOLUME,
268 frame_cap: S::DEFAULT_FRAME_CAP,
269 layout: None,
270 game: S::default(),
271 }
272 }
273}
274
275fn volume_factor(volume: u8) -> f32 {
278 f32::from(volume.min(MAX_VOLUME)) / f32::from(MAX_VOLUME)
279}
280
281impl<S: GameSettings> Settings<S> {
282 pub fn layout(&self) -> Option<Layout> {
284 self.layout
285 }
286
287 #[must_use]
289 pub fn with_layout(mut self, layout: Layout) -> Self {
290 self.layout = Some(layout);
291 self
292 }
293
294 #[must_use]
297 pub fn with_layout_of(mut self, other: &Self) -> Self {
298 self.layout = other.layout;
299 self
300 }
301
302 #[must_use]
305 pub fn restored(&self) -> Self {
306 Self {
307 layout: self.layout,
308 ..Self::default()
309 }
310 }
311
312 pub fn music_factor(&self) -> f32 {
314 volume_factor(self.music_volume)
315 }
316
317 pub fn sound_factor(&self) -> f32 {
319 volume_factor(self.sound_volume)
320 }
321
322 pub fn voice_factor(&self) -> f32 {
324 volume_factor(self.voice_volume)
325 }
326
327 pub fn to_ron(&self) -> String {
329 S::to_ron(self)
330 }
331
332 pub fn from_ron(text: &str) -> Result<Self, String> {
335 S::from_ron(text)
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342 use crate::input::sample::LEFT_HANDED;
343 use crate::toy::{Prefs, Theme};
344
345 type Settings = super::Settings<Prefs>;
346
347 #[test]
348 fn the_defaults_are_a_new_players() {
349 let s = Settings::default();
350 assert_eq!(s.text_speed, TextSpeed::Normal);
351 assert_eq!(s.text_speed.chars_per_s(), Some(60.0));
352 assert!(!s.fullscreen);
353 assert_eq!(s.theme, Theme::default());
354 assert_eq!((s.music_volume, s.sound_volume), (80, 80));
355 assert_eq!((s.voices, s.voice_volume), (true, 80));
356 assert_eq!(s.frame_cap, FrameCap::Fps(60));
357 assert_eq!(s.layout(), None);
358 assert_eq!(s.game, Prefs::default());
359 }
360
361 #[test]
362 fn restoring_keeps_only_the_layout() {
363 let mut s = Settings::default().with_layout(LEFT_HANDED);
364 s.text_speed = TextSpeed::Fast;
365 s.fullscreen = true;
366 s.theme = Theme::Night;
367 s.music_volume = 3;
368 s.sound_volume = 4;
369 s.voices = false;
370 s.voice_volume = 5;
371 s.frame_cap = FrameCap::Off;
372 s.game.speed = 9;
373 assert_eq!(s.layout(), Some(LEFT_HANDED));
374 assert_eq!(s.restored(), Settings::default().with_layout(LEFT_HANDED));
375 }
376
377 #[test]
378 fn another_settings_layout_is_taken_with_or_without_one() {
379 let mut s = Settings::default();
380 s.game.speed = 4;
381 let left = Settings::default().with_layout(LEFT_HANDED);
382 let taken = s.clone().with_layout_of(&left);
383 assert_eq!(taken.layout(), Some(LEFT_HANDED));
384 assert_eq!(taken.game.speed, 4);
385 assert_eq!(taken.with_layout_of(&Settings::default()), s);
386 }
387
388 #[test]
389 fn the_saved_form_is_the_games() {
390 let mut s = Settings::default().with_layout(LEFT_HANDED);
391 s.game.speed = 7;
392 s.theme = Theme::Night;
393 let text = s.to_ron();
394 assert_eq!(text, Prefs::to_ron(&s));
395 assert_eq!(Settings::from_ron(&text), Ok(s));
396 assert!(Settings::from_ron("nonsense").is_err());
397 }
398
399 #[test]
400 fn a_game_with_no_settings_of_its_own_saves_the_engines() {
401 let mut s = super::Settings::<()>::default().with_layout(LEFT_HANDED);
402 s.text_speed = TextSpeed::Instant;
403 s.sound_volume = 12;
404 s.voices = false;
405 s.voice_volume = 30;
406 s.frame_cap = FrameCap::Fps(144);
407 assert_eq!(super::Settings::from_ron(&s.to_ron()), Ok(s));
408 let s = super::Settings::<()>::default();
409 assert_eq!(super::Settings::from_ron(&s.to_ron()), Ok(s));
410 }
411
412 #[test]
413 fn the_blank_templates_saved_settings_read_back_through_no_settings() {
414 let saved = "(text_speed:Fast,fullscreen:true,music_volume:3,sound_volume:7)";
417 let s = super::Settings::<()> {
418 text_speed: TextSpeed::Fast,
419 fullscreen: true,
420 music_volume: 3,
421 sound_volume: 7,
422 ..super::Settings::default()
423 };
424 assert_eq!(super::Settings::from_ron(saved), Ok(s));
425 assert_eq!(
426 super::Settings::from_ron("Settings()"),
427 Ok(super::Settings::<()>::default())
428 );
429 assert!(super::Settings::<()>::from_ron("(fullscreen: maybe)").is_err());
430 let odd = super::Settings::<()>::from_ron("(layout: Some(\"two words\"))");
432 assert_eq!(odd.map(|s| s.layout()), Ok(None));
433 }
434
435 #[derive(Debug, Clone, Default, PartialEq, Eq)]
437 struct Fast;
438
439 impl GameSettings for Fast {
440 type Theme = ();
441 const DEFAULT_FRAME_CAP: FrameCap = FrameCap::Fps(120);
442 const FRAME_CAP_CHOICES: &'static [FrameCap] = &[FrameCap::Fps(120), FrameCap::Off];
443
444 fn to_ron(_: &super::Settings<Self>) -> String {
445 String::new()
446 }
447
448 fn from_ron(_: &str) -> Result<super::Settings<Self>, String> {
449 Ok(super::Settings::default())
450 }
451 }
452
453 #[test]
454 fn the_frame_cap_is_the_games_default_until_the_player_picks() {
455 assert_eq!(
456 super::Settings::<()>::default().frame_cap,
457 FrameCap::Fps(60)
458 );
459 assert_eq!(<() as GameSettings>::FRAME_CAP_CHOICES, &frame_cap::CHOICES);
460 let mut s = super::Settings::<Fast>::default();
461 assert_eq!(s.frame_cap, FrameCap::Fps(120));
462 assert_eq!(Fast::FRAME_CAP_CHOICES.len(), 2);
463 s.frame_cap = FrameCap::Off;
464 assert_eq!(s.restored().frame_cap, FrameCap::Fps(120));
465 }
466
467 #[test]
468 fn the_background_keeps_running_until_the_game_says() {
469 let keep = Background::KEEP_RUNNING;
470 assert_eq!((keep.pause, keep.audio), (false, BackgroundAudio::Keep));
471 assert_eq!(Background::default(), keep);
472 assert_eq!(<() as GameSettings>::BACKGROUND, keep);
473 assert_eq!(Prefs::BACKGROUND, keep);
474 let paused = keep.paused();
475 assert_eq!((paused.pause, paused.audio), (true, BackgroundAudio::Keep));
476 let muted = keep.muted();
477 assert_eq!((muted.pause, muted.audio), (false, BackgroundAudio::Mute));
478 let both = keep.paused().muted();
479 assert_eq!(both, keep.muted().paused());
480 assert_eq!((both.pause, both.audio), (true, BackgroundAudio::Mute));
481 }
482
483 #[test]
484 fn a_toy_save_without_a_frame_cap_reads_the_default() {
485 let read = |text| Settings::from_ron(text).map(|s| s.frame_cap);
486 assert_eq!(read("Toy(speed: 1)"), Ok(FrameCap::Fps(60)));
487 assert_eq!(read("Toy(frame_cap: Off)"), Ok(FrameCap::Off));
488 let s = Settings {
489 frame_cap: FrameCap::Fps(240),
490 ..Settings::default()
491 };
492 assert!(s.to_ron().contains("frame_cap:Fps(240)"), "{}", s.to_ron());
493 assert_eq!(Settings::from_ron(&s.to_ron()), Ok(s));
494 }
495
496 #[test]
497 fn engine_settings_saved_without_a_frame_cap_read_the_default() {
498 let read = |text| super::Settings::<()>::from_ron(text).map(|s| s.frame_cap);
499 assert_eq!(read("(text_speed:Fast)"), Ok(FrameCap::Fps(60)));
500 assert_eq!(read("(frame_cap:Off)"), Ok(FrameCap::Off));
501 assert_eq!(read("(frame_cap:Fps(30))"), Ok(FrameCap::Fps(30)));
502 }
503
504 #[test]
505 fn volumes_are_factors_from_silent_to_full() {
506 let mut s = Settings::default();
507 assert!((s.music_factor() - 0.8).abs() < 1e-6);
508 assert!((s.sound_factor() - 0.8).abs() < 1e-6);
509 s.music_volume = 0;
510 s.sound_volume = 100;
511 assert!(s.music_factor().abs() < f32::EPSILON);
512 s.music_volume = 37;
513 assert!((s.music_factor() - 0.37).abs() < 1e-6);
514 s.music_volume = 0;
515 assert!((s.sound_factor() - 1.0).abs() < f32::EPSILON);
516 s.sound_volume = 200;
518 assert!((s.sound_factor() - 1.0).abs() < f32::EPSILON);
519 assert!((s.voice_factor() - 0.8).abs() < 1e-6);
520 s.voice_volume = 0;
521 assert!(s.voice_factor().abs() < f32::EPSILON);
522 s.voice_volume = 30;
523 assert!((s.voice_factor() - 0.3).abs() < 1e-6);
524 }
525
526 #[test]
527 fn text_speeds_double_each_step_and_instant_has_none() {
528 let speeds: Vec<_> = TextSpeed::ALL.iter().map(|s| s.chars_per_s()).collect();
529 assert_eq!(speeds, [Some(30.0), Some(60.0), Some(120.0), None]);
530 let keys: Vec<_> = TextSpeed::ALL.iter().map(|s| s.key()).collect();
531 let value = |name: &str| format!("options.value.{name}");
532 assert_eq!(keys, ["slow", "normal", "fast", "instant"].map(value));
533 }
534}