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),
});
}