From 0373f68cc39c4c640ba14db5799a1a83f6cb311e Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Fri, 23 Feb 2024 16:38:38 -0500 Subject: [PATCH] resource: create the ability to keep resources as type erased data --- Cargo.lock | 85 +++++---------------------- lyra-game/src/delta_time.rs | 2 +- lyra-game/src/events.rs | 2 +- lyra-game/src/game.rs | 8 ++- lyra-game/src/input/system.rs | 4 ++ lyra-game/src/plugin.rs | 3 +- lyra-game/src/render/window.rs | 2 +- lyra-game/src/scene/model.rs | 5 +- lyra-game/src/stage.rs | 5 +- lyra-resource/Cargo.toml | 2 +- lyra-resource/src/lib.rs | 12 +++- lyra-resource/src/model.rs | 3 +- lyra-resource/src/resource.rs | 83 ++++++++++++++++---------- lyra-resource/src/resource_manager.rs | 65 ++++++++++++-------- lyra-resource/src/world_ext.rs | 75 +++++++++++++++++++++++ 15 files changed, 220 insertions(+), 136 deletions(-) create mode 100644 lyra-resource/src/world_ext.rs diff --git a/Cargo.lock b/Cargo.lock index 593b602..448879d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -319,12 +319,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "atomicell" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "157342dd84c64f16899b4b16c1fb2cce54b887990362aac3c590b3d13810890f" - [[package]] name = "autocfg" version = "1.1.0" @@ -690,42 +684,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" -[[package]] -name = "edict" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85bf7cde5687ce04b093bfe183453fa12996b6bdfd2567a0262ebd621761d77" -dependencies = [ - "atomicell", - "edict-proc", - "hashbrown 0.13.2", - "parking_lot", - "smallvec", - "tiny-fn", -] - -[[package]] -name = "edict-proc" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c94d80dc0f05250a9082bb9455bbf3d6c6c51db388b060df914aebcfb4a9b9f1" -dependencies = [ - "edict-proc-lib", - "syn 2.0.48", -] - -[[package]] -name = "edict-proc-lib" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d98f9931a4f71c7eb7d85cf4ef1271b27014625c85a65376a52c10ac4ffaea" -dependencies = [ - "proc-easy", - "proc-macro2", - "quote", - "syn 2.0.48", -] - [[package]] name = "either" version = "1.9.0" @@ -1149,15 +1107,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" -dependencies = [ - "ahash", -] - [[package]] name = "hashbrown" version = "0.14.3" @@ -1504,8 +1453,10 @@ dependencies = [ "anyhow", "lyra-ecs-derive", "lyra-math", + "paste", "rand", "thiserror", + "unique", ] [[package]] @@ -1569,6 +1520,7 @@ dependencies = [ "lyra-ecs", "lyra-math", "lyra-reflect-derive", + "lyra-resource", ] [[package]] @@ -1587,11 +1539,11 @@ dependencies = [ "anyhow", "base64 0.21.5", "crossbeam", - "edict", "glam", "gltf", "image", "infer", + "lyra-ecs", "mime", "notify", "notify-debouncer-full", @@ -2035,6 +1987,12 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -2125,17 +2083,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" -[[package]] -name = "proc-easy" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea59c637cd0e6b71ae18e589854e9de9b7cb17fefdbf2047e42bd38e24285b19" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.48", -] - [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -2595,12 +2542,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tiny-fn" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af7b2c33e09916c65a15c92c1e583946052527e06102689ed11c6125f64fa8ba" - [[package]] name = "tiny-skia" version = "0.8.4" @@ -2747,6 +2688,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "unique" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d360722e1f3884f5b14d332185f02ff111f771f0c76a313268fe6af1409aba96" + [[package]] name = "urlencoding" version = "2.1.3" diff --git a/lyra-game/src/delta_time.rs b/lyra-game/src/delta_time.rs index f64735a..d074677 100644 --- a/lyra-game/src/delta_time.rs +++ b/lyra-game/src/delta_time.rs @@ -37,7 +37,7 @@ pub struct DeltaTimePlugin; impl Plugin for DeltaTimePlugin { fn setup(&self, game: &mut crate::game::Game) { - game.world().add_resource(DeltaTime(0.0, None)); + game.world_mut().add_resource(DeltaTime(0.0, None)); game.add_system_to_stage(GameStages::First, "delta_time", delta_time_system, &[]); } } \ No newline at end of file diff --git a/lyra-game/src/events.rs b/lyra-game/src/events.rs index aee0bb0..11f538a 100644 --- a/lyra-game/src/events.rs +++ b/lyra-game/src/events.rs @@ -78,6 +78,6 @@ pub struct EventsPlugin; impl Plugin for EventsPlugin { fn setup(&self, game: &mut crate::game::Game) { - game.world().add_resource(EventQueue::new()); + game.world_mut().add_resource(EventQueue::new()); } } \ No newline at end of file diff --git a/lyra-game/src/game.rs b/lyra-game/src/game.rs index a3d8666..953b462 100755 --- a/lyra-game/src/game.rs +++ b/lyra-game/src/game.rs @@ -246,11 +246,17 @@ impl Game { } /// Get the world of this game - pub fn world(&mut self) -> &mut World { + pub fn world_mut(&mut self) -> &mut World { // world is always `Some`, so unwrapping is safe self.world.as_mut().unwrap() } + /// Get the world of this game + pub fn world(&self) -> &World { + // world is always `Some`, so unwrapping is safe + self.world.as_ref().unwrap() + } + /// Add a system to the ecs world pub fn with_system(&mut self, name: &str, system: S, depends: &[&str]) -> &mut Self where diff --git a/lyra-game/src/input/system.rs b/lyra-game/src/input/system.rs index 1a5171e..419d079 100755 --- a/lyra-game/src/input/system.rs +++ b/lyra-game/src/input/system.rs @@ -120,6 +120,10 @@ impl crate::ecs::system::System for InputSystem { fn world_access(&self) -> lyra_ecs::Access { lyra_ecs::Access::Write } + + fn execute_deferred(&mut self, _: NonNull) -> anyhow::Result<()> { + Ok(()) + } } impl IntoSystem<()> for InputSystem { diff --git a/lyra-game/src/plugin.rs b/lyra-game/src/plugin.rs index c7adca2..e014e61 100644 --- a/lyra-game/src/plugin.rs +++ b/lyra-game/src/plugin.rs @@ -1,3 +1,4 @@ +use lyra_ecs::Commands; use lyra_resource::ResourceManager; use crate::EventsPlugin; @@ -98,7 +99,7 @@ pub struct ResourceManagerPlugin; impl Plugin for ResourceManagerPlugin { fn setup(&self, game: &mut Game) { - game.world().add_resource(ResourceManager::new()); + game.world_mut().add_resource(ResourceManager::new()); } } diff --git a/lyra-game/src/render/window.rs b/lyra-game/src/render/window.rs index a974544..d577996 100644 --- a/lyra-game/src/render/window.rs +++ b/lyra-game/src/render/window.rs @@ -373,7 +373,7 @@ impl Plugin for WindowPlugin { fn setup(&self, game: &mut crate::game::Game) { let window_options = WindowOptions::default(); - game.world().add_resource(Ct::new(window_options)); + game.world_mut().add_resource(Ct::new(window_options)); game.with_system("window_updater", window_updater_system, &[]); } } diff --git a/lyra-game/src/scene/model.rs b/lyra-game/src/scene/model.rs index 8e62541..2c5a6b3 100644 --- a/lyra-game/src/scene/model.rs +++ b/lyra-game/src/scene/model.rs @@ -1,10 +1,11 @@ use lyra_ecs::Component; +use lyra_reflect::Reflect; use lyra_resource::ResHandle; use crate::assets::Model; -#[derive(Clone, Component)] -pub struct ModelComponent(pub ResHandle); +#[derive(Clone, Component, Reflect)] +pub struct ModelComponent(#[reflect(skip)] pub ResHandle); impl From> for ModelComponent { fn from(value: ResHandle) -> Self { diff --git a/lyra-game/src/stage.rs b/lyra-game/src/stage.rs index e06e8aa..15d79ff 100644 --- a/lyra-game/src/stage.rs +++ b/lyra-game/src/stage.rs @@ -9,7 +9,9 @@ pub enum StagedExecutorError { #[error("[stage={0}] could not find a system's dependency named `{1}`")] MissingSystem(String, String), #[error("[stage={0}] system `{1}` returned with an error: `{2}`")] - SystemError(String, String, anyhow::Error) + SystemError(String, String, anyhow::Error), + #[error("[stage={0}] a command returned with an error: `{1}`")] + CommandError(String, anyhow::Error), } impl StagedExecutorError { @@ -17,6 +19,7 @@ impl StagedExecutorError { match value { GraphExecutorError::MissingSystem(s) => Self::MissingSystem(stage, s), GraphExecutorError::SystemError(s, e) => Self::SystemError(stage, s, e), + GraphExecutorError::Command(e) => Self::CommandError(stage, e) } } } diff --git a/lyra-resource/Cargo.toml b/lyra-resource/Cargo.toml index 0f6adf3..753caff 100644 --- a/lyra-resource/Cargo.toml +++ b/lyra-resource/Cargo.toml @@ -6,10 +6,10 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +lyra-ecs = { path = "../lyra-ecs" } anyhow = "1.0.75" base64 = "0.21.4" crossbeam = { version = "0.8.4", features = [ "crossbeam-channel" ] } -edict = "0.5.0" glam = "0.24.1" gltf = { version = "1.3.0", features = ["KHR_materials_pbrSpecularGlossiness", "KHR_materials_specular"] } image = "0.24.7" diff --git a/lyra-resource/src/lib.rs b/lyra-resource/src/lib.rs index 65fe735..0427c2f 100644 --- a/lyra-resource/src/lib.rs +++ b/lyra-resource/src/lib.rs @@ -16,7 +16,17 @@ pub use model::*; pub mod material; pub use material::*; +pub mod world_ext; +pub use world_ext::*; + pub(crate) mod util; pub use crossbeam::channel as channel; -pub use notify; \ No newline at end of file +pub use notify; + +#[allow(unused_imports)] +pub(crate) mod lyra_engine { + pub(crate) mod ecs { + pub use lyra_ecs::*; + } +} diff --git a/lyra-resource/src/model.rs b/lyra-resource/src/model.rs index f1c24c2..acc8c2e 100644 --- a/lyra-resource/src/model.rs +++ b/lyra-resource/src/model.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use crate::Material; +use crate::lyra_engine; #[repr(C)] #[derive(Clone, Debug, PartialEq)] @@ -81,7 +82,7 @@ pub enum MeshVertexAttribute { Other(String), } -#[derive(Clone, edict::Component)] +#[derive(Clone, lyra_ecs::Component)] pub struct Mesh { pub uuid: uuid::Uuid, pub attributes: HashMap, diff --git a/lyra-resource/src/resource.rs b/lyra-resource/src/resource.rs index ef85222..1423de2 100644 --- a/lyra-resource/src/resource.rs +++ b/lyra-resource/src/resource.rs @@ -1,7 +1,9 @@ -use std::sync::{Arc, RwLock}; +use std::{any::Any, sync::{Arc, RwLock}}; use uuid::Uuid; +use crate::ResourceStorage; + #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum ResourceState { Loading, @@ -21,7 +23,7 @@ impl<'a, T> std::ops::Deref for ResourceDataRef<'a, T> { } } -pub(crate) struct Resource { +pub struct Resource { path: String, pub(crate) data: Option, pub(crate) version: usize, @@ -122,34 +124,51 @@ impl ResHandle { None } } - - /* /// Get a reference to the data in the resource - /// - /// # Panics - /// Panics if the resource was not loaded yet. - pub fn data_ref(&self) -> &T { - self.data.as_ref() - .expect("Resource is not loaded yet (use try_data_ref, or wait until its loaded)!") - } - - /// If the resource is loaded, returns `Some` reference to the data in the resource, - /// else it will return `None` - pub fn try_data_ref(&self) -> Option<&T> { - self.data.as_ref() - } - - /// Get a **mutable** reference to the data in the resource - /// - /// # Panics - /// Panics if the resource was not loaded yet. - pub fn data_mut(&mut self) -> &mut T { - self.data.as_mut() - .expect("Resource is not loaded yet (use try_data_ref, or wait until its loaded)!") - } - - /// If the resource is loaded, returns `Some` **mutable** reference to the data in the resource, - /// else it will return `None` - pub fn try_data_mut(&mut self) -> Option<&mut T> { - self.data.as_mut() - } */ } + +impl ResourceStorage for ResHandle { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn as_arc_any(self: Arc) -> Arc { + self.clone() + } + + fn as_box_any(self: Box) -> Box { + self + } + + fn set_watched(&self, watched: bool) { + let mut w = self.data.write().unwrap(); + w.is_watched = watched; + } + + fn path(&self) -> String { + self.path() + } + + fn version(&self) -> usize { + self.version() + } + + fn state(&self) -> ResourceState { + self.state() + } + + fn uuid(&self) -> Uuid { + self.uuid() + } + + fn is_watched(&self) -> bool { + self.is_watched() + } + + fn is_loaded(&self) -> bool { + self.is_loaded() + } +} \ No newline at end of file diff --git a/lyra-resource/src/resource_manager.rs b/lyra-resource/src/resource_manager.rs index 6b84686..dc24d33 100644 --- a/lyra-resource/src/resource_manager.rs +++ b/lyra-resource/src/resource_manager.rs @@ -4,39 +4,27 @@ use crossbeam::channel::Receiver; use notify::{Watcher, RecommendedWatcher}; use notify_debouncer_full::{DebouncedEvent, FileIdMap}; use thiserror::Error; +use uuid::Uuid; -use crate::{resource::ResHandle, loader::{ResourceLoader, LoaderError, image::ImageLoader, model::ModelLoader}}; +use crate::{loader::{image::ImageLoader, model::ModelLoader, LoaderError, ResourceLoader}, resource::ResHandle, ResourceState}; +/// A trait for type erased storage of a resource. +/// Implemented for [`ResHandle`] pub trait ResourceStorage: Send + Sync + Any + 'static { fn as_any(&self) -> &dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any; fn as_arc_any(self: Arc) -> Arc; fn as_box_any(self: Box) -> Box; + /// Do not set a resource to watched if it is not actually watched. + /// This is used internally. fn set_watched(&self, watched: bool); -} -/// Implements this trait for anything that fits the type bounds -impl ResourceStorage for ResHandle { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn as_arc_any(self: Arc) -> Arc { - self.clone() - } - - fn as_box_any(self: Box) -> Box { - self - } - - fn set_watched(&self, watched: bool) { - let mut w = self.data.write().unwrap(); - w.is_watched = watched; - } + fn path(&self) -> String; + fn version(&self) -> usize; + fn state(&self) -> ResourceState; + fn uuid(&self) -> Uuid; + fn is_watched(&self) -> bool; + fn is_loaded(&self) -> bool; } #[derive(Error, Debug)] @@ -131,6 +119,35 @@ impl ResourceManager { } } + /// Request a resource without downcasting to a ResHandle. + /// Whenever you're ready to downcast, you can do so like this: + /// ```compile_fail + /// let arc_any = res_arc.as_arc_any(); + /// let res: Arc> = res.downcast::>().expect("Failure to downcast resource"); + /// ``` + pub fn request_raw(&mut self, path: &str) -> Result, RequestError> { + match self.resources.get(&path.to_string()) { + Some(res) => { + Ok(res.clone()) + }, + None => { + if let Some(loader) = self.loaders.iter() + .find(|l| l.does_support_file(path)) { + + // Load the resource and store it + let loader = Arc::clone(loader); // stop borrowing from self + let res = loader.load(self, path)?; + let res: Arc = Arc::from(res); + self.resources.insert(path.to_string(), res.clone()); + + Ok(res) + } else { + Err(RequestError::UnsupportedFileExtension(path.to_string())) + } + } + } + } + /// Store bytes in the manager. If there is already an entry with the same identifier it will be updated. /// /// Panics: If there is already an entry with the same `ident`, and the entry is not bytes, this function will panic. diff --git a/lyra-resource/src/world_ext.rs b/lyra-resource/src/world_ext.rs new file mode 100644 index 0000000..2db2b04 --- /dev/null +++ b/lyra-resource/src/world_ext.rs @@ -0,0 +1,75 @@ +use std::any::Any; + +use crossbeam::channel::Receiver; +use lyra_ecs::World; +use notify_debouncer_full::DebouncedEvent; + +use crate::{RequestError, ResHandle, ResourceLoader, ResourceManager}; + +pub trait WorldAssetExt { + /// Register a resource loader with the resource manager. + fn register_res_loader(&mut self) + where + L: ResourceLoader + Default + 'static; + + /// Request a resource from the resource manager. + fn request_res(&mut self, path: &str) -> Result, RequestError> + where + T: Send + Sync + Any + 'static; + + /// Start watching a resource for changes. Returns a crossbeam channel that can be used to listen for events. + fn watch_res(&mut self, path: &str, recursive: bool) -> notify::Result, Vec>>>; + + /// Stop watching a resource for changes. + fn stop_watching_res(&mut self, path: &str) -> notify::Result<()>; + + /// Try to retrieve a crossbeam channel for a path that is currently watched. Returns None if the path is not watched + fn res_watcher_recv(&self, path: &str) -> Option, Vec>>>; + + /// Reload a resource. The data will be updated in the handle. This is not + /// automatically triggered if the resource is being watched. + fn reload_res(&mut self, resource: ResHandle) -> Result<(), RequestError> + where + T: Send + Sync + Any + 'static; +} + +impl WorldAssetExt for World { + fn register_res_loader(&mut self) + where + L: ResourceLoader + Default + 'static + { + let mut man = self.get_resource_or_default::(); + man.register_loader::(); + } + + fn request_res(&mut self, path: &str) -> Result, RequestError> + where + T: Send + Sync + Any + 'static + { + let mut man = self.get_resource_or_default::(); + man.request(path) + } + + fn watch_res(&mut self, path: &str, recursive: bool) -> notify::Result, Vec>>> { + let mut man = self.get_resource_or_default::(); + man.watch(path, recursive) + } + + fn stop_watching_res(&mut self, path: &str) -> notify::Result<()> { + let mut man = self.get_resource_or_default::(); + man.stop_watching(path) + } + + fn res_watcher_recv(&self, path: &str) -> Option, Vec>>> { + let man = self.get_resource::(); + man.watcher_event_recv(path) + } + + fn reload_res(&mut self, resource: ResHandle) -> Result<(), RequestError> + where + T: Send + Sync + Any + 'static + { + let mut man = self.get_resource_or_default::(); + man.reload(resource) + } +} \ No newline at end of file