Skip to main content

bevy_convars/
loader.rs

1//! Provides the ability to load TOML configuration files as a collection of CVars.
2//!
3//! # Recommendations
4//! No default for the user's config file is provided, however one can use the [directories](https://crates.io/crates/directories) library to get platform-specific locations for those files.
5//!
6
7use std::{fmt::Display, fs::File, io::Read, path::PathBuf};
8
9use bevy_app::Plugin;
10#[cfg(feature = "config_loader_asset")]
11use bevy_asset::{AssetPath, AssetServer, WaitForAssetError};
12use bevy_ecs::world::World;
13use bevy_log::warn;
14use serde::de::IntoDeserializer;
15use toml_edit::{ImDocument, TomlError};
16
17#[cfg(feature = "config_loader_asset")]
18mod assets;
19mod cvar_doc;
20#[cfg(test)]
21mod tests;
22
23#[cfg(feature = "config_loader_asset")]
24pub use assets::*;
25
26pub use cvar_doc::*;
27
28use crate::{CVarError, CVarManagement, WorldExtensions, builtin::ConfigLayers};
29
30/// A config loader, which injests [DocumentContext]s and applies them to the world.
31#[derive(Default)]
32pub struct ConfigLoader {}
33
34/// Methods for creating a config loader.
35impl ConfigLoader {
36    /// Applies a given config to the world.
37    pub fn apply<S: AsRef<str>>(
38        &self,
39        world: &mut World,
40        document: DocumentContext<S>,
41    ) -> Result<(), CVarError> {
42        let scanner = CVarDocScanner::new(document);
43
44        let cvars: Vec<(&str, toml_edit::Item)> =
45            scanner.find_cvars(world.resource::<CVarManagement>());
46
47        for (cvar, value) in cvars {
48            if let toml_edit::Item::Value(value) = value {
49                world.set_cvar_deserialize(cvar, IntoDeserializer::into_deserializer(value))?;
50            } else {
51                warn!("CVar {cvar} couldn't be parsed, as it wasn't value-compatible.");
52            }
53        }
54
55        Ok(())
56    }
57
58    /// Applies a given config to the world, by parsing it into a TOML document and [ConfigLoader::apply]ing that.
59    pub fn apply_from_string(
60        &self,
61        world: &mut World,
62        document: &str,
63        source: Option<&str>,
64    ) -> Result<(), CVarError> {
65        let document = ImDocument::parse(document)?;
66
67        let document = DocumentContext::new(document, source.unwrap_or("NO_SOURCE").to_owned());
68
69        self.apply(world, document)?;
70
71        Ok(())
72    }
73}
74
75/// A non-recoverable error that can occur when loading configuration.
76#[derive(Debug)]
77#[non_exhaustive]
78pub enum ConfigLoaderError {
79    /// Wrapper over an inner parsing error.
80    ParseError(TomlError),
81    /// Wrapper over an inner IO error.
82    IoError(std::io::Error),
83}
84
85impl Display for ConfigLoaderError {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        match self {
88            ConfigLoaderError::ParseError(toml_error) => write!(f, "{toml_error}"),
89            ConfigLoaderError::IoError(error) => write!(f, "{error}"),
90        }
91    }
92}
93
94impl From<TomlError> for ConfigLoaderError {
95    fn from(value: TomlError) -> Self {
96        Self::ParseError(value)
97    }
98}
99
100impl From<std::io::Error> for ConfigLoaderError {
101    fn from(value: std::io::Error) -> Self {
102        Self::IoError(value)
103    }
104}
105
106/// A builder to create a new [CVarLoaderPlugin]
107#[derive(Default)]
108pub struct CVarLoaderPluginBuilder {
109    #[cfg(feature = "config_loader_asset")]
110    layers_root: Option<AssetPath<'static>>,
111    /// The user's config file within the OS filesystem
112    #[cfg(feature = "config_loader_fs")]
113    user_config_file: Option<PathBuf>,
114    /// Any asset-managed config layers to load at startup.
115    #[cfg(feature = "config_loader_asset")]
116    asset_layers: Vec<PathBuf>,
117    /// Any extra layers to load at startup.
118    extra_layers: Vec<DocumentContext<String>>,
119}
120
121impl CVarLoaderPluginBuilder {
122    /// The fancy default, loading layers from the asset path `ConfigLayers/` and automatically loading the default layers.
123    /// Does not set the user config file path or add any extra layers.
124    pub fn fancy() -> Self {
125        Self {
126            #[cfg(feature = "config_loader_asset")]
127            layers_root: Some(AssetPath::parse("ConfigLayers/")),
128            ..Default::default()
129        }
130        .load_default_layers()
131    }
132
133    /// Adds the default layers to the load list.
134    /// The following layers are 'default' and may be added depending on build configuration:
135    ///
136    /// - `debug_assertions.toml` for `cfg(debug_assertions)`
137    #[cfg(feature = "config_loader_asset")]
138    pub fn load_default_layers(mut self) -> Self {
139        #[cfg(debug_assertions)]
140        self.asset_layers
141            .push(PathBuf::from("debug_assertions.toml"));
142
143        self
144    }
145
146    /// Conditionally adds an asset layer, meant to be used with [cfg!] or other conditions.
147    /// You should prefer actual rust `if` statements for anything complex.
148    ///
149    /// ```
150    /// # #![allow(unexpected_cfgs)]
151    /// # use bevy_convars::*;
152    /// # use bevy_convars::loader::*;
153    ///
154    /// let builder =
155    ///     CVarLoaderPluginBuilder::fancy()
156    ///         .add_asset_layer_if(cfg!(feature = "dev_tools"), "dev_tools.toml")
157    ///         .add_asset_layer_if(cfg!(feature = "release"), "release.toml");
158    ///
159    /// ```
160    #[cfg(feature = "config_loader_asset")]
161    pub fn add_asset_layer_if(mut self, given: bool, layer: &'static str) -> Self {
162        if given {
163            self.asset_layers.push(PathBuf::from(layer));
164        }
165
166        self
167    }
168
169    /// Adds an asset layer to the builder.
170    #[cfg(feature = "config_loader_asset")]
171    pub fn add_asset_layer(mut self, layer: &'static str) -> Self {
172        self.asset_layers.push(PathBuf::from(layer));
173
174        self
175    }
176
177    /// Sets the root for config layers.
178    #[cfg(feature = "config_loader_asset")]
179    pub fn with_layers_root(self, path: AssetPath<'static>) -> Self {
180        Self {
181            layers_root: Some(path),
182            ..self
183        }
184    }
185
186    /// Sets the user config file location.
187    #[cfg(feature = "config_loader_fs")]
188    pub fn with_user_config_file(self, path: PathBuf) -> Self {
189        Self {
190            user_config_file: Some(path),
191            ..self
192        }
193    }
194
195    /// Adds a layer to load from the layer root. This should be a file relative to the root.
196    #[cfg(feature = "config_loader_asset")]
197    pub fn with_asset_layer(mut self, path: PathBuf) -> Self {
198        self.asset_layers.push(path);
199
200        self
201    }
202
203    /// Adds a pre-parsed config layer to apply.
204    pub fn add_layer(mut self, layer: DocumentContext<String>) -> Self {
205        self.extra_layers.push(layer);
206
207        self
208    }
209
210    /// Consumes the builder to create a [CVarLoaderPlugin].
211    pub fn build(self) -> CVarLoaderPlugin {
212        if !self.asset_layers.is_empty() {
213            assert!(
214                self.layers_root.is_some(),
215                "Can't add asset layers without a root."
216            );
217        }
218
219        CVarLoaderPlugin {
220            layers_root: self.layers_root,
221            user_config_file: self.user_config_file,
222            asset_layers: self.asset_layers,
223            extra_layers: self.extra_layers,
224        }
225    }
226}
227
228/// Plugin that provides layered config loading for CVars, and additionally manages the user config file.
229///
230/// During build, the plugin will load any layers it was configured to load, and also any asset layers named by [ConfigLayers](crate::builtin::ConfigLayers)
231///
232/// # Remarks
233/// This plugin **MUST** be added after all other CVar registering plugins. It's recommended to seperate CVar registration from other plugin registration to ensure it's done first.
234pub struct CVarLoaderPlugin {
235    /// The built-in layers root folder within assets.
236    #[cfg(feature = "config_loader_asset")]
237    layers_root: Option<AssetPath<'static>>,
238    /// The user's config file within the OS filesystem.
239    #[cfg(feature = "config_loader_fs")]
240    user_config_file: Option<PathBuf>,
241    /// Any asset-managed config layers to load at startup.
242    #[cfg(feature = "config_loader_asset")]
243    asset_layers: Vec<PathBuf>,
244    /// Any extra layers to load at startup.
245    extra_layers: Vec<DocumentContext<String>>,
246}
247
248impl Plugin for CVarLoaderPlugin {
249    fn build(&self, app: &mut bevy_app::App) {
250        let loader = ConfigLoader::default();
251        // Begin with any extra layers.
252
253        for layer in self.extra_layers.iter() {
254            let res = loader.apply(app.world_mut(), layer.clone());
255
256            if let Err(e) = res {
257                warn!(
258                    "Failed to load an extra layer ({}), got error: {}",
259                    layer.source(),
260                    e
261                );
262            }
263        }
264
265        // Load the layers we were told to via cvar first.
266        let extra_asset_layers = (**app.world().resource::<ConfigLayers>()).clone();
267
268        #[cfg(feature = "config_loader_asset")]
269        {
270            let server = app.world().resource::<AssetServer>().clone();
271            for layer in extra_asset_layers.iter().chain(self.asset_layers.iter()) {
272                let root = self.layers_root.as_ref().unwrap().clone();
273
274                let path = root
275                    .resolve(layer.to_str().unwrap())
276                    .expect("Trying to resolve an asset layer should never fail.");
277
278                let handle = server.load::<CVarConfig>(&path);
279
280                match bevy_tasks::block_on(server.wait_for_asset(&handle)) {
281                    Ok(()) => {}
282                    Err(WaitForAssetError::Failed(err)) => {
283                        match &*err {
284                            bevy_asset::AssetLoadError::AssetReaderError(_) => {
285                                bevy_log::warn!("Couldn't find config layer {layer:?}, skipping.")
286                            }
287                            e => bevy_log::error!(
288                                "Failed to load the config layer {layer:?}, reason: {e}"
289                            ),
290                        }
291                        continue;
292                    }
293                    Err(e) => {
294                        bevy_log::error!("Failed to load the config layer {layer:?}, reason: {e}");
295                        continue;
296                    }
297                }
298
299                let res = loader.apply_asset(app.world_mut(), handle);
300
301                if let Err(e) = res {
302                    warn!(
303                        "Failed to load an asset layer ({:?}), got error: {}",
304                        path, e
305                    );
306                }
307            }
308        }
309
310        #[cfg(feature = "config_loader_fs")]
311        {
312            if let Some(ref path) = self.user_config_file {
313                let res = File::options()
314                    .read(true)
315                    .create(true)
316                    .append(true)
317                    .open(path);
318
319                if let Err(e) = res {
320                    warn!(
321                        "Failed to create or open the user config file at {path:?}, got error: {e}"
322                    );
323                } else if let Ok(mut file) = res {
324                    let mut buf = String::new();
325                    file.read_to_string(&mut buf).unwrap();
326
327                    let res = loader.apply_from_string(
328                        app.world_mut(),
329                        &buf,
330                        Some(&path.to_string_lossy()),
331                    );
332
333                    if let Err(e) = res {
334                        warn!(
335                            "Failed to load the user's config file ({:?}), got error: {}",
336                            path, e
337                        );
338                    }
339                }
340            }
341        }
342    }
343}