diff options
-rw-r--r-- | Cargo.toml | 25 | ||||
-rw-r--r-- | src/actor/mod.rs | 3 | ||||
-rw-r--r-- | src/cache/mod.rs | 3 | ||||
-rw-r--r-- | src/cache/resources.rs | 22 | ||||
-rw-r--r-- | src/commands.rs | 7 | ||||
-rw-r--r-- | src/components.rs | 3 | ||||
-rw-r--r-- | src/conditionals.rs | 22 | ||||
-rw-r--r-- | src/events.rs | 11 | ||||
-rw-r--r-- | src/lib.rs | 109 | ||||
-rw-r--r-- | src/lua_api.rs | 184 | ||||
-rw-r--r-- | src/observers.rs | 47 | ||||
-rw-r--r-- | src/payload/components/mod.rs | 20 | ||||
-rw-r--r-- | src/payload/mod.rs | 13 | ||||
-rw-r--r-- | src/preload/events.rs | 12 | ||||
-rw-r--r-- | src/preload/mod.rs | 82 | ||||
-rw-r--r-- | src/preload/resources.rs | 7 | ||||
-rw-r--r-- | src/preload/systems.rs | 41 | ||||
-rw-r--r-- | src/resources.rs | 13 | ||||
-rw-r--r-- | src/room_generation/mod.rs | 0 | ||||
-rw-r--r-- | src/systems.rs | 26 | ||||
-rw-r--r-- | src/utils.rs | 39 | ||||
-rw-r--r-- | src/watcher.rs | 14 |
22 files changed, 475 insertions, 228 deletions
@@ -1,10 +1,9 @@ [package] name = "bevy_dirworld" -version = "0.2.1" +version = "0.4.0" edition = "2021" [dependencies] -anyhow = "1.0" async-channel = "2.3" notify = "7.0" tar = "0.4" @@ -13,10 +12,8 @@ rust-crypto = "0.2" multi_key_map = "0.3" serde = "1.0" rmp-serde = "1.3" -anymap = "0.12" notify-debouncer-full = "0.4" md5 = "0.7" -bevy-async-ecs = "0.6" aes = "0.8" hex = "0.4" hex-literal = "0.4" @@ -24,30 +21,30 @@ uuid = "1.11" lazy_static = "1.5" [dependencies.bevy] -version = "0.14" +version = "0.15" default-features = false -features = ["serialize", "multi_threaded"] +features = ["serialize", "multi_threaded", "bevy_state"] [dependencies.avian3d] -version = "0.1" +version = "0.2" features = ["serialize"] [dependencies.occule] -git = "http://github.com/exvacuum/occule" -branch = "wip" +git = "https://git.exvacuum.dev/occule" +tag = "v0.3.1" [dependencies.yarnspinner] git = "https://github.com/YarnSpinnerTool/YarnSpinner-Rust" optional = true features = ["serde"] -[dependencies.bevy_scriptum] -version = "0.6" -features = ["lua"] +[dependencies.bevy_mod_scripting] +version = "0.8" +features = ["lua54", "lua_script_api"] [dependencies.bevy_basic_interaction] -git = "https://github.com/exvacuum/bevy_basic_interaction.git" -branch = "wip" +git = "https://git.exvacuum.dev/bevy_basic_interaction" +tag = "v0.2.0" [dependencies.strum] version = "0.26" diff --git a/src/actor/mod.rs b/src/actor/mod.rs index 9d0af43..eabf0fe 100644 --- a/src/actor/mod.rs +++ b/src/actor/mod.rs @@ -6,7 +6,7 @@ use std::sync::{Arc, Mutex}; use bevy::{prelude::*, utils::HashMap}; use lazy_static::lazy_static; use resources::FunctionLibrary; -use yarnspinner::{core::{LineId, YarnValue}, runtime::Dialogue}; +use yarnspinner::core::YarnValue; pub mod components; pub mod events; @@ -23,6 +23,7 @@ lazy_static! { /// Plugin which controls the behavior of actors pub struct ActorPlugin { + /// Callback for registering custom yarnspinner functions pub custom_function_registration: Option<fn(&mut FunctionLibrary)>, } diff --git a/src/cache/mod.rs b/src/cache/mod.rs new file mode 100644 index 0000000..19c3a6f --- /dev/null +++ b/src/cache/mod.rs @@ -0,0 +1,3 @@ +mod resources; +pub use resources::DirworldCache; + diff --git a/src/cache/resources.rs b/src/cache/resources.rs new file mode 100644 index 0000000..6aba61a --- /dev/null +++ b/src/cache/resources.rs @@ -0,0 +1,22 @@ +use std::{collections::HashMap, path::PathBuf}; + +use bevy::prelude::*; + +use crate::{components::DirworldEntity, payload::DirworldEntityPayload}; + +/// Structure containing payload data for cached (non-current) rooms +#[derive(Resource, Default, Debug, Deref, DerefMut)] +pub struct DirworldCache(pub HashMap<PathBuf, DirworldEntityPayload>); + +impl DirworldCache { + /// Stores an entity's payload in the cache, if it exists + pub fn cache_entity(&mut self, dirworld_entity: &DirworldEntity) { + if let Some(payload) = &dirworld_entity.payload { + self.insert(dirworld_entity.path.clone(), payload.clone()); + } + } + + pub fn get_entity_cache(&mut self, path: impl Into<PathBuf>) -> Option<DirworldEntityPayload> { + self.remove(&path.into()) + } +} diff --git a/src/commands.rs b/src/commands.rs index 60da108..616f326 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -261,19 +261,20 @@ pub trait DirworldCommands { /// Unlock Door fn dirworld_unlock_door(&mut self, path: PathBuf, key: Vec<u8>); + /// Save entity fn dirworld_save_entity(&mut self, path: PathBuf, payload: DirworldEntityPayload); } impl<'w, 's> DirworldCommands for Commands<'w, 's> { fn dirworld_lock_door(&mut self, path: PathBuf, key: Vec<u8>) { - self.add(DirworldLockDoorCommand { key, path }); + self.queue(DirworldLockDoorCommand { key, path }); } fn dirworld_unlock_door(&mut self, path: PathBuf, key: Vec<u8>) { - self.add(DirworldUnlockDoorCommand { key, path }); + self.queue(DirworldUnlockDoorCommand { key, path }); } fn dirworld_save_entity(&mut self, path: PathBuf, payload: DirworldEntityPayload) { - self.add(DirworldSaveEntityCommand { path, payload }); + self.queue(DirworldSaveEntityCommand { path, payload }); } } diff --git a/src/components.rs b/src/components.rs index 8bb2bff..0a52560 100644 --- a/src/components.rs +++ b/src/components.rs @@ -11,9 +11,12 @@ pub struct Tooltip(pub String); /// A marker component for entities spawned by dirworld handlers, i.e. they should be removed when the room changes. #[derive(Component, Clone, Debug)] pub struct DirworldEntity { + /// Path on filesystem corresponding to this entity pub path: PathBuf, + /// Extracted payload if present pub payload: Option<DirworldEntityPayload>, } +/// Marker component that prevents an entity from despawning on room change #[derive(Debug, Component)] pub struct Persist; diff --git a/src/conditionals.rs b/src/conditionals.rs index 266379a..5362066 100644 --- a/src/conditionals.rs +++ b/src/conditionals.rs @@ -3,44 +3,60 @@ use bevy::{ prelude::{AncestorIter, Entity, Parent, Query, World}, }; use serde::{Deserialize, Serialize}; -use strum::{AsRefStr, EnumString}; +use strum::AsRefStr; use uuid::Uuid; use crate::{components::DirworldEntity, resources::DirworldCurrentDir}; -// I Store Conditions as Enum Data +/// Conditions which can be checked in lua and yarnspinner scripts #[derive(Serialize, Deserialize, AsRefStr, Debug, Default, Clone, PartialEq, Eq)] pub enum Condition { + /// Always true #[default] #[strum(serialize = "Always True")] True, + /// True if `child` is a child of `parent` #[strum(serialize = "Child Of")] ChildOf { + /// Entity that must be child child: Uuid, + /// Entity that must be parent parent: Uuid, }, + /// True if `parent` is the parent of `child` #[strum(serialize = "Parent Of")] ParentOf { + /// Entity that must be parent parent: Uuid, + /// Entity that must be child child: Uuid, }, + /// True if `descendant` is a descendant of `ancestor` #[strum(serialize = "Descendant Of")] DescendantOf { + /// Entity that must be descendant descendant: Uuid, + /// Entity that must be ancestor ancestor: Uuid, }, + /// True if `ancestor` is an ancestor of `descendant` #[strum(serialize = "Ancestor Of")] AncestorOf { + /// Entity that must be ancestor ancestor: Uuid, + /// Entity that must be descendant descendant: Uuid, }, + /// True if current room matches provided id #[strum(serialize = "In Room")] InRoom(Uuid), + /// True if an object with the provided id is in the current room #[strum(serialize = "Object In Room")] ObjectInRoom(Uuid), } impl Condition { + /// Evaluate the condition and return the result pub fn evaluate(&self, world: &mut World) -> bool { match self { Condition::True => true, @@ -59,6 +75,7 @@ impl Condition { } } + /// Get the name of the condition's corresponding function for lua/yarnspinner APIs pub fn get_api_function_name(&self) -> &'static str { match self { Condition::True => "conditional_true", @@ -71,6 +88,7 @@ impl Condition { } } + /// Parses function name and argument strings into the corresponding condition representation pub fn from_api_function_name_and_args(name: &str, args: &[&str]) -> Option<Self> { match name { "conditional_true" => Some(Condition::True), diff --git a/src/events.rs b/src/events.rs index 2fa7f81..7932a67 100644 --- a/src/events.rs +++ b/src/events.rs @@ -17,17 +17,18 @@ pub enum DirworldNavigationEvent { }, } +/// Event called when leaving a room #[derive(Debug, Event, Deref, DerefMut, Clone)] pub struct DirworldLeaveRoom(pub PathBuf); +/// Event called when entering a room #[derive(Debug, Event, Deref, DerefMut, Clone)] pub struct DirworldEnterRoom(pub PathBuf); +/// Event called when changing the world root #[derive(Debug, Event, Deref, DerefMut, Clone)] pub struct DirworldChangeRoot(pub PathBuf); -#[derive(Event)] -pub struct DirworldSpawn { - pub entity: Entity, - pub data: Option<Vec<u8>>, -} +/// Event called to spawn a dirworld entities +#[derive(Event, Debug, Deref, DerefMut, Clone, Copy)] +pub struct DirworldSpawn(pub Entity); @@ -1,26 +1,23 @@ -// #![warn(missing_docs)] +#![warn(missing_docs)] //! Plugin for bevy engine enabling interaction with and representation of the file system in the world. use std::{ffi::OsStr, path::PathBuf}; use actor::ActorPlugin; -use bevy::render::mesh::ExtrusionBuilder; use bevy::{ecs::system::IntoObserverSystem, prelude::*}; -use bevy_scriptum::{runtimes::lua::LuaRuntime, BuildScriptingRuntime, ScriptingRuntimeBuilder}; -use events::{ - DirworldChangeRoot, DirworldEnterRoom, DirworldLeaveRoom, DirworldNavigationEvent, - DirworldSpawn, -}; +use bevy_mod_scripting::core::{AddScriptApiProvider, AddScriptHost, AddScriptHostHandler, ScriptingPlugin}; +use bevy_mod_scripting::lua::LuaScriptHost; +use cache::DirworldCache; +use events::{DirworldChangeRoot, DirworldEnterRoom, DirworldLeaveRoom, DirworldSpawn}; use occule::Codec; -use resources::DirworldCache; +use preload::{DirworldPreload, DirworldPreloadPlugin}; +use resources::EntryType; use resources::{ DirworldCodecs, DirworldCurrentDir, DirworldObservers, DirworldRootDir, DirworldTasks, - EntryType, }; pub use watcher::DirworldWatcherEvent; pub use watcher::DirworldWatcherSet; -use yarnspinner::core::Library; /// Components used by this plugin pub mod components; @@ -31,15 +28,10 @@ pub mod events; /// Resources used by this plugin pub mod resources; -mod watcher; - /// Commands for this plugin pub mod commands; -mod systems; - -mod observers; - +/// Utility functions pub mod utils; /// Payload for dirworld entities @@ -48,27 +40,37 @@ pub mod payload; /// Actor component pub mod actor; +/// System for dirworld-related condition checking +pub mod conditionals; + +/// Room/asset preloading +pub mod preload; + +mod cache; + +mod yarnspinner_api; + mod lua_api; -pub mod conditionals; +mod systems; -pub mod yarnspinner_api; +mod observers; -pub mod room_generation; +mod watcher; /// Plugin which enables high-level interaction #[derive(Default)] -pub struct DirworldPlugin { - pub register_custom_lua_api: - Option<Box<dyn Fn(ScriptingRuntimeBuilder<LuaRuntime>) + Send + Sync>>, -} +pub struct DirworldPlugin; impl Plugin for DirworldPlugin { fn build(&self, app: &mut App) { - info!("building"); - app.add_plugins(ActorPlugin { - custom_function_registration: Some(yarnspinner_api::setup_yarnspinner_functions), - }) + app.add_plugins(( + ActorPlugin { + custom_function_registration: Some(yarnspinner_api::setup_yarnspinner_functions), + }, + DirworldPreloadPlugin, + ScriptingPlugin, + )) .add_systems(Startup, watcher::setup) .add_systems( Update, @@ -78,18 +80,12 @@ impl Plugin for DirworldPlugin { yarnspinner_api::process_commands, ), ) - .add_systems( - PostUpdate, - watcher::update, - ) - .add_scripting::<LuaRuntime>(|runtime| { - let runtime = lua_api::register(runtime); - if let Some(register_custom) = &self.register_custom_lua_api { - (register_custom)(runtime); - } - }) - .init_resource::<DirworldCache>() + .add_script_host::<LuaScriptHost<()>>(PostUpdate) + .add_script_handler::<LuaScriptHost<()>, 0, 0>(PostUpdate) + .add_api_provider::<LuaScriptHost<()>>(Box::new(lua_api::ConditionalAPI)) + .add_systems(PostUpdate, watcher::update) .init_resource::<DirworldRootDir>() + .init_resource::<DirworldCache>() .init_resource::<DirworldCurrentDir>() .init_resource::<DirworldTasks>() .init_resource::<DirworldObservers>() @@ -98,18 +94,22 @@ impl Plugin for DirworldPlugin { .add_event::<DirworldLeaveRoom>() .add_event::<DirworldChangeRoot>() .add_event::<DirworldWatcherEvent>() - .observe(observers::navigate_to_room) - .observe(observers::handle_changes) - .observe(observers::change_root) - .observe(observers::navigate_from_room); + .add_observer(observers::navigate_to_room) + .add_observer(observers::handle_changes) + .add_observer(observers::change_root) + .add_observer(observers::navigate_from_room); } } +/// Extension trait for working with multiple file extensions on paths pub trait Extensions { + /// Get all the extensions on this path if applicable fn extensions(&self) -> Option<String>; + /// Gets the file stem (without any extensions) of this path if applicable fn file_stem_no_extensions(&self) -> Option<String>; + /// Gets the path with any extensions removed fn no_extensions(&self) -> PathBuf; } @@ -148,13 +148,20 @@ impl Extensions for PathBuf { } } +/// Extension trait providing functions for registering callbacks and codecs for filesystem entries pub trait DirworldApp { - fn register_dirworld_entry_callback<B: Bundle, M>( + /// Register callbacks to be executed when a file with given [`EntryType`]s is loaded. The + /// `preload_callback` parameter controls loading assets and is called before spawning any + /// entities in the room, and the `spawn_callback` handles initializing the spawned entities. + fn register_dirworld_entry_callbacks<B: Bundle, M, PB: Bundle, PM>( &mut self, extensions: Vec<EntryType>, - observer: impl IntoObserverSystem<DirworldSpawn, B, M>, + preload_callback: Option<impl IntoObserverSystem<DirworldPreload, PB, PM>>, + spawn_callback: impl IntoObserverSystem<DirworldSpawn, B, M>, ) -> &mut Self; + /// Register a [`Codec`] to be used to extract [`crate::payload::DirworldEntityPayload`]s from + /// files with matching extensions. fn register_dirworld_entry_codec<C: Codec + Send + Sync + 'static>( &mut self, extensions: Vec<String>, @@ -163,10 +170,11 @@ pub trait DirworldApp { } impl DirworldApp for App { - fn register_dirworld_entry_callback<B: Bundle, M>( + fn register_dirworld_entry_callbacks<B: Bundle, M, PB: Bundle, PM>( &mut self, extensions: Vec<EntryType>, - observer: impl IntoObserverSystem<DirworldSpawn, B, M>, + preload_callback: Option<impl IntoObserverSystem<DirworldPreload, PB, PM>>, + spawn_observer: impl IntoObserverSystem<DirworldSpawn, B, M>, ) -> &mut Self { let world = self.world_mut(); let observer_entity_id; @@ -174,7 +182,14 @@ impl DirworldApp for App { { let mut observer_entity = world.spawn_empty(); observer_entity_id = observer_entity.id(); - observer_entity.insert(Observer::new(observer).with_entity(observer_entity_id)); + if let Some(preload_callback) = preload_callback { + observer_entity.with_children(|parent| { + parent.spawn(Observer::new(preload_callback).with_entity(observer_entity_id)); + }); + } + observer_entity.with_children(|parent| { + parent.spawn(Observer::new(spawn_observer).with_entity(observer_entity_id)); + }); } world.flush(); diff --git a/src/lua_api.rs b/src/lua_api.rs index f9a34d4..53252a5 100644 --- a/src/lua_api.rs +++ b/src/lua_api.rs @@ -1,25 +1,21 @@ -use std::str::FromStr; +use std::{str::FromStr, sync::Mutex}; use bevy::prelude::*; -use bevy_scriptum::{ - runtimes::lua::{BevyEntity, BevyVec3, LuaRuntime, LuaScriptData}, - Runtime, ScriptingRuntimeBuilder, -}; +use bevy_mod_scripting::api::providers::bevy_reflect::LuaVec3; +use bevy_mod_scripting::{api::providers::bevy_ecs::LuaEntity, lua::tealr::mlu::mlua::Error as LuaError}; +use bevy_mod_scripting::lua::LuaEvent; +use bevy_mod_scripting::prelude::*; use uuid::Uuid; use crate::{components::DirworldEntity, conditionals::Condition}; -pub fn trigger_update( - mut scripted_entities: Query<(Entity, &mut LuaScriptData)>, - scripting_runtime: Res<LuaRuntime>, - time: Res<Time>, -) { - let delta = time.delta_seconds(); - for (entity, mut script_data) in scripted_entities.iter_mut() { - if let Err(e) = scripting_runtime.call_fn("on_update", &mut script_data, entity, (delta,)) { - error!("Encountered lua scripting error: {:?}", e); - } - } +pub fn trigger_update(mut w: PriorityEventWriter<LuaEvent<()>>) { + let event = LuaEvent::<()> { + args: (), + hook_name: "on_update".into(), + recipients: Recipients::All, + }; + w.send(event, 0); } // ACTUAL API STUFF BELOW THIS POINT {{{ @@ -27,16 +23,15 @@ pub fn trigger_update( macro_rules! register_fns { ($runtime:expr, $($function:expr),+) => { { - $runtime$(.add_function(stringify!($function).to_string(), $function))+ + let ctx = $runtime.get_mut().unwrap(); + $(ctx.globals().set(stringify!($function).to_string(), ctx.create_function($function).unwrap()).unwrap();)+ } }; } -pub fn register( - runtime: ScriptingRuntimeBuilder<LuaRuntime>, -) -> ScriptingRuntimeBuilder<LuaRuntime> { +pub fn register(api: &mut Mutex<Lua>) { register_fns!( - runtime, + api, translate, rotate, get_dirworld_id, @@ -50,113 +45,148 @@ pub fn register( ) } -fn translate( - In((BevyEntity(entity), BevyVec3(translation))): In<(BevyEntity, BevyVec3)>, - mut transform_query: Query<&mut Transform>, -) { - if let Ok(mut transform) = transform_query.get_mut(entity) { - transform.translation += translation; +fn translate(ctx: &Lua, (entity, translation): (LuaEntity, LuaVec3)) -> Result<(), LuaError> { + let world = ctx.get_world()?; + let mut world = world.write(); + if let Some(mut transform) = world.entity_mut(entity.inner().unwrap()).get_mut::<Transform>() { + transform.translation += translation.inner().unwrap(); } + Ok(()) } -fn rotate( - In((BevyEntity(entity), BevyVec3(axis), angle)): In<(BevyEntity, BevyVec3, f32)>, - mut transform_query: Query<&mut Transform>, -) { - if let Ok(mut transform) = transform_query.get_mut(entity) { - if let Ok(direction) = Dir3::new(axis) { - transform.rotate_axis(direction, angle); - } else { - warn!("Provided axis was not a valid direction!"); - } +fn rotate(ctx: &Lua, (entity, axis, angle): (LuaEntity, LuaVec3, f32)) -> Result<(), LuaError> { + let world = ctx.get_world()?; + let mut world = world.write(); + if let Some(mut transform) = world.entity_mut(entity.inner().unwrap()).get_mut::<Transform>() { + transform.rotation *= Quat::from_axis_angle(axis.inner().unwrap(), angle); } + Ok(()) } -fn get_dirworld_id(In((BevyEntity(entity),)): In<(BevyEntity,)>, dirworld_entity_query: Query<&DirworldEntity>) -> Option<String> { - dirworld_entity_query.get(entity).ok().and_then(|entity| entity.payload.as_ref().map(|payload| payload.id.to_string())) +fn get_dirworld_id(ctx: &Lua, (entity,): (LuaEntity,)) -> Result<String, LuaError> { + let world = ctx.get_world()?; + let world = world.read(); + if let Some(dirworld_entity) = world.entity(entity.inner().unwrap()).get::<DirworldEntity>() { + dirworld_entity.payload.as_ref().map(|p| p.id.to_string()).ok_or(LuaError::runtime("Failed to get entity id from payload")) + } else { + Err(LuaError::runtime("Entity missing DirworldEntity component")) + } } // Conditionals -fn condition_true(world: &mut World) -> bool { - Condition::True.evaluate(world) + +pub struct ConditionalAPI; + +impl APIProvider for ConditionalAPI { + type APITarget = Mutex<Lua>; + + type ScriptContext = Mutex<Lua>; + + type DocTarget = LuaDocFragment; + + fn attach_api( + &mut self, + api: &mut Self::APITarget, + ) -> Result<(), bevy_mod_scripting::prelude::ScriptError> { + register(api); + Ok(()) + } } -fn condition_ancestor_of( - In((ancestor, descendant)): In<(String, String)>, - world: &mut World, -) -> bool { + + +fn condition_true(ctx: &Lua, _: ()) -> Result<bool, LuaError> { + let world = ctx.get_world()?; + let mut world = world.write(); + Ok(Condition::True.evaluate(&mut world)) +} + +fn condition_ancestor_of(ctx: &Lua, (ancestor, descendant): (String, String)) -> Result<bool, LuaError> { + let world = ctx.get_world()?; + let mut world = world.write(); let Ok(ancestor) = Uuid::from_str(&ancestor) else { warn!("Provided ancestor is not a valid UUID"); - return false; + return Ok(false); }; let Ok(descendant) = Uuid::from_str(&descendant) else { warn!("Provided descendant is not a valid UUID"); - return false; + return Ok(false); }; - Condition::AncestorOf { + Ok(Condition::AncestorOf { ancestor, descendant, - } - .evaluate(world) + }.evaluate(&mut world)) } -fn condition_descendant_of( - In((descendant, ancestor)): In<(String, String)>, - world: &mut World, -) -> bool { +fn condition_descendant_of(ctx: &Lua, (descendant, ancestor): (String, String)) -> Result<bool, LuaError> { + let world = ctx.get_world()?; + let mut world = world.write(); let Ok(ancestor) = Uuid::from_str(&ancestor) else { warn!("Provided ancestor is not a valid UUID"); - return false; + return Ok(false); }; let Ok(descendant) = Uuid::from_str(&descendant) else { warn!("Provided descendant is not a valid UUID"); - return false; + return Ok(false); }; - Condition::DescendantOf { + Ok(Condition::DescendantOf { ancestor, descendant, - } - .evaluate(world) + }.evaluate(&mut world)) } -fn condition_parent_of(In((parent, child)): In<(String, String)>, world: &mut World) -> bool { +fn condition_parent_of(ctx: &Lua, (parent, child): (String, String)) -> Result<bool, LuaError> { + let world = ctx.get_world()?; + let mut world = world.write(); let Ok(parent) = Uuid::from_str(&parent) else { - warn!("Provided parent is not a valid UUID"); - return false; + warn!("Provided ancestor is not a valid UUID"); + return Ok(false); }; let Ok(child) = Uuid::from_str(&child) else { - warn!("Provided child is not a valid UUID"); - return false; + warn!("Provided descendant is not a valid UUID"); + return Ok(false); }; - Condition::ParentOf { parent, child }.evaluate(world) + Ok(Condition::ParentOf { + parent, + child, + }.evaluate(&mut world)) } -fn condition_child_of(In((child, parent)): In<(String, String)>, world: &mut World) -> bool { +fn condition_child_of(ctx: &Lua, (child, parent): (String, String)) -> Result<bool, LuaError> { + let world = ctx.get_world()?; + let mut world = world.write(); let Ok(parent) = Uuid::from_str(&parent) else { - warn!("Provided parent is not a valid UUID"); - return false; + warn!("Provided ancestor is not a valid UUID"); + return Ok(false); }; let Ok(child) = Uuid::from_str(&child) else { - warn!("Provided child is not a valid UUID"); - return false; + warn!("Provided descendant is not a valid UUID"); + return Ok(false); }; - Condition::ChildOf { parent, child }.evaluate(world) + Ok(Condition::ChildOf { + parent, + child, + }.evaluate(&mut world)) } -fn condition_in_room(In((room,)): In<(String,)>, world: &mut World) -> bool { +fn condition_in_room(ctx: &Lua, (room,): (String,)) -> Result<bool, LuaError> { + let world = ctx.get_world()?; + let mut world = world.write(); let Ok(room) = Uuid::from_str(&room) else { warn!("Provided room is not a valid UUID"); - return false; + return Ok(false); }; - Condition::InRoom(room).evaluate(world) + Ok(Condition::InRoom(room).evaluate(&mut world)) } -fn condition_object_in_room(In((object,)): In<(String,)>, world: &mut World) -> bool { +fn condition_object_in_room(ctx: &Lua, (object,): (String,)) -> Result<bool, LuaError> { + let world = ctx.get_world()?; + let mut world = world.write(); let Ok(object) = Uuid::from_str(&object) else { warn!("Provided object is not a valid UUID"); - return false; + return Ok(false); }; - Condition::ObjectInRoom(object).evaluate(world) + Ok(Condition::ObjectInRoom(object).evaluate(&mut world)) } // }}} diff --git a/src/observers.rs b/src/observers.rs index 44edb50..bd1d1a6 100644 --- a/src/observers.rs +++ b/src/observers.rs @@ -7,13 +7,9 @@ use notify::{ }; use crate::{ - components::{DirworldEntity, Persist}, - events::{DirworldChangeRoot, DirworldEnterRoom, DirworldLeaveRoom}, - resources::{ - DirworldCache, DirworldCodecs, DirworldCurrentDir, DirworldObservers, DirworldRootDir, - }, - utils::{despawn_entity_by_path, extract_entity_payload, spawn_entity}, - DirworldWatcherEvent, + cache::DirworldCache, components::{DirworldEntity, Persist}, events::{DirworldChangeRoot, DirworldEnterRoom, DirworldLeaveRoom}, preload::{load_entity, PreloadState, RoomAssets}, resources::{ + DirworldCodecs, DirworldCurrentDir, DirworldObservers, DirworldRootDir, + }, utils::{despawn_entity_by_path, extract_entity_payload}, DirworldWatcherEvent }; /// On navigation from a room, insert modified payloads into the cache @@ -25,10 +21,7 @@ pub fn navigate_from_room( mut event_writer: EventWriter<DirworldLeaveRoom>, ) { for (entity, dirworld_entity) in entities.iter() { - if let Some(payload) = &dirworld_entity.payload { - info!("Caching {entity:?}"); - cache.insert(dirworld_entity.path.clone(), payload.clone()); - } + cache.cache_entity(&dirworld_entity); commands.entity(entity).despawn_recursive(); } event_writer.send(trigger.event().clone()); @@ -43,6 +36,8 @@ pub fn navigate_to_room( mut commands: Commands, mut event_writer: EventWriter<DirworldEnterRoom>, mut current_dir: ResMut<DirworldCurrentDir>, + mut next_preload_state: ResMut<NextState<PreloadState>>, + mut room_assets: ResMut<RoomAssets>, ) { let path = &trigger.event().0; @@ -81,7 +76,15 @@ pub fn navigate_to_room( }; for entry in entries { - spawn_entity(&entry, &mut cache, &codecs, &observers, &mut commands); + load_entity( + &entry, + &mut cache, + &codecs, + &observers, + &mut commands, + &mut next_preload_state, + &mut room_assets, + ); } event_writer.send(trigger.event().clone()); } @@ -94,6 +97,8 @@ pub fn handle_changes( codecs: Res<DirworldCodecs>, mut cache: ResMut<DirworldCache>, mut event_writer: EventWriter<DirworldWatcherEvent>, + mut next_preload_state: ResMut<NextState<PreloadState>>, + mut room_assets: ResMut<RoomAssets>, ) { let event = &trigger.event().0; info!("Watcher Event: {event:?}"); @@ -105,27 +110,39 @@ pub fn handle_changes( } EventKind::Create(_) | EventKind::Modify(ModifyKind::Name(RenameMode::To)) => { for path in &event.paths { - spawn_entity(path, &mut cache, &codecs, &observers, &mut commands); + load_entity( + &path, + &mut cache, + &codecs, + &observers, + &mut commands, + &mut next_preload_state, + &mut room_assets, + ); } } EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => { despawn_entity_by_path(&mut commands, &dirworld_entities, &event.paths[0]); - spawn_entity( + load_entity( &event.paths[1], &mut cache, &codecs, &observers, &mut commands, + &mut next_preload_state, + &mut room_assets, ); } EventKind::Modify(ModifyKind::Metadata(MetadataKind::Any)) => { despawn_entity_by_path(&mut commands, &dirworld_entities, &event.paths[1]); - spawn_entity( + load_entity( &event.paths[0], &mut cache, &codecs, &observers, &mut commands, + &mut next_preload_state, + &mut room_assets, ); } _ => { diff --git a/src/payload/components/mod.rs b/src/payload/components/mod.rs index 2a83713..fc24ac1 100644 --- a/src/payload/components/mod.rs +++ b/src/payload/components/mod.rs @@ -5,24 +5,36 @@ use bevy::prelude::*; use serde::{Deserialize, Serialize}; use yarnspinner::core::YarnValue; +/// Payload component that corresponds to [`bevy::prelude::Transform`] #[derive(Serialize, Deserialize, Clone, Default, Deref, DerefMut, Debug)] pub struct Transform(pub bevy::prelude::Transform); +/// Payload component that represent's an entity's name #[derive(Serialize, Deserialize, Clone, Default, Deref, DerefMut, Debug)] pub struct Name(pub String); +/// Payload component that represents a yarnspinner actor #[derive(Serialize, Deserialize, Clone, Default, Debug)] pub struct Actor { + /// Actor-local variables pub local_variables: HashMap<String, YarnValue>, + /// Source for the yarnspinner dialog pub yarn_source: Vec<u8>, } +/// Payload component that represents a character's voice. Uses rustysynth to generate random MIDI +/// tones based on given parameters. #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Voice { + /// Base MIDI pitch of voice. Defaults to 60 pub pitch: i32, + /// MIDI preset to use for voice. Defaults to 0 pub preset: i32, + /// MIDI bank to use. Defaults to 0 pub bank: i32, + /// Variance in pitch of voice. Defaults to 3 pub variance: u32, + /// Speed of voice. Defaults to 1.0 pub speed: f32, } @@ -38,22 +50,30 @@ impl Default for Voice { } } +/// Payload component that wraps a [`avian3d::prelude::RigidBody`] #[derive(Serialize, Deserialize, Clone, Default, Deref, DerefMut, Debug)] pub struct Rigidbody(pub RigidBody); +/// Payload component that represents mesh colliders that will be generated for this entity #[derive(Serialize, Deserialize, Clone, Default, Debug)] pub struct MeshCollider { + /// Whether the generated colliders should be convex hulls pub convex: bool, + /// Whether the generated colliders should be triggers pub sensor: bool, } +/// Payload component representing a lua script that will be attached to an entity #[derive(Serialize, Deserialize, Clone, Default, Debug)] pub struct Script { + /// Lua script source pub lua_source: Vec<u8>, } +/// Payload component for an arbitrary relationship map, can store 128-bit identifiers indexed by names #[derive(Serialize, Deserialize, Clone, Default, Deref, DerefMut, Debug)] pub struct Relationships(pub HashMap<String, [u8; 16]>); +/// Payload component that indicates that this entity should be able to be picked up #[derive(Serialize, Deserialize, Clone, Default, Debug)] pub struct Pickup; diff --git a/src/payload/mod.rs b/src/payload/mod.rs index dd064f2..68e23a0 100644 --- a/src/payload/mod.rs +++ b/src/payload/mod.rs @@ -1,23 +1,36 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; +/// Payload components pub mod components; +/// Payload steganographically embedded into asset files #[derive(Serialize, Deserialize, Default, Clone, Debug)] pub struct DirworldEntityPayload { + /// Unique identifier for this entity, used by conditional system pub id: Uuid, + /// Transform of this entity pub transform: components::Transform, + /// Name for this entity pub name: Option<components::Name>, + /// Actor information for this entity pub actor: Option<components::Actor>, + /// Voice information for this entity pub voice: Option<components::Voice>, + /// Rigidbody for this entity pub rigidbody: Option<components::Rigidbody>, + /// Mesh collider information for this entity pub mesh_collider: Option<components::MeshCollider>, + /// Lua scripts for this entity pub scripts: Option<Vec<components::Script>>, + /// Relationships for this entity pub relationships: Option<components::Relationships>, + /// Pickup information for this entity pub pickup: Option<components::Pickup>, } impl DirworldEntityPayload { + /// Create a new default payload with a randomized UUID pub fn new() -> Self { Self { id: Uuid::new_v4(), diff --git a/src/preload/events.rs b/src/preload/events.rs new file mode 100644 index 0000000..7167d73 --- /dev/null +++ b/src/preload/events.rs @@ -0,0 +1,12 @@ +use bevy::prelude::*; + +/// Event used to trigger preload callbacks after the asset file has been pre-processed to extract +/// the payload +#[derive(Debug, Event, Clone)] +pub struct DirworldPreload { + /// Entity with the `[DirworldEntity]` component corresponding to the entity being preloaded + pub entity: Entity, + /// The data portion of the file after being pre-processed + pub data: Option<Vec<u8>>, +} + diff --git a/src/preload/mod.rs b/src/preload/mod.rs new file mode 100644 index 0000000..b90db38 --- /dev/null +++ b/src/preload/mod.rs @@ -0,0 +1,82 @@ +use crate::cache::DirworldCache; +use crate::{ + components::DirworldEntity, + resources::{DirworldCodecs, DirworldObservers, EntryType}, + utils::extract_entity_payload, + Extensions, +}; +use bevy::prelude::*; +use std::{collections::HashMap, path::PathBuf}; + +mod systems; + +mod resources; +pub use resources::*; + +mod events; +pub use events::DirworldPreload; + +pub(crate) struct DirworldPreloadPlugin; + +impl Plugin for DirworldPreloadPlugin { + fn build(&self, app: &mut App) { + app.add_systems( + PostUpdate, + systems::handle_preload.run_if(in_state(PreloadState::Loading)), + ) + .add_systems(OnEnter(PreloadState::Done), systems::handle_spawn) + .init_resource::<RoomAssets>() + .init_state::<PreloadState>(); + } +} + +/// State of asset preloading +#[derive(States, Debug, Clone, PartialEq, Eq, Hash, Default)] +pub enum PreloadState { + /// Indicates assets are in the process of loading + #[default] + Loading, + /// Indicates all room assets are finished loading, i.e. all assets are loaded with + /// dependencies + Done, +} + +/// Initiates loading of an asset +// TODO: Make into a command extension +pub fn load_entity( + entry: &PathBuf, + cache: &mut DirworldCache, + codecs: &DirworldCodecs, + observers: &DirworldObservers, + commands: &mut Commands, + preload_state: &mut NextState<PreloadState>, + room_assets: &mut RoomAssets, +) { + let (mut payload, data) = extract_entity_payload(&entry, &codecs); + payload = payload.map(|p| cache.get_entity_cache(&entry).unwrap_or(p)); + let entry_type = if entry.is_dir() { + EntryType::Folder + } else { + EntryType::File(entry.extensions()) + }; + let transform = payload + .as_ref() + .map(|payload| payload.transform.clone()) + .unwrap_or_default(); + let entity = commands + .spawn(( + *transform, + Visibility::Inherited, + DirworldEntity { + path: entry.clone(), + payload, + }, + )) + .id(); + if let Some(observer) = observers.get(&entry_type) { + preload_state.set(PreloadState::Loading); + room_assets.insert(entry.clone(), HashMap::default()); + commands.trigger_targets(DirworldPreload { entity, data }, observer.clone()); + info!("Triggered preload for {entry:?}"); + } +} diff --git a/src/preload/resources.rs b/src/preload/resources.rs new file mode 100644 index 0000000..4060c10 --- /dev/null +++ b/src/preload/resources.rs @@ -0,0 +1,7 @@ +use std::{collections::HashMap, path::PathBuf}; + +use bevy::prelude::*; + +/// A map of asset handles required by each entry in a room, indexed by their paths +#[derive(Resource, Default, Debug, Deref, DerefMut)] +pub struct RoomAssets(pub HashMap<PathBuf, HashMap<String, UntypedHandle>>); diff --git a/src/preload/systems.rs b/src/preload/systems.rs new file mode 100644 index 0000000..ec867ae --- /dev/null +++ b/src/preload/systems.rs @@ -0,0 +1,41 @@ +use bevy::prelude::*; + +use crate::{components::DirworldEntity, events::DirworldSpawn, resources::{DirworldObservers, EntryType}, Extensions}; + +use super::{PreloadState, RoomAssets}; + +pub fn handle_preload( + asset_server: Res<AssetServer>, + room_assets: Res<RoomAssets>, + mut next_state: ResMut<NextState<PreloadState>>, +) { + if room_assets.is_empty() + || room_assets + .values() + .flat_map(|v| v.values()) + .all(|a| asset_server.is_loaded_with_dependencies(a)) + { + info!("Preload Done."); + next_state.set(PreloadState::Done); + } +} + +pub fn handle_spawn( + dirworld_entity_query: Query<(Entity, &DirworldEntity)>, + mut commands: Commands, + observers: Res<DirworldObservers>, +) { + info!("Spawning"); + for (entity, DirworldEntity { path, .. }) in dirworld_entity_query.iter() { + let entry_type = if path.is_dir() { + EntryType::Folder + } else { + EntryType::File(path.extensions()) + }; + if let Some(observer) = observers.get(&entry_type) { + info!("Found observer {observer:?} for {entry_type:?}"); + commands.trigger_targets(DirworldSpawn(entity), observer.clone()); + } + } +} + diff --git a/src/resources.rs b/src/resources.rs index cbf570d..bf0c072 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -1,9 +1,8 @@ -use std::{collections::{BTreeMap, HashMap}, path::PathBuf}; +use std::{collections::BTreeMap, path::PathBuf}; use bevy::{ecs::world::CommandQueue, prelude::*, tasks::Task}; use multi_key_map::MultiKeyMap; use occule::Codec; -use uuid::Uuid; use crate::payload::DirworldEntityPayload; @@ -14,7 +13,9 @@ pub struct DirworldRootDir(pub Option<PathBuf>); /// Current directory of the world #[derive(Resource, Default)] pub struct DirworldCurrentDir{ + /// Path of current directory pub path: PathBuf, + /// Payload (contents of .door file) in current directory, if present pub payload: Option<DirworldEntityPayload>, } @@ -22,18 +23,20 @@ pub struct DirworldCurrentDir{ #[derive(Default, Resource, Deref, DerefMut)] pub struct DirworldTasks(pub BTreeMap<String, Task<Option<CommandQueue>>>); +/// A map between file types and their corresponding preload/spawn callback observers #[derive(Debug, Default, Resource, Deref, DerefMut)] pub struct DirworldObservers(pub MultiKeyMap<EntryType, Entity>); +/// A map between file extensions and their corresponding [`Codec`]s #[derive(Default, Resource, Deref, DerefMut)] pub struct DirworldCodecs(pub MultiKeyMap<String, Box<dyn Codec + Send + Sync>>); +/// Type of a filesystem entry #[derive(Debug, PartialEq, Eq, Hash)] pub enum EntryType { + /// A file with an optional extension File(Option<String>), + /// A folder Folder, } -/// Structure containing payload data for cached (non-current) rooms -#[derive(Resource, Default, Debug, Deref, DerefMut)] -pub struct DirworldCache(pub HashMap<PathBuf, DirworldEntityPayload>); diff --git a/src/room_generation/mod.rs b/src/room_generation/mod.rs deleted file mode 100644 index e69de29..0000000 --- a/src/room_generation/mod.rs +++ /dev/null diff --git a/src/systems.rs b/src/systems.rs index 3f894ec..d6840ee 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -3,7 +3,7 @@ use bevy::{ tasks::{block_on, futures_lite::future}, }; -use crate::{components::DirworldEntity, resources::DirworldTasks}; +use crate::resources::DirworldTasks; pub fn remove_completed_tasks(mut commands: Commands, mut tasks: ResMut<DirworldTasks>) { tasks.retain(|_, task| { @@ -16,15 +16,15 @@ pub fn remove_completed_tasks(mut commands: Commands, mut tasks: ResMut<Dirworld }); } -pub fn sync_entity_transforms( - mut dirworld_entity_query: Query<(&mut DirworldEntity, Ref<Transform>, &GlobalTransform)>, -) { - for (mut dirworld_entity, transform, global_transform) in dirworld_entity_query.iter_mut() { - if transform.is_changed() && !transform.is_added() { - if let Some(payload) = &mut dirworld_entity.payload { - let transform = global_transform.compute_transform(); - *payload.transform = transform; - } - } - } -} +// pub fn sync_entity_transforms( +// mut dirworld_entity_query: Query<(&mut DirworldEntity, Ref<Transform>, &GlobalTransform)>, +// ) { +// for (mut dirworld_entity, transform, global_transform) in dirworld_entity_query.iter_mut() { +// if transform.is_changed() && !transform.is_added() { +// if let Some(payload) = &mut dirworld_entity.payload { +// let transform = global_transform.compute_transform(); +// *payload.transform = transform; +// } +// } +// } +// } diff --git a/src/utils.rs b/src/utils.rs index e4692bb..74451ae 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -3,9 +3,10 @@ use std::{fs, path::PathBuf}; use bevy::prelude::*; use crate::{ - components::DirworldEntity, events::DirworldSpawn, payload::DirworldEntityPayload, resources::{DirworldCache, DirworldCodecs, DirworldObservers, EntryType}, Extensions + components::DirworldEntity, payload::DirworldEntityPayload, resources::DirworldCodecs, Extensions }; +/// Extracts the binary payload from a file pub fn extract_entity_payload( path: &PathBuf, codecs: &DirworldCodecs, @@ -62,41 +63,7 @@ pub fn extract_entity_payload( (payload, data) } -pub fn spawn_entity( - entry: &PathBuf, - cache: &mut DirworldCache, - codecs: &DirworldCodecs, - observers: &DirworldObservers, - commands: &mut Commands, -) { - let (mut payload, data) = extract_entity_payload(&entry, &codecs); - if let Some(cached_payload) = cache.remove(entry) { - payload = Some(cached_payload); - } - - let transform = payload.as_ref().map(|payload| payload.transform.clone()).unwrap_or_default(); - let entry_type = if entry.is_dir() { - EntryType::Folder - } else { - EntryType::File(entry.extensions()) - }; - let entity = commands - .spawn(( - SpatialBundle { - transform: *transform, - ..Default::default() - }, - DirworldEntity { - path: entry.clone(), - payload, - }, - )) - .id(); - if let Some(observer) = observers.get(&entry_type) { - commands.trigger_targets(DirworldSpawn { entity, data }, observer.clone()); - } -} - +/// Despawns an entity corresponding to a path on the filesystem pub fn despawn_entity_by_path( commands: &mut Commands, dirworld_entities: &Query<(Entity, &DirworldEntity)>, diff --git a/src/watcher.rs b/src/watcher.rs index 8918dda..0e23c55 100644 --- a/src/watcher.rs +++ b/src/watcher.rs @@ -1,21 +1,16 @@ use std::{ - path::{Path, PathBuf}, + path::PathBuf, time::Duration, }; use async_channel::{Receiver, Sender}; use bevy::{prelude::*, tasks::IoTaskPool}; -use notify::{ - event::{AccessKind, AccessMode, DataChange, MetadataKind, ModifyKind, RenameMode}, - EventKind, RecursiveMode, Watcher, -}; +use notify::RecursiveMode; use notify_debouncer_full::{new_debouncer, DebounceEventResult}; -use crate::{ - components::DirworldEntity, - resources::{DirworldCache, DirworldCodecs, DirworldObservers, DirworldRootDir}, -}; +use crate::resources::DirworldRootDir; +/// SystemSet for dirworld watcher systems #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] pub struct DirworldWatcherSet; @@ -94,4 +89,3 @@ pub fn update( } } } - |