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        reloading_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            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        // Avoid using `unwrap_or_else` to avoid breaking `#[track_caller]`
150        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    /// Load combined table without hot-reload support, panic on error
159    #[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
181/// Extension to AssetCache to combine Ron files from filesystem and plugins
182pub 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
207/// Loads directory and all files in it.
208///
209/// "rec" stands for "recursively"
210///
211/// Note, this only gets the ids of assets, they are not actually loaded. The
212/// returned handle can be used to iterate over the IDs or to iterate over
213/// assets trying to load them.
214///
215/// # Errors
216/// An error is returned if the given id does not match a valid readable
217/// directory.
218///
219/// When loading a directory recursively, directories that can't be read are
220/// ignored.
221pub 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/// This wrapper combines several RON files from multiple sources
293#[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    // the passed cache registers with hot reloading
303    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// fallback
314#[cfg(not(feature = "plugins"))]
315pub use assets_manager::asset::Ron as MultiRon;
316
317/// Return path to repository root by searching 10 directories back
318pub fn find_root() -> Option<PathBuf> {
319    std::env::current_dir().map_or(None, |path| {
320        // If we are in the root, push path
321        if path.join(".git").exists() {
322            return Some(path);
323        }
324        // Search .git directory in parent directories
325        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    /// Lazy static to find and cache where the asset directory is.
336    /// Cases we need to account for:
337    /// 1. Running through airshipper (`assets` next to binary)
338    /// 2. Install with package manager and run (assets probably in `/usr/share/veloren/assets` while binary in `/usr/bin/`)
339    /// 3. Download & hopefully extract zip (`assets` next to binary)
340    /// 4. Running through cargo (`assets` in workspace root but not always in cwd in case you `cd voxygen && cargo r`)
341    /// 5. Running executable in the target dir (`assets` in workspace)
342    /// 6. Running tests (`assets` in workspace root)
343    pub static ref ASSETS_PATH: PathBuf = {
344        let mut paths = Vec::new();
345
346        // Note: Ordering matters here!
347
348        // 1. VELOREN_ASSETS environment variable
349        if let Ok(var) = std::env::var("VELOREN_ASSETS") {
350            paths.push(var.into());
351        }
352
353        // 2. Executable path
354        if let Ok(mut path) = std::env::current_exe() {
355            path.pop();
356            paths.push(path);
357        }
358
359        // 3. Root of the repository
360        if let Some(path) = find_root() {
361            paths.push(path);
362        }
363
364        // 4. System paths
365        #[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                // Fallback
377                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        // Loading the asset cache will automatically cause the canary to load
416        let _ = *super::ASSETS;
417    }
418
419    /// Fail unless all `.ron` asset files successfully parse to `ron::Value`.
420    #[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    //! Set of functions and macros for easy tweaking values
448    //! using our asset cache machinery.
449    //!
450    //! Because of how macros works, you will not find
451    //! [tweak] and [tweak_from] macros in this module,
452    //! import it from [assets](super) crate directly.
453    //!
454    //! Will hot-reload (if corresponded feature is enabled).
455    // TODO: don't use the same ASSETS_PATH as game uses?
456    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    /// Specifier to use with tweak functions in this module
462    ///
463    /// `Tweak("test")` will be interpreted as `<assets_dir>/tweak/test.ron`.
464    ///
465    /// `Asset(&["path", "to", "file"])` will be interpreted as
466    /// `<assets_dir>/path/to/file.ron`
467    pub enum Specifier<'a> {
468        Tweak(&'a str),
469        Asset(&'a [&'a str]),
470    }
471
472    /// Read value from file, will panic if file doesn't exist.
473    ///
474    /// If you don't have a file or its content is invalid,
475    /// this function will panic.
476    /// If you want to have some default content,
477    /// read documentation for [tweak_expect_or_create] for more.
478    ///
479    /// # Examples:
480    /// How not to use.
481    /// ```should_panic
482    /// use veloren_common_assets::asset_tweak::{Specifier, tweak_expect};
483    ///
484    /// // will panic if you don't have a file
485    /// let specifier = Specifier::Asset(&["no_way_we_have_this_directory", "x"]);
486    /// let x: i32 = tweak_expect(specifier);
487    /// ```
488    ///
489    /// How to use.
490    /// ```
491    /// use std::fs;
492    /// use veloren_common_assets::{
493    ///     ASSETS_PATH,
494    ///     asset_tweak::{Specifier, tweak_expect},
495    /// };
496    ///
497    /// // you need to create file first
498    /// let tweak_path = ASSETS_PATH.join("tweak/year.ron");
499    /// // note lack of parentheses
500    /// fs::write(&tweak_path, b"10");
501    ///
502    /// let y: i32 = tweak_expect(Specifier::Tweak("year"));
503    /// assert_eq!(y, 10);
504    ///
505    /// // Specifier::Tweak is just a shorthand
506    /// // for Specifier::Asset(&["tweak", ..])
507    /// let y1: i32 = tweak_expect(Specifier::Asset(&["tweak", "year"]));
508    /// assert_eq!(y1, 10);
509    ///
510    /// // you may want to remove this file later
511    /// fs::remove_file(tweak_path);
512    /// ```
513    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    // Helper function to create new file to tweak.
528    //
529    // The file will be filled with passed value
530    // returns passed value.
531    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    // Helper function to get directory and file from asset list.
548    //
549    // Converts ["path", "to", "file"] to (String("path/to"), "file")
550    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    /// Read a value from asset, creating file if not exists.
558    ///
559    /// If file exists will read a value from such file
560    /// using [tweak_expect].
561    ///
562    /// File should look like that (note the lack of parentheses).
563    /// ```text
564    /// assets/tweak/x.ron
565    /// 5
566    /// ```
567    ///
568    /// # Example:
569    /// Tweaking integer value
570    /// ```
571    /// use veloren_common_assets::{
572    ///     ASSETS_PATH,
573    ///     asset_tweak::{Specifier, tweak_expect_or_create},
574    /// };
575    ///
576    /// // first time it will create the file
577    /// let x: i32 = tweak_expect_or_create(Specifier::Tweak("stars"), 5);
578    /// let file_path = ASSETS_PATH.join("tweak/stars.ron");
579    /// assert!(file_path.is_file());
580    /// assert_eq!(x, 5);
581    ///
582    /// // next time it will read value from file
583    /// // whatever you will pass as default
584    /// let x1: i32 = tweak_expect_or_create(Specifier::Tweak("stars"), 42);
585    /// assert_eq!(x1, 5);
586    ///
587    /// // you may want to remove this file later
588    /// std::fs::remove_file(file_path);
589    /// ```
590    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    /// Convenient macro to quickly tweak value.
610    ///
611    /// Will use [Specifier]`::Tweak` specifier and call
612    /// [tweak_expect] if passed only name
613    /// or [tweak_expect_or_create] if default is passed.
614    ///
615    /// # Examples:
616    /// ```
617    /// // note that you need to export it from `assets` crate,
618    /// // not from `assets::asset_tweak`
619    /// use veloren_common_assets::{ASSETS_PATH, tweak};
620    ///
621    /// // you need to create file first
622    /// let own_path = ASSETS_PATH.join("tweak/grizelda.ron");
623    /// // note lack of parentheses
624    /// std::fs::write(&own_path, b"10");
625    ///
626    /// let z: i32 = tweak!("grizelda");
627    /// assert_eq!(z, 10);
628    ///
629    /// // voila, you don't need to care about creating file first
630    /// let p: i32 = tweak!("peter", 8);
631    ///
632    /// let created_path = ASSETS_PATH.join("tweak/peter.ron");
633    /// assert!(created_path.is_file());
634    /// assert_eq!(p, 8);
635    ///
636    /// // will use default value only first time
637    /// // if file exists, will load from this file
638    /// let p: i32 = tweak!("peter", 50);
639    /// assert_eq!(p, 8);
640    ///
641    /// // you may want to remove this file later
642    /// std::fs::remove_file(own_path);
643    /// std::fs::remove_file(created_path);
644    /// ```
645    #[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    /// Convenient macro to quickly tweak value from some existing path.
661    ///
662    /// Will use [Specifier]`::Asset` specifier and call
663    /// [tweak_expect] if passed only name
664    /// or [tweak_expect_or_create] if default is passed.
665    ///
666    /// The main use case is when you have some object
667    /// which needs constant tuning of values, but you can't afford
668    /// loading a file.
669    /// So you can use tweak_from! and then just copy values from asset
670    /// to your object.
671    ///
672    /// # Examples:
673    /// ```no_run
674    /// // note that you need to export it from `assets` crate,
675    /// // not from `assets::asset_tweak`
676    /// use serde::{Deserialize, Serialize};
677    /// use veloren_common_assets::{ASSETS_PATH, tweak_from};
678    ///
679    /// #[derive(Clone, PartialEq, Deserialize, Serialize)]
680    /// struct Data {
681    ///     x: i32,
682    ///     y: i32,
683    /// }
684    ///
685    /// let default = Data { x: 5, y: 7 };
686    /// let data: Data = tweak_from!(&["common", "body", "dimensions"], default);
687    /// ```
688    #[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        // helper function to create environment with needed directory and file
771        // and responsible for cleaning
772        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            // partial eq and debug because of assert_eq in this test
836            #[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                // Recheck it loads back correctly
884                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                // Recheck it loads back correctly
898                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                // Recheck it loads back
913                // with content as priority
914                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}