veloren_voxygen/scene/terrain/
sprite.rs1use 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#[serde(deny_unknown_fields)]
19pub(super) struct SpriteModelConfig {
20 pub model: String,
22 pub offset: (f32, f32, f32),
24 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 #[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#[derive(Deserialize, Debug)]
80#[serde(deny_unknown_fields)]
81struct SpriteConfig {
82 #[serde(default)]
84 filter: SpriteAttributeFilters,
85 #[serde(default)]
89 variations: Vec<SpriteModelConfig>,
90 #[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#[derive(Deserialize)]
108#[serde(try_from = "SpriteSpecRaw")]
109pub struct SpriteSpec(HashMap<SpriteKind, Vec<SpriteConfig>>);
110
111struct 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 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 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 pub vert_pages: core::ops::Range<u32>,
191 pub scale: Vec3<f32>,
193 pub offset: Vec3<f32>,
195}
196
197pub(in crate::scene) struct SpriteData {
198 pub variations: Box<[[SpriteModelData; SPRITE_LOD_LEVELS]]>,
199 pub wind_sway: f32,
201}
202
203pub(in crate::scene) enum FilteredSpriteData {
204 Unfiltered(SpriteData),
207 Filtered(Box<[(SpriteAttributeFilters, SpriteData)]>),
208}
209
210impl FilteredSpriteData {
211 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 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 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}