Skip to main content

steel_utils/value_providers/
int.rs

1use serde::{Deserialize, Deserializer, de::Error as _};
2
3use crate::random::Random;
4
5/// An `int`-valued provider.
6///
7/// Mirrors vanilla's `IntProvider` hierarchy used by feature placement and
8/// feature configuration data.
9#[derive(Debug, Clone)]
10pub enum IntProvider {
11    /// Always returns the same value.
12    Constant(i32),
13    /// Uniform inclusive over `[min_inclusive, max_inclusive]`.
14    Uniform {
15        /// Inclusive lower bound.
16        min_inclusive: i32,
17        /// Inclusive upper bound.
18        max_inclusive: i32,
19    },
20    /// Biased toward the bottom.
21    BiasedToBottom {
22        /// Inclusive lower bound.
23        min_inclusive: i32,
24        /// Inclusive upper bound.
25        max_inclusive: i32,
26    },
27    /// Heavily biased toward the bottom.
28    VeryBiasedToBottom {
29        /// Inclusive lower bound.
30        min_inclusive: i32,
31        /// Inclusive upper bound.
32        max_inclusive: i32,
33    },
34    /// Sum of two uniform draws, symmetric triangle when `plateau == 0`.
35    Trapezoid {
36        /// Lower bound.
37        min: i32,
38        /// Upper bound.
39        max: i32,
40        /// Flat-top width.
41        plateau: i32,
42    },
43    /// Gaussian with given mean/deviation, clamped to `[min_inclusive, max_inclusive]`.
44    ClampedNormal {
45        /// Distribution mean.
46        mean: f32,
47        /// Standard deviation.
48        deviation: f32,
49        /// Inclusive lower bound.
50        min_inclusive: i32,
51        /// Inclusive upper bound.
52        max_inclusive: i32,
53    },
54    /// Clamps another provider to an inclusive range.
55    Clamped {
56        /// Source provider.
57        source: Box<IntProvider>,
58        /// Inclusive lower bound.
59        min_inclusive: i32,
60        /// Inclusive upper bound.
61        max_inclusive: i32,
62    },
63    /// Weighted provider selection.
64    WeightedList {
65        /// Weighted alternatives.
66        distribution: Vec<WeightedIntProvider>,
67    },
68}
69
70/// A weighted int-provider entry.
71#[derive(Debug, Clone)]
72pub struct WeightedIntProvider {
73    /// Provider data.
74    pub data: IntProvider,
75    /// Entry weight.
76    pub weight: i32,
77}
78
79/// Uniform inclusive int provider.
80///
81/// This is used for vanilla fields whose codec is specifically `UniformInt`,
82/// not the general `IntProvider` dispatch.
83#[derive(Debug, Clone, Copy)]
84pub struct UniformIntProvider {
85    /// Inclusive lower bound.
86    pub min_inclusive: i32,
87    /// Inclusive upper bound.
88    pub max_inclusive: i32,
89}
90
91impl UniformIntProvider {
92    /// Sample a value.
93    pub fn sample<R: Random + ?Sized>(self, random: &mut R) -> i32 {
94        random.next_i32_between(self.min_inclusive, self.max_inclusive)
95    }
96
97    /// Returns a provider with the same lower bound and a different inclusive upper bound.
98    #[must_use]
99    pub const fn with_max_inclusive(self, max_inclusive: i32) -> Self {
100        Self {
101            min_inclusive: self.min_inclusive,
102            max_inclusive,
103        }
104    }
105}
106
107impl<'de> Deserialize<'de> for UniformIntProvider {
108    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
109        #[derive(Deserialize)]
110        #[serde(deny_unknown_fields)]
111        struct Range {
112            min_inclusive: i32,
113            max_inclusive: i32,
114        }
115
116        #[derive(Deserialize)]
117        #[serde(tag = "type", deny_unknown_fields)]
118        enum Tagged {
119            #[serde(rename = "minecraft:uniform")]
120            Uniform {
121                min_inclusive: i32,
122                max_inclusive: i32,
123            },
124        }
125
126        let value = serde_json::Value::deserialize(d)?;
127        let has_type = value
128            .as_object()
129            .is_some_and(|object| object.contains_key("type"));
130
131        let (min_inclusive, max_inclusive) = if has_type {
132            match serde_json::from_value(value).map_err(D::Error::custom)? {
133                Tagged::Uniform {
134                    min_inclusive,
135                    max_inclusive,
136                } => (min_inclusive, max_inclusive),
137            }
138        } else {
139            let Range {
140                min_inclusive,
141                max_inclusive,
142            } = Range::deserialize(value).map_err(D::Error::custom)?;
143            (min_inclusive, max_inclusive)
144        };
145
146        if min_inclusive > max_inclusive {
147            return Err(D::Error::custom(
148                "UniformIntProvider min_inclusive exceeds max_inclusive",
149            ));
150        }
151
152        Ok(Self {
153            min_inclusive,
154            max_inclusive,
155        })
156    }
157}
158
159impl IntProvider {
160    /// Static lower bound for this provider.
161    #[must_use]
162    pub fn min(&self) -> i32 {
163        match self {
164            Self::Constant(value) => *value,
165            Self::Uniform { min_inclusive, .. }
166            | Self::BiasedToBottom { min_inclusive, .. }
167            | Self::VeryBiasedToBottom { min_inclusive, .. }
168            | Self::Clamped { min_inclusive, .. }
169            | Self::ClampedNormal { min_inclusive, .. } => *min_inclusive,
170            Self::Trapezoid { min, .. } => *min,
171            Self::WeightedList { distribution } => {
172                let mut min = 0;
173                let mut found = false;
174                for entry in distribution {
175                    let value = entry.data.min();
176                    if !found || value < min {
177                        min = value;
178                        found = true;
179                    }
180                }
181                min
182            }
183        }
184    }
185
186    /// Static upper bound for this provider.
187    #[must_use]
188    pub fn max(&self) -> i32 {
189        match self {
190            Self::Constant(value) => *value,
191            Self::Uniform { max_inclusive, .. }
192            | Self::BiasedToBottom { max_inclusive, .. }
193            | Self::VeryBiasedToBottom { max_inclusive, .. }
194            | Self::Clamped { max_inclusive, .. }
195            | Self::ClampedNormal { max_inclusive, .. } => *max_inclusive,
196            Self::Trapezoid { max, .. } => *max,
197            Self::WeightedList { distribution } => {
198                let mut max = 0;
199                let mut found = false;
200                for entry in distribution {
201                    let value = entry.data.max();
202                    if !found || value > max {
203                        max = value;
204                        found = true;
205                    }
206                }
207                max
208            }
209        }
210    }
211
212    /// Sample a value.
213    ///
214    /// Matches vanilla's provider structure. Weighted-list selection is the
215    /// standard total-weight draw used by vanilla's `SimpleWeightedRandomList`.
216    pub fn sample<R: Random + ?Sized>(&self, random: &mut R) -> i32 {
217        match self {
218            Self::Constant(v) => *v,
219            Self::Uniform {
220                min_inclusive,
221                max_inclusive,
222            } => random.next_i32_between(*min_inclusive, *max_inclusive),
223            Self::BiasedToBottom {
224                min_inclusive,
225                max_inclusive,
226            } => {
227                let span = *max_inclusive - *min_inclusive + 1;
228                let bound = random.next_i32_bounded(span) + 1;
229                *min_inclusive + random.next_i32_bounded(bound)
230            }
231            Self::VeryBiasedToBottom {
232                min_inclusive,
233                max_inclusive,
234            } => {
235                let range = *max_inclusive - *min_inclusive + 1;
236                let outer = random.next_i32_bounded(range);
237                let middle = random.next_i32_bounded(outer + 1);
238                *min_inclusive + random.next_i32_bounded(middle + 1)
239            }
240            Self::Trapezoid { min, max, plateau } => {
241                if *plateau == 0 && *max == -*min {
242                    random.next_i32_bounded(*max + 1) - random.next_i32_bounded(*max + 1)
243                } else {
244                    let range = *max - *min;
245                    if *plateau >= range {
246                        random.next_i32_between(*min, *max)
247                    } else {
248                        let plateau_start = (range - *plateau) / 2;
249                        let plateau_end = range - plateau_start;
250                        *min + random.next_i32_between(0, plateau_end)
251                            + random.next_i32_between(0, plateau_start)
252                    }
253                }
254            }
255            Self::ClampedNormal {
256                mean,
257                deviation,
258                min_inclusive,
259                max_inclusive,
260            } => {
261                let sample = *mean + *deviation * random.next_gaussian() as f32;
262                sample.clamp(*min_inclusive as f32, *max_inclusive as f32) as i32
263            }
264            Self::Clamped {
265                source,
266                min_inclusive,
267                max_inclusive,
268            } => source.sample(random).clamp(*min_inclusive, *max_inclusive),
269            Self::WeightedList { distribution } => {
270                let total_weight: i32 = distribution.iter().map(|entry| entry.weight).sum();
271                if total_weight <= 0 {
272                    return 0;
273                }
274                let mut target = random.next_i32_bounded(total_weight);
275                for entry in distribution {
276                    target -= entry.weight;
277                    if target < 0 {
278                        return entry.data.sample(random);
279                    }
280                }
281                0
282            }
283        }
284    }
285}
286
287impl<'de> Deserialize<'de> for IntProvider {
288    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
289        #[derive(Deserialize)]
290        #[serde(tag = "type", deny_unknown_fields)]
291        enum Tagged {
292            #[serde(rename = "minecraft:constant")]
293            Constant { value: i32 },
294            #[serde(rename = "minecraft:uniform")]
295            Uniform {
296                min_inclusive: i32,
297                max_inclusive: i32,
298            },
299            #[serde(rename = "minecraft:biased_to_bottom")]
300            BiasedToBottom {
301                min_inclusive: i32,
302                max_inclusive: i32,
303            },
304            #[serde(rename = "minecraft:very_biased_to_bottom")]
305            VeryBiasedToBottom {
306                min_inclusive: i32,
307                max_inclusive: i32,
308            },
309            #[serde(rename = "minecraft:trapezoid")]
310            Trapezoid { min: i32, max: i32, plateau: i32 },
311            #[serde(rename = "minecraft:clamped_normal")]
312            ClampedNormal {
313                mean: f32,
314                deviation: f32,
315                min_inclusive: i32,
316                max_inclusive: i32,
317            },
318            #[serde(rename = "minecraft:clamped")]
319            Clamped {
320                source: Box<IntProvider>,
321                min_inclusive: i32,
322                max_inclusive: i32,
323            },
324            #[serde(rename = "minecraft:weighted_list")]
325            WeightedList {
326                distribution: Vec<WeightedIntProvider>,
327            },
328        }
329
330        let value = serde_json::Value::deserialize(d)?;
331        if value.is_number() {
332            return Ok(Self::Constant(
333                i32::deserialize(value).map_err(D::Error::custom)?,
334            ));
335        }
336
337        Ok(
338            match serde_json::from_value(value).map_err(D::Error::custom)? {
339                Tagged::Constant { value } => Self::Constant(value),
340                Tagged::Uniform {
341                    min_inclusive,
342                    max_inclusive,
343                } => Self::Uniform {
344                    min_inclusive,
345                    max_inclusive,
346                },
347                Tagged::BiasedToBottom {
348                    min_inclusive,
349                    max_inclusive,
350                } => Self::BiasedToBottom {
351                    min_inclusive,
352                    max_inclusive,
353                },
354                Tagged::VeryBiasedToBottom {
355                    min_inclusive,
356                    max_inclusive,
357                } => Self::VeryBiasedToBottom {
358                    min_inclusive,
359                    max_inclusive,
360                },
361                Tagged::Trapezoid { min, max, plateau } => Self::Trapezoid { min, max, plateau },
362                Tagged::ClampedNormal {
363                    mean,
364                    deviation,
365                    min_inclusive,
366                    max_inclusive,
367                } => Self::ClampedNormal {
368                    mean,
369                    deviation,
370                    min_inclusive,
371                    max_inclusive,
372                },
373                Tagged::Clamped {
374                    source,
375                    min_inclusive,
376                    max_inclusive,
377                } => Self::Clamped {
378                    source,
379                    min_inclusive,
380                    max_inclusive,
381                },
382                Tagged::WeightedList { distribution } => Self::WeightedList { distribution },
383            },
384        )
385    }
386}
387
388impl<'de> Deserialize<'de> for WeightedIntProvider {
389    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
390        #[derive(Deserialize)]
391        #[serde(deny_unknown_fields)]
392        struct Raw {
393            data: IntProvider,
394            weight: i32,
395        }
396
397        let raw = Raw::deserialize(d)?;
398        Ok(Self {
399            data: raw.data,
400            weight: raw.weight,
401        })
402    }
403}