1use std::path::PathBuf;
2
3use anyhow::Context;
4use async_channel::Sender;
5use bones_utils::{default, futures::future::Boxed as BoxedFuture, HashMap};
6use path_absolutize::Absolutize;
7
8use crate::{AssetLocRef, ChangedAsset};
9
10pub trait AssetIo: Sync + Send {
13 fn enumerate_packs(&self) -> BoxedFuture<anyhow::Result<Vec<String>>>;
19
20 fn load_file(&self, loc: AssetLocRef) -> BoxedFuture<anyhow::Result<Vec<u8>>>;
25
26 fn watch(&self, change_sender: Sender<ChangedAsset>) -> bool {
30 let _ = change_sender;
31 false
32 }
33}
34
35#[cfg(not(target_arch = "wasm32"))]
37pub struct FileAssetIo {
38 pub core_dir: PathBuf,
40 pub packs_dir: PathBuf,
42 pub watcher: bones_utils::parking_lot::Mutex<Option<Box<dyn notify::Watcher + Sync + Send>>>,
44}
45
46#[cfg(not(target_arch = "wasm32"))]
47impl FileAssetIo {
48 pub fn new(core_dir: &std::path::Path, packs_dir: &std::path::Path) -> Self {
50 let cwd = std::env::current_dir().unwrap();
51 let core_dir = cwd.join(core_dir);
52 let packs_dir = cwd.join(packs_dir);
53 Self {
54 core_dir: core_dir.clone(),
55 packs_dir: packs_dir.clone(),
56 watcher: bones_utils::parking_lot::Mutex::new(None),
57 }
58 }
59}
60
61#[cfg(not(target_arch = "wasm32"))]
62impl AssetIo for FileAssetIo {
63 fn enumerate_packs(&self) -> BoxedFuture<anyhow::Result<Vec<String>>> {
64 if !self.packs_dir.exists() {
65 return Box::pin(async { Ok(Vec::new()) });
66 }
67
68 let packs_dir = self.packs_dir.clone();
69 Box::pin(async move {
70 let dirs = std::fs::read_dir(&packs_dir)?
72 .map(|entry| {
73 let entry = entry?;
74 let name = entry
75 .file_name()
76 .to_str()
77 .expect("non-unicode filename")
78 .to_owned();
79 Ok::<_, std::io::Error>(name)
80 })
81 .filter(|x| {
82 x.as_ref()
83 .map(|name| packs_dir.join(name).is_dir())
84 .unwrap_or(true)
85 })
86 .collect::<Result<Vec<_>, _>>()?;
87
88 Ok(dirs)
89 })
90 }
91
92 fn load_file(&self, loc: AssetLocRef) -> BoxedFuture<anyhow::Result<Vec<u8>>> {
93 let packs_dir = self.packs_dir.clone();
94 let core_dir = self.core_dir.clone();
95 let loc = loc.to_owned();
96
97 Box::pin(async move {
99 let base_dir = match loc.pack {
100 Some(folder) => packs_dir.join(folder),
101 None => core_dir.clone(),
102 };
103 let path = loc.path.absolutize_from("/").unwrap();
105 let path = path.strip_prefix("/").unwrap();
106 let path = base_dir.join(path);
107 std::fs::read(&path).with_context(|| format!("Could not load file: {path:?}"))
108 })
109 }
110
111 fn watch(&self, sender: Sender<ChangedAsset>) -> bool {
112 use notify::{RecursiveMode, Result, Watcher};
113
114 let core_dir_ = self.core_dir.clone();
115 let packs_dir_ = self.packs_dir.clone();
116 notify::recommended_watcher(move |res: Result<notify::Event>| match res {
117 Ok(event) => match &event.kind {
118 notify::EventKind::Create(_) | notify::EventKind::Modify(_) => {
119 for path in event.paths {
120 let (path, pack) = if let Ok(path) = path.strip_prefix(&core_dir_) {
121 (path, None)
122 } else if let Ok(path) = path.strip_prefix(&packs_dir_) {
123 let pack = path.iter().next().unwrap().to_str().unwrap().to_string();
124 let path = path.strip_prefix(&pack).unwrap();
125 (path, Some(pack))
126 } else {
127 continue;
128 };
129 sender
130 .send_blocking(ChangedAsset::Loc(crate::AssetLoc {
131 path: path.into(),
132 pack,
133 }))
134 .unwrap();
135 }
136 }
137 _ => (),
138 },
139 Err(e) => tracing::error!("watch error: {e:?}"),
140 })
141 .and_then(|mut w| {
142 if self.core_dir.exists() {
143 w.watch(&self.core_dir, RecursiveMode::Recursive)?;
144 }
145 if self.packs_dir.exists() {
146 w.watch(&self.packs_dir, RecursiveMode::Recursive)?;
147 }
148
149 *self.watcher.lock() = Some(Box::new(w) as _);
150 Ok(())
151 })
152 .map_err(|e| {
153 tracing::error!("watch error: {e:?}");
154 })
155 .map(|_| true)
156 .unwrap_or(false)
157 }
158}
159
160pub struct WebAssetIo {
162 pub asset_url: String,
164}
165
166impl WebAssetIo {
167 pub fn new(asset_url: &str) -> Self {
169 Self {
170 asset_url: asset_url.into(),
171 }
172 }
173}
174
175impl AssetIo for WebAssetIo {
176 fn enumerate_packs(&self) -> BoxedFuture<anyhow::Result<Vec<String>>> {
177 Box::pin(async move { Ok(default()) })
178 }
179
180 fn load_file(&self, loc: AssetLocRef) -> BoxedFuture<anyhow::Result<Vec<u8>>> {
181 let loc = loc.to_owned();
182 let asset_url = self.asset_url.clone();
183 Box::pin(async move {
184 if loc.pack.is_some() {
185 return Err(anyhow::format_err!("Cannot load asset packs on WASM yet"));
186 }
187 let url = format!(
188 "{}{}",
189 asset_url,
190 loc.path.absolutize_from("/").unwrap().to_str().unwrap()
191 );
192 let (sender, receiver) = async_channel::bounded(1);
193 let req = ehttp::Request::get(&url);
194 ehttp::fetch(req, move |resp| {
195 sender.send_blocking(resp.map(|resp| resp.bytes)).unwrap();
196 });
197 let result = receiver
198 .recv()
199 .await
200 .unwrap()
201 .map_err(|e| anyhow::format_err!("{e}"))
202 .with_context(|| format!("Could not download file: {url}"))?;
203
204 Ok(result)
205 })
206 }
207}
208
209pub struct DummyIo {
211 core: HashMap<PathBuf, Vec<u8>>,
212 packs: HashMap<String, HashMap<PathBuf, Vec<u8>>>,
213}
214
215impl DummyIo {
216 pub fn new<'a, I: IntoIterator<Item = (&'a str, Vec<u8>)>>(core: I) -> Self {
218 Self {
219 core: core
220 .into_iter()
221 .map(|(p, d)| (PathBuf::from(p), d))
222 .collect(),
223 packs: Default::default(),
224 }
225 }
226}
227
228impl AssetIo for DummyIo {
229 fn enumerate_packs(&self) -> BoxedFuture<anyhow::Result<Vec<String>>> {
230 let packs = self.packs.keys().cloned().collect();
231 Box::pin(async { Ok(packs) })
232 }
233
234 fn load_file(&self, loc: AssetLocRef) -> BoxedFuture<anyhow::Result<Vec<u8>>> {
235 let err = || {
236 anyhow::format_err!(
237 "File not found: `{:?}` in pack `{:?}`",
238 loc.path,
239 loc.pack.unwrap_or("[core]")
240 )
241 };
242 let data = (|| {
243 if let Some(pack_folder) = loc.pack {
244 self.packs
245 .get(pack_folder)
246 .ok_or_else(err)?
247 .get(loc.path)
248 .cloned()
249 .ok_or_else(err)
250 } else {
251 self.core.get(loc.path).cloned().ok_or_else(err)
252 }
253 })();
254 Box::pin(async move { data })
255 }
256}