use std::{any::Any, collections::HashMap, path::Path, sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}, time::Duration}; use async_std::task; use crossbeam::channel::Receiver; use notify::{Watcher, RecommendedWatcher}; use notify_debouncer_full::{DebouncedEvent, FileIdMap}; use thiserror::Error; use uuid::Uuid; use crate::{gltf::ModelLoader, loader::{ImageLoader, LoaderError, ResourceLoader}, resource::ResHandle, ResourceData, ResourceState, UntypedResHandle}; /// 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); fn version(&self) -> usize; fn uuid(&self) -> Uuid; fn path(&self) -> Option; fn is_watched(&self) -> bool; fn is_loaded(&self) -> bool; fn set_state(&self, new: ResourceState); fn clone_untyped(&self) -> UntypedResHandle; } #[derive(Error, Debug)] pub enum RequestError { #[error("{0}")] Loader(LoaderError), #[error("The file extension is unsupported: '{0}'")] UnsupportedFileExtension(String), #[error("The mimetype is unsupported: '{0}'")] UnsupportedMime(String), #[error("The identifier is not found: '{0}'")] IdentNotFound(String), #[error("The resource was not loaded from a path so cannot be reloaded")] NoReloadPath } impl From for RequestError { fn from(value: LoaderError) -> Self { RequestError::Loader(value) } } /// A struct that stores some things used for watching resources. pub struct ResourceWatcher { debouncer: Arc>>, events_recv: Receiver, Vec>>, } /// The state of the ResourceManager pub struct ResourceManagerState { resources: HashMap>, uuid_resources: HashMap>, loaders: Vec>, watchers: HashMap, } /// The ResourceManager /// /// This exists since we need the manager to be `Send + Sync`. #[derive(Clone)] pub struct ResourceManager { inner: Arc>, } impl Default for ResourceManager { fn default() -> Self { Self { inner: Arc::new(RwLock::new( ResourceManagerState { resources: HashMap::new(), uuid_resources: HashMap::new(), loaders: vec![ Arc::new(ImageLoader), Arc::new(ModelLoader) ], watchers: HashMap::new(), } )) } } } impl ResourceManager { pub fn new() -> Self { Self::default() } /// Retrieves a non-mutable guard of the manager's state. pub fn state(&self) -> RwLockReadGuard { self.inner.read().unwrap() } /// Retrieves a mutable guard of the manager's state. pub fn state_mut(&self) -> RwLockWriteGuard { self.inner.write().unwrap() } /// Registers a loader to the manager. pub fn register_loader(&self) where L: ResourceLoader + Default + 'static { let mut state = self.state_mut(); state.loaders.push(Arc::new(L::default())); } /// Request a resource at `path`. /// /// When a resource for a path is requested for the first time, the resource will be loaded /// and cached. In the future, the cached version will be returned. There is only ever one copy /// of the resource's data in memory at a time. /// /// Loading resources is done asynchronously on a task spawned by `async-std`. You can use the /// handle to check if the resource is loaded. #[inline(always)] pub fn request(&self, path: &str) -> Result, RequestError> where T: ResourceData { self.request_raw(path) .map(|res| res.as_typed::() .expect("mismatched asset type, cannot downcast")) } /// Request a resource without downcasting to a `ResHandle`. /// Whenever you're ready to downcast, you can do so like this: /// ```nobuild /// let arc_any = res_arc.as_arc_any(); /// let res: Arc> = res.downcast::>().expect("Failure to downcast resource"); /// ``` pub fn request_raw(&self, path: &str) -> Result { let mut state = self.state_mut(); match state.resources.get(&path.to_string()) { Some(res) => { Ok(res.clone().clone_untyped()) }, None => { if let Some(loader) = state.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.clone(), path); let handle = loader.create_erased_handle(); let untyped = handle.clone_untyped(); untyped.write().path = Some(path.to_string()); task::spawn(async move { match res.await { Ok(data) => { let mut d = untyped.write(); d.state = ResourceState::Ready(data); d.condvar.1.notify_all(); } Err(err) => { let mut d = untyped.write(); d.state = ResourceState::Error(Arc::new(err)); } } }); let res: Arc = Arc::from(handle.clone()); state.resources.insert(path.to_string(), res.clone()); state.uuid_resources.insert(res.uuid(), res); Ok(handle.clone_untyped()) } else { Err(RequestError::UnsupportedFileExtension(path.to_string())) } } } } /// Store a resource using its uuid. /// /// The resource cannot be requested with [`ResourceManager::request`], it can only be /// retrieved with [`ResourceManager::request_uuid`]. pub fn store_uuid(&self, res: ResHandle) { let mut state = self.state_mut(); state.resources.insert(res.uuid().to_string(), Arc::new(res)); } /// Request a resource via its uuid. /// /// Returns `None` if the resource was not found. The resource must of had been /// stored with [`ResourceManager::request`] to return `Some`. pub fn request_uuid(&self, uuid: &Uuid) -> Option> { let state = self.state(); match state.resources.get(&uuid.to_string()) .or_else(|| state.uuid_resources.get(&uuid)) { Some(res) => { let res = res.clone().as_arc_any(); let res: Arc> = res.downcast::>().expect("Failure to downcast resource"); Some(ResHandle::::clone(&res)) }, None => None, } } /// 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. /// /// Parameters: /// * `ident` - The identifier to store along with these bytes. Make sure its unique to avoid overriding something. /// * `bytes` - The bytes to store. /// /// Returns: The `Arc` to the now stored resource pub fn load_bytes(&self, ident: &str, mime_type: &str, bytes: Vec, offset: usize, length: usize) -> Result, RequestError> where T: ResourceData { let mut state = self.state_mut(); if let Some(loader) = state.loaders.iter() .find(|l| l.does_support_mime(mime_type)) { let loader = loader.clone(); let res = loader.load_bytes(self.clone(), bytes, offset, length); let handle = ResHandle::::new_loading(None); let thand = handle.clone(); task::spawn(async move { match res.await { Ok(data) => { let mut d = thand.write(); d.state = ResourceState::Ready(data); } Err(err) => { let mut d = thand.write(); d.state = ResourceState::Error(Arc::new(err)); } } }); let res: Arc = Arc::from(handle.clone()); state.resources.insert(ident.to_string(), res.clone()); state.uuid_resources.insert(res.uuid(), res); Ok(handle) } else { Err(RequestError::UnsupportedMime(mime_type.to_string())) } } /// Requests bytes from the manager. pub fn request_loaded_bytes(&self, ident: &str) -> Result>, RequestError> where T: ResourceData { let state = self.state(); match state.resources.get(&ident.to_string()) { Some(res) => { let res = res.clone().as_arc_any(); let res = res.downcast::>().expect("Failure to downcast resource"); Ok(res) }, None => { Err(RequestError::IdentNotFound(ident.to_string())) } } } /// Start watching a path for changes. Returns a mspc channel that will send events pub fn watch(&self, path: &str, recursive: bool) -> notify::Result, Vec>>> { let (send, recv) = crossbeam::channel::bounded(15); let mut watcher = notify_debouncer_full::new_debouncer(Duration::from_millis(1000), None, send)?; let recurse_mode = match recursive { true => notify::RecursiveMode::Recursive, false => notify::RecursiveMode::NonRecursive, }; watcher.watcher().watch(path.as_ref(), recurse_mode)?; let watcher = Arc::new(RwLock::new(watcher)); let watcher = ResourceWatcher { debouncer: watcher, events_recv: recv.clone(), }; let mut state = self.state_mut(); state.watchers.insert(path.to_string(), watcher); let res = state.resources.get(&path.to_string()) .expect("The path that was watched has not been loaded as a resource yet"); res.set_watched(true); Ok(recv) } /// Stops watching a path pub fn stop_watching(&self, path: &str) -> notify::Result<()> { let state = self.state(); if let Some(watcher) = state.watchers.get(path) { let mut watcher = watcher.debouncer.write().unwrap(); watcher.watcher().unwatch(Path::new(path))?; // unwrap is safe since only loaded resources can be watched let res = state.resources.get(&path.to_string()).unwrap(); res.set_watched(false); } Ok(()) } /// Returns a mspc receiver for watcher events of a specific path. The path must already /// be watched with [`ResourceManager::watch`] for this to return `Some`. pub fn watcher_event_recv(&self, path: &str) -> Option, Vec>>> { let state = self.state(); state.watchers.get(&path.to_string()) .map(|w| w.events_recv.clone()) } /// Trigger a reload of a resource. /// /// The version of the resource will be incremented by one. /// /// > Note: Since reloading is done asynchronously, the reloaded data will not be immediately /// accessible. Until the resource is reloaded, the previous data will stay inside of /// the handle. pub fn reload(&self, resource: ResHandle) -> Result<(), RequestError> where T: ResourceData { let state = self.state(); let path = resource.path() .ok_or(RequestError::NoReloadPath)?; if let Some(loader) = state.loaders.iter() .find(|l| l.does_support_file(&path)) { let loader = Arc::clone(loader); // stop borrowing from self let res = loader.load(self.clone(), &path); /* let res_lock = &resource.data; let mut res_lock = res_lock.write().unwrap(); res_lock.state = ResourceState::Loading; drop(res_lock); */ let thand = resource.clone(); task::spawn(async move { match res.await { Ok(data) => { let mut d = thand.write(); d.state = ResourceState::Ready(data); d.version += 1; } Err(err) => { let mut d = thand.write(); d.state = ResourceState::Error(Arc::new(err)); } } }); } Ok(()) } } #[cfg(test)] pub(crate) mod tests { use std::{io, ops::Deref}; use instant::Instant; use crate::{Image, ResourceData}; use super::*; fn get_image(path: &str) -> String { let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap(); format!("{manifest}/test_files/img/{path}") } pub(crate) fn busy_wait_resource(handle: &ResHandle, timeout: f32) { let start = Instant::now(); while !handle.is_loaded() { // loop until the image is loaded let now = Instant::now(); let diff = now - start; if diff.as_secs_f32() >= timeout { panic!("Image never loaded"); } } } pub(crate) fn busy_wait_resource_reload(handle: &ResHandle, timeout: f32) { let version = handle.version(); let start = Instant::now(); while !handle.is_loaded() || handle.version() == version { // loop until the image is loaded let now = Instant::now(); let diff = now - start; if diff.as_secs_f32() >= timeout { panic!("Image never loaded"); } } } #[test] fn load_image() { let man = ResourceManager::new(); let res = man.request::(&get_image("squiggles.png")).unwrap(); assert!(!res.is_loaded()); res.wait_for_load(); //busy_wait_resource(&res, 10.0); // shouldn't panic because of the loop res.data_ref().unwrap(); } /// Ensures that only one copy of the same data was made #[test] fn ensure_single() { let man = ResourceManager::new(); let res = man.request::(&get_image("squiggles.png")).unwrap(); assert_eq!(Arc::strong_count(&res.handle.res), 3); let resagain = man.request::(&get_image("squiggles.png")).unwrap(); assert_eq!(Arc::strong_count(&resagain.handle.res), 4); } /// Ensures that an error is returned when a file that doesn't exist is requested #[test] fn ensure_none() { let man = ResourceManager::new(); let res = man.request::(&get_image("squigglesfff.png")).unwrap(); //let err = res.err().unwrap(); // 1 second should be enough to run into an error std::thread::sleep(Duration::from_secs(1)); //busy_wait_resource(&res, 10.0); let state = &res.read().state; assert!( match state { // make sure the error is NotFound //RequestError::Loader(LoaderError::IoError(e)) if e.kind() == io::ErrorKind::NotFound => true, ResourceState::Error(err) => { match err.deref() { LoaderError::IoError(e) if e.kind() == io::ErrorKind::NotFound => true, _ => false, } }, _ => false } ); } #[test] fn reload_image() { let man = ResourceManager::new(); let res = man.request::(&get_image("squiggles.png")).unwrap(); busy_wait_resource(&res, 10.0); let img = res.data_ref(); img.unwrap(); man.reload(res.clone()).unwrap(); busy_wait_resource_reload(&res, 10.0); assert_eq!(res.version(), 1); man.reload(res.clone()).unwrap(); busy_wait_resource_reload(&res, 10.0); assert_eq!(res.version(), 2); } #[test] fn watch_image() { let orig_path = get_image("squiggles.png"); let image_path = get_image("squiggles_test.png"); std::fs::copy(orig_path, &image_path).unwrap(); let man = ResourceManager::new(); let res = man.request::(&image_path).unwrap(); busy_wait_resource(&res, 10.0); let img = res.data_ref(); img.unwrap(); let recv = man.watch(&image_path, false).unwrap(); std::fs::remove_file(&image_path).unwrap(); let event = recv.recv().unwrap(); let event = event.unwrap(); assert!(event.iter().any(|ev| ev.kind.is_remove() || ev.kind.is_modify())); } }