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 reloading_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 Self::load_and_combine(ASSETS.non_reloading_cache(), specifier)
137 }
138 #[cfg(not(feature = "plugins"))]
139 {
140 Self::load(specifier)
141 }
142 }
143
144 #[track_caller]
145 fn load_expect_combined(
146 reloading_cache: &'static AssetCache,
147 specifier: &str,
148 ) -> AssetHandle<Self> {
149 match Self::load_and_combine(reloading_cache, specifier) {
151 Ok(handle) => handle,
152 Err(err) => {
153 panic!("Failed loading essential combined asset: {specifier} (error={err:?})")
154 },
155 }
156 }
157
158 #[track_caller]
160 fn load_expect_combined_static(specifier: &str) -> AssetHandle<Self> {
161 #[cfg(feature = "plugins")]
162 {
163 Self::load_expect_combined(ASSETS.non_reloading_cache(), specifier)
164 }
165 #[cfg(not(feature = "plugins"))]
166 {
167 Self::load_expect(specifier)
168 }
169 }
170}
171
172impl<T: Asset + Concatenate> AssetCombined for T {
173 fn load_and_combine(
174 reloading_cache: &'static AssetCache,
175 specifier: &str,
176 ) -> Result<AssetHandle<Self>, Error> {
177 reloading_cache.load_and_combine(specifier)
178 }
179}
180
181pub trait CacheCombined {
183 fn load_and_combine<A: Asset + Concatenate>(
184 &self,
185 id: &str,
186 ) -> Result<&assets_manager::Handle<A>, Error>;
187}
188
189impl CacheCombined for AssetCache {
190 fn load_and_combine<A: Asset + Concatenate>(
191 &self,
192 specifier: &str,
193 ) -> Result<&assets_manager::Handle<A>, Error> {
194 #[cfg(feature = "plugins")]
195 {
196 tracing::info!("combine {specifier}");
197 let data: Result<A, _> = ASSETS.combine(self, |cache| cache.load_owned::<A>(specifier));
198 data.map(|data| self.get_or_insert(specifier, data))
199 }
200 #[cfg(not(feature = "plugins"))]
201 {
202 self.load(specifier)
203 }
204 }
205}
206
207pub fn load_rec_dir<T: DirLoadable + Asset>(specifier: &str) -> Result<AssetDirHandle<T>, Error> {
222 let specifier = specifier.strip_suffix(".*").unwrap_or(specifier);
223 ASSETS.load_rec_dir(specifier)
224}
225
226pub struct Image(pub Arc<DynamicImage>);
227
228impl Image {
229 pub fn to_image(&self) -> Arc<DynamicImage> { Arc::clone(&self.0) }
230}
231
232impl FileAsset for Image {
233 const EXTENSIONS: &'static [&'static str] = &["png", "jpg"];
234
235 fn from_bytes(bytes: Cow<[u8]>) -> Result<Self, BoxedError> {
236 let image = image::load_from_memory(&bytes)?;
237 Ok(Image(Arc::new(image)))
238 }
239}
240
241pub struct DotVox(pub dot_vox::DotVoxData);
242
243impl FileAsset for DotVox {
244 const EXTENSION: &'static str = "vox";
245
246 fn from_bytes(bytes: Cow<[u8]>) -> Result<Self, BoxedError> {
247 let data = dot_vox::load_bytes(&bytes).map_err(|err| err.to_owned())?;
248 Ok(DotVox(data))
249 }
250}
251
252pub struct Obj(pub wavefront::Obj);
253
254impl FileAsset for Obj {
255 const EXTENSION: &'static str = "obj";
256
257 fn from_bytes(bytes: Cow<[u8]>) -> Result<Self, BoxedError> {
258 let data = wavefront::Obj::from_reader(&*bytes)?;
259 Ok(Obj(data))
260 }
261}
262
263pub trait Concatenate {
264 fn concatenate(self, b: Self) -> Self;
265}
266
267impl<K: Eq + Hash, V, S: BuildHasher> Concatenate for HashMap<K, V, S> {
268 fn concatenate(mut self, b: Self) -> Self {
269 self.extend(b);
270 self
271 }
272}
273
274impl<V> Concatenate for Vec<V> {
275 fn concatenate(mut self, b: Self) -> Self {
276 self.extend(b);
277 self
278 }
279}
280
281impl<K: Eq + Hash, V, S: BuildHasher> Concatenate for hashbrown::HashMap<K, V, S> {
282 fn concatenate(mut self, b: Self) -> Self {
283 self.extend(b);
284 self
285 }
286}
287
288impl<T: Concatenate> Concatenate for Ron<T> {
289 fn concatenate(self, b: Self) -> Self { Self(self.into_inner().concatenate(b.into_inner())) }
290}
291
292#[cfg(feature = "plugins")]
294#[derive(Clone)]
295pub struct MultiRon<T>(pub T);
296
297#[cfg(feature = "plugins")]
298impl<T> Asset for MultiRon<T>
299where
300 T: for<'de> serde::Deserialize<'de> + Send + Sync + 'static + Concatenate,
301{
302 fn load(cache: &AssetCache, id: &SharedString) -> Result<Self, BoxedError> {
304 ASSETS
305 .combine(cache, |cache| {
306 cache.load_owned::<Ron<T>>(id).map(|ron| ron.into_inner())
307 })
308 .map(MultiRon)
309 .map_err(Into::<BoxedError>::into)
310 }
311}
312
313#[cfg(not(feature = "plugins"))]
315pub use assets_manager::asset::Ron as MultiRon;
316
317pub fn find_root() -> Option<PathBuf> {
319 std::env::current_dir().map_or(None, |path| {
320 if path.join(".git").exists() {
322 return Some(path);
323 }
324 for ancestor in path.ancestors().take(10) {
326 if ancestor.join(".git").exists() {
327 return Some(ancestor.to_path_buf());
328 }
329 }
330 None
331 })
332}
333
334lazy_static! {
335 pub static ref ASSETS_PATH: PathBuf = {
344 let mut paths = Vec::new();
345
346 if let Ok(var) = std::env::var("VELOREN_ASSETS") {
350 paths.push(var.into());
351 }
352
353 if let Ok(mut path) = std::env::current_exe() {
355 path.pop();
356 paths.push(path);
357 }
358
359 if let Some(path) = find_root() {
361 paths.push(path);
362 }
363
364 #[cfg(all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android")))]
366 {
367 if let Ok(result) = std::env::var("XDG_DATA_HOME") {
368 paths.push(format!("{}/veloren/", result).into());
369 } else if let Ok(result) = std::env::var("HOME") {
370 paths.push(format!("{}/.local/share/veloren/", result).into());
371 }
372
373 if let Ok(result) = std::env::var("XDG_DATA_DIRS") {
374 result.split(':').for_each(|x| paths.push(format!("{}/veloren/", x).into()));
375 } else {
376 let fallback_paths = vec!["/usr/local/share", "/usr/share"];
378 for fallback_path in fallback_paths {
379 paths.push(format!("{}/veloren/", fallback_path).into());
380 }
381 }
382 }
383
384 tracing::trace!("Possible asset locations paths={:?}", paths);
385
386 for mut path in paths.clone() {
387 if !path.ends_with("assets") {
388 path = path.join("assets");
389 }
390
391 if path.is_dir() {
392 tracing::info!("Assets found path={}", path.display());
393 return path;
394 }
395 }
396
397 panic!(
398 "Asset directory not found. In attempting to find it, we searched:\n{})",
399 paths.iter().fold(String::new(), |mut a, path| {
400 a += &path.to_string_lossy();
401 a += "\n";
402 a
403 }),
404 );
405 };
406}
407
408#[cfg(test)]
409mod tests {
410 use std::{ffi::OsStr, fs::File};
411 use walkdir::WalkDir;
412
413 #[test]
414 fn load_canary() {
415 let _ = *super::ASSETS;
417 }
418
419 #[test]
421 fn parse_all_ron_files_to_value() {
422 let ext = OsStr::new("ron");
423 WalkDir::new(crate::ASSETS_PATH.as_path())
424 .into_iter()
425 .map(|ent| {
426 ent.expect("Failed to walk over asset directory")
427 .into_path()
428 })
429 .filter(|path| path.is_file())
430 .filter(|path| {
431 path.extension()
432 .is_some_and(|e| ext == e.to_ascii_lowercase())
433 })
434 .for_each(|path| {
435 let file = File::open(&path).expect("Failed to open the file");
436 if let Err(err) = ron::de::from_reader::<_, ron::Value>(file) {
437 println!("{:?}", path);
438 println!("{:#?}", err);
439 panic!("Parse failed");
440 }
441 });
442 }
443}
444
445#[cfg(feature = "asset_tweak")]
446pub mod asset_tweak {
447 use super::{ASSETS_PATH, AssetExt, Ron};
457 use ron::{options::Options, ser::PrettyConfig};
458 use serde::{Serialize, de::DeserializeOwned};
459 use std::{fs, path::Path};
460
461 pub enum Specifier<'a> {
468 Tweak(&'a str),
469 Asset(&'a [&'a str]),
470 }
471
472 pub fn tweak_expect<T>(specifier: Specifier) -> T
514 where
515 T: Clone + Sized + Send + Sync + 'static + DeserializeOwned,
516 {
517 let asset_specifier = match specifier {
518 Specifier::Tweak(specifier) => format!("tweak.{}", specifier),
519 Specifier::Asset(path) => path.join("."),
520 };
521 let handle = <Ron<T> as AssetExt>::load_expect(&asset_specifier);
522 let Ron(value) = handle.cloned();
523
524 value
525 }
526
527 fn create_new<T>(tweak_dir: &Path, filename: &str, value: T) -> T
532 where
533 T: Sized + Send + Sync + 'static + DeserializeOwned + Serialize,
534 {
535 fs::create_dir_all(tweak_dir).expect("failed to create directory for tweak files");
536 let f = fs::File::create(tweak_dir.join(filename)).unwrap_or_else(|error| {
537 panic!("failed to create file {:?}. Error: {:?}", filename, error)
538 });
539 let tweaker = Ron(&value);
540 if let Err(e) = Options::default().to_io_writer_pretty(f, &tweaker, PrettyConfig::new()) {
541 panic!("failed to write to file {:?}. Error: {:?}", filename, e);
542 }
543
544 value
545 }
546
547 fn directory_and_name<'a>(path: &'a [&'a str]) -> (String, &'a str) {
551 let (file, path) = path.split_last().expect("empty asset list");
552 let directory = path.join("/");
553
554 (directory, file)
555 }
556
557 pub fn tweak_expect_or_create<T>(specifier: Specifier, value: T) -> T
591 where
592 T: Clone + Sized + Send + Sync + 'static + DeserializeOwned + Serialize,
593 {
594 let (dir, filename) = match specifier {
595 Specifier::Tweak(name) => (ASSETS_PATH.join("tweak"), format!("{}.ron", name)),
596 Specifier::Asset(list) => {
597 let (directory, name) = directory_and_name(list);
598 (ASSETS_PATH.join(directory), format!("{}.ron", name))
599 },
600 };
601
602 if Path::new(&dir.join(&filename)).is_file() {
603 tweak_expect(specifier)
604 } else {
605 create_new(&dir, &filename, value)
606 }
607 }
608
609 #[macro_export]
646 macro_rules! tweak {
647 ($name:literal) => {{
648 use $crate::asset_tweak::{Specifier::Tweak, tweak_expect};
649
650 tweak_expect(Tweak($name))
651 }};
652
653 ($name:literal, $default:expr) => {{
654 use $crate::asset_tweak::{Specifier::Tweak, tweak_expect_or_create};
655
656 tweak_expect_or_create(Tweak($name), $default)
657 }};
658 }
659
660 #[macro_export]
689 macro_rules! tweak_from {
690 ($path:expr) => {{
691 use $crate::asset_tweak::{Specifier::Asset, tweak_expect};
692
693 tweak_expect(Asset($path))
694 }};
695
696 ($path:expr, $default:expr) => {{
697 use $crate::asset_tweak::{Specifier::Asset, tweak_expect_or_create};
698
699 tweak_expect_or_create(Asset($path), $default)
700 }};
701 }
702
703 #[cfg(test)]
704 mod tests {
705 use super::*;
706 use serde::Deserialize;
707 use std::{
708 convert::AsRef,
709 fmt::Debug,
710 fs::{self, File},
711 io::Write,
712 path::Path,
713 };
714
715 struct DirectoryGuard<P>
716 where
717 P: AsRef<Path>,
718 {
719 dir: P,
720 }
721
722 impl<P> DirectoryGuard<P>
723 where
724 P: AsRef<Path>,
725 {
726 fn create(dir: P) -> Self {
727 fs::create_dir_all(&dir).expect("failed to create directory");
728 Self { dir }
729 }
730 }
731
732 impl<P> Drop for DirectoryGuard<P>
733 where
734 P: AsRef<Path>,
735 {
736 fn drop(&mut self) { fs::remove_dir(&self.dir).expect("failed to remove directory"); }
737 }
738
739 struct FileGuard<P>
740 where
741 P: AsRef<Path> + Debug,
742 {
743 file: P,
744 }
745
746 impl<P> FileGuard<P>
747 where
748 P: AsRef<Path> + Debug,
749 {
750 fn create(file: P) -> (Self, File) {
751 let f = File::create(&file)
752 .unwrap_or_else(|_| panic!("failed to create file {:?}", &file));
753 (Self { file }, f)
754 }
755
756 fn hold(file: P) -> Self { Self { file } }
757 }
758
759 impl<P> Drop for FileGuard<P>
760 where
761 P: AsRef<Path> + Debug,
762 {
763 fn drop(&mut self) {
764 fs::remove_file(&self.file).unwrap_or_else(|e| {
765 panic!("failed to remove file {:?}. Error: {:?}", &self.file, e)
766 });
767 }
768 }
769
770 fn run_with_file(tweak_path: &[&str], test: impl Fn(&mut File)) {
773 let (tweak_dir, tweak_name) = directory_and_name(tweak_path);
774 let tweak_folder = ASSETS_PATH.join(tweak_dir);
775 let tweak_file = tweak_folder.join(format!("{}.ron", tweak_name));
776
777 let _dir_guard = DirectoryGuard::create(tweak_folder);
778 let (_file_guard, mut file) = FileGuard::create(tweak_file);
779
780 test(&mut file);
781 }
782
783 #[test]
784 fn test_tweaked_int() {
785 let tweak_path = &["tweak_test_int", "tweak"];
786
787 run_with_file(tweak_path, |file| {
788 file.write_all(b"5").expect("failed to write to the file");
789 let x: i32 = tweak_expect(Specifier::Asset(tweak_path));
790 assert_eq!(x, 5);
791 });
792 }
793
794 #[test]
795 fn test_tweaked_string() {
796 let tweak_path = &["tweak_test_string", "tweak"];
797
798 run_with_file(tweak_path, |file| {
799 file.write_all(br#""Hello Zest""#)
800 .expect("failed to write to the file");
801
802 let x: String = tweak_expect(Specifier::Asset(tweak_path));
803 assert_eq!(x, "Hello Zest".to_owned());
804 });
805 }
806
807 #[test]
808 fn test_tweaked_hashmap() {
809 type Map = std::collections::HashMap<String, i32>;
810
811 let tweak_path = &["tweak_test_map", "tweak"];
812
813 run_with_file(tweak_path, |file| {
814 file.write_all(
815 br#"
816 {
817 "wow": 4,
818 "such": 5,
819 }
820 "#,
821 )
822 .expect("failed to write to the file");
823
824 let x: Map = tweak_expect(Specifier::Asset(tweak_path));
825
826 let mut map = Map::new();
827 map.insert("wow".to_owned(), 4);
828 map.insert("such".to_owned(), 5);
829 assert_eq!(x, map);
830 });
831 }
832
833 #[test]
834 fn test_tweaked_with_macro_struct() {
835 #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
837 struct Wow {
838 such: i32,
839 field: f32,
840 }
841
842 let tweak_path = &["tweak_test_struct", "tweak"];
843
844 run_with_file(tweak_path, |file| {
845 file.write_all(
846 br"
847 (
848 such: 5,
849 field: 35.752346,
850 )
851 ",
852 )
853 .expect("failed to write to the file");
854
855 let x: Wow = crate::tweak_from!(tweak_path);
856 let expected = Wow {
857 such: 5,
858 field: 35.752_346,
859 };
860 assert_eq!(x, expected);
861 });
862 }
863
864 fn run_with_path(tweak_path: &[&str], test: impl Fn(&Path)) {
865 let (tweak_dir, tweak_name) = directory_and_name(tweak_path);
866
867 let tweak_folder = ASSETS_PATH.join(tweak_dir);
868 let test_path = tweak_folder.join(format!("{}.ron", tweak_name));
869
870 let _file_guard = FileGuard::hold(&test_path);
871
872 test(&test_path);
873 }
874
875 #[test]
876 fn test_create_tweak() {
877 let tweak_path = &["tweak_create_test", "tweak"];
878
879 run_with_path(tweak_path, |test_path| {
880 let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
881 assert_eq!(x, 5);
882 assert!(test_path.is_file());
883 let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
885 assert_eq!(x, 5);
886 });
887 }
888
889 #[test]
890 fn test_create_tweak_deep() {
891 let tweak_path = &["so_much", "deep_test", "tweak_create_test", "tweak"];
892
893 run_with_path(tweak_path, |test_path| {
894 let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
895 assert_eq!(x, 5);
896 assert!(test_path.is_file());
897 let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
899 assert_eq!(x, 5);
900 });
901 }
902
903 #[test]
904 fn test_create_but_prioritize_loaded() {
905 let tweak_path = &["tweak_create_and_prioritize_test", "tweak"];
906
907 run_with_path(tweak_path, |test_path| {
908 let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
909 assert_eq!(x, 5);
910 assert!(test_path.is_file());
911
912 fs::write(test_path, b"10").expect("failed to write to the file");
915 let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
916 assert_eq!(x, 10);
917 });
918 }
919 }
920}