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}
441
442#[cfg(feature = "asset_tweak")]
443pub mod asset_tweak {
444 use super::{ASSETS_PATH, AssetExt, Ron};
454 use ron::{options::Options, ser::PrettyConfig};
455 use serde::{Serialize, de::DeserializeOwned};
456 use std::{fs, path::Path};
457
458 pub enum Specifier<'a> {
465 Tweak(&'a str),
466 Asset(&'a [&'a str]),
467 }
468
469 pub fn tweak_expect<T>(specifier: Specifier) -> T
511 where
512 T: Clone + Sized + Send + Sync + 'static + DeserializeOwned,
513 {
514 let asset_specifier = match specifier {
515 Specifier::Tweak(specifier) => format!("tweak.{}", specifier),
516 Specifier::Asset(path) => path.join("."),
517 };
518 let handle = <Ron<T> as AssetExt>::load_expect(&asset_specifier);
519 let Ron(value) = handle.cloned();
520
521 value
522 }
523
524 fn create_new<T>(tweak_dir: &Path, filename: &str, value: T) -> T
529 where
530 T: Sized + Send + Sync + 'static + DeserializeOwned + Serialize,
531 {
532 fs::create_dir_all(tweak_dir).expect("failed to create directory for tweak files");
533 let f = fs::File::create(tweak_dir.join(filename)).unwrap_or_else(|error| {
534 panic!("failed to create file {:?}. Error: {:?}", filename, error)
535 });
536 let tweaker = Ron(&value);
537 if let Err(e) = Options::default().to_io_writer_pretty(f, &tweaker, PrettyConfig::new()) {
538 panic!("failed to write to file {:?}. Error: {:?}", filename, e);
539 }
540
541 value
542 }
543
544 fn directory_and_name<'a>(path: &'a [&'a str]) -> (String, &'a str) {
548 let (file, path) = path.split_last().expect("empty asset list");
549 let directory = path.join("/");
550
551 (directory, file)
552 }
553
554 pub fn tweak_expect_or_create<T>(specifier: Specifier, value: T) -> T
588 where
589 T: Clone + Sized + Send + Sync + 'static + DeserializeOwned + Serialize,
590 {
591 let (dir, filename) = match specifier {
592 Specifier::Tweak(name) => (ASSETS_PATH.join("tweak"), format!("{}.ron", name)),
593 Specifier::Asset(list) => {
594 let (directory, name) = directory_and_name(list);
595 (ASSETS_PATH.join(directory), format!("{}.ron", name))
596 },
597 };
598
599 if Path::new(&dir.join(&filename)).is_file() {
600 tweak_expect(specifier)
601 } else {
602 create_new(&dir, &filename, value)
603 }
604 }
605
606 #[macro_export]
643 macro_rules! tweak {
644 ($name:literal) => {{
645 use $crate::asset_tweak::{Specifier::Tweak, tweak_expect};
646
647 tweak_expect(Tweak($name))
648 }};
649
650 ($name:literal, $default:expr) => {{
651 use $crate::asset_tweak::{Specifier::Tweak, tweak_expect_or_create};
652
653 tweak_expect_or_create(Tweak($name), $default)
654 }};
655 }
656
657 #[macro_export]
686 macro_rules! tweak_from {
687 ($path:expr) => {{
688 use $crate::asset_tweak::{Specifier::Asset, tweak_expect};
689
690 tweak_expect(Asset($path))
691 }};
692
693 ($path:expr, $default:expr) => {{
694 use $crate::asset_tweak::{Specifier::Asset, tweak_expect_or_create};
695
696 tweak_expect_or_create(Asset($path), $default)
697 }};
698 }
699
700 #[cfg(test)]
701 mod tests {
702 use super::*;
703 use serde::Deserialize;
704 use std::{
705 convert::AsRef,
706 fmt::Debug,
707 fs::{self, File},
708 io::Write,
709 path::Path,
710 };
711
712 struct DirectoryGuard<P>
713 where
714 P: AsRef<Path>,
715 {
716 dir: P,
717 }
718
719 impl<P> DirectoryGuard<P>
720 where
721 P: AsRef<Path>,
722 {
723 fn create(dir: P) -> Self {
724 fs::create_dir_all(&dir).expect("failed to create directory");
725 Self { dir }
726 }
727 }
728
729 impl<P> Drop for DirectoryGuard<P>
730 where
731 P: AsRef<Path>,
732 {
733 fn drop(&mut self) { fs::remove_dir(&self.dir).expect("failed to remove directory"); }
734 }
735
736 struct FileGuard<P>
737 where
738 P: AsRef<Path> + Debug,
739 {
740 file: P,
741 }
742
743 impl<P> FileGuard<P>
744 where
745 P: AsRef<Path> + Debug,
746 {
747 fn create(file: P) -> (Self, File) {
748 let f = File::create(&file)
749 .unwrap_or_else(|_| panic!("failed to create file {:?}", &file));
750 (Self { file }, f)
751 }
752
753 fn hold(file: P) -> Self { Self { file } }
754 }
755
756 impl<P> Drop for FileGuard<P>
757 where
758 P: AsRef<Path> + Debug,
759 {
760 fn drop(&mut self) {
761 fs::remove_file(&self.file).unwrap_or_else(|e| {
762 panic!("failed to remove file {:?}. Error: {:?}", &self.file, e)
763 });
764 }
765 }
766
767 fn run_with_file(tweak_path: &[&str], test: impl Fn(&mut File)) {
770 let (tweak_dir, tweak_name) = directory_and_name(tweak_path);
771 let tweak_folder = ASSETS_PATH.join(tweak_dir);
772 let tweak_file = tweak_folder.join(format!("{}.ron", tweak_name));
773
774 let _dir_guard = DirectoryGuard::create(tweak_folder);
775 let (_file_guard, mut file) = FileGuard::create(tweak_file);
776
777 test(&mut file);
778 }
779
780 #[test]
781 fn test_tweaked_int() {
782 let tweak_path = &["tweak_test_int", "tweak"];
783
784 run_with_file(tweak_path, |file| {
785 file.write_all(b"5").expect("failed to write to the file");
786 let x: i32 = tweak_expect(Specifier::Asset(tweak_path));
787 assert_eq!(x, 5);
788 });
789 }
790
791 #[test]
792 fn test_tweaked_string() {
793 let tweak_path = &["tweak_test_string", "tweak"];
794
795 run_with_file(tweak_path, |file| {
796 file.write_all(br#""Hello Zest""#)
797 .expect("failed to write to the file");
798
799 let x: String = tweak_expect(Specifier::Asset(tweak_path));
800 assert_eq!(x, "Hello Zest".to_owned());
801 });
802 }
803
804 #[test]
805 fn test_tweaked_hashmap() {
806 type Map = std::collections::HashMap<String, i32>;
807
808 let tweak_path = &["tweak_test_map", "tweak"];
809
810 run_with_file(tweak_path, |file| {
811 file.write_all(
812 br#"
813 {
814 "wow": 4,
815 "such": 5,
816 }
817 "#,
818 )
819 .expect("failed to write to the file");
820
821 let x: Map = tweak_expect(Specifier::Asset(tweak_path));
822
823 let mut map = Map::new();
824 map.insert("wow".to_owned(), 4);
825 map.insert("such".to_owned(), 5);
826 assert_eq!(x, map);
827 });
828 }
829
830 #[test]
831 fn test_tweaked_with_macro_struct() {
832 #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
834 struct Wow {
835 such: i32,
836 field: f32,
837 }
838
839 let tweak_path = &["tweak_test_struct", "tweak"];
840
841 run_with_file(tweak_path, |file| {
842 file.write_all(
843 br"
844 (
845 such: 5,
846 field: 35.752346,
847 )
848 ",
849 )
850 .expect("failed to write to the file");
851
852 let x: Wow = crate::tweak_from!(tweak_path);
853 let expected = Wow {
854 such: 5,
855 field: 35.752_346,
856 };
857 assert_eq!(x, expected);
858 });
859 }
860
861 fn run_with_path(tweak_path: &[&str], test: impl Fn(&Path)) {
862 let (tweak_dir, tweak_name) = directory_and_name(tweak_path);
863
864 let tweak_folder = ASSETS_PATH.join(tweak_dir);
865 let test_path = tweak_folder.join(format!("{}.ron", tweak_name));
866
867 let _file_guard = FileGuard::hold(&test_path);
868
869 test(&test_path);
870 }
871
872 #[test]
873 fn test_create_tweak() {
874 let tweak_path = &["tweak_create_test", "tweak"];
875
876 run_with_path(tweak_path, |test_path| {
877 let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
878 assert_eq!(x, 5);
879 assert!(test_path.is_file());
880 let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
882 assert_eq!(x, 5);
883 });
884 }
885
886 #[test]
887 fn test_create_tweak_deep() {
888 let tweak_path = &["so_much", "deep_test", "tweak_create_test", "tweak"];
889
890 run_with_path(tweak_path, |test_path| {
891 let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
892 assert_eq!(x, 5);
893 assert!(test_path.is_file());
894 let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
896 assert_eq!(x, 5);
897 });
898 }
899
900 #[test]
901 fn test_create_but_prioritize_loaded() {
902 let tweak_path = &["tweak_create_and_prioritize_test", "tweak"];
903
904 run_with_path(tweak_path, |test_path| {
905 let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
906 assert_eq!(x, 5);
907 assert!(test_path.is_file());
908
909 fs::write(test_path, b"10").expect("failed to write to the file");
912 let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
913 assert_eq!(x, 10);
914 });
915 }
916 }
917}