From 462d6c5aaa2281bf875e04d5e2c3241a13256836 Mon Sep 17 00:00:00 2001 From: Silas Bartha Date: Mon, 17 Jun 2024 19:37:21 -0400 Subject: Mon Jun 17 07:37:21 PM EDT 2024 --- Cargo.toml | 3 ++- src/components.rs | 6 ++++++ src/lib.rs | 7 ++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index df73011..86079f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_dirworld" -version = "0.1.0" +version = "0.1.1" edition = "2021" [dependencies] @@ -8,6 +8,7 @@ anyhow = "1.0.83" [dependencies.bevy] version = "0.13" +features = ["file_watcher"] [dependencies.bevy_rapier3d] version = "0.26" diff --git a/src/components.rs b/src/components.rs index 69a5cde..bea87f2 100644 --- a/src/components.rs +++ b/src/components.rs @@ -1,5 +1,11 @@ +use std::path::PathBuf; + use bevy::prelude::*; /// 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); diff --git a/src/lib.rs b/src/lib.rs index 6b46502..f4bd783 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; -use bevy::prelude::*; +use bevy::{asset::io::AssetSource, prelude::*}; use events::DirworldNavigationEvent; use resources::{Dirworld, DirworldConfig}; @@ -25,7 +25,12 @@ pub struct DirworldPlugin { 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)) .add_event::() .init_resource::(); } -- cgit v1.2.3 From 16c1574e400d73198713336e18975ff37ab78290 Mon Sep 17 00:00:00 2001 From: Silas Bartha Date: Fri, 11 Oct 2024 16:02:07 -0400 Subject: Way too many changes (0.2) --- Cargo.toml | 43 ++++- LICENSE-0BSD | 5 + src/actor.rs | 8 + src/commands.rs | 547 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/components.rs | 9 +- src/events.rs | 6 + src/lib.rs | 158 ++++++++++++++-- src/lua_api.rs | 53 ++++++ src/payload.rs | 84 +++++++++ src/resources.rs | 93 +++------- src/systems.rs | 14 ++ src/watcher.rs | 139 ++++++++++++++ 12 files changed, 1066 insertions(+), 93 deletions(-) create mode 100644 LICENSE-0BSD create mode 100644 src/actor.rs create mode 100644 src/commands.rs create mode 100644 src/lua_api.rs create mode 100644 src/payload.rs create mode 100644 src/systems.rs create mode 100644 src/watcher.rs diff --git a/Cargo.toml b/Cargo.toml index 86079f9..0fd838a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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>, +} 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::().unwrap(); + let mut current_dir = world.remove_resource::().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, + Res, + )> = 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, + 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 = directory + .flatten() + .map(|entry| entry.path().canonicalize().unwrap()) + .collect::>(); + 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, Option>) { + let entry_type = if entry_path.is_dir() { + EntryType::Folder + } else { + let extensions = entry_path.extensions(); + EntryType::File(extensions) + }; + + let mut data: Option> = None; + let mut payload: Option = 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::( + 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::(&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::().unwrap(); + let mut current_dir = world.remove_resource::().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, + Res, + )> = 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, +} + +impl Command for DirworldLockDoorCommand { + fn apply(self, world: &mut World) { + let path = self.path.clone(); + // Get existing payload + let codecs = world.remove_resource::().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::().insert( + format!("Locking {:?}", self.path.file_name().unwrap()), + task, + ); + } +} + +struct DirworldUnlockDoorCommand { + path: PathBuf, + key: Vec, +} + +impl Command for DirworldUnlockDoorCommand { + fn apply(self, world: &mut World) { + let path = self.path.clone(); + // Get existing payload + let codecs = world.remove_resource::().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::().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::().unwrap(); + let codecs = world.remove_resource::().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); + + /// Unlock Door + fn dirworld_unlock_door(&mut self, path: PathBuf, key: Vec); + + 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) { + self.add(DirworldLockDoorCommand { key, path }); + } + + fn dirworld_unlock_door(&mut self, path: PathBuf, key: Vec) { + 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, +} 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>, +} \ 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) + 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::(|runtime| { + let runtime = lua_api::register(runtime); + if let Some(register_custom) = &self.register_custom_lua_api { + (register_custom)(runtime); + } + }) .add_event::() - .init_resource::(); + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .add_event::(); + } +} + +pub trait Extensions { + fn extensions(&self) -> Option; + + fn file_stem_no_extensions(&self) -> Option; + + fn no_extensions(&self) -> PathBuf; +} + +impl Extensions for PathBuf { + fn extensions(&self) -> Option { + let mut temp_path = self.clone(); + let mut extensions = Vec::::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 { + 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( + &mut self, + extensions: Vec, + observer: impl IntoObserverSystem, + ) -> &mut Self; + + fn register_dirworld_entry_codec( + &mut self, + extensions: Vec, + codec: C, + ) -> &mut Self; +} + +impl DirworldApp for App { + fn register_dirworld_entry_callback( + &mut self, + extensions: Vec, + observer: impl IntoObserverSystem, + ) -> &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::() + .insert_many(extensions, observer_entity_id); + self + } + + fn register_dirworld_entry_codec( + &mut self, + extensions: Vec, + codec: C, + ) -> &mut Self { + self.world_mut() + .resource_mut::() + .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, + time: Res