diff options
author | 2024-10-11 16:02:07 -0400 | |
---|---|---|
committer | 2024-10-11 16:02:07 -0400 | |
commit | 16c1574e400d73198713336e18975ff37ab78290 (patch) | |
tree | db370db8e0caea99a9e6e3c501c7ea7e3b6dd708 | |
parent | 462d6c5aaa2281bf875e04d5e2c3241a13256836 (diff) |
Way too many changes (0.2)
-rw-r--r-- | Cargo.toml | 43 | ||||
-rw-r--r-- | LICENSE-0BSD | 5 | ||||
-rw-r--r-- | src/actor.rs | 8 | ||||
-rw-r--r-- | src/commands.rs | 547 | ||||
-rw-r--r-- | src/components.rs | 9 | ||||
-rw-r--r-- | src/events.rs | 6 | ||||
-rw-r--r-- | src/lib.rs | 158 | ||||
-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 |
12 files changed, 1066 insertions, 93 deletions
@@ -1,15 +1,48 @@ [package] name = "bevy_dirworld" -version = "0.1.1" +version = "0.2.0" edition = "2021" [dependencies] anyhow = "1.0.83" +async-channel = "2.3.1" +notify = "6.1.1" +tar = "0.4.41" +xz2 = "0.1.7" +rust-crypto = "0.2.36" +multi_key_map = "0.3.0" +serde = "1.0" +rmp-serde = "1.3.0" +anymap = "0.12" +strum = "0.26" +notify-debouncer-full = "0.3" +md5 = "0.7" +bevy-async-ecs = "0.6" +aes = "0.8" +hex = "0.4" +hex-literal = "0.4" [dependencies.bevy] -version = "0.13" -features = ["file_watcher"] +version = "0.14" +default-features = false +features = ["serialize"] -[dependencies.bevy_rapier3d] -version = "0.26" +[dependencies.avian3d] +version = "0.1" +features = ["serialize"] +[dependencies.occule] +git = "http://github.com/exvacuum/occule" + +[dependencies.yarnspinner] +version = "0.3" +optional = true +features = ["serde"] + +[dependencies.bevy_scriptum] +version = "0.6" +features = ["lua"] + +[features] +default = ["yarnspinner"] +yarnspinner = ["dep:yarnspinner"] diff --git a/LICENSE-0BSD b/LICENSE-0BSD new file mode 100644 index 0000000..7a39b21 --- /dev/null +++ b/LICENSE-0BSD @@ -0,0 +1,5 @@ +Copyright (C) 2024 by Silas Bartha silas@exvacuum.dev + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 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 bea87f2..ea76185 100644 --- a/src/components.rs +++ b/src/components.rs @@ -2,10 +2,15 @@ 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)] -pub struct DirworldEntity(pub PathBuf); +#[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::{asset::io::AssetSource, 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,21 +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) { - info!("building"); - let path_string = self.path.to_string_lossy().to_string(); - app.insert_resource(DirworldConfig::new(self.path.clone())) - .register_asset_source("dirworld", AssetSource::build() - .with_reader(AssetSource::get_default_reader(path_string.clone())) - .with_watcher(|_| None)) + 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:?}"); + } +} |