// TODO: // * Determine where local assets folder needs to be created // * Create local assets folder // * Recusrively laod that folder and watch for changes // * Copy assets into local folder in leue of importing them // * Check portability by moving binary + folder to new location use std::{ fs::{DirBuilder, File}, io::Write, time::Duration, }; use bevy::{ asset::{Asset, ChangeWatcher}, audio::PlaybackMode, gltf::Gltf, prelude::*, tasks::IoTaskPool, }; use monologue_trees::{debug::*, ui}; fn main() { App::new() .init_resource::() .add_plugins(( DefaultPlugins .set(WindowPlugin { primary_window: Some(Window { title: "Serialization WTF".into(), resolution: (640., 480.).into(), ..default() }), ..default() }) .set(AssetPlugin { asset_folder: "assets".into(), // Tell the asset server to watch for asset changes on disk: watch_for_changes: ChangeWatcher::with_delay(Duration::from_millis(0)), ..default() }), DebugInfoPlugin, ui::GameUiPlugin { ..default() }, )) .add_systems(Startup, (init, init_assets_dir)) .add_systems( Update, ( export.run_if(interaction_condition::), clear.run_if(interaction_condition::), import.run_if(interaction_condition::), inspect.run_if(interaction_condition::), load.run_if(interaction_condition::), unload.run_if(interaction_condition::), spawn_level.run_if(interaction_condition::), asset_inspector::, asset_inspector::, ), ) .add_systems( PostUpdate, ( rehydrate::, rehydrate::, PlaybackSettings>, fallback_camera.run_if(fallback_camera_condition), ), ) .run(); } #[derive(Debug, Component, Reflect, Default)] #[reflect(Component)] struct LevelRoot; #[derive(Debug, Component)] struct ExportAction; #[derive(Debug, Component)] struct ClearAction; #[derive(Debug, Component)] struct ImportAction; #[derive(Debug, Component)] struct InspectAction; #[derive(Debug, Component)] struct LoadAssetsAction; #[derive(Debug, Component)] struct UnloadAssetsAction; #[derive(Debug, Component)] struct SpawnLevelAction; #[derive(Debug, Resource, Default)] struct AssetRegistry { handles: Vec, } fn init_assets_dir() { IoTaskPool::get() .spawn(async move { match DirBuilder::new().create("assets") { Ok(_) => info!("Created assets directory"), Err(e) => warn!("Error creating assets directory", e), } }) .detach(); } fn init(mut commands: Commands) { commands.spawn(( Camera2dBundle { ..default() }, UiCameraConfig { show_ui: true }, )); commands .spawn(( NodeBundle { style: Style { top: Val::Px(0.0), right: Val::Px(0.0), margin: UiRect::all(Val::Px(5.0)), padding: UiRect::all(Val::Px(5.0)), border: UiRect::all(Val::Px(1.0)), position_type: PositionType::Absolute, ..default() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), ..default() }, ui::Select::Action, )) .with_children(|parent| { parent.spawn(( ButtonBundle { style: Style { margin: UiRect::all(Val::Px(5.0)), padding: UiRect::all(Val::Px(5.0)), border: UiRect::all(Val::Px(1.0)), ..default() }, border_color: Color::BLACK.into(), ..default() }, ui::Title { text: "Load Assets".into(), ..default() }, LoadAssetsAction, )); parent.spawn(( ButtonBundle { style: Style { margin: UiRect::all(Val::Px(5.0)), padding: UiRect::all(Val::Px(5.0)), border: UiRect::all(Val::Px(1.0)), ..default() }, border_color: Color::BLACK.into(), ..default() }, ui::Title { text: "Dump Assets".into(), ..default() }, UnloadAssetsAction, )); parent.spawn(( ButtonBundle { style: Style { margin: UiRect::all(Val::Px(5.0)), padding: UiRect::all(Val::Px(5.0)), border: UiRect::all(Val::Px(1.0)), ..default() }, border_color: Color::BLACK.into(), ..default() }, ui::Title { text: "Spawn Level".into(), ..default() }, SpawnLevelAction, )); parent.spawn(( ButtonBundle { style: Style { margin: UiRect::all(Val::Px(5.0)), padding: UiRect::all(Val::Px(5.0)), border: UiRect::all(Val::Px(1.0)), ..default() }, border_color: Color::BLACK.into(), ..default() }, ui::Title { text: "Export".into(), ..default() }, ExportAction, )); parent.spawn(( ButtonBundle { style: Style { margin: UiRect::all(Val::Px(5.0)), padding: UiRect::all(Val::Px(5.0)), border: UiRect::all(Val::Px(1.0)), ..default() }, border_color: Color::BLACK.into(), ..default() }, ui::Title { text: "Clear".into(), ..default() }, ClearAction, )); parent.spawn(( ButtonBundle { style: Style { margin: UiRect::all(Val::Px(5.0)), padding: UiRect::all(Val::Px(5.0)), border: UiRect::all(Val::Px(1.0)), ..default() }, border_color: Color::BLACK.into(), ..default() }, ui::Title { text: "Import".into(), ..default() }, ImportAction, )); parent.spawn(( ButtonBundle { style: Style { margin: UiRect::all(Val::Px(5.0)), padding: UiRect::all(Val::Px(5.0)), border: UiRect::all(Val::Px(1.0)), ..default() }, border_color: Color::BLACK.into(), ..default() }, ui::Title { text: "Inspect".into(), ..default() }, InspectAction, )); }); } fn interaction_condition( events: Query<&Interaction, (Changed, With)>, ) -> bool { events .iter() .find(|&interaction| *interaction == Interaction::Pressed) .is_some() } fn rehydrate( events: Query, Without)>, mut commands: Commands, ) { events.iter().for_each(|entity| { info!("Rehydrating {:?}", WO::default()); commands.entity(entity).insert(WO::default()); }); } fn ser( root: &Query>, children: &Query<&Children>, world: &World, ) -> String { let app_type_registry = world.resource::().clone(); let mut builder = DynamicSceneBuilder::from_world(world.clone()); builder.deny_all_resources(); // builder.allow_all(); builder.deny::(); // Level administrivia builder.allow::(); // Scene components builder.allow::>(); builder.allow::(); builder.allow::(); builder.allow::(); // Audio components builder.allow::>(); builder.allow::(); root.iter().for_each(|r| { // Extract the level root builder.extract_entity(r); // Extract all level root children builder.extract_entities( children .get(r) .expect("Root has children") .iter() .map(|&entity| entity), ); }); let scene = builder.build(); scene .serialize_ron(&app_type_registry) .expect("Serialize scene") } fn export(root: Query>, children: Query<&Children>, world: &World) { info!("Export level"); let serialized = ser(&root, &children, world); IoTaskPool::get() .spawn(async move { // Write the scene RON data to file File::create(format!("assets/output.scn.ron")) .and_then(|mut file| file.write(serialized.as_bytes())) .expect("Error while writing scene to file"); }) .detach(); } fn inspect(root: Query>, children: Query<&Children>, world: &World) { info!("Inexpect level"); let serialized = ser(&root, &children, world); print!("{}", serialized); } fn clear(root: Query>, mut commands: Commands) { info!("Clearing level"); root.iter().for_each(|entity| { commands.entity(entity).despawn_recursive(); }); } // TODO: Figure out how to import the same asset from a differnt source // How do the plugins do it?? fn import(mut commands: Commands, server: Res) { info!("Importing level"); let scene_handle: Handle = server.load("output.scn.ron"); commands.spawn(( LevelRoot, DynamicSceneBundle { scene: scene_handle.clone(), ..default() }, )); } fn fallback_camera_condition( added: Query>, mut removed: RemovedComponents, ) -> bool { added.iter().chain(removed.iter()).count() > 0 } fn fallback_camera( mut ui_camera: Query<&mut Camera, With>, cameras: Query<&mut Camera, Without>, ) { ui_camera.single_mut().is_active = cameras.iter().len() <= 0; } fn asset_inspector(mut events: EventReader>) { events.iter().for_each(|event| match event { AssetEvent::Created { handle } => info!("Asset Created {:?}", handle), AssetEvent::Modified { handle } => info!("Asset Modified {:?}", handle), AssetEvent::Removed { handle } => info!("Asset Removed {:?}", handle), }); } // OK seems like `load_folder` does not automatically pick up added files fn load(mut registry: ResMut, server: Res) { info!("Loading assets"); registry.handles = server.load_folder("./dynamic").unwrap(); info!("Current files: {:?}", registry.handles); } fn unload(mut registry: ResMut, mut gltfs: ResMut>) { info!("Unloading asstes"); registry.handles.clear(); // This is required to clear scenes from asset cache gltfs.clear(); } fn spawn_level(mut commands: Commands, server: Res) { commands .spawn((SpatialBundle { ..default() }, LevelRoot)) .with_children(|parent| { parent.spawn(AudioSourceBundle { source: server.load::("dynamic/Lake Sound 1.ogg"), settings: PlaybackSettings { mode: PlaybackMode::Loop, paused: false, ..default() }, }); parent.spawn((SceneBundle { scene: server.load("dynamic/materials.glb#Scene0"), ..default() },)); }); }