Skip to main content

steel_utils/
value_providers.rs

1//! Value providers matching vanilla's `VerticalAnchor`, `HeightProvider`,
2//! and `FloatProvider`.
3//!
4//! JSON parsing follows vanilla's codec shape:
5//! * `VerticalAnchor` is a single-key object: `{"absolute": 180}`,
6//!   `{"above_bottom": 8}`, or `{"below_top": 1}`.
7//! * `HeightProvider` accepts either a bare `VerticalAnchor` (shortcut for
8//!   `ConstantHeight`) or a typed object with a namespaced vanilla registry id,
9//!   e.g. `{"type": "minecraft:uniform", ...}`.
10//! * `FloatProvider` accepts either a bare float (shortcut for `ConstantFloat`)
11//!   or a typed object with a namespaced vanilla registry id.
12
13use serde::{Deserialize, Deserializer, de::Error as _};
14
15use crate::random::Random;
16
17// ── VerticalAnchor ───────────────────────────────────────────────────────────
18
19/// A vertical anchor resolving to a world Y coordinate given the dimension
20/// bounds.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum VerticalAnchor {
23    /// Absolute Y coordinate.
24    Absolute(i32),
25    /// `min_y + offset`.
26    AboveBottom(i32),
27    /// `min_y + height - 1 - offset` (i.e. `max_y - offset`).
28    BelowTop(i32),
29}
30
31impl VerticalAnchor {
32    /// Resolve this anchor to a world Y coordinate.
33    ///
34    /// Matches vanilla's `VerticalAnchor.resolveY(WorldGenerationContext)`.
35    #[must_use]
36    pub const fn resolve_y(self, min_y: i32, height: i32) -> i32 {
37        match self {
38            Self::Absolute(y) => y,
39            Self::AboveBottom(offset) => min_y + offset,
40            Self::BelowTop(offset) => min_y + height - 1 - offset,
41        }
42    }
43}
44
45impl<'de> Deserialize<'de> for VerticalAnchor {
46    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
47        #[derive(Deserialize)]
48        #[serde(deny_unknown_fields)]
49        struct Raw {
50            #[serde(default)]
51            absolute: Option<i32>,
52            #[serde(default)]
53            above_bottom: Option<i32>,
54            #[serde(default)]
55            below_top: Option<i32>,
56        }
57        let raw = Raw::deserialize(d)?;
58        match (raw.absolute, raw.above_bottom, raw.below_top) {
59            (Some(y), None, None) => Ok(Self::Absolute(y)),
60            (None, Some(o), None) => Ok(Self::AboveBottom(o)),
61            (None, None, Some(o)) => Ok(Self::BelowTop(o)),
62            (None, None, None) => Err(D::Error::custom(
63                "VerticalAnchor requires exactly one of absolute/above_bottom/below_top",
64            )),
65            _ => Err(D::Error::custom(
66                "VerticalAnchor must have exactly one of absolute/above_bottom/below_top",
67            )),
68        }
69    }
70}
71
72// ── HeightProvider ───────────────────────────────────────────────────────────
73
74/// An `int`-valued provider parameterised by world-generation bounds
75/// (`min_y`, `height`).
76///
77/// Mirrors vanilla's `HeightProvider` hierarchy.
78#[derive(Debug, Clone, Copy)]
79pub enum HeightProvider {
80    /// Always resolves to a fixed anchor.
81    Constant(VerticalAnchor),
82    /// Uniform inclusive over \[min, max\].
83    Uniform {
84        /// Inclusive lower bound.
85        min_inclusive: VerticalAnchor,
86        /// Inclusive upper bound.
87        max_inclusive: VerticalAnchor,
88    },
89    /// Sum of two `next_i32_bounded` draws — symmetric triangle when
90    /// `plateau == 0`, trapezoid otherwise.
91    Trapezoid {
92        /// Inclusive lower bound.
93        min_inclusive: VerticalAnchor,
94        /// Inclusive upper bound.
95        max_inclusive: VerticalAnchor,
96        /// Flat-top width; `0` gives a pure triangle.
97        plateau: i32,
98    },
99    /// Biased toward the bottom: two nested `nextInt` draws.
100    BiasedToBottom {
101        /// Inclusive lower bound.
102        min_inclusive: VerticalAnchor,
103        /// Inclusive upper bound.
104        max_inclusive: VerticalAnchor,
105        /// Minimum span of the inner window (default `1`).
106        inner: i32,
107    },
108    /// Heavily biased toward the bottom: three nested `nextInt` draws.
109    VeryBiasedToBottom {
110        /// Inclusive lower bound.
111        min_inclusive: VerticalAnchor,
112        /// Inclusive upper bound.
113        max_inclusive: VerticalAnchor,
114        /// Minimum span of the inner window (default `1`).
115        inner: i32,
116    },
117}
118
119impl HeightProvider {
120    /// Sample a Y coordinate.
121    ///
122    /// Matches vanilla's `HeightProvider.sample` — including the "empty range
123    /// returns min" fallback (vanilla logs a warning once; we silently fall
124    /// back to `min` since this branch isn't hit in practice).
125    pub fn sample<R: Random + ?Sized>(self, random: &mut R, min_y: i32, height: i32) -> i32 {
126        match self {
127            Self::Constant(anchor) => anchor.resolve_y(min_y, height),
128            Self::Uniform {
129                min_inclusive,
130                max_inclusive,
131            } => {
132                let min = min_inclusive.resolve_y(min_y, height);
133                let max = max_inclusive.resolve_y(min_y, height);
134                if min > max {
135                    min
136                } else {
137                    random.next_i32_between(min, max)
138                }
139            }
140            Self::Trapezoid {
141                min_inclusive,
142                max_inclusive,
143                plateau,
144            } => {
145                let min = min_inclusive.resolve_y(min_y, height);
146                let max = max_inclusive.resolve_y(min_y, height);
147                if min > max {
148                    min
149                } else {
150                    let range = max - min;
151                    if plateau >= range {
152                        random.next_i32_between(min, max)
153                    } else {
154                        let plateau_start = (range - plateau) / 2;
155                        let plateau_end = range - plateau_start;
156                        min + random.next_i32_between(0, plateau_end)
157                            + random.next_i32_between(0, plateau_start)
158                    }
159                }
160            }
161            Self::BiasedToBottom {
162                min_inclusive,
163                max_inclusive,
164                inner,
165            } => {
166                let min = min_inclusive.resolve_y(min_y, height);
167                let max = max_inclusive.resolve_y(min_y, height);
168                if max - min - inner < 0 {
169                    min
170                } else {
171                    let limit = random.next_i32_bounded(max - min - inner + 1);
172                    random.next_i32_bounded(limit + inner) + min
173                }
174            }
175            Self::VeryBiasedToBottom {
176                min_inclusive,
177                max_inclusive,
178                inner,
179            } => {
180                let min = min_inclusive.resolve_y(min_y, height);
181                let max = max_inclusive.resolve_y(min_y, height);
182                if max - min - inner < 0 {
183                    min
184                } else {
185                    let upper_inclusive = random.next_i32_between(min + inner, max);
186                    let biased_upper_inclusive = random.next_i32_between(min, upper_inclusive - 1);
187                    random.next_i32_between(min, biased_upper_inclusive - 1 + inner)
188                }
189            }
190        }
191    }
192}
193
194impl<'de> Deserialize<'de> for HeightProvider {
195    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
196        #[derive(Deserialize)]
197        #[serde(tag = "type", deny_unknown_fields)]
198        enum Tagged {
199            #[serde(rename = "minecraft:constant")]
200            Constant { value: VerticalAnchor },
201            #[serde(rename = "minecraft:uniform")]
202            Uniform {
203                min_inclusive: VerticalAnchor,
204                max_inclusive: VerticalAnchor,
205            },
206            #[serde(rename = "minecraft:trapezoid")]
207            Trapezoid {
208                min_inclusive: VerticalAnchor,
209                max_inclusive: VerticalAnchor,
210                #[serde(default)]
211                plateau: i32,
212            },
213            #[serde(rename = "minecraft:biased_to_bottom")]
214            BiasedToBottom {
215                min_inclusive: VerticalAnchor,
216                max_inclusive: VerticalAnchor,
217                #[serde(default = "default_inner")]
218                inner: i32,
219            },
220            #[serde(rename = "minecraft:very_biased_to_bottom")]
221            VeryBiasedToBottom {
222                min_inclusive: VerticalAnchor,
223                max_inclusive: VerticalAnchor,
224                #[serde(default = "default_inner")]
225                inner: i32,
226            },
227        }
228
229        const fn default_inner() -> i32 {
230            1
231        }
232
233        let value = serde_json::Value::deserialize(d)?;
234        let has_type = value
235            .as_object()
236            .is_some_and(|object| object.contains_key("type"));
237
238        if !has_type {
239            let anchor = VerticalAnchor::deserialize(value).map_err(D::Error::custom)?;
240            return Ok(Self::Constant(anchor));
241        }
242
243        Ok(
244            match serde_json::from_value(value).map_err(D::Error::custom)? {
245                Tagged::Constant { value } => Self::Constant(value),
246                Tagged::Uniform {
247                    min_inclusive,
248                    max_inclusive,
249                } => Self::Uniform {
250                    min_inclusive,
251                    max_inclusive,
252                },
253                Tagged::Trapezoid {
254                    min_inclusive,
255                    max_inclusive,
256                    plateau,
257                } => Self::Trapezoid {
258                    min_inclusive,
259                    max_inclusive,
260                    plateau,
261                },
262                Tagged::BiasedToBottom {
263                    min_inclusive,
264                    max_inclusive,
265                    inner,
266                } => Self::BiasedToBottom {
267                    min_inclusive,
268                    max_inclusive,
269                    inner,
270                },
271                Tagged::VeryBiasedToBottom {
272                    min_inclusive,
273                    max_inclusive,
274                    inner,
275                } => Self::VeryBiasedToBottom {
276                    min_inclusive,
277                    max_inclusive,
278                    inner,
279                },
280            },
281        )
282    }
283}
284
285// ── IntProvider ──────────────────────────────────────────────────────────────
286
287/// An `int`-valued provider.
288///
289/// Mirrors vanilla's `IntProvider` hierarchy used by feature placement and
290/// feature configuration data.
291#[derive(Debug, Clone)]
292pub enum IntProvider {
293    /// Always returns the same value.
294    Constant(i32),
295    /// Uniform inclusive over `[min_inclusive, max_inclusive]`.
296    Uniform {
297        /// Inclusive lower bound.
298        min_inclusive: i32,
299        /// Inclusive upper bound.
300        max_inclusive: i32,
301    },
302    /// Biased toward the bottom.
303    BiasedToBottom {
304        /// Inclusive lower bound.
305        min_inclusive: i32,
306        /// Inclusive upper bound.
307        max_inclusive: i32,
308    },
309    /// Heavily biased toward the bottom.
310    VeryBiasedToBottom {
311        /// Inclusive lower bound.
312        min_inclusive: i32,
313        /// Inclusive upper bound.
314        max_inclusive: i32,
315        /// Minimum span of the inner window.
316        inner: i32,
317    },
318    /// Sum of two uniform draws, symmetric triangle when `plateau == 0`.
319    Trapezoid {
320        /// Lower bound.
321        min: i32,
322        /// Upper bound.
323        max: i32,
324        /// Flat-top width.
325        plateau: i32,
326    },
327    /// Gaussian with given mean/deviation, clamped to `[min_inclusive, max_inclusive]`.
328    ClampedNormal {
329        /// Distribution mean.
330        mean: f32,
331        /// Standard deviation.
332        deviation: f32,
333        /// Inclusive lower bound.
334        min_inclusive: i32,
335        /// Inclusive upper bound.
336        max_inclusive: i32,
337    },
338    /// Clamps another provider to an inclusive range.
339    Clamped {
340        /// Source provider.
341        source: Box<IntProvider>,
342        /// Inclusive lower bound.
343        min_inclusive: i32,
344        /// Inclusive upper bound.
345        max_inclusive: i32,
346    },
347    /// Weighted provider selection.
348    WeightedList {
349        /// Weighted alternatives.
350        distribution: Vec<WeightedIntProvider>,
351    },
352}
353
354/// A weighted int-provider entry.
355#[derive(Debug, Clone)]
356pub struct WeightedIntProvider {
357    /// Provider data.
358    pub data: IntProvider,
359    /// Entry weight.
360    pub weight: i32,
361}
362
363/// Uniform inclusive int provider.
364///
365/// This is used for vanilla fields whose codec is specifically `UniformInt`,
366/// not the general `IntProvider` dispatch.
367#[derive(Debug, Clone, Copy)]
368pub struct UniformIntProvider {
369    /// Inclusive lower bound.
370    pub min_inclusive: i32,
371    /// Inclusive upper bound.
372    pub max_inclusive: i32,
373}
374
375impl UniformIntProvider {
376    /// Sample a value.
377    pub fn sample<R: Random + ?Sized>(self, random: &mut R) -> i32 {
378        random.next_i32_between(self.min_inclusive, self.max_inclusive)
379    }
380
381    /// Returns a provider with the same lower bound and a different inclusive upper bound.
382    #[must_use]
383    pub const fn with_max_inclusive(self, max_inclusive: i32) -> Self {
384        Self {
385            min_inclusive: self.min_inclusive,
386            max_inclusive,
387        }
388    }
389}
390
391impl<'de> Deserialize<'de> for UniformIntProvider {
392    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
393        #[derive(Deserialize)]
394        #[serde(deny_unknown_fields)]
395        struct Range {
396            min_inclusive: i32,
397            max_inclusive: i32,
398        }
399
400        #[derive(Deserialize)]
401        #[serde(tag = "type", deny_unknown_fields)]
402        enum Tagged {
403            #[serde(rename = "minecraft:uniform")]
404            Uniform {
405                min_inclusive: i32,
406                max_inclusive: i32,
407            },
408        }
409
410        let value = serde_json::Value::deserialize(d)?;
411        let has_type = value
412            .as_object()
413            .is_some_and(|object| object.contains_key("type"));
414
415        let (min_inclusive, max_inclusive) = if has_type {
416            match serde_json::from_value(value).map_err(D::Error::custom)? {
417                Tagged::Uniform {
418                    min_inclusive,
419                    max_inclusive,
420                } => (min_inclusive, max_inclusive),
421            }
422        } else {
423            let Range {
424                min_inclusive,
425                max_inclusive,
426            } = Range::deserialize(value).map_err(D::Error::custom)?;
427            (min_inclusive, max_inclusive)
428        };
429
430        if min_inclusive > max_inclusive {
431            return Err(D::Error::custom(
432                "UniformIntProvider min_inclusive exceeds max_inclusive",
433            ));
434        }
435
436        Ok(Self {
437            min_inclusive,
438            max_inclusive,
439        })
440    }
441}
442
443impl IntProvider {
444    /// Static lower bound for this provider.
445    #[must_use]
446    pub fn min(&self) -> i32 {
447        match self {
448            Self::Constant(value) => *value,
449            Self::Uniform { min_inclusive, .. }
450            | Self::BiasedToBottom { min_inclusive, .. }
451            | Self::VeryBiasedToBottom { min_inclusive, .. }
452            | Self::Clamped { min_inclusive, .. }
453            | Self::ClampedNormal { min_inclusive, .. } => *min_inclusive,
454            Self::Trapezoid { min, .. } => *min,
455            Self::WeightedList { distribution } => {
456                let mut min = 0;
457                let mut found = false;
458                for entry in distribution {
459                    let value = entry.data.min();
460                    if !found || value < min {
461                        min = value;
462                        found = true;
463                    }
464                }
465                min
466            }
467        }
468    }
469
470    /// Static upper bound for this provider.
471    #[must_use]
472    pub fn max(&self) -> i32 {
473        match self {
474            Self::Constant(value) => *value,
475            Self::Uniform { max_inclusive, .. }
476            | Self::BiasedToBottom { max_inclusive, .. }
477            | Self::VeryBiasedToBottom { max_inclusive, .. }
478            | Self::Clamped { max_inclusive, .. }
479            | Self::ClampedNormal { max_inclusive, .. } => *max_inclusive,
480            Self::Trapezoid { max, .. } => *max,
481            Self::WeightedList { distribution } => {
482                let mut max = 0;
483                let mut found = false;
484                for entry in distribution {
485                    let value = entry.data.max();
486                    if !found || value > max {
487                        max = value;
488                        found = true;
489                    }
490                }
491                max
492            }
493        }
494    }
495
496    /// Sample a value.
497    ///
498    /// Matches vanilla's provider structure. Weighted-list selection is the
499    /// standard total-weight draw used by vanilla's `SimpleWeightedRandomList`.
500    pub fn sample<R: Random + ?Sized>(&self, random: &mut R) -> i32 {
501        match self {
502            Self::Constant(v) => *v,
503            Self::Uniform {
504                min_inclusive,
505                max_inclusive,
506            } => random.next_i32_between(*min_inclusive, *max_inclusive),
507            Self::BiasedToBottom {
508                min_inclusive,
509                max_inclusive,
510            } => {
511                let span = *max_inclusive - *min_inclusive + 1;
512                let bound = random.next_i32_bounded(span) + 1;
513                *min_inclusive + random.next_i32_bounded(bound)
514            }
515            Self::VeryBiasedToBottom {
516                min_inclusive,
517                max_inclusive,
518                inner,
519            } => {
520                let limit = *max_inclusive - *min_inclusive - *inner + 1;
521                if limit <= 0 {
522                    *min_inclusive
523                } else {
524                    let upper_inclusive = random.next_i32_bounded(limit) + *min_inclusive + *inner;
525                    let biased_upper_inclusive =
526                        random.next_i32_between(*min_inclusive, upper_inclusive - 1);
527                    random.next_i32_between(*min_inclusive, biased_upper_inclusive - 1 + *inner)
528                }
529            }
530            Self::Trapezoid { min, max, plateau } => {
531                if *plateau == 0 && *max == -*min {
532                    random.next_i32_bounded(*max + 1) - random.next_i32_bounded(*max + 1)
533                } else {
534                    let range = *max - *min;
535                    if *plateau >= range {
536                        random.next_i32_between(*min, *max)
537                    } else {
538                        let plateau_start = (range - *plateau) / 2;
539                        let plateau_end = range - plateau_start;
540                        *min + random.next_i32_between(0, plateau_end)
541                            + random.next_i32_between(0, plateau_start)
542                    }
543                }
544            }
545            Self::ClampedNormal {
546                mean,
547                deviation,
548                min_inclusive,
549                max_inclusive,
550            } => {
551                let sample = *mean + *deviation * random.next_gaussian() as f32;
552                sample.clamp(*min_inclusive as f32, *max_inclusive as f32) as i32
553            }
554            Self::Clamped {
555                source,
556                min_inclusive,
557                max_inclusive,
558            } => source.sample(random).clamp(*min_inclusive, *max_inclusive),
559            Self::WeightedList { distribution } => {
560                let total_weight: i32 = distribution.iter().map(|entry| entry.weight).sum();
561                if total_weight <= 0 {
562                    return 0;
563                }
564                let mut target = random.next_i32_bounded(total_weight);
565                for entry in distribution {
566                    target -= entry.weight;
567                    if target < 0 {
568                        return entry.data.sample(random);
569                    }
570                }
571                0
572            }
573        }
574    }
575}
576
577impl<'de> Deserialize<'de> for IntProvider {
578    #[expect(
579        clippy::too_many_lines,
580        reason = "keeps the vanilla int-provider schema variants in one deserialization table"
581    )]
582    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
583        #[derive(Deserialize)]
584        #[serde(tag = "type", deny_unknown_fields)]
585        enum Tagged {
586            #[serde(rename = "minecraft:constant")]
587            Constant { value: i32 },
588            #[serde(rename = "minecraft:uniform")]
589            Uniform {
590                min_inclusive: i32,
591                max_inclusive: i32,
592            },
593            #[serde(rename = "minecraft:biased_to_bottom")]
594            BiasedToBottom {
595                min_inclusive: i32,
596                max_inclusive: i32,
597            },
598            #[serde(rename = "minecraft:very_biased_to_bottom")]
599            VeryBiasedToBottom {
600                min_inclusive: i32,
601                max_inclusive: i32,
602                #[serde(default = "default_inner")]
603                inner: i32,
604            },
605            #[serde(rename = "minecraft:trapezoid")]
606            Trapezoid { min: i32, max: i32, plateau: i32 },
607            #[serde(rename = "minecraft:clamped_normal")]
608            ClampedNormal {
609                mean: f32,
610                deviation: f32,
611                min_inclusive: i32,
612                max_inclusive: i32,
613            },
614            #[serde(rename = "minecraft:clamped")]
615            Clamped {
616                source: Box<IntProvider>,
617                min_inclusive: i32,
618                max_inclusive: i32,
619            },
620            #[serde(rename = "minecraft:weighted_list")]
621            WeightedList {
622                distribution: Vec<WeightedIntProvider>,
623            },
624        }
625
626        const fn default_inner() -> i32 {
627            1
628        }
629
630        let value = serde_json::Value::deserialize(d)?;
631        if value.is_number() {
632            return Ok(Self::Constant(
633                i32::deserialize(value).map_err(D::Error::custom)?,
634            ));
635        }
636
637        Ok(
638            match serde_json::from_value(value).map_err(D::Error::custom)? {
639                Tagged::Constant { value } => Self::Constant(value),
640                Tagged::Uniform {
641                    min_inclusive,
642                    max_inclusive,
643                } => Self::Uniform {
644                    min_inclusive,
645                    max_inclusive,
646                },
647                Tagged::BiasedToBottom {
648                    min_inclusive,
649                    max_inclusive,
650                } => Self::BiasedToBottom {
651                    min_inclusive,
652                    max_inclusive,
653                },
654                Tagged::VeryBiasedToBottom {
655                    min_inclusive,
656                    max_inclusive,
657                    inner,
658                } => Self::VeryBiasedToBottom {
659                    min_inclusive,
660                    max_inclusive,
661                    inner,
662                },
663                Tagged::Trapezoid { min, max, plateau } => Self::Trapezoid { min, max, plateau },
664                Tagged::ClampedNormal {
665                    mean,
666                    deviation,
667                    min_inclusive,
668                    max_inclusive,
669                } => Self::ClampedNormal {
670                    mean,
671                    deviation,
672                    min_inclusive,
673                    max_inclusive,
674                },
675                Tagged::Clamped {
676                    source,
677                    min_inclusive,
678                    max_inclusive,
679                } => Self::Clamped {
680                    source,
681                    min_inclusive,
682                    max_inclusive,
683                },
684                Tagged::WeightedList { distribution } => Self::WeightedList { distribution },
685            },
686        )
687    }
688}
689
690impl<'de> Deserialize<'de> for WeightedIntProvider {
691    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
692        #[derive(Deserialize)]
693        #[serde(deny_unknown_fields)]
694        struct Raw {
695            data: IntProvider,
696            weight: i32,
697        }
698
699        let raw = Raw::deserialize(d)?;
700        Ok(Self {
701            data: raw.data,
702            weight: raw.weight,
703        })
704    }
705}
706
707// ── FloatProvider ────────────────────────────────────────────────────────────
708
709/// A `float`-valued provider.
710///
711/// Mirrors vanilla's `FloatProvider` hierarchy. `WeightedList` is omitted
712/// until a carver or feature needs it.
713#[derive(Debug, Clone, Copy)]
714pub enum FloatProvider {
715    /// Always returns the same value.
716    Constant(f32),
717    /// Uniform over `[min_inclusive, max_exclusive)`.
718    Uniform {
719        /// Inclusive lower bound.
720        min_inclusive: f32,
721        /// Exclusive upper bound.
722        max_exclusive: f32,
723    },
724    /// Sum of two uniform draws — symmetric triangle when `plateau == 0`,
725    /// trapezoid otherwise.
726    Trapezoid {
727        /// Lower bound.
728        min: f32,
729        /// Upper bound.
730        max: f32,
731        /// Flat-top width.
732        plateau: f32,
733    },
734    /// Gaussian with given mean/deviation, clamped to `[min, max]`.
735    ClampedNormal {
736        /// Distribution mean.
737        mean: f32,
738        /// Standard deviation.
739        deviation: f32,
740        /// Inclusive lower bound.
741        min: f32,
742        /// Inclusive upper bound.
743        max: f32,
744    },
745}
746
747impl FloatProvider {
748    /// Sample a value.
749    ///
750    /// Matches vanilla's `FloatProvider.sample` exactly. Order of
751    /// `random.next_*` calls is preserved for hash-level determinism.
752    pub fn sample<R: Random + ?Sized>(self, random: &mut R) -> f32 {
753        match self {
754            Self::Constant(v) => v,
755            Self::Uniform {
756                min_inclusive,
757                max_exclusive,
758            } => random.next_f32() * (max_exclusive - min_inclusive) + min_inclusive,
759            Self::Trapezoid { min, max, plateau } => {
760                let range = max - min;
761                let plateau_start = (range - plateau) / 2.0;
762                let plateau_end = range - plateau_start;
763                min + random.next_f32() * plateau_end + random.next_f32() * plateau_start
764            }
765            Self::ClampedNormal {
766                mean,
767                deviation,
768                min,
769                max,
770            } => {
771                // Mth.normal: mean + deviation * (float)nextGaussian()
772                let sample = mean + deviation * random.next_gaussian() as f32;
773                sample.clamp(min, max)
774            }
775        }
776    }
777
778    /// Static lower bound.
779    #[must_use]
780    pub const fn min(self) -> f32 {
781        match self {
782            Self::Constant(v) => v,
783            Self::Uniform { min_inclusive, .. } => min_inclusive,
784            Self::Trapezoid { min, .. } | Self::ClampedNormal { min, .. } => min,
785        }
786    }
787
788    /// Static upper bound.
789    #[must_use]
790    pub const fn max(self) -> f32 {
791        match self {
792            Self::Constant(v) => v,
793            Self::Uniform { max_exclusive, .. } => max_exclusive,
794            Self::Trapezoid { max, .. } | Self::ClampedNormal { max, .. } => max,
795        }
796    }
797}
798
799impl<'de> Deserialize<'de> for FloatProvider {
800    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
801        #[derive(Deserialize)]
802        #[serde(tag = "type", deny_unknown_fields)]
803        enum Tagged {
804            #[serde(rename = "minecraft:constant")]
805            Constant { value: f32 },
806            #[serde(rename = "minecraft:uniform")]
807            Uniform {
808                min_inclusive: f32,
809                max_exclusive: f32,
810            },
811            #[serde(rename = "minecraft:trapezoid")]
812            Trapezoid { min: f32, max: f32, plateau: f32 },
813            #[serde(rename = "minecraft:clamped_normal")]
814            ClampedNormal {
815                mean: f32,
816                deviation: f32,
817                min: f32,
818                max: f32,
819            },
820        }
821
822        let value = serde_json::Value::deserialize(d)?;
823        if value.is_number() {
824            return Ok(Self::Constant(
825                f32::deserialize(value).map_err(D::Error::custom)?,
826            ));
827        }
828
829        Ok(
830            match serde_json::from_value(value).map_err(D::Error::custom)? {
831                Tagged::Constant { value: v } => Self::Constant(v),
832                Tagged::Uniform {
833                    min_inclusive,
834                    max_exclusive,
835                } => Self::Uniform {
836                    min_inclusive,
837                    max_exclusive,
838                },
839                Tagged::Trapezoid { min, max, plateau } => Self::Trapezoid { min, max, plateau },
840                Tagged::ClampedNormal {
841                    mean,
842                    deviation,
843                    min,
844                    max,
845                } => Self::ClampedNormal {
846                    mean,
847                    deviation,
848                    min,
849                    max,
850                },
851            },
852        )
853    }
854}
855
856// ── Tests ────────────────────────────────────────────────────────────────────
857
858#[cfg(test)]
859#[expect(
860    clippy::unwrap_used,
861    clippy::float_cmp,
862    reason = "test assertions: unwrap panics on parse failures, float equality is the check"
863)]
864mod test {
865    use super::*;
866    use crate::random::legacy_random::LegacyRandom;
867
868    #[test]
869    fn vertical_anchor_resolve() {
870        assert_eq!(VerticalAnchor::Absolute(42).resolve_y(-64, 384), 42);
871        assert_eq!(VerticalAnchor::AboveBottom(8).resolve_y(-64, 384), -56);
872        assert_eq!(VerticalAnchor::BelowTop(1).resolve_y(0, 128), 126);
873    }
874
875    #[test]
876    fn vertical_anchor_deserialize() {
877        let a: VerticalAnchor = serde_json::from_str(r#"{"absolute": 180}"#).unwrap();
878        assert_eq!(a, VerticalAnchor::Absolute(180));
879        let b: VerticalAnchor = serde_json::from_str(r#"{"above_bottom": 8}"#).unwrap();
880        assert_eq!(b, VerticalAnchor::AboveBottom(8));
881        let c: VerticalAnchor = serde_json::from_str(r#"{"below_top": 1}"#).unwrap();
882        assert_eq!(c, VerticalAnchor::BelowTop(1));
883        assert!(serde_json::from_str::<VerticalAnchor>(r"{}").is_err());
884        assert!(
885            serde_json::from_str::<VerticalAnchor>(r#"{"absolute": 1, "above_bottom": 2}"#)
886                .is_err()
887        );
888    }
889
890    #[test]
891    fn height_provider_deserialize_shortcut() {
892        // A bare VerticalAnchor is a ConstantHeight.
893        let hp: HeightProvider = serde_json::from_str(r#"{"absolute": 180}"#).unwrap();
894        match hp {
895            HeightProvider::Constant(VerticalAnchor::Absolute(180)) => (),
896            other => panic!("expected Constant(Absolute(180)), got {other:?}"),
897        }
898    }
899
900    #[test]
901    fn height_provider_uniform_from_carver_json() {
902        let hp: HeightProvider = serde_json::from_str(
903            r#"{
904                "type": "minecraft:uniform",
905                "max_inclusive": {"absolute": 180},
906                "min_inclusive": {"above_bottom": 8}
907            }"#,
908        )
909        .unwrap();
910        match hp {
911            HeightProvider::Uniform {
912                min_inclusive,
913                max_inclusive,
914            } => {
915                assert_eq!(min_inclusive, VerticalAnchor::AboveBottom(8));
916                assert_eq!(max_inclusive, VerticalAnchor::Absolute(180));
917            }
918            other => panic!("expected Uniform, got {other:?}"),
919        }
920    }
921
922    #[test]
923    fn float_provider_bare_float() {
924        let fp: FloatProvider = serde_json::from_str("3.0").unwrap();
925        match fp {
926            FloatProvider::Constant(v) => assert!((v - 3.0).abs() < 1e-6),
927            other => panic!("expected Constant, got {other:?}"),
928        }
929    }
930
931    #[test]
932    fn float_provider_uniform_from_carver_json() {
933        let fp: FloatProvider = serde_json::from_str(
934            r#"{
935                "type": "minecraft:uniform",
936                "max_exclusive": 1.4,
937                "min_inclusive": 0.7
938            }"#,
939        )
940        .unwrap();
941        match fp {
942            FloatProvider::Uniform {
943                min_inclusive,
944                max_exclusive,
945            } => {
946                assert!((min_inclusive - 0.7).abs() < 1e-6);
947                assert!((max_exclusive - 1.4).abs() < 1e-6);
948            }
949            other => panic!("expected Uniform, got {other:?}"),
950        }
951    }
952
953    #[test]
954    fn float_provider_trapezoid_from_carver_json() {
955        let fp: FloatProvider = serde_json::from_str(
956            r#"{
957                "type": "minecraft:trapezoid",
958                "max": 6.0,
959                "min": 0.0,
960                "plateau": 2.0
961            }"#,
962        )
963        .unwrap();
964        match fp {
965            FloatProvider::Trapezoid { min, max, plateau } => {
966                assert_eq!(min, 0.0);
967                assert_eq!(max, 6.0);
968                assert_eq!(plateau, 2.0);
969            }
970            other => panic!("expected Trapezoid, got {other:?}"),
971        }
972    }
973
974    #[test]
975    fn int_provider_clamped_normal_prefers_tagged_shape() {
976        let provider: IntProvider = serde_json::from_str(
977            r#"{
978                "type": "minecraft:clamped_normal",
979                "mean": 0.0,
980                "deviation": 3.0,
981                "min_inclusive": -10,
982                "max_inclusive": 10
983            }"#,
984        )
985        .unwrap();
986
987        match provider {
988            IntProvider::ClampedNormal {
989                mean,
990                deviation,
991                min_inclusive,
992                max_inclusive,
993            } => {
994                assert_eq!(mean, 0.0);
995                assert_eq!(deviation, 3.0);
996                assert_eq!(min_inclusive, -10);
997                assert_eq!(max_inclusive, 10);
998            }
999            other => panic!("expected ClampedNormal, got {other:?}"),
1000        }
1001    }
1002
1003    #[test]
1004    fn provider_type_tags_require_extracted_registry_ids() {
1005        assert!(
1006            serde_json::from_str::<HeightProvider>(
1007                r#"{
1008                    "type": "uniform",
1009                    "max_inclusive": {"absolute": 180},
1010                    "min_inclusive": {"above_bottom": 8}
1011                }"#,
1012            )
1013            .is_err()
1014        );
1015        assert!(
1016            serde_json::from_str::<UniformIntProvider>(
1017                r#"{
1018                    "type": "uniform",
1019                    "min_inclusive": 0,
1020                    "max_inclusive": 10
1021                }"#,
1022            )
1023            .is_err()
1024        );
1025        assert!(
1026            serde_json::from_str::<IntProvider>(
1027                r#"{
1028                    "type": "uniform",
1029                    "min_inclusive": 0,
1030                    "max_inclusive": 10
1031                }"#,
1032            )
1033            .is_err()
1034        );
1035        assert!(
1036            serde_json::from_str::<FloatProvider>(
1037                r#"{
1038                    "type": "uniform",
1039                    "min_inclusive": 0.0,
1040                    "max_exclusive": 1.0
1041                }"#,
1042            )
1043            .is_err()
1044        );
1045    }
1046
1047    #[test]
1048    fn provider_typed_payloads_deny_unknown_fields() {
1049        assert!(
1050            serde_json::from_str::<HeightProvider>(
1051                r#"{
1052                    "type": "minecraft:uniform",
1053                    "max_inclusive": {"absolute": 180},
1054                    "min_inclusive": {"above_bottom": 8},
1055                    "extra": 0
1056                }"#,
1057            )
1058            .is_err()
1059        );
1060        assert!(
1061            serde_json::from_str::<UniformIntProvider>(
1062                r#"{
1063                    "type": "minecraft:uniform",
1064                    "min_inclusive": 0,
1065                    "max_inclusive": 10,
1066                    "extra": 0
1067                }"#,
1068            )
1069            .is_err()
1070        );
1071        assert!(
1072            serde_json::from_str::<IntProvider>(
1073                r#"{
1074                    "type": "minecraft:clamped",
1075                    "source": 4,
1076                    "min_inclusive": 0,
1077                    "max_inclusive": 10,
1078                    "extra": 0
1079                }"#,
1080            )
1081            .is_err()
1082        );
1083        assert!(
1084            serde_json::from_str::<FloatProvider>(
1085                r#"{
1086                    "type": "minecraft:uniform",
1087                    "min_inclusive": 0.0,
1088                    "max_exclusive": 1.0,
1089                    "extra": 0.0
1090                }"#,
1091            )
1092            .is_err()
1093        );
1094    }
1095
1096    #[test]
1097    fn int_provider_requires_typed_object_or_bare_constant() {
1098        assert!(
1099            serde_json::from_str::<IntProvider>(
1100                r#"{
1101                    "min_inclusive": 0,
1102                    "max_inclusive": 10
1103                }"#,
1104            )
1105            .is_err()
1106        );
1107        assert!(
1108            serde_json::from_str::<IntProvider>(
1109                r#"{
1110                    "type": "minecraft:weighted_list",
1111                    "distribution": [
1112                        {
1113                            "data": 1,
1114                            "weight": 2,
1115                            "extra": 3
1116                        }
1117                    ]
1118                }"#,
1119            )
1120            .is_err()
1121        );
1122    }
1123
1124    #[test]
1125    fn int_provider_symmetric_trapezoid_sample_matches_vanilla_shortcut() {
1126        let provider = IntProvider::Trapezoid {
1127            min: -7,
1128            max: 7,
1129            plateau: 0,
1130        };
1131        let mut rng = LegacyRandom::from_seed(123);
1132        let mut rng_ref = LegacyRandom::from_seed(123);
1133        let sample = provider.sample(&mut rng);
1134        let expected = rng_ref.next_i32_bounded(8) - rng_ref.next_i32_bounded(8);
1135        assert_eq!(sample, expected);
1136    }
1137
1138    /// Matches vanilla's `Mth.randomBetween`: `min + nextFloat()*(max-min)`.
1139    #[test]
1140    fn float_provider_uniform_sample_matches_vanilla() {
1141        let fp = FloatProvider::Uniform {
1142            min_inclusive: 0.7,
1143            max_exclusive: 1.4,
1144        };
1145        let mut rng = LegacyRandom::from_seed(0);
1146        let mut rng_ref = LegacyRandom::from_seed(0);
1147        let sample = fp.sample(&mut rng);
1148        let expected = rng_ref.next_f32() * (1.4 - 0.7) + 0.7;
1149        assert_eq!(sample, expected);
1150    }
1151
1152    /// Height uniform sample: `random.nextInt(max - min + 1) + min`.
1153    #[test]
1154    fn height_provider_uniform_sample_matches_vanilla() {
1155        let hp = HeightProvider::Uniform {
1156            min_inclusive: VerticalAnchor::AboveBottom(8),
1157            max_inclusive: VerticalAnchor::Absolute(180),
1158        };
1159        let min_y = -64;
1160        let height = 384;
1161        let mut rng = LegacyRandom::from_seed(42);
1162        let mut rng_ref = LegacyRandom::from_seed(42);
1163        let sample = hp.sample(&mut rng, min_y, height);
1164        // min_y + 8 = -56, absolute 180
1165        let expected = rng_ref.next_i32_between(-56, 180);
1166        assert_eq!(sample, expected);
1167    }
1168}