use bevy::asset::{AssetApp, AssetEvent, AssetId, AssetServer, Assets, Handle};
use bevy::camera::visibility::Visibility;
use bevy::ecs::prelude::*;
use bevy::reflect::Reflect;
use bevy::transform::components::Transform;
use crate::params::TreeParams;
use crate::plugin::ProceduralTree;
#[derive(Component, Clone, Debug, PartialEq, Eq, Reflect)]
#[reflect(Component)]
#[require(Transform, Visibility)]
pub struct ProceduralTreeAsset(pub Handle<TreeParams>);
pub(crate) fn sync_tree_assets(
mut events: MessageReader<AssetEvent<TreeParams>>,
assets: Res<Assets<TreeParams>>,
changed_sources: Query<(), Changed<ProceduralTreeAsset>>,
mut trees: Query<(Entity, Ref<ProceduralTreeAsset>, Option<&mut ProceduralTree>)>,
mut commands: Commands,
) {
let changed: Vec<AssetId<TreeParams>> = events
.read()
.filter_map(|event| match event {
AssetEvent::Added { id } | AssetEvent::Modified { id } | AssetEvent::LoadedWithDependencies { id } => {
Some(*id)
}
AssetEvent::Removed { .. } | AssetEvent::Unused { .. } => None,
})
.collect();
if changed.is_empty() && changed_sources.is_empty() {
return;
}
for (entity, source, tree) in &mut trees {
if !source.is_changed() && !changed.contains(&source.0.id()) {
continue;
}
let Some(params) = assets.get(&source.0) else { continue };
match tree {
Some(mut tree) => {
if tree.0 != *params {
tree.0 = params.clone();
}
}
None => {
commands.entity(entity).insert(ProceduralTree(params.clone()));
}
}
}
}
pub(crate) fn build(app: &mut bevy::app::App) {
app.register_type::<ProceduralTreeAsset>();
add_asset_type(app);
}
pub(crate) fn finish(app: &mut bevy::app::App) {
add_asset_type(app);
}
fn add_asset_type(app: &mut bevy::app::App) {
if !app.world().contains_resource::<AssetServer>() || app.world().contains_resource::<Assets<TreeParams>>() {
return;
}
app.init_asset::<TreeParams>().register_asset_reflect::<TreeParams>();
#[cfg(feature = "ron")]
app.register_asset_loader(TreeParamsLoader);
}
#[cfg(feature = "ron")]
pub use file::{TreeFileError, TreeParamsLoader};
#[cfg(feature = "ron")]
mod file {
use bevy::asset::io::Reader;
use bevy::asset::{AssetLoader, LoadContext};
use bevy::reflect::TypePath;
use crate::params::TreeParams;
#[derive(Clone, Copy, Debug, Default, TypePath)]
#[type_path = "bevy_flora::tree_asset"]
pub struct TreeParamsLoader;
impl AssetLoader for TreeParamsLoader {
type Asset = TreeParams;
type Settings = ();
type Error = TreeFileError;
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &(),
_load_context: &mut LoadContext<'_>,
) -> Result<TreeParams, TreeFileError> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await.map_err(|error| TreeFileError(Problem::Io(error)))?;
let text = std::str::from_utf8(&bytes).map_err(|error| TreeFileError(Problem::Utf8(error)))?;
TreeParams::from_ron(text)
}
fn extensions(&self) -> &[&str] {
&["tree.ron"]
}
}
#[derive(Debug)]
pub struct TreeFileError(Problem);
#[derive(Debug)]
enum Problem {
Io(std::io::Error),
Utf8(std::str::Utf8Error),
Ron(ron::error::SpannedError),
}
impl std::fmt::Display for TreeFileError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.0 {
Problem::Io(error) => write!(f, "could not read the tree file: {error}"),
Problem::Utf8(error) => write!(f, "the tree file is not UTF-8: {error}"),
Problem::Ron(error) => write!(f, "the tree file is not valid: {error}"),
}
}
}
impl std::error::Error for TreeFileError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self.0 {
Problem::Io(error) => Some(error),
Problem::Utf8(error) => Some(error),
Problem::Ron(error) => Some(error),
}
}
}
impl TreeParams {
pub fn to_ron(&self) -> String {
ron::ser::to_string_pretty(self, ron::ser::PrettyConfig::default())
.expect("every field of TreeParams has a RON form")
}
pub fn from_ron(text: &str) -> Result<Self, TreeFileError> {
ron::from_str(text).map_err(|error| TreeFileError(Problem::Ron(error)))
}
}
}
#[cfg(test)]
mod tests {
use bevy::asset::AssetPlugin;
use bevy::prelude::*;
use crate::{FloraPlugin, ProceduralTree, ProceduralTreeAsset, TreeParams, TreePreset};
fn app() -> App {
let mut app = App::new();
app.add_plugins((bevy::MinimalPlugins, AssetPlugin::default()));
app.init_asset::<Mesh>();
app.init_asset::<StandardMaterial>();
app.add_plugins(FloraPlugin);
app
}
#[test]
fn trees_sharing_an_asset_follow_it_and_wait_for_it() {
let mut app = app();
let handle = app.world_mut().resource_mut::<Assets<TreeParams>>().reserve_handle();
let trees: Vec<Entity> =
(0..2).map(|_| app.world_mut().spawn(ProceduralTreeAsset(handle.clone())).id()).collect();
app.update();
assert!(app.world().get::<ProceduralTree>(trees[0]).is_none(), "a tree was drawn before its asset");
let maple = TreePreset::Maple.params();
app.world_mut().resource_mut::<Assets<TreeParams>>().insert(handle.id(), maple.clone()).unwrap();
app.update();
app.update();
for &tree in &trees {
assert_eq!(app.world().get::<ProceduralTree>(tree), Some(&ProceduralTree(maple.clone())));
}
let reseeded = TreeParams { seed: 7, ..maple };
*app.world_mut().resource_mut::<Assets<TreeParams>>().get_mut(&handle).unwrap() = reseeded.clone();
app.update();
app.update();
for &tree in &trees {
assert_eq!(app.world().get::<ProceduralTree>(tree), Some(&ProceduralTree(reseeded.clone())));
}
let late = app.world_mut().spawn(ProceduralTreeAsset(handle.clone())).id();
app.update();
assert_eq!(app.world().get::<ProceduralTree>(late), Some(&ProceduralTree(reseeded)));
}
#[cfg(feature = "ron")]
#[test]
fn every_preset_survives_a_tree_file() {
for preset in TreePreset::ALL {
let params = preset.params();
assert_eq!(TreeParams::from_ron(¶ms.to_ron()).unwrap(), params, "{preset:?}");
}
}
#[cfg(feature = "ron")]
#[test]
fn a_tree_file_may_leave_fields_out_but_not_misspell_them() {
let partial = TreeParams::from_ron("(seed: 7, trunk: (height: 9.0))").unwrap();
let defaults = TreeParams::default();
assert_eq!(partial.seed, 7);
assert_eq!(partial.trunk.height, 9.0);
assert_eq!(partial.trunk.radius, defaults.trunk.radius);
assert_eq!(partial.foliage, defaults.foliage);
let typo = TreeParams::from_ron("(trunk: (hieght: 9.0))").unwrap_err().to_string();
assert!(typo.contains("hieght"), "{typo}");
}
#[cfg(feature = "ron")]
#[test]
fn the_example_tree_file_loads() {
let text = include_str!("../examples/assets/trees/maple.tree.ron");
TreeParams::from_ron(text).unwrap();
}
}