1use image::DynamicImage;
5use lazy_static::lazy_static;
6use std::{
7 borrow::Cow,
8 collections::HashMap,
9 hash::{BuildHasher, Hash},
10 path::PathBuf,
11 sync::Arc,
12};
13
14pub use assets_manager::{
15 Asset, AssetCache, BoxedError, Error, FileAsset, SharedString,
16 asset::{DirLoadable, Ron, load_bincode_legacy, load_ron},
17 source::{self, Source},
18};
19
20mod fs;
21#[cfg(feature = "plugins")] mod plugin_cache;
22mod walk;
23pub use walk::{Walk, walk_tree};
24
25#[cfg(feature = "plugins")]
26lazy_static! {
27 static ref ASSETS: plugin_cache::CombinedCache = plugin_cache::CombinedCache::new().unwrap();
29}
30#[cfg(not(feature = "plugins"))]
31lazy_static! {
32 static ref ASSETS: AssetCache =
34 AssetCache::with_source(fs::FileSystem::new().unwrap());
35}
36
37#[cfg(feature = "plugins")]
39pub fn register_tar(path: PathBuf) -> std::io::Result<()> { ASSETS.register_tar(path) }
40
41pub type AssetHandle<T> = &'static assets_manager::Handle<T>;
42pub type AssetReadGuard<T> = assets_manager::AssetReadGuard<'static, T>;
43pub type AssetDirHandle<T> = AssetHandle<assets_manager::RecursiveDirectory<T>>;
44pub type ReloadWatcher = assets_manager::ReloadWatcher<'static>;
45
46pub trait AssetExt: Sized + Send + Sync + 'static {
49 fn load(specifier: &str) -> Result<AssetHandle<Self>, Error>;
57
58 fn load_cloned(specifier: &str) -> Result<Self, Error>
61 where
62 Self: Clone,
63 {
64 Self::load(specifier).map(|h| h.cloned())
65 }
66
67 fn load_or_insert_with(
68 specifier: &str,
69 default: impl FnOnce(Error) -> Self,
70 ) -> AssetHandle<Self> {
71 Self::load(specifier).unwrap_or_else(|err| Self::get_or_insert(specifier, default(err)))
72 }
73
74 #[track_caller]
82 fn load_expect(specifier: &str) -> AssetHandle<Self> {
83 #[track_caller]
84 #[cold]
85 fn expect_failed(err: Error) -> ! {
86 panic!(
87 "Failed loading essential asset: {} (error={:?})",
88 err.id(),
89 err.reason()
90 )
91 }
92
93 match Self::load(specifier) {
95 Ok(handle) => handle,
96 Err(err) => expect_failed(err),
97 }
98 }
99
100 #[track_caller]
103 fn load_expect_cloned(specifier: &str) -> Self
104 where
105 Self: Clone,
106 {
107 Self::load_expect(specifier).cloned()
108 }
109
110 fn load_owned(specifier: &str) -> Result<Self, Error>;
111
112 fn get_or_insert(specifier: &str, default: Self) -> AssetHandle<Self>;
113}
114
115impl<T: Asset> AssetExt for T {
116 fn load(specifier: &str) -> Result<AssetHandle<Self>, Error> { ASSETS.load(specifier) }
117
118 fn load_owned(specifier: &str) -> Result<Self, Error> { ASSETS.load_owned(specifier) }
119
120 fn get_or_insert(specifier: &str, default: Self) -> AssetHandle<Self> {
121 ASSETS.get_or_insert(specifier, default)
122 }
123}
124
125pub trait AssetCombined: AssetExt {
127 fn load_and_combine(
128 cache: &'static AssetCache,
129 specifier: &str,
130 ) -> Result<AssetHandle<Self>, Error>;
131
132 fn load_and_combine_static(specifier: &str) -> Result<AssetHandle<Self>, Error> {
134 #[cfg(feature = "plugins")]
135 {
136 ASSETS.no_record(|| Self::load_and_combine(ASSETS.as_cache(), specifier))
137 }
138 #[cfg(not(feature = "plugins"))]
139 {
140 Self::load(specifier)
141 }
142 }
143
144 #[track_caller]
145 fn load_expect_combined(cache: &'static AssetCache, specifier: &str) -> AssetHandle<Self> {
146 match Self::load_and_combine(cache, specifier) {
148 Ok(handle) => handle,
149 Err(err) => {
150 panic!("Failed loading essential combined asset: {specifier} (error={err:?})")
151 },
152 }
153 }
154
155 #[track_caller]
157 fn load_expect_combined_static(specifier: &str) -> AssetHandle<Self> {
158 #[cfg(feature = "plugins")]
159 {
160 ASSETS.no_record(|| Self::load_expect_combined(ASSETS.as_cache(), specifier))
161 }
162 #[cfg(not(feature = "plugins"))]
163 {
164 Self::load_expect(specifier)
165 }
166 }
167}
168
169impl<T: Asset + Concatenate> AssetCombined for T {
170 fn load_and_combine(
171 cache: &'static AssetCache,
172 specifier: &str,
173 ) -> Result<AssetHandle<Self>, Error> {
174 cache.load_and_combine(specifier)
175 }
176}
177
178pub trait CacheCombined {
180 fn load_and_combine<A: Asset + Concatenate>(
181 &self,
182 id: &str,
183 ) -> Result<&assets_manager::Handle<A>, Error>;
184}
185
186impl CacheCombined for AssetCache {
187 fn load_and_combine<A: Asset + Concatenate>(
188 &self,
189 specifier: &str,
190 ) -> Result<&assets_manager::Handle<A>, Error> {
191 #[cfg(feature = "plugins")]
192 {
193 tracing::info!("combine {specifier}");
194 let data: Result<A, _> = ASSETS.combine(self, |cache| cache.load_owned::<A>(specifier));
195 data.map(|data| self.get_or_insert(specifier, data))
196 }
197 #[cfg(not(feature = "plugins"))]
198 {
199 self.load(specifier)
200 }
201 }
202}
203
204pub fn load_rec_dir<T: DirLoadable + Asset>(specifier: &str) -> Result<AssetDirHandle<T>, Error> {
219 let specifier = specifier.strip_suffix(".*").unwrap_or(specifier);
220 ASSETS.load_rec_dir(specifier)
221}
222
223pub struct Image(pub Arc<DynamicImage>);
224
225impl Image {
226 pub fn to_image(&self) -> Arc<DynamicImage> { Arc::clone(&self.0) }
227}
228
229impl FileAsset for Image {
230 const EXTENSIONS: &'static [&'static str] = &["png", "jpg"];
231
232 fn from_bytes(bytes: Cow<[u8]>) -> Result<Self, BoxedError> {
233 let image = image::load_from_memory(&bytes)?;
234 Ok(Image(Arc::new(image)))
235 }
236}
237
238pub struct DotVox(pub dot_vox::DotVoxData);
239
240impl FileAsset for DotVox {
241 const EXTENSION: &'static str = "vox";
242
243 fn from_bytes(bytes: Cow<[u8]>) -> Result<Self, BoxedError> {
244 let data = dot_vox::load_bytes(&bytes).map_err(|err| err.to_owned())?;
245 Ok(DotVox(data))
246 }
247}
248
249pub struct Obj(pub wavefront::Obj);
250
251impl FileAsset for Obj {
252 const EXTENSION: &'static str = "obj";
253
254 fn from_bytes(bytes: Cow<[u8]>) -> Result<Self, BoxedError> {
255 let data = wavefront::Obj::from_reader(&*bytes)?;
256 Ok(Obj(data))
257 }
258}
259
260pub trait Concatenate {
261 fn concatenate(self, b: Self) -> Self;
262}
263
264impl<K: Eq + Hash, V, S: BuildHasher> Concatenate for HashMap<K, V, S> {
265 fn concatenate(mut self, b: Self) -> Self {
266 self.extend(b);
267 self
268 }
269}
270
271impl<V> Concatenate for Vec<V> {
272 fn concatenate(mut self, b: Self) -> Self {
273 self.extend(b);
274 self
275 }
276}
277
278impl<K: Eq + Hash, V, S: BuildHasher> Concatenate for hashbrown::HashMap<K, V, S> {
279 fn concatenate(mut self, b: Self) -> Self {
280 self.extend(b);
281 self
282 }
283}
284
285impl<T: Concatenate> Concatenate for Ron<T> {
286 fn concatenate(self, b: Self) -> Self { Self(self.into_inner().concatenate(b.into_inner())) }
287}
288
289#[cfg(feature = "plugins")]
291#[derive(Clone)]
292pub struct MultiRon<T>(pub T);
293
294#[cfg(feature = "plugins")]
295impl<T> Asset for MultiRon<T>
296where
297 T: for<'de> serde::Deserialize<'de> + Send + Sync + 'static + Concatenate,
298{
299 fn load(cache: &AssetCache, id: &SharedString) -> Result<Self, BoxedError> {
301 ASSETS
302 .combine(cache, |cache| {
303 cache.load_owned::<Ron<T>>(id).map(|ron| ron.into_inner())
304 })
305 .map(MultiRon)
306 .map_err(Into::<BoxedError>::into)
307 }
308}
309
310#[cfg(not(feature = "plugins"))]
312pub use assets_manager::asset::Ron as MultiRon;
313
314pub fn find_root() -> Option<PathBuf> {
316 std::env::current_dir().map_or(None, |path| {
317 if path.join(".git").exists() {
319 return Some(path);
320 }
321 for ancestor in path.ancestors().take(10) {
323 if ancestor.join(".git").exists() {
324 return Some(ancestor.to_path_buf());
325 }
326 }
327 None
328 })
329}
330
331lazy_static! {
332 pub static ref ASSETS_PATH: PathBuf = {
341 let mut paths = Vec::new();
342
343 if let Ok(var) = std::env::var("VELOREN_ASSETS") {
347 paths.push(var.into());
348 }
349
350 if let Ok(mut path) = std::env::current_exe() {
352 path.pop();
353 paths.push(path);
354 }
355
356 if let Some(path) = find_root() {
358 paths.push(path);
359 }
360
361 #[cfg(all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android")))]
363 {
364 if let Ok(result) = std::env::var("XDG_DATA_HOME") {
365 paths.push(format!("{}/veloren/", result).into());
366 } else if let Ok(result) = std::env::var("HOME") {
367 paths.push(format!("{}/.local/share/veloren/", result).into());
368 }
369
370 if let Ok(result) = std::env::var("XDG_DATA_DIRS") {
371 result.split(':').for_each(|x| paths.push(format!("{}/veloren/", x).into()));
372 } else {
373 let fallback_paths = vec!["/usr/local/share", "/usr/share"];
375 for fallback_path in fallback_paths {
376 paths.push(format!("{}/veloren/", fallback_path).into());
377 }
378 }
379 }
380
381 tracing::trace!("Possible asset locations paths={:?}", paths);
382
383 for mut path in paths.clone() {
384 if !path.ends_with("assets") {
385 path = path.join("assets");
386 }
387
388 if path.is_dir() {
389 tracing::info!("Assets found path={}", path.display());
390 return path;
391 }
392 }
393
394 panic!(
395 "Asset directory not found. In attempting to find it, we searched:\n{})",
396 paths.iter().fold(String::new(), |mut a, path| {
397 a += &path.to_string_lossy();
398 a += "\n";
399 a
400 }),
401 );
402 };
403}
404
405#[cfg(test)]
406mod tests {
407 use std::{ffi::OsStr, fs::File};
408 use walkdir::WalkDir;
409
410 #[test]
411 fn load_canary() {
412 let _ = *super::ASSETS;
414 }
415
416 #[test]
418 fn parse_all_ron_files_to_value() {
419 let ext = OsStr::new("ron");
420 WalkDir::new(crate::ASSETS_PATH.as_path())
421 .into_iter()
422 .map(|ent| {
423 ent.expect("Failed to walk over asset directory")
424 .into_path()
425 })
426 .filter(|path| path.is_file())
427 .filter(|path| {
428 path.extension()
429 .is_some_and(|e| ext == e.to_ascii_lowercase())
430 })
431 .for_each(|path| {
432 let file = File::open(&path).expect("Failed to open the file");
433 if let Err(err) = ron::de::from_reader::<_, ron::Value>(file) {
434 println!("{:?}", path);
435 println!("{:#?}", err);
436 panic!("Parse failed");
437 }
438 });
439 }
440}