2021-07-05 15:43:30 +00:00
|
|
|
//#![warn(clippy::pedantic)]
|
2021-05-04 08:55:40 +00:00
|
|
|
//! Load assets (images or voxel data) from files
|
|
|
|
|
2021-05-07 11:24:37 +00:00
|
|
|
use dot_vox::DotVoxData;
|
|
|
|
use image::DynamicImage;
|
2021-05-04 08:55:40 +00:00
|
|
|
use lazy_static::lazy_static;
|
2021-06-25 16:47:03 +00:00
|
|
|
use std::{borrow::Cow, path::PathBuf, sync::Arc};
|
2021-05-04 08:55:40 +00:00
|
|
|
|
|
|
|
pub use assets_manager::{
|
2021-06-25 16:47:03 +00:00
|
|
|
asset::{DirLoadable, Ron},
|
2021-05-07 11:24:37 +00:00
|
|
|
loader::{
|
|
|
|
self, BincodeLoader, BytesLoader, JsonLoader, LoadFrom, Loader, RonLoader, StringLoader,
|
|
|
|
},
|
2021-06-25 16:47:03 +00:00
|
|
|
source::{self, Source},
|
|
|
|
Asset, AssetCache, BoxedError, Compound, Error, SharedString,
|
2021-05-04 08:55:40 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
lazy_static! {
|
|
|
|
/// The HashMap where all loaded assets are stored in.
|
2021-05-08 16:06:57 +00:00
|
|
|
static ref ASSETS: AssetCache =
|
|
|
|
AssetCache::new(&*ASSETS_PATH).unwrap();
|
2021-05-04 08:55:40 +00:00
|
|
|
}
|
|
|
|
|
2021-06-27 18:34:32 +00:00
|
|
|
#[cfg(feature = "hot-reloading")]
|
2021-05-07 11:24:37 +00:00
|
|
|
pub fn start_hot_reloading() { ASSETS.enhance_hot_reloading(); }
|
|
|
|
|
2021-05-04 08:55:40 +00:00
|
|
|
pub type AssetHandle<T> = assets_manager::Handle<'static, T>;
|
|
|
|
pub type AssetGuard<T> = assets_manager::AssetGuard<'static, T>;
|
2021-06-25 16:47:03 +00:00
|
|
|
pub type AssetDirHandle<T> = assets_manager::DirHandle<'static, T, source::FileSystem>;
|
2021-05-04 08:55:40 +00:00
|
|
|
|
|
|
|
/// The Asset trait, which is implemented by all structures that have their data
|
|
|
|
/// stored in the filesystem.
|
|
|
|
pub trait AssetExt: Sized + Send + Sync + 'static {
|
|
|
|
/// Function used to load assets from the filesystem or the cache.
|
2021-05-07 11:24:37 +00:00
|
|
|
/// Example usage:
|
|
|
|
/// ```no_run
|
2021-05-08 16:06:57 +00:00
|
|
|
/// use veloren_common_assets::{AssetExt, Image};
|
2021-05-07 11:24:37 +00:00
|
|
|
///
|
2021-05-08 16:06:57 +00:00
|
|
|
/// let my_image = Image::load("core.ui.backgrounds.city").unwrap();
|
2021-05-07 11:24:37 +00:00
|
|
|
/// ```
|
2021-05-04 08:55:40 +00:00
|
|
|
fn load(specifier: &str) -> Result<AssetHandle<Self>, Error>;
|
|
|
|
|
|
|
|
/// Function used to load assets from the filesystem or the cache and return
|
|
|
|
/// a clone.
|
|
|
|
fn load_cloned(specifier: &str) -> Result<Self, Error>
|
|
|
|
where
|
|
|
|
Self: Clone,
|
|
|
|
{
|
|
|
|
Self::load(specifier).map(AssetHandle::cloned)
|
|
|
|
}
|
|
|
|
|
2021-06-25 16:47:03 +00:00
|
|
|
fn load_or_insert_with(
|
|
|
|
specifier: &str,
|
|
|
|
default: impl FnOnce(Error) -> Self,
|
|
|
|
) -> AssetHandle<Self> {
|
|
|
|
Self::load(specifier).unwrap_or_else(|err| Self::get_or_insert(specifier, default(err)))
|
|
|
|
}
|
|
|
|
|
2021-05-04 08:55:40 +00:00
|
|
|
/// Function used to load essential assets from the filesystem or the cache.
|
2021-05-07 11:24:37 +00:00
|
|
|
/// It will panic if the asset is not found. Example usage:
|
|
|
|
/// ```no_run
|
2021-05-08 16:06:57 +00:00
|
|
|
/// use veloren_common_assets::{AssetExt, Image};
|
2021-05-07 11:24:37 +00:00
|
|
|
///
|
2021-05-08 16:06:57 +00:00
|
|
|
/// let my_image = Image::load_expect("core.ui.backgrounds.city");
|
2021-05-07 11:24:37 +00:00
|
|
|
/// ```
|
2021-05-04 08:55:40 +00:00
|
|
|
#[track_caller]
|
|
|
|
fn load_expect(specifier: &str) -> AssetHandle<Self> {
|
|
|
|
Self::load(specifier).unwrap_or_else(|err| {
|
|
|
|
panic!(
|
|
|
|
"Failed loading essential asset: {} (error={:?})",
|
|
|
|
specifier, err
|
|
|
|
)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Function used to load essential assets from the filesystem or the cache
|
|
|
|
/// and return a clone. It will panic if the asset is not found.
|
|
|
|
#[track_caller]
|
|
|
|
fn load_expect_cloned(specifier: &str) -> Self
|
|
|
|
where
|
|
|
|
Self: Clone,
|
|
|
|
{
|
|
|
|
Self::load_expect(specifier).cloned()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn load_owned(specifier: &str) -> Result<Self, Error>;
|
2021-06-25 16:47:03 +00:00
|
|
|
|
|
|
|
fn get_or_insert(specifier: &str, default: Self) -> AssetHandle<Self>;
|
2021-05-04 08:55:40 +00:00
|
|
|
}
|
|
|
|
|
2021-07-05 15:43:30 +00:00
|
|
|
/// Loads directory and all files in it
|
|
|
|
///
|
|
|
|
/// NOTE: If you call `.iter()` on it, all failed files will be ignored
|
|
|
|
/// If you want to handle errors, call `.ids()` which will return
|
|
|
|
/// iterator over assets specifiers
|
|
|
|
///
|
|
|
|
/// # Errors
|
|
|
|
/// An error is returned if the given id does not match a valid readable
|
|
|
|
/// directory.
|
|
|
|
///
|
|
|
|
/// When loading a directory recursively, directories that can't be read are
|
|
|
|
/// ignored.
|
2021-06-25 16:47:03 +00:00
|
|
|
pub fn load_dir<T: DirLoadable>(
|
|
|
|
specifier: &str,
|
|
|
|
recursive: bool,
|
|
|
|
) -> Result<AssetDirHandle<T>, Error> {
|
|
|
|
let specifier = specifier.strip_suffix(".*").unwrap_or(specifier);
|
|
|
|
ASSETS.load_dir(specifier, recursive)
|
|
|
|
}
|
|
|
|
|
2021-07-05 15:43:30 +00:00
|
|
|
/// Loads directory and all files in it
|
|
|
|
///
|
|
|
|
/// # Panics
|
|
|
|
/// 1) If can't load directory (filesystem errors)
|
|
|
|
/// 2) If file can't be loaded (parsing problem)
|
2021-06-25 16:47:03 +00:00
|
|
|
#[track_caller]
|
2021-07-05 15:43:30 +00:00
|
|
|
pub fn read_expect_dir<T: DirLoadable>(
|
|
|
|
specifier: &str,
|
|
|
|
recursive: bool,
|
|
|
|
) -> impl Iterator<Item = AssetGuard<T>> {
|
|
|
|
load_dir::<T>(specifier, recursive)
|
|
|
|
.unwrap_or_else(|e| panic!("Failed loading directory {}. error={:?}", e, specifier))
|
|
|
|
.ids()
|
|
|
|
.map(|entry| T::load_expect(entry).read())
|
2021-05-07 11:24:37 +00:00
|
|
|
}
|
|
|
|
|
2021-05-04 08:55:40 +00:00
|
|
|
impl<T: Compound> AssetExt for T {
|
|
|
|
fn load(specifier: &str) -> Result<AssetHandle<Self>, Error> { ASSETS.load(specifier) }
|
|
|
|
|
|
|
|
fn load_owned(specifier: &str) -> Result<Self, Error> { ASSETS.load_owned(specifier) }
|
2021-06-25 16:47:03 +00:00
|
|
|
|
|
|
|
fn get_or_insert(specifier: &str, default: Self) -> AssetHandle<Self> {
|
|
|
|
ASSETS.get_or_insert(specifier, default)
|
|
|
|
}
|
2021-05-04 08:55:40 +00:00
|
|
|
}
|
|
|
|
|
2021-05-07 11:24:37 +00:00
|
|
|
pub struct Image(pub Arc<DynamicImage>);
|
|
|
|
|
|
|
|
impl Image {
|
|
|
|
pub fn to_image(&self) -> Arc<DynamicImage> { Arc::clone(&self.0) }
|
|
|
|
}
|
|
|
|
|
2021-06-18 11:27:39 +00:00
|
|
|
pub struct ImageLoader;
|
|
|
|
impl Loader<Image> for ImageLoader {
|
|
|
|
fn load(content: Cow<[u8]>, ext: &str) -> Result<Image, BoxedError> {
|
|
|
|
let format = image::ImageFormat::from_extension(ext)
|
|
|
|
.ok_or_else(|| format!("Invalid file extension {}", ext))?;
|
2021-05-06 15:49:25 +00:00
|
|
|
let image = image::load_from_memory_with_format(&content, format)?;
|
2021-05-07 11:24:37 +00:00
|
|
|
Ok(Image(Arc::new(image)))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Asset for Image {
|
2021-06-18 11:27:39 +00:00
|
|
|
type Loader = ImageLoader;
|
2021-05-07 11:24:37 +00:00
|
|
|
|
2021-06-18 11:42:08 +00:00
|
|
|
const EXTENSIONS: &'static [&'static str] = &["png", "jpg"];
|
2021-05-07 11:24:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub struct DotVoxAsset(pub DotVoxData);
|
|
|
|
|
|
|
|
pub struct DotVoxLoader;
|
|
|
|
impl Loader<DotVoxAsset> for DotVoxLoader {
|
|
|
|
fn load(content: std::borrow::Cow<[u8]>, _: &str) -> Result<DotVoxAsset, BoxedError> {
|
|
|
|
let data = dot_vox::load_bytes(&content).map_err(|err| err.to_owned())?;
|
|
|
|
Ok(DotVoxAsset(data))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Asset for DotVoxAsset {
|
|
|
|
type Loader = DotVoxLoader;
|
|
|
|
|
|
|
|
const EXTENSION: &'static str = "vox";
|
|
|
|
}
|
|
|
|
|
2021-06-07 22:26:59 +00:00
|
|
|
/// Return path to repository root by searching 10 directories back
|
|
|
|
pub fn find_root() -> Option<PathBuf> {
|
2021-06-07 17:29:37 +00:00
|
|
|
std::env::current_dir().map_or(None, |path| {
|
|
|
|
// If we are in the root, push path
|
2021-06-07 22:26:59 +00:00
|
|
|
if path.join(".git").exists() {
|
2021-06-07 17:29:37 +00:00
|
|
|
return Some(path);
|
|
|
|
}
|
|
|
|
// Search .git directory in parent directries
|
|
|
|
for ancestor in path.ancestors().take(10) {
|
2021-06-07 22:26:59 +00:00
|
|
|
if ancestor.join(".git").exists() {
|
2021-06-07 17:29:37 +00:00
|
|
|
return Some(ancestor.to_path_buf());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-05-04 08:55:40 +00:00
|
|
|
lazy_static! {
|
|
|
|
/// Lazy static to find and cache where the asset directory is.
|
|
|
|
/// Cases we need to account for:
|
|
|
|
/// 1. Running through airshipper (`assets` next to binary)
|
|
|
|
/// 2. Install with package manager and run (assets probably in `/usr/share/veloren/assets` while binary in `/usr/bin/`)
|
|
|
|
/// 3. Download & hopefully extract zip (`assets` next to binary)
|
|
|
|
/// 4. Running through cargo (`assets` in workspace root but not always in cwd incase you `cd voxygen && cargo r`)
|
|
|
|
/// 5. Running executable in the target dir (`assets` in workspace)
|
2021-05-08 14:51:47 +00:00
|
|
|
/// 6. Running tests (`assets` in workspace root)
|
2021-05-04 08:55:40 +00:00
|
|
|
pub static ref ASSETS_PATH: PathBuf = {
|
|
|
|
let mut paths = Vec::new();
|
|
|
|
|
|
|
|
// Note: Ordering matters here!
|
|
|
|
|
|
|
|
// 1. VELOREN_ASSETS environment variable
|
|
|
|
if let Ok(var) = std::env::var("VELOREN_ASSETS") {
|
|
|
|
paths.push(var.into());
|
|
|
|
}
|
|
|
|
|
|
|
|
// 2. Executable path
|
|
|
|
if let Ok(mut path) = std::env::current_exe() {
|
|
|
|
path.pop();
|
|
|
|
paths.push(path);
|
|
|
|
}
|
|
|
|
|
2021-05-08 16:06:57 +00:00
|
|
|
// 3. Root of the repository
|
2021-06-07 17:29:37 +00:00
|
|
|
if let Some(path) = find_root() {
|
|
|
|
paths.push(path);
|
2021-05-08 14:51:47 +00:00
|
|
|
}
|
|
|
|
|
2021-05-08 16:06:57 +00:00
|
|
|
// 4. System paths
|
2021-05-04 08:55:40 +00:00
|
|
|
#[cfg(all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android")))]
|
|
|
|
{
|
|
|
|
if let Ok(result) = std::env::var("XDG_DATA_HOME") {
|
|
|
|
paths.push(format!("{}/veloren/", result).into());
|
|
|
|
} else if let Ok(result) = std::env::var("HOME") {
|
|
|
|
paths.push(format!("{}/.local/share/veloren/", result).into());
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Ok(result) = std::env::var("XDG_DATA_DIRS") {
|
|
|
|
result.split(':').for_each(|x| paths.push(format!("{}/veloren/", x).into()));
|
|
|
|
} else {
|
|
|
|
// Fallback
|
|
|
|
let fallback_paths = vec!["/usr/local/share", "/usr/share"];
|
|
|
|
for fallback_path in fallback_paths {
|
|
|
|
paths.push(format!("{}/veloren/", fallback_path).into());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
tracing::trace!("Possible asset locations paths={:?}", paths);
|
|
|
|
|
|
|
|
for mut path in paths.clone() {
|
|
|
|
if !path.ends_with("assets") {
|
|
|
|
path = path.join("assets");
|
|
|
|
}
|
|
|
|
|
|
|
|
if path.is_dir() {
|
|
|
|
tracing::info!("Assets found path={}", path.display());
|
|
|
|
return path;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
panic!(
|
|
|
|
"Asset directory not found. In attempting to find it, we searched:\n{})",
|
|
|
|
paths.iter().fold(String::new(), |mut a, path| {
|
|
|
|
a += &path.to_string_lossy();
|
|
|
|
a += "\n";
|
|
|
|
a
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns the actual path of the specifier with the extension.
|
|
|
|
///
|
|
|
|
/// For directories, give `""` as extension.
|
2021-06-25 16:47:03 +00:00
|
|
|
pub fn path_of(specifier: &str, ext: &str) -> PathBuf {
|
|
|
|
ASSETS
|
|
|
|
.source()
|
|
|
|
.path_of(source::DirEntry::File(specifier, ext))
|
2021-05-07 11:24:37 +00:00
|
|
|
}
|
2021-05-16 22:05:55 +00:00
|
|
|
|
2021-07-03 18:11:04 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use std::{ffi::OsStr, fs::File};
|
|
|
|
use walkdir::WalkDir;
|
|
|
|
|
|
|
|
/// Fail unless all `.ron` asset files successfully parse to `ron::Value`.
|
|
|
|
#[test]
|
|
|
|
fn parse_all_ron_files_to_value() {
|
|
|
|
let ext = OsStr::new("ron");
|
|
|
|
WalkDir::new(crate::ASSETS_PATH.as_path())
|
|
|
|
.into_iter()
|
2021-07-04 13:55:14 +00:00
|
|
|
.map(|ent| {
|
|
|
|
ent.expect("Failed to walk over asset directory")
|
|
|
|
.into_path()
|
|
|
|
})
|
2021-07-03 18:11:04 +00:00
|
|
|
.filter(|path| path.is_file())
|
|
|
|
.filter(|path| {
|
|
|
|
path.extension()
|
2021-07-05 15:43:30 +00:00
|
|
|
.map_or(false, |e| ext == e.to_ascii_lowercase())
|
2021-07-03 18:11:04 +00:00
|
|
|
})
|
|
|
|
.for_each(|path| {
|
2021-07-04 13:55:14 +00:00
|
|
|
let file = File::open(&path).expect("Failed to open the file");
|
2021-07-03 18:11:04 +00:00
|
|
|
if let Err(err) = ron::de::from_reader::<_, ron::Value>(file) {
|
|
|
|
println!("{:?}", path);
|
|
|
|
println!("{:#?}", err);
|
|
|
|
panic!("Parse failed");
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-07 21:09:29 +00:00
|
|
|
#[cfg(feature = "asset_tweak")]
|
2021-05-16 22:05:55 +00:00
|
|
|
pub mod asset_tweak {
|
2021-08-03 13:13:35 +00:00
|
|
|
//! Set of functions and macros for easy tweaking values
|
|
|
|
//! using our asset cache machinery.
|
|
|
|
//!
|
|
|
|
//! Because of how macros works, you will not find
|
|
|
|
//! [tweak] and [tweak_from] macros in this module,
|
|
|
|
//! import it from [assets](super) crate directly.
|
|
|
|
//!
|
|
|
|
//! Will hot-reload (if corresponded feature is enabled).
|
|
|
|
// TODO: don't use the same ASSETS_PATH as game uses?
|
2021-08-02 14:42:28 +00:00
|
|
|
use super::{Asset, AssetExt, RonLoader, ASSETS_PATH};
|
2021-06-07 17:29:37 +00:00
|
|
|
use ron::ser::{to_writer_pretty, PrettyConfig};
|
|
|
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
2021-06-25 16:47:03 +00:00
|
|
|
use std::{fs, path::Path};
|
2021-05-16 22:05:55 +00:00
|
|
|
|
2021-08-02 14:42:28 +00:00
|
|
|
/// Specifier to use with tweak functions in this module
|
|
|
|
///
|
|
|
|
/// `Tweak("test")` will be interpreted as `<assets_dir>/tweak/test.ron`.
|
|
|
|
///
|
|
|
|
/// `Asset(&["path", "to", "file"])` will be interpreted as
|
|
|
|
/// `<assets_dir>/path/to/file.ron`
|
|
|
|
pub enum Specifier<'a> {
|
|
|
|
Tweak(&'a str),
|
|
|
|
Asset(&'a [&'a str]),
|
|
|
|
}
|
|
|
|
|
2021-06-08 19:01:44 +00:00
|
|
|
#[derive(Clone, Deserialize, Serialize)]
|
2021-05-16 22:05:55 +00:00
|
|
|
struct AssetTweakWrapper<T>(T);
|
|
|
|
|
|
|
|
impl<T> Asset for AssetTweakWrapper<T>
|
|
|
|
where
|
|
|
|
T: Clone + Sized + Send + Sync + 'static + DeserializeOwned,
|
|
|
|
{
|
|
|
|
type Loader = RonLoader;
|
|
|
|
|
|
|
|
const EXTENSION: &'static str = "ron";
|
|
|
|
}
|
|
|
|
|
2021-08-02 14:42:28 +00:00
|
|
|
/// Read value from file, will panic if file doesn't exist.
|
|
|
|
///
|
|
|
|
/// If you don't have a file or its content is invalid,
|
|
|
|
/// this function will panic.
|
|
|
|
/// If you want to have some default content,
|
|
|
|
/// read documentation for [tweak_expect_or_create] for more.
|
|
|
|
///
|
|
|
|
/// # Examples:
|
|
|
|
/// How not to use.
|
|
|
|
/// ```should_panic
|
|
|
|
/// use veloren_common_assets::asset_tweak::{tweak_expect, Specifier};
|
|
|
|
///
|
|
|
|
/// // will panic if you don't have a file
|
|
|
|
/// let specifier = Specifier::Asset(&["no_way_we_have_this_directory", "x"]);
|
|
|
|
/// let x: i32 = tweak_expect(specifier);
|
|
|
|
/// ```
|
|
|
|
///
|
|
|
|
/// How to use.
|
|
|
|
/// ```
|
|
|
|
/// use std::fs;
|
|
|
|
/// use veloren_common_assets::{
|
|
|
|
/// asset_tweak::{tweak_expect, Specifier},
|
|
|
|
/// ASSETS_PATH,
|
|
|
|
/// };
|
|
|
|
///
|
|
|
|
/// // you need to create file first
|
2021-08-03 13:13:35 +00:00
|
|
|
/// let tweak_path = ASSETS_PATH.join("tweak/year.ron");
|
2021-08-02 14:42:28 +00:00
|
|
|
/// // note parentheses
|
|
|
|
/// fs::write(&tweak_path, b"(10)");
|
|
|
|
///
|
2021-08-03 13:13:35 +00:00
|
|
|
/// let y: i32 = tweak_expect(Specifier::Tweak("year"));
|
2021-08-02 14:42:28 +00:00
|
|
|
/// assert_eq!(y, 10);
|
|
|
|
///
|
|
|
|
/// // Specifier::Tweak is just a shorthand
|
|
|
|
/// // for Specifier::Asset(&["tweak", ..])
|
2021-08-03 13:13:35 +00:00
|
|
|
/// let y1: i32 = tweak_expect(Specifier::Asset(&["tweak", "year"]));
|
|
|
|
/// assert_eq!(y1, 10);
|
2021-08-02 14:42:28 +00:00
|
|
|
///
|
|
|
|
/// // you may want to remove this file later
|
|
|
|
/// std::fs::remove_file(tweak_path);
|
|
|
|
/// ```
|
|
|
|
pub fn tweak_expect<T>(specifier: Specifier) -> T
|
2021-08-02 11:29:45 +00:00
|
|
|
where
|
|
|
|
T: Clone + Sized + Send + Sync + 'static + DeserializeOwned,
|
|
|
|
{
|
2021-08-02 14:42:28 +00:00
|
|
|
let asset_specifier = match specifier {
|
|
|
|
Specifier::Tweak(specifier) => format!("tweak.{}", specifier),
|
|
|
|
Specifier::Asset(path) => path.join("."),
|
|
|
|
};
|
|
|
|
let handle = <AssetTweakWrapper<T> as AssetExt>::load_expect(&asset_specifier);
|
2021-08-02 11:29:45 +00:00
|
|
|
let AssetTweakWrapper(value) = handle.read().clone();
|
2021-08-02 14:42:28 +00:00
|
|
|
|
2021-08-02 11:29:45 +00:00
|
|
|
value
|
|
|
|
}
|
|
|
|
|
2021-08-02 14:42:28 +00:00
|
|
|
// Helper function to create new file to tweak.
|
|
|
|
//
|
|
|
|
// The file will be filled with passed value
|
|
|
|
// returns passed value.
|
2021-08-02 11:29:45 +00:00
|
|
|
fn create_new<T>(tweak_dir: &Path, filename: &str, value: T) -> T
|
|
|
|
where
|
2021-08-02 13:27:44 +00:00
|
|
|
T: Sized + Send + Sync + 'static + DeserializeOwned + Serialize,
|
2021-08-02 11:29:45 +00:00
|
|
|
{
|
|
|
|
fs::create_dir_all(tweak_dir).expect("failed to create directory for tweak files");
|
|
|
|
let f = fs::File::create(tweak_dir.join(filename)).unwrap_or_else(|error| {
|
|
|
|
panic!("failed to create file {:?}. Error: {:?}", filename, error)
|
|
|
|
});
|
2021-08-02 13:27:44 +00:00
|
|
|
let tweaker = AssetTweakWrapper(&value);
|
2021-08-02 11:29:45 +00:00
|
|
|
if let Err(e) = to_writer_pretty(f, &tweaker, PrettyConfig::new()) {
|
|
|
|
panic!("failed to write to file {:?}. Error: {:?}", filename, e);
|
|
|
|
}
|
|
|
|
|
|
|
|
value
|
|
|
|
}
|
|
|
|
|
2021-08-02 14:42:28 +00:00
|
|
|
// Helper function to get directory and file from asset list.
|
|
|
|
//
|
|
|
|
// Converts ["path", "to", "file"] to (String("path/to"), "file")
|
|
|
|
fn directory_and_name<'a>(path: &'a [&'a str]) -> (String, &'a str) {
|
|
|
|
let (file, path) = path.split_last().expect("empty asset list");
|
|
|
|
let directory = path.join("/");
|
|
|
|
|
|
|
|
(directory, file)
|
2021-05-16 22:05:55 +00:00
|
|
|
}
|
2021-06-07 17:29:37 +00:00
|
|
|
|
2021-08-02 14:42:28 +00:00
|
|
|
/// Read a value from asset, creating file if not exists.
|
2021-06-07 21:09:29 +00:00
|
|
|
///
|
2021-08-02 14:42:28 +00:00
|
|
|
/// If file exists will read a value from such file
|
|
|
|
/// using [tweak_expect].
|
2021-06-07 17:29:37 +00:00
|
|
|
///
|
2021-08-02 14:42:28 +00:00
|
|
|
/// File should look like that (note the parentheses).
|
2021-06-07 17:29:37 +00:00
|
|
|
/// ```text
|
|
|
|
/// assets/tweak/x.ron
|
|
|
|
/// (5)
|
|
|
|
/// ```
|
2021-08-02 14:42:28 +00:00
|
|
|
///
|
|
|
|
/// # Example:
|
|
|
|
/// Tweaking integer value
|
|
|
|
/// ```
|
|
|
|
/// use veloren_common_assets::{
|
|
|
|
/// asset_tweak::{tweak_expect_or_create, Specifier},
|
|
|
|
/// ASSETS_PATH,
|
|
|
|
/// };
|
|
|
|
///
|
|
|
|
/// // first time it will create the file
|
2021-08-03 13:13:35 +00:00
|
|
|
/// let x: i32 = tweak_expect_or_create(Specifier::Tweak("stars"), 5);
|
|
|
|
/// let file_path = ASSETS_PATH.join("tweak/stars.ron");
|
2021-08-02 14:42:28 +00:00
|
|
|
/// assert!(file_path.is_file());
|
|
|
|
/// assert_eq!(x, 5);
|
|
|
|
///
|
|
|
|
/// // next time it will read value from file
|
|
|
|
/// // whatever you will pass as default
|
2021-08-03 13:13:35 +00:00
|
|
|
/// let x1: i32 = tweak_expect_or_create(Specifier::Tweak("stars"), 42);
|
|
|
|
/// assert_eq!(x1, 5);
|
2021-08-02 14:42:28 +00:00
|
|
|
///
|
|
|
|
/// // you may want to remove this file later
|
|
|
|
/// std::fs::remove_file(file_path);
|
|
|
|
/// ```
|
|
|
|
pub fn tweak_expect_or_create<T>(specifier: Specifier, value: T) -> T
|
2021-06-07 17:29:37 +00:00
|
|
|
where
|
|
|
|
T: Clone + Sized + Send + Sync + 'static + DeserializeOwned + Serialize,
|
|
|
|
{
|
2021-08-02 14:42:28 +00:00
|
|
|
let (dir, filename) = match specifier {
|
|
|
|
Specifier::Tweak(name) => (ASSETS_PATH.join("tweak"), format!("{}.ron", name)),
|
|
|
|
Specifier::Asset(list) => {
|
|
|
|
let (directory, name) = directory_and_name(list);
|
|
|
|
(ASSETS_PATH.join(directory), format!("{}.ron", name))
|
|
|
|
},
|
|
|
|
};
|
2021-06-07 17:29:37 +00:00
|
|
|
|
2021-08-02 14:42:28 +00:00
|
|
|
if Path::new(&dir.join(&filename)).is_file() {
|
|
|
|
tweak_expect(specifier)
|
2021-06-07 17:29:37 +00:00
|
|
|
} else {
|
2021-08-02 14:42:28 +00:00
|
|
|
create_new(&dir, &filename, value)
|
2021-06-07 17:29:37 +00:00
|
|
|
}
|
|
|
|
}
|
2021-05-16 22:05:55 +00:00
|
|
|
|
2021-08-03 13:13:35 +00:00
|
|
|
/// Convinient macro to quickly tweak value.
|
|
|
|
///
|
|
|
|
/// Will use [Specifier]`::Tweak` specifier and call
|
|
|
|
/// [tweak_expect] if passed only name
|
|
|
|
/// or [tweak_expect_or_create] if default is passed.
|
|
|
|
///
|
|
|
|
/// # Examples:
|
|
|
|
/// ```
|
|
|
|
/// // note that you need to export it from `assets` crate,
|
|
|
|
/// // not from `assets::asset_tweak`
|
|
|
|
/// use veloren_common_assets::{tweak, ASSETS_PATH};
|
|
|
|
///
|
|
|
|
/// // you need to create file first
|
|
|
|
/// let own_path = ASSETS_PATH.join("tweak/grizelda.ron");
|
|
|
|
/// // note parentheses
|
|
|
|
/// std::fs::write(&own_path, b"(10)");
|
|
|
|
///
|
|
|
|
/// let z: i32 = tweak!("grizelda");
|
|
|
|
/// assert_eq!(z, 10);
|
|
|
|
///
|
|
|
|
/// // voila, you don't need to care about creating file first
|
|
|
|
/// let p: i32 = tweak!("peter", 8);
|
|
|
|
///
|
|
|
|
/// let created_path = ASSETS_PATH.join("tweak/peter.ron");
|
|
|
|
/// assert!(created_path.is_file());
|
|
|
|
/// assert_eq!(p, 8);
|
|
|
|
///
|
|
|
|
/// // will use default value only first time
|
|
|
|
/// // if file exists, will load from this file
|
|
|
|
/// let p: i32 = tweak!("peter", 50);
|
|
|
|
/// assert_eq!(p, 8);
|
|
|
|
///
|
|
|
|
/// // you may want to remove this file later
|
|
|
|
/// std::fs::remove_file(own_path);
|
|
|
|
/// std::fs::remove_file(created_path);
|
|
|
|
/// ```
|
|
|
|
#[macro_export]
|
|
|
|
macro_rules! tweak {
|
|
|
|
($name:literal) => {{
|
|
|
|
use $crate::asset_tweak::{tweak_expect, Specifier::Tweak};
|
|
|
|
|
|
|
|
tweak_expect(Tweak($name))
|
|
|
|
}};
|
|
|
|
|
|
|
|
($name:literal, $default:expr) => {{
|
|
|
|
use $crate::asset_tweak::{tweak_expect_or_create, Specifier::Tweak};
|
|
|
|
|
|
|
|
tweak_expect_or_create(Tweak($name), $default)
|
|
|
|
}};
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Convinient macro to quickly tweak value from some existing path.
|
|
|
|
///
|
|
|
|
/// Will use [Specifier]`::Asset` specifier and call
|
|
|
|
/// [tweak_expect] if passed only name
|
|
|
|
/// or [tweak_expect_or_create] if default is passed.
|
|
|
|
///
|
|
|
|
/// The main use case is when you have some object
|
|
|
|
/// which needs constant tuning of values, but you can't afford
|
|
|
|
/// loading a file.
|
|
|
|
/// So you can use tweak_from! and then just copy values from asset
|
|
|
|
/// to your object.
|
|
|
|
///
|
|
|
|
/// # Examples:
|
|
|
|
/// ```no_run
|
|
|
|
/// // note that you need to export it from `assets` crate,
|
|
|
|
/// // not from `assets::asset_tweak`
|
|
|
|
/// use serde::{Deserialize, Serialize};
|
|
|
|
/// use veloren_common_assets::{tweak_from, ASSETS_PATH};
|
|
|
|
///
|
|
|
|
/// #[derive(Clone, PartialEq, Deserialize, Serialize)]
|
|
|
|
/// struct Data {
|
|
|
|
/// x: i32,
|
|
|
|
/// y: i32,
|
|
|
|
/// }
|
|
|
|
///
|
|
|
|
/// let default = Data { x: 5, y: 7 };
|
|
|
|
/// let data: Data = tweak_from!(&["common", "body", "dimensions"], default);
|
|
|
|
/// ```
|
|
|
|
#[macro_export]
|
|
|
|
macro_rules! tweak_from {
|
|
|
|
($path:expr) => {{
|
|
|
|
use $crate::asset_tweak::{tweak_expect, Specifier::Asset};
|
|
|
|
|
|
|
|
tweak_expect(Asset($path))
|
|
|
|
}};
|
|
|
|
|
|
|
|
($path:expr, $default:expr) => {{
|
|
|
|
use $crate::asset_tweak::{tweak_expect_or_create, Specifier::Asset};
|
|
|
|
|
|
|
|
tweak_expect_or_create(Asset($path), $default)
|
|
|
|
}};
|
|
|
|
}
|
|
|
|
|
2021-06-07 21:09:29 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
2021-08-02 14:42:28 +00:00
|
|
|
use super::*;
|
2021-06-07 21:09:29 +00:00
|
|
|
use std::{
|
|
|
|
convert::AsRef,
|
|
|
|
fmt::Debug,
|
|
|
|
fs::{self, File},
|
|
|
|
io::Write,
|
|
|
|
path::Path,
|
|
|
|
};
|
|
|
|
|
|
|
|
struct DirectoryGuard<P>
|
|
|
|
where
|
|
|
|
P: AsRef<Path>,
|
|
|
|
{
|
|
|
|
dir: P,
|
|
|
|
}
|
2021-05-16 22:05:55 +00:00
|
|
|
|
2021-06-07 21:09:29 +00:00
|
|
|
impl<P> DirectoryGuard<P>
|
|
|
|
where
|
|
|
|
P: AsRef<Path>,
|
|
|
|
{
|
|
|
|
fn create(dir: P) -> Self {
|
|
|
|
fs::create_dir_all(&dir).expect("failed to create directory");
|
|
|
|
Self { dir }
|
|
|
|
}
|
|
|
|
}
|
2021-05-16 22:05:55 +00:00
|
|
|
|
2021-06-07 21:09:29 +00:00
|
|
|
impl<P> Drop for DirectoryGuard<P>
|
|
|
|
where
|
|
|
|
P: AsRef<Path>,
|
|
|
|
{
|
|
|
|
fn drop(&mut self) { fs::remove_dir(&self.dir).expect("failed to remove directory"); }
|
2021-05-16 22:05:55 +00:00
|
|
|
}
|
|
|
|
|
2021-06-07 21:09:29 +00:00
|
|
|
struct FileGuard<P>
|
|
|
|
where
|
|
|
|
P: AsRef<Path> + Debug,
|
|
|
|
{
|
|
|
|
file: P,
|
|
|
|
}
|
2021-05-16 22:05:55 +00:00
|
|
|
|
2021-06-07 21:09:29 +00:00
|
|
|
impl<P> FileGuard<P>
|
|
|
|
where
|
|
|
|
P: AsRef<Path> + Debug,
|
|
|
|
{
|
|
|
|
fn create(file: P) -> (Self, File) {
|
|
|
|
let f = File::create(&file)
|
|
|
|
.unwrap_or_else(|_| panic!("failed to create file {:?}", &file));
|
|
|
|
(Self { file }, f)
|
|
|
|
}
|
2021-05-16 22:05:55 +00:00
|
|
|
|
2021-06-07 21:09:29 +00:00
|
|
|
fn hold(file: P) -> Self { Self { file } }
|
2021-05-16 22:05:55 +00:00
|
|
|
}
|
2021-06-07 18:00:54 +00:00
|
|
|
|
2021-06-07 21:09:29 +00:00
|
|
|
impl<P> Drop for FileGuard<P>
|
|
|
|
where
|
|
|
|
P: AsRef<Path> + Debug,
|
|
|
|
{
|
|
|
|
fn drop(&mut self) {
|
2021-08-02 14:42:28 +00:00
|
|
|
fs::remove_file(&self.file).unwrap_or_else(|e| {
|
|
|
|
panic!("failed to remove file {:?}. Error: {:?}", &self.file, e)
|
|
|
|
});
|
2021-06-07 21:09:29 +00:00
|
|
|
}
|
2021-05-16 22:05:55 +00:00
|
|
|
}
|
|
|
|
|
2021-08-02 14:42:28 +00:00
|
|
|
// helper function to create environment with needed directory and file
|
|
|
|
// and responsible for cleaning
|
|
|
|
fn run_with_file(tweak_path: &[&str], test: impl Fn(&mut File)) {
|
|
|
|
let (tweak_dir, tweak_name) = directory_and_name(tweak_path);
|
|
|
|
let tweak_folder = ASSETS_PATH.join(tweak_dir);
|
|
|
|
let tweak_file = tweak_folder.join(format!("{}.ron", tweak_name));
|
|
|
|
|
|
|
|
let _dir_guard = DirectoryGuard::create(tweak_folder);
|
|
|
|
let (_file_guard, mut file) = FileGuard::create(tweak_file);
|
|
|
|
|
|
|
|
test(&mut file);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_tweaked_int() {
|
|
|
|
let tweak_path = &["tweak_test_int", "tweak"];
|
|
|
|
|
|
|
|
run_with_file(tweak_path, |file| {
|
|
|
|
file.write_all(b"(5)").expect("failed to write to the file");
|
2021-08-06 09:57:29 +00:00
|
|
|
let x: i32 = tweak_expect(Specifier::Asset(tweak_path));
|
2021-08-02 14:42:28 +00:00
|
|
|
assert_eq!(x, 5);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-06-07 21:09:29 +00:00
|
|
|
#[test]
|
|
|
|
fn test_tweaked_string() {
|
2021-08-02 14:42:28 +00:00
|
|
|
let tweak_path = &["tweak_test_string", "tweak"];
|
2021-06-07 21:09:29 +00:00
|
|
|
|
2021-08-02 14:42:28 +00:00
|
|
|
run_with_file(tweak_path, |file| {
|
|
|
|
file.write_all(br#"("Hello Zest")"#)
|
|
|
|
.expect("failed to write to the file");
|
|
|
|
|
2021-08-06 09:57:29 +00:00
|
|
|
let x: String = tweak_expect(Specifier::Asset(tweak_path));
|
2021-08-02 14:42:28 +00:00
|
|
|
assert_eq!(x, "Hello Zest".to_owned());
|
|
|
|
});
|
|
|
|
}
|
2021-06-07 21:09:29 +00:00
|
|
|
|
2021-08-02 14:42:28 +00:00
|
|
|
#[test]
|
|
|
|
fn test_tweaked_hashmap() {
|
|
|
|
type Map = std::collections::HashMap<String, i32>;
|
|
|
|
|
|
|
|
let tweak_path = &["tweak_test_map", "tweak"];
|
|
|
|
|
|
|
|
run_with_file(tweak_path, |file| {
|
|
|
|
file.write_all(
|
2021-06-07 21:09:29 +00:00
|
|
|
br#"
|
2021-08-02 14:42:28 +00:00
|
|
|
({
|
|
|
|
"wow": 4,
|
|
|
|
"such": 5,
|
|
|
|
})
|
|
|
|
"#,
|
2021-06-07 21:09:29 +00:00
|
|
|
)
|
|
|
|
.expect("failed to write to the file");
|
2021-08-02 14:42:28 +00:00
|
|
|
|
2021-08-06 09:57:29 +00:00
|
|
|
let x: Map = tweak_expect(Specifier::Asset(tweak_path));
|
2021-08-02 14:42:28 +00:00
|
|
|
|
|
|
|
let mut map = Map::new();
|
|
|
|
map.insert("wow".to_owned(), 4);
|
|
|
|
map.insert("such".to_owned(), 5);
|
|
|
|
assert_eq!(x, map);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-08-06 09:57:29 +00:00
|
|
|
#[test]
|
|
|
|
fn test_tweaked_with_macro_struct() {
|
|
|
|
// partial eq and debug because of assert_eq in this test
|
|
|
|
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
|
|
|
|
struct Wow {
|
|
|
|
such: i32,
|
|
|
|
field: f32,
|
|
|
|
}
|
|
|
|
|
|
|
|
let tweak_path = &["tweak_test_struct", "tweak"];
|
|
|
|
|
|
|
|
run_with_file(tweak_path, |file| {
|
|
|
|
file.write_all(
|
|
|
|
br#"
|
|
|
|
((
|
|
|
|
such: 5,
|
|
|
|
field: 35.752346,
|
|
|
|
))
|
|
|
|
"#,
|
|
|
|
)
|
|
|
|
.expect("failed to write to the file");
|
|
|
|
|
|
|
|
let x: Wow = crate::tweak_from!(tweak_path);
|
|
|
|
let expected = Wow {
|
|
|
|
such: 5,
|
|
|
|
field: 35.752_346,
|
|
|
|
};
|
|
|
|
assert_eq!(x, expected);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-08-02 14:42:28 +00:00
|
|
|
fn run_with_path(tweak_path: &[&str], test: impl Fn(&Path)) {
|
|
|
|
let (tweak_dir, tweak_name) = directory_and_name(tweak_path);
|
|
|
|
|
|
|
|
let tweak_folder = ASSETS_PATH.join(tweak_dir);
|
|
|
|
let test_path = tweak_folder.join(format!("{}.ron", tweak_name));
|
|
|
|
|
|
|
|
let _file_guard = FileGuard::hold(&test_path);
|
|
|
|
|
|
|
|
test(&test_path);
|
2021-06-07 21:09:29 +00:00
|
|
|
}
|
2021-06-07 18:00:54 +00:00
|
|
|
|
2021-06-07 21:09:29 +00:00
|
|
|
#[test]
|
2021-08-02 14:42:28 +00:00
|
|
|
fn test_create_tweak() {
|
|
|
|
let tweak_path = &["tweak_create_test", "tweak"];
|
|
|
|
|
|
|
|
run_with_path(tweak_path, |test_path| {
|
2021-08-06 09:57:29 +00:00
|
|
|
let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
|
2021-08-02 14:42:28 +00:00
|
|
|
assert_eq!(x, 5);
|
|
|
|
assert!(test_path.is_file());
|
|
|
|
// Recheck it loads back correctly
|
2021-08-06 09:57:29 +00:00
|
|
|
let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
|
2021-08-02 14:42:28 +00:00
|
|
|
assert_eq!(x, 5);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_create_tweak_deep() {
|
|
|
|
let tweak_path = &["so_much", "deep_test", "tweak_create_test", "tweak"];
|
|
|
|
|
|
|
|
run_with_path(tweak_path, |test_path| {
|
2021-08-06 09:57:29 +00:00
|
|
|
let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
|
2021-08-02 14:42:28 +00:00
|
|
|
assert_eq!(x, 5);
|
|
|
|
assert!(test_path.is_file());
|
|
|
|
// Recheck it loads back correctly
|
2021-08-06 09:57:29 +00:00
|
|
|
let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
|
2021-08-02 14:42:28 +00:00
|
|
|
assert_eq!(x, 5);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_create_but_prioritize_loaded() {
|
|
|
|
let tweak_path = &["tweak_create_and_prioritize_test", "tweak"];
|
|
|
|
|
|
|
|
run_with_path(tweak_path, |test_path| {
|
2021-08-06 09:57:29 +00:00
|
|
|
let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
|
2021-08-02 14:42:28 +00:00
|
|
|
assert_eq!(x, 5);
|
|
|
|
assert!(test_path.is_file());
|
|
|
|
|
|
|
|
// Recheck it loads back
|
|
|
|
// with content as priority
|
|
|
|
fs::write(test_path, b"(10)").expect("failed to write to the file");
|
2021-08-06 09:57:29 +00:00
|
|
|
let x = tweak_expect_or_create(Specifier::Asset(tweak_path), 5);
|
2021-08-02 14:42:28 +00:00
|
|
|
assert_eq!(x, 10);
|
|
|
|
});
|
2021-06-07 21:09:29 +00:00
|
|
|
}
|
2021-06-07 18:00:54 +00:00
|
|
|
}
|
2021-05-16 22:05:55 +00:00
|
|
|
}
|