veloren_voxygen/scene/terrain/
sprite.rs1use std::ops::Range;
2
3use super::SPRITE_LOD_LEVELS;
4use common::{
5 assets::{BoxedError, FileAsset, load_ron},
6 figure::Cell,
7 terrain::{
8 Block, SpriteKind,
9 sprite::{self, RelativeNeighborPosition},
10 },
11};
12use hashbrown::HashMap;
13use serde::Deserialize;
14use std::borrow::Cow;
15use vek::*;
16
17#[derive(Deserialize, Debug)]
18#[serde(deny_unknown_fields)]
20pub(super) struct SpriteModelConfig {
21 pub model: String,
23 pub offset: (f32, f32, f32),
25 pub lod_axes: (f32, f32, f32),
28 #[serde(default)]
30 pub custom_indices: HashMap<u8, Cell>,
31}
32
33macro_rules! impl_sprite_attribute_filter {
34 (
35 $($attr:ident $field_name:ident = |$filter_arg:ident: $filter_ty:ty, $value_arg:ident| $filter:block),+ $(,)?
36 ) => {
37 #[derive(Debug, Clone, Deserialize, Default, PartialEq, Eq, Hash)]
39 #[serde(default, deny_unknown_fields)]
40 pub struct SpriteAttributeFilters {
41 $(
42 pub $field_name: Option<$filter_ty>,
43 )+
44 }
45
46 impl SpriteAttributeFilters {
47 fn matches_filter(&self, block: &Block) -> bool {
48 $(
49 self.$field_name.as_ref().map_or(true, |$filter_arg| {
50 block
51 .get_attr::<sprite::$attr>()
52 .map_or(false, |$value_arg| $filter)
53 })
54 )&&+
55 }
56
57 #[cfg(test)]
58 fn is_valid_for_category(&self, category: sprite::Category) -> Result<(), &'static str> {
59 $(if self.$field_name.is_some() && !category.has_attr::<sprite::$attr>() {
60 return Err(::std::any::type_name::<sprite::$attr>());
61 })*
62 Ok(())
63 }
64
65 fn no_filters(&self) -> bool {
66 true $(&& self.$field_name.is_none())+
67 }
68 }
69 };
70}
71
72impl_sprite_attribute_filter!(
73 Growth growth_stage = |filter: Range<u8>, growth| { filter.contains(&growth.0) },
74 LightEnabled light_enabled = |filter: bool, light_enabled| { *filter == light_enabled.0 },
75 Collectable collectable = |filter: bool, collectable| { *filter == collectable.0 },
76 Damage damage = |filter: Range<u8>, damage| { filter.contains(&damage.0) },
77 AdjacentType adjacent_type = |filter: RelativeNeighborPosition, adjacent_type| { (*filter as u8) == adjacent_type.0 },
78 SnowCovered snow_covered = |filter: bool, snow_covered| { *filter == snow_covered.0 },
79);
80
81#[derive(Deserialize, Debug)]
84#[serde(deny_unknown_fields)]
85struct SpriteConfig {
86 #[serde(default)]
88 filter: SpriteAttributeFilters,
89 #[serde(default)]
93 variations: Vec<SpriteModelConfig>,
94 #[serde(default)]
98 wind_sway: f32,
99}
100
101#[serde_with::serde_as]
102#[derive(Deserialize)]
103struct SpriteSpecRaw(
104 #[serde_as(as = "serde_with::MapPreventDuplicates<_, _>")]
105 HashMap<SpriteKind, Vec<SpriteConfig>>,
106);
107
108#[derive(Deserialize)]
112#[serde(try_from = "SpriteSpecRaw")]
113pub struct SpriteSpec(HashMap<SpriteKind, Vec<SpriteConfig>>);
114
115struct SpritesMissing(Vec<SpriteKind>);
118
119impl core::fmt::Display for SpritesMissing {
120 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
121 writeln!(
122 f,
123 "Missing entries in the sprite manifest for these sprites: {:?}",
124 &self.0,
125 )
126 }
127}
128
129impl TryFrom<SpriteSpecRaw> for SpriteSpec {
130 type Error = SpritesMissing;
131
132 fn try_from(SpriteSpecRaw(map): SpriteSpecRaw) -> Result<Self, Self::Error> {
133 let sprites_missing = SpriteKind::all()
134 .iter()
135 .copied()
136 .filter(|kind| !map.contains_key(kind))
137 .collect::<Vec<_>>();
138
139 if sprites_missing.is_empty() {
140 Ok(Self(map))
141 } else {
142 Err(SpritesMissing(sprites_missing))
143 }
144 }
145}
146
147impl FileAsset for SpriteSpec {
148 const EXTENSION: &'static str = "ron";
149
150 fn from_bytes(bytes: Cow<[u8]>) -> Result<Self, BoxedError> { load_ron(&bytes) }
151}
152
153impl SpriteSpec {
154 pub fn map_to_data(
155 &self,
156 mut map_variation: impl FnMut(&SpriteModelConfig) -> [SpriteModelData; super::SPRITE_LOD_LEVELS],
157 ) -> HashMap<SpriteKind, FilteredSpriteData> {
158 let mut to_sprite_data = |config: &SpriteConfig| SpriteData {
159 variations: config.variations.iter().map(&mut map_variation).collect(),
160 wind_sway: config.wind_sway,
161 };
162
163 self.0
169 .iter()
170 .map(|(kind, config)| {
171 let filtered_data = match config.as_slice() {
172 [config] if config.filter.no_filters() => {
173 FilteredSpriteData::Unfiltered(to_sprite_data(config))
174 },
175 filtered_configs => {
179 let list = filtered_configs
180 .iter()
181 .map(|config| (config.filter.clone(), to_sprite_data(config)))
182 .collect::<Box<[_]>>();
183 FilteredSpriteData::Filtered(list)
184 },
185 };
186 (*kind, filtered_data)
187 })
188 .collect()
189 }
190}
191
192pub(in crate::scene) struct SpriteModelData {
193 pub vert_pages: core::ops::Range<u32>,
195 pub scale: Vec3<f32>,
197 pub offset: Vec3<f32>,
199}
200
201pub(in crate::scene) struct SpriteData {
202 pub variations: Box<[[SpriteModelData; SPRITE_LOD_LEVELS]]>,
203 pub wind_sway: f32,
205}
206
207pub(in crate::scene) enum FilteredSpriteData {
208 Unfiltered(SpriteData),
211 Filtered(Box<[(SpriteAttributeFilters, SpriteData)]>),
212}
213
214impl FilteredSpriteData {
215 pub fn for_block(&self, block: &Block) -> Option<&SpriteData> {
221 match self {
222 Self::Unfiltered(data) => Some(data),
223 Self::Filtered(multiple) => multiple
224 .iter()
225 .find_map(|(filter, data)| filter.matches_filter(block).then_some(data)),
226 }
227 }
228}
229
230#[cfg(test)]
231mod test {
232 use super::SpriteSpec;
233 use common::assets::AssetExt;
234
235 #[test]
236 fn test_sprite_spec_valid() {
237 let spec = SpriteSpec::load_expect("voxygen.voxel.sprite_manifest").read();
238
239 for (sprite, filter) in spec.0.iter().flat_map(|(&sprite, configs)| {
241 configs.iter().map(move |config| (sprite, &config.filter))
242 }) {
243 if let Err(invalid_attribute) = filter.is_valid_for_category(sprite.category()) {
244 panic!(
245 "Sprite category '{:?}' does not have attribute '{}' (in sprite config for \
246 {:?})",
247 sprite.category(),
248 invalid_attribute,
249 sprite,
250 );
251 }
252 }
253
254 let mut empty_config = Vec::new();
257 for (kind, configs) in &spec.0 {
258 if configs.is_empty() {
259 empty_config.push(kind)
260 }
261 }
262 assert!(
263 empty_config.is_empty(),
264 "Sprite config(s) with no entries, if these sprite(s) are intended to have no models \
265 use an explicit entry with an empty `variations` list instead: {empty_config:?}",
266 );
267 }
268}