You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

428 lines
13 KiB
Rust

// 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::<AssetRegistry>()
.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::<ExportAction>),
clear.run_if(interaction_condition::<ClearAction>),
import.run_if(interaction_condition::<ImportAction>),
inspect.run_if(interaction_condition::<InspectAction>),
load.run_if(interaction_condition::<LoadAssetsAction>),
unload.run_if(interaction_condition::<UnloadAssetsAction>),
spawn_level.run_if(interaction_condition::<SpawnLevelAction>),
asset_inspector::<AudioSource>,
asset_inspector::<Scene>,
),
)
.add_systems(
PostUpdate,
(
rehydrate::<Visibility, ComputedVisibility>,
rehydrate::<Handle<AudioSource>, 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<HandleUntyped>,
}
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<T: Component>(
events: Query<&Interaction, (Changed<Interaction>, With<T>)>,
) -> bool {
events
.iter()
.find(|&interaction| *interaction == Interaction::Pressed)
.is_some()
}
fn rehydrate<W: Component, WO: Component + Default + std::fmt::Debug>(
events: Query<Entity, (Added<W>, Without<WO>)>,
mut commands: Commands,
) {
events.iter().for_each(|entity| {
info!("Rehydrating {:?}", WO::default());
commands.entity(entity).insert(WO::default());
});
}
fn ser(
root: &Query<Entity, With<LevelRoot>>,
children: &Query<&Children>,
world: &World,
) -> String {
let app_type_registry = world.resource::<AppTypeRegistry>().clone();
let mut builder = DynamicSceneBuilder::from_world(world.clone());
builder.deny_all_resources();
// builder.allow_all();
builder.deny::<ComputedVisibility>();
// Level administrivia
builder.allow::<LevelRoot>();
// Scene components
builder.allow::<Handle<Scene>>();
builder.allow::<Visibility>();
builder.allow::<Transform>();
builder.allow::<GlobalTransform>();
// Audio components
builder.allow::<Handle<AudioSource>>();
builder.allow::<PlaybackSettings>();
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<Entity, With<LevelRoot>>, 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<Entity, With<LevelRoot>>, children: Query<&Children>, world: &World) {
info!("Inexpect level");
let serialized = ser(&root, &children, world);
print!("{}", serialized);
}
fn clear(root: Query<Entity, With<LevelRoot>>, 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<AssetServer>) {
info!("Importing level");
let scene_handle: Handle<DynamicScene> = server.load("output.scn.ron");
commands.spawn((
LevelRoot,
DynamicSceneBundle {
scene: scene_handle.clone(),
..default()
},
));
}
fn fallback_camera_condition(
added: Query<Entity, Added<Camera>>,
mut removed: RemovedComponents<Camera>,
) -> bool {
added.iter().chain(removed.iter()).count() > 0
}
fn fallback_camera(
mut ui_camera: Query<&mut Camera, With<Camera2d>>,
cameras: Query<&mut Camera, Without<Camera2d>>,
) {
ui_camera.single_mut().is_active = cameras.iter().len() <= 0;
}
fn asset_inspector<T: Asset>(mut events: EventReader<AssetEvent<T>>) {
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<AssetRegistry>, server: Res<AssetServer>) {
info!("Loading assets");
registry.handles = server.load_folder("./dynamic").unwrap();
info!("Current files: {:?}", registry.handles);
}
fn unload(mut registry: ResMut<AssetRegistry>, mut gltfs: ResMut<Assets<Gltf>>) {
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<AssetServer>) {
commands
.spawn((SpatialBundle { ..default() }, LevelRoot))
.with_children(|parent| {
parent.spawn(AudioSourceBundle {
source: server.load::<AudioSource, &str>("dynamic/Lake Sound 1.ogg"),
settings: PlaybackSettings {
mode: PlaybackMode::Loop,
paused: false,
..default()
},
});
parent.spawn((SceneBundle {
scene: server.load("dynamic/materials.glb#Scene0"),
..default()
},));
});
}