veloren_common_assets/
lib.rs

1//#![warn(clippy::pedantic)]
2//! Load assets (images or voxel data) from files
3
4use 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    /// The HashMap where all loaded assets are stored in.
32    static ref ASSETS: plugin_cache::CombinedCache = plugin_cache::CombinedCache::new().unwrap();
33}
34#[cfg(not(feature = "plugins"))]
35lazy_static! {
36    /// The HashMap where all loaded assets are stored in.
37    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// register a new plugin
45#[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
53/// The Asset trait, which is implemented by all structures that have their data
54/// stored in the filesystem.
55pub trait AssetExt: Sized + Send + Sync + 'static {
56    /// Function used to load assets from the filesystem or the cache.
57    /// Example usage:
58    /// ```no_run
59    /// use veloren_common_assets::{AssetExt, Image};
60    ///
61    /// let my_image = Image::load("core.ui.backgrounds.city").unwrap();
62    /// ```
63    fn load(specifier: &str) -> Result<AssetHandle<Self>, Error>;
64
65    /// Function used to load assets from the filesystem or the cache and return
66    /// a clone.
67    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    /// Function used to load essential assets from the filesystem or the cache.
82    /// It will panic if the asset is not found. Example usage:
83    /// ```no_run
84    /// use veloren_common_assets::{AssetExt, Image};
85    ///
86    /// let my_image = Image::load_expect("core.ui.backgrounds.city");
87    /// ```
88    #[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        // Avoid using `unwrap_or_else` to avoid breaking `#[track_caller]`
101        match Self::load(specifier) {
102            Ok(handle) => handle,
103            Err(err) => expect_failed(err),
104        }
105    }
106
107    /// Function used to load essential assets from the filesystem or the cache
108    /// and return a clone. It will panic if the asset is not found.
109    #[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
122/// Extension to AssetExt to combine Ron files from filesystem and plugins
123pub trait AssetCombined: AssetExt {
124    fn load_and_combine(
125        reloading_cache: AnyCache<'static>,
126        specifier: &str,
127    ) -> Result<AssetHandle<Self>, Error>;
128
129    /// Load combined table without hot-reload support
130    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        // Avoid using `unwrap_or_else` to avoid breaking `#[track_caller]`
147        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    /// 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            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
169/// Extension to AnyCache to combine Ron files from filesystem and plugins
170pub 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
177/// Loads directory and all files in it
178///
179/// # Errors
180/// An error is returned if the given id does not match a valid readable
181/// directory.
182///
183/// When loading a directory recursively, directories that can't be read are
184/// ignored.
185pub 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/// This wrapper combines several RON files from multiple sources
312#[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    // the passed cache registers with hot reloading
322    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// fallback
333#[cfg(not(feature = "plugins"))]
334pub use assets_manager::asset::Ron as MultiRon;
335
336/// Return path to repository root by searching 10 directories back
337pub fn find_root() -> Option<PathBuf> {
338    std::env::current_dir().map_or(None, |path| {
339        // If we are in the root, push path
340        if path.join(".git").exists() {
341            return Some(path);
342        }
343        // Search .git directory in parent directories
344        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    /// Lazy static to find and cache where the asset directory is.
355    /// Cases we need to account for:
356    /// 1. Running through airshipper (`assets` next to binary)
357    /// 2. Install with package manager and run (assets probably in `/usr/share/veloren/assets` while binary in `/usr/bin/`)
358    /// 3. Download & hopefully extract zip (`assets` next to binary)
359    /// 4. Running through cargo (`assets` in workspace root but not always in cwd in case you `cd voxygen && cargo r`)
360    /// 5. Running executable in the target dir (`assets` in workspace)
361    /// 6. Running tests (`assets` in workspace root)
362    pub static ref ASSETS_PATH: PathBuf = {
363        let mut paths = Vec::new();
364
365        // Note: Ordering matters here!
366
367        // 1. VELOREN_ASSETS environment variable
368        if let Ok(var) = std::env::var("VELOREN_ASSETS") {
369            paths.push(var.into());
370        }
371
372        // 2. Executable path
373        if let Ok(mut path) = std::env::current_exe() {
374            path.pop();
375            paths.push(path);
376        }
377
378        // 3. Root of the repository
379        if let Some(path) = find_root() {
380            paths.push(path);
381        }
382
383        // 4. System paths
384        #[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                // Fallback
396                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        // Loading the asset cache will automatically cause the canary to load
435        let _ = *super::ASSETS;
436    }
437
438    /// Fail unless all `.ron` asset files successfully parse to `ron::Value`.
439    #[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    //! Set of functions and macros for easy tweaking values
467    //! using our asset cache machinery.
468    //!
469    //! Because of how macros works, you will not find
470    //! [tweak] and [tweak_from] macros in this module,
471    //! import it from [assets](super) crate directly.
472    //!
473    //! Will hot-reload (if corresponded feature is enabled).
474    // TODO: don't use the same ASSETS_PATH as game uses?
475    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    /// Specifier to use with tweak functions in this module
481    ///
482    /// `Tweak("test")` will be interpreted as `<assets_dir>/tweak/test.ron`.
483    ///
484    /// `Asset(&["path", "to", "file"])` will be interpreted as
485    /// `<assets_dir>/path/to/file.ron`
486    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    /// Read value from file, will panic if file doesn't exist.
504    ///
505    /// If you don't have a file or its content is invalid,
506    /// this function will panic.
507    /// If you want to have some default content,
508    /// read documentation for [tweak_expect_or_create] for more.
509    ///
510    /// # Examples:
511    /// How not to use.
512    /// ```should_panic
513    /// use veloren_common_assets::asset_tweak::{Specifier, tweak_expect};
514    ///
515    /// // will panic if you don't have a file
516    /// let specifier = Specifier::Asset(&["no_way_we_have_this_directory", "x"]);
517    /// let x: i32 = tweak_expect(specifier);
518    /// ```
519    ///
520    /// How to use.
521    /// ```
522    /// use std::fs;
523    /// use veloren_common_assets::{
524    ///     ASSETS_PATH,
525    ///     asset_tweak::{Specifier, tweak_expect},
526    /// };
527    ///
528    /// // you need to create file first
529    /// let tweak_path = ASSETS_PATH.join("tweak/year.ron");
530    /// // note parentheses
531    /// fs::write(&tweak_path, b"(10)");
532    ///
533    /// let y: i32 = tweak_expect(Specifier::Tweak("year"));
534    /// assert_eq!(y, 10);
535    ///
536    /// // Specifier::Tweak is just a shorthand
537    /// // for Specifier::Asset(&["tweak", ..])
538    /// let y1: i32 = tweak_expect(Specifier::Asset(&["tweak", "year"]));
539    /// assert_eq!(y1, 10);
540    ///
541    /// // you may want to remove this file later
542    /// fs::remove_file(tweak_path);
543    /// ```
544    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    // Helper function to create new file to tweak.
559    //
560    // The file will be filled with passed value
561    // returns passed value.
562    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    // Helper function to get directory and file from asset list.
579    //
580    // Converts ["path", "to", "file"] to (String("path/to"), "file")
581    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    /// Read a value from asset, creating file if not exists.
589    ///
590    /// If file exists will read a value from such file
591    /// using [tweak_expect].
592    ///
593    /// File should look like that (note the parentheses).
594    /// ```text
595    /// assets/tweak/x.ron
596    /// (5)
597    /// ```
598    ///
599    /// # Example:
600    /// Tweaking integer value
601    /// ```
602    /// use veloren_common_assets::{
603    ///     ASSETS_PATH,
604    ///     asset_tweak::{Specifier, tweak_expect_or_create},
605    /// };
606    ///
607    /// // first time it will create the file
608    /// let x: i32 = tweak_expect_or_create(Specifier::Tweak("stars"), 5);
609    /// let file_path = ASSETS_PATH.join("tweak/stars.ron");
610    /// assert!(file_path.is_file());
611    /// assert_eq!(x, 5);
612    ///
613    /// // next time it will read value from file
614    /// // whatever you will pass as default
615    /// let x1: i32 = tweak_expect_or_create(Specifier::Tweak("stars"), 42);
616    /// assert_eq!(x1, 5);
617    ///
618    /// // you may want to remove this file later
619    /// std::fs::remove_file(file_path);
620    /// ```
621    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    /// Convenient macro to quickly tweak value.
641    ///
642    /// Will use [Specifier]`::Tweak` specifier and call
643    /// [tweak_expect] if passed only name
644    /// or [tweak_expect_or_create] if default is passed.
645    ///
646    /// # Examples:
647    /// ```
648    /// // note that you need to export it from `assets` crate,
649    /// // not from `assets::asset_tweak`
650    /// use veloren_common_assets::{ASSETS_PATH, tweak};
651    ///
652    /// // you need to create file first
653    /// let own_path = ASSETS_PATH.join("tweak/grizelda.ron");
654    /// // note parentheses
655    /// std::fs::write(&own_path, b"(10)");
656    ///
657    /// let z: i32 = tweak!("grizelda");
658    /// assert_eq!(z, 10);
659    ///
660    /// // voila, you don't need to care about creating file first
661    /// let p: i32 = tweak!("peter", 8);
662    ///
663    /// let created_path = ASSETS_PATH.join("tweak/peter.ron");
664    /// assert!(created_path.is_file());
665    /// assert_eq!(p, 8);
666    ///
667    /// // will use default value only first time
668    /// // if file exists, will load from this file
669    /// let p: i32 = tweak!("peter", 50);
670    /// assert_eq!(p, 8);
671    ///
672    /// // you may want to remove this file later
673    /// std::fs::remove_file(own_path);
674    /// std::fs::remove_file(created_path);
675    /// ```
676    #[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    /// Convenient macro to quickly tweak value from some existing path.
692    ///
693    /// Will use [Specifier]`::Asset` specifier and call
694    /// [tweak_expect] if passed only name
695    /// or [tweak_expect_or_create] if default is passed.
696    ///
697    /// The main use case is when you have some object
698    /// which needs constant tuning of values, but you can't afford
699    /// loading a file.
700    /// So you can use tweak_from! and then just copy values from asset
701    /// to your object.
702    ///
703    /// # Examples:
704    /// ```no_run
705    /// // note that you need to export it from `assets` crate,
706    /// // not from `assets::asset_tweak`
707    /// use serde::{Deserialize, Serialize};
708    /// use veloren_common_assets::{ASSETS_PATH, tweak_from};
709    ///
710    /// #[derive(Clone, PartialEq, Deserialize, Serialize)]
711    /// struct Data {
712    ///     x: i32,
713    ///     y: i32,
714    /// }
715    ///
716    /// let default = Data { x: 5, y: 7 };
717    /// let data: Data = tweak_from!(&["common", "body", "dimensions"], default);
718    /// ```
719    #[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        // helper function to create environment with needed directory and file
801        // and responsible for cleaning
802        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            // partial eq and debug because of assert_eq in this test
866            #[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                // Recheck it loads back correctly
914                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                // Recheck it loads back correctly
928                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                // Recheck it loads back
943                // with content as priority
944                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}