veloren_voxygen/scene/terrain/
sprite.rs

1use std::ops::Range;
2
3use super::SPRITE_LOD_LEVELS;
4use common::{
5    assets::{BoxedError, FileAsset, load_ron},
6    terrain::{
7        Block, SpriteKind,
8        sprite::{self, RelativeNeighborPosition},
9    },
10};
11use hashbrown::HashMap;
12use serde::Deserialize;
13use std::borrow::Cow;
14use vek::*;
15
16#[derive(Deserialize, Debug)]
17/// Configuration data for an individual sprite model.
18#[serde(deny_unknown_fields)]
19pub(super) struct SpriteModelConfig {
20    /// Data for the .vox model associated with this sprite.
21    pub model: String,
22    /// Sprite model center (as an offset from 0 in the .vox file).
23    pub offset: (f32, f32, f32),
24    /// LOD axes (how LOD gets applied along each axis, when we switch
25    /// to an LOD model).
26    pub lod_axes: (f32, f32, f32),
27}
28
29macro_rules! impl_sprite_attribute_filter {
30    (
31        $($attr:ident $field_name:ident = |$filter_arg:ident: $filter_ty:ty, $value_arg:ident| $filter:block),+ $(,)?
32    ) => {
33        // TODO: depending on what types of filters we end up with an enum may end up being more suitable.
34        #[derive(Debug, Clone, Deserialize, Default, PartialEq, Eq, Hash)]
35        #[serde(default, deny_unknown_fields)]
36        pub struct SpriteAttributeFilters {
37            $(
38                pub $field_name: Option<$filter_ty>,
39            )+
40        }
41
42        impl SpriteAttributeFilters {
43            fn matches_filter(&self, block: &Block) -> bool {
44                $(
45                    self.$field_name.as_ref().map_or(true, |$filter_arg| {
46                        block
47                            .get_attr::<sprite::$attr>()
48                            .map_or(false, |$value_arg| $filter)
49                    })
50                )&&+
51            }
52
53            #[cfg(test)]
54            fn is_valid_for_category(&self, category: sprite::Category) -> Result<(), &'static str> {
55                $(if self.$field_name.is_some() && !category.has_attr::<sprite::$attr>() {
56                    return Err(::std::any::type_name::<sprite::$attr>());
57                })*
58                Ok(())
59            }
60
61            fn no_filters(&self) -> bool {
62                true $(&& self.$field_name.is_none())+
63            }
64        }
65    };
66}
67
68impl_sprite_attribute_filter!(
69    Growth growth_stage = |filter: Range<u8>, growth| { filter.contains(&growth.0) },
70    LightEnabled light_enabled = |filter: bool, light_enabled| { *filter == light_enabled.0 },
71    Collectable collectable = |filter: bool, collectable| { *filter == collectable.0 },
72    Damage damage = |filter: Range<u8>, damage| { filter.contains(&damage.0) },
73    AdjacentType adjacent_type = |filter: RelativeNeighborPosition, adjacent_type| { (*filter as u8) == adjacent_type.0 },
74    SnowCovered snow_covered = |filter: bool, snow_covered| { *filter == snow_covered.0 },
75);
76
77/// Configuration data for a group of sprites (currently associated with a
78/// particular SpriteKind).
79#[derive(Deserialize, Debug)]
80#[serde(deny_unknown_fields)]
81struct SpriteConfig {
82    /// Filter for selecting what config to use based on sprite attributes.
83    #[serde(default)]
84    filter: SpriteAttributeFilters,
85    /// All possible model variations for this sprite.
86    // NOTE: Could make constant per sprite type, but eliminating this indirection and
87    // allocation is probably not that important considering how sprites are used.
88    #[serde(default)]
89    variations: Vec<SpriteModelConfig>,
90    /// The extent to which the sprite sways in the wind.
91    ///
92    /// 0.0 is normal.
93    #[serde(default)]
94    wind_sway: f32,
95}
96
97#[serde_with::serde_as]
98#[derive(Deserialize)]
99struct SpriteSpecRaw(
100    #[serde_as(as = "serde_with::MapPreventDuplicates<_, _>")]
101    HashMap<SpriteKind, Vec<SpriteConfig>>,
102);
103
104/// Configuration data for all sprite models.
105///
106/// NOTE: Model is an asset path to the appropriate sprite .vox model.
107#[derive(Deserialize)]
108#[serde(try_from = "SpriteSpecRaw")]
109pub struct SpriteSpec(HashMap<SpriteKind, Vec<SpriteConfig>>);
110
111/// Conversion of [`SpriteSpec`] from a hashmap failed because some sprite kinds
112/// were missing.
113struct SpritesMissing(Vec<SpriteKind>);
114
115impl core::fmt::Display for SpritesMissing {
116    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
117        writeln!(
118            f,
119            "Missing entries in the sprite manifest for these sprites: {:?}",
120            &self.0,
121        )
122    }
123}
124
125impl TryFrom<SpriteSpecRaw> for SpriteSpec {
126    type Error = SpritesMissing;
127
128    fn try_from(SpriteSpecRaw(map): SpriteSpecRaw) -> Result<Self, Self::Error> {
129        let sprites_missing = SpriteKind::all()
130            .iter()
131            .copied()
132            .filter(|kind| !map.contains_key(kind))
133            .collect::<Vec<_>>();
134
135        if sprites_missing.is_empty() {
136            Ok(Self(map))
137        } else {
138            Err(SpritesMissing(sprites_missing))
139        }
140    }
141}
142
143impl FileAsset for SpriteSpec {
144    const EXTENSION: &'static str = "ron";
145
146    fn from_bytes(bytes: Cow<[u8]>) -> Result<Self, BoxedError> { load_ron(&bytes) }
147}
148
149impl SpriteSpec {
150    pub fn map_to_data(
151        &self,
152        mut map_variation: impl FnMut(&SpriteModelConfig) -> [SpriteModelData; super::SPRITE_LOD_LEVELS],
153    ) -> HashMap<SpriteKind, FilteredSpriteData> {
154        let mut to_sprite_data = |config: &SpriteConfig| SpriteData {
155            variations: config.variations.iter().map(&mut map_variation).collect(),
156            wind_sway: config.wind_sway,
157        };
158
159        // Note, the returned datastructure can potentially be optimized further from a
160        // HashMap, a phf could be used or if we can rely on the sprite kind
161        // discriminants in each sprite category being packed fairly densely, we
162        // could just have an offset per sprite catagory used to
163        // convert a sprite kind into a flat index.
164        self.0
165            .iter()
166            .map(|(kind, config)| {
167                let filtered_data = match config.as_slice() {
168                    [config] if config.filter.no_filters() => {
169                        FilteredSpriteData::Unfiltered(to_sprite_data(config))
170                    },
171                    // Note, we have a test that checks if this is completely empty. That should be
172                    // represented by an entry with no variantions instead of having an empty
173                    // top-level list.
174                    filtered_configs => {
175                        let list = filtered_configs
176                            .iter()
177                            .map(|config| (config.filter.clone(), to_sprite_data(config)))
178                            .collect::<Box<[_]>>();
179                        FilteredSpriteData::Filtered(list)
180                    },
181                };
182                (*kind, filtered_data)
183            })
184            .collect()
185    }
186}
187
188pub(in crate::scene) struct SpriteModelData {
189    // Sprite vert page ranges that need to be drawn
190    pub vert_pages: core::ops::Range<u32>,
191    // Scale
192    pub scale: Vec3<f32>,
193    // Offset
194    pub offset: Vec3<f32>,
195}
196
197pub(in crate::scene) struct SpriteData {
198    pub variations: Box<[[SpriteModelData; SPRITE_LOD_LEVELS]]>,
199    /// See [`SpriteConfig::wind_sway`].
200    pub wind_sway: f32,
201}
202
203pub(in crate::scene) enum FilteredSpriteData {
204    // Special case when there is only one entry with the an empty filter since this is most
205    // cases, and it will reduce indirection.
206    Unfiltered(SpriteData),
207    Filtered(Box<[(SpriteAttributeFilters, SpriteData)]>),
208}
209
210impl FilteredSpriteData {
211    /// Gets sprite data for the filter that matches the provided block.
212    ///
213    /// This only returns `None` if no filters matches the provided block (i.e.
214    /// the set of filters does not cover all values). A "missing"
215    /// placeholder model can be displayed in this case in this case.
216    pub fn for_block(&self, block: &Block) -> Option<&SpriteData> {
217        match self {
218            Self::Unfiltered(data) => Some(data),
219            Self::Filtered(multiple) => multiple
220                .iter()
221                .find_map(|(filter, data)| filter.matches_filter(block).then_some(data)),
222        }
223    }
224}
225
226#[cfg(test)]
227mod test {
228    use super::SpriteSpec;
229    use common::assets::AssetExt;
230
231    #[test]
232    fn test_sprite_spec_valid() {
233        let spec = SpriteSpec::load_expect("voxygen.voxel.sprite_manifest").read();
234
235        // Test that filters are relevant for the particular sprite kind.
236        for (sprite, filter) in spec.0.iter().flat_map(|(&sprite, configs)| {
237            configs.iter().map(move |config| (sprite, &config.filter))
238        }) {
239            if let Err(invalid_attribute) = filter.is_valid_for_category(sprite.category()) {
240                panic!(
241                    "Sprite category '{:?}' does not have attribute '{}' (in sprite config for \
242                     {:?})",
243                    sprite.category(),
244                    invalid_attribute,
245                    sprite,
246                );
247            }
248        }
249
250        // Test that there is at least one entry per sprite. An empty variations list in
251        // an entry is used to represent a sprite that doesn't have a model.
252        let mut empty_config = Vec::new();
253        for (kind, configs) in &spec.0 {
254            if configs.is_empty() {
255                empty_config.push(kind)
256            }
257        }
258        assert!(
259            empty_config.is_empty(),
260            "Sprite config(s) with no entries, if these sprite(s) are intended to have no models \
261             use an explicit entry with an empty `variations` list instead: {empty_config:?}",
262        );
263    }
264}