1use dot_vox::DotVoxData;
5use image::DynamicImage;
6use lazy_static::lazy_static;
7use std::{
8 borrow::Cow,
9 collections::HashMap,
10 hash::{BuildHasher, Hash},
11 path::PathBuf,
12 sync::Arc,
13};
14
15pub use assets_manager::{
16 AnyCache, Asset, AssetCache, BoxedError, Compound, Error, SharedString,
17 asset::{DirLoadable, Ron},
18 loader::{
19 self, BincodeLoader, BytesLoader, JsonLoader, LoadFrom, Loader, RonLoader, StringLoader,
20 },
21 source::{self, Source},
22};
23
24mod fs;
25#[cfg(feature = "plugins")] mod plugin_cache;
26mod walk;
27pub use walk::{Walk, walk_tree};
28
29#[cfg(feature = "plugins")]
30lazy_static! {
31 static ref ASSETS: plugin_cache::CombinedCache = plugin_cache::CombinedCache::new().unwrap();
33}
34#[cfg(not(feature = "plugins"))]
35lazy_static! {
36 static ref ASSETS: AssetCache<fs::FileSystem> =
38 AssetCache::with_source(fs::FileSystem::new().unwrap());
39}
40
41#[cfg(feature = "hot-reloading")]
42pub fn start_hot_reloading() { ASSETS.enhance_hot_reloading(); }
43
44#[cfg(feature = "plugins")]
46pub fn register_tar(path: PathBuf) -> std::io::Result<()> { ASSETS.register_tar(path) }
47
48pub type AssetHandle<T> = &'static assets_manager::Handle<T>;
49pub type AssetReadGuard<T> = assets_manager::AssetReadGuard<'static, T>;
50pub type AssetDirHandle<T> = AssetHandle<assets_manager::RecursiveDirectory<T>>;
51pub type ReloadWatcher = assets_manager::ReloadWatcher<'static>;
52
53pub trait AssetExt: Sized + Send + Sync + 'static {
56 fn load(specifier: &str) -> Result<AssetHandle<Self>, Error>;
64
65 fn load_cloned(specifier: &str) -> Result<Self, Error>
68 where
69 Self: Clone,
70 {
71 Self::load(specifier).map(|h| h.cloned())
72 }
73
74 fn load_or_insert_with(
75 specifier: &str,
76 default: impl FnOnce(Error) -> Self,
77 ) -> AssetHandle<Self> {
78 Self::load(specifier).unwrap_or_else(|err| Self::get_or_insert(specifier, default(err)))
79 }
80
81 #[track_caller]
89 fn load_expect(specifier: &str) -> AssetHandle<Self> {
90 #[track_caller]
91 #[cold]
92 fn expect_failed(err: Error) -> ! {
93 panic!(
94 "Failed loading essential asset: {} (error={:?})",
95 err.id(),
96 err.reason()
97 )
98 }
99
100 match Self::load(specifier) {
102 Ok(handle) => handle,
103 Err(err) => expect_failed(err),
104 }
105 }
106
107 #[track_caller]
110 fn load_expect_cloned(specifier: &str) -> Self
111 where
112 Self: Clone,
113 {
114 Self::load_expect(specifier).cloned()
115 }
116
117 fn load_owned(specifier: &str) -> Result<Self, Error>;
118
119 fn get_or_insert(specifier: &str, default: Self) -> AssetHandle<Self>;
120}
121
122pub trait AssetCombined: AssetExt {
124 fn load_and_combine(
125 reloading_cache: AnyCache<'static>,
126 specifier: &str,
127 ) -> Result<AssetHandle<Self>, Error>;
128
129 fn load_and_combine_static(specifier: &str) -> Result<AssetHandle<Self>, Error> {
131 #[cfg(feature = "plugins")]
132 {
133 Self::load_and_combine(ASSETS.non_reloading_cache(), specifier)
134 }
135 #[cfg(not(feature = "plugins"))]
136 {
137 Self::load(specifier)
138 }
139 }
140
141 #[track_caller]
142 fn load_expect_combined(
143 reloading_cache: AnyCache<'static>,
144 specifier: &str,
145 ) -> AssetHandle<Self> {
146 match Self::load_and_combine(reloading_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 Self::load_expect_combined(ASSETS.non_reloading_cache(), specifier)
161 }
162 #[cfg(not(feature = "plugins"))]
163 {
164 Self::load_expect(specifier)
165 }
166 }
167}
168
169pub trait CacheCombined<'a> {
171 fn load_and_combine<A: Compound + Concatenate>(
172 self,
173 id: &str,
174 ) -> Result<&'a assets_manager::Handle<A>, Error>;
175}
176
177pub fn load_rec_dir<T: DirLoadable>(specifier: &str) -> Result<AssetDirHandle<T>, Error> {
186 let specifier = specifier.strip_suffix(".*").unwrap_or(specifier);
187 ASSETS.load_rec_dir(specifier)
188}
189
190impl<T: Compound> AssetExt for T {
191 fn load(specifier: &str) -> Result<AssetHandle<Self>, Error> { ASSETS.load(specifier) }
192
193 fn load_owned(specifier: &str) -> Result<Self, Error> { ASSETS.load_owned(specifier) }
194
195 fn get_or_insert(specifier: &str, default: Self) -> AssetHandle<Self> {
196 ASSETS.get_or_insert(specifier, default)
197 }
198}
199
200impl<'a> CacheCombined<'a> for AnyCache<'a> {
201 fn load_and_combine<A: Compound + Concatenate>(
202 self,
203 specifier: &str,
204 ) -> Result<&'a assets_manager::Handle<A>, Error> {
205 #[cfg(feature = "plugins")]
206 {
207 tracing::info!("combine {specifier}");
208 let data: Result<A, _> =
209 ASSETS.combine(self, |cache: AnyCache| cache.load_owned::<A>(specifier));
210 data.map(|data| self.get_or_insert(specifier, data))
211 }
212 #[cfg(not(feature = "plugins"))]
213 {
214 self.load(specifier)
215 }
216 }
217}
218
219impl<T: Compound + Concatenate> AssetCombined for T {
220 fn load_and_combine(
221 reloading_cache: AnyCache<'static>,
222 specifier: &str,
223 ) -> Result<AssetHandle<Self>, Error> {
224 reloading_cache.load_and_combine(specifier)
225 }
226}
227
228pub struct Image(pub Arc<DynamicImage>);
229
230impl Image {
231 pub fn to_image(&self) -> Arc<DynamicImage> { Arc::clone(&self.0) }
232}
233
234pub struct ImageLoader;
235impl Loader<Image> for ImageLoader {
236 fn load(content: Cow<[u8]>, ext: &str) -> Result<Image, BoxedError> {
237 let format = image::ImageFormat::from_extension(ext)
238 .ok_or_else(|| format!("Invalid file extension {}", ext))?;
239 let image = image::load_from_memory_with_format(&content, format)?;
240 Ok(Image(Arc::new(image)))
241 }
242}
243
244impl Asset for Image {
245 type Loader = ImageLoader;
246
247 const EXTENSIONS: &'static [&'static str] = &["png", "jpg"];
248}
249
250pub struct DotVoxAsset(pub DotVoxData);
251
252pub struct DotVoxLoader;
253impl Loader<DotVoxAsset> for DotVoxLoader {
254 fn load(content: Cow<[u8]>, _: &str) -> Result<DotVoxAsset, BoxedError> {
255 let data = dot_vox::load_bytes(&content).map_err(|err| err.to_owned())?;
256 Ok(DotVoxAsset(data))
257 }
258}
259
260impl Asset for DotVoxAsset {
261 type Loader = DotVoxLoader;
262
263 const EXTENSION: &'static str = "vox";
264}
265
266pub struct ObjAsset(pub wavefront::Obj);
267
268impl Asset for ObjAsset {
269 type Loader = ObjAssetLoader;
270
271 const EXTENSION: &'static str = "obj";
272}
273
274pub struct ObjAssetLoader;
275impl Loader<ObjAsset> for ObjAssetLoader {
276 fn load(content: Cow<[u8]>, _: &str) -> Result<ObjAsset, BoxedError> {
277 let data = wavefront::Obj::from_reader(&*content)?;
278 Ok(ObjAsset(data))
279 }
280}
281
282pub trait Concatenate {
283 fn concatenate(self, b: Self) -> Self;
284}
285
286impl<K: Eq + Hash, V, S: BuildHasher> Concatenate for HashMap<K, V, S> {
287 fn concatenate(mut self, b: Self) -> Self {
288 self.extend(b);
289 self
290 }
291}
292
293impl<V> Concatenate for Vec<V> {
294 fn concatenate(mut self, b: Self) -> Self {
295 self.extend(b);
296 self
297 }
298}
299
300impl<K: Eq + Hash, V, S: BuildHasher> Concatenate for hashbrown::HashMap<K, V, S> {
301 fn concatenate(mut self, b: Self) -> Self {
302 self.extend(b);
303 self
304 }
305}
306
307impl<T: Concatenate> Concatenate for Ron<T> {
308 fn concatenate(self, _b: Self) -> Self { todo!() }
309}
310
311#[cfg(feature = "plugins")]
313#[derive(Clone)]
314pub struct MultiRon<T>(pub T);
315
316#[cfg(feature = "plugins")]
317impl<T> Compound for MultiRon<T>
318where
319 T: for<'de> serde::Deserialize<'de> + Send + Sync + 'static + Concatenate,
320{
321 fn load(reloading_cache: AnyCache, id: &SharedString) -> Result<Self, BoxedError> {
323 ASSETS
324 .combine(reloading_cache, |cache: AnyCache| {
325 cache.load_owned::<Ron<T>>(id).map(|ron| ron.into_inner())
326 })
327 .map(MultiRon)
328 .map_err(Into::<BoxedError>::into)
329 }
330}
331
332#[cfg(not(feature = "plugins"))]
334pub use assets_manager::asset::Ron as MultiRon;
335
336pub fn find_root() -> Option<PathBuf> {
338 std::env::current_dir().map_or(None, |path| {
339 if path.join(".git").exists() {
341 return Some(path);
342 }
343 for ancestor in path.ancestors().take(10) {
345 if ancestor.join(".git").exists() {
346 return Some(ancestor.to_path_buf());
347 }
348 }
349 None
350 })
351}
352
353lazy_static! {
354 pub static ref ASSETS_PATH: PathBuf = {
363 let mut paths = Vec::new();
364
365 if let Ok(var) = std::env::var("VELOREN_ASSETS") {
369 paths.push(var.into());
370 }
371
372 if let Ok(mut path) = std::env::current_exe() {
374 path.pop();
375 paths.push(path);
376 }
377
378 if let Some(path) = find_root() {
380 paths.push(path);
381 }
382
383 #[cfg(all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android")))]
385 {
386 if let Ok(result) = std::env::var("XDG_DATA_HOME") {
387 paths.push(format!("{}/veloren/", result).into());
388 } else if let Ok(result) = std::env::var("HOME") {
389 paths.push(format!("{}/.local/share/veloren/", result).into());
390 }
391
392 if let Ok(result) = std::env::var("XDG_DATA_DIRS") {
393 result.split(':').for_each(|x| paths.push(format!("{}/veloren/", x).into()));
394 } else {
395 let fallback_paths = vec!["/usr/local/share", "/usr/share"];
397 for fallback_path in fallback_paths {
398 paths.push(format!("{}/veloren/", fallback_path).into());
399 }
400 }
401 }
402
403 tracing::trace!("Possible asset locations paths={:?}", paths);
404
405 for mut path in paths.clone() {
406 if !path.ends_with("assets") {
407 path = path.join("assets");
408 }
409
410 if path.is_dir() {
411 tracing::info!("Assets found path={}", path.display());
412 return path;
413 }
414 }
415
416 panic!(
417 "Asset directory not found. In attempting to find it, we searched:\n{})",
418 paths.iter().fold(String::new(), |mut a, path| {
419 a += &path.to_string_lossy();
420 a += "\n";
421 a
422 }),
423 );
424 };
425}
426
427#[cfg(test)]
428mod tests {
429 use std::{ffi::OsStr, fs::File};
430 use walkdir::WalkDir;
431
432 #[test]
433 fn load_canary() {
434 let _ = *super::ASSETS;
436 }
437
438 #[test]
440 fn parse_all_ron_files_to_value() {
441 let ext = OsStr::new("ron");
442 WalkDir::new(crate::ASSETS_PATH.as_path())
443 .into_iter()
444 .map(|ent| {
445 ent.expect("Failed to walk over asset directory")
446 .into_path()
447 })
448 .filter(|path| path.is_file())
449 .filter(|path| {
450 path.extension()
451 .is_some_and(|e| ext == e.to_ascii_lowercase())
452 })
453 .for_each(|path| {
454 let file = File::open(&path).expect("Failed to open the file");
455 if let Err(err) = ron::de::from_reader::<_, ron::Value>(file) {
456 println!("{:?}", path);
457 println!("{:#?}", err);
458 panic!("Parse failed");
459 }
460 });
461 }
462}
463
464#[cfg(feature = "asset_tweak")]
465pub mod asset_tweak {
466 use super::{ASSETS_PATH, Asset, AssetExt, RonLoader};
476 use ron::ser::{PrettyConfig, to_writer_pretty};
477 use serde::{Deserialize, Serialize, de::DeserializeOwned};
478 use std::{fs, path::Path};
479
480 pub enum Specifier<'a> {
487 Tweak(&'a str),
488 Asset(&'a [&'a str]),
489 }
490
491 #[derive(Clone, Deserialize, Serialize)]
492 struct AssetTweakWrapper<T>(T);
493
494 impl<T> Asset for AssetTweakWrapper<T>
495 where
496 T: Clone + Sized + Send + Sync + 'static + DeserializeOwned,
497 {
498 type Loader = RonLoader;
499
500 const EXTENSION: &'static str = "ron";
501 }
502
503 pub fn tweak_expect<T>(specifier: Specifier) -> T
545 where
546 T: Clone + Sized + Send + Sync + 'static + DeserializeOwned,
547 {
548 let asset_specifier = match specifier {
549 Specifier::Tweak(specifier) => format!("tweak.{}", specifier),
550 Specifier::Asset(path) => path.join("."),
551 };
552 let handle = <AssetTweakWrapper<T> as AssetExt>::load_expect(&asset_specifier);
553 let AssetTweakWrapper(value) = handle.cloned();
554
555 value
556 }
557
558 fn create_new<T>(tweak_dir: &Path, filename: &str, value: T) -> T
563 where
564 T: Sized + Send + Sync + 'static + DeserializeOwned + Serialize,
565 {
566 fs::create_dir_all(tweak_dir).expect("failed to create directory for tweak files");
567 let f = fs::File::create(tweak_dir.join(filename)).unwrap_or_else(|error| {
568 panic!("failed to create file {:?}. Error: {:?}", filename, error)
569 });
570 let tweaker = AssetTweakWrapper(&value);
571 if let Err(e) = to_writer_pretty(f, &tweaker, PrettyConfig::new()) {
572 panic!("failed to write to file {:?}. Error: {:?}", filename, e);
573 }
574
575 value
576 }
577
578 fn directory_and_name<'a>(path: &'a [&'a str]) -> (String, &'a str) {
582 let (file, path) = path.split_last().expect("empty asset list");
583 let directory = path.join("/");
584
585 (directory, file)
586 }
587
588 pub fn tweak_expect_or_create<T>(specifier: Specifier, value: T) -> T
622 where
623 T: Clone + Sized + Send + Sync + 'static + DeserializeOwned + Serialize,
624 {
625 let (dir, filename) = match specifier {
626 Specifier::Tweak(name) => (ASSETS_PATH.join("tweak"), format!("{}.ron", name)),
627 Specifier::Asset(list) => {
628 let (directory, name) = directory_and_name(list);
629 (ASSETS_PATH.join(directory), format!("{}.ron", name))
630 },
631 };
632
633 if Path::new(&dir.join(&filename)).is_file() {
634 tweak_expect(specifier)
635 } else {
636 create_new(&dir, &filename, value)
637 }
638 }
639
640 #[macro_export]
677 macro_rules! tweak {
678 ($name:literal) => {{
679 use $crate::asset_tweak::{Specifier::Tweak, tweak_expect};
680
681 tweak_expect(Tweak($name))
682 }};
683
684 ($name:literal, $default:expr) => {{
685 use $crate::asset_tweak::{Specifier::Tweak, tweak_expect_or_create};
686
687 tweak_expect_or_create(Tweak($name), $default)
688 }};
689 }
690
691 #[macro_export]
720 macro_rules! tweak_from {
721 ($path:expr) => {{
722 use $crate::asset_tweak::{Specifier::Asset, tweak_expect};
723
724 tweak_expect(Asset($path))
725 }};
726
727 ($path:expr, $default:expr) => {{
728 use $crate::asset_tweak::{Specifier::Asset, tweak_expect_or_create};
729
730 tweak_expect_or_create(Asset($path), $default)
731 }};
732 }
733
734 #[cfg(test)]
735 mod tests {
736 use super::*;
737 use std::{
738 convert::AsRef,
739 fmt::Debug,
740 fs::{self, File},
741 io::Write,
742 path::Path,
743 };
744
745 struct DirectoryGuard<P>
746 where
747 P: AsRef<Path>,
748 {
749 dir: P,
750 }
751
752 impl<P> DirectoryGuard<P>
753 where
754 P: AsRef<Path>,
755 {
756 fn create(dir: P) -> Self {
757 fs::create_dir_all(&dir).expect("failed to create directory");
758 Self { dir }
759 }
760 }
761
762 impl<P> Drop for DirectoryGuard<P>
763 where
764 P: AsRef<Path>,
765 {
766 fn drop(&mut self) { fs::remove_dir(&self.dir).expect("failed to remove directory"); }
767 }
768
769 struct FileGuard<P>
770 where
771 P: AsRef<Path> + Debug,
772 {
773 file: P,
774 }
775
776 impl<P> FileGuard<P>
777 where
778 P: AsRef<Path> + Debug,
779 {
780 fn create(file: P) -> (Self, File) {
781 let f = File::create(&file)
782 .unwrap_or_else(|_| panic!("failed to create file {:?}", &file));
783 (Self { file }, f)
784 }
785
786 fn hold(file: P) -> Self { Self { file } }
787 }
788
789 impl<P> Drop for FileGuard<P>
790 where
791 P: AsRef<Path> + Debug,
792 {
793 fn drop(&mut self) {
794 fs::remove_file(&self.file).unwrap_or_else(|e| {
795 panic!("failed to remove file {:?}. Error: {:?}", &self.file, e)
796 });
797 }
798 }
799
800 fn run_with_file(tweak_path: &[&str], test: impl Fn(&mut File)) {
803 let (tweak_dir, tweak_name) = directory_and_name(tweak_path);
804 let tweak_folder = ASSETS_PATH.join(tweak_dir);
805 let tweak_file = tweak_folder.join(format!("{}.ron", tweak_name));
806
807 let _dir_guard = DirectoryGuard::create(tweak_folder);
808 let (_file_guard, mut file) = FileGuard::create(tweak_file);
809
810 test(&mut file);
811 }
812
813 #[test]
814 fn test_tweaked_int() {
815 let tweak_path = &["tweak_test_int", "tweak"];
816
817 run_with_file(tweak_path, |file| {
818 file.write_all(b"(5)").expect("failed to write to the file");
819 let x: i32 = tweak_expect(Specifier::Asset(tweak_path));
820 assert_eq!(x, 5);
821 });
822 }
823
824 #[test]
825 fn test_tweaked_string() {
826 let tweak_path = &["tweak_test_string", "tweak"];
827
828 run_with_file(tweak_path, |file| {
829 file.write_all(br#"("Hello Zest")"#)
830 .expect("failed to write to the file");
831
832 let x: String = tweak_expect(Specifier::Asset(tweak_path));
833 assert_eq!(x, "Hello Zest".to_owned());
834 });
835 }
836
837 #[test]
838 fn test_tweaked_hashmap() {
839 type Map = std::collections::HashMap<String, i32>;
840
841 let tweak_path = &["tweak_test_map", "tweak"];
842
843 run_with_file(tweak_path, |file| {
844 file.write_all(
845 br#"
846 ({
847 "wow": 4,
848 "such": 5,
849 })
850 "#,
851 )
852 .expect("failed to write to the file");
853
854 let x: Map = tweak_expect(Specifier::Asset(tweak_path));
855
856 let mut map = Map::new();
857 map.insert("wow".to_owned(), 4);
858 map.insert("such".to_owned(), 5);
859 assert_eq!(x, map);
860 });
861 }
862
863 #[test]
864 fn test_tweaked_with_macro_struct() {
865 #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
867 struct Wow {
868 such: i32,
869 field: f32,
870 }
871
872 let tweak_path = &["tweak_test_struct", "tweak"];
873
874 run_with_file(tweak_path, |file| {
875 file.write_all(
876 br"
877 ((
878 such: 5,
879 field: 35.752346,
880 ))
881 ",
882 )
883 .expect("failed to write to the file");
884
885 let x: Wow = crate::tweak_from!(tweak_path);
886 let expected = Wow {
887 such: 5,
888 field: 35.752_346,
889 };
890 assert_eq!(x, expected);
891 });
892 }
893
894 fn run_with_path(tweak_path: &[&str], test: impl Fn(&Path)) {
895 let (tweak_dir, tweak_name) = directory_and_name(tweak_path);
896
897 let tweak_folder = ASSETS_PATH.join(tweak_dir);
898 let test_path = tweak_folder.join(format!("{}.ron", tweak_name));
899
900 let _file_guard = FileGuard::hold(&test_path);
901
902 test(&test_path);
903 }
904
905 #[test]
906 fn test_create_tweak() {
907 let tweak_path = &["tweak_create_test", "tweak"];
908
909 run_with_path(tweak_path, |test_path| {
910 let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
911 assert_eq!(x, 5);
912 assert!(test_path.is_file());
913 let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
915 assert_eq!(x, 5);
916 });
917 }
918
919 #[test]
920 fn test_create_tweak_deep() {
921 let tweak_path = &["so_much", "deep_test", "tweak_create_test", "tweak"];
922
923 run_with_path(tweak_path, |test_path| {
924 let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
925 assert_eq!(x, 5);
926 assert!(test_path.is_file());
927 let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
929 assert_eq!(x, 5);
930 });
931 }
932
933 #[test]
934 fn test_create_but_prioritize_loaded() {
935 let tweak_path = &["tweak_create_and_prioritize_test", "tweak"];
936
937 run_with_path(tweak_path, |test_path| {
938 let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
939 assert_eq!(x, 5);
940 assert!(test_path.is_file());
941
942 fs::write(test_path, b"(10)").expect("failed to write to the file");
945 let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
946 assert_eq!(x, 10);
947 });
948 }
949 }
950}