Skip to main content

pumpkinplus/
config.rs

1//! Configuration management system.
2//!
3//! Uses a registry pattern where modules register their configs by name,
4//! and ConfigManager handles loading from disk with merge semantics.
5//!
6//! ## Config Location
7//!
8//! The config file is stored at `{data_folder}/config.json`.
9//! It is created automatically on first load with all registered defaults.
10
11use figment::Figment;
12use figment::providers::{Format, Json, Serialized};
13use pumpkin_plugin_api::Context;
14use serde::de::DeserializeOwned;
15use serde::{Deserialize, Serialize};
16use serde_json::Value;
17use std::cell::RefCell;
18use std::collections::HashMap;
19use std::fs;
20use std::path::PathBuf;
21use tracing::error;
22
23thread_local! {
24    static CONFIG: RefCell<Option<ConfigManager>> = const { RefCell::new(None) };
25}
26
27/// Extracts a config key from a type's full name.
28///
29/// This helper inspects the fully-qualified type name at compile time and
30/// derives a short, snake_case key suitable for a config object name.
31///
32/// # Examples
33///
34/// - `crate::modules::mechanics::player::Config` → `"player"`
35/// - `crate::modules::mechanics::player::PlayerConfig` → `"player"`
36fn config_key<T>() -> String {
37    use std::any::type_name;
38
39    let full_name = type_name::<T>();
40    let parts: Vec<&str> = full_name.split("::").collect();
41
42    if parts.len() >= 2 {
43        parts[parts.len() - 2].to_string()
44    } else if let Some(&last) = parts.last() {
45        last.strip_suffix("Config")
46            .map_or_else(|| last.to_string(), |s| s.to_string())
47    } else {
48        full_name.to_string()
49    }
50}
51
52/// Manages plugin configuration using a registry pattern.
53/// Modules register their configs by name, and ConfigManager handles
54/// loading from disk with merge semantics for missing fields.
55#[derive(Clone, Debug, Default, Serialize, Deserialize)]
56pub struct ConfigManager {
57    #[serde(flatten)]
58    configs: HashMap<String, Value>,
59}
60
61impl ConfigManager {
62    /// Creates an empty ConfigManager ready for registration.
63    pub fn empty() -> Self {
64        Self::default()
65    }
66
67    /// Returns the global config manager instance.
68    pub fn get() -> Option<Self> {
69        CONFIG.with(|c| c.borrow().clone())
70    }
71
72    /// Gets a config by type, deriving the key from the type name.
73    /// Returns defaults if not found or parse fails.
74    pub fn get_config<T: DeserializeOwned + Default + 'static>(&self) -> T {
75        let key = config_key::<T>();
76        self.configs
77            .get(&key)
78            .and_then(|v| {
79                serde_json::from_value(v.clone())
80                    .inspect_err(|e| error!("Failed to parse config for key '{}': {}", key, e))
81                    .ok()
82            })
83            .unwrap_or_default()
84    }
85
86    /// Registers a config with default values for a module.
87    /// The key is derived automatically from the type name.
88    pub fn register<T: Serialize + Default + 'static>(&mut self) {
89        let key = config_key::<T>();
90        let config = T::default();
91        match serde_json::to_value(config) {
92            Ok(value) => {
93                self.configs.insert(key, value);
94            }
95            Err(e) => error!("Failed to serialize config for key: {}", e),
96        }
97    }
98
99    /// Loads config from disk, merging with registered defaults.
100    /// Call this after all modules have registered their configs.
101    pub fn finalize(&mut self, context: &Context) {
102        let path =
103            PathBuf::from(context.get_data_folder().trim_start_matches("./")).join("config.json");
104
105        let mut figment = Figment::new();
106
107        for (key, value) in &self.configs {
108            figment = figment.merge(Serialized::default(key, value));
109        }
110
111        if path.exists() {
112            figment = figment.merge(Json::file(&path));
113        }
114
115        self.configs = figment
116            .extract()
117            .inspect_err(|e| error!("Failed to extract merged config: {:?}", e))
118            .unwrap_or_default();
119
120        if let Some(parent) = path.parent() {
121            fs::create_dir_all(parent).ok();
122        }
123
124        fs::write(
125            &path,
126            serde_json::to_string_pretty(self).unwrap_or_default(),
127        )
128        .inspect_err(|e| error!("Failed to write config: {}", e))
129        .ok();
130
131        CONFIG.set(Some(self.clone()));
132    }
133}