diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/actor.rs | 8 | ||||
-rw-r--r-- | src/commands.rs | 547 | ||||
-rw-r--r-- | src/components.rs | 11 | ||||
-rw-r--r-- | src/events.rs | 6 | ||||
-rw-r--r-- | src/lib.rs | 153 | ||||
-rw-r--r-- | src/lua_api.rs | 53 | ||||
-rw-r--r-- | src/payload.rs | 84 | ||||
-rw-r--r-- | src/resources.rs | 93 | ||||
-rw-r--r-- | src/systems.rs | 14 | ||||
-rw-r--r-- | src/watcher.rs | 139 |
10 files changed, 1027 insertions, 81 deletions
diff --git a/src/actor.rs b/src/actor.rs new file mode 100644 index 0000000..3198f32 --- /dev/null +++ b/src/actor.rs @@ -0,0 +1,8 @@ +use bevy::{prelude::*, utils::HashMap}; +use yarnspinner::{core::LineId, runtime::Dialogue}; + +#[derive(Component)] +pub struct Actor { + pub dialogue: Dialogue, + pub metadata: HashMap<LineId, Vec<String>>, +} diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..69cc83a --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,547 @@ +use std::{ + fs, iter, + path::{Path, PathBuf}, +}; + +use bevy::{ + ecs::{ + system::SystemState, + world::{Command, CommandQueue}, + }, + prelude::*, + tasks::AsyncComputeTaskPool, +}; +use crypto::{ + aes::KeySize, + blockmodes::{EcbEncryptor, PkcsPadding}, + buffer::{BufferResult, ReadBuffer, RefReadBuffer, RefWriteBuffer, WriteBuffer}, +}; +use occule::Error; +use xz2::read::{XzDecoder, XzEncoder}; + +use crate::{ + components::DirworldEntity, + events::{DirworldNavigationEvent, DirworldSpawn}, + payload::{DirworldComponent, DirworldComponentDiscriminants, DirworldEntityPayload}, + resources::{ + DirworldCodecs, DirworldCurrentDir, DirworldObservers, DirworldRootDir, DirworldTasks, + EntryType, + }, + Extensions, +}; + +struct DirworldNavigateCommand { + pub path: PathBuf, +} + +impl Command for DirworldNavigateCommand { + fn apply(self, world: &mut World) { + let root_dir = world.remove_resource::<DirworldRootDir>().unwrap(); + let mut current_dir = world.remove_resource::<DirworldCurrentDir>().unwrap(); + + let current_path; + let old_dir; + if let Some(old_path) = ¤t_dir.0 { + world.send_event(DirworldNavigationEvent::LeftRoom { + path: old_path.clone(), + }); + + current_path = old_path.join(self.path); + old_dir = Some(old_path.clone()); + } else { + current_path = self.path; + old_dir = None; + } + current_dir.0 = Some(current_path.clone()); + + let mut system_state: SystemState<( + Commands, + Query<(Entity, &DirworldEntity)>, + Res<DirworldObservers>, + Res<DirworldCodecs>, + )> = SystemState::new(world); + let (mut commands, dirworld_entities, observers, codecs) = system_state.get_mut(world); + update_entries( + &mut commands, + &dirworld_entities, + old_dir, + ¤t_path, + &root_dir.0.clone().unwrap(), + &observers, + &codecs, + ); + system_state.apply(world); + + world.send_event(DirworldNavigationEvent::EnteredRoom { path: current_path }); + world.insert_resource(current_dir); + world.insert_resource(root_dir); + } +} + +pub(crate) fn update_entries( + commands: &mut Commands, + dirworld_entities: &Query<(Entity, &DirworldEntity)>, + old_dir: Option<PathBuf>, + current_dir: &PathBuf, + project_dir: &PathBuf, + observers: &DirworldObservers, + codecs: &DirworldCodecs, +) { + let directory = current_dir.read_dir().unwrap(); + + if let Some(old_dir) = old_dir { + let mut entities_to_despawn = vec![]; + for (entity, dirworld_entity) in dirworld_entities.iter() { + if dirworld_entity.path.parent().unwrap() == old_dir { + entities_to_despawn.push(entity); + } + } + for entity in entities_to_despawn { + commands.entity(entity).despawn_recursive(); + } + } + + let mut entry_paths: Vec<PathBuf> = directory + .flatten() + .map(|entry| entry.path().canonicalize().unwrap()) + .collect::<Vec<_>>(); + entry_paths.retain(|entry| { + !entry + .file_name() + .is_some_and(|entry| entry.to_string_lossy().starts_with(".")) + }); + if current_dir != project_dir { + entry_paths = iter::once(current_dir.join("..")) + .chain(entry_paths) + .collect(); + } + + for entry_path in entry_paths { + process_entry(commands, &entry_path, &observers, &codecs); + } +} + +pub(crate) fn process_entry( + commands: &mut Commands, + entry_path: &PathBuf, + observers: &DirworldObservers, + codecs: &DirworldCodecs, +) { + let (payload, data) = extract_payload(entry_path, codecs); + let transform = if let Some(component) = payload + .as_ref() + .and_then(|payload| payload.component("Transform")) + { + if let DirworldComponent::Transform(component) = component { + component.clone() + } else { + panic!("Failed to decompose component") + } + } else { + Transform::default() + }; + + let entity = commands.spawn(( + SpatialBundle { + transform, + ..Default::default() + }, + DirworldEntity { + path: entry_path.clone(), + payload: payload.clone(), + }, + )); + + let entity = entity.id(); + let entry_type = if entry_path.is_dir() { + EntryType::Folder + } else { + let extensions = entry_path.extensions(); + EntryType::File(extensions) + }; + if let Some(observer) = observers.get(&entry_type) { + commands.trigger_targets(DirworldSpawn { entity, data }, observer.clone()); + } +} + +fn extract_payload( + entry_path: &PathBuf, + codecs: &DirworldCodecs, +) -> (Option<DirworldEntityPayload>, Option<Vec<u8>>) { + let entry_type = if entry_path.is_dir() { + EntryType::Folder + } else { + let extensions = entry_path.extensions(); + EntryType::File(extensions) + }; + + let mut data: Option<Vec<u8>> = None; + let mut payload: Option<DirworldEntityPayload> = None; + match &entry_type { + EntryType::File(Some(extension)) => { + if let Ok(file_data) = fs::read(entry_path.clone()) { + match codecs.get(extension) { + Some(codec) => match codec.decode(&file_data.as_slice()) { + Ok((carrier, extracted_payload)) => { + match rmp_serde::from_slice::<DirworldEntityPayload>( + extracted_payload.as_slice(), + ) { + Ok(deserialized_payload) => { + data = Some(carrier); + payload = Some(deserialized_payload); + } + Err(e) => { + warn!("{:?}", e); + data = Some(file_data); + } + } + } + Err(e) => match e { + Error::DataNotEncoded => { + data = Some(file_data); + } + _ => error!("{:?}", e), + }, + }, + None => { + data = Some(file_data); + } + } + } else { + warn!("Failed to read data from {entry_path:?}"); + } + } + EntryType::Folder => { + let door_path = entry_path.join(".door"); + if door_path.exists() { + let door_file_data = fs::read(door_path).unwrap(); + match rmp_serde::from_slice::<DirworldEntityPayload>(&door_file_data.as_slice()) { + Ok(deserialized_payload) => { + payload = Some(deserialized_payload); + } + Err(e) => { + warn!("{:?}", e); + } + } + } + } + _ => {} + } + (payload, data) +} + +struct DirworldChangeRootCommand { + pub path: PathBuf, +} + +impl Command for DirworldChangeRootCommand { + fn apply(self, world: &mut World) { + let mut root_dir = world.remove_resource::<DirworldRootDir>().unwrap(); + let mut current_dir = world.remove_resource::<DirworldCurrentDir>().unwrap(); + + let old_root; + if let DirworldRootDir(Some(old_dir)) = root_dir { + world.send_event(DirworldNavigationEvent::LeftRoom { + path: self.path.clone(), + }); + old_root = Some(old_dir); + } else { + old_root = None; + } + + root_dir.0 = Some(self.path.canonicalize().unwrap()); + current_dir.0 = Some(self.path.canonicalize().unwrap()); + + let mut system_state: SystemState<( + Commands, + Query<(Entity, &DirworldEntity)>, + Res<DirworldObservers>, + Res<DirworldCodecs>, + )> = SystemState::new(world); + let (mut commands, dirworld_entities, observers, codecs) = system_state.get_mut(world); + update_entries( + &mut commands, + &dirworld_entities, + old_root, + ¤t_dir.0.clone().unwrap(), + &root_dir.0.clone().unwrap(), + &observers, + &codecs, + ); + system_state.apply(world); + + world.send_event(DirworldNavigationEvent::EnteredRoom { path: self.path }); + + world.insert_resource(root_dir); + world.insert_resource(current_dir); + } +} + +struct DirworldLockDoorCommand { + path: PathBuf, + key: Vec<u8>, +} + +impl Command for DirworldLockDoorCommand { + fn apply(self, world: &mut World) { + let path = self.path.clone(); + // Get existing payload + let codecs = world.remove_resource::<DirworldCodecs>().unwrap(); + let (payload, _) = extract_payload(&path, &codecs); + world.insert_resource(codecs); + let task = AsyncComputeTaskPool::get().spawn(async move { + // Tar directory + let mut tar = tar::Builder::new(Vec::new()); + tar.append_dir_all(path.file_stem().unwrap(), path.clone()) + .unwrap(); + let tar_buffer = tar.into_inner().unwrap(); + + // XZ archive + let tar_xz = XzEncoder::new(tar_buffer.as_slice(), 0).into_inner(); + + // Encrypt archive + let mut crypter = + crypto::aes::ecb_encryptor(KeySize::KeySize128, &self.key[..16], PkcsPadding); + let mut encrypted = vec![]; + let mut buffer = [0; 4096]; + + let mut read_buffer = RefReadBuffer::new(tar_xz); + let mut write_buffer = RefWriteBuffer::new(&mut buffer); + loop { + let result = crypter + .encrypt(&mut read_buffer, &mut write_buffer, true) + .expect("Failed to encrypt data!"); + encrypted.extend(write_buffer.take_read_buffer().take_remaining().iter().map(|&i|i)); + match result { + BufferResult::BufferUnderflow => break, + BufferResult::BufferOverflow => {} + } + } + + let newpath = format!("{}.tar.xz.aes", path.display()); + fs::write(&newpath, encrypted).unwrap(); + + // Remove original folder + fs::remove_dir_all(path).unwrap(); + + // Insert key hash as payload relationship + let key_digest = md5::compute(&self.key[..16]); + let mut payload = payload.unwrap_or_default(); + payload.push(DirworldComponent::Relationship { + label: "key".into(), + hash: key_digest.0, + }); + + // Write payload + let mut command_queue = CommandQueue::default(); + command_queue.push(DirworldSaveEntityCommand { + path: newpath.into(), + payload, + }); + Some(command_queue) + }); + world.resource_mut::<DirworldTasks>().insert( + format!("Locking {:?}", self.path.file_name().unwrap()), + task, + ); + } +} + +struct DirworldUnlockDoorCommand { + path: PathBuf, + key: Vec<u8>, +} + +impl Command for DirworldUnlockDoorCommand { + fn apply(self, world: &mut World) { + let path = self.path.clone(); + // Get existing payload + let codecs = world.remove_resource::<DirworldCodecs>().unwrap(); + let (payload, carrier) = extract_payload(&path, &codecs); + world.insert_resource(codecs); + let task = AsyncComputeTaskPool::get().spawn(async move { + // Decrypt archive + let mut decrypter = + crypto::aes::ecb_decryptor(KeySize::KeySize128, &self.key[..16], PkcsPadding); + let encrypted = carrier.unwrap(); + let mut decrypted = vec![]; + let mut buffer = [0; 4096]; + + let mut read_buffer = RefReadBuffer::new(&encrypted); + let mut write_buffer = RefWriteBuffer::new(&mut buffer); + loop { + let result = decrypter + .decrypt(&mut read_buffer, &mut write_buffer, true) + .expect("Failed to encrypt data!"); + decrypted.extend(write_buffer.take_read_buffer().take_remaining().iter().map(|&i|i)); + match result { + BufferResult::BufferUnderflow => break, + BufferResult::BufferOverflow => {} + } + } + + // Unzip archive + let tar = XzDecoder::new(decrypted.as_slice()).into_inner(); + + // Untar archive + let mut tar = tar::Archive::new(tar); + let parent = path.parent().unwrap(); + tar.unpack(parent).unwrap(); + + fs::remove_file(path.clone()).unwrap(); + + if let Some(mut payload) = payload { + for (index, relationship) in payload.iter().enumerate().filter(|(_, x)| { + DirworldComponentDiscriminants::from(*x) + == DirworldComponentDiscriminants::Relationship + }) { + if let DirworldComponent::Relationship { label, .. } = relationship { + if label == "key" { + payload.remove(index); + break; + } + } + } + + // Write payload + let mut command_queue = CommandQueue::default(); + let new_path = parent.join(path.file_stem_no_extensions().unwrap()); + let _ = fs::create_dir(new_path.clone()); + command_queue.push(DirworldSaveEntityCommand { + path: new_path.into(), + payload, + }); + return Some(command_queue); + } + None + }); + world.resource_mut::<DirworldTasks>().insert( + format!("Unlocking {:?}", self.path.file_name().unwrap()), + task, + ); + } +} + +struct DirworldSaveEntityCommand { + path: PathBuf, + payload: DirworldEntityPayload, +} + +impl Command for DirworldSaveEntityCommand { + fn apply(self, world: &mut World) { + info!("Saving {}", &self.path.display()); + let is_dir = self.path.is_dir(); + let observers = world.remove_resource::<DirworldObservers>().unwrap(); + let codecs = world.remove_resource::<DirworldCodecs>().unwrap(); + let codec = if is_dir { + None + } else { + match codecs.get(&self.path.extensions().unwrap()) { + Some(codec) => Some(codec), + None => { + warn!( + "No matching codec found for {:?}", + self.path.file_name().unwrap() + ); + world.insert_resource(codecs); + world.insert_resource(observers); + return; + } + } + }; + + let payload = match rmp_serde::to_vec(&self.payload) { + Ok(payload) => payload, + Err(e) => { + error!("{e:?}"); + world.insert_resource(codecs); + world.insert_resource(observers); + return; + } + }; + + if is_dir { + let target_path = self.path.join(".door"); + if let Err(e) = fs::write(target_path, payload) { + error!("{e:?}"); + } + } else { + let codec = codec.unwrap(); + let carrier = match fs::read(&self.path) { + Ok(raw_carrier) => match codec.decode(&raw_carrier) { + Ok((carrier, _)) => carrier, + Err(e) => match e { + Error::DependencyError(_) => { + error!("{e:?}"); + world.insert_resource(codecs); + world.insert_resource(observers); + return; + } + _ => raw_carrier, + }, + }, + Err(e) => { + error!("{e:?}"); + world.insert_resource(codecs); + world.insert_resource(observers); + return; + } + }; + + let encoded = match codec.encode(&carrier, &payload) { + Ok(encoded) => encoded, + Err(e) => { + error!("Error encoding payload: {e:?}"); + world.insert_resource(codecs); + world.insert_resource(observers); + return; + } + }; + if let Err(e) = fs::write(&self.path, encoded) { + error!("{e:?}"); + } + } + + world.insert_resource(codecs); + world.insert_resource(observers); + } +} + +/// Commands for dirworld navigation +pub trait DirworldCommands { + /// Change the root of the world. This will also set the current directory. This is not really meant to be used in-game but is useful for editor applications. + fn dirworld_change_root(&mut self, path: PathBuf); + + /// Move to given directory + fn dirworld_navigate(&mut self, path: PathBuf); + + /// Lock Door + fn dirworld_lock_door(&mut self, path: PathBuf, key: Vec<u8>); + + /// Unlock Door + fn dirworld_unlock_door(&mut self, path: PathBuf, key: Vec<u8>); + + fn dirworld_save_entity(&mut self, path: PathBuf, payload: DirworldEntityPayload); +} + +impl<'w, 's> DirworldCommands for Commands<'w, 's> { + fn dirworld_change_root(&mut self, path: PathBuf) { + self.add(DirworldChangeRootCommand { path }); + } + + fn dirworld_navigate(&mut self, path: PathBuf) { + self.add(DirworldNavigateCommand { path }); + } + + fn dirworld_lock_door(&mut self, path: PathBuf, key: Vec<u8>) { + self.add(DirworldLockDoorCommand { key, path }); + } + + fn dirworld_unlock_door(&mut self, path: PathBuf, key: Vec<u8>) { + self.add(DirworldUnlockDoorCommand { key, path }); + } + + fn dirworld_save_entity(&mut self, path: PathBuf, payload: DirworldEntityPayload) { + self.add(DirworldSaveEntityCommand { path, payload }); + } +} diff --git a/src/components.rs b/src/components.rs index 69a5cde..ea76185 100644 --- a/src/components.rs +++ b/src/components.rs @@ -1,5 +1,16 @@ +use std::path::PathBuf; + use bevy::prelude::*; +use crate::payload::DirworldEntityPayload; + /// A tooltip on an object, which can be displayed. #[derive(Component)] 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 { + pub path: PathBuf, + pub payload: Option<DirworldEntityPayload>, +} diff --git a/src/events.rs b/src/events.rs index 0c2e7f1..41a0db3 100644 --- a/src/events.rs +++ b/src/events.rs @@ -16,3 +16,9 @@ pub enum DirworldNavigationEvent { path: PathBuf, }, } + +#[derive(Event)] +pub struct DirworldSpawn { + pub entity: Entity, + pub data: Option<Vec<u8>>, +}
\ No newline at end of file @@ -1,12 +1,19 @@ -#![warn(missing_docs)] +// #![warn(missing_docs)] //! Plugin for bevy engine enabling interaction with and representation of the file system in the world. -use std::path::PathBuf; +use std::{ffi::OsStr, path::PathBuf}; -use bevy::prelude::*; -use events::DirworldNavigationEvent; -use resources::{Dirworld, DirworldConfig}; +use bevy::{ecs::system::IntoObserverSystem, prelude::*}; +use bevy_scriptum::{runtimes::lua::LuaRuntime, BuildScriptingRuntime, ScriptingRuntimeBuilder}; +use events::{DirworldNavigationEvent, DirworldSpawn}; +use occule::Codec; +use resources::{ + DirworldCodecs, DirworldCurrentDir, DirworldObservers, DirworldRootDir, DirworldTasks, + EntryType, +}; +pub use watcher::DirworldWatcherEvent; +pub use watcher::DirworldWatcherSet; /// Components used by this plugin pub mod components; @@ -17,16 +24,144 @@ pub mod events; /// Resources used by this plugin pub mod resources; +mod watcher; + +/// Commands for this plugin +pub mod commands; + +mod systems; + +/// Payload for dirworld entities +pub mod payload; + +/// Actor component +pub mod actor; + +mod lua_api; + /// Plugin which enables high-level interaction +#[derive(Default)] pub struct DirworldPlugin { - /// Root path of world - pub path: PathBuf, + pub register_custom_lua_api: + Option<Box<dyn Fn(ScriptingRuntimeBuilder<LuaRuntime>) + Send + Sync>>, } impl Plugin for DirworldPlugin { fn build(&self, app: &mut App) { - app.insert_resource(DirworldConfig::new(self.path.clone())) + info!("building"); + app.add_systems(Startup, watcher::setup) + .add_systems( + Update, + (systems::remove_completed_tasks, lua_api::trigger_update), + ) + .add_systems(PostUpdate, watcher::update) + .add_systems( + PreUpdate, + watcher::handle_changes, + ) + .add_scripting::<LuaRuntime>(|runtime| { + let runtime = lua_api::register(runtime); + if let Some(register_custom) = &self.register_custom_lua_api { + (register_custom)(runtime); + } + }) .add_event::<DirworldNavigationEvent>() - .init_resource::<Dirworld>(); + .init_resource::<DirworldRootDir>() + .init_resource::<DirworldCurrentDir>() + .init_resource::<DirworldTasks>() + .init_resource::<DirworldObservers>() + .init_resource::<DirworldCodecs>() + .add_event::<DirworldWatcherEvent>(); + } +} + +pub trait Extensions { + fn extensions(&self) -> Option<String>; + + fn file_stem_no_extensions(&self) -> Option<String>; + + fn no_extensions(&self) -> PathBuf; +} + +impl Extensions for PathBuf { + fn extensions(&self) -> Option<String> { + let mut temp_path = self.clone(); + let mut extensions = Vec::<String>::new(); + while let Some(extension) = temp_path.extension() { + extensions.insert(0, extension.to_string_lossy().into()); + temp_path.set_extension(""); + } + if extensions.is_empty() { + None + } else { + Some(extensions.join(".")) + } + } + + fn file_stem_no_extensions(&self) -> Option<String> { + let mut temp_path = self.clone(); + while let Some(_) = temp_path.extension() { + temp_path.set_extension(""); + } + temp_path + .file_stem() + .and_then(OsStr::to_str) + .map(str::to_string) + } + + fn no_extensions(&self) -> PathBuf { + let mut temp_path = self.clone(); + while let Some(_) = temp_path.extension() { + temp_path.set_extension(""); + } + temp_path + } +} + +pub trait DirworldApp { + fn register_dirworld_entry_callback<B: Bundle, M>( + &mut self, + extensions: Vec<EntryType>, + observer: impl IntoObserverSystem<DirworldSpawn, B, M>, + ) -> &mut Self; + + fn register_dirworld_entry_codec<C: Codec + Send + Sync + 'static>( + &mut self, + extensions: Vec<String>, + codec: C, + ) -> &mut Self; +} + +impl DirworldApp for App { + fn register_dirworld_entry_callback<B: Bundle, M>( + &mut self, + extensions: Vec<EntryType>, + observer: impl IntoObserverSystem<DirworldSpawn, B, M>, + ) -> &mut Self { + let world = self.world_mut(); + let observer_entity_id; + + { + let mut observer_entity = world.spawn_empty(); + observer_entity_id = observer_entity.id(); + observer_entity.insert(Observer::new(observer).with_entity(observer_entity_id)); + } + + world.flush(); + world + .resource_mut::<DirworldObservers>() + .insert_many(extensions, observer_entity_id); + self + } + + fn register_dirworld_entry_codec<C: Codec + Send + Sync + 'static>( + &mut self, + extensions: Vec<String>, + codec: C, + ) -> &mut Self { + self.world_mut() + .resource_mut::<DirworldCodecs>() + .insert_many(extensions, Box::new(codec)); + self } } diff --git a/src/lua_api.rs b/src/lua_api.rs new file mode 100644 index 0000000..cda2486 --- /dev/null +++ b/src/lua_api.rs @@ -0,0 +1,53 @@ +use bevy::prelude::*; +use bevy_scriptum::{runtimes::lua::{BevyEntity, BevyVec3, LuaRuntime, LuaScriptData}, ScriptingRuntimeBuilder, Runtime}; + +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); + } + } +} + +// ACTUAL API STUFF BELOW THIS POINT {{{ + +macro_rules! register_fns { + ($runtime:expr, $($function:expr),+) => { + { + $runtime$(.add_function(stringify!($function).to_string(), $function))+ + } + }; +} + +pub fn register(runtime: ScriptingRuntimeBuilder<LuaRuntime>) -> ScriptingRuntimeBuilder<LuaRuntime> { + register_fns!(runtime, translate, rotate) +} + +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 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!"); + } + } +} + +// }}} diff --git a/src/payload.rs b/src/payload.rs new file mode 100644 index 0000000..2aa3f23 --- /dev/null +++ b/src/payload.rs @@ -0,0 +1,84 @@ +use std::{collections::HashMap, str::FromStr}; + +use avian3d::prelude::RigidBody; +use bevy::prelude::*; +use serde::{Deserialize, Serialize}; +use strum::{EnumDiscriminants, EnumString}; +use yarnspinner::core::YarnValue; + +#[derive(Serialize, Deserialize, Default, Clone, Deref, DerefMut, Debug)] +pub struct DirworldEntityPayload(Vec<DirworldComponent>); + +impl DirworldEntityPayload { + pub fn component(&self, name: &str) -> Option<&DirworldComponent> { + if let Ok(discriminant) = DirworldComponentDiscriminants::from_str(name) { + self.iter() + .find(|component| discriminant == DirworldComponentDiscriminants::from(*component)) + } else { + None + } + } + + pub fn component_mut(&mut self, name: &str) -> Option<&mut DirworldComponent> { + if let Ok(discriminant) = DirworldComponentDiscriminants::from_str(name) { + self.iter_mut() + .find(|component| discriminant == DirworldComponentDiscriminants::from(&**component)) + } else { + None + } + } + + pub fn components(&self, name: &str) -> Vec<&DirworldComponent> { + if let Ok(discriminant) = DirworldComponentDiscriminants::from_str(name) { + self.iter() + .filter(|component| { + discriminant == DirworldComponentDiscriminants::from(*component) + }) + .collect() + } else { + vec![] + } + } + + pub fn components_mut(&mut self, name: &str) -> Vec<&mut DirworldComponent> { + if let Ok(discriminant) = DirworldComponentDiscriminants::from_str(name) { + self.iter_mut() + .filter(|component| { + discriminant == DirworldComponentDiscriminants::from(&**component) + }) + .collect() + } else { + vec![] + } + } +} + +#[derive(Serialize, Deserialize, Clone, EnumDiscriminants, Debug)] +#[strum_discriminants(derive(EnumString))] +pub enum DirworldComponent { + Transform(Transform), + Name(String), + Actor { + local_variables: HashMap<String, YarnValue>, + yarn_source: Vec<u8>, + }, + Voice { + pitch: i32, + preset: i32, + bank: i32, + variance: u32, + speed: f32, + }, + Rigidbody(RigidBody), + MeshCollider { + convex: bool, + sensor: bool, + }, + Script { + lua_source: Vec<u8>, + }, + Relationship { + label: String, + hash: [u8; 16], + }, +} diff --git a/src/resources.rs b/src/resources.rs index c145960..c9f7b40 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -1,80 +1,29 @@ -use std::{ - fs, - path::{Path, PathBuf}, -}; +use std::{collections::BTreeMap, path::PathBuf}; -use anyhow::{Context, Result}; -use bevy::prelude::*; +use bevy::{ecs::world::CommandQueue, prelude::*, tasks::Task}; +use multi_key_map::MultiKeyMap; +use occule::Codec; -use crate::events::DirworldNavigationEvent; +/// Root directory of the world +#[derive(Resource, Deref, DerefMut, Default)] +pub struct DirworldRootDir(pub Option<PathBuf>); -/// Configuration for Dirworld. -#[derive(Resource)] -pub struct DirworldConfig { - root: PathBuf, -} - -impl DirworldConfig { - /// Construct a new dirworld config with the given root path. Will panic if the provided path - /// cannot be canonicalized. - // TODO: Don't panic? lol - pub fn new(root: PathBuf) -> Self { - Self { - root: fs::canonicalize(root).expect("Failed to canonicalize path!"), - } - } - - /// - pub fn root(&self) -> &PathBuf { - &self.root - } -} +/// Current directory of the world +#[derive(Resource, Deref, DerefMut, Default)] +pub struct DirworldCurrentDir(pub Option<PathBuf>); -/// Contains the dirworld state. -#[derive(Resource)] -pub struct Dirworld { - /// Current active directory. - pub path: PathBuf, +/// Running background tasks +#[derive(Default, Resource, Deref, DerefMut)] +pub struct DirworldTasks(pub BTreeMap<String, Task<Option<CommandQueue>>>); - /// Entities local to the current room. - pub tracked_entities: Vec<Entity>, -} +#[derive(Debug, Default, Resource, Deref, DerefMut)] +pub(crate) struct DirworldObservers(pub MultiKeyMap<EntryType, Entity>); -impl FromWorld for Dirworld { - fn from_world(world: &mut World) -> Self { - let config = world.remove_resource::<DirworldConfig>().unwrap(); - world.send_event(DirworldNavigationEvent::EnteredRoom { - path: config.root().clone(), - }); - let result = Self { - path: config.root().clone(), - tracked_entities: vec![], - }; - world.insert_resource(config); - result - } -} +#[derive(Default, Resource, Deref, DerefMut)] +pub(crate) struct DirworldCodecs(pub MultiKeyMap<String, Box<dyn Codec + Send + Sync>>); -impl Dirworld { - /// Move into a new room. - // TODO: Clear tracked entities? - // TODO: Make into command extension trait? - pub fn navigate_to( - &mut self, - path: PathBuf, - event_writer: &mut EventWriter<DirworldNavigationEvent>, - ) -> Result<()> { - event_writer.send(DirworldNavigationEvent::LeftRoom { - path: self.path.clone(), - }); - self.path = Path::new(&self.path) - .join(path) - .to_str() - .context("Path not valid UTF-8")? - .into(); - event_writer.send(DirworldNavigationEvent::EnteredRoom { - path: self.path.clone(), - }); - Ok(()) - } +#[derive(Debug, PartialEq, Eq, Hash)] +pub enum EntryType { + File(Option<String>), + Folder, } diff --git a/src/systems.rs b/src/systems.rs new file mode 100644 index 0000000..ac5f667 --- /dev/null +++ b/src/systems.rs @@ -0,0 +1,14 @@ +use bevy::{prelude::{Commands, ResMut}, tasks::{block_on, futures_lite::future}}; + +use crate::resources::DirworldTasks; + +pub fn remove_completed_tasks(mut commands: Commands, mut tasks: ResMut<DirworldTasks>) { + tasks.retain(|_, task| { + if task.is_finished() { + if let Some(Some(mut command_queue)) = block_on(future::poll_once(&mut *task)) { + commands.append(&mut command_queue); + } + } + !task.is_finished() + }); +} diff --git a/src/watcher.rs b/src/watcher.rs new file mode 100644 index 0000000..b94c0d4 --- /dev/null +++ b/src/watcher.rs @@ -0,0 +1,139 @@ +use std::{path::{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_debouncer_full::{new_debouncer, DebounceEventResult}; + +use crate::{ + commands::process_entry, + components::DirworldEntity, + resources::{DirworldCodecs, DirworldObservers, DirworldRootDir}, +}; + +#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] +pub struct DirworldWatcherSet; + +/// Event fired when a file watcher event is caught. +#[derive(Event)] +pub struct DirworldWatcherEvent(pub notify::Event); + +#[derive(Resource)] +pub struct WatcherChannels { + tx_control: Sender<PathBuf>, + rx_changes: Receiver<notify::Event>, +} + +pub fn setup(mut commands: Commands) { + let (tx_control, rx_control) = async_channel::unbounded(); + let (tx_changes, rx_changes) = async_channel::unbounded(); + IoTaskPool::get() + .spawn(async move { file_watcher(rx_control, tx_changes).await }) + .detach(); + + commands.insert_resource(WatcherChannels { + tx_control, + rx_changes, + }) +} + +async fn file_watcher(rx: Receiver<PathBuf>, tx: Sender<notify::Event>) { + let (watcher_tx, watcher_rx) = std::sync::mpsc::channel(); + let mut debouncer = new_debouncer(Duration::from_millis(500), None, move |result: DebounceEventResult| { + match result { + Ok(events) => for event in events.iter() { + watcher_tx.send(event.clone()).unwrap(); + } + Err(errors) => for error in errors.iter() { + error!("{error:?}"); + } + } + }).unwrap(); + let mut old_path: Option<PathBuf> = None; + loop { + while let Ok(message) = rx.try_recv() { + if let Some(old_path) = &old_path { + debouncer.watcher().unwatch(old_path).unwrap(); + } + debouncer.watcher().watch(&message, RecursiveMode::NonRecursive).unwrap(); + old_path = Some(message); + } + + while let Ok(event) = watcher_rx.try_recv() { + tx.send(event.event.clone()).await.unwrap(); + } + } +} + +pub fn update( + watcher_channels: Res<WatcherChannels>, + mut event_writer: EventWriter<DirworldWatcherEvent>, + root_dir: Res<DirworldRootDir>, +) { + if root_dir.is_changed() { + if let Some(project_dir) = &root_dir.0 { + let _ = watcher_channels.tx_control.try_send(project_dir.clone()); + } + } else { + while let Ok(event) = watcher_channels.rx_changes.try_recv() { + event_writer.send(DirworldWatcherEvent(event)); + } + } +} + +pub fn handle_changes( + mut event_reader: EventReader<DirworldWatcherEvent>, + mut commands: Commands, + dirworld_entities: Query<(Entity, &DirworldEntity)>, + observers: Res<DirworldObservers>, + codecs: Res<DirworldCodecs>, +) { + if !event_reader.is_empty() { + for DirworldWatcherEvent(event) in event_reader.read() { + info!("Watcher Event: {event:?}"); + match event.kind { + EventKind::Remove(_) | EventKind::Modify(ModifyKind::Name(RenameMode::From)) => { + for path in &event.paths { + remove_entity(&mut commands, &dirworld_entities, path); + } + } + EventKind::Create(_) | EventKind::Modify(ModifyKind::Name(RenameMode::To)) => { + for path in &event.paths { + process_entry(&mut commands, path, &observers, &codecs); + } + } + EventKind::Modify(ModifyKind::Name(RenameMode::Both)) + => { + remove_entity(&mut commands, &dirworld_entities, &event.paths[0]); + process_entry(&mut commands, &event.paths[1], &observers, &codecs); + } + // EventKind::Modify(ModifyKind::Data(DataChange::Content)) + EventKind::Modify(ModifyKind::Metadata(MetadataKind::Any)) => { + remove_entity(&mut commands, &dirworld_entities, &event.paths[0]); + process_entry(&mut commands, &event.paths[0], &observers, &codecs); + } + _ => { + // warn!("Not Processed.") + } + } + } + } +} + +fn remove_entity( + commands: &mut Commands, + dirworld_entities: &Query<(Entity, &DirworldEntity)>, + path: &Path, +) { + if let Some((entity, _)) = dirworld_entities + .iter() + .find(|(_, dirworld_entity)| dirworld_entity.path == *path) + { + commands.entity(entity).despawn_recursive(); + } else { + warn!("Failed to find entity corresponding to path for despawning: {path:?}"); + } +} |