nightshade 0.13.3

A cross-platform data-oriented game engine.
Documentation
use crate::ecs::material::components::Material;
use crate::ecs::world::World;
use crate::ecs::world::commands::WorldCommand;
use std::collections::HashMap;
use std::path::PathBuf;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AssetKind {
    Texture,
    Material,
}

struct WatchedAsset {
    name: String,
    file_path: PathBuf,
    kind: AssetKind,
}

pub struct AssetWatcher {
    watched_assets: Vec<WatchedAsset>,
    key_to_index: HashMap<String, usize>,
    pending_watches: Vec<(String, PathBuf, AssetKind)>,
}

impl Default for AssetWatcher {
    fn default() -> Self {
        Self {
            watched_assets: Vec::new(),
            key_to_index: HashMap::new(),
            pending_watches: Vec::new(),
        }
    }
}

impl AssetWatcher {
    pub fn track_texture(&mut self, name: String, path: PathBuf) {
        self.pending_watches.push((name, path, AssetKind::Texture));
    }

    pub fn track_material(&mut self, name: String, path: PathBuf) {
        self.pending_watches.push((name, path, AssetKind::Material));
    }

    fn register_asset(&mut self, key: String, name: String, path: PathBuf, kind: AssetKind) {
        let index = self.watched_assets.len();
        self.watched_assets.push(WatchedAsset {
            name,
            file_path: path,
            kind,
        });
        self.key_to_index.insert(key, index);
    }

    fn get_by_key(&self, key: &str) -> Option<(AssetKind, String, PathBuf)> {
        self.key_to_index.get(key).map(|&index| {
            let watched = &self.watched_assets[index];
            (
                watched.kind,
                watched.name.clone(),
                watched.file_path.clone(),
            )
        })
    }
}

pub fn poll_asset_watcher_system(world: &mut World) {
    let pending = std::mem::take(&mut world.resources.asset_watcher.pending_watches);
    for (name, path, kind) in pending {
        let key = format!("asset:{name}");
        world
            .resources
            .file_watcher
            .watch(key.clone(), path.clone());
        world
            .resources
            .asset_watcher
            .register_asset(key, name, path, kind);
    }

    let keys: Vec<String> = world
        .resources
        .asset_watcher
        .key_to_index
        .keys()
        .cloned()
        .collect();
    for key in keys {
        if world.resources.file_watcher.take_change(&key) {
            if let Some((kind, name, path)) = world.resources.asset_watcher.get_by_key(&key) {
                match kind {
                    AssetKind::Texture => reload_texture(world, name, path),
                    AssetKind::Material => reload_material(world, name, path),
                }
            }
        }
    }
}

fn reload_texture(world: &mut World, name: String, path: PathBuf) {
    tracing::info!(
        "File changed on disk, reloading texture: {}",
        path.display()
    );
    let bytes = match std::fs::read(&path) {
        Ok(b) => b,
        Err(error) => {
            tracing::warn!("Failed to read changed file {}: {}", path.display(), error);
            return;
        }
    };
    let img = match image::load_from_memory(&bytes) {
        Ok(i) => i.to_rgba8(),
        Err(error) => {
            tracing::warn!(
                "Failed to decode changed image {}: {}",
                path.display(),
                error
            );
            return;
        }
    };
    let (width, height) = img.dimensions();
    tracing::info!("Queuing texture reload: {} ({}x{})", name, width, height);
    world.queue_command(WorldCommand::ReloadTexture {
        name,
        rgba_data: img.into_raw(),
        width,
        height,
    });
}

fn reload_material(world: &mut World, name: String, path: PathBuf) {
    tracing::info!(
        "File changed on disk, reloading material: {}",
        path.display()
    );
    let contents = match std::fs::read_to_string(&path) {
        Ok(s) => s,
        Err(error) => {
            tracing::warn!("Failed to read material file {}: {}", path.display(), error);
            return;
        }
    };
    let material: Material = match serde_json::from_str(&contents) {
        Ok(m) => m,
        Err(error) => {
            tracing::warn!(
                "Failed to parse material JSON {}: {}",
                path.display(),
                error
            );
            return;
        }
    };
    tracing::info!("Queuing material reload: {}", name);
    world.queue_command(WorldCommand::ReloadMaterial {
        name,
        material: Box::new(material),
    });
}