Skip to main content

veloren_common_assets/
lib.rs

1//#![warn(clippy::pedantic)]
2//! Load assets (images or voxel data) from files
3
4use 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    /// The HashMap where all loaded assets are stored in.
28    static ref ASSETS: plugin_cache::CombinedCache = plugin_cache::CombinedCache::new().unwrap();
29}
30#[cfg(not(feature = "plugins"))]
31lazy_static! {
32    /// The HashMap where all loaded assets are stored in.
33    static ref ASSETS: AssetCache =
34            AssetCache::with_source(fs::FileSystem::new().unwrap());
35}
36
37// register a new plugin
38#[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
46/// The Asset trait, which is implemented by all structures that have their data
47/// stored in the filesystem.
48pub trait AssetExt: Sized + Send + Sync + 'static {
49    /// Function used to load assets from the filesystem or the cache.
50    /// Example usage:
51    /// ```no_run
52    /// use veloren_common_assets::{AssetExt, Image};
53    ///
54    /// let my_image = Image::load("core.ui.backgrounds.city").unwrap();
55    /// ```
56    fn load(specifier: &str) -> Result<AssetHandle<Self>, Error>;
57
58    /// Function used to load assets from the filesystem or the cache and return
59    /// a clone.
60    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    /// Function used to load essential assets from the filesystem or the cache.
75    /// It will panic if the asset is not found. Example usage:
76    /// ```no_run
77    /// use veloren_common_assets::{AssetExt, Image};
78    ///
79    /// let my_image = Image::load_expect("core.ui.backgrounds.city");
80    /// ```
81    #[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        // Avoid using `unwrap_or_else` to avoid breaking `#[track_caller]`
94        match Self::load(specifier) {
95            Ok(handle) => handle,
96            Err(err) => expect_failed(err),
97        }
98    }
99
100    /// Function used to load essential assets from the filesystem or the cache
101    /// and return a clone. It will panic if the asset is not found.
102    #[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
125/// Extension to AssetExt to combine Ron files from filesystem and plugins
126pub trait AssetCombined: AssetExt {
127    fn load_and_combine(
128        cache: &'static AssetCache,
129        specifier: &str,
130    ) -> Result<AssetHandle<Self>, Error>;
131
132    /// Load combined table without hot-reload support
133    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        // Avoid using `unwrap_or_else` to avoid breaking `#[track_caller]`
147        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    /// Load combined table without hot-reload support, panic on error
156    #[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
178/// Extension to AssetCache to combine Ron files from filesystem and plugins
179pub 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
204/// Loads directory and all files in it.
205///
206/// "rec" stands for "recursively"
207///
208/// Note, this only gets the ids of assets, they are not actually loaded. The
209/// returned handle can be used to iterate over the IDs or to iterate over
210/// assets trying to load them.
211///
212/// # Errors
213/// An error is returned if the given id does not match a valid readable
214/// directory.
215///
216/// When loading a directory recursively, directories that can't be read are
217/// ignored.
218pub 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/// This wrapper combines several RON files from multiple sources
290#[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    // the passed cache registers with hot reloading
300    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// fallback
311#[cfg(not(feature = "plugins"))]
312pub use assets_manager::asset::Ron as MultiRon;
313
314/// Return path to repository root by searching 10 directories back
315pub fn find_root() -> Option<PathBuf> {
316    std::env::current_dir().map_or(None, |path| {
317        // If we are in the root, push path
318        if path.join(".git").exists() {
319            return Some(path);
320        }
321        // Search .git directory in parent directories
322        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    /// Lazy static to find and cache where the asset directory is.
333    /// Cases we need to account for:
334    /// 1. Running through airshipper (`assets` next to binary)
335    /// 2. Install with package manager and run (assets probably in `/usr/share/veloren/assets` while binary in `/usr/bin/`)
336    /// 3. Download & hopefully extract zip (`assets` next to binary)
337    /// 4. Running through cargo (`assets` in workspace root but not always in cwd in case you `cd voxygen && cargo r`)
338    /// 5. Running executable in the target dir (`assets` in workspace)
339    /// 6. Running tests (`assets` in workspace root)
340    pub static ref ASSETS_PATH: PathBuf = {
341        let mut paths = Vec::new();
342
343        // Note: Ordering matters here!
344
345        // 1. VELOREN_ASSETS environment variable
346        if let Ok(var) = std::env::var("VELOREN_ASSETS") {
347            paths.push(var.into());
348        }
349
350        // 2. Executable path
351        if let Ok(mut path) = std::env::current_exe() {
352            path.pop();
353            paths.push(path);
354        }
355
356        // 3. Root of the repository
357        if let Some(path) = find_root() {
358            paths.push(path);
359        }
360
361        // 4. System paths
362        #[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                // Fallback
374                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        // Loading the asset cache will automatically cause the canary to load
413        let _ = *super::ASSETS;
414    }
415
416    /// Fail unless all `.ron` asset files successfully parse to `ron::Value`.
417    #[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}