resonance 0.1.0

A modular game engine. Heavy work in progress.
Documentation
use crate::assets::cache::AssetCache;
use crate::assets::handle::AssetId;
use bevy_ecs::prelude::*;
use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher};
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use tokio::sync::mpsc;

#[derive(Debug, Clone)]
pub struct HotReloadEvent {
    pub path: PathBuf,
    pub asset_id: AssetId,
    pub event_kind: HotReloadEventKind,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HotReloadEventKind {

    Modified,

    Created,

    Deleted,
}

#[derive(Resource)]
pub struct HotReloadWatcher {
    _watcher: RecommendedWatcher,
    receiver: Arc<tokio::sync::Mutex<mpsc::UnboundedReceiver<HotReloadEvent>>>,
    watched_assets: Arc<RwLock<HashMap<PathBuf, AssetId>>>,
    dependencies: Arc<RwLock<HashMap<AssetId, HashSet<PathBuf>>>>,
}

impl HotReloadWatcher {

    pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
        let (tx, rx) = mpsc::unbounded_channel();
        let watched_assets = Arc::new(RwLock::new(HashMap::new()));
        let dependencies = Arc::new(RwLock::new(HashMap::new()));

        let watched_assets_clone = watched_assets.clone();

        let watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
            match res {
                Ok(event) => {

                    let event_kind = match event.kind {
                        notify::EventKind::Modify(_) => Some(HotReloadEventKind::Modified),
                        notify::EventKind::Create(_) => Some(HotReloadEventKind::Created),
                        notify::EventKind::Remove(_) => Some(HotReloadEventKind::Deleted),
                        _ => None,
                    };

                    if let Some(kind) = event_kind {
                        for path in event.paths {

                            let watched = watched_assets_clone.read().unwrap();
                            if let Some(&asset_id) = watched.get(&path) {
                                log::info!("Hot reload: {:?} was {:?}", path, kind);

                                let _ = tx.send(HotReloadEvent {
                                    path,
                                    asset_id,
                                    event_kind: kind,
                                });
                            }
                        }
                    }
                }
                Err(e) => log::error!("Watch error: {:?}", e),
            }
        })?;

        Ok(Self {
            _watcher: watcher,
            receiver: Arc::new(tokio::sync::Mutex::new(rx)),
            watched_assets,
            dependencies,
        })
    }

    pub fn watch(&mut self, path: impl Into<PathBuf>, asset_id: AssetId) -> Result<(), Box<dyn std::error::Error>> {
        let path = path.into();

        {
            let mut watched = self.watched_assets.write().unwrap();
            watched.insert(path.clone(), asset_id);
        }

        self._watcher.watch(&path, RecursiveMode::NonRecursive)?;

        log::debug!("Watching asset file: {:?}", path);

        Ok(())
    }

    pub fn unwatch(&mut self, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
        {
            let mut watched = self.watched_assets.write().unwrap();
            watched.remove(path);
        }

        self._watcher.unwatch(path)?;

        log::debug!("Stopped watching: {:?}", path);

        Ok(())
    }

    pub fn watch_directory(&mut self, path: impl AsRef<Path>) -> Result<(), Box<dyn std::error::Error>> {
        let path = path.as_ref();
        self._watcher.watch(path, RecursiveMode::Recursive)?;
        log::info!("Watching directory: {:?}", path);
        Ok(())
    }

    pub fn add_dependency(&self, asset_id: AssetId, dependency_path: PathBuf) {
        let mut deps = self.dependencies.write().unwrap();
        deps.entry(asset_id).or_insert_with(HashSet::new).insert(dependency_path);
    }

    pub fn get_dependencies(&self, asset_id: AssetId) -> Vec<PathBuf> {
        let deps = self.dependencies.read().unwrap();
        deps.get(&asset_id)
            .map(|set| set.iter().cloned().collect())
            .unwrap_or_default()
    }

    pub async fn process_events(&self) -> Vec<HotReloadEvent> {
        let mut receiver = self.receiver.lock().await;
        let mut events = Vec::new();

        while let Ok(event) = receiver.try_recv() {
            events.push(event);
        }

        events
    }

    pub fn is_watched(&self, path: &Path) -> bool {
        let watched = self.watched_assets.read().unwrap();
        watched.contains_key(path)
    }
}

pub fn process_hot_reload_events(
    hot_reload: Res<HotReloadWatcher>,
    _cache: Res<AssetCache>,
) {

    let rt = tokio::runtime::Handle::try_current();
    if let Ok(rt) = rt {
        rt.block_on(async {
            let events = hot_reload.process_events().await;

            for event in events {
                match event.event_kind {
                    HotReloadEventKind::Modified | HotReloadEventKind::Created => {
                        log::info!("Asset changed, invalidating cache: {:?}", event.path);

                    }
                    HotReloadEventKind::Deleted => {
                        log::warn!("Asset deleted: {:?}", event.path);

                    }
                }

                let dependencies = hot_reload.get_dependencies(event.asset_id);
                for dep_path in dependencies {
                    log::info!("Reloading dependent asset: {:?}", dep_path);
                }
            }
        });
    }
}