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    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/// Configuration data for an individual sprite model.
19#[serde(deny_unknown_fields)]
20pub(super) struct SpriteModelConfig {
21    /// Data for the .vox model associated with this sprite.
22    pub model: String,
23    /// Sprite model center (as an offset from 0 in the .vox file).
24    pub offset: (f32, f32, f32),
25    /// LOD axes (how LOD gets applied along each axis, when we switch
26    /// to an LOD model).
27    pub lod_axes: (f32, f32, f32),
28    /// Custom overrides for index to `Cell` mapping.
29    #[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        // TODO: depending on what types of filters we end up with an enum may end up being more suitable.
38        #[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/// Configuration data for a group of sprites (currently associated with a
82/// particular SpriteKind).
83#[derive(Deserialize, Debug)]
84#[serde(deny_unknown_fields)]
85struct SpriteConfig {
86    /// Filter for selecting what config to use based on sprite attributes.
87    #[serde(default)]
88    filter: SpriteAttributeFilters,
89    /// All possible model variations for this sprite.
90    // NOTE: Could make constant per sprite type, but eliminating this indirection and
91    // allocation is probably not that important considering how sprites are used.
92    #[serde(default)]
93    variations: Vec<SpriteModelConfig>,
94    /// The extent to which the sprite sways in the wind.
95    ///
96    /// 0.0 is normal.
97    #[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/// Configuration data for all sprite models.
109///
110/// NOTE: Model is an asset path to the appropriate sprite .vox model.
111#[derive(Deserialize)]
112#[serde(try_from = "SpriteSpecRaw")]
113pub struct SpriteSpec(HashMap<SpriteKind, Vec<SpriteConfig>>);
114
115/// Conversion of [`SpriteSpec`] from a hashmap failed because some sprite kinds
116/// were missing.
117struct 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        // Note, the returned datastructure can potentially be optimized further from a
164        // HashMap, a phf could be used or if we can rely on the sprite kind
165        // discriminants in each sprite category being packed fairly densely, we
166        // could just have an offset per sprite catagory used to
167        // convert a sprite kind into a flat index.
168        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                    // Note, we have a test that checks if this is completely empty. That should be
176                    // represented by an entry with no variantions instead of having an empty
177                    // top-level list.
178                    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    // Sprite vert page ranges that need to be drawn
194    pub vert_pages: core::ops::Range<u32>,
195    // Scale
196    pub scale: Vec3<f32>,
197    // Offset
198    pub offset: Vec3<f32>,
199}
200
201pub(in crate::scene) struct SpriteData {
202    pub variations: Box<[[SpriteModelData; SPRITE_LOD_LEVELS]]>,
203    /// See [`SpriteConfig::wind_sway`].
204    pub wind_sway: f32,
205}
206
207pub(in crate::scene) enum FilteredSpriteData {
208    // Special case when there is only one entry with the an empty filter since this is most
209    // cases, and it will reduce indirection.
210    Unfiltered(SpriteData),
211    Filtered(Box<[(SpriteAttributeFilters, SpriteData)]>),
212}
213
214impl FilteredSpriteData {
215    /// Gets sprite data for the filter that matches the provided block.
216    ///
217    /// This only returns `None` if no filters matches the provided block (i.e.
218    /// the set of filters does not cover all values). A "missing"
219    /// placeholder model can be displayed in this case in this case.
220    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        // Test that filters are relevant for the particular sprite kind.
240        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        // Test that there is at least one entry per sprite. An empty variations list in
255        // an entry is used to represent a sprite that doesn't have a model.
256        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}