wasserxr 0.2.3

A **dynamic XR Engine** to help you stay in the **flow**
Documentation
use crate::error::SceneError;
use crate::scene::Scene;

use std::collections::hash_map::Entry;
use std::ffi::c_void;

pub type ResourceDestroyer = unsafe extern "C" fn(*mut c_void);

/// Type-erased value stored in a `Scene`.
///
/// Resources are addressed by string name and keep their concrete Rust type
/// outside the ECS component system.
pub struct Resource {
    data: *mut c_void,
    destroyer: ResourceDestroyer,
}

impl Resource {
    /// Stores any `'static` Rust value as a type-erased resource.
    pub fn new<T>(value: T) -> Self {
        Self {
            data: Box::into_raw(Box::new(value)).cast(),
            destroyer: destroy_value::<T>,
        }
    }

    pub(crate) fn from_raw(data: *mut c_void, destroyer: ResourceDestroyer) -> Self {
        Self { data, destroyer }
    }

    pub(crate) fn data(&self) -> *mut c_void {
        self.data
    }

    /// Borrows the resource as `T`.
    ///
    /// # Safety
    ///
    /// This function is safe to call but type-erased internally. The caller
    /// must request the same `T` that was used with `Resource::new`; requesting
    /// a different type creates an invalid reference.
    pub fn get<T>(&self) -> &T {
        unsafe { &*self.data.cast::<T>() }
    }

    /// Mutably borrows the resource as `T`.
    ///
    /// # Safety
    ///
    /// This function is safe to call but type-erased internally. The caller
    /// must request the same `T` that was used with `Resource::new`; requesting
    /// a different type creates an invalid mutable reference.
    pub fn get_mut<T>(&mut self) -> &mut T {
        unsafe { &mut *self.data.cast::<T>() }
    }
}

impl Drop for Resource {
    fn drop(&mut self) {
        unsafe { (self.destroyer)(self.data) };
    }
}

unsafe extern "C" fn destroy_value<T>(data: *mut c_void) {
    unsafe {
        drop(Box::from_raw(data.cast::<T>()));
    }
}

impl Scene {
    /// Adds a named resource to the scene.
    ///
    /// # Examples
    ///
    /// ```
    /// let mut scene = wasserxr::scene::Scene::new();
    ///
    /// scene.add_resource("score".to_owned(), 0usize).unwrap();
    /// assert_eq!(*scene.get_resource::<usize>("score").unwrap(), 0);
    /// ```
    pub fn add_resource<T>(&mut self, name: String, value: T) -> Result<(), SceneError> {
        match self.resources.entry(name) {
            Entry::Occupied(_) => Err(SceneError::ResourceAlreadyExists),
            Entry::Vacant(entry) => {
                entry.insert(Resource::new(value));
                Ok(())
            }
        }
    }

    pub(crate) fn add_raw_resource(
        &mut self,
        name: String,
        data: *mut c_void,
        destroyer: ResourceDestroyer,
    ) -> Result<(), SceneError> {
        match self.resources.entry(name) {
            Entry::Occupied(_) => Err(SceneError::ResourceAlreadyExists),
            Entry::Vacant(entry) => {
                entry.insert(Resource::from_raw(data, destroyer));
                Ok(())
            }
        }
    }

    /// Borrows a named resource by type.
    ///
    /// # Safety
    ///
    /// The stored value is type-erased. The requested `T` must match the type
    /// originally passed to `add_resource`.
    pub fn get_resource<T>(&self, name: &str) -> Result<&T, SceneError> {
        self.resources
            .get(name)
            .map(Resource::get)
            .ok_or(SceneError::ResourceNotFound)
    }

    /// Mutably borrows a named resource by type.
    ///
    /// # Safety
    ///
    /// The stored value is type-erased. The requested `T` must match the type
    /// originally passed to `add_resource`.
    pub fn get_mut_resource<T>(&mut self, name: &str) -> Result<&mut T, SceneError> {
        self.resources
            .get_mut(name)
            .map(Resource::get_mut)
            .ok_or(SceneError::ResourceNotFound)
    }

    pub(crate) fn get_raw_resource(&mut self, name: &str) -> Result<*mut c_void, SceneError> {
        self.resources
            .get_mut(name)
            .map(|resource| resource.data())
            .ok_or(SceneError::ResourceNotFound)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use std::sync::{
        Arc,
        atomic::{AtomicUsize, Ordering},
    };

    struct CountDrop(Arc<AtomicUsize>);

    impl Drop for CountDrop {
        fn drop(&mut self) {
            self.0.fetch_add(1, Ordering::SeqCst);
        }
    }

    #[test]
    fn resource_returns_stored_value() {
        let mut resource = Resource::new(1usize);

        assert_eq!(*resource.get::<usize>(), 1);
        *resource.get_mut::<usize>() = 2;
        assert_eq!(*resource.get::<usize>(), 2);
    }

    #[test]
    fn resource_drops_stored_value() {
        let drops = Arc::new(AtomicUsize::new(0));

        {
            let _resource = Resource::new(CountDrop(Arc::clone(&drops)));
            assert_eq!(drops.load(Ordering::SeqCst), 0);
        }

        assert_eq!(drops.load(Ordering::SeqCst), 1);
    }

    #[test]
    fn scene_adds_and_gets_resource() {
        let mut scene = Scene::new();

        scene.add_resource("counter".to_owned(), 1usize).unwrap();
        *scene.get_mut_resource::<usize>("counter").unwrap() = 2;

        assert_eq!(*scene.get_resource::<usize>("counter").unwrap(), 2);
    }

    #[test]
    fn scene_rejects_duplicate_resource() {
        let mut scene = Scene::new();
        scene.add_resource("counter".to_owned(), 1usize).unwrap();

        assert_eq!(
            scene.add_resource("counter".to_owned(), 2usize),
            Err(SceneError::ResourceAlreadyExists)
        );
    }

    #[test]
    fn scene_rejects_missing_resource() {
        let scene = Scene::new();

        assert_eq!(
            scene.get_resource::<usize>("missing"),
            Err(SceneError::ResourceNotFound)
        );
    }

    #[test]
    fn scene_resource_survives_reload() {
        let mut scene = Scene::new();
        scene.add_resource("counter".to_owned(), 1usize).unwrap();

        scene.reload().unwrap();

        assert_eq!(*scene.get_resource::<usize>("counter").unwrap(), 1);
    }
}