use crate::prelude::*; /// A Tweaks is resource used to specify game customization like asset names, /// and non-user customizations made to the game during development. pub(crate) struct TweakPlugin; impl Plugin for TweakPlugin { fn build(&self, app: &mut App) { app.add_systems(OnEnter(GameState::Loading), load_tweakfile); app.init_asset::() .register_asset_loader(TweaksLoader); } } fn load_tweakfile(server: Res, mut commands: Commands) { let handle: Handle = server.load("martian.tweak.toml"); commands.insert_resource(GameTweaks { handle }); } #[derive(Debug, Resource)] pub(crate) struct GameTweaks { pub handle: Handle, } #[derive(Debug, Asset, TypePath)] pub struct Tweaks { table: toml::Table, handles: HashMap, } impl Tweaks { fn from_table(table: &toml::Table, load_context: &mut LoadContext) -> Tweaks { let handles = Tweaks::iter_all(table, "") .iter() .filter_map(|(k, v)| match v { toml::Value::String(s) => std::path::Path::new(format!("assets/{}", s).as_str()) .exists() .then(|| { if s.ends_with(".gltf") || s.ends_with(".glb") { Some(load_context.load::(s).untyped()) } else if s.ends_with(".png") { Some(load_context.load::(s).untyped()) } else if s.ends_with(".ttf") || s.ends_with(".otf") { Some(load_context.load::(s).untyped()) } else { None } }) .flatten() .map(|h| (k.clone(), h)), _ => None, }) .collect(); Tweaks { table: table.clone(), handles, } } pub fn get_handle(&self, key: &str) -> Option> { self.handles.get(key).map(|h| h.clone().typed::()) } pub fn get_handle_unchecked(&self, key: &str) -> Option> { self.handles .get(key) .map(|h| h.clone().typed_unchecked::()) } pub fn get<'de, T: Deserialize<'de>>(&self, key: &str) -> Option { Tweaks::locate(&self.table, key).map(|val| match val.try_into() { Ok(val) => val, Err(e) => panic!("{}", e.message()), }) } fn iter_all(t: &toml::Table, key: &str) -> Vec<(String, toml::Value)> { t.iter() .flat_map(|(k, v)| { let nk = if key == "" { k.to_string() } else { format!("{}_{}", key, k) }; match v { toml::Value::Table(nt) => Tweaks::iter_all(nt, nk.as_str()), _ => vec![(nk, v.clone())], } }) .collect() } fn locate(t: &toml::Table, key: &str) -> Option { t.iter().find_map(|(k, v)| { if key == k { Some(v.clone()) } else if key.starts_with(k) { let prefix = format!("{}_", k); match v { toml::Value::Table(nt) => { Tweaks::locate(nt, key.strip_prefix(prefix.as_str()).unwrap()) } _ => Some(v.clone()), } } else { None } }) } } #[derive(Default)] pub struct TweaksLoader; #[derive(Debug, Error)] pub enum TweaksError { #[error("Failed to read file")] IO(#[from] std::io::Error), #[error("Failed to decode file")] Decode(#[from] Utf8Error), #[error("Failed to parse file")] Parse(#[from] toml::de::Error), } impl AssetLoader for TweaksLoader { type Asset = Tweaks; type Settings = (); type Error = TweaksError; fn load<'a>( &'a self, reader: &'a mut Reader, _settings: &'a Self::Settings, load_context: &'a mut LoadContext, ) -> BoxedFuture<'a, Result> { use bevy::asset::AsyncReadExt; Box::pin(async move { let mut bytes = Vec::new(); reader.read_to_end(&mut bytes).await?; let s = std::str::from_utf8(bytes.as_slice())?; match toml::from_str::(s) { Ok(parsed) => { let result = Tweaks::from_table(&parsed, load_context); Ok(result) } Err(e) => { println!("Error parsing tweaks: {:?}", e.message()); Err(TweaksError::Parse(e)) } } }) } fn extensions(&self) -> &[&str] { &["tweak.toml"] } }