Skip to main content

yog_config/
lib.rs

1//! Mod configuration — typed key/value config files under `<game_dir>/yog-config/`.
2//!
3//! File format: `key = value` pairs, one per line. Lines starting with `#` are
4//! comments. Whitespace around keys and values is stripped.
5//!
6//! ```text
7//! # mymod configuration
8//! max_players = 20
9//! welcome_message = Hello World!
10//! pvp_enabled = true
11//! damage_multiplier = 1.5
12//! ```
13//!
14//! Obtain a game dir from `srv.game_dir()` inside an event handler.
15
16use std::collections::HashMap;
17use std::io::{self, BufRead, Write};
18use std::path::{Path, PathBuf};
19
20/// A key/value configuration file backed by `<game_dir>/yog-config/<mod_id>.cfg`.
21pub struct Config {
22    path: PathBuf,
23    data: HashMap<String, String>,
24}
25
26impl Config {
27    /// Load (or create) a config file for `mod_id` under `game_dir`.
28    ///
29    /// The file lives at `<game_dir>/yog-config/<mod_id>.cfg`.
30    /// `:` and `/` in `mod_id` are replaced with `_` in the filename.
31    pub fn load(game_dir: &str, mod_id: &str) -> Self {
32        let safe = mod_id.replace([':', '/'], "_");
33        let dir  = Path::new(game_dir).join("yog-config");
34        let path = dir.join(format!("{safe}.cfg"));
35        let data = Self::parse(&path).unwrap_or_default();
36        Self { path, data }
37    }
38
39    fn parse(path: &Path) -> io::Result<HashMap<String, String>> {
40        let file = std::fs::File::open(path)?;
41        let mut map = HashMap::new();
42        for line in io::BufReader::new(file).lines() {
43            let line = line?;
44            let t = line.trim();
45            if t.is_empty() || t.starts_with('#') { continue; }
46            if let Some((k, v)) = t.split_once('=') {
47                map.insert(k.trim().to_string(), v.trim().to_string());
48            }
49        }
50        Ok(map)
51    }
52
53    /// Get a value as a string slice.
54    pub fn get(&self, key: &str) -> Option<&str> {
55        self.data.get(key).map(String::as_str)
56    }
57
58    /// Get a string value, or `default` if the key is absent.
59    pub fn get_or<'a>(&'a self, key: &str, default: &'a str) -> &'a str {
60        self.get(key).unwrap_or(default)
61    }
62
63    /// Get a value parsed as `i64`.
64    pub fn get_int(&self, key: &str) -> Option<i64> {
65        self.get(key)?.trim().parse().ok()
66    }
67
68    /// Get an integer value, or `default` if absent or unparseable.
69    pub fn get_int_or(&self, key: &str, default: i64) -> i64 {
70        self.get_int(key).unwrap_or(default)
71    }
72
73    /// Get a value parsed as `f64`.
74    pub fn get_float(&self, key: &str) -> Option<f64> {
75        self.get(key)?.trim().parse().ok()
76    }
77
78    /// Get a float value, or `default` if absent or unparseable.
79    pub fn get_float_or(&self, key: &str, default: f64) -> f64 {
80        self.get_float(key).unwrap_or(default)
81    }
82
83    /// Get a value parsed as `bool`.
84    ///
85    /// Truthy: `true`, `yes`, `1`, `on`. Falsy: `false`, `no`, `0`, `off`.
86    pub fn get_bool(&self, key: &str) -> Option<bool> {
87        match self.get(key)?.trim().to_lowercase().as_str() {
88            "true" | "yes" | "1" | "on"  => Some(true),
89            "false"| "no"  | "0" | "off" => Some(false),
90            _ => None,
91        }
92    }
93
94    /// Get a bool value, or `default` if absent or unrecognised.
95    pub fn get_bool_or(&self, key: &str, default: bool) -> bool {
96        self.get_bool(key).unwrap_or(default)
97    }
98
99    /// Set (or overwrite) a key. Call [`save`] to persist.
100    pub fn set(&mut self, key: impl Into<String>, value: impl ToString) {
101        self.data.insert(key.into(), value.to_string());
102    }
103
104    /// Remove a key. Returns the old value if present.
105    pub fn remove(&mut self, key: &str) -> Option<String> {
106        self.data.remove(key)
107    }
108
109    /// Returns `true` if the key exists in the config.
110    pub fn contains(&self, key: &str) -> bool {
111        self.data.contains_key(key)
112    }
113
114    /// Persist the current state to disk. Creates `yog-config/` if needed.
115    ///
116    /// Comments from the original file are not preserved; keys are written
117    /// in alphabetical order for determinism.
118    pub fn save(&self) -> io::Result<()> {
119        if let Some(p) = self.path.parent() {
120            std::fs::create_dir_all(p)?;
121        }
122        let mut file = std::fs::File::create(&self.path)?;
123        writeln!(file, "# Yog mod configuration — auto-generated")?;
124        let mut keys: Vec<_> = self.data.keys().collect();
125        keys.sort();
126        for k in keys {
127            writeln!(file, "{} = {}", k, self.data[k])?;
128        }
129        Ok(())
130    }
131
132    /// Save only if the config file does not yet exist (write defaults on first run).
133    pub fn save_defaults(&self) -> io::Result<()> {
134        if !self.path.exists() { self.save() } else { Ok(()) }
135    }
136
137    /// Number of stored entries.
138    pub fn len(&self) -> usize { self.data.len() }
139
140    pub fn is_empty(&self) -> bool { self.data.is_empty() }
141
142    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
143        self.data.iter().map(|(k, v)| (k.as_str(), v.as_str()))
144    }
145}