game_toolkit_assets/
lib.rs1#![forbid(unsafe_code)]
21
22use std::collections::{HashMap, HashSet};
23use std::path::{Path, PathBuf};
24#[cfg(not(target_arch = "wasm32"))]
25use std::sync::mpsc::{Receiver, channel};
26
27#[cfg(not(target_arch = "wasm32"))]
28use anyhow::Context;
29use anyhow::Result;
30#[cfg(not(target_arch = "wasm32"))]
31use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
32
33#[cfg(target_arch = "wasm32")]
34mod web;
35
36pub const MANIFEST_FILE: &str = "manifest.txt";
38
39pub struct Assets {
40 root: PathBuf,
41 mem: HashMap<PathBuf, Vec<u8>>,
43 #[cfg(not(target_arch = "wasm32"))]
44 watcher: Option<RecommendedWatcher>,
45 #[cfg(not(target_arch = "wasm32"))]
46 rx: Option<Receiver<PathBuf>>,
47 pending: HashSet<PathBuf>,
48}
49
50impl Assets {
51 pub fn new(root: impl AsRef<Path>) -> Result<Self> {
55 let root = root.as_ref();
56 #[cfg(not(target_arch = "wasm32"))]
57 let root = root
58 .canonicalize()
59 .with_context(|| format!("asset root {} not found", root.display()))?;
60 #[cfg(target_arch = "wasm32")]
61 let root = root.to_path_buf();
62 Ok(Self {
63 root,
64 mem: HashMap::new(),
65 #[cfg(not(target_arch = "wasm32"))]
66 watcher: None,
67 #[cfg(not(target_arch = "wasm32"))]
68 rx: None,
69 pending: HashSet::new(),
70 })
71 }
72
73 pub fn root(&self) -> &Path {
74 &self.root
75 }
76
77 pub fn resolve(&self, rel: impl AsRef<Path>) -> PathBuf {
78 let rel = rel.as_ref();
79 if rel.is_absolute() {
80 rel.to_path_buf()
81 } else {
82 self.root.join(rel)
83 }
84 }
85
86 pub fn read(&self, rel: impl AsRef<Path>) -> Result<Vec<u8>> {
89 let p = self.resolve(rel.as_ref());
90 if let Some(bytes) = self.mem.get(&p) {
91 return Ok(bytes.clone());
92 }
93 #[cfg(not(target_arch = "wasm32"))]
94 {
95 std::fs::read(&p).with_context(|| format!("read {}", p.display()))
96 }
97 #[cfg(target_arch = "wasm32")]
98 {
99 anyhow::bail!(
100 "asset {} was not preloaded; list it in {}/{MANIFEST_FILE}",
101 p.display(),
102 self.root.display()
103 )
104 }
105 }
106
107 pub fn insert(&mut self, rel: impl AsRef<Path>, bytes: impl Into<Vec<u8>>) {
109 let p = self.resolve(rel);
110 self.mem.insert(p, bytes.into());
111 }
112
113 pub fn is_loaded(&self, rel: impl AsRef<Path>) -> bool {
115 self.mem.contains_key(&self.resolve(rel))
116 }
117
118 #[cfg(not(target_arch = "wasm32"))]
121 pub fn watch(&mut self) -> Result<()> {
122 if self.watcher.is_some() {
123 return Ok(());
124 }
125 let (tx, rx) = channel::<PathBuf>();
126 let mut watcher =
127 notify::recommended_watcher(move |res: notify::Result<Event>| match res {
128 Ok(event) => {
129 if matches!(
130 event.kind,
131 EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)
132 ) {
133 for p in event.paths {
134 let _ = tx.send(p);
135 }
136 }
137 }
138 Err(e) => log::warn!("notify error: {e}"),
139 })?;
140 watcher.watch(&self.root, RecursiveMode::Recursive)?;
141 self.watcher = Some(watcher);
142 self.rx = Some(rx);
143 Ok(())
144 }
145
146 #[cfg(target_arch = "wasm32")]
149 pub fn watch(&mut self) -> Result<()> {
150 log::info!("asset hot-reload is not available on the web");
151 Ok(())
152 }
153
154 pub fn drain_changes(&mut self) -> Vec<PathBuf> {
158 #[cfg(not(target_arch = "wasm32"))]
159 if let Some(rx) = self.rx.as_ref() {
160 while let Ok(p) = rx.try_recv() {
161 let canon = p.canonicalize().unwrap_or(p);
162 self.pending.insert(canon);
163 }
164 }
165 self.pending.drain().collect()
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn inserted_bytes_shadow_the_filesystem() {
175 let mut assets = Assets::new(".").unwrap();
176 assert!(!assets.is_loaded("Cargo.toml"));
177 assets.insert("Cargo.toml", b"in memory".to_vec());
178 assert!(assets.is_loaded("Cargo.toml"));
179 assert_eq!(assets.read("Cargo.toml").unwrap(), b"in memory");
180 }
181
182 #[test]
183 fn falls_back_to_disk() {
184 let assets = Assets::new(".").unwrap();
185 assert!(assets.read("Cargo.toml").unwrap().starts_with(b"[package]"));
186 assert!(assets.read("does-not-exist.bin").is_err());
187 }
188}