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}
441
442#[cfg(feature = "asset_tweak")]
443pub mod asset_tweak {
444    //! Set of functions and macros for easy tweaking values
445    //! using our asset cache machinery.
446    //!
447    //! Because of how macros works, you will not find
448    //! [tweak] and [tweak_from] macros in this module,
449    //! import it from [assets](super) crate directly.
450    //!
451    //! Will hot-reload (if corresponded feature is enabled).
452    // TODO: don't use the same ASSETS_PATH as game uses?
453    use super::{ASSETS_PATH, AssetExt, Ron};
454    use ron::{options::Options, ser::PrettyConfig};
455    use serde::{Serialize, de::DeserializeOwned};
456    use std::{fs, path::Path};
457
458    /// Specifier to use with tweak functions in this module
459    ///
460    /// `Tweak("test")` will be interpreted as `<assets_dir>/tweak/test.ron`.
461    ///
462    /// `Asset(&["path", "to", "file"])` will be interpreted as
463    /// `<assets_dir>/path/to/file.ron`
464    pub enum Specifier<'a> {
465        Tweak(&'a str),
466        Asset(&'a [&'a str]),
467    }
468
469    /// Read value from file, will panic if file doesn't exist.
470    ///
471    /// If you don't have a file or its content is invalid,
472    /// this function will panic.
473    /// If you want to have some default content,
474    /// read documentation for [tweak_expect_or_create] for more.
475    ///
476    /// # Examples:
477    /// How not to use.
478    /// ```should_panic
479    /// use veloren_common_assets::asset_tweak::{Specifier, tweak_expect};
480    ///
481    /// // will panic if you don't have a file
482    /// let specifier = Specifier::Asset(&["no_way_we_have_this_directory", "x"]);
483    /// let x: i32 = tweak_expect(specifier);
484    /// ```
485    ///
486    /// How to use.
487    /// ```
488    /// use std::fs;
489    /// use veloren_common_assets::{
490    ///     ASSETS_PATH,
491    ///     asset_tweak::{Specifier, tweak_expect},
492    /// };
493    ///
494    /// // you need to create file first
495    /// let tweak_path = ASSETS_PATH.join("tweak/year.ron");
496    /// // note lack of parentheses
497    /// fs::write(&tweak_path, b"10");
498    ///
499    /// let y: i32 = tweak_expect(Specifier::Tweak("year"));
500    /// assert_eq!(y, 10);
501    ///
502    /// // Specifier::Tweak is just a shorthand
503    /// // for Specifier::Asset(&["tweak", ..])
504    /// let y1: i32 = tweak_expect(Specifier::Asset(&["tweak", "year"]));
505    /// assert_eq!(y1, 10);
506    ///
507    /// // you may want to remove this file later
508    /// fs::remove_file(tweak_path);
509    /// ```
510    pub fn tweak_expect<T>(specifier: Specifier) -> T
511    where
512        T: Clone + Sized + Send + Sync + 'static + DeserializeOwned,
513    {
514        let asset_specifier = match specifier {
515            Specifier::Tweak(specifier) => format!("tweak.{}", specifier),
516            Specifier::Asset(path) => path.join("."),
517        };
518        let handle = <Ron<T> as AssetExt>::load_expect(&asset_specifier);
519        let Ron(value) = handle.cloned();
520
521        value
522    }
523
524    // Helper function to create new file to tweak.
525    //
526    // The file will be filled with passed value
527    // returns passed value.
528    fn create_new<T>(tweak_dir: &Path, filename: &str, value: T) -> T
529    where
530        T: Sized + Send + Sync + 'static + DeserializeOwned + Serialize,
531    {
532        fs::create_dir_all(tweak_dir).expect("failed to create directory for tweak files");
533        let f = fs::File::create(tweak_dir.join(filename)).unwrap_or_else(|error| {
534            panic!("failed to create file {:?}. Error: {:?}", filename, error)
535        });
536        let tweaker = Ron(&value);
537        if let Err(e) = Options::default().to_io_writer_pretty(f, &tweaker, PrettyConfig::new()) {
538            panic!("failed to write to file {:?}. Error: {:?}", filename, e);
539        }
540
541        value
542    }
543
544    // Helper function to get directory and file from asset list.
545    //
546    // Converts ["path", "to", "file"] to (String("path/to"), "file")
547    fn directory_and_name<'a>(path: &'a [&'a str]) -> (String, &'a str) {
548        let (file, path) = path.split_last().expect("empty asset list");
549        let directory = path.join("/");
550
551        (directory, file)
552    }
553
554    /// Read a value from asset, creating file if not exists.
555    ///
556    /// If file exists will read a value from such file
557    /// using [tweak_expect].
558    ///
559    /// File should look like that (note the lack of parentheses).
560    /// ```text
561    /// assets/tweak/x.ron
562    /// 5
563    /// ```
564    ///
565    /// # Example:
566    /// Tweaking integer value
567    /// ```
568    /// use veloren_common_assets::{
569    ///     ASSETS_PATH,
570    ///     asset_tweak::{Specifier, tweak_expect_or_create},
571    /// };
572    ///
573    /// // first time it will create the file
574    /// let x: i32 = tweak_expect_or_create(Specifier::Tweak("stars"), 5);
575    /// let file_path = ASSETS_PATH.join("tweak/stars.ron");
576    /// assert!(file_path.is_file());
577    /// assert_eq!(x, 5);
578    ///
579    /// // next time it will read value from file
580    /// // whatever you will pass as default
581    /// let x1: i32 = tweak_expect_or_create(Specifier::Tweak("stars"), 42);
582    /// assert_eq!(x1, 5);
583    ///
584    /// // you may want to remove this file later
585    /// std::fs::remove_file(file_path);
586    /// ```
587    pub fn tweak_expect_or_create<T>(specifier: Specifier, value: T) -> T
588    where
589        T: Clone + Sized + Send + Sync + 'static + DeserializeOwned + Serialize,
590    {
591        let (dir, filename) = match specifier {
592            Specifier::Tweak(name) => (ASSETS_PATH.join("tweak"), format!("{}.ron", name)),
593            Specifier::Asset(list) => {
594                let (directory, name) = directory_and_name(list);
595                (ASSETS_PATH.join(directory), format!("{}.ron", name))
596            },
597        };
598
599        if Path::new(&dir.join(&filename)).is_file() {
600            tweak_expect(specifier)
601        } else {
602            create_new(&dir, &filename, value)
603        }
604    }
605
606    /// Convenient macro to quickly tweak value.
607    ///
608    /// Will use [Specifier]`::Tweak` specifier and call
609    /// [tweak_expect] if passed only name
610    /// or [tweak_expect_or_create] if default is passed.
611    ///
612    /// # Examples:
613    /// ```
614    /// // note that you need to export it from `assets` crate,
615    /// // not from `assets::asset_tweak`
616    /// use veloren_common_assets::{ASSETS_PATH, tweak};
617    ///
618    /// // you need to create file first
619    /// let own_path = ASSETS_PATH.join("tweak/grizelda.ron");
620    /// // note lack of parentheses
621    /// std::fs::write(&own_path, b"10");
622    ///
623    /// let z: i32 = tweak!("grizelda");
624    /// assert_eq!(z, 10);
625    ///
626    /// // voila, you don't need to care about creating file first
627    /// let p: i32 = tweak!("peter", 8);
628    ///
629    /// let created_path = ASSETS_PATH.join("tweak/peter.ron");
630    /// assert!(created_path.is_file());
631    /// assert_eq!(p, 8);
632    ///
633    /// // will use default value only first time
634    /// // if file exists, will load from this file
635    /// let p: i32 = tweak!("peter", 50);
636    /// assert_eq!(p, 8);
637    ///
638    /// // you may want to remove this file later
639    /// std::fs::remove_file(own_path);
640    /// std::fs::remove_file(created_path);
641    /// ```
642    #[macro_export]
643    macro_rules! tweak {
644        ($name:literal) => {{
645            use $crate::asset_tweak::{Specifier::Tweak, tweak_expect};
646
647            tweak_expect(Tweak($name))
648        }};
649
650        ($name:literal, $default:expr) => {{
651            use $crate::asset_tweak::{Specifier::Tweak, tweak_expect_or_create};
652
653            tweak_expect_or_create(Tweak($name), $default)
654        }};
655    }
656
657    /// Convenient macro to quickly tweak value from some existing path.
658    ///
659    /// Will use [Specifier]`::Asset` specifier and call
660    /// [tweak_expect] if passed only name
661    /// or [tweak_expect_or_create] if default is passed.
662    ///
663    /// The main use case is when you have some object
664    /// which needs constant tuning of values, but you can't afford
665    /// loading a file.
666    /// So you can use tweak_from! and then just copy values from asset
667    /// to your object.
668    ///
669    /// # Examples:
670    /// ```no_run
671    /// // note that you need to export it from `assets` crate,
672    /// // not from `assets::asset_tweak`
673    /// use serde::{Deserialize, Serialize};
674    /// use veloren_common_assets::{ASSETS_PATH, tweak_from};
675    ///
676    /// #[derive(Clone, PartialEq, Deserialize, Serialize)]
677    /// struct Data {
678    ///     x: i32,
679    ///     y: i32,
680    /// }
681    ///
682    /// let default = Data { x: 5, y: 7 };
683    /// let data: Data = tweak_from!(&["common", "body", "dimensions"], default);
684    /// ```
685    #[macro_export]
686    macro_rules! tweak_from {
687        ($path:expr) => {{
688            use $crate::asset_tweak::{Specifier::Asset, tweak_expect};
689
690            tweak_expect(Asset($path))
691        }};
692
693        ($path:expr, $default:expr) => {{
694            use $crate::asset_tweak::{Specifier::Asset, tweak_expect_or_create};
695
696            tweak_expect_or_create(Asset($path), $default)
697        }};
698    }
699
700    #[cfg(test)]
701    mod tests {
702        use super::*;
703        use serde::Deserialize;
704        use std::{
705            convert::AsRef,
706            fmt::Debug,
707            fs::{self, File},
708            io::Write,
709            path::Path,
710        };
711
712        struct DirectoryGuard<P>
713        where
714            P: AsRef<Path>,
715        {
716            dir: P,
717        }
718
719        impl<P> DirectoryGuard<P>
720        where
721            P: AsRef<Path>,
722        {
723            fn create(dir: P) -> Self {
724                fs::create_dir_all(&dir).expect("failed to create directory");
725                Self { dir }
726            }
727        }
728
729        impl<P> Drop for DirectoryGuard<P>
730        where
731            P: AsRef<Path>,
732        {
733            fn drop(&mut self) { fs::remove_dir(&self.dir).expect("failed to remove directory"); }
734        }
735
736        struct FileGuard<P>
737        where
738            P: AsRef<Path> + Debug,
739        {
740            file: P,
741        }
742
743        impl<P> FileGuard<P>
744        where
745            P: AsRef<Path> + Debug,
746        {
747            fn create(file: P) -> (Self, File) {
748                let f = File::create(&file)
749                    .unwrap_or_else(|_| panic!("failed to create file {:?}", &file));
750                (Self { file }, f)
751            }
752
753            fn hold(file: P) -> Self { Self { file } }
754        }
755
756        impl<P> Drop for FileGuard<P>
757        where
758            P: AsRef<Path> + Debug,
759        {
760            fn drop(&mut self) {
761                fs::remove_file(&self.file).unwrap_or_else(|e| {
762                    panic!("failed to remove file {:?}. Error: {:?}", &self.file, e)
763                });
764            }
765        }
766
767        // helper function to create environment with needed directory and file
768        // and responsible for cleaning
769        fn run_with_file(tweak_path: &[&str], test: impl Fn(&mut File)) {
770            let (tweak_dir, tweak_name) = directory_and_name(tweak_path);
771            let tweak_folder = ASSETS_PATH.join(tweak_dir);
772            let tweak_file = tweak_folder.join(format!("{}.ron", tweak_name));
773
774            let _dir_guard = DirectoryGuard::create(tweak_folder);
775            let (_file_guard, mut file) = FileGuard::create(tweak_file);
776
777            test(&mut file);
778        }
779
780        #[test]
781        fn test_tweaked_int() {
782            let tweak_path = &["tweak_test_int", "tweak"];
783
784            run_with_file(tweak_path, |file| {
785                file.write_all(b"5").expect("failed to write to the file");
786                let x: i32 = tweak_expect(Specifier::Asset(tweak_path));
787                assert_eq!(x, 5);
788            });
789        }
790
791        #[test]
792        fn test_tweaked_string() {
793            let tweak_path = &["tweak_test_string", "tweak"];
794
795            run_with_file(tweak_path, |file| {
796                file.write_all(br#""Hello Zest""#)
797                    .expect("failed to write to the file");
798
799                let x: String = tweak_expect(Specifier::Asset(tweak_path));
800                assert_eq!(x, "Hello Zest".to_owned());
801            });
802        }
803
804        #[test]
805        fn test_tweaked_hashmap() {
806            type Map = std::collections::HashMap<String, i32>;
807
808            let tweak_path = &["tweak_test_map", "tweak"];
809
810            run_with_file(tweak_path, |file| {
811                file.write_all(
812                    br#"
813                    {
814                        "wow": 4,
815                        "such": 5,
816                    }
817                    "#,
818                )
819                .expect("failed to write to the file");
820
821                let x: Map = tweak_expect(Specifier::Asset(tweak_path));
822
823                let mut map = Map::new();
824                map.insert("wow".to_owned(), 4);
825                map.insert("such".to_owned(), 5);
826                assert_eq!(x, map);
827            });
828        }
829
830        #[test]
831        fn test_tweaked_with_macro_struct() {
832            // partial eq and debug because of assert_eq in this test
833            #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
834            struct Wow {
835                such: i32,
836                field: f32,
837            }
838
839            let tweak_path = &["tweak_test_struct", "tweak"];
840
841            run_with_file(tweak_path, |file| {
842                file.write_all(
843                    br"
844                    (
845                        such: 5,
846                        field: 35.752346,
847                    )
848                    ",
849                )
850                .expect("failed to write to the file");
851
852                let x: Wow = crate::tweak_from!(tweak_path);
853                let expected = Wow {
854                    such: 5,
855                    field: 35.752_346,
856                };
857                assert_eq!(x, expected);
858            });
859        }
860
861        fn run_with_path(tweak_path: &[&str], test: impl Fn(&Path)) {
862            let (tweak_dir, tweak_name) = directory_and_name(tweak_path);
863
864            let tweak_folder = ASSETS_PATH.join(tweak_dir);
865            let test_path = tweak_folder.join(format!("{}.ron", tweak_name));
866
867            let _file_guard = FileGuard::hold(&test_path);
868
869            test(&test_path);
870        }
871
872        #[test]
873        fn test_create_tweak() {
874            let tweak_path = &["tweak_create_test", "tweak"];
875
876            run_with_path(tweak_path, |test_path| {
877                let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
878                assert_eq!(x, 5);
879                assert!(test_path.is_file());
880                // Recheck it loads back correctly
881                let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
882                assert_eq!(x, 5);
883            });
884        }
885
886        #[test]
887        fn test_create_tweak_deep() {
888            let tweak_path = &["so_much", "deep_test", "tweak_create_test", "tweak"];
889
890            run_with_path(tweak_path, |test_path| {
891                let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
892                assert_eq!(x, 5);
893                assert!(test_path.is_file());
894                // Recheck it loads back correctly
895                let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
896                assert_eq!(x, 5);
897            });
898        }
899
900        #[test]
901        fn test_create_but_prioritize_loaded() {
902            let tweak_path = &["tweak_create_and_prioritize_test", "tweak"];
903
904            run_with_path(tweak_path, |test_path| {
905                let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
906                assert_eq!(x, 5);
907                assert!(test_path.is_file());
908
909                // Recheck it loads back
910                // with content as priority
911                fs::write(test_path, b"10").expect("failed to write to the file");
912                let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
913                assert_eq!(x, 10);
914            });
915        }
916    }
917}