1use 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#[derive(Default)]
32pub struct ConfigLoader {}
33
34impl ConfigLoader {
36 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 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#[derive(Debug)]
77#[non_exhaustive]
78pub enum ConfigLoaderError {
79 ParseError(TomlError),
81 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#[derive(Default)]
108pub struct CVarLoaderPluginBuilder {
109 #[cfg(feature = "config_loader_asset")]
110 layers_root: Option<AssetPath<'static>>,
111 #[cfg(feature = "config_loader_fs")]
113 user_config_file: Option<PathBuf>,
114 #[cfg(feature = "config_loader_asset")]
116 asset_layers: Vec<PathBuf>,
117 extra_layers: Vec<DocumentContext<String>>,
119}
120
121impl CVarLoaderPluginBuilder {
122 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 #[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 #[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 #[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 #[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 #[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 #[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 pub fn add_layer(mut self, layer: DocumentContext<String>) -> Self {
205 self.extra_layers.push(layer);
206
207 self
208 }
209
210 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
228pub struct CVarLoaderPlugin {
235 #[cfg(feature = "config_loader_asset")]
237 layers_root: Option<AssetPath<'static>>,
238 #[cfg(feature = "config_loader_fs")]
240 user_config_file: Option<PathBuf>,
241 #[cfg(feature = "config_loader_asset")]
243 asset_layers: Vec<PathBuf>,
244 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 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 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}