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