Skip to main content

pacenotes_installer\install/
rbr.rs

1//! Richard Burns Rally: the Pacenote plugin's settings file.
2
3/// `/`-separated like a package path, since it is also what the ledger records.
4pub const PACENOTE_SETTINGS: &str = "Plugins/Pacenote/PaceNote.ini";
5
6/// Present only when the plugin is installed.
7const PACENOTE_CONFIG_DIR: &str = "Plugins/Pacenote/config";
8
9/// Each co-driver's audio sits under here at its path below its category.
10const PACENOTE_SOUNDS: &str = "Plugins/Pacenote/sounds";
11
12/// The plugin's documented defaults, written only when the file is absent and
13/// then edited like a shipped one.
14const TEMPLATE: &str = "[SETTINGS]\r\n\
15     sounds=\r\n\
16     language=english\r\n\
17     replaySpeeds=0.005 0.01 0.03 0.06 0.125 0.25 0.5 0.75 1 1.25 1.5 1.75 2 4 6 8 10\r\n\
18     enableGUI=1\r\n\
19     showReplayHUD=1\r\n\
20     muteReplayPacenotes=0\r\n";
21
22const SECTION: &str = "SETTINGS";
23
24pub fn configure(
25    game_dir: &std::path::Path,
26    picks: &std::collections::BTreeMap<String, String>,
27) -> Result<super::Configured, super::ConfigureError> {
28    if !crate::relative_path::join(game_dir, PACENOTE_CONFIG_DIR).is_dir() {
29        return Err(super::ConfigureError::PluginMissing);
30    }
31
32    let mut outcome = super::Configured::default();
33    let path = crate::relative_path::join(game_dir, PACENOTE_SETTINGS);
34    let shipped = std::fs::read_to_string(&path).ok();
35    if shipped.is_none() {
36        outcome.notes.push(format!(
37            "{PACENOTE_SETTINGS} was missing; writing a default one"
38        ));
39    }
40    let original = shipped.as_deref().unwrap_or(TEMPLATE);
41
42    let mut text = original.to_string();
43    if let Some(language) = picks
44        .get(pacenotes_shared::CATEGORY_LANGUAGE)
45        .and_then(|key| leaf_of(key))
46    {
47        text = super::ini::set_ini_key(&text, SECTION, "language", language);
48    }
49    if let Some(sounds) = picks
50        .get(pacenotes_shared::CATEGORY_CODRIVER)
51        .and_then(|key| below_category(key))
52    {
53        // A value naming a missing folder silences the co-driver, so the package's own stays.
54        let audio = crate::relative_path::join(game_dir, &format!("{PACENOTE_SOUNDS}/{sounds}"));
55        if audio.is_dir() {
56            text = super::ini::set_ini_key(&text, SECTION, "sounds", sounds);
57        } else {
58            outcome.notes.push(format!(
59                "no audio folder at {PACENOTE_SOUNDS}/{sounds}; leaving the sounds setting \
60                 as the package shipped it"
61            ));
62        }
63    }
64
65    if shipped.as_deref() == Some(text.as_str()) {
66        return Ok(outcome);
67    }
68    write_settings(&path, &text)?;
69    outcome.files.push(PACENOTE_SETTINGS.to_string());
70    Ok(outcome)
71}
72
73fn write_settings(path: &std::path::Path, text: &str) -> Result<(), super::ConfigureError> {
74    let io_error = |doing: String, e: std::io::Error| super::ConfigureError::Io {
75        doing,
76        reason: e.to_string(),
77    };
78    if let Some(parent) = path.parent() {
79        std::fs::create_dir_all(parent)
80            .map_err(|e| io_error(format!("creating {}", parent.display()), e))?;
81    }
82    std::fs::write(path, text.as_bytes())
83        .map_err(|e| io_error(format!("writing {}", path.display()), e))
84}
85
86/// `rbr/public/language/polish` → `polish`, the plugin's own folder name.
87fn leaf_of(key: &str) -> Option<&str> {
88    let (_, _, name) = pacenotes_shared::split_download_path(key)?;
89    name.rsplit('/').next()
90}
91
92/// `rbr/public/codriver/polish/male/Kamil` → `polish/male/Kamil`, the value the
93/// voice packages write themselves.
94fn below_category(key: &str) -> Option<&str> {
95    let (_, _, name) = pacenotes_shared::split_download_path(key)?;
96    let (_, rest) = name.split_once('/')?;
97    Some(rest)
98}
99
100#[cfg(test)]
101mod tests;