use std::{fmt::Display, fs::File, io::Read, path::PathBuf};
use bevy_app::Plugin;
#[cfg(feature = "config_loader_asset")]
use bevy_asset::{AssetPath, AssetServer, WaitForAssetError};
use bevy_ecs::world::World;
use bevy_log::warn;
use serde::de::IntoDeserializer;
use toml_edit::{ImDocument, TomlError};
#[cfg(feature = "config_loader_asset")]
mod assets;
mod cvar_doc;
#[cfg(test)]
mod tests;
#[cfg(feature = "config_loader_asset")]
pub use assets::*;
pub use cvar_doc::*;
use crate::{CVarError, CVarManagement, WorldExtensions, builtin::ConfigLayers};
#[derive(Default)]
pub struct ConfigLoader {}
impl ConfigLoader {
pub fn apply<S: AsRef<str>>(
&self,
world: &mut World,
document: DocumentContext<S>,
) -> Result<(), CVarError> {
let scanner = CVarDocScanner::new(document);
let cvars: Vec<(&str, toml_edit::Item)> =
scanner.find_cvars(world.resource::<CVarManagement>());
for (cvar, value) in cvars {
if let toml_edit::Item::Value(value) = value {
world.set_cvar_deserialize(cvar, IntoDeserializer::into_deserializer(value))?;
} else {
warn!("CVar {cvar} couldn't be parsed, as it wasn't value-compatible.");
}
}
Ok(())
}
pub fn apply_from_string(
&self,
world: &mut World,
document: &str,
source: Option<&str>,
) -> Result<(), CVarError> {
let document = ImDocument::parse(document)?;
let document = DocumentContext::new(document, source.unwrap_or("NO_SOURCE").to_owned());
self.apply(world, document)?;
Ok(())
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ConfigLoaderError {
ParseError(TomlError),
IoError(std::io::Error),
}
impl Display for ConfigLoaderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConfigLoaderError::ParseError(toml_error) => write!(f, "{toml_error}"),
ConfigLoaderError::IoError(error) => write!(f, "{error}"),
}
}
}
impl From<TomlError> for ConfigLoaderError {
fn from(value: TomlError) -> Self {
Self::ParseError(value)
}
}
impl From<std::io::Error> for ConfigLoaderError {
fn from(value: std::io::Error) -> Self {
Self::IoError(value)
}
}
#[derive(Default)]
pub struct CVarLoaderPluginBuilder {
#[cfg(feature = "config_loader_asset")]
layers_root: Option<AssetPath<'static>>,
#[cfg(feature = "config_loader_fs")]
user_config_file: Option<PathBuf>,
#[cfg(feature = "config_loader_asset")]
asset_layers: Vec<PathBuf>,
extra_layers: Vec<DocumentContext<String>>,
}
impl CVarLoaderPluginBuilder {
pub fn fancy() -> Self {
Self {
#[cfg(feature = "config_loader_asset")]
layers_root: Some(AssetPath::parse("ConfigLayers/")),
..Default::default()
}
.load_default_layers()
}
#[cfg(feature = "config_loader_asset")]
pub fn load_default_layers(mut self) -> Self {
#[cfg(debug_assertions)]
self.asset_layers
.push(PathBuf::from("debug_assertions.toml"));
self
}
#[cfg(feature = "config_loader_asset")]
pub fn add_asset_layer_if(mut self, given: bool, layer: &'static str) -> Self {
if given {
self.asset_layers.push(PathBuf::from(layer));
}
self
}
#[cfg(feature = "config_loader_asset")]
pub fn add_asset_layer(mut self, layer: &'static str) -> Self {
self.asset_layers.push(PathBuf::from(layer));
self
}
#[cfg(feature = "config_loader_asset")]
pub fn with_layers_root(self, path: AssetPath<'static>) -> Self {
Self {
layers_root: Some(path),
..self
}
}
#[cfg(feature = "config_loader_fs")]
pub fn with_user_config_file(self, path: PathBuf) -> Self {
Self {
user_config_file: Some(path),
..self
}
}
#[cfg(feature = "config_loader_asset")]
pub fn with_asset_layer(mut self, path: PathBuf) -> Self {
self.asset_layers.push(path);
self
}
pub fn add_layer(mut self, layer: DocumentContext<String>) -> Self {
self.extra_layers.push(layer);
self
}
pub fn build(self) -> CVarLoaderPlugin {
if !self.asset_layers.is_empty() {
assert!(
self.layers_root.is_some(),
"Can't add asset layers without a root."
);
}
CVarLoaderPlugin {
layers_root: self.layers_root,
user_config_file: self.user_config_file,
asset_layers: self.asset_layers,
extra_layers: self.extra_layers,
}
}
}
pub struct CVarLoaderPlugin {
#[cfg(feature = "config_loader_asset")]
layers_root: Option<AssetPath<'static>>,
#[cfg(feature = "config_loader_fs")]
user_config_file: Option<PathBuf>,
#[cfg(feature = "config_loader_asset")]
asset_layers: Vec<PathBuf>,
extra_layers: Vec<DocumentContext<String>>,
}
impl Plugin for CVarLoaderPlugin {
fn build(&self, app: &mut bevy_app::App) {
let loader = ConfigLoader::default();
for layer in self.extra_layers.iter() {
let res = loader.apply(app.world_mut(), layer.clone());
if let Err(e) = res {
warn!(
"Failed to load an extra layer ({}), got error: {}",
layer.source(),
e
);
}
}
let extra_asset_layers = (**app.world().resource::<ConfigLayers>()).clone();
#[cfg(feature = "config_loader_asset")]
{
let server = app.world().resource::<AssetServer>().clone();
for layer in extra_asset_layers.iter().chain(self.asset_layers.iter()) {
let root = self.layers_root.as_ref().unwrap().clone();
let path = root
.resolve(layer.to_str().unwrap())
.expect("Trying to resolve an asset layer should never fail.");
let handle = server.load::<CVarConfig>(&path);
match bevy_tasks::block_on(server.wait_for_asset(&handle)) {
Ok(()) => {}
Err(WaitForAssetError::Failed(err)) => {
match &*err {
bevy_asset::AssetLoadError::AssetReaderError(_) => {
bevy_log::warn!("Couldn't find config layer {layer:?}, skipping.")
}
e => bevy_log::error!(
"Failed to load the config layer {layer:?}, reason: {e}"
),
}
continue;
}
Err(e) => {
bevy_log::error!("Failed to load the config layer {layer:?}, reason: {e}");
continue;
}
}
let res = loader.apply_asset(app.world_mut(), handle);
if let Err(e) = res {
warn!(
"Failed to load an asset layer ({:?}), got error: {}",
path, e
);
}
}
}
#[cfg(feature = "config_loader_fs")]
{
if let Some(ref path) = self.user_config_file {
let res = File::options()
.read(true)
.create(true)
.append(true)
.open(path);
if let Err(e) = res {
warn!(
"Failed to create or open the user config file at {path:?}, got error: {e}"
);
} else if let Ok(mut file) = res {
let mut buf = String::new();
file.read_to_string(&mut buf).unwrap();
let res = loader.apply_from_string(
app.world_mut(),
&buf,
Some(&path.to_string_lossy()),
);
if let Err(e) = res {
warn!(
"Failed to load the user's config file ({:?}), got error: {}",
path, e
);
}
}
}
}
}
}