diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ee38ebac5..da674ffef9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial support for game plugins, both server-side and client-side - Reflective LoD water - Map indicators for group members +- Hot-reloading for i18n, sounds, loot lotteries, and more ### Changed diff --git a/Cargo.lock b/Cargo.lock index 4199c7fa4e..89999392bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,17 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" +[[package]] +name = "ahash" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75b7e6a93ecd6dbd2c225154d0fa7f86205574ecaa6c87429fb5f66ee677c44" +dependencies = [ + "getrandom 0.2.0", + "lazy_static", + "version_check 0.9.2", +] + [[package]] name = "aho-corasick" version = "0.7.13" @@ -230,6 +241,23 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbf56136a5198c7b01a49e3afcbef6cf84597273d298f54432926024107b0109" +[[package]] +name = "assets_manager" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3593b8ddb9708251c7a57b07c917c77ecc685e804b697366d26fb4af64c9b960" +dependencies = [ + "ahash 0.6.2", + "bincode", + "crossbeam-channel 0.5.0", + "log", + "notify 4.0.15", + "parking_lot 0.11.0", + "ron", + "serde", + "serde_json", +] + [[package]] name = "async-std" version = "1.5.0" @@ -791,7 +819,7 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25e4c606eb459dd29f7c57b2e0879f2b6f14ee130918c2b78ccb58a9624e6c7a" dependencies = [ - "getrandom", + "getrandom 0.1.15", "proc-macro-hack", ] @@ -1745,6 +1773,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "fsevent" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6" +dependencies = [ + "bitflags", + "fsevent-sys 2.0.1", +] + [[package]] name = "fsevent" version = "2.0.2" @@ -1752,7 +1790,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97f347202c95c98805c216f9e1df210e8ebaec9fdb2365700a43c10797a35e63" dependencies = [ "bitflags", - "fsevent-sys", + "fsevent-sys 3.0.2", +] + +[[package]] +name = "fsevent-sys" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0" +dependencies = [ + "libc", ] [[package]] @@ -1972,6 +2019,17 @@ dependencies = [ "wasi 0.9.0+wasi-snapshot-preview1", ] +[[package]] +name = "getrandom" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8025cf36f917e6a52cce185b7c7177689b838b7ec138364e50cc2277a56cf4" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "gfx" version = "0.18.2" @@ -2561,6 +2619,17 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "inotify" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f" +dependencies = [ + "bitflags", + "inotify-sys", + "libc", +] + [[package]] name = "inotify" version = "0.8.3" @@ -3249,6 +3318,24 @@ dependencies = [ "version_check 0.9.2", ] +[[package]] +name = "notify" +version = "4.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80ae4a7688d1fab81c5bf19c64fc8db920be8d519ce6336ed4e7efe024724dbd" +dependencies = [ + "bitflags", + "filetime", + "fsevent 0.4.0", + "fsevent-sys 2.0.1", + "inotify 0.7.1", + "libc", + "mio 0.6.22", + "mio-extras", + "walkdir 2.3.1", + "winapi 0.3.9", +] + [[package]] name = "notify" version = "5.0.0-pre.3" @@ -3259,9 +3346,9 @@ dependencies = [ "bitflags", "crossbeam-channel 0.4.4", "filetime", - "fsevent", - "fsevent-sys", - "inotify", + "fsevent 2.0.2", + "fsevent-sys 3.0.2", + "inotify 0.8.3", "libc", "mio 0.6.22", "mio-extras", @@ -4033,7 +4120,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ - "getrandom", + "getrandom 0.1.15", "libc", "rand_chacha 0.2.2", "rand_core 0.5.1", @@ -4082,7 +4169,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" dependencies = [ - "getrandom", + "getrandom 0.1.15", ] [[package]] @@ -4231,7 +4318,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" dependencies = [ - "getrandom", + "getrandom 0.1.15", "redox_syscall", "rust-argon2", ] @@ -5699,6 +5786,7 @@ name = "veloren-common" version = "0.8.0" dependencies = [ "arraygen", + "assets_manager", "criterion", "crossbeam-channel 0.5.0", "crossbeam-utils 0.8.1", @@ -5710,7 +5798,6 @@ dependencies = [ "image", "indexmap", "lazy_static", - "notify", "num-derive", "num-traits 0.2.14", "ordered-float 2.0.1", @@ -5933,7 +6020,7 @@ dependencies = [ "inline_tweak", "lazy_static", "libloading 0.6.3", - "notify", + "notify 5.0.0-pre.3", "tracing", "vek 0.12.0", "veloren-common", diff --git a/common/Cargo.toml b/common/Cargo.toml index d5590a5d6c..9260c640a9 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -30,10 +30,10 @@ vek = { version = "0.12.0", features = ["serde"] } uuid = { version = "0.8.1", default-features = false, features = ["serde", "v4"] } # Assets +assets_manager = {version = "0.4.2", features = ["bincode", "ron", "json", "hot-reloading"]} directories-next = "2.0" dot_vox = "4.0" image = { version = "0.23.12", default-features = false, features = ["png"] } -notify = "5.0.0-pre.3" # Data structures hashbrown = { version = "0.9", features = ["rayon", "serde", "nightly"] } diff --git a/common/src/assets.rs b/common/src/assets.rs new file mode 100644 index 0000000000..871f924dc1 --- /dev/null +++ b/common/src/assets.rs @@ -0,0 +1,244 @@ +//! Load assets (images or voxel data) from files + +use dot_vox::DotVoxData; +use image::DynamicImage; +use lazy_static::lazy_static; +use std::{ + borrow::Cow, + fs, io, + path::{Path, PathBuf}, + sync::Arc, +}; + +pub use assets_manager::{ + asset::Ron, + loader::{ + self, BincodeLoader, BytesLoader, JsonLoader, LoadFrom, Loader, RonLoader, StringLoader, + }, + source, Asset, AssetCache, BoxedError, Compound, Error, +}; + +lazy_static! { + /// The HashMap where all loaded assets are stored in. + static ref ASSETS: AssetCache = AssetCache::new(&*ASSETS_PATH).unwrap(); +} + +pub fn start_hot_reloading() { ASSETS.enhance_hot_reloading(); } + +pub type AssetHandle = assets_manager::Handle<'static, T>; +pub type AssetDir = assets_manager::DirReader<'static, T, source::FileSystem>; + +/// 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. + /// Example usage: + /// ```no_run + /// use veloren_common::assets::{self, AssetExt}; + /// + /// let my_image = assets::Image::load("core.ui.backgrounds.city").unwrap(); + /// ``` + fn load(specifier: &str) -> Result, Error>; + + /// Function used to load assets from the filesystem or the cache and return + /// a clone. + fn load_cloned(specifier: &str) -> Result + where + Self: Clone, + { + Self::load(specifier).map(AssetHandle::cloned) + } + + /// Function used to load essential assets from the filesystem or the cache. + /// It will panic if the asset is not found. Example usage: + /// ```no_run + /// use veloren_common::assets::{self, AssetExt}; + /// + /// let my_image = assets::Image::load_expect("core.ui.backgrounds.city"); + /// ``` + #[track_caller] + fn load_expect(specifier: &str) -> AssetHandle { + 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; +} + +pub fn load_dir(specifier: &str) -> Result, Error> { + Ok(ASSETS.load_dir(specifier)?) +} + +impl AssetExt for T { + fn load(specifier: &str) -> Result, Error> { ASSETS.load(specifier) } + + fn load_owned(specifier: &str) -> Result { ASSETS.load_owned(specifier) } +} + +pub struct Image(pub Arc); + +impl Image { + pub fn to_image(&self) -> Arc { Arc::clone(&self.0) } +} + +pub struct ImageLoader; +impl Loader for ImageLoader { + fn load(content: Cow<[u8]>, _: &str) -> Result { + let image = image::load_from_memory(&content)?; + Ok(Image(Arc::new(image))) + } +} + +impl Asset for Image { + type Loader = ImageLoader; + + const EXTENSIONS: &'static [&'static str] = &["png", "jpg"]; +} + +pub struct DotVoxAsset(pub DotVoxData); + +pub struct DotVoxLoader; +impl Loader for DotVoxLoader { + fn load(content: std::borrow::Cow<[u8]>, _: &str) -> Result { + 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"; +} + +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) + 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); + } + + // 3. Working path + if let Ok(path) = std::env::current_dir() { + paths.push(path); + } + + // 4. Cargo Workspace (e.g. local development) + // https://github.com/rust-lang/cargo/issues/3946#issuecomment-359619839 + if let Ok(Ok(path)) = std::env::var("CARGO_MANIFEST_DIR").map(|s| s.parse::()) { + paths.push(path.parent().unwrap().to_path_buf()); + paths.push(path); + } + + // 5. System paths + #[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 + }), + ); + }; +} + +fn get_dir_files(files: &mut Vec, path: &Path, specifier: &str) -> io::Result<()> { + for entry in fs::read_dir(path)? { + if let Ok(entry) = entry { + let path = entry.path(); + let maybe_stem = path.file_stem().and_then(|stem| stem.to_str()); + + if let Some(stem) = maybe_stem { + let specifier = format!("{}.{}", specifier, stem); + + if path.is_dir() { + get_dir_files(files, &path, &specifier)?; + } else { + files.push(specifier); + } + } + } + } + + Ok(()) +} + +pub struct Directory(Vec); + +impl Directory { + pub fn iter(&self) -> impl Iterator { self.0.iter() } +} + +impl Compound for Directory { + fn load(_: &AssetCache, specifier: &str) -> Result { + let root = ASSETS.source().path_of(specifier, ""); + let mut files = Vec::new(); + + get_dir_files(&mut files, &root, specifier)?; + + Ok(Directory(files)) + } +} diff --git a/common/src/assets/mod.rs b/common/src/assets/mod.rs deleted file mode 100644 index d12421a2ef..0000000000 --- a/common/src/assets/mod.rs +++ /dev/null @@ -1,508 +0,0 @@ -//! Load assets (images or voxel data) from files -pub mod watch; - -use core::{any::Any, fmt, marker::PhantomData}; -use dot_vox::DotVoxData; -use hashbrown::HashMap; -use image::DynamicImage; -use lazy_static::lazy_static; -use serde::Deserialize; -use serde_json::Value; -use std::{ - fs::{self, File, ReadDir}, - io::{BufReader, Read}, - path::PathBuf, - sync::{Arc, RwLock}, -}; -use tracing::{error, trace}; - -/// The error returned by asset loading functions -#[derive(Debug, Clone)] -pub enum Error { - /// Parsing error occurred. - ParseError(Arc), - /// An asset of a different type has already been loaded with this - /// specifier. - InvalidType, - /// Asset does not exist. - NotFound(String), -} - -impl Error { - pub fn parse_error(err: E) -> Self { - Self::ParseError(Arc::new(err)) - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Error::ParseError(err) => write!(f, "{:?}", err), - Error::InvalidType => write!( - f, - "an asset of a different type has already been loaded with this specifier." - ), - Error::NotFound(s) => write!(f, "{}", s), - } - } -} - -impl From> for Error { - fn from(_: Arc) -> Self { Error::InvalidType } -} - -impl From for Error { - fn from(err: std::io::Error) -> Self { Error::NotFound(format!("{}", err)) } -} - -lazy_static! { - /// The HashMap where all loaded assets are stored in. - static ref ASSETS: RwLock>> = - RwLock::new(HashMap::new()); -} - -fn reload(specifier: &str) -> Result<(), Error> -where - A::Output: Send + Sync + 'static, -{ - let asset = Arc::new(A::parse(load_file(specifier, A::ENDINGS)?, specifier)?); - let mut assets_write = ASSETS.write().unwrap(); - match assets_write.get_mut(specifier) { - Some(a) => *a = asset, - None => { - assets_write.insert(specifier.to_owned(), asset); - }, - } - - Ok(()) -} - -/// The Asset trait, which is implemented by all structures that have their data -/// stored in the filesystem. -pub trait Asset: Sized { - type Output = Self; - - const ENDINGS: &'static [&'static str]; - /// Parse the input file and return the correct Asset. - fn parse(buf_reader: BufReader, specifier: &str) -> Result; - - // TODO: Remove this function. It's only used in world/ in a really ugly way.To - // do this properly assets should have all their necessary data in one file. A - // ron file could be used to combine voxel data with positioning data for - // example. - /// Function used to load assets from the filesystem or the cache. Permits - /// manipulating the loaded asset with a mapping function. Example usage: - /// ```no_run - /// use vek::*; - /// use veloren_common::{assets::Asset, terrain::Structure}; - /// - /// let my_tree_structure = Structure::load_map("world.tree.oak_green.1", |s: Structure| { - /// s.with_center(Vec3::new(15, 18, 14)) - /// }) - /// .unwrap(); - /// ``` - fn load_map Self::Output>( - specifier: &str, - f: F, - ) -> Result, Error> - where - Self::Output: Send + Sync + 'static, - { - let assets_read = ASSETS.read().unwrap(); - match assets_read.get(specifier) { - Some(asset) => Ok(Arc::clone(asset).downcast()?), - None => { - drop(assets_read); // Drop the asset hashmap to permit recursive loading - let asset = Arc::new(f(Self::parse( - load_file(specifier, Self::ENDINGS)?, - specifier, - )?)); - let clone = Arc::clone(&asset); - ASSETS.write().unwrap().insert(specifier.to_owned(), clone); - Ok(asset) - }, - } - } - - fn load_glob(specifier: &str) -> Result>>, Error> - where - Self::Output: Send + Sync + 'static, - { - if let Some(assets) = ASSETS.read().unwrap().get(specifier) { - return Ok(Arc::clone(assets).downcast()?); - } - - match get_glob_matches(specifier) { - Ok(glob_matches) => { - let assets = Arc::new( - glob_matches - .into_iter() - .filter_map(|name| { - Self::load(&name) - .map_err(|e| { - error!( - ?e, - "Failed to load \"{}\" as part of glob \"{}\"", - name, - specifier - ) - }) - .ok() - }) - .collect::>(), - ); - let clone = Arc::clone(&assets); - - let mut assets_write = ASSETS.write().unwrap(); - assets_write.insert(specifier.to_owned(), clone); - Ok(assets) - }, - Err(error) => Err(error), - } - } - - fn load_glob_cloned(specifier: &str) -> Result, Error> - where - Self::Output: Clone + Send + Sync + 'static, - { - match get_glob_matches(specifier) { - Ok(glob_matches) => Ok(glob_matches - .into_iter() - .map(|name| { - let full_specifier = &specifier.replace("*", &name); - ( - Self::load_expect_cloned(full_specifier), - full_specifier.to_string(), - ) - }) - .collect::>()), - Err(error) => Err(error), - } - } - - /// Function used to load assets from the filesystem or the cache. - /// Example usage: - /// ```no_run - /// use image::DynamicImage; - /// use veloren_common::assets::Asset; - /// - /// let my_image = DynamicImage::load("core.ui.backgrounds.city").unwrap(); - /// ``` - fn load(specifier: &str) -> Result, Error> - where - Self::Output: Send + Sync + 'static, - { - Self::load_map(specifier, |x| x) - } - - /// Function used to load assets from the filesystem or the cache and return - /// a clone. - fn load_cloned(specifier: &str) -> Result - where - Self::Output: Clone + Send + Sync + 'static, - { - Self::load(specifier).map(|asset| (*asset).clone()) - } - - /// Function used to load essential assets from the filesystem or the cache. - /// It will panic if the asset is not found. Example usage: - /// ```no_run - /// use image::DynamicImage; - /// use veloren_common::assets::Asset; - /// - /// let my_image = DynamicImage::load_expect("core.ui.backgrounds.city"); - /// ``` - fn load_expect(specifier: &str) -> Arc - where - Self::Output: Send + Sync + 'static, - { - 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. - fn load_expect_cloned(specifier: &str) -> Self::Output - where - Self::Output: Clone + Send + Sync + 'static, - { - Self::load_expect(specifier).as_ref().clone() - } - - /// Load an asset while registering it to be watched and reloaded when it - /// changes - fn load_watched( - specifier: &str, - indicator: &mut watch::ReloadIndicator, - ) -> Result, Error> - where - Self::Output: Send + Sync + 'static, - { - let asset = Self::load(specifier)?; - - // Determine path to watch - let path = unpack_specifier(specifier); - let mut path_with_extension = None; - for ending in Self::ENDINGS { - let mut path = path.clone(); - path.set_extension(ending); - - if path.exists() { - path_with_extension = Some(path); - break; - } - } - - let owned_specifier = specifier.to_string(); - indicator.add( - path_with_extension - .ok_or_else(|| Error::NotFound(path.to_string_lossy().into_owned()))?, - move || { - if let Err(e) = reload::(&owned_specifier) { - error!(?e, ?owned_specifier, "Error reloading owned_specifier"); - } - }, - ); - - Ok(asset) - } -} - -impl Asset for DynamicImage { - const ENDINGS: &'static [&'static str] = &["png", "jpg"]; - - fn parse(mut buf_reader: BufReader, _specifier: &str) -> Result { - let mut buf = Vec::new(); - buf_reader.read_to_end(&mut buf)?; - image::load_from_memory(&buf).map_err(Error::parse_error) - } -} - -impl Asset for DotVoxData { - const ENDINGS: &'static [&'static str] = &["vox"]; - - fn parse(mut buf_reader: BufReader, _specifier: &str) -> Result { - let mut buf = Vec::new(); - buf_reader.read_to_end(&mut buf)?; - dot_vox::load_bytes(&buf).map_err(Error::parse_error) - } -} - -// Read a JSON file -impl Asset for Value { - const ENDINGS: &'static [&'static str] = &["json"]; - - fn parse(buf_reader: BufReader, _specifier: &str) -> Result { - serde_json::from_reader(buf_reader).map_err(Error::parse_error) - } -} - -/// Load from an arbitrary RON file. -pub struct Ron(pub PhantomData); - -impl Deserialize<'de>> Asset for Ron { - type Output = T; - - const ENDINGS: &'static [&'static str] = &["ron"]; - - fn parse(buf_reader: BufReader, _specifier: &str) -> Result { - ron::de::from_reader(buf_reader).map_err(Error::parse_error) - } -} - -/// Load from a specific asset path. -pub struct AssetWith { - pub asset: Arc, -} - -impl Clone for AssetWith { - fn clone(&self) -> Self { - Self { - asset: Arc::clone(&self.asset), - } - } -} - -impl AssetWith -where - T::Output: Send + Sync + 'static, -{ - #[inline] - pub fn load_watched(indicator: &mut watch::ReloadIndicator) -> Result { - T::load_watched(ASSET_PATH, indicator).map(|asset| Self { asset }) - } - - #[inline] - pub fn reload(&mut self) -> Result<(), Error> { - self.asset = T::load(ASSET_PATH)?; - Ok(()) - } -} - -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) - 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); - } - - // 3. Working path - if let Ok(path) = std::env::current_dir() { - paths.push(path); - } - - // 4. Cargo Workspace (e.g. local development) - // https://github.com/rust-lang/cargo/issues/3946#issuecomment-359619839 - if let Ok(Ok(path)) = std::env::var("CARGO_MANIFEST_DIR").map(|s| s.parse::()) { - paths.push(path.parent().unwrap().to_path_buf()); - paths.push(path); - } - - // 5. System paths - #[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 - }), - ); - }; -} - -/// Converts a specifier like "core.backgrounds.city" to -/// ".../veloren/assets/core/backgrounds/city". -fn unpack_specifier(specifier: &str) -> PathBuf { - let mut path = ASSETS_PATH.clone(); - path.push(specifier.replace(".", "/")); - path -} - -/// Loads a file based on the specifier and possible extensions -pub fn load_file(specifier: &str, endings: &[&str]) -> Result, Error> { - let path = unpack_specifier(specifier); - for ending in endings { - let mut path = path.clone(); - path.set_extension(ending); - - trace!(?path, "Trying to access"); - if let Ok(file) = File::open(path) { - return Ok(BufReader::new(file)); - } - } - - Err(Error::NotFound(path.to_string_lossy().into_owned())) -} - -/// Loads a file based on the specifier and possible extensions -pub fn load_file_glob(specifier: &str, endings: &[&str]) -> Result, Error> { - let path = unpack_specifier(specifier); - for ending in endings { - let mut path = path.clone(); - path.set_extension(ending); - - trace!(?path, "Trying to access"); - if let Ok(file) = File::open(path) { - return Ok(BufReader::new(file)); - } - } - - Err(Error::NotFound(path.to_string_lossy().into_owned())) -} - -/// Read directory from `veloren/assets/*` -pub fn read_dir(specifier: &str) -> Result { - let dir_name = unpack_specifier(specifier); - if dir_name.exists() { - Ok(fs::read_dir(dir_name).expect("`read_dir` failed.")) - } else { - Err(Error::NotFound(dir_name.to_string_lossy().into_owned())) - } -} - -// Finds all files matching the provided glob specifier - includes files from -// subdirectories -fn get_glob_matches(specifier: &str) -> Result, Error> { - let specifier = specifier.trim_end_matches(".*"); - read_dir(specifier).map(|dir| { - dir.filter_map(|direntry| { - direntry.ok().and_then(|dir_entry| { - if dir_entry.path().is_dir() { - let sub_dir_glob = format!( - "{}.{}.*", - specifier.to_string(), - dir_entry.file_name().to_string_lossy() - ); - Some(get_glob_matches(&sub_dir_glob).ok()?) - } else { - Some(vec![format!( - "{}.{}", - specifier, - dir_entry - .file_name() - .to_string_lossy() - .rsplitn(2, '.') - .last() - .map(|s| s.to_owned()) - .unwrap() - )]) - } - }) - }) - .flat_map(|x| x) - .collect::>() - }) -} diff --git a/common/src/assets/watch.rs b/common/src/assets/watch.rs deleted file mode 100644 index ed23ecc98d..0000000000 --- a/common/src/assets/watch.rs +++ /dev/null @@ -1,182 +0,0 @@ -use crossbeam_channel::{select, unbounded, Receiver, Sender}; -use lazy_static::lazy_static; -use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher as _}; -use std::{ - collections::HashMap, - path::PathBuf, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, Mutex, Weak, - }, - thread, -}; -use tracing::{error, warn}; - -type Handler = Box; - -lazy_static! { - static ref WATCHER_TX: Mutex)>> = - Mutex::new(Watcher::new().run()); -} - -// This will need to be adjusted when specifier mapping to asset location -// becomes more dynamic -struct Watcher { - watching: HashMap>)>, - watcher: RecommendedWatcher, - event_rx: Receiver>, -} -impl Watcher { - fn new() -> Self { - let (event_tx, event_rx) = unbounded(); - Watcher { - watching: HashMap::new(), - watcher: notify::Watcher::new_immediate(move |event| { - let _ = event_tx.send(event); - }) - .expect("Failed to create notify::Watcher"), - event_rx, - } - } - - fn watch(&mut self, path: PathBuf, handler: Handler, signal: Weak) { - match self.watching.get_mut(&path) { - Some((_, ref mut v)) => { - if !v.iter().any(|s| match (s.upgrade(), signal.upgrade()) { - (Some(arc1), Some(arc2)) => Arc::ptr_eq(&arc1, &arc2), - _ => false, - }) { - v.push(signal); - } - }, - None => { - if let Err(e) = self.watcher.watch(path.clone(), RecursiveMode::Recursive) { - warn!(?e, ?path, "Could not start watching file"); - return; - } - self.watching.insert(path, (handler, vec![signal])); - }, - } - } - - fn handle_event(&mut self, event: Event) { - if let Event { - kind: EventKind::Modify(_), - paths, - .. - } = event - { - for path in paths { - match self.watching.get_mut(&path) { - Some((reloader, ref mut signals)) => { - if !signals.is_empty() { - // Reload this file - reloader(); - - signals.retain(|signal| match signal.upgrade() { - Some(signal) => { - signal.store(true, Ordering::Release); - true - }, - None => false, - }); - } - // If there is no one to signal stop watching this path - if signals.is_empty() { - if let Err(err) = self.watcher.unwatch(&path) { - warn!("Error unwatching: {}", err); - } - self.watching.remove(&path); - } - }, - None => { - warn!( - "Watching {:#?} but there are no signals for this path. The path will \ - be unwatched.", - path - ); - if let Err(err) = self.watcher.unwatch(&path) { - warn!("Error unwatching: {}", err); - } - }, - } - } - } - } - - #[allow(clippy::drop_copy)] // TODO: Pending review in #587 - #[allow(clippy::single_match)] // TODO: Pending review in #587 - #[allow(clippy::zero_ptr)] // TODO: Pending review in #587 - fn run(mut self) -> Sender<(PathBuf, Handler, Weak)> { - let (watch_tx, watch_rx) = unbounded(); - - thread::spawn(move || { - loop { - select! { - recv(watch_rx) -> res => match res { - Ok((path, handler, signal)) => self.watch(path, handler, signal), - // Disconnected - Err(_) => (), - }, - recv(self.event_rx) -> res => match res { - Ok(Ok(event)) => self.handle_event(event), - // Notify Error - Ok(Err(e)) => error!(?e, "Notify error"), - // Disconnected - Err(_) => (), - }, - } - } - }); - - watch_tx - } -} - -pub struct ReloadIndicator { - reloaded: Arc, - // Paths that have already been added - paths: Vec, -} -impl ReloadIndicator { - #[allow(clippy::new_without_default)] // TODO: Pending review in #587 - pub fn new() -> Self { - Self { - reloaded: Arc::new(AtomicBool::new(false)), - paths: Vec::new(), - } - } - - pub fn add(&mut self, path: PathBuf, reloader: F) - where - F: 'static + Fn() + Send, - { - // Check to see if this was already added - if self.paths.iter().any(|p| *p == path) { - // Nothing else needs to be done - return; - } else { - self.paths.push(path.clone()); - }; - - if WATCHER_TX - .lock() - .unwrap() - .send((path, Box::new(reloader), Arc::downgrade(&self.reloaded))) - .is_err() - { - error!("Could not add. Asset watcher channel disconnected."); - } - } - - // Returns true if the watched file was changed - pub fn reloaded(&self) -> bool { - // Optimize for the common case by performing an initial relaxed read, avoiding - // the atomic write. - if self.reloaded.load(Ordering::Relaxed) { - self.reloaded.swap(false, Ordering::Acquire) - } else { - false - } - } -} diff --git a/common/src/cmd.rs b/common/src/cmd.rs index c77d51f3dc..c2a8fc63d6 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -151,8 +151,9 @@ lazy_static! { .iter() .map(|s| s.to_string()) .collect(); + /// TODO: Make this use hot-reloading static ref ENTITIES: Vec = { - let npc_names = &*npc::NPC_NAMES; + let npc_names = &*npc::NPC_NAMES.read(); npc::ALL_NPCS .iter() .map(|&npc| npc_names[npc].keyword.clone()) diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index 9294bb08d8..f3b57ce606 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -16,7 +16,7 @@ use arraygen::Arraygen; use serde::{Deserialize, Serialize}; use specs::{Component, FlaggedStorage}; use specs_idvs::IdvStorage; -use std::{fs::File, io::BufReader, time::Duration}; +use std::time::Duration; use vek::Vec3; #[derive(Copy, Clone, Hash, Eq, PartialEq, Debug, Serialize, Deserialize)] @@ -238,11 +238,9 @@ impl Default for CharacterAbility { } impl Asset for CharacterAbility { - const ENDINGS: &'static [&'static str] = &["ron"]; + type Loader = assets::RonLoader; - fn parse(buf_reader: BufReader, _specifier: &str) -> Result { - ron::de::from_reader(buf_reader).map_err(assets::Error::parse_error) - } + const EXTENSION: &'static str = "ron"; } impl CharacterAbility { diff --git a/common/src/comp/body.rs b/common/src/comp/body.rs index 44b30970b6..eee67542ec 100644 --- a/common/src/comp/body.rs +++ b/common/src/comp/body.rs @@ -20,7 +20,6 @@ use crate::{ use serde::{Deserialize, Serialize}; use specs::{Component, FlaggedStorage}; use specs_idvs::IdvStorage; -use std::{fs::File, io::BufReader}; use vek::*; make_case_elim!( @@ -123,15 +122,13 @@ impl<'a, BodyMeta, SpeciesMeta> core::ops::Index<&'a Body> for AllBodies serde::Deserialize<'de>, - SpeciesMeta: Send + Sync + for<'de> serde::Deserialize<'de>, + BodyMeta: Send + Sync + for<'de> serde::Deserialize<'de> + 'static, + SpeciesMeta: Send + Sync + for<'de> serde::Deserialize<'de> + 'static, > Asset for AllBodies { - const ENDINGS: &'static [&'static str] = &["json"]; + type Loader = assets::JsonLoader; - fn parse(buf_reader: BufReader, _specifier: &str) -> Result { - serde_json::de::from_reader(buf_reader).map_err(assets::Error::parse_error) - } + const EXTENSION: &'static str = "json"; } impl Body { diff --git a/common/src/comp/inventory/item/mod.rs b/common/src/comp/inventory/item/mod.rs index c05c49b72c..de3b26c707 100644 --- a/common/src/comp/inventory/item/mod.rs +++ b/common/src/comp/inventory/item/mod.rs @@ -5,7 +5,7 @@ pub mod tool; pub use tool::{AbilitySet, Hands, Tool, ToolKind, UniqueKind}; use crate::{ - assets::{self, Asset, Error}, + assets::{self, AssetExt, Error}, effect::Effect, lottery::Lottery, terrain::{Block, SpriteKind}, @@ -16,8 +16,6 @@ use serde::{Deserialize, Serialize}; use specs::{Component, FlaggedStorage}; use specs_idvs::IdvStorage; use std::{ - fs::File, - io::BufReader, num::{NonZeroU32, NonZeroU64}, sync::Arc, }; @@ -160,28 +158,58 @@ impl PartialEq for Item { } } -impl Asset for ItemDef { - const ENDINGS: &'static [&'static str] = &["ron"]; +impl assets::Compound for ItemDef { + fn load( + cache: &assets_manager::AssetCache, + specifier: &str, + ) -> Result { + let raw = cache.load_owned::(specifier)?; - fn parse(buf_reader: BufReader, specifier: &str) -> Result { - let item: Result = - ron::de::from_reader(buf_reader).map_err(Error::parse_error); + let RawItemDef { + name, + description, + kind, + quality, + } = raw; // Some commands like /give_item provide the asset specifier separated with \ // instead of . - let specifier = specifier.replace('\\', "."); + // + // TODO: This probably does not belong here + let item_definition_id = specifier.replace('\\', "."); - item.map(|item| ItemDef { - item_definition_id: specifier, - ..item + Ok(ItemDef { + item_definition_id, + name, + description, + kind, + quality, }) } } +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename = "ItemDef")] +struct RawItemDef { + name: String, + description: String, + kind: ItemKind, + quality: Quality, +} + +impl assets::Asset for RawItemDef { + type Loader = assets::RonLoader; + + const EXTENSION: &'static str = "ron"; +} + +#[derive(Debug)] +pub struct OperationFailure; + impl Item { // TODO: consider alternatives such as default abilities that can be added to a // loadout when no weapon is present - pub fn empty() -> Self { Item::new(ItemDef::load_expect("common.items.weapons.empty.empty")) } + pub fn empty() -> Self { Item::new_from_asset_expect("common.items.weapons.empty.empty") } pub fn new(inner_item: Arc) -> Self { Item { @@ -194,27 +222,28 @@ impl Item { /// Creates a new instance of an `Item` from the provided asset identifier /// Panics if the asset does not exist. pub fn new_from_asset_expect(asset_specifier: &str) -> Self { - let inner_item = ItemDef::load_expect(asset_specifier); + let inner_item = Arc::::load_expect_cloned(asset_specifier); Item::new(inner_item) } /// Creates a Vec containing one of each item that matches the provided /// asset glob pattern pub fn new_from_asset_glob(asset_glob: &str) -> Result, Error> { - let items = ItemDef::load_glob(asset_glob)?; + //let items = ItemDef::load_glob(asset_glob)?; - let result = items + let specifiers = assets::Directory::load(asset_glob)?; + + specifiers + .read() .iter() - .map(|item_def| Item::new(Arc::clone(item_def))) - .collect::>(); - - Ok(result) + .map(|spec| Self::new_from_asset(&spec)) + .collect() } /// Creates a new instance of an `Item from the provided asset identifier if /// it exists pub fn new_from_asset(asset: &str) -> Result { - let inner_item = ItemDef::load(asset)?; + let inner_item = Arc::::load_cloned(asset)?; Ok(Item::new(inner_item)) } @@ -254,30 +283,30 @@ impl Item { /// up by another player. pub fn put_in_world(&mut self) { self.reset_item_id() } - pub fn increase_amount(&mut self, increase_by: u32) -> Result<(), assets::Error> { + pub fn increase_amount(&mut self, increase_by: u32) -> Result<(), OperationFailure> { let amount = u32::from(self.amount); self.amount = amount .checked_add(increase_by) .and_then(NonZeroU32::new) - .ok_or(assets::Error::InvalidType)?; + .ok_or(OperationFailure)?; Ok(()) } - pub fn decrease_amount(&mut self, decrease_by: u32) -> Result<(), assets::Error> { + pub fn decrease_amount(&mut self, decrease_by: u32) -> Result<(), OperationFailure> { let amount = u32::from(self.amount); self.amount = amount .checked_sub(decrease_by) .and_then(NonZeroU32::new) - .ok_or(assets::Error::InvalidType)?; + .ok_or(OperationFailure)?; Ok(()) } - pub fn set_amount(&mut self, give_amount: u32) -> Result<(), assets::Error> { + pub fn set_amount(&mut self, give_amount: u32) -> Result<(), OperationFailure> { if give_amount == 1 || self.item_def.is_stackable() { - self.amount = NonZeroU32::new(give_amount).ok_or(assets::Error::InvalidType)?; + self.amount = NonZeroU32::new(give_amount).ok_or(OperationFailure)?; Ok(()) } else { - Err(assets::Error::InvalidType) + Err(OperationFailure) } } @@ -327,7 +356,8 @@ impl Item { 3 => "common.loot_tables.loot_table_armor_cloth", 4 => "common.loot_tables.loot_table_armor_heavy", _ => "common.loot_tables.loot_table_armor_misc", - }); + }) + .read(); chosen.choose() }, SpriteKind::ChestBurried => { @@ -336,7 +366,8 @@ impl Item { 2 => "common.loot_tables.loot_table_armor_light", 3 => "common.loot_tables.loot_table_armor_cloth", _ => "common.loot_tables.loot_table_armor_misc", - }); + }) + .read(); chosen.choose() }, SpriteKind::Mud => { @@ -345,14 +376,16 @@ impl Item { 1 => "common.loot_tables.loot_table_weapon_common", 2 => "common.loot_tables.loot_table_armor_misc", _ => "common.loot_tables.loot_table_rocks", - }); + }) + .read(); chosen.choose() }, SpriteKind::Crate => { chosen = Lottery::::load_expect(match rng.gen_range(0, 4) { 0 => "common.loot_tables.loot_table_crafting", _ => "common.loot_tables.loot_table_food", - }); + }) + .read(); chosen.choose() }, SpriteKind::Beehive => "common.items.crafting_ing.honey", diff --git a/common/src/comp/inventory/item/tool.rs b/common/src/comp/inventory/item/tool.rs index d1a32225ca..729281ad6a 100644 --- a/common/src/comp/inventory/item/tool.rs +++ b/common/src/comp/inventory/item/tool.rs @@ -7,7 +7,7 @@ use crate::{ }; use hashbrown::HashMap; use serde::{Deserialize, Serialize}; -use std::{fs::File, io::BufReader, time::Duration}; +use std::time::Duration; use tracing::error; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -107,7 +107,7 @@ pub struct AbilitySet { } impl AbilitySet { - pub fn modified_by_tool(self, tool: &Tool) -> Self { + fn modified_by_tool(self, tool: &Tool) -> Self { self.map(|a| a.adjusted_by_stats(tool.base_power(), tool.base_speed())) } } @@ -120,6 +120,14 @@ impl AbilitySet { skills: self.skills.into_iter().map(|x| f(x)).collect(), } } + + pub fn map_ref U>(&self, mut f: F) -> AbilitySet { + AbilitySet { + primary: f(&self.primary), + secondary: f(&self.secondary), + skills: self.skills.iter().map(|x| f(x)).collect(), + } + } } impl Default for AbilitySet { @@ -135,37 +143,33 @@ impl Default for AbilitySet { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AbilityMap(HashMap>); -impl Asset for AbilityMap { - const ENDINGS: &'static [&'static str] = &["ron"]; +impl Asset for AbilityMap { + type Loader = assets::RonLoader; - fn parse(buf_reader: BufReader, specifier: &str) -> Result { - ron::de::from_reader::, AbilityMap>(buf_reader) - .map(|map| { - AbilityMap( - map.0 - .into_iter() - .map(|(kind, set)| { - ( - kind, - set.map(|s| match CharacterAbility::load(&s) { - Ok(ability) => ability.as_ref().clone(), - Err(err) => { - error!( - ?err, - "Error loading CharacterAbility: {} for the ability \ - map: {} replacing with default", - s, - specifier - ); - CharacterAbility::default() - }, - }), - ) - }) - .collect(), - ) - }) - .map_err(assets::Error::parse_error) + const EXTENSION: &'static str = "ron"; +} + +impl assets::Compound for AbilityMap { + fn load( + cache: &assets_manager::AssetCache, + specifier: &str, + ) -> Result { + let manifest = cache.load::>(specifier)?.read(); + + Ok(AbilityMap( + manifest + .0 + .iter() + .map(|(kind, set)| { + ( + *kind, + // expect cannot fail because CharacterAbility always + // provides a default value in case of failure + set.map_ref(|s| cache.load_expect(&s).cloned()), + ) + }) + .collect(), + )) } } diff --git a/common/src/comp/inventory/slot.rs b/common/src/comp/inventory/slot.rs index 817aa930f1..1d55dff848 100644 --- a/common/src/comp/inventory/slot.rs +++ b/common/src/comp/inventory/slot.rs @@ -139,7 +139,7 @@ fn loadout_insert( /// /// ``` /// use veloren_common::{ -/// assets::Asset, +/// assets::AssetExt, /// comp::{ /// item::tool::AbilityMap, /// slot::{loadout_remove, EquipSlot}, @@ -263,7 +263,7 @@ pub fn swap( /// /// ``` /// use veloren_common::{ -/// assets::Asset, +/// assets::AssetExt, /// comp::{ /// item::tool::AbilityMap, /// slot::{equip, EquipSlot}, @@ -324,7 +324,7 @@ pub fn equip(slot: usize, inventory: &mut Inventory, loadout: &mut Loadout, map: /// /// ``` /// use veloren_common::{ -/// assets::Asset, +/// assets::AssetExt, /// comp::{ /// item::tool::AbilityMap, /// slot::{unequip, EquipSlot}, @@ -365,7 +365,7 @@ pub fn unequip( #[cfg(test)] mod tests { use super::*; - use crate::{comp::Item, LoadoutBuilder}; + use crate::{assets::AssetExt, comp::Item, LoadoutBuilder}; #[test] fn test_unequip_items_both_hands() { @@ -374,7 +374,6 @@ mod tests { amount: 0, }; - use crate::assets::Asset; let map = AbilityMap::load_expect_cloned("common.abilities.weapon_ability_manifest"); let sword = LoadoutBuilder::default_item_config_from_str( @@ -419,7 +418,6 @@ mod tests { let mut loadout = LoadoutBuilder::new().defaults().build(); - use crate::assets::Asset; let map = AbilityMap::load_expect_cloned("common.abilities.weapon_ability_manifest"); // We should start with the starting sandles @@ -444,7 +442,6 @@ mod tests { "common.items.armor.starter.sandals_0", )); - use crate::assets::Asset; let map = AbilityMap::load_expect_cloned("common.abilities.weapon_ability_manifest"); let mut loadout = LoadoutBuilder::new().defaults().build(); @@ -469,7 +466,6 @@ mod tests { #[test] fn test_loadout_remove() { - use crate::assets::Asset; let map = AbilityMap::load_expect_cloned("common.abilities.weapon_ability_manifest"); let sword = LoadoutBuilder::default_item_config_from_str( diff --git a/common/src/generation.rs b/common/src/generation.rs index c16131918b..7f163f8224 100644 --- a/common/src/generation.rs +++ b/common/src/generation.rs @@ -115,22 +115,23 @@ impl EntityInfo { } pub fn with_automatic_name(mut self) -> Self { + let npc_names = NPC_NAMES.read(); self.name = match &self.body { - Body::Humanoid(body) => Some(get_npc_name(&NPC_NAMES.humanoid, body.species)), + Body::Humanoid(body) => Some(get_npc_name(&npc_names.humanoid, body.species)), Body::QuadrupedMedium(body) => { - Some(get_npc_name(&NPC_NAMES.quadruped_medium, body.species)) + Some(get_npc_name(&npc_names.quadruped_medium, body.species)) }, - Body::BirdMedium(body) => Some(get_npc_name(&NPC_NAMES.bird_medium, body.species)), - Body::FishSmall(body) => Some(get_npc_name(&NPC_NAMES.fish_small, body.species)), - Body::FishMedium(body) => Some(get_npc_name(&NPC_NAMES.fish_medium, body.species)), - Body::Theropod(body) => Some(get_npc_name(&NPC_NAMES.theropod, body.species)), + Body::BirdMedium(body) => Some(get_npc_name(&npc_names.bird_medium, body.species)), + Body::FishSmall(body) => Some(get_npc_name(&npc_names.fish_small, body.species)), + Body::FishMedium(body) => Some(get_npc_name(&npc_names.fish_medium, body.species)), + Body::Theropod(body) => Some(get_npc_name(&npc_names.theropod, body.species)), Body::QuadrupedSmall(body) => { - Some(get_npc_name(&NPC_NAMES.quadruped_small, body.species)) + Some(get_npc_name(&npc_names.quadruped_small, body.species)) }, - Body::Dragon(body) => Some(get_npc_name(&NPC_NAMES.dragon, body.species)), - Body::QuadrupedLow(body) => Some(get_npc_name(&NPC_NAMES.quadruped_low, body.species)), - Body::Golem(body) => Some(get_npc_name(&NPC_NAMES.golem, body.species)), - Body::BipedLarge(body) => Some(get_npc_name(&NPC_NAMES.biped_large, body.species)), + Body::Dragon(body) => Some(get_npc_name(&npc_names.dragon, body.species)), + Body::QuadrupedLow(body) => Some(get_npc_name(&npc_names.quadruped_low, body.species)), + Body::Golem(body) => Some(get_npc_name(&npc_names.golem, body.species)), + Body::BipedLarge(body) => Some(get_npc_name(&npc_names.biped_large, body.species)), _ => None, } .map(|s| { diff --git a/common/src/loadout_builder.rs b/common/src/loadout_builder.rs index 86d96b98ba..a6b8c75813 100644 --- a/common/src/loadout_builder.rs +++ b/common/src/loadout_builder.rs @@ -11,7 +11,7 @@ use rand::Rng; /// /// ``` /// use veloren_common::{ -/// assets::Asset, +/// assets::AssetExt, /// comp::item::tool::AbilityMap, /// LoadoutBuilder, /// }; diff --git a/common/src/lottery.rs b/common/src/lottery.rs index 2b04b7231d..4aaa7db851 100644 --- a/common/src/lottery.rs +++ b/common/src/lottery.rs @@ -1,7 +1,6 @@ -use crate::assets::{self, Asset}; +use crate::assets; use rand::prelude::*; use serde::{de::DeserializeOwned, Deserialize}; -use std::{fs::File, io::BufReader}; #[derive(Clone, Debug, PartialEq, Deserialize)] pub struct Lottery { @@ -9,28 +8,26 @@ pub struct Lottery { total: f32, } -impl Asset for Lottery { - const ENDINGS: &'static [&'static str] = &["ron"]; +impl assets::Asset for Lottery { + type Loader = assets::LoadFrom, assets::RonLoader>; - fn parse(buf_reader: BufReader, _specifier: &str) -> Result { - ron::de::from_reader::, Vec<(f32, T)>>(buf_reader) - .map(|items| Lottery::from_rates(items.into_iter())) - .map_err(assets::Error::parse_error) + const EXTENSION: &'static str = "ron"; +} + +impl From> for Lottery { + fn from(mut items: Vec<(f32, T)>) -> Lottery { + let mut total = 0.0; + + for (rate, _) in &mut items { + total += *rate; + *rate = total - *rate; + } + + Self { items, total } } } impl Lottery { - pub fn from_rates(items: impl Iterator) -> Self { - let mut total = 0.0; - let items = items - .map(|(rate, item)| { - total += rate; - (total - rate, item) - }) - .collect(); - Self { items, total } - } - pub fn choose_seeded(&self, seed: u32) -> &T { let x = ((seed % 65536) as f32 / 65536.0) * self.total; &self.items[self @@ -48,13 +45,13 @@ impl Lottery { #[cfg(test)] mod tests { use super::*; - use crate::{assets::Asset, comp::Item}; + use crate::{assets::AssetExt, comp::Item}; #[test] fn test_loot_table() { let test = Lottery::::load_expect("common.loot_tables.loot_table"); - for (_, item_asset_specifier) in test.iter() { + for (_, item_asset_specifier) in test.read().iter() { assert!( Item::new_from_asset(item_asset_specifier).is_ok(), "Invalid loot table item '{}'", diff --git a/common/src/npc.rs b/common/src/npc.rs index ca8d58f2f6..87f4e72fc7 100644 --- a/common/src/npc.rs +++ b/common/src/npc.rs @@ -1,11 +1,11 @@ use crate::{ - assets::Asset, + assets::{AssetExt, AssetHandle}, comp::{self, AllBodies, Body}, }; use lazy_static::lazy_static; use rand::seq::SliceRandom; use serde::Deserialize; -use std::{str::FromStr, sync::Arc}; +use std::str::FromStr; #[derive(Clone, Copy, PartialEq)] pub enum NpcKind { @@ -67,14 +67,14 @@ pub struct SpeciesNames { pub type NpcNames = AllBodies; lazy_static! { - pub static ref NPC_NAMES: Arc = NpcNames::load_expect("common.npc_names"); + pub static ref NPC_NAMES: AssetHandle = NpcNames::load_expect("common.npc_names"); } impl FromStr for NpcKind { type Err = (); - fn from_str(s: &str) -> Result { - let npc_names = &*NPC_NAMES; + fn from_str(s: &str) -> Result { + let npc_names = &*NPC_NAMES.read(); ALL_NPCS .iter() .copied() @@ -83,11 +83,15 @@ impl FromStr for NpcKind { } } -pub fn get_npc_name(npc_type: NpcKind) -> &'static str { - let BodyNames { keyword, names } = &NPC_NAMES[npc_type]; +pub fn get_npc_name(npc_type: NpcKind) -> String { + let npc_names = NPC_NAMES.read(); + let BodyNames { keyword, names } = &npc_names[npc_type]; // If no pretty name is found, fall back to the keyword. - names.choose(&mut rand::thread_rng()).unwrap_or(keyword) + names + .choose(&mut rand::thread_rng()) + .unwrap_or(keyword) + .clone() } /// Randomly generates a body associated with this NPC kind. @@ -164,7 +168,7 @@ impl NpcBody { ) }) } - let npc_names = &NPC_NAMES; + let npc_names = &*NPC_NAMES.read(); // First, parse npc kind names. NpcKind::from_str(s) .map(|kind| NpcBody(kind, Box::new(move || kind_to_body(kind)))) diff --git a/common/src/recipe.rs b/common/src/recipe.rs index e6aa0936e0..24ae29396f 100644 --- a/common/src/recipe.rs +++ b/common/src/recipe.rs @@ -1,10 +1,10 @@ use crate::{ - assets::{self, Asset}, + assets::{self, AssetExt, AssetHandle}, comp::{item::ItemDef, Inventory, Item}, }; use hashbrown::HashMap; use serde::{Deserialize, Serialize}; -use std::{fs::File, io::BufReader, sync::Arc}; +use std::sync::Arc; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Recipe { @@ -65,36 +65,44 @@ impl RecipeBook { } } -impl Asset for RecipeBook { - const ENDINGS: &'static [&'static str] = &["ron"]; +#[derive(Deserialize)] +#[serde(transparent)] +#[allow(clippy::type_complexity)] +struct RawRecipeBook(HashMap)>); - fn parse(buf_reader: BufReader, _specifier: &str) -> Result { - ron::de::from_reader::< - BufReader, - HashMap)>, - >(buf_reader) - .map_err(assets::Error::parse_error) - .and_then(|recipes| { - Ok(RecipeBook { - recipes: recipes - .into_iter() - .map::, _>( - |(name, ((output, amount), inputs))| { - Ok((name, Recipe { - output: (ItemDef::load(&output)?, amount), - inputs: inputs - .into_iter() - .map::, u32), assets::Error>, _>( - |(name, amount)| Ok((ItemDef::load(&name)?, amount)), - ) - .collect::>()?, - })) - }, - ) - .collect::>()?, +impl assets::Asset for RawRecipeBook { + type Loader = assets::RonLoader; + + const EXTENSION: &'static str = "ron"; +} + +impl assets::Compound for RecipeBook { + fn load( + cache: &assets_manager::AssetCache, + specifier: &str, + ) -> Result { + #[inline] + fn load_item_def(spec: &(String, u32)) -> Result<(Arc, u32), assets::Error> { + let def = Arc::::load_cloned(&spec.0)?; + Ok((def, spec.1)) + } + + let raw = cache.load::(specifier)?.read(); + + let recipes = raw + .0 + .iter() + .map(|(name, (output, inputs))| { + let inputs = inputs.iter().map(load_item_def).collect::>()?; + let output = load_item_def(output)?; + Ok((name.clone(), Recipe { inputs, output })) }) - }) + .collect::>()?; + + Ok(RecipeBook { recipes }) } } -pub fn default_recipe_book() -> Arc { RecipeBook::load_expect("common.recipe_book") } +pub fn default_recipe_book() -> AssetHandle { + RecipeBook::load_expect("common.recipe_book") +} diff --git a/common/src/terrain/mod.rs b/common/src/terrain/mod.rs index d1c478381c..a0f6bf7c46 100644 --- a/common/src/terrain/mod.rs +++ b/common/src/terrain/mod.rs @@ -13,7 +13,7 @@ pub use self::{ map::MapSizeLg, site::SitesKind, sprite::SpriteKind, - structure::Structure, + structure::{Structure, StructuresGroup}, }; use roots::find_roots_cubic; use serde::{Deserialize, Serialize}; diff --git a/common/src/terrain/structure.rs b/common/src/terrain/structure.rs index 34905ddd96..57235240c5 100644 --- a/common/src/terrain/structure.rs +++ b/common/src/terrain/structure.rs @@ -1,13 +1,12 @@ use super::BlockKind; use crate::{ - assets::{self, Asset, Ron}, + assets::{self, AssetExt, AssetHandle, DotVoxAsset, Error}, make_case_elim, vol::{BaseVol, ReadVol, SizedVol, WriteVol}, volumes::dyna::{Dyna, DynaError}, }; -use dot_vox::DotVoxData; use serde::Deserialize; -use std::{fs::File, io::BufReader, sync::Arc}; +use std::sync::Arc; use vek::*; make_case_elim!( @@ -40,20 +39,49 @@ pub enum StructureError {} #[derive(Clone)] pub struct Structure { center: Vec3, + base: Arc, +} + +struct BaseStructure { vol: Dyna, empty: StructureBlock, default_kind: BlockKind, } +pub struct StructuresGroup(Vec); + +impl std::ops::Deref for StructuresGroup { + type Target = [Structure]; + + fn deref(&self) -> &[Structure] { &self.0 } +} + +impl assets::Compound for StructuresGroup { + fn load( + cache: &assets_manager::AssetCache, + specifier: &str, + ) -> Result { + let specs = cache.load::(specifier)?.read(); + + Ok(StructuresGroup( + specs + .0 + .iter() + .map(|sp| { + let base = cache.load::>(&sp.specifier)?.cloned(); + Ok(Structure { + center: Vec3::from(sp.center), + base, + }) + }) + .collect::>()?, + )) + } +} + impl Structure { - pub fn load_group(specifier: &str) -> Vec> { - let spec = StructuresSpec::load_expect(&["world.manifests.", specifier].concat()); - spec.iter() - .map(|sp| { - Structure::load_map(&sp.specifier[..], |s| s.with_center(Vec3::from(sp.center))) - .unwrap() - }) - .collect() + pub fn load_group(specifier: &str) -> AssetHandle { + StructuresGroup::load_expect(&["world.manifests.", specifier].concat()) } pub fn with_center(mut self, center: Vec3) -> Self { @@ -61,19 +89,14 @@ impl Structure { self } - pub fn with_default_kind(mut self, kind: BlockKind) -> Self { - self.default_kind = kind; - self - } - pub fn get_bounds(&self) -> Aabb { Aabb { min: -self.center, - max: self.vol.size().map(|e| e as i32) - self.center, + max: self.base.vol.size().map(|e| e as i32) - self.center, } } - pub fn default_kind(&self) -> BlockKind { self.default_kind } + pub fn default_kind(&self) -> BlockKind { self.base.default_kind } } impl BaseVol for Structure { @@ -84,18 +107,20 @@ impl BaseVol for Structure { impl ReadVol for Structure { #[inline(always)] fn get(&self, pos: Vec3) -> Result<&Self::Vox, StructureError> { - match self.vol.get(pos + self.center) { + match self.base.vol.get(pos + self.center) { Ok(block) => Ok(block), - Err(DynaError::OutOfBounds) => Ok(&self.empty), + Err(DynaError::OutOfBounds) => Ok(&self.base.empty), } } } -impl Asset for Structure { - const ENDINGS: &'static [&'static str] = &["vox"]; - - fn parse(buf_reader: BufReader, specifier: &str) -> Result { - let dot_vox_data = DotVoxData::parse(buf_reader, specifier)?; +impl assets::Compound for BaseStructure { + fn load( + cache: &assets_manager::AssetCache, + specifier: &str, + ) -> Result { + let dot_vox_data = cache.load::(specifier)?.read(); + let dot_vox_data = &dot_vox_data.0; if let Some(model) = dot_vox_data.models.get(0) { let palette = dot_vox_data @@ -138,15 +163,13 @@ impl Asset for Structure { let _ = vol.set(Vec3::new(voxel.x, voxel.y, voxel.z).map(i32::from), block); } - Ok(Structure { - center: Vec3::zero(), + Ok(BaseStructure { vol, empty: StructureBlock::None, default_kind: BlockKind::Misc, }) } else { - Ok(Self { - center: Vec3::zero(), + Ok(BaseStructure { vol: Dyna::filled(Vec3::zero(), StructureBlock::None, ()), empty: StructureBlock::None, default_kind: BlockKind::Misc, @@ -161,4 +184,11 @@ struct StructureSpec { center: [i32; 3], } -type StructuresSpec = Ron>; +#[derive(Deserialize)] +struct StructuresGroupSpec(Vec); + +impl assets::Asset for StructuresGroupSpec { + type Loader = assets::RonLoader; + + const EXTENSION: &'static str = "ron"; +} diff --git a/server/src/cmd.rs b/server/src/cmd.rs index d2eb700ffe..8c42ee1b49 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -777,7 +777,7 @@ fn handle_spawn( .state .create_npc( pos, - comp::Stats::new(get_npc_name(id).into(), body), + comp::Stats::new(get_npc_name(id), body), comp::Health::new(body, 1), loadout, body, diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index c91d7a5dcc..c9c2bf81f9 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -5,7 +5,7 @@ use crate::{ Server, SpawnPoint, StateExt, }; use common::{ - assets::Asset, + assets::AssetExt, comp::{ self, aura, buff, chat::{KillSource, KillType}, @@ -354,7 +354,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc let item = { let mut item_drops = state.ecs().write_storage::(); item_drops.remove(entity).map_or_else( - || Item::new_from_asset_expect(lottery().choose()), + || Item::new_from_asset_expect(lottery().read().choose()), |item_drop| item_drop.0, ) }; diff --git a/server/src/events/inventory_manip.rs b/server/src/events/inventory_manip.rs index 85e7beea5a..1cdc97c897 100644 --- a/server/src/events/inventory_manip.rs +++ b/server/src/events/inventory_manip.rs @@ -446,7 +446,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv .write_storage::() .get_mut(entity) { - let recipe_book = default_recipe_book(); + let recipe_book = default_recipe_book().read(); let craft_result = recipe_book.get(&recipe).and_then(|r| r.perform(inv).ok()); // FIXME: We should really require the drop and write to be atomic! diff --git a/server/src/lib.rs b/server/src/lib.rs index 91c6667290..e7d3d107b4 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -47,7 +47,7 @@ use crate::{ sys::sentinel::{DeletedEntities, TrackedComps}, }; use common::{ - assets::Asset, + assets::AssetExt, cmd::ChatCommand, comp, event::{EventBus, ServerEvent}, @@ -939,7 +939,7 @@ impl Server { max_group_size: self.settings().max_player_group_size, client_timeout: self.settings().client_timeout, world_map: self.map.clone(), - recipe_book: (&*default_recipe_book()).clone(), + recipe_book: default_recipe_book().cloned(), ability_map: (&*self .state .ecs() diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index ca3b0c357d..8e1ed95b41 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -145,11 +145,13 @@ impl<'a> System<'a> for Sys { if entity.is_giant { if rand::random::() < 0.65 && entity.alignment != Alignment::Enemy { let body_new = comp::humanoid::Body::random(); + let npc_names = NPC_NAMES.read(); + body = comp::Body::Humanoid(body_new); stats = comp::Stats::new( format!( "Gentle Giant {}", - get_npc_name(&NPC_NAMES.humanoid, body_new.species) + get_npc_name(&npc_names.humanoid, body_new.species) ), body, ); diff --git a/voxygen/src/audio/ambient.rs b/voxygen/src/audio/ambient.rs index d8bb9a7c17..c8dfe4c907 100644 --- a/voxygen/src/audio/ambient.rs +++ b/voxygen/src/audio/ambient.rs @@ -4,7 +4,10 @@ use crate::{ scene::Camera, }; use client::Client; -use common::{assets, vol::ReadVol}; +use common::{ + assets::{self, AssetExt, AssetHandle}, + vol::ReadVol, +}; use common_sys::state::State; use serde::Deserialize; use std::time::Instant; @@ -27,7 +30,7 @@ pub struct AmbientItem { } pub struct AmbientMgr { - soundtrack: AmbientCollection, + soundtrack: AssetHandle, began_playing: Instant, next_track_change: f32, volume: f32, @@ -56,7 +59,7 @@ impl AmbientMgr { client: &Client, camera: &Camera, ) { - if audio.sfx_enabled() && !self.soundtrack.tracks.is_empty() { + if audio.sfx_enabled() && !self.soundtrack.read().tracks.is_empty() { let focus_off = camera.get_focus_pos().map(f32::trunc); let cam_pos = camera.dependents().cam_pos + focus_off; @@ -107,8 +110,8 @@ impl AmbientMgr { // Right now there is only wind non-positional sfx so it is always // selected. Modify this variable assignment when adding other non- // positional sfx - let track = &self - .soundtrack + let soundtrack = self.soundtrack.read(); + let track = &soundtrack .tracks .iter() .find(|track| track.tag == AmbientChannelTag::Wind); @@ -123,27 +126,23 @@ impl AmbientMgr { } } - fn load_soundtrack_items() -> AmbientCollection { - match assets::load_file("voxygen.audio.ambient", &["ron"]) { - Ok(file) => match ron::de::from_reader(file) { - Ok(config) => config, - Err(error) => { - warn!( - "Error parsing music config file, music will not be available: {}", - format!("{:#?}", error) - ); - - AmbientCollection::default() - }, - }, - Err(error) => { - warn!( - "Error reading music config file, music will not be available: {}", - format!("{:#?}", error) - ); - - AmbientCollection::default() - }, - } + fn load_soundtrack_items() -> AssetHandle { + // Cannot fail: A default value is always provided + AmbientCollection::load_expect("voxygen.audio.ambient") + } +} + +impl assets::Asset for AmbientCollection { + type Loader = assets::RonLoader; + + const EXTENSION: &'static str = "ron"; + + fn default_value(_: &str, error: assets::Error) -> Result { + warn!( + "Error reading music config file, music will not be available: {:#?}", + error + ); + + Ok(AmbientCollection::default()) } } diff --git a/voxygen/src/audio/mod.rs b/voxygen/src/audio/mod.rs index 20b78c4429..0d6479b7ac 100644 --- a/voxygen/src/audio/mod.rs +++ b/voxygen/src/audio/mod.rs @@ -10,12 +10,12 @@ pub mod soundcache; use channel::{AmbientChannel, AmbientChannelTag, MusicChannel, MusicChannelTag, SfxChannel}; use fader::Fader; use sfx::{SfxEvent, SfxTriggerItem}; -use soundcache::SoundCache; +use soundcache::{OggSound, WavSound}; use std::time::Duration; use tracing::debug; -use common::assets; -use rodio::{source::Source, Decoder, OutputStream, OutputStreamHandle, StreamError}; +use common::assets::AssetExt; +use rodio::{source::Source, OutputStream, OutputStreamHandle, StreamError}; use vek::*; #[derive(Default, Clone)] @@ -38,7 +38,6 @@ pub struct AudioFrontend { //pub audio_device: Option, pub stream: Option, audio_stream: Option, - sound_cache: SoundCache, music_channels: Vec, ambient_channels: Vec, @@ -76,7 +75,6 @@ impl AudioFrontend { //audio_device, stream, audio_stream, - sound_cache: SoundCache::default(), music_channels: Vec::new(), sfx_channels, ambient_channels: Vec::new(), @@ -95,7 +93,6 @@ impl AudioFrontend { //audio_device: None, stream: None, audio_stream: None, - sound_cache: SoundCache::default(), music_channels: Vec::new(), sfx_channels: Vec::new(), ambient_channels: Vec::new(), @@ -231,9 +228,9 @@ impl AudioFrontend { /// Play (once) an sfx file by file path at the give position and volume pub fn play_sfx(&mut self, sound: &str, pos: Vec3, vol: Option) { if self.audio_stream.is_some() { - let sound = self - .sound_cache - .load_sound(sound) + let sound = WavSound::load_expect(sound) + .cloned() + .decoder() .amplify(vol.unwrap_or(1.0)); let listener = self.listener.clone(); @@ -250,9 +247,9 @@ impl AudioFrontend { /// being underwater pub fn play_underwater_sfx(&mut self, sound: &str, pos: Vec3, vol: Option) { if self.audio_stream.is_some() { - let sound = self - .sound_cache - .load_sound(sound) + let sound = WavSound::load_expect(sound) + .cloned() + .decoder() .amplify(vol.unwrap_or(1.0)); let listener = self.listener.clone(); @@ -272,9 +269,7 @@ impl AudioFrontend { ) { if self.audio_stream.is_some() { if let Some(channel) = self.get_ambient_channel(channel_tag, volume_multiplier) { - let file = assets::load_file(&sound, &["ogg"]).expect("Failed to load sound"); - let sound = Decoder::new(file).expect("Failed to decode sound"); - + let sound = OggSound::load_expect(sound).cloned().decoder(); channel.play(sound); } } @@ -326,9 +321,7 @@ impl AudioFrontend { fn play_music(&mut self, sound: &str, channel_tag: MusicChannelTag) { if self.music_enabled() { if let Some(channel) = self.get_music_channel(channel_tag) { - let file = assets::load_file(&sound, &["ogg"]).expect("Failed to load sound"); - let sound = Decoder::new(file).expect("Failed to decode sound"); - + let sound = OggSound::load_expect(sound).cloned().decoder(); channel.play(sound, channel_tag); } } diff --git a/voxygen/src/audio/music.rs b/voxygen/src/audio/music.rs index af3d6c9b5d..e3d5b7ec09 100644 --- a/voxygen/src/audio/music.rs +++ b/voxygen/src/audio/music.rs @@ -45,7 +45,7 @@ use crate::audio::{AudioFrontend, MusicChannelTag}; use client::Client; use common::{ - assets, + assets::{self, AssetExt, AssetHandle}, terrain::{BiomeKind, SitesKind}, }; use common_sys::state::State; @@ -103,7 +103,7 @@ enum PlayState { /// Provides methods to control music playback pub struct MusicMgr { /// Collection of all the tracks - soundtrack: SoundtrackCollection, + soundtrack: AssetHandle, /// Instant at which the current track began playing began_playing: Instant, /// Time until the next track should be played @@ -139,7 +139,7 @@ impl MusicMgr { //} if audio.music_enabled() - && !self.soundtrack.tracks.is_empty() + && !self.soundtrack.read().tracks.is_empty() && self.began_playing.elapsed().as_secs_f32() > self.next_track_change { self.play_random_track(audio, state, client); @@ -158,8 +158,8 @@ impl MusicMgr { let current_site = client.current_site(); // Filters out tracks not matching the timing, site, and biome - let maybe_tracks = self - .soundtrack + let soundtrack = self.soundtrack.read(); + let maybe_tracks = soundtrack .tracks .iter() .filter(|track| { @@ -225,27 +225,23 @@ impl MusicMgr { } } - fn load_soundtrack_items() -> SoundtrackCollection { - match assets::load_file("voxygen.audio.soundtrack", &["ron"]) { - Ok(file) => match ron::de::from_reader(file) { - Ok(config) => config, - Err(error) => { - warn!( - "Error parsing music config file, music will not be available: {}", - format!("{:#?}", error) - ); - - SoundtrackCollection::default() - }, - }, - Err(error) => { - warn!( - "Error reading music config file, music will not be available: {}", - format!("{:#?}", error) - ); - - SoundtrackCollection::default() - }, - } + fn load_soundtrack_items() -> AssetHandle { + // Cannot fail: A default value is always provided + SoundtrackCollection::load_expect("voxygen.audio.soundtrack") + } +} + +impl assets::Asset for SoundtrackCollection { + type Loader = assets::RonLoader; + + const EXTENSION: &'static str = "ron"; + + fn default_value(_: &str, error: assets::Error) -> Result { + warn!( + "Error reading music config file, music will not be available: {:#?}", + error + ); + + Ok(SoundtrackCollection::default()) } } diff --git a/voxygen/src/audio/sfx/mod.rs b/voxygen/src/audio/sfx/mod.rs index a706b1f4e7..de1e1bdc26 100644 --- a/voxygen/src/audio/sfx/mod.rs +++ b/voxygen/src/audio/sfx/mod.rs @@ -89,7 +89,7 @@ use crate::{ use client::Client; use common::{ - assets, + assets::{self, AssetExt, AssetHandle}, comp::{ item::{ItemKind, ToolKind}, object, Body, CharacterAbilityType, InventoryUpdateEvent, @@ -237,7 +237,9 @@ impl SfxTriggers { } pub struct SfxMgr { - pub triggers: SfxTriggers, + /// This is an `AssetHandle` so it is reloaded automatically + /// when the manifest is edited. + pub triggers: AssetHandle, event_mapper: SfxEventMapper, } @@ -273,12 +275,14 @@ impl SfxMgr { // same direction as the camera audio.set_listener_pos(cam_pos, camera.dependents().cam_dir); + let triggers = self.triggers.read(); + self.event_mapper.maintain( audio, state, player_entity, camera, - &self.triggers, + &triggers, terrain, client, ); @@ -357,27 +361,23 @@ impl SfxMgr { } } - fn load_sfx_items() -> SfxTriggers { - match assets::load_file("voxygen.audio.sfx", &["ron"]) { - Ok(file) => match ron::de::from_reader(file) { - Ok(config) => config, - Err(error) => { - warn!( - "Error parsing sfx config file, sfx will not be available: {}", - format!("{:#?}", error) - ); - - SfxTriggers::default() - }, - }, - Err(error) => { - warn!( - "Error reading sfx config file, sfx will not be available: {}", - format!("{:#?}", error) - ); - - SfxTriggers::default() - }, - } + fn load_sfx_items() -> AssetHandle { + // Cannot fail: A default value is always provided + SfxTriggers::load_expect("voxygen.audio.sfx") + } +} + +impl assets::Asset for SfxTriggers { + type Loader = assets::RonLoader; + + const EXTENSION: &'static str = "ron"; + + fn default_value(_: &str, error: assets::Error) -> Result { + warn!( + "Error reading sfx config file, sfx will not be available: {:#?}", + error + ); + + Ok(SfxTriggers::default()) } } diff --git a/voxygen/src/audio/soundcache.rs b/voxygen/src/audio/soundcache.rs index ea92f3e6d3..943b584fa4 100644 --- a/voxygen/src/audio/soundcache.rs +++ b/voxygen/src/audio/soundcache.rs @@ -1,63 +1,84 @@ //! Handles caching and retrieval of decoded `.wav` sfx sound data, eliminating //! the need to decode files on each playback use common::assets; -use hashbrown::HashMap; -use std::{convert::AsRef, io, io::Read, sync::Arc}; +use std::{borrow::Cow, io, sync::Arc}; use tracing::warn; // Implementation of sound taken from this github issue: // https://github.com/RustAudio/rodio/issues/141 -pub struct Sound(Arc>); +#[derive(Clone)] +pub struct WavSound(Arc>); -impl AsRef<[u8]> for Sound { +impl AsRef<[u8]> for WavSound { fn as_ref(&self) -> &[u8] { &self.0 } } +pub struct SoundLoader; + +impl assets::Loader for SoundLoader { + fn load(content: Cow<[u8]>, _: &str) -> Result { + let arc = Arc::new(content.into_owned()); + Ok(WavSound(arc)) + } +} + +impl assets::Asset for WavSound { + type Loader = SoundLoader; + + const EXTENSION: &'static str = "wav"; + + fn default_value(specifier: &str, error: assets::Error) -> Result { + warn!(?specifier, ?error, "Failed to load sound"); + + Ok(WavSound::empty()) + } +} + /// Wrapper for decoded audio data -impl Sound { - pub fn load(filename: &str) -> Result { - let mut file = assets::load_file(filename, &["wav"])?; - let mut buf = Vec::new(); - file.read_to_end(&mut buf)?; - Ok(Sound(Arc::new(buf))) +impl WavSound { + pub fn decoder(self) -> rodio::Decoder> { + let cursor = io::Cursor::new(self); + rodio::Decoder::new(cursor).unwrap() } - pub fn cursor(&self) -> io::Cursor { io::Cursor::new(Sound(Arc::clone(&self.0))) } - - pub fn decoder(&self) -> rodio::Decoder> { - rodio::Decoder::new(self.cursor()).unwrap() - } - - /// Returns a `Sound` containing empty .wav data. This intentionally doesn't - /// load from the filesystem so we have a reliable fallback when there - /// is a failure to read a file. + /// Returns a `WavSound` containing empty .wav data. This intentionally + /// doesn't load from the filesystem so we have a reliable fallback when + /// there is a failure to read a file. /// /// The data below is the result of passing a very short, silent .wav file /// to `Sound::load()`. - pub fn empty() -> Sound { - Sound(Arc::new(vec![ + pub fn empty() -> WavSound { + WavSound(Arc::new(vec![ 82, 73, 70, 70, 40, 0, 0, 0, 87, 65, 86, 69, 102, 109, 116, 32, 16, 0, 0, 0, 1, 0, 1, 0, 68, 172, 0, 0, 136, 88, 1, 0, 2, 0, 16, 0, 100, 97, 116, 97, 4, 0, 0, 0, 0, 0, 0, 0, ])) } } -#[derive(Default)] -pub struct SoundCache { - sounds: HashMap, +#[derive(Clone)] +pub struct OggSound(Arc>); + +impl AsRef<[u8]> for OggSound { + fn as_ref(&self) -> &[u8] { &self.0 } } -impl SoundCache { - pub fn load_sound(&mut self, name: &str) -> rodio::Decoder> { - self.sounds - .entry(name.to_string()) - .or_insert_with(|| { - Sound::load(name).unwrap_or_else(|_| { - warn!(?name, "SoundCache: Failed to load sound"); - - Sound::empty() - }) - }) - .decoder() +impl assets::Loader for SoundLoader { + fn load(content: Cow<[u8]>, _: &str) -> Result { + let arc = Arc::new(content.into_owned()); + Ok(OggSound(arc)) + } +} + +impl assets::Asset for OggSound { + type Loader = SoundLoader; + + const EXTENSION: &'static str = "ogg"; +} + +/// Wrapper for decoded audio data +impl OggSound { + pub fn decoder(self) -> rodio::Decoder> { + let cursor = io::Cursor::new(self); + rodio::Decoder::new(cursor).unwrap() } } diff --git a/voxygen/src/hud/item_imgs.rs b/voxygen/src/hud/item_imgs.rs index 9ec179e3a0..dadfc98965 100644 --- a/voxygen/src/hud/item_imgs.rs +++ b/voxygen/src/hud/item_imgs.rs @@ -1,6 +1,6 @@ use crate::ui::{Graphic, SampleStrat, Transform, Ui}; use common::{ - assets::{self, watch::ReloadIndicator, Asset}, + assets::{self, AssetExt, AssetHandle, DotVoxAsset}, comp::item::{ armor::{Armor, ArmorKind}, Glider, ItemDesc, ItemKind, Lantern, Throwable, Utility, @@ -8,11 +8,10 @@ use common::{ figure::Segment, }; use conrod_core::image::Id; -use dot_vox::DotVoxData; use hashbrown::HashMap; use image::DynamicImage; use serde::{Deserialize, Serialize}; -use std::{fs::File, io::BufReader, sync::Arc}; +use std::sync::Arc; use tracing::{error, warn}; use vek::*; @@ -84,37 +83,35 @@ impl ImageSpec { } #[derive(Serialize, Deserialize)] struct ItemImagesSpec(HashMap); -impl Asset for ItemImagesSpec { - const ENDINGS: &'static [&'static str] = &["ron"]; +impl assets::Asset for ItemImagesSpec { + type Loader = assets::RonLoader; - fn parse(buf_reader: BufReader, _specifier: &str) -> Result { - ron::de::from_reader(buf_reader).map_err(assets::Error::parse_error) - } + const EXTENSION: &'static str = "ron"; } // TODO: when there are more images don't load them all into memory pub struct ItemImgs { map: HashMap, - indicator: ReloadIndicator, + manifest: AssetHandle, not_found: Id, } + impl ItemImgs { pub fn new(ui: &mut Ui, not_found: Id) -> Self { - let mut indicator = ReloadIndicator::new(); - Self { - map: ItemImagesSpec::load_watched( - "voxygen.item_image_manifest", - &mut indicator, - ) - .expect("Unable to load item image manifest") + let manifest = ItemImagesSpec::load_expect("voxygen.item_image_manifest"); + let map = manifest + .read() .0 .iter() // TODO: what if multiple kinds map to the same image, it would be nice to use the same // image id for both, although this does interfere with the current hot-reloading // strategy .map(|(kind, spec)| (kind.clone(), ui.add_graphic(spec.create_graphic()))) - .collect(), - indicator, + .collect(); + + Self { + map, + manifest, not_found, } } @@ -122,12 +119,8 @@ impl ItemImgs { /// Checks if the manifest has been changed and reloads the images if so /// Reuses img ids pub fn reload_if_changed(&mut self, ui: &mut Ui) { - if self.indicator.reloaded() { - for (kind, spec) in ItemImagesSpec::load("voxygen.item_image_manifest") - .expect("Unable to load item image manifest") - .0 - .iter() - { + if self.manifest.reloaded() { + for (kind, spec) in self.manifest.read().0.iter() { // Load new graphic let graphic = spec.create_graphic(); // See if we already have an id we can use @@ -163,30 +156,31 @@ impl ItemImgs { // Copied from figure/load.rs // TODO: remove code dup? -fn graceful_load_vox(specifier: &str) -> Arc { +fn graceful_load_vox(specifier: &str) -> AssetHandle { let full_specifier: String = ["voxygen.", specifier].concat(); - match DotVoxData::load(full_specifier.as_str()) { + match DotVoxAsset::load(full_specifier.as_str()) { Ok(dot_vox) => dot_vox, Err(_) => { error!(?full_specifier, "Could not load vox file for item images",); - DotVoxData::load_expect("voxygen.voxel.not_found") + DotVoxAsset::load_expect("voxygen.voxel.not_found") }, } } fn graceful_load_img(specifier: &str) -> Arc { let full_specifier: String = ["voxygen.", specifier].concat(); - match DynamicImage::load(full_specifier.as_str()) { + let handle = match assets::Image::load(&full_specifier) { Ok(img) => img, Err(_) => { error!(?full_specifier, "Could not load image file for item images"); - DynamicImage::load_expect("voxygen.element.not_found") + assets::Image::load_expect("voxygen.element.not_found") }, - } + }; + handle.read().to_image() } fn graceful_load_segment_no_skin(specifier: &str) -> Arc { use common::figure::{mat_cell::MatCell, MatSegment}; - let mat_seg = MatSegment::from(&*graceful_load_vox(specifier)); + let mat_seg = MatSegment::from(&graceful_load_vox(specifier).read().0); let seg = mat_seg .map(|mat_cell| match mat_cell { MatCell::None => None, diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 00eae0ab1d..3662b0695d 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -46,7 +46,7 @@ use spell::Spell; use crate::{ ecs::{comp as vcomp, comp::HpFloaterList}, hud::img_ids::ImgsRot, - i18n::{i18n_asset_key, LanguageMetadata, Localization}, + i18n::{LanguageMetadata, Localization}, render::{Consts, Globals, RenderMode, Renderer}, scene::camera::{self, Camera}, ui::{fonts::Fonts, img_ids::Rotations, slot, Graphic, Ingameable, ScaleMode, Ui}, @@ -55,7 +55,6 @@ use crate::{ }; use client::Client; use common::{ - assets::Asset, comp, comp::{ item::{ItemDesc, Quality}, @@ -627,7 +626,6 @@ pub struct Hud { tab_complete: Option, pulse: f32, velocity: f32, - i18n: std::sync::Arc, slot_manager: slots::SlotManager, hotbar: hotbar::State, events: Vec, @@ -660,12 +658,9 @@ impl Hud { let rot_imgs = ImgsRot::load(&mut ui).expect("Failed to load rot images!"); // Load item images. let item_imgs = ItemImgs::new(&mut ui, imgs.not_found); - // Load language. - let i18n = Localization::load_expect(&i18n_asset_key( - &global_state.settings.language.selected_language, - )); // Load fonts. - let fonts = Fonts::load(&i18n.fonts, &mut ui).expect("Impossible to load fonts!"); + let fonts = Fonts::load(&global_state.i18n.read().fonts, &mut ui) + .expect("Impossible to load fonts!"); // Get the server name. let server = &client.server_info().name; // Get the id, unwrap is safe because this CANNOT be None at this @@ -726,7 +721,6 @@ impl Hud { tab_complete: None, pulse: 0.0, velocity: 0.0, - i18n, slot_manager, hotbar: hotbar_state, events: Vec::new(), @@ -734,10 +728,8 @@ impl Hud { } } - pub fn update_language(&mut self, i18n: std::sync::Arc) { - self.i18n = i18n; - self.fonts = - Fonts::load(&self.i18n.fonts, &mut self.ui).expect("Impossible to load fonts!"); + pub fn update_fonts(&mut self, i18n: &Localization) { + self.fonts = Fonts::load(&i18n.fonts, &mut self.ui).expect("Impossible to load fonts!"); } #[allow(clippy::assign_op_pattern)] // TODO: Pending review in #587 @@ -759,6 +751,7 @@ impl Hud { // FPS let fps = global_state.clock.stats().average_tps; let version = common::util::DISPLAY_VERSION_LONG.clone(); + let i18n = &*global_state.i18n.read(); if self.show.ingame { let ecs = client.state().ecs(); @@ -1267,7 +1260,7 @@ impl Hud { in_group, &global_state.settings.gameplay, self.pulse, - &self.i18n, + i18n, &self.imgs, &self.fonts, ) @@ -1470,8 +1463,8 @@ impl Hud { Intro::Show => { if self.pulse > 20.0 { self.show.want_grab = false; - let quest_headline = &self.i18n.get("hud.temp_quest_headline"); - let quest_text = &self.i18n.get("hud.temp_quest_text"); + let quest_headline = &i18n.get("hud.temp_quest_headline"); + let quest_text = &i18n.get("hud.temp_quest_text"); Image::new(self.imgs.quest_bg) .w_h(404.0, 858.0) .middle_of(ui_widgets.window) @@ -1508,7 +1501,7 @@ impl Hud { .hover_image(self.imgs.button_hover) .press_image(self.imgs.button_press) .mid_bottom_with_margin_on(self.ids.q_text_bg, -120.0) - .label(&self.i18n.get("common.accept")) + .label(&i18n.get("common.accept")) .label_font_id(self.fonts.cyri.conrod_id) .label_font_size(self.fonts.cyri.scale(22)) .label_color(TEXT_COLOR) @@ -1685,8 +1678,7 @@ impl Hud { // Help Window if let Some(help_key) = global_state.settings.controls.get_binding(GameInput::Help) { Text::new( - &self - .i18n + &i18n .get("hud.press_key_to_toggle_keybindings_fmt") .replace("{key}", help_key.to_string().as_str()), ) @@ -1703,8 +1695,7 @@ impl Hud { .get_binding(GameInput::ToggleDebug) { Text::new( - &self - .i18n + &i18n .get("hud.press_key_to_toggle_debug_info_fmt") .replace("{key}", toggle_debug_key.to_string().as_str()), ) @@ -1718,8 +1709,7 @@ impl Hud { // Help Window if let Some(help_key) = global_state.settings.controls.get_binding(GameInput::Help) { Text::new( - &self - .i18n + &i18n .get("hud.press_key_to_show_keybindings_fmt") .replace("{key}", help_key.to_string().as_str()), ) @@ -1736,8 +1726,7 @@ impl Hud { .get_binding(GameInput::ToggleDebug) { Text::new( - &self - .i18n + &i18n .get("hud.press_key_to_show_debug_info_fmt") .replace("{key}", toggle_debug_key.to_string().as_str()), ) @@ -1754,8 +1743,7 @@ impl Hud { .get_binding(GameInput::ToggleLantern) { Text::new( - &self - .i18n + &i18n .get("hud.press_key_to_toggle_lantern_fmt") .replace("{key}", toggle_lantern_key.to_string().as_str()), ) @@ -1800,7 +1788,7 @@ impl Hud { global_state, &self.rot_imgs, tooltip_manager, - &self.i18n, + i18n, &player_stats, ) .set(self.ids.buttons, ui_widgets) @@ -1822,7 +1810,7 @@ impl Hud { &self.fonts, &self.rot_imgs, tooltip_manager, - &self.i18n, + i18n, &player_buffs, self.pulse, &global_state, @@ -1842,7 +1830,7 @@ impl Hud { &self.imgs, &self.rot_imgs, &self.fonts, - &self.i18n, + i18n, self.pulse, &global_state, tooltip_manager, @@ -1859,7 +1847,7 @@ impl Hud { } // Popup (waypoint saved and similar notifications) Popup::new( - &self.i18n, + i18n, client, &self.new_notifications, &self.fonts, @@ -1895,7 +1883,7 @@ impl Hud { tooltip_manager, &mut self.slot_manager, self.pulse, - &self.i18n, + i18n, &player_stats, &self.show, ) @@ -1963,7 +1951,7 @@ impl Hud { &self.hotbar, tooltip_manager, &mut self.slot_manager, - &self.i18n, + i18n, &self.show, &ability_map, ) @@ -1978,7 +1966,7 @@ impl Hud { client, &self.imgs, &self.fonts, - &self.i18n, + i18n, &self.rot_imgs, tooltip_manager, &self.item_imgs, @@ -2017,7 +2005,7 @@ impl Hud { global_state, &self.imgs, &self.fonts, - &self.i18n, + i18n, ) .and_then(self.force_chat_input.take(), |c, input| c.input(input)) .and_then(self.tab_complete.take(), |c, input| { @@ -2054,7 +2042,7 @@ impl Hud { &self.show, &self.imgs, &self.fonts, - &self.i18n, + i18n, fps as f32, ) .set(self.ids.settings_window, ui_widgets) @@ -2210,7 +2198,7 @@ impl Hud { client, &self.imgs, &self.fonts, - &self.i18n, + i18n, info.selected_entity, &self.rot_imgs, tooltip_manager, @@ -2238,7 +2226,7 @@ impl Hud { // Spellbook if self.show.spell { - match Spell::new(&self.show, client, &self.imgs, &self.fonts, &self.i18n) + match Spell::new(&self.show, client, &self.imgs, &self.fonts, i18n) .set(self.ids.spell, ui_widgets) { Some(spell::Event::Close) => { @@ -2258,7 +2246,7 @@ impl Hud { &self.world_map, &self.fonts, self.pulse, - &self.i18n, + i18n, &global_state, tooltip_manager, ) @@ -2302,9 +2290,7 @@ impl Hud { } if self.show.esc_menu { - match EscMenu::new(&self.imgs, &self.fonts, &self.i18n) - .set(self.ids.esc_menu, ui_widgets) - { + match EscMenu::new(&self.imgs, &self.fonts, i18n).set(self.ids.esc_menu, ui_widgets) { Some(esc_menu::Event::OpenSettings(tab)) => { self.show.open_setting_tab(tab); }, @@ -2344,8 +2330,7 @@ impl Hud { { if self.show.free_look { Text::new( - &self - .i18n + &i18n .get("hud.free_look_indicator") .replace("{key}", freelook_key.to_string().as_str()), ) @@ -2355,8 +2340,7 @@ impl Hud { .font_size(self.fonts.cyri.scale(20)) .set(self.ids.free_look_bg, ui_widgets); Text::new( - &self - .i18n + &i18n .get("hud.free_look_indicator") .replace("{key}", freelook_key.to_string().as_str()), ) @@ -2370,13 +2354,13 @@ impl Hud { // Auto walk indicator if self.show.auto_walk { - Text::new(&self.i18n.get("hud.auto_walk_indicator")) + Text::new(i18n.get("hud.auto_walk_indicator")) .color(TEXT_BG) .mid_top_with_margin_on(ui_widgets.window, 70.0) .font_id(self.fonts.cyri.conrod_id) .font_size(self.fonts.cyri.scale(20)) .set(self.ids.auto_walk_bg, ui_widgets); - Text::new(&self.i18n.get("hud.auto_walk_indicator")) + Text::new(i18n.get("hud.auto_walk_indicator")) .color(KILL_COLOR) .top_left_with_margins_on(self.ids.auto_walk_bg, -1.0, -1.0) .font_id(self.fonts.cyri.conrod_id) diff --git a/voxygen/src/i18n.rs b/voxygen/src/i18n.rs index 11eb66cab7..d55f81637c 100644 --- a/voxygen/src/i18n.rs +++ b/voxygen/src/i18n.rs @@ -1,9 +1,8 @@ -use common::assets::{self, Asset}; +use common::assets::{self, AssetExt}; use deunicode::deunicode; use hashbrown::{HashMap, HashSet}; -use ron::de::from_reader; use serde::{Deserialize, Serialize}; -use std::{fs::File, io::BufReader}; +use std::borrow::Cow; use tracing::warn; /// The reference language, aka the more up-to-date localization data. @@ -44,7 +43,7 @@ impl Font { pub type Fonts = HashMap; /// Store internationalization data -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct Localization { /// A map storing the localized texts /// @@ -92,9 +91,9 @@ impl Localization { } /// Return the missing keys compared to the reference language - pub fn list_missing_entries(&self) -> (HashSet, HashSet) { + fn list_missing_entries(&self) -> (HashSet, HashSet) { let reference_localization = - Localization::load_expect(i18n_asset_key(REFERENCE_LANG).as_ref()); + Localization::load_expect(&i18n_asset_key(REFERENCE_LANG)).read(); let reference_string_keys: HashSet<_> = reference_localization.string_map.keys().cloned().collect(); @@ -133,15 +132,16 @@ impl Localization { } } -impl Asset for Localization { - const ENDINGS: &'static [&'static str] = &["ron"]; +impl assets::Asset for Localization { + type Loader = LocalizationLoader; - /// Load the translations located in the input buffer and convert them - /// into a `Localization` object. - #[allow(clippy::into_iter_on_ref)] // TODO: Pending review in #587 - fn parse(buf_reader: BufReader, _specifier: &str) -> Result { - let mut asked_localization: Localization = - from_reader(buf_reader).map_err(assets::Error::parse_error)?; + const EXTENSION: &'static str = "ron"; +} + +pub struct LocalizationLoader; +impl assets::Loader for LocalizationLoader { + fn load(content: Cow<[u8]>, ext: &str) -> Result { + let mut asked_localization: Localization = assets::RonLoader::load(content, ext)?; // Update the text if UTF-8 to ASCII conversion is enabled if asked_localization.convert_utf8_to_ascii { @@ -150,7 +150,7 @@ impl Asset for Localization { } for value in asked_localization.vector_map.values_mut() { - *value = value.into_iter().map(|s| deunicode(s)).collect(); + *value = value.iter().map(|s| deunicode(s)).collect(); } } asked_localization.metadata.language_name = @@ -162,9 +162,11 @@ impl Asset for Localization { /// Load all the available languages located in the voxygen asset directory pub fn list_localizations() -> Vec { - let voxygen_locales_assets = "voxygen.i18n.*"; - let lang_list = Localization::load_glob(voxygen_locales_assets).unwrap(); - lang_list.iter().map(|e| (*e).metadata.clone()).collect() + assets::load_dir::("voxygen.i18n") + .unwrap() + .iter_all() + .filter_map(|(_, lang)| lang.ok().map(|e| e.read().metadata.clone())) + .collect() } /// Return the asset associated with the language_id diff --git a/voxygen/src/lib.rs b/voxygen/src/lib.rs index 6675f4781f..83c55ffc28 100644 --- a/voxygen/src/lib.rs +++ b/voxygen/src/lib.rs @@ -34,12 +34,13 @@ pub use crate::error::Error; use crate::singleplayer::Singleplayer; use crate::{ audio::AudioFrontend, + i18n::Localization, profile::Profile, render::Renderer, settings::Settings, window::{Event, Window}, }; -use common::{assets::watch, clock::Clock, span}; +use common::{assets::AssetHandle, clock::Clock, span}; /// A type used to store state that is shared between all play states. pub struct GlobalState { @@ -52,7 +53,7 @@ pub struct GlobalState { #[cfg(feature = "singleplayer")] pub singleplayer: Option, // TODO: redo this so that the watcher doesn't have to exist for reloading to occur - pub localization_watcher: watch::ReloadIndicator, + pub i18n: AssetHandle, pub clipboard: Option, } diff --git a/voxygen/src/main.rs b/voxygen/src/main.rs index 8c564768fb..48d2a9a44d 100644 --- a/voxygen/src/main.rs +++ b/voxygen/src/main.rs @@ -15,7 +15,7 @@ use veloren_voxygen::{ }; use common::{ - assets::{watch, Asset}, + assets::{self, AssetExt}, clock::Clock, }; use std::panic; @@ -138,6 +138,8 @@ fn main() { default_hook(panic_info); })); + assets::start_hot_reloading(); + // Initialise watcher for animation hotreloading #[cfg(feature = "hot-anim")] anim::init(); @@ -155,26 +157,18 @@ fn main() { // Load the profile. let profile = Profile::load(); - let mut localization_watcher = watch::ReloadIndicator::new(); - let localized_strings = Localization::load_watched( - &i18n_asset_key(&settings.language.selected_language), - &mut localization_watcher, - ) - .unwrap_or_else(|error| { - let selected_language = &settings.language.selected_language; - warn!( - ?error, - ?selected_language, - "Impossible to load language: change to the default language (English) instead.", - ); - settings.language.selected_language = i18n::REFERENCE_LANG.to_owned(); - Localization::load_watched( - &i18n_asset_key(&settings.language.selected_language), - &mut localization_watcher, - ) - .unwrap() - }); - localized_strings.log_missing_entries(); + let i18n = Localization::load(&i18n_asset_key(&settings.language.selected_language)) + .unwrap_or_else(|error| { + let selected_language = &settings.language.selected_language; + warn!( + ?error, + ?selected_language, + "Impossible to load language: change to the default language (English) instead.", + ); + settings.language.selected_language = i18n::REFERENCE_LANG.to_owned(); + Localization::load_expect(&i18n_asset_key(&settings.language.selected_language)) + }); + i18n.read().log_missing_entries(); // Create window let (window, event_loop) = Window::new(&settings).expect("Failed to create window!"); @@ -192,7 +186,7 @@ fn main() { info_message: None, #[cfg(feature = "singleplayer")] singleplayer: None, - localization_watcher, + i18n, clipboard, }; diff --git a/voxygen/src/menu/char_selection/mod.rs b/voxygen/src/menu/char_selection/mod.rs index 592d3fc07f..1356c2686c 100644 --- a/voxygen/src/menu/char_selection/mod.rs +++ b/voxygen/src/menu/char_selection/mod.rs @@ -1,7 +1,6 @@ mod ui; use crate::{ - i18n::{i18n_asset_key, Localization}, render::Renderer, scene::simple::{self as scene, Scene}, session::SessionState, @@ -10,7 +9,7 @@ use crate::{ Direction, GlobalState, PlayState, PlayStateResult, }; use client::{self, Client}; -use common::{assets::Asset, comp, resources::DeltaTime, span}; +use common::{comp, resources::DeltaTime, span}; use specs::WorldExt; use std::{cell::RefCell, rc::Rc}; use tracing::error; @@ -64,11 +63,7 @@ impl PlayState for CharSelectionState { self.client.borrow_mut().load_character_list(); // Updated localization in case the selected language was changed - let localized_strings = crate::i18n::Localization::load_expect( - &crate::i18n::i18n_asset_key(&global_state.settings.language.selected_language), - ); - self.char_selection_ui - .update_language(std::sync::Arc::clone(&localized_strings)); + self.char_selection_ui.update_language(global_state.i18n); // Set scale mode in case it was change self.char_selection_ui .set_scale_mode(global_state.settings.gameplay.ui_scale); @@ -167,9 +162,7 @@ impl PlayState for CharSelectionState { } // Tick the client (currently only to keep the connection alive). - let localized_strings = Localization::load_expect(&i18n_asset_key( - &global_state.settings.language.selected_language, - )); + let localized_strings = &*global_state.i18n.read(); match self.client.borrow_mut().tick( comp::ControllerInputs::default(), diff --git a/voxygen/src/menu/char_selection/ui/mod.rs b/voxygen/src/menu/char_selection/ui/mod.rs index b484bd2995..29afbfe713 100644 --- a/voxygen/src/menu/char_selection/ui/mod.rs +++ b/voxygen/src/menu/char_selection/ui/mod.rs @@ -1,5 +1,5 @@ use crate::{ - i18n::{i18n_asset_key, Localization}, + i18n::Localization, render::Renderer, ui::{ self, @@ -22,7 +22,7 @@ use crate::{ }; use client::Client; use common::{ - assets::Asset, + assets::AssetHandle, character::{CharacterId, CharacterItem, MAX_CHARACTERS_PER_PLAYER}, comp::{self, humanoid, item::tool::AbilityMap}, LoadoutBuilder, @@ -220,7 +220,6 @@ enum InfoContent { struct Controls { fonts: Fonts, imgs: Imgs, - i18n: std::sync::Arc, // Voxygen version version: String, // Alpha disclaimer @@ -264,19 +263,13 @@ enum Message { } impl Controls { - fn new( - fonts: Fonts, - imgs: Imgs, - i18n: std::sync::Arc, - selected: Option, - ) -> Self { + fn new(fonts: Fonts, imgs: Imgs, selected: Option) -> Self { let version = common::util::DISPLAY_VERSION_LONG.clone(); let alpha = format!("Veloren {}", common::util::DISPLAY_VERSION.as_str()); Self { fonts, imgs, - i18n, version, alpha, @@ -287,12 +280,13 @@ impl Controls { } } - fn view( - &mut self, + fn view<'a>( + &'a mut self, _settings: &Settings, client: &Client, error: &Option, - ) -> Element { + i18n: &'a Localization, + ) -> Element<'a, Message> { // TODO: use font scale thing for text size (use on button size for buttons with // text) @@ -301,7 +295,6 @@ impl Controls { let imgs = &self.imgs; let fonts = &self.fonts; - let i18n = &self.i18n; let tooltip_manager = &self.tooltip_manager; let button_style = style::button::Style::new(imgs.button) @@ -1298,7 +1291,7 @@ impl Controls { body.skin = rng.gen_range(0, species.num_skin_colors()); body.eye_color = rng.gen_range(0, species.num_eye_colors()); body.eyes = rng.gen_range(0, species.num_eyes(body_type)); - *name = npc::get_npc_name(npc::NpcKind::Humanoid).to_string(); + *name = npc::get_npc_name(npc::NpcKind::Humanoid); } }, Message::ConfirmDeletion => { @@ -1402,20 +1395,10 @@ impl CharSelectionUi { let selected_character = global_state.profile.get_selected_character(server_name); // Load language - let i18n = Localization::load_expect(&i18n_asset_key( - &global_state.settings.language.selected_language, - )); + let i18n = global_state.i18n.read(); // TODO: don't add default font twice - let font = { - use std::io::Read; - let mut buf = Vec::new(); - common::assets::load_file(&i18n.fonts.get("cyri").unwrap().asset_key, &["ttf"]) - .unwrap() - .read_to_end(&mut buf) - .unwrap(); - ui::ice::Font::try_from_vec(buf).unwrap() - }; + let font = ui::ice::load_font(&i18n.fonts.get("cyri").unwrap().asset_key); let mut ui = Ui::new( &mut global_state.window, @@ -1429,7 +1412,6 @@ impl CharSelectionUi { let controls = Controls::new( fonts, Imgs::load(&mut ui).expect("Failed to load images"), - i18n, selected_character, ); @@ -1476,20 +1458,13 @@ impl CharSelectionUi { } } - pub fn update_language(&mut self, i18n: std::sync::Arc) { - let font = { - use std::io::Read; - let mut buf = Vec::new(); - common::assets::load_file(&i18n.fonts.get("cyri").unwrap().asset_key, &["ttf"]) - .unwrap() - .read_to_end(&mut buf) - .unwrap(); - ui::ice::Font::try_from_vec(buf).unwrap() - }; - self.controls.i18n = i18n; + pub fn update_language(&mut self, i18n: AssetHandle) { + let i18n = i18n.read(); + let font = ui::ice::load_font(&i18n.fonts.get("cyri").unwrap().asset_key); + self.ui.clear_fonts(font); - self.controls.fonts = Fonts::load(&self.controls.i18n.fonts, &mut self.ui) - .expect("Impossible to load fonts!"); + self.controls.fonts = + Fonts::load(&i18n.fonts, &mut self.ui).expect("Impossible to load fonts!"); } pub fn set_scale_mode(&mut self, scale_mode: ui::ScaleMode) { @@ -1503,10 +1478,11 @@ impl CharSelectionUi { // TODO: do we need whole client here or just character list? pub fn maintain(&mut self, global_state: &mut GlobalState, client: &Client) -> Vec { let mut events = Vec::new(); + let i18n = global_state.i18n.read(); let (mut messages, _) = self.ui.maintain( self.controls - .view(&global_state.settings, &client, &self.error), + .view(&global_state.settings, &client, &self.error, &i18n), global_state.window.renderer_mut(), global_state.clipboard.as_ref(), ); diff --git a/voxygen/src/menu/main/mod.rs b/voxygen/src/menu/main/mod.rs index 6fc23d96a9..37ffc6472a 100644 --- a/voxygen/src/menu/main/mod.rs +++ b/voxygen/src/menu/main/mod.rs @@ -12,7 +12,7 @@ use crate::{ Direction, GlobalState, PlayState, PlayStateResult, }; use client_init::{ClientInit, Error as InitError, Msg as InitMsg}; -use common::{assets::Asset, comp, span}; +use common::{assets::AssetExt, comp, span}; use tracing::error; use ui::{Event as MainMenuEvent, MainMenuUi}; @@ -48,13 +48,8 @@ impl PlayState for MainMenuState { } // Updated localization in case the selected language was changed - let localized_strings = crate::i18n::Localization::load_expect( - &crate::i18n::i18n_asset_key(&global_state.settings.language.selected_language), - ); - self.main_menu_ui.update_language( - std::sync::Arc::clone(&localized_strings), - &global_state.settings, - ); + self.main_menu_ui + .update_language(global_state.i18n, &global_state.settings); // Set scale mode in case it was change self.main_menu_ui .set_scale_mode(global_state.settings.gameplay.ui_scale); @@ -63,9 +58,6 @@ impl PlayState for MainMenuState { #[allow(clippy::single_match)] // TODO: remove when event match has multiple arms fn tick(&mut self, global_state: &mut GlobalState, events: Vec) -> PlayStateResult { span!(_guard, "tick", "::tick"); - let mut localized_strings = crate::i18n::Localization::load_expect( - &crate::i18n::i18n_asset_key(&global_state.settings.language.selected_language), - ); // Poll server creation #[cfg(feature = "singleplayer")] @@ -121,6 +113,7 @@ impl PlayState for MainMenuState { ))); }, Some(InitMsg::Done(Err(err))) => { + let localized_strings = global_state.i18n.read(); self.client_init = None; global_state.info_message = Some({ let err = match err { @@ -268,14 +261,12 @@ impl PlayState for MainMenuState { MainMenuEvent::ChangeLanguage(new_language) => { global_state.settings.language.selected_language = new_language.language_identifier; - localized_strings = Localization::load_expect(&i18n_asset_key( + global_state.i18n = Localization::load_expect(&i18n_asset_key( &global_state.settings.language.selected_language, )); - localized_strings.log_missing_entries(); - self.main_menu_ui.update_language( - std::sync::Arc::clone(&localized_strings), - &global_state.settings, - ); + global_state.i18n.read().log_missing_entries(); + self.main_menu_ui + .update_language(global_state.i18n, &global_state.settings); }, #[cfg(feature = "singleplayer")] MainMenuEvent::StartSingleplayer => { diff --git a/voxygen/src/menu/main/ui/mod.rs b/voxygen/src/menu/main/ui/mod.rs index 18c5d42505..f394c4ee9c 100644 --- a/voxygen/src/menu/main/ui/mod.rs +++ b/voxygen/src/menu/main/ui/mod.rs @@ -5,12 +5,12 @@ mod login; mod servers; use crate::{ - i18n::{i18n_asset_key, LanguageMetadata, Localization}, + i18n::{LanguageMetadata, Localization}, render::Renderer, ui::{ self, fonts::IcedFonts as Fonts, - ice::{style, widget, Element, Font, IcedUi as Ui}, + ice::{load_font, style, widget, Element, IcedUi as Ui}, img_ids::{ImageGraphic, VoxelGraphic}, Graphic, }, @@ -19,8 +19,7 @@ use crate::{ use iced::{text_input, Column, Container, HorizontalAlignment, Length, Row, Space}; //ImageFrame, Tooltip, use crate::settings::Settings; -use common::assets::Asset; -use image::DynamicImage; +use common::assets::{self, AssetExt, AssetHandle}; use rand::{seq::SliceRandom, thread_rng}; use std::time::Duration; @@ -132,7 +131,7 @@ struct Controls { fonts: Fonts, imgs: Imgs, bg_img: widget::image::Handle, - i18n: std::sync::Arc, + i18n: AssetHandle, // Voxygen version version: String, // Alpha disclaimer @@ -177,7 +176,7 @@ impl Controls { fonts: Fonts, imgs: Imgs, bg_img: widget::image::Handle, - i18n: std::sync::Arc, + i18n: AssetHandle, settings: &Settings, ) -> Self { let version = common::util::DISPLAY_VERSION_LONG.clone(); @@ -281,7 +280,7 @@ impl Controls { &self.imgs, &self.login_info, error.as_deref(), - &self.i18n, + &self.i18n.read(), self.is_selecting_language, self.selected_language_index, &language_metadatas, @@ -293,7 +292,7 @@ impl Controls { &self.imgs, &settings.networking.servers, self.selected_server_index, - &self.i18n, + &self.i18n.read(), button_style, ), Screen::Connecting { @@ -304,7 +303,7 @@ impl Controls { &self.imgs, &connection_state, self.time, - &self.i18n, + &self.i18n.read(), button_style, settings.gameplay.loading_tips, ), @@ -482,20 +481,9 @@ pub struct MainMenuUi { impl<'a> MainMenuUi { pub fn new(global_state: &mut GlobalState) -> Self { // Load language - let i18n = Localization::load_expect(&i18n_asset_key( - &global_state.settings.language.selected_language, - )); - + let i18n = &*global_state.i18n.read(); // TODO: don't add default font twice - let font = { - use std::io::Read; - let mut buf = Vec::new(); - common::assets::load_file(&i18n.fonts.get("cyri").unwrap().asset_key, &["ttf"]) - .unwrap() - .read_to_end(&mut buf) - .unwrap(); - Font::try_from_vec(buf).unwrap() - }; + let font = load_font(&i18n.fonts.get("cyri").unwrap().asset_key); let mut ui = Ui::new( &mut global_state.window, @@ -508,31 +496,25 @@ impl<'a> MainMenuUi { let bg_img_spec = BG_IMGS.choose(&mut thread_rng()).unwrap(); + let bg_img = assets::Image::load_expect(bg_img_spec).read().to_image(); let controls = Controls::new( fonts, Imgs::load(&mut ui).expect("Failed to load images"), - ui.add_graphic(Graphic::Image(DynamicImage::load_expect(bg_img_spec), None)), - i18n, + ui.add_graphic(Graphic::Image(bg_img, None)), + global_state.i18n, &global_state.settings, ); Self { ui, controls } } - pub fn update_language(&mut self, i18n: std::sync::Arc, settings: &Settings) { - let font = { - use std::io::Read; - let mut buf = Vec::new(); - common::assets::load_file(&i18n.fonts.get("cyri").unwrap().asset_key, &["ttf"]) - .unwrap() - .read_to_end(&mut buf) - .unwrap(); - Font::try_from_vec(buf).unwrap() - }; + pub fn update_language(&mut self, i18n: AssetHandle, settings: &Settings) { self.controls.i18n = i18n; + let i18n = &*i18n.read(); + let font = load_font(&i18n.fonts.get("cyri").unwrap().asset_key); self.ui.clear_fonts(font); - self.controls.fonts = Fonts::load(&self.controls.i18n.fonts, &mut self.ui) - .expect("Impossible to load fonts!"); + self.controls.fonts = + Fonts::load(&i18n.fonts, &mut self.ui).expect("Impossible to load fonts!"); let language_metadatas = crate::i18n::list_localizations(); self.controls.selected_language_index = language_metadatas .iter() diff --git a/voxygen/src/render/renderer.rs b/voxygen/src/render/renderer.rs index a503796dc1..f1ae11e340 100644 --- a/voxygen/src/render/renderer.rs +++ b/voxygen/src/render/renderer.rs @@ -13,7 +13,7 @@ use super::{ ShadowMapMode, ShadowMode, WrapMode, }; use common::{ - assets::{self, watch::ReloadIndicator, Asset}, + assets::{self, AssetExt, AssetHandle}, span, }; use core::convert::TryFrom; @@ -24,11 +24,6 @@ use gfx::{ traits::{Device, Factory, FactoryExt}, }; use glsl_include::Context as IncludeContext; -use image::DynamicImage; -use std::{ - fs::File, - io::{BufReader, Read}, -}; use tracing::{error, warn}; use vek::*; @@ -104,17 +99,126 @@ pub type ColLightInfo = ( ); /// Load from a GLSL file. -pub struct Glsl; +pub struct Glsl(String); -impl Asset for Glsl { - type Output = String; +impl From for Glsl { + fn from(s: String) -> Glsl { Glsl(s) } +} - const ENDINGS: &'static [&'static str] = &["glsl"]; +impl assets::Asset for Glsl { + type Loader = assets::LoadFrom; - fn parse(mut buf_reader: BufReader, _specifier: &str) -> Result { - let mut string = String::new(); - buf_reader.read_to_string(&mut string)?; - Ok(string) + const EXTENSION: &'static str = "glsl"; +} + +struct Shaders { + constants: AssetHandle, + globals: AssetHandle, + sky: AssetHandle, + light: AssetHandle, + srgb: AssetHandle, + random: AssetHandle, + lod: AssetHandle, + shadows: AssetHandle, + + anti_alias_none: AssetHandle, + anti_alias_fxaa: AssetHandle, + anti_alias_msaa_x4: AssetHandle, + anti_alias_msaa_x8: AssetHandle, + anti_alias_msaa_x16: AssetHandle, + cloud_none: AssetHandle, + cloud_regular: AssetHandle, + figure_vert: AssetHandle, + + terrain_point_shadow_vert: AssetHandle, + terrain_directed_shadow_vert: AssetHandle, + figure_directed_shadow_vert: AssetHandle, + directed_shadow_frag: AssetHandle, + + skybox_vert: AssetHandle, + skybox_frag: AssetHandle, + figure_frag: AssetHandle, + terrain_vert: AssetHandle, + terrain_frag: AssetHandle, + fluid_vert: AssetHandle, + fluid_frag_cheap: AssetHandle, + fluid_frag_shiny: AssetHandle, + sprite_vert: AssetHandle, + sprite_frag: AssetHandle, + particle_vert: AssetHandle, + particle_frag: AssetHandle, + ui_vert: AssetHandle, + ui_frag: AssetHandle, + lod_terrain_vert: AssetHandle, + lod_terrain_frag: AssetHandle, + clouds_vert: AssetHandle, + clouds_frag: AssetHandle, + postprocess_vert: AssetHandle, + postprocess_frag: AssetHandle, + player_shadow_frag: AssetHandle, + light_shadows_geom: AssetHandle, + light_shadows_frag: AssetHandle, +} + +impl assets::Compound for Shaders { + // TODO: Taking the specifier argument as a base for shaders specifiers + // would allow to use several shaders groups easily + fn load( + _: &assets::AssetCache, + _: &str, + ) -> Result { + Ok(Shaders { + constants: AssetExt::load("voxygen.shaders.include.constants")?, + globals: AssetExt::load("voxygen.shaders.include.globals")?, + sky: AssetExt::load("voxygen.shaders.include.sky")?, + light: AssetExt::load("voxygen.shaders.include.light")?, + srgb: AssetExt::load("voxygen.shaders.include.srgb")?, + random: AssetExt::load("voxygen.shaders.include.random")?, + lod: AssetExt::load("voxygen.shaders.include.lod")?, + shadows: AssetExt::load("voxygen.shaders.include.shadows")?, + + anti_alias_none: AssetExt::load("voxygen.shaders.antialias.none")?, + anti_alias_fxaa: AssetExt::load("voxygen.shaders.antialias.fxaa")?, + anti_alias_msaa_x4: AssetExt::load("voxygen.shaders.antialias.msaa-x4")?, + anti_alias_msaa_x8: AssetExt::load("voxygen.shaders.antialias.msaa-x8")?, + anti_alias_msaa_x16: AssetExt::load("voxygen.shaders.antialias.msaa-x16")?, + cloud_none: AssetExt::load("voxygen.shaders.include.cloud.none")?, + cloud_regular: AssetExt::load("voxygen.shaders.include.cloud.regular")?, + figure_vert: AssetExt::load("voxygen.shaders.figure-vert")?, + + terrain_point_shadow_vert: AssetExt::load("voxygen.shaders.light-shadows-vert")?, + terrain_directed_shadow_vert: AssetExt::load( + "voxygen.shaders.light-shadows-directed-vert", + )?, + figure_directed_shadow_vert: AssetExt::load( + "voxygen.shaders.light-shadows-figure-vert", + )?, + directed_shadow_frag: AssetExt::load("voxygen.shaders.light-shadows-directed-frag")?, + + skybox_vert: AssetExt::load("voxygen.shaders.skybox-vert")?, + skybox_frag: AssetExt::load("voxygen.shaders.skybox-frag")?, + figure_frag: AssetExt::load("voxygen.shaders.figure-frag")?, + terrain_vert: AssetExt::load("voxygen.shaders.terrain-vert")?, + terrain_frag: AssetExt::load("voxygen.shaders.terrain-frag")?, + fluid_vert: AssetExt::load("voxygen.shaders.fluid-vert")?, + fluid_frag_cheap: AssetExt::load("voxygen.shaders.fluid-frag.cheap")?, + fluid_frag_shiny: AssetExt::load("voxygen.shaders.fluid-frag.shiny")?, + sprite_vert: AssetExt::load("voxygen.shaders.sprite-vert")?, + sprite_frag: AssetExt::load("voxygen.shaders.sprite-frag")?, + particle_vert: AssetExt::load("voxygen.shaders.particle-vert")?, + particle_frag: AssetExt::load("voxygen.shaders.particle-frag")?, + ui_vert: AssetExt::load("voxygen.shaders.ui-vert")?, + ui_frag: AssetExt::load("voxygen.shaders.ui-frag")?, + lod_terrain_vert: AssetExt::load("voxygen.shaders.lod-terrain-vert")?, + lod_terrain_frag: AssetExt::load("voxygen.shaders.lod-terrain-frag")?, + clouds_vert: AssetExt::load("voxygen.shaders.clouds-vert")?, + clouds_frag: AssetExt::load("voxygen.shaders.clouds-frag")?, + postprocess_vert: AssetExt::load("voxygen.shaders.postprocess-vert")?, + postprocess_frag: AssetExt::load("voxygen.shaders.postprocess-frag")?, + player_shadow_frag: AssetExt::load("voxygen.shaders.player-shadow-frag")?, + light_shadows_geom: AssetExt::load("voxygen.shaders.light-shadows-geom")?, + light_shadows_frag: AssetExt::load("voxygen.shaders.light-shadows-frag")?, + }) } } @@ -172,7 +276,7 @@ pub struct Renderer { postprocess_pipeline: GfxPipeline>, player_shadow_pipeline: GfxPipeline>, - shader_reload_indicator: ReloadIndicator, + shaders: AssetHandle, noise_tex: Texture<(gfx::format::R8, gfx::format::Unorm)>, @@ -198,7 +302,6 @@ impl Renderer { let dims = win_color_view.get_dimensions(); - let mut shader_reload_indicator = ReloadIndicator::new(); let shadow_views = Self::create_shadow_views( &mut factory, (dims.0, dims.1), @@ -209,6 +312,8 @@ impl Renderer { }) .ok(); + let shaders = Shaders::load_expect(""); + let ( skybox_pipeline, figure_pipeline, @@ -224,12 +329,7 @@ impl Renderer { point_shadow_pipeline, terrain_directed_shadow_pipeline, figure_directed_shadow_pipeline, - ) = create_pipelines( - &mut factory, - &mode, - shadow_views.is_some(), - &mut shader_reload_indicator, - )?; + ) = create_pipelines(&mut factory, &shaders.read(), &mode, shadow_views.is_some())?; let ( tgt_color_view, @@ -285,7 +385,7 @@ impl Renderer { let noise_tex = Texture::new( &mut factory, - &DynamicImage::load_expect("voxygen.texture.noise"), + &assets::Image::load_expect("voxygen.texture.noise").read().0, Some(gfx::texture::FilterMethod::Trilinear), Some(gfx::texture::WrapMode::Tile), None, @@ -323,7 +423,7 @@ impl Renderer { postprocess_pipeline, player_shadow_pipeline, - shader_reload_indicator, + shaders, noise_tex, @@ -789,7 +889,7 @@ impl Renderer { self.device.cleanup(); // If the shaders files were changed attempt to recreate the shaders - if self.shader_reload_indicator.reloaded() { + if self.shaders.reloaded() { self.recreate_pipelines(); } } @@ -798,9 +898,9 @@ impl Renderer { fn recreate_pipelines(&mut self) { match create_pipelines( &mut self.factory, + &self.shaders.read(), &self.mode, self.shadow_map.is_some(), - &mut self.shader_reload_indicator, ) { Ok(( skybox_pipeline, @@ -1771,9 +1871,9 @@ struct GfxPipeline { #[allow(clippy::type_complexity)] // TODO: Pending review in #587 fn create_pipelines( factory: &mut gfx_backend::Factory, + shaders: &Shaders, mode: &RenderMode, has_shadow_views: bool, - shader_reload_indicator: &mut ReloadIndicator, ) -> Result< ( GfxPipeline>, @@ -1793,20 +1893,6 @@ fn create_pipelines( ), RenderError, > { - let constants = - Glsl::load_watched("voxygen.shaders.include.constants", shader_reload_indicator).unwrap(); - let globals = - Glsl::load_watched("voxygen.shaders.include.globals", shader_reload_indicator).unwrap(); - let sky = Glsl::load_watched("voxygen.shaders.include.sky", shader_reload_indicator).unwrap(); - let light = - Glsl::load_watched("voxygen.shaders.include.light", shader_reload_indicator).unwrap(); - let srgb = Glsl::load_watched("voxygen.shaders.include.srgb", shader_reload_indicator).unwrap(); - let random = - Glsl::load_watched("voxygen.shaders.include.random", shader_reload_indicator).unwrap(); - let lod = Glsl::load_watched("voxygen.shaders.include.lod", shader_reload_indicator).unwrap(); - let shadows = - Glsl::load_watched("voxygen.shaders.include.shadows", shader_reload_indicator).unwrap(); - // We dynamically add extra configuration settings to the constants file. let constants = format!( r#" @@ -1819,7 +1905,7 @@ fn create_pipelines( #define SHADOW_MODE {} "#, - constants, + shaders.constants.read().0, // TODO: Configurable vertex/fragment shader preference. "VOXYGEN_COMPUTATION_PREFERENCE_FRAGMENT", match mode.fluid { @@ -1846,74 +1932,37 @@ fn create_pipelines( }, ); - let anti_alias = Glsl::load_watched( - &["voxygen.shaders.antialias.", match mode.aa { - AaMode::None => "none", - AaMode::Fxaa => "fxaa", - AaMode::MsaaX4 => "msaa-x4", - AaMode::MsaaX8 => "msaa-x8", - AaMode::MsaaX16 => "msaa-x16", - }] - .concat(), - shader_reload_indicator, - ) - .unwrap(); + let anti_alias = &match mode.aa { + AaMode::None => shaders.anti_alias_none, + AaMode::Fxaa => shaders.anti_alias_fxaa, + AaMode::MsaaX4 => shaders.anti_alias_msaa_x4, + AaMode::MsaaX8 => shaders.anti_alias_msaa_x8, + AaMode::MsaaX16 => shaders.anti_alias_msaa_x16, + }; - let cloud = Glsl::load_watched( - &["voxygen.shaders.include.cloud.", match mode.cloud { - CloudMode::None => "none", - _ => "regular", - }] - .concat(), - shader_reload_indicator, - ) - .unwrap(); + let cloud = &match mode.cloud { + CloudMode::None => shaders.cloud_none, + _ => shaders.cloud_regular, + }; let mut include_ctx = IncludeContext::new(); include_ctx.include("constants.glsl", &constants); - include_ctx.include("globals.glsl", &globals); - include_ctx.include("shadows.glsl", &shadows); - include_ctx.include("sky.glsl", &sky); - include_ctx.include("light.glsl", &light); - include_ctx.include("srgb.glsl", &srgb); - include_ctx.include("random.glsl", &random); - include_ctx.include("lod.glsl", &lod); - include_ctx.include("anti-aliasing.glsl", &anti_alias); - include_ctx.include("cloud.glsl", &cloud); - - let figure_vert = - Glsl::load_watched("voxygen.shaders.figure-vert", shader_reload_indicator).unwrap(); - - let terrain_point_shadow_vert = Glsl::load_watched( - "voxygen.shaders.light-shadows-vert", - shader_reload_indicator, - ) - .unwrap(); - - let terrain_directed_shadow_vert = Glsl::load_watched( - "voxygen.shaders.light-shadows-directed-vert", - shader_reload_indicator, - ) - .unwrap(); - - let figure_directed_shadow_vert = Glsl::load_watched( - "voxygen.shaders.light-shadows-figure-vert", - shader_reload_indicator, - ) - .unwrap(); - - let directed_shadow_frag = Glsl::load_watched( - "voxygen.shaders.light-shadows-directed-frag", - shader_reload_indicator, - ) - .unwrap(); + include_ctx.include("globals.glsl", &shaders.globals.read().0); + include_ctx.include("shadows.glsl", &shaders.shadows.read().0); + include_ctx.include("sky.glsl", &shaders.sky.read().0); + include_ctx.include("light.glsl", &shaders.light.read().0); + include_ctx.include("srgb.glsl", &shaders.srgb.read().0); + include_ctx.include("random.glsl", &shaders.random.read().0); + include_ctx.include("lod.glsl", &shaders.lod.read().0); + include_ctx.include("anti-aliasing.glsl", &anti_alias.read().0); + include_ctx.include("cloud.glsl", &cloud.read().0); // Construct a pipeline for rendering skyboxes let skybox_pipeline = create_pipeline( factory, skybox::pipe::new(), - &Glsl::load_watched("voxygen.shaders.skybox-vert", shader_reload_indicator).unwrap(), - &Glsl::load_watched("voxygen.shaders.skybox-frag", shader_reload_indicator).unwrap(), + &shaders.skybox_vert.read().0, + &shaders.skybox_frag.read().0, &include_ctx, gfx::state::CullFace::Back, )?; @@ -1922,8 +1971,8 @@ fn create_pipelines( let figure_pipeline = create_pipeline( factory, figure::pipe::new(), - &figure_vert, - &Glsl::load_watched("voxygen.shaders.figure-frag", shader_reload_indicator).unwrap(), + &shaders.figure_vert.read().0, + &shaders.figure_frag.read().0, &include_ctx, gfx::state::CullFace::Back, )?; @@ -1932,8 +1981,8 @@ fn create_pipelines( let terrain_pipeline = create_pipeline( factory, terrain::pipe::new(), - &Glsl::load_watched("voxygen.shaders.terrain-vert", shader_reload_indicator).unwrap(), - &Glsl::load_watched("voxygen.shaders.terrain-frag", shader_reload_indicator).unwrap(), + &shaders.terrain_vert.read().0, + &shaders.terrain_frag.read().0, &include_ctx, gfx::state::CullFace::Back, )?; @@ -1942,16 +1991,13 @@ fn create_pipelines( let fluid_pipeline = create_pipeline( factory, fluid::pipe::new(), - &Glsl::load_watched("voxygen.shaders.fluid-vert", shader_reload_indicator).unwrap(), - &Glsl::load_watched( - &["voxygen.shaders.fluid-frag.", match mode.fluid { - FluidMode::Cheap => "cheap", - FluidMode::Shiny => "shiny", - }] - .concat(), - shader_reload_indicator, - ) - .unwrap(), + &shaders.fluid_vert.read().0, + &match mode.fluid { + FluidMode::Cheap => shaders.fluid_frag_cheap, + FluidMode::Shiny => shaders.fluid_frag_shiny, + } + .read() + .0, &include_ctx, gfx::state::CullFace::Nothing, )?; @@ -1960,8 +2006,8 @@ fn create_pipelines( let sprite_pipeline = create_pipeline( factory, sprite::pipe::new(), - &Glsl::load_watched("voxygen.shaders.sprite-vert", shader_reload_indicator).unwrap(), - &Glsl::load_watched("voxygen.shaders.sprite-frag", shader_reload_indicator).unwrap(), + &shaders.sprite_vert.read().0, + &shaders.sprite_frag.read().0, &include_ctx, gfx::state::CullFace::Back, )?; @@ -1970,8 +2016,8 @@ fn create_pipelines( let particle_pipeline = create_pipeline( factory, particle::pipe::new(), - &Glsl::load_watched("voxygen.shaders.particle-vert", shader_reload_indicator).unwrap(), - &Glsl::load_watched("voxygen.shaders.particle-frag", shader_reload_indicator).unwrap(), + &shaders.particle_vert.read().0, + &shaders.particle_frag.read().0, &include_ctx, gfx::state::CullFace::Back, )?; @@ -1980,8 +2026,8 @@ fn create_pipelines( let ui_pipeline = create_pipeline( factory, ui::pipe::new(), - &Glsl::load_watched("voxygen.shaders.ui-vert", shader_reload_indicator).unwrap(), - &Glsl::load_watched("voxygen.shaders.ui-frag", shader_reload_indicator).unwrap(), + &shaders.ui_vert.read().0, + &shaders.ui_frag.read().0, &include_ctx, gfx::state::CullFace::Back, )?; @@ -1990,8 +2036,8 @@ fn create_pipelines( let lod_terrain_pipeline = create_pipeline( factory, lod_terrain::pipe::new(), - &Glsl::load_watched("voxygen.shaders.lod-terrain-vert", shader_reload_indicator).unwrap(), - &Glsl::load_watched("voxygen.shaders.lod-terrain-frag", shader_reload_indicator).unwrap(), + &shaders.lod_terrain_vert.read().0, + &shaders.lod_terrain_frag.read().0, &include_ctx, gfx::state::CullFace::Back, )?; @@ -2000,8 +2046,8 @@ fn create_pipelines( let clouds_pipeline = create_pipeline( factory, clouds::pipe::new(), - &Glsl::load_watched("voxygen.shaders.clouds-vert", shader_reload_indicator).unwrap(), - &Glsl::load_watched("voxygen.shaders.clouds-frag", shader_reload_indicator).unwrap(), + &shaders.clouds_vert.read().0, + &shaders.clouds_frag.read().0, &include_ctx, gfx::state::CullFace::Back, )?; @@ -2010,8 +2056,8 @@ fn create_pipelines( let postprocess_pipeline = create_pipeline( factory, postprocess::pipe::new(), - &Glsl::load_watched("voxygen.shaders.postprocess-vert", shader_reload_indicator).unwrap(), - &Glsl::load_watched("voxygen.shaders.postprocess-frag", shader_reload_indicator).unwrap(), + &shaders.postprocess_vert.read().0, + &shaders.postprocess_frag.read().0, &include_ctx, gfx::state::CullFace::Back, )?; @@ -2028,12 +2074,8 @@ fn create_pipelines( ),*/), ..figure::pipe::new() }, - &figure_vert, - &Glsl::load_watched( - "voxygen.shaders.player-shadow-frag", - shader_reload_indicator, - ) - .unwrap(), + &shaders.figure_vert.read().0, + &shaders.player_shadow_frag.read().0, &include_ctx, gfx::state::CullFace::Back, )?; @@ -2042,19 +2084,9 @@ fn create_pipelines( let point_shadow_pipeline = match create_shadow_pipeline( factory, shadow::pipe::new(), - &terrain_point_shadow_vert, - Some( - &Glsl::load_watched( - "voxygen.shaders.light-shadows-geom", - shader_reload_indicator, - ) - .unwrap(), - ), - &Glsl::load_watched( - "voxygen.shaders.light-shadows-frag", - shader_reload_indicator, - ) - .unwrap(), + &shaders.terrain_point_shadow_vert.read().0, + Some(&shaders.light_shadows_geom.read().0), + &shaders.light_shadows_frag.read().0, &include_ctx, gfx::state::CullFace::Back, None, // Some(gfx::state::Offset(2, 0)) @@ -2070,9 +2102,9 @@ fn create_pipelines( let terrain_directed_shadow_pipeline = match create_shadow_pipeline( factory, shadow::pipe::new(), - &terrain_directed_shadow_vert, + &shaders.terrain_directed_shadow_vert.read().0, None, - &directed_shadow_frag, + &shaders.directed_shadow_frag.read().0, &include_ctx, gfx::state::CullFace::Back, None, // Some(gfx::state::Offset(2, 1)) @@ -2091,9 +2123,9 @@ fn create_pipelines( let figure_directed_shadow_pipeline = match create_shadow_pipeline( factory, shadow::figure_pipe::new(), - &figure_directed_shadow_vert, + &shaders.figure_directed_shadow_vert.read().0, None, - &directed_shadow_frag, + &shaders.directed_shadow_frag.read().0, &include_ctx, gfx::state::CullFace::Back, None, // Some(gfx::state::Offset(2, 1)) diff --git a/voxygen/src/scene/figure/cache.rs b/voxygen/src/scene/figure/cache.rs index b405c5b56e..9b1cd2fd08 100644 --- a/voxygen/src/scene/figure/cache.rs +++ b/voxygen/src/scene/figure/cache.rs @@ -8,7 +8,7 @@ use crate::{ }; use anim::Skeleton; use common::{ - assets::watch::ReloadIndicator, + assets::AssetHandle, comp::{ item::{ armor::{Armor, ArmorKind}, @@ -234,8 +234,7 @@ where Skel::Body: BodySpec, { models: HashMap, ((FigureModelEntryFuture, Skel::Attr), u64)>, - manifests: Arc<::Spec>, - manifest_indicator: ReloadIndicator, + manifests: AssetHandle<::Spec>, } impl FigureModelCache @@ -244,15 +243,10 @@ where { #[allow(clippy::new_without_default)] // TODO: Pending review in #587 pub fn new() -> Self { - let mut manifest_indicator = ReloadIndicator::new(); Self { models: HashMap::new(), // NOTE: It might be better to bubble this error up rather than panicking. - manifests: Arc::new( - ::load_watched(&mut manifest_indicator) - .expect("Could not load manifests for body type"), - ), - manifest_indicator, + manifests: ::load_spec().unwrap(), } } @@ -364,13 +358,13 @@ where Entry::Vacant(v) => { let key = v.key().clone(); let slot = Arc::new(atomic::AtomicCell::new(None)); - let manifests = Arc::clone(&self.manifests); + let manifests = self.manifests; let slot_ = Arc::clone(&slot); thread_pool.execute(move || { // First, load all the base vertex data. - let manifests = &*manifests; - let meshes = ::bone_meshes(&key, &*manifests); + let manifests = &*manifests.read(); + let meshes = ::bone_meshes(&key, manifests); // Then, set up meshing context. let mut greedy = FigureModel::make_greedy(); @@ -522,12 +516,9 @@ where { // Check for reloaded manifests // TODO: maybe do this in a different function, maintain? - if self.manifest_indicator.reloaded() { + if self.manifests.reloaded() { col_lights.atlas.clear(); self.models.clear(); - if let Err(err) = ::reload(Arc::make_mut(&mut self.manifests)) { - tracing::warn!(?err, "Hot reload failed."); - } } // TODO: Don't hard-code this. if tick % 60 == 0 { diff --git a/voxygen/src/scene/figure/load.rs b/voxygen/src/scene/figure/load.rs index 5241f9e860..089c216c86 100644 --- a/voxygen/src/scene/figure/load.rs +++ b/voxygen/src/scene/figure/load.rs @@ -1,6 +1,6 @@ use super::cache::FigureKey; use common::{ - assets::{self, watch::ReloadIndicator, Asset, AssetWith, Ron}, + assets::{self, AssetExt, AssetHandle, DotVoxAsset, Ron}, comp::{ biped_large::{self, BodyType as BLBodyType, Species as BLSpecies}, bird_medium::{self, BodyType as BMBodyType, Species as BMSpecies}, @@ -18,10 +18,8 @@ use common::{ }, figure::{DynaUnionizer, MatSegment, Material, Segment}, }; -use dot_vox::DotVoxData; use hashbrown::HashMap; use serde::Deserialize; -use std::sync::Arc; use tracing::{error, warn}; use vek::*; @@ -29,29 +27,29 @@ pub type BoneMeshes = (Segment, Vec3); fn load_segment(mesh_name: &str) -> Segment { let full_specifier: String = ["voxygen.voxel.", mesh_name].concat(); - Segment::from(DotVoxData::load_expect(full_specifier.as_str()).as_ref()) + Segment::from(&DotVoxAsset::load_expect(&full_specifier).read().0) } -fn graceful_load_vox(mesh_name: &str) -> Arc { +fn graceful_load_vox(mesh_name: &str) -> AssetHandle { let full_specifier: String = ["voxygen.voxel.", mesh_name].concat(); - match DotVoxData::load(full_specifier.as_str()) { + match DotVoxAsset::load(&full_specifier) { Ok(dot_vox) => dot_vox, Err(_) => { error!(?full_specifier, "Could not load vox file for figure"); - DotVoxData::load_expect("voxygen.voxel.not_found") + DotVoxAsset::load_expect("voxygen.voxel.not_found") }, } } fn graceful_load_segment(mesh_name: &str) -> Segment { - Segment::from(graceful_load_vox(mesh_name).as_ref()) + Segment::from(&graceful_load_vox(mesh_name).read().0) } fn graceful_load_segment_flipped(mesh_name: &str) -> Segment { - Segment::from_vox(graceful_load_vox(mesh_name).as_ref(), true) + Segment::from_vox(&graceful_load_vox(mesh_name).read().0, true) } fn graceful_load_mat_segment(mesh_name: &str) -> MatSegment { - MatSegment::from(graceful_load_vox(mesh_name).as_ref()) + MatSegment::from(&graceful_load_vox(mesh_name).read().0) } fn graceful_load_mat_segment_flipped(mesh_name: &str) -> MatSegment { - MatSegment::from_vox(graceful_load_vox(mesh_name).as_ref(), true) + MatSegment::from_vox(&graceful_load_vox(mesh_name).read().0, true) } pub fn load_mesh(mesh_name: &str, position: Vec3) -> BoneMeshes { @@ -76,12 +74,8 @@ fn recolor_grey(rgb: Rgb, color: Rgb) -> Rgb { pub trait BodySpec: Sized { type Spec; - /// Initialize all the specifications for this Body and watch for changes. - fn load_watched(indicator: &mut ReloadIndicator) -> Result; - - /// Reload all specifications for this Body (to be called if the reload - /// indicator is set). - fn reload(spec: &mut Self::Spec) -> Result<(), assets::Error>; + /// Initialize all the specifications for this Body. + fn load_spec() -> Result, assets::Error>; /// Mesh bones using the given spec, character state, and mesh generation /// function. @@ -104,23 +98,23 @@ macro_rules! make_vox_spec { ) => { #[derive(Clone)] pub struct $Spec { - $( $field: AssetWith, $asset_path>, )* + $( $field: AssetHandle>, )* + } + + impl assets::Compound for $Spec { + fn load(_: &assets::AssetCache, _: &str) -> Result { + Ok($Spec { + $( $field: AssetExt::load($asset_path)?, )* + }) + } } impl BodySpec for $body { type Spec = $Spec; #[allow(unused_variables)] - fn load_watched(indicator: &mut ReloadIndicator) -> Result { - Ok(Self::Spec { - $( $field: AssetWith::load_watched(indicator)?, )* - }) - } - - #[allow(unused_variables)] - fn reload(spec: &mut Self::Spec) -> Result<(), assets::Error> { - $( spec.$field.reload()?; )* - Ok(()) + fn load_spec() -> Result, assets::Error> { + Self::Spec::load("") } fn bone_meshes( @@ -375,95 +369,97 @@ make_vox_spec!( let hand = loadout.hand.as_deref(); let foot = loadout.foot.as_deref(); + let color = &spec.color.read().0; + [ third_person.map(|_| { - spec.head.asset.mesh_head( + spec.head.read().0.mesh_head( body, - &spec.color.asset, + color, ) }), third_person.map(|loadout| { - spec.armor_chest.asset.mesh_chest( + spec.armor_chest.read().0.mesh_chest( body, - &spec.color.asset, + color, loadout.chest.as_deref(), ) }), third_person.map(|loadout| { - spec.armor_belt.asset.mesh_belt( + spec.armor_belt.read().0.mesh_belt( body, - &spec.color.asset, + color, loadout.belt.as_deref(), ) }), third_person.map(|loadout| { - spec.armor_back.asset.mesh_back( + spec.armor_back.read().0.mesh_back( body, - &spec.color.asset, + color, loadout.back.as_deref(), ) }), third_person.map(|loadout| { - spec.armor_pants.asset.mesh_pants( + spec.armor_pants.read().0.mesh_pants( body, - &spec.color.asset, + color, loadout.pants.as_deref(), ) }), - Some(spec.armor_hand.asset.mesh_left_hand( + Some(spec.armor_hand.read().0.mesh_left_hand( body, - &spec.color.asset, + color, hand, )), - Some(spec.armor_hand.asset.mesh_right_hand( + Some(spec.armor_hand.read().0.mesh_right_hand( body, - &spec.color.asset, + color, hand, )), - Some(spec.armor_foot.asset.mesh_left_foot( + Some(spec.armor_foot.read().0.mesh_left_foot( body, - &spec.color.asset, + color, foot, )), - Some(spec.armor_foot.asset.mesh_right_foot( + Some(spec.armor_foot.read().0.mesh_right_foot( body, - &spec.color.asset, + color, foot, )), third_person.map(|loadout| { - spec.armor_shoulder.asset.mesh_left_shoulder( + spec.armor_shoulder.read().0.mesh_left_shoulder( body, - &spec.color.asset, + color, loadout.shoulder.as_deref(), ) }), third_person.map(|loadout| { - spec.armor_shoulder.asset.mesh_right_shoulder( + spec.armor_shoulder.read().0.mesh_right_shoulder( body, - &spec.color.asset, + color, loadout.shoulder.as_deref(), ) }), - Some(spec.armor_glider.asset.mesh_glider( + Some(spec.armor_glider.read().0.mesh_glider( body, - &spec.color.asset, + color, glider, )), tool.and_then(|tool| tool.active.as_ref()).map(|tool| { - spec.main_weapon.asset.mesh_main_weapon( + spec.main_weapon.read().0.mesh_main_weapon( tool, false, ) }), tool.and_then(|tool| tool.second.as_ref()).map(|tool| { - spec.main_weapon.asset.mesh_main_weapon( + spec.main_weapon.read().0.mesh_main_weapon( tool, true, ) }), - Some(spec.armor_lantern.asset.mesh_lantern( + Some(spec.armor_lantern.read().0.mesh_lantern( body, - &spec.color.asset, + color, lantern, )), Some(mesh_hold()), @@ -1065,31 +1061,31 @@ make_vox_spec!( }, |FigureKey { body, .. }, spec| { [ - Some(spec.central.asset.mesh_head( + Some(spec.central.read().0.mesh_head( body.species, body.body_type, )), - Some(spec.central.asset.mesh_chest( + Some(spec.central.read().0.mesh_chest( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_foot_fl( + Some(spec.lateral.read().0.mesh_foot_fl( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_foot_fr( + Some(spec.lateral.read().0.mesh_foot_fr( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_foot_bl( + Some(spec.lateral.read().0.mesh_foot_bl( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_foot_br( + Some(spec.lateral.read().0.mesh_foot_br( body.species, body.body_type, )), - Some(spec.central.asset.mesh_tail( + Some(spec.central.read().0.mesh_tail( body.species, body.body_type, )), @@ -1269,63 +1265,63 @@ make_vox_spec!( }, |FigureKey { body, .. }, spec| { [ - Some(spec.central.asset.mesh_head( + Some(spec.central.read().0.mesh_head( body.species, body.body_type, )), - Some(spec.central.asset.mesh_neck( + Some(spec.central.read().0.mesh_neck( body.species, body.body_type, )), - Some(spec.central.asset.mesh_jaw( + Some(spec.central.read().0.mesh_jaw( body.species, body.body_type, )), - Some(spec.central.asset.mesh_tail( + Some(spec.central.read().0.mesh_tail( body.species, body.body_type, )), - Some(spec.central.asset.mesh_torso_front( + Some(spec.central.read().0.mesh_torso_front( body.species, body.body_type, )), - Some(spec.central.asset.mesh_torso_back( + Some(spec.central.read().0.mesh_torso_back( body.species, body.body_type, )), - Some(spec.central.asset.mesh_ears( + Some(spec.central.read().0.mesh_ears( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_leg_fl( + Some(spec.lateral.read().0.mesh_leg_fl( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_leg_fr( + Some(spec.lateral.read().0.mesh_leg_fr( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_leg_bl( + Some(spec.lateral.read().0.mesh_leg_bl( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_leg_br( + Some(spec.lateral.read().0.mesh_leg_br( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_foot_fl( + Some(spec.lateral.read().0.mesh_foot_fl( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_foot_fr( + Some(spec.lateral.read().0.mesh_foot_fr( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_foot_bl( + Some(spec.lateral.read().0.mesh_foot_bl( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_foot_br( + Some(spec.lateral.read().0.mesh_foot_br( body.species, body.body_type, )), @@ -1618,31 +1614,31 @@ make_vox_spec!( }, |FigureKey { body, .. }, spec| { [ - Some(spec.central.asset.mesh_head( + Some(spec.central.read().0.mesh_head( body.species, body.body_type, )), - Some(spec.central.asset.mesh_torso( + Some(spec.central.read().0.mesh_torso( body.species, body.body_type, )), - Some(spec.central.asset.mesh_tail( + Some(spec.central.read().0.mesh_tail( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_wing_l( + Some(spec.lateral.read().0.mesh_wing_l( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_wing_r( + Some(spec.lateral.read().0.mesh_wing_r( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_foot_l( + Some(spec.lateral.read().0.mesh_foot_l( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_foot_r( + Some(spec.lateral.read().0.mesh_foot_r( body.species, body.body_type, )), @@ -1817,55 +1813,55 @@ make_vox_spec!( }, |FigureKey { body, .. }, spec| { [ - Some(spec.central.asset.mesh_head( + Some(spec.central.read().0.mesh_head( body.species, body.body_type, )), - Some(spec.central.asset.mesh_jaw( + Some(spec.central.read().0.mesh_jaw( body.species, body.body_type, )), - Some(spec.central.asset.mesh_neck( + Some(spec.central.read().0.mesh_neck( body.species, body.body_type, )), - Some(spec.central.asset.mesh_chest_front( + Some(spec.central.read().0.mesh_chest_front( body.species, body.body_type, )), - Some(spec.central.asset.mesh_chest_back( + Some(spec.central.read().0.mesh_chest_back( body.species, body.body_type, )), - Some(spec.central.asset.mesh_tail_front( + Some(spec.central.read().0.mesh_tail_front( body.species, body.body_type, )), - Some(spec.central.asset.mesh_tail_back( + Some(spec.central.read().0.mesh_tail_back( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_hand_l( + Some(spec.lateral.read().0.mesh_hand_l( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_hand_r( + Some(spec.lateral.read().0.mesh_hand_r( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_leg_l( + Some(spec.lateral.read().0.mesh_leg_l( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_leg_r( + Some(spec.lateral.read().0.mesh_leg_r( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_foot_l( + Some(spec.lateral.read().0.mesh_foot_l( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_foot_r( + Some(spec.lateral.read().0.mesh_foot_r( body.species, body.body_type, )), @@ -2124,31 +2120,31 @@ make_vox_spec!( }, |FigureKey { body, .. }, spec| { [ - Some(spec.central.asset.mesh_head( + Some(spec.central.read().0.mesh_head( body.species, body.body_type, )), - Some(spec.central.asset.mesh_jaw( + Some(spec.central.read().0.mesh_jaw( body.species, body.body_type, )), - Some(spec.central.asset.mesh_chest_front( + Some(spec.central.read().0.mesh_chest_front( body.species, body.body_type, )), - Some(spec.central.asset.mesh_chest_back( + Some(spec.central.read().0.mesh_chest_back( body.species, body.body_type, )), - Some(spec.central.asset.mesh_tail( + Some(spec.central.read().0.mesh_tail( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_fin_l( + Some(spec.lateral.read().0.mesh_fin_l( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_fin_r( + Some(spec.lateral.read().0.mesh_fin_r( body.species, body.body_type, )), @@ -2316,19 +2312,19 @@ make_vox_spec!( }, |FigureKey { body, .. }, spec| { [ - Some(spec.central.asset.mesh_chest( + Some(spec.central.read().0.mesh_chest( body.species, body.body_type, )), - Some(spec.central.asset.mesh_tail( + Some(spec.central.read().0.mesh_tail( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_fin_l( + Some(spec.lateral.read().0.mesh_fin_l( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_fin_r( + Some(spec.lateral.read().0.mesh_fin_r( body.species, body.body_type, )), @@ -2465,63 +2461,63 @@ make_vox_spec!( }, |FigureKey { body, .. }, spec| { [ - Some(spec.central.asset.mesh_head_upper( + Some(spec.central.read().0.mesh_head_upper( body.species, body.body_type, )), - Some(spec.central.asset.mesh_head_lower( + Some(spec.central.read().0.mesh_head_lower( body.species, body.body_type, )), - Some(spec.central.asset.mesh_jaw( + Some(spec.central.read().0.mesh_jaw( body.species, body.body_type, )), - Some(spec.central.asset.mesh_chest_front( + Some(spec.central.read().0.mesh_chest_front( body.species, body.body_type, )), - Some(spec.central.asset.mesh_chest_rear( + Some(spec.central.read().0.mesh_chest_rear( body.species, body.body_type, )), - Some(spec.central.asset.mesh_tail_front( + Some(spec.central.read().0.mesh_tail_front( body.species, body.body_type, )), - Some(spec.central.asset.mesh_tail_rear( + Some(spec.central.read().0.mesh_tail_rear( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_wing_in_l( + Some(spec.lateral.read().0.mesh_wing_in_l( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_wing_in_r( + Some(spec.lateral.read().0.mesh_wing_in_r( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_wing_out_l( + Some(spec.lateral.read().0.mesh_wing_out_l( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_wing_out_r( + Some(spec.lateral.read().0.mesh_wing_out_r( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_foot_fl( + Some(spec.lateral.read().0.mesh_foot_fl( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_foot_fr( + Some(spec.lateral.read().0.mesh_foot_fr( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_foot_bl( + Some(spec.lateral.read().0.mesh_foot_bl( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_foot_br( + Some(spec.lateral.read().0.mesh_foot_br( body.species, body.body_type, )), @@ -2883,63 +2879,63 @@ make_vox_spec!( }, |FigureKey { body, .. }, spec| { [ - Some(spec.central.asset.mesh_head( + Some(spec.central.read().0.mesh_head( body.species, body.body_type, )), - Some(spec.central.asset.mesh_jaw( + Some(spec.central.read().0.mesh_jaw( body.species, body.body_type, )), - Some(spec.central.asset.mesh_torso_upper( + Some(spec.central.read().0.mesh_torso_upper( body.species, body.body_type, )), - Some(spec.central.asset.mesh_torso_lower( + Some(spec.central.read().0.mesh_torso_lower( body.species, body.body_type, )), - Some(spec.central.asset.mesh_tail( + Some(spec.central.read().0.mesh_tail( body.species, body.body_type, )), - Some(spec.central.asset.mesh_main( + Some(spec.central.read().0.mesh_main( body.species, body.body_type, )), - Some(spec.central.asset.mesh_second( + Some(spec.central.read().0.mesh_second( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_shoulder_l( + Some(spec.lateral.read().0.mesh_shoulder_l( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_shoulder_r( + Some(spec.lateral.read().0.mesh_shoulder_r( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_hand_l( + Some(spec.lateral.read().0.mesh_hand_l( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_hand_r( + Some(spec.lateral.read().0.mesh_hand_r( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_leg_l( + Some(spec.lateral.read().0.mesh_leg_l( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_leg_r( + Some(spec.lateral.read().0.mesh_leg_r( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_foot_l( + Some(spec.lateral.read().0.mesh_foot_l( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_foot_r( + Some(spec.lateral.read().0.mesh_foot_r( body.species, body.body_type, )), @@ -3235,51 +3231,51 @@ make_vox_spec!( }, |FigureKey { body, .. }, spec| { [ - Some(spec.central.asset.mesh_head( + Some(spec.central.read().0.mesh_head( body.species, body.body_type, )), - Some(spec.central.asset.mesh_jaw( + Some(spec.central.read().0.mesh_jaw( body.species, body.body_type, )), - Some(spec.central.asset.mesh_torso_upper( + Some(spec.central.read().0.mesh_torso_upper( body.species, body.body_type, )), - Some(spec.central.asset.mesh_torso_lower( + Some(spec.central.read().0.mesh_torso_lower( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_shoulder_l( + Some(spec.lateral.read().0.mesh_shoulder_l( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_shoulder_r( + Some(spec.lateral.read().0.mesh_shoulder_r( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_hand_l( + Some(spec.lateral.read().0.mesh_hand_l( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_hand_r( + Some(spec.lateral.read().0.mesh_hand_r( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_leg_l( + Some(spec.lateral.read().0.mesh_leg_l( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_leg_r( + Some(spec.lateral.read().0.mesh_leg_r( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_foot_l( + Some(spec.lateral.read().0.mesh_foot_l( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_foot_r( + Some(spec.lateral.read().0.mesh_foot_r( body.species, body.body_type, )), @@ -3529,43 +3525,43 @@ make_vox_spec!( }, |FigureKey { body, .. }, spec| { [ - Some(spec.central.asset.mesh_head_upper( + Some(spec.central.read().0.mesh_head_upper( body.species, body.body_type, )), - Some(spec.central.asset.mesh_head_lower( + Some(spec.central.read().0.mesh_head_lower( body.species, body.body_type, )), - Some(spec.central.asset.mesh_jaw( + Some(spec.central.read().0.mesh_jaw( body.species, body.body_type, )), - Some(spec.central.asset.mesh_chest( + Some(spec.central.read().0.mesh_chest( body.species, body.body_type, )), - Some(spec.central.asset.mesh_tail_front( + Some(spec.central.read().0.mesh_tail_front( body.species, body.body_type, )), - Some(spec.central.asset.mesh_tail_rear( + Some(spec.central.read().0.mesh_tail_rear( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_foot_fl( + Some(spec.lateral.read().0.mesh_foot_fl( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_foot_fr( + Some(spec.lateral.read().0.mesh_foot_fr( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_foot_bl( + Some(spec.lateral.read().0.mesh_foot_bl( body.species, body.body_type, )), - Some(spec.lateral.asset.mesh_foot_br( + Some(spec.lateral.read().0.mesh_foot_br( body.species, body.body_type, )), @@ -3765,7 +3761,7 @@ make_vox_spec!( }, |FigureKey { body, .. }, spec| { [ - Some(spec.central.asset.mesh_bone0( + Some(spec.central.read().0.mesh_bone0( body, )), None, diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index 0c1b6e2fa6..47803338c4 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -7,7 +7,7 @@ use crate::{ }, }; use common::{ - assets::Asset, + assets::{AssetExt, DotVoxAsset}, comp::{item::Reagent, object, Body, CharacterState, Ori, Pos, Shockwave}, figure::Segment, outcome::Outcome, @@ -18,7 +18,6 @@ use common::{ terrain::TerrainChunk, vol::{RectRasterableVol, SizedVol}, }; -use dot_vox::DotVoxData; use hashbrown::HashMap; use rand::prelude::*; use specs::{Join, WorldExt}; @@ -686,7 +685,7 @@ fn default_cache(renderer: &mut Renderer) -> HashMap<&'static str, Model HashMap<&'static str, Model::generate_mesh(segment, &mut greedy).0; diff --git a/voxygen/src/scene/terrain.rs b/voxygen/src/scene/terrain.rs index bb4e1bc8e9..13676cba11 100644 --- a/voxygen/src/scene/terrain.rs +++ b/voxygen/src/scene/terrain.rs @@ -13,7 +13,7 @@ use crate::{ use super::{math, LodData, SceneData}; use common::{ - assets::{Asset, Ron}, + assets::{self, AssetExt, DotVoxAsset}, figure::Segment, span, spiral::Spiral2d, @@ -23,11 +23,9 @@ use common::{ }; use core::{f32, fmt::Debug, i32, marker::PhantomData, time::Duration}; use crossbeam::channel; -use dot_vox::DotVoxData; use enum_iterator::IntoEnumIterator; use guillotiere::AtlasAllocator; use hashbrown::HashMap; -use image::DynamicImage; use serde::Deserialize; use std::sync::Arc; use tracing::warn; @@ -125,7 +123,15 @@ struct SpriteConfig { /// Configuration data for all sprite models. /// /// NOTE: Model is an asset path to the appropriate sprite .vox model. -type SpriteSpec = sprite::sprite_kind::PureCases>>; +#[derive(Deserialize)] +#[serde(transparent)] +struct SpriteSpec(sprite::sprite_kind::PureCases>>); + +impl assets::Asset for SpriteSpec { + type Loader = assets::RonLoader; + + const EXTENSION: &'static str = "ron"; +} /// Function executed by worker threads dedicated to chunk meshing. #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 @@ -177,7 +183,7 @@ fn mesh_worker + RectRasterableVol + ReadVol + Debug + ' continue; }; - if let Some(cfg) = sprite.elim_case_pure(&sprite_config) { + if let Some(cfg) = sprite.elim_case_pure(&sprite_config.0) { let seed = wpos.x as u64 * 3 + wpos.y as u64 * 7 + wpos.x as u64 * wpos.y as u64; // Awful PRNG @@ -227,6 +233,9 @@ struct SpriteData { pub struct Terrain { atlas: AtlasAllocator, + /// FIXME: This could possibly become an `AssetHandle`, to get + /// hot-reloading for free, but I am not sure if sudden changes of this + /// value would break something sprite_config: Arc, chunks: HashMap, TerrainChunkData>, /// Temporary storage for dead chunks that might still be shadowing chunks @@ -268,8 +277,8 @@ impl Terrain { #[allow(clippy::float_cmp)] // TODO: Pending review in #587 pub fn new(renderer: &mut Renderer) -> Self { // Load all the sprite config data. - let sprite_config = Ron::::load("voxygen.voxel.sprite_manifest") - .expect("Failed to find sprite model data!"); + let sprite_config = + Arc::::load_expect("voxygen.voxel.sprite_manifest").cloned(); // Create a new mpsc (Multiple Produced, Single Consumer) pair for communicating // with worker threads that are meshing chunks. @@ -287,7 +296,7 @@ impl Terrain { // NOTE: Tracks the start vertex of the next model to be meshed. let sprite_data: HashMap<(SpriteKind, usize), _> = SpriteKind::into_enum_iter() - .filter_map(|kind| Some((kind, kind.elim_case_pure(&sprite_config_).as_ref()?))) + .filter_map(|kind| Some((kind, kind.elim_case_pure(&sprite_config_.0).as_ref()?))) .flat_map(|(kind, sprite_config)| { let wind_sway = sprite_config.wind_sway; sprite_config.variations.iter().enumerate().map( @@ -302,9 +311,11 @@ impl Terrain { let scaled = [1.0, 0.8, 0.6, 0.4, 0.2]; let offset = Vec3::from(*offset); let lod_axes = Vec3::from(*lod_axes); - let model = DotVoxData::load_expect(model); + let model = DotVoxAsset::load_expect(model); let zero = Vec3::zero(); let model_size = model + .read() + .0 .models .first() .map( @@ -344,7 +355,7 @@ impl Terrain { // interesting return value, but updates the mesh. let mut opaque_mesh = Mesh::new(); Meshable::::generate_mesh( - Segment::from(model.as_ref()).scaled_by(lod_scale), + Segment::from(&model.read().0).scaled_by(lod_scale), ( greedy, &mut opaque_mesh, @@ -403,7 +414,7 @@ impl Terrain { sprite_col_lights, waves: renderer .create_texture( - &DynamicImage::load_expect("voxygen.texture.waves"), + &assets::Image::load_expect("voxygen.texture.waves").read().0, Some(gfx::texture::FilterMethod::Trilinear), Some(gfx::texture::WrapMode::Tile), None, @@ -1154,7 +1165,7 @@ impl Terrain { for (kind, instances) in (&chunk.sprite_instances).into_iter() { let SpriteData { model, locals, .. } = if kind .0 - .elim_case_pure(&self.sprite_config) + .elim_case_pure(&self.sprite_config.0) .as_ref() .map(|config| config.wind_sway >= 0.4) .unwrap_or(false) diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index c5997b0b5d..779e26f0cc 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -13,7 +13,7 @@ use crate::{ }; use client::{self, Client}; use common::{ - assets::Asset, + assets::AssetExt, comp, comp::{ChatMsg, ChatType, InventoryUpdateEvent, Pos, Vel}, consts::{MAX_MOUNT_RANGE, MAX_PICKUP_RANGE}, @@ -29,7 +29,7 @@ use common::{ use common_net::msg::PresenceKind; use ordered_float::OrderedFloat; use specs::{Join, WorldExt}; -use std::{cell::RefCell, rc::Rc, sync::Arc, time::Duration}; +use std::{cell::RefCell, rc::Rc, time::Duration}; use tracing::{error, info}; use vek::*; @@ -48,7 +48,6 @@ pub struct SessionState { key_state: KeyState, inputs: comp::ControllerInputs, selected_block: Block, - i18n: std::sync::Arc, walk_forward_dir: Vec2, walk_right_dir: Vec2, freefly_vel: Vec3, @@ -75,10 +74,6 @@ impl SessionState { .camera_mut() .set_fov_deg(global_state.settings.graphics.fov); let hud = Hud::new(global_state, &client.borrow()); - let i18n = Localization::load_expect(&i18n_asset_key( - &global_state.settings.language.selected_language, - )); - let walk_forward_dir = scene.camera().forward_xy(); let walk_right_dir = scene.camera().right_xy(); @@ -89,7 +84,6 @@ impl SessionState { inputs: comp::ControllerInputs::default(), hud, selected_block: Block::new(BlockKind::Misc, Rgb::broadcast(255)), - i18n, walk_forward_dir, walk_right_dir, freefly_vel: Vec3::zero(), @@ -125,24 +119,23 @@ impl SessionState { self.hud.new_message(m); }, client::Event::InventoryUpdated(inv_event) => { - let sfx_trigger_item = self - .scene - .sfx_mgr - .triggers - .get_key_value(&SfxEvent::from(&inv_event)); + let sfx_triggers = self.scene.sfx_mgr.triggers.read(); + + let sfx_trigger_item = sfx_triggers.get_key_value(&SfxEvent::from(&inv_event)); global_state.audio.emit_sfx_item(sfx_trigger_item); + let i18n = global_state.i18n.read(); + match inv_event { InventoryUpdateEvent::CollectFailed => { self.hud.new_message(ChatMsg { - message: self.i18n.get("hud.chat.loot_fail").to_string(), + message: i18n.get("hud.chat.loot_fail").to_string(), chat_type: ChatType::CommandError, }); }, InventoryUpdateEvent::Collected(item) => { self.hud.new_message(ChatMsg { - message: self - .i18n + message: i18n .get("hud.chat.loot_msg") .replace("{item}", item.name()), chat_type: ChatType::Loot, @@ -153,10 +146,11 @@ impl SessionState { }, client::Event::Disconnect => return Ok(TickAction::Disconnect), client::Event::DisconnectionNotification(time) => { + let i18n = global_state.i18n.read(); + let message = match time { - 0 => String::from(self.i18n.get("hud.chat.goodbye")), - _ => self - .i18n + 0 => String::from(i18n.get("hud.chat.goodbye")), + _ => i18n .get("hud.chat.connection_lost") .replace("{time}", time.to_string().as_str()), }; @@ -169,7 +163,11 @@ impl SessionState { client::Event::Kicked(reason) => { global_state.info_message = Some(format!( "{}: {}", - self.i18n.get("main.login.kicked").to_string(), + global_state + .i18n + .read() + .get("main.login.kicked") + .to_string(), reason )); return Ok(TickAction::Disconnect); @@ -212,10 +210,6 @@ impl PlayState for SessionState { fn tick(&mut self, global_state: &mut GlobalState, events: Vec) -> PlayStateResult { span!(_guard, "tick", "::tick"); // TODO: let mut client = self.client.borrow_mut(); - // NOTE: Not strictly necessary, but useful for hotloading translation changes. - self.i18n = Localization::load_expect(&i18n_asset_key( - &global_state.settings.language.selected_language, - )); // TODO: can this be a method on the session or are there borrowcheck issues? let (client_presence, client_registered) = { @@ -680,8 +674,13 @@ impl PlayState for SessionState { Ok(TickAction::Continue) => {}, // Do nothing Ok(TickAction::Disconnect) => return PlayStateResult::Pop, // Go to main menu Err(err) => { - global_state.info_message = - Some(self.i18n.get("common.connection_lost").to_owned()); + global_state.info_message = Some( + global_state + .i18n + .read() + .get("common.connection_lost") + .to_owned(), + ); error!("[session] Failed to tick the scene: {:?}", err); return PlayStateResult::Pop; @@ -758,9 +757,9 @@ impl PlayState for SessionState { ); // Look for changes in the localization files - if global_state.localization_watcher.reloaded() { + if global_state.i18n.reloaded() { hud_events.push(HudEvent::ChangeLanguage(Box::new( - self.i18n.metadata.clone(), + global_state.i18n.read().metadata.clone(), ))); } @@ -1016,13 +1015,11 @@ impl PlayState for SessionState { HudEvent::ChangeLanguage(new_language) => { global_state.settings.language.selected_language = new_language.language_identifier; - self.i18n = Localization::load_watched( - &i18n_asset_key(&global_state.settings.language.selected_language), - &mut global_state.localization_watcher, - ) - .unwrap(); - self.i18n.log_missing_entries(); - self.hud.update_language(Arc::clone(&self.i18n)); + global_state.i18n = Localization::load_expect(&i18n_asset_key( + &global_state.settings.language.selected_language, + )); + global_state.i18n.read().log_missing_entries(); + self.hud.update_fonts(&global_state.i18n.read()); }, HudEvent::ChangeFullscreenMode(new_fullscreen_settings) => { global_state diff --git a/voxygen/src/ui/fonts.rs b/voxygen/src/ui/fonts.rs index 583bee4282..bfe5a29884 100644 --- a/voxygen/src/ui/fonts.rs +++ b/voxygen/src/ui/fonts.rs @@ -1,5 +1,5 @@ -use crate::i18n; -use common::assets::Asset; +use crate::{i18n, ui::ice::RawFont}; +use common::assets::{self, AssetExt}; pub struct Font { metadata: i18n::Font, @@ -8,11 +8,13 @@ pub struct Font { impl Font { #[allow(clippy::needless_return)] // TODO: Pending review in #587 - pub fn new(font: &i18n::Font, ui: &mut crate::ui::Ui) -> Self { - Self { + fn new(font: &i18n::Font, ui: &mut crate::ui::Ui) -> Result { + let raw_font = RawFont::load(&font.asset_key)?.cloned(); + + Ok(Self { metadata: font.clone(), - conrod_id: ui.new_font(crate::ui::ice::RawFont::load_expect(&font.asset_key)), - } + conrod_id: ui.new_font(raw_font), + }) } /// Scale input size to final UI size @@ -27,9 +29,9 @@ macro_rules! conrod_fonts { } impl Fonts { - pub fn load(fonts: &i18n::Fonts, ui: &mut crate::ui::Ui) -> Result { + pub fn load(fonts: &i18n::Fonts, ui: &mut crate::ui::Ui) -> Result { Ok(Self { - $( $name: Font::new(fonts.get(stringify!($name)).unwrap(), ui),)* + $( $name: Font::new(fonts.get(stringify!($name)).unwrap(), ui)?, )* }) } } @@ -47,11 +49,13 @@ pub struct IcedFont { } impl IcedFont { - pub fn new(font: &i18n::Font, ui: &mut crate::ui::ice::IcedUi) -> Self { - Self { + fn new(font: &i18n::Font, ui: &mut crate::ui::ice::IcedUi) -> Result { + let raw_font = RawFont::load(&font.asset_key)?.cloned(); + + Ok(Self { metadata: font.clone(), - id: ui.add_font((*crate::ui::ice::RawFont::load_expect(&font.asset_key)).clone()), - } + id: ui.add_font(raw_font), + }) } /// Scale input size to final UI size @@ -67,9 +71,9 @@ macro_rules! iced_fonts { } impl IcedFonts { - pub fn load(fonts: &i18n::Fonts, ui: &mut crate::ui::ice::IcedUi) -> Result { + pub fn load(fonts: &i18n::Fonts, ui: &mut crate::ui::ice::IcedUi) -> Result { Ok(Self { - $( $name: IcedFont::new(fonts.get(stringify!($name)).unwrap(), ui),)* + $( $name: IcedFont::new(fonts.get(stringify!($name)).unwrap(), ui)?, )* }) } } diff --git a/voxygen/src/ui/ice/cache.rs b/voxygen/src/ui/ice/cache.rs index ac8b8f87fc..2757450571 100644 --- a/voxygen/src/ui/ice/cache.rs +++ b/voxygen/src/ui/ice/cache.rs @@ -3,8 +3,12 @@ use crate::{ render::{Renderer, Texture}, Error, }; +use common::assets::{self, AssetExt}; use glyph_brush::GlyphBrushBuilder; -use std::cell::{RefCell, RefMut}; +use std::{ + borrow::Cow, + cell::{RefCell, RefMut}, +}; use vek::*; // Multiplied by current window size @@ -20,6 +24,23 @@ type GlyphBrush = glyph_brush::GlyphBrush<(Aabr, Aabr), ()>; // TODO: might not need pub pub type Font = glyph_brush::ab_glyph::FontArc; +struct FontAsset(Font); +struct FontLoader; +impl assets::Loader for FontLoader { + fn load(data: Cow<[u8]>, _: &str) -> Result { + let font = Font::try_from_vec(data.into_owned())?; + Ok(FontAsset(font)) + } +} + +impl assets::Asset for FontAsset { + type Loader = FontLoader; + + const EXTENSION: &'static str = "ttf"; +} + +pub fn load_font(specifier: &str) -> Font { FontAsset::load_expect(specifier).read().0.clone() } + #[derive(Clone, Copy, Default)] pub struct FontId(pub(super) glyph_brush::FontId); @@ -121,16 +142,13 @@ impl Cache { // TODO: use font type instead of raw vec once we convert to full iced #[derive(Clone)] pub struct RawFont(pub Vec); -impl common::assets::Asset for RawFont { - const ENDINGS: &'static [&'static str] = &["ttf"]; - fn parse( - mut buf_reader: std::io::BufReader, - _specifier: &str, - ) -> Result { - use std::io::Read; - let mut buf = Vec::new(); - buf_reader.read_to_end(&mut buf)?; - Ok(Self(buf)) - } +impl From> for RawFont { + fn from(raw: Vec) -> RawFont { RawFont(raw) } +} + +impl assets::Asset for RawFont { + type Loader = assets::LoadFrom, assets::BytesLoader>; + + const EXTENSION: &'static str = "ttf"; } diff --git a/voxygen/src/ui/ice/mod.rs b/voxygen/src/ui/ice/mod.rs index 6cc78329a6..47515babe6 100644 --- a/voxygen/src/ui/ice/mod.rs +++ b/voxygen/src/ui/ice/mod.rs @@ -4,7 +4,7 @@ pub mod component; mod renderer; pub mod widget; -pub use cache::{Font, FontId, RawFont}; +pub use cache::{load_font, Font, FontId, RawFont}; pub use graphic::{Id, Rotation}; pub use iced::Event; pub use iced_winit::conversion::window_event; diff --git a/voxygen/src/ui/img_ids.rs b/voxygen/src/ui/img_ids.rs index e6e740eb24..660675dc07 100644 --- a/voxygen/src/ui/img_ids.rs +++ b/voxygen/src/ui/img_ids.rs @@ -1,10 +1,8 @@ use super::{Graphic, SampleStrat, Transform}; use common::{ - assets::{Asset, Error}, + assets::{self, AssetExt, DotVoxAsset, Error}, figure::Segment, }; -use dot_vox::DotVoxData; -use image::DynamicImage; use std::sync::Arc; use vek::*; @@ -24,7 +22,8 @@ impl<'a> GraphicCreator<'a> for ImageGraphic { type Specifier = &'a str; fn new_graphic(specifier: Self::Specifier) -> Result { - Ok(Graphic::Image(DynamicImage::load(specifier)?, None)) + let image = assets::Image::load(specifier)?.read().to_image(); + Ok(Graphic::Image(image, None)) } } @@ -37,8 +36,8 @@ pub enum VoxelSs9Graphic {} pub enum VoxelPixArtGraphic {} fn load_segment(specifier: &str) -> Result, Error> { - let dot_vox = DotVoxData::load(specifier)?; - let seg = dot_vox.as_ref().into(); + let dot_vox = DotVoxAsset::load(specifier)?; + let seg = Segment::from(&dot_vox.read().0); Ok(Arc::new(seg)) } diff --git a/voxygen/src/ui/mod.rs b/voxygen/src/ui/mod.rs index b2363afa3e..1ea62ee7dd 100644 --- a/voxygen/src/ui/mod.rs +++ b/voxygen/src/ui/mod.rs @@ -47,7 +47,7 @@ use conrod_core::{ use core::{convert::TryInto, f32, f64, ops::Range}; use graphic::TexId; use hashbrown::hash_map::Entry; -use std::{sync::Arc, time::Duration}; +use std::time::Duration; use tracing::{error, warn}; use vek::*; @@ -215,8 +215,8 @@ impl Ui { self.image_map.replace(id, (graphic_id, Rotation::None)); } - pub fn new_font(&mut self, font: Arc) -> font::Id { - let font = text::Font::from_bytes(font.0.clone()).unwrap(); + pub fn new_font(&mut self, font: crate::ui::ice::RawFont) -> font::Id { + let font = text::Font::from_bytes(font.0).unwrap(); self.ui.fonts.insert(font) } diff --git a/world/examples/settlement_viewer.rs b/world/examples/settlement_viewer.rs index 623876d893..18b6fdc90c 100644 --- a/world/examples/settlement_viewer.rs +++ b/world/examples/settlement_viewer.rs @@ -8,7 +8,7 @@ const H: usize = 480; #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 fn main() { let seed = 1337; - let (ref index, ref colors) = Index::new(seed); + let index = &Index::new(seed); let mut win = minifb::Window::new("Settlement Viewer", W, H, minifb::WindowOptions::default()).unwrap(); @@ -17,6 +17,7 @@ fn main() { let mut focus = Vec2::::zero(); let mut zoom = 1.0; + let colors = &**index.colors().read(); let index = IndexRef { colors, index }; while win.is_open() { diff --git a/world/src/index.rs b/world/src/index.rs index 5ad7b88721..cf20aafda7 100644 --- a/world/src/index.rs +++ b/world/src/index.rs @@ -1,6 +1,6 @@ use crate::{site::Site, Colors}; use common::{ - assets::{watch::ReloadIndicator, Asset, Ron}, + assets::{AssetExt, AssetHandle}, store::Store, }; use core::ops::Deref; @@ -14,7 +14,7 @@ pub struct Index { pub time: f32, pub noise: Noise, pub sites: Store, - indicator: ReloadIndicator, + colors: AssetHandle>, } /// An owned reference to indexed data. @@ -51,26 +51,25 @@ impl<'a> Deref for IndexRef<'a> { impl Index { /// NOTE: Panics if the color manifest cannot be loaded. - pub fn new(seed: u32) -> (Self, Arc) { - let mut indicator = ReloadIndicator::new(); - let colors = Ron::::load_watched(WORLD_COLORS_MANIFEST, &mut indicator) - .expect("Could not load world colors!"); + pub fn new(seed: u32) -> Self { + let colors = Arc::::load_expect(WORLD_COLORS_MANIFEST); - ( - Self { - seed, - time: 0.0, - noise: Noise::new(seed), - sites: Store::default(), - indicator, - }, + Self { + seed, + time: 0.0, + noise: Noise::new(seed), + sites: Store::default(), colors, - ) + } } + + pub fn colors(&self) -> AssetHandle> { self.colors } } impl IndexOwned { - pub fn new(index: Index, colors: Arc) -> Self { + pub fn new(index: Index) -> Self { + let colors = index.colors.cloned(); + Self { index: Arc::new(index), colors, @@ -88,9 +87,9 @@ impl IndexOwned { &mut self, reload: impl FnOnce(&mut Self) -> R, ) -> Option { - self.indicator.reloaded().then(move || { - // We know the asset was loaded before, so load_expect should be fine. - self.colors = Ron::::load_expect(WORLD_COLORS_MANIFEST); + self.index.colors.reloaded_global().then(move || { + // Reload the color from the asset handle, which is updated automatically + self.colors = self.index.colors.cloned(); reload(self) }) } diff --git a/world/src/layer/mod.rs b/world/src/layer/mod.rs index 1891bf1e6a..22c023023f 100644 --- a/world/src/layer/mod.rs +++ b/world/src/layer/mod.rs @@ -10,7 +10,7 @@ use crate::{ Canvas, IndexRef, }; use common::{ - assets::Asset, + assets::AssetExt, comp, generation::{ChunkSupplement, EntityInfo}, lottery::Lottery, @@ -182,7 +182,8 @@ pub fn apply_caves_to(canvas: &mut Canvas) { .chance(wpos2d.into(), 0.001 * difficulty.powf(1.5)) && cave_base < surface_z as i32 - 25 { - let kind = *Lottery::::load_expect("common.cave_scatter") + let lottery = Lottery::::load_expect("common.cave_scatter").read(); + let kind = *lottery .choose_seeded(RandomField::new(info.index().seed + 1).get(wpos2d.into())); canvas.map(Vec3::new(wpos2d.x, wpos2d.y, cave_base), |block| { block.with_sprite(kind) diff --git a/world/src/layer/tree.rs b/world/src/layer/tree.rs index 7160504c0a..ee1e9ba8f3 100644 --- a/world/src/layer/tree.rs +++ b/world/src/layer/tree.rs @@ -6,27 +6,29 @@ use crate::{ Canvas, CONFIG, }; use common::{ - terrain::{structure::Structure, Block, BlockKind}, + assets::AssetHandle, + terrain::{Block, BlockKind, Structure, StructuresGroup}, vol::ReadVol, }; use hashbrown::HashMap; use lazy_static::lazy_static; -use std::{f32, sync::Arc}; +use std::f32; use vek::*; lazy_static! { - pub static ref OAKS: Vec> = Structure::load_group("oaks"); - pub static ref OAK_STUMPS: Vec> = Structure::load_group("oak_stumps"); - pub static ref PINES: Vec> = Structure::load_group("pines"); - pub static ref PALMS: Vec> = Structure::load_group("palms"); - pub static ref ACACIAS: Vec> = Structure::load_group("acacias"); - pub static ref BAOBABS: Vec> = Structure::load_group("baobabs"); - pub static ref FRUIT_TREES: Vec> = Structure::load_group("fruit_trees"); - pub static ref BIRCHES: Vec> = Structure::load_group("birch"); - pub static ref MANGROVE_TREES: Vec> = Structure::load_group("mangrove_trees"); - pub static ref QUIRKY: Vec> = Structure::load_group("quirky"); - pub static ref QUIRKY_DRY: Vec> = Structure::load_group("quirky_dry"); - pub static ref SWAMP_TREES: Vec> = Structure::load_group("swamp_trees"); + static ref OAKS: AssetHandle = Structure::load_group("oaks"); + static ref OAK_STUMPS: AssetHandle = Structure::load_group("oak_stumps"); + static ref PINES: AssetHandle = Structure::load_group("pines"); + static ref PALMS: AssetHandle = Structure::load_group("palms"); + static ref ACACIAS: AssetHandle = Structure::load_group("acacias"); + static ref BAOBABS: AssetHandle = Structure::load_group("baobabs"); + static ref FRUIT_TREES: AssetHandle = Structure::load_group("fruit_trees"); + static ref BIRCHES: AssetHandle = Structure::load_group("birch"); + static ref MANGROVE_TREES: AssetHandle = + Structure::load_group("mangrove_trees"); + static ref QUIRKY: AssetHandle = Structure::load_group("quirky"); + static ref QUIRKY_DRY: AssetHandle = Structure::load_group("quirky_dry"); + static ref SWAMP_TREES: AssetHandle = Structure::load_group("swamp_trees"); } static MODEL_RAND: RandomPerm = RandomPerm::new(0xDB21C052); @@ -37,7 +39,7 @@ static QUIRKY_RAND: RandomPerm = RandomPerm::new(0xA634460F); pub fn apply_trees_to(canvas: &mut Canvas) { struct Tree { pos: Vec3, - model: Arc, + model: Structure, seed: u32, units: (Vec2, Vec2), } @@ -72,34 +74,34 @@ pub fn apply_trees_to(canvas: &mut Canvas) { Some(Tree { pos: Vec3::new(tree_wpos.x, tree_wpos.y, col.alt as i32), model: { - let models: &'static [_] = if is_quirky { + let models: AssetHandle<_> = if is_quirky { if col.temp > CONFIG.desert_temp { - &QUIRKY_DRY + *QUIRKY_DRY } else { - &QUIRKY + *QUIRKY } } else { match col.forest_kind { ForestKind::Oak if QUIRKY_RAND.chance(seed + 1, 1.0 / 16.0) => { - &OAK_STUMPS + *OAK_STUMPS }, ForestKind::Oak if QUIRKY_RAND.chance(seed + 2, 1.0 / 20.0) => { - &FRUIT_TREES + *FRUIT_TREES }, - ForestKind::Palm => &PALMS, - ForestKind::Acacia => &ACACIAS, - ForestKind::Baobab => &BAOBABS, - ForestKind::Oak => &OAKS, - ForestKind::Pine => &PINES, - ForestKind::Birch => &BIRCHES, - ForestKind::Mangrove => &MANGROVE_TREES, - ForestKind::Swamp => &SWAMP_TREES, + ForestKind::Palm => *PALMS, + ForestKind::Acacia => *ACACIAS, + ForestKind::Baobab => *BAOBABS, + ForestKind::Oak => *OAKS, + ForestKind::Pine => *PINES, + ForestKind::Birch => *BIRCHES, + ForestKind::Mangrove => *MANGROVE_TREES, + ForestKind::Swamp => *SWAMP_TREES, } }; - Arc::clone( - &models[(MODEL_RAND.get(seed.wrapping_mul(17)) / 13) as usize - % models.len()], - ) + + let models = models.read(); + models[(MODEL_RAND.get(seed.wrapping_mul(17)) / 13) as usize % models.len()] + .clone() }, seed, units: UNIT_CHOOSER.get(seed), diff --git a/world/src/lib.rs b/world/src/lib.rs index 348ba158f6..770be7c1d5 100644 --- a/world/src/lib.rs +++ b/world/src/lib.rs @@ -41,6 +41,7 @@ use crate::{ util::{Grid, Sampler}, }; use common::{ + assets, generation::{ChunkSupplement, EntityInfo}, terrain::{Block, BlockKind, SpriteKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize}, vol::{ReadVol, RectVolSize, WriteVol}, @@ -70,17 +71,23 @@ pub struct Colors { pub site: site::Colors, } +impl assets::Asset for Colors { + type Loader = assets::RonLoader; + + const EXTENSION: &'static str = "ron"; +} + impl World { pub fn generate(seed: u32, opts: sim::WorldOpts) -> (Self, IndexOwned) { // NOTE: Generating index first in order to quickly fail if the color manifest // is broken. - let (mut index, colors) = Index::new(seed); + let mut index = Index::new(seed); let mut sim = sim::WorldSim::generate(seed, opts); let civs = civ::Civs::generate(seed, &mut sim, &mut index); sim2::simulate(&mut index, &mut sim); - (Self { sim, civs }, IndexOwned::new(index, colors)) + (Self { sim, civs }, IndexOwned::new(index)) } pub fn sim(&self) -> &sim::WorldSim { &self.sim } diff --git a/world/src/sim/mod.rs b/world/src/sim/mod.rs index adff0cb42c..b1e847b972 100644 --- a/world/src/sim/mod.rs +++ b/world/src/sim/mod.rs @@ -35,7 +35,7 @@ use crate::{ IndexRef, CONFIG, }; use common::{ - assets, + assets::{self, AssetExt}, grid::Grid, store::Id, terrain::{ @@ -220,7 +220,7 @@ pub enum WorldFileError { /// invalidation or make sure that the map is synchronized with updates to /// noise-rs, changes to other parameters, etc. /// -/// The map is verisoned to enable format detection between versions of Veloren, +/// The map is versioned to enable format detection between versions of Veloren, /// so that when we update the map format we don't break existing maps (or at /// least, we will try hard not to break maps between versions; if we can't /// avoid it, we can at least give a reasonable error message). @@ -245,6 +245,12 @@ pub enum WorldFile { Veloren0_7_0(WorldMap_0_7_0) = 1, } +impl assets::Asset for WorldFile { + type Loader = assets::BincodeLoader; + + const EXTENSION: &'static str = "bin"; +} + /// Data for the most recent map type. Update this when you add a new map /// version. pub type ModernMap = WorldMap_0_7_0; @@ -408,27 +414,24 @@ impl WorldSim { map.into_modern() }, - FileOpts::LoadAsset(ref specifier) => { - let reader = match assets::load_file(specifier, &["bin"]) { - Ok(reader) => reader, - Err(e) => { - warn!(?e, ?specifier, "Couldn't read asset specifier for maps",); - return None; - }, - }; - - let map: WorldFile = match bincode::deserialize_from(reader) { - Ok(map) => map, - Err(e) => { - warn!( - ?e, - "Couldn't parse modern map. Maybe you meant to try a legacy load?" - ); - return None; - }, - }; - - map.into_modern() + FileOpts::LoadAsset(ref specifier) => match WorldFile::load_owned(&specifier) { + Ok(map) => map.into_modern(), + Err(err) => { + match err { + assets::Error::Io(e) => { + warn!(?e, ?specifier, "Couldn't read asset specifier for maps"); + }, + assets::Error::Conversion(e) => { + warn!( + ?e, + "Couldn't parse modern map. Maybe you meant to try a legacy \ + load?" + ); + }, + assets::Error::NoDefaultValue => unreachable!(), + } + return None; + }, }, FileOpts::Generate | FileOpts::Save => return None, }; diff --git a/world/src/site/dungeon/mod.rs b/world/src/site/dungeon/mod.rs index 47aa2ac104..9f4d550d1d 100644 --- a/world/src/site/dungeon/mod.rs +++ b/world/src/site/dungeon/mod.rs @@ -8,13 +8,13 @@ use crate::{ IndexRef, }; use common::{ - assets::Asset, + assets::{AssetExt, AssetHandle}, astar::Astar, comp::{self}, generation::{ChunkSupplement, EntityInfo}, lottery::Lottery, store::{Id, Store}, - terrain::{Block, BlockKind, SpriteKind, Structure, TerrainChunkSize}, + terrain::{Block, BlockKind, SpriteKind, Structure, StructuresGroup, TerrainChunkSize}, vol::{BaseVol, ReadVol, RectSizedVol, RectVolSize, WriteVol}, }; use core::{f32, hash::BuildHasherDefault}; @@ -22,7 +22,6 @@ use fxhash::FxHasher64; use lazy_static::lazy_static; use rand::prelude::*; use serde::Deserialize; -use std::sync::Arc; use vek::*; pub struct Dungeon { @@ -111,11 +110,12 @@ impl Dungeon { vol: &mut (impl BaseVol + RectSizedVol + ReadVol + WriteVol), ) { lazy_static! { - pub static ref ENTRANCES: Vec> = + pub static ref ENTRANCES: AssetHandle = Structure::load_group("dungeon_entrances"); } - let entrance = &ENTRANCES[self.seed as usize % ENTRANCES.len()]; + let entrances = ENTRANCES.read(); + let entrance = &entrances[self.seed as usize % entrances.len()]; for y in 0..vol.size_xy().y as i32 { for x in 0..vol.size_xy().x as i32 { @@ -581,6 +581,7 @@ impl Floor { "common.loot_tables.loot_table_armor_misc", ), }; + let chosen = chosen.read(); let chosen = chosen.choose(); //let is_giant = // RandomField::new(room.seed.wrapping_add(1)).chance(Vec3::from(tile_pos), @@ -741,6 +742,7 @@ impl Floor { "common.loot_tables.loot_table_armor_misc", ), }; + let chosen = chosen.read(); let chosen = chosen.choose(); let entity = match room.difficulty { 0 => vec![ @@ -923,6 +925,7 @@ impl Floor { "common.loot_tables.loot_table_armor_misc", ), }; + let chosen = chosen.read(); let chosen = chosen.choose(); let entity = match room.difficulty { 0 => vec![