use std::path::PathBuf;
use anyhow::Context;
use async_channel::Sender;
use bones_utils::{default, HashMap};
use futures_lite::future::Boxed as BoxedFuture;
use path_absolutize::Absolutize;
use crate::{AssetLocRef, ChangedAsset};
pub trait AssetIo: Sync + Send {
fn enumerate_packs(&self) -> BoxedFuture<anyhow::Result<Vec<String>>>;
fn load_file(&self, loc: AssetLocRef) -> BoxedFuture<anyhow::Result<Vec<u8>>>;
fn watch(&self, change_sender: Sender<ChangedAsset>) -> bool {
let _ = change_sender;
false
}
}
#[cfg(not(target_arch = "wasm32"))]
pub struct FileAssetIo {
pub core_dir: PathBuf,
pub packs_dir: PathBuf,
pub watcher: parking_lot::Mutex<Option<Box<dyn notify::Watcher + Sync + Send>>>,
}
#[cfg(not(target_arch = "wasm32"))]
impl FileAssetIo {
pub fn new(core_dir: &std::path::Path, packs_dir: &std::path::Path) -> Self {
let cwd = std::env::current_dir().unwrap();
let core_dir = cwd.join(core_dir);
let packs_dir = cwd.join(packs_dir);
Self {
core_dir: core_dir.clone(),
packs_dir: packs_dir.clone(),
watcher: parking_lot::Mutex::new(None),
}
}
}
#[cfg(not(target_arch = "wasm32"))]
impl AssetIo for FileAssetIo {
fn enumerate_packs(&self) -> BoxedFuture<anyhow::Result<Vec<String>>> {
if !self.packs_dir.exists() {
return Box::pin(async { Ok(Vec::new()) });
}
let packs_dir = self.packs_dir.clone();
Box::pin(async move {
let dirs = std::fs::read_dir(&packs_dir)?
.map(|entry| {
let entry = entry?;
let name = entry
.file_name()
.to_str()
.expect("non-unicode filename")
.to_owned();
Ok::<_, std::io::Error>(name)
})
.filter(|x| {
x.as_ref()
.map(|name| packs_dir.join(name).is_dir())
.unwrap_or(true)
})
.collect::<Result<Vec<_>, _>>()?;
Ok(dirs)
})
}
fn load_file(&self, loc: AssetLocRef) -> BoxedFuture<anyhow::Result<Vec<u8>>> {
let packs_dir = self.packs_dir.clone();
let core_dir = self.core_dir.clone();
let loc = loc.to_owned();
Box::pin(async move {
let base_dir = match loc.pack {
Some(folder) => packs_dir.join(folder),
None => core_dir.clone(),
};
let path = loc.path.absolutize_from("/").unwrap();
let path = path.strip_prefix("/").unwrap();
let path = base_dir.join(path);
std::fs::read(&path).with_context(|| format!("Could not load file: {path:?}"))
})
}
fn watch(&self, sender: Sender<ChangedAsset>) -> bool {
use notify::{RecursiveMode, Result, Watcher};
let core_dir_ = self.core_dir.clone();
let packs_dir_ = self.packs_dir.clone();
notify::recommended_watcher(move |res: Result<notify::Event>| match res {
Ok(event) => match &event.kind {
notify::EventKind::Create(_) | notify::EventKind::Modify(_) => {
for path in event.paths {
let (path, pack) = if let Ok(path) = path.strip_prefix(&core_dir_) {
(path, None)
} else if let Ok(path) = path.strip_prefix(&packs_dir_) {
let pack = path.iter().next().unwrap().to_str().unwrap().to_string();
let path = path.strip_prefix(&pack).unwrap();
(path, Some(pack))
} else {
continue;
};
sender
.send_blocking(ChangedAsset::Loc(crate::AssetLoc {
path: path.into(),
pack,
}))
.unwrap();
}
}
_ => (),
},
Err(e) => tracing::error!("watch error: {e:?}"),
})
.and_then(|mut w| {
if self.core_dir.exists() {
w.watch(&self.core_dir, RecursiveMode::Recursive)?;
}
if self.packs_dir.exists() {
w.watch(&self.packs_dir, RecursiveMode::Recursive)?;
}
*self.watcher.lock() = Some(Box::new(w) as _);
Ok(())
})
.map_err(|e| {
tracing::error!("watch error: {e:?}");
})
.map(|_| true)
.unwrap_or(false)
}
}
pub struct WebAssetIo {
pub asset_url: String,
}
impl WebAssetIo {
pub fn new(asset_url: &str) -> Self {
Self {
asset_url: asset_url.into(),
}
}
}
impl AssetIo for WebAssetIo {
fn enumerate_packs(&self) -> BoxedFuture<anyhow::Result<Vec<String>>> {
Box::pin(async move { Ok(default()) })
}
fn load_file(&self, loc: AssetLocRef) -> BoxedFuture<anyhow::Result<Vec<u8>>> {
let loc = loc.to_owned();
let asset_url = self.asset_url.clone();
Box::pin(async move {
if loc.pack.is_some() {
return Err(anyhow::format_err!("Cannot load asset packs on WASM yet"));
}
let url = format!(
"{}{}",
asset_url,
loc.path.absolutize_from("/").unwrap().to_str().unwrap()
);
let (sender, receiver) = async_channel::bounded(1);
let req = ehttp::Request::get(&url);
ehttp::fetch(req, move |resp| {
sender.send_blocking(resp.map(|resp| resp.bytes)).unwrap();
});
let result = receiver
.recv()
.await
.unwrap()
.map_err(|e| anyhow::format_err!("{e}"))
.with_context(|| format!("Could not download file: {url}"))?;
Ok(result)
})
}
}
pub struct DummyIo {
core: HashMap<PathBuf, Vec<u8>>,
packs: HashMap<String, HashMap<PathBuf, Vec<u8>>>,
}
impl DummyIo {
pub fn new<'a, I: IntoIterator<Item = (&'a str, Vec<u8>)>>(core: I) -> Self {
Self {
core: core
.into_iter()
.map(|(p, d)| (PathBuf::from(p), d))
.collect(),
packs: Default::default(),
}
}
}
impl AssetIo for DummyIo {
fn enumerate_packs(&self) -> BoxedFuture<anyhow::Result<Vec<String>>> {
let packs = self.packs.keys().cloned().collect();
Box::pin(async { Ok(packs) })
}
fn load_file(&self, loc: AssetLocRef) -> BoxedFuture<anyhow::Result<Vec<u8>>> {
let err = || {
anyhow::format_err!(
"File not found: `{:?}` in pack `{:?}`",
loc.path,
loc.pack.unwrap_or("[core]")
)
};
let data = (|| {
if let Some(pack_folder) = loc.pack {
self.packs
.get(pack_folder)
.ok_or_else(err)?
.get(loc.path)
.cloned()
.ok_or_else(err)
} else {
self.core.get(loc.path).cloned().ok_or_else(err)
}
})();
Box::pin(async move { data })
}
}