aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLibravatar Silas Bartha <silas@exvacuum.dev>2024-10-11 16:02:07 -0400
committerLibravatar Silas Bartha <silas@exvacuum.dev>2024-10-11 16:02:07 -0400
commit16c1574e400d73198713336e18975ff37ab78290 (patch)
treedb370db8e0caea99a9e6e3c501c7ea7e3b6dd708 /src
parent462d6c5aaa2281bf875e04d5e2c3241a13256836 (diff)
Way too many changes (0.2)
Diffstat (limited to 'src')
-rw-r--r--src/actor.rs8
-rw-r--r--src/commands.rs547
-rw-r--r--src/components.rs9
-rw-r--r--src/events.rs6
-rw-r--r--src/lib.rs158
-rw-r--r--src/lua_api.rs53
-rw-r--r--src/payload.rs84
-rw-r--r--src/resources.rs93
-rw-r--r--src/systems.rs14
-rw-r--r--src/watcher.rs139
10 files changed, 1023 insertions, 88 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) = &current_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,
+ &current_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,
+ &current_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
diff --git a/src/lib.rs b/src/lib.rs
index f4bd783..2bfbc57 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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:?}");
+ }
+}