Skip to main content

rgpre/import/
menu_settings.rs

1//! Installed defaults for book-menu preferences; see `docs/engine/menu.md`.
2use std::collections::BTreeMap;
3
4/// SYSTEM.INI preferences expressed in the menu's units.
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct MenuSettings {
7    pub subtitles: bool,
8    pub movie_interlace: bool,
9    pub voice: bool,
10    /// The engine's 0..256 volume divided by 12.8 and truncated.
11    ///
12    /// Open question: exact multiples of 64 divide to just under a whole number
13    /// in extended precision; whether the original then shows one step lower is unverified.
14    pub sound_volume: u8,
15    pub music_volume: u8,
16    /// Held-direction repeat delay in BIOS timer ticks.
17    pub repeat_ticks: u32,
18}
19
20impl MenuSettings {
21    /// Parse required settings from their original sections; the first occurrence wins.
22    pub fn parse(content: &str) -> Result<Self, String> {
23        let mut section = String::new();
24        let mut fields = BTreeMap::new();
25        for line in content.lines() {
26            let line = line.split(';').next().unwrap_or_default().trim();
27            if line.starts_with('[') && line.ends_with(']') {
28                section = line[1..line.len() - 1].to_ascii_lowercase();
29            } else if let Some((key, value)) = line.split_once('=') {
30                fields
31                    .entry((section.clone(), key.trim().to_ascii_lowercase()))
32                    .or_insert(value.trim());
33            }
34        }
35        let number = |section: &str, key: &str, max: u32| -> Result<u32, String> {
36            let context = format!("SYSTEM.INI [{section}] {key}");
37            let value = fields
38                .get(&(section.into(), key.into()))
39                .ok_or_else(|| format!("{context}: missing value"))?;
40            value
41                .parse::<u32>()
42                .ok()
43                .filter(|n| *n <= max)
44                .ok_or_else(|| format!("{context}: expected 0..{max}, got {value:?}"))
45        };
46        let repeat_ticks = number("dialog", "menu_traverse_delay", u32::MAX)?;
47        if repeat_ticks == 0 {
48            return Err("SYSTEM.INI [dialog] menu_traverse_delay: must be positive".into());
49        }
50        Ok(Self {
51            subtitles: number("dialog", "dialog_print_text", 1)? != 0,
52            movie_interlace: number("screen", "smk_interlace", 1)? != 0,
53            voice: number("dialog", "dialog_use_speech", 1)? != 0,
54            sound_volume: (f64::from(number("system", "volume", 256)?) / 12.8).trunc() as u8,
55            music_volume: (f64::from(number("system", "redbook_volume", 256)?) / 12.8).trunc()
56                as u8,
57            repeat_ticks,
58        })
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    const CONFIG: &str = "[dialog]\nmenu_traverse_delay=4\ndialog_print_text=1\ndialog_use_speech=0\n[screen]\nsmk_interlace=1\n[system]\nvolume=255\nredbook_volume=200\n";
66    #[test]
67    fn installed_preferences_use_menu_units_and_sections() {
68        assert_eq!(
69            MenuSettings::parse(CONFIG).unwrap(),
70            MenuSettings {
71                subtitles: true,
72                movie_interlace: true,
73                voice: false,
74                sound_volume: 19,
75                music_volume: 15,
76                repeat_ticks: 4,
77            }
78        );
79        for text in [
80            CONFIG.replace("volume=255", "volume=257"),
81            CONFIG.replace("delay=4", "delay=0"),
82            CONFIG.replace("text=1", "text=bad"),
83        ] {
84            assert!(
85                MenuSettings::parse(&text)
86                    .unwrap_err()
87                    .contains("SYSTEM.INI")
88            );
89        }
90    }
91}