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