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