Skip to main content

steel_utils/value_providers/
height.rs

1use serde::{Deserialize, Deserializer, de::Error as _};
2
3use crate::random::Random;
4
5use super::VerticalAnchor;
6
7/// An `int`-valued provider parameterised by world-generation bounds
8/// (`min_y`, `height`, `sea_level`).
9///
10/// Mirrors vanilla's `HeightProvider` hierarchy.
11#[derive(Debug, Clone, Copy)]
12pub enum HeightProvider {
13    /// Always resolves to a fixed anchor.
14    Constant(VerticalAnchor),
15    /// Uniform inclusive over \[min, max\].
16    Uniform {
17        /// Inclusive lower bound.
18        min_inclusive: VerticalAnchor,
19        /// Inclusive upper bound.
20        max_inclusive: VerticalAnchor,
21    },
22    /// Sum of two `next_i32_bounded` draws — symmetric triangle when
23    /// `plateau == 0`, trapezoid otherwise.
24    Trapezoid {
25        /// Inclusive lower bound.
26        min_inclusive: VerticalAnchor,
27        /// Inclusive upper bound.
28        max_inclusive: VerticalAnchor,
29        /// Flat-top width; `0` gives a pure triangle.
30        plateau: i32,
31    },
32    /// Biased toward the bottom: two nested `nextInt` draws.
33    BiasedToBottom {
34        /// Inclusive lower bound.
35        min_inclusive: VerticalAnchor,
36        /// Inclusive upper bound.
37        max_inclusive: VerticalAnchor,
38        /// Minimum span of the inner window (default `1`).
39        inner: i32,
40    },
41    /// Heavily biased toward the bottom: three nested `nextInt` draws.
42    VeryBiasedToBottom {
43        /// Inclusive lower bound.
44        min_inclusive: VerticalAnchor,
45        /// Inclusive upper bound.
46        max_inclusive: VerticalAnchor,
47        /// Minimum span of the inner window (default `1`).
48        inner: i32,
49    },
50}
51
52impl HeightProvider {
53    /// Sample a Y coordinate.
54    ///
55    /// Matches vanilla's `HeightProvider.sample` — including the "empty range
56    /// returns min" fallback (vanilla logs a warning once; we silently fall
57    /// back to `min` since this branch isn't hit in practice).
58    pub fn sample<R: Random + ?Sized>(
59        self,
60        random: &mut R,
61        min_y: i32,
62        height: i32,
63        sea_level: i32,
64    ) -> i32 {
65        match self {
66            Self::Constant(anchor) => anchor.resolve_y(min_y, height, sea_level),
67            Self::Uniform {
68                min_inclusive,
69                max_inclusive,
70            } => {
71                let min = min_inclusive.resolve_y(min_y, height, sea_level);
72                let max = max_inclusive.resolve_y(min_y, height, sea_level);
73                if min > max {
74                    min
75                } else {
76                    random.next_i32_between(min, max)
77                }
78            }
79            Self::Trapezoid {
80                min_inclusive,
81                max_inclusive,
82                plateau,
83            } => {
84                let min = min_inclusive.resolve_y(min_y, height, sea_level);
85                let max = max_inclusive.resolve_y(min_y, height, sea_level);
86                if min > max {
87                    min
88                } else {
89                    let range = max - min;
90                    if plateau >= range {
91                        random.next_i32_between(min, max)
92                    } else {
93                        let plateau_start = (range - plateau) / 2;
94                        let plateau_end = range - plateau_start;
95                        min + random.next_i32_between(0, plateau_end)
96                            + random.next_i32_between(0, plateau_start)
97                    }
98                }
99            }
100            Self::BiasedToBottom {
101                min_inclusive,
102                max_inclusive,
103                inner,
104            } => {
105                let min = min_inclusive.resolve_y(min_y, height, sea_level);
106                let max = max_inclusive.resolve_y(min_y, height, sea_level);
107                if max - min - inner < 0 {
108                    min
109                } else {
110                    let limit = random.next_i32_bounded(max - min - inner + 1);
111                    random.next_i32_bounded(limit + inner) + min
112                }
113            }
114            Self::VeryBiasedToBottom {
115                min_inclusive,
116                max_inclusive,
117                inner,
118            } => {
119                let min = min_inclusive.resolve_y(min_y, height, sea_level);
120                let max = max_inclusive.resolve_y(min_y, height, sea_level);
121                if max - min - inner < 0 {
122                    min
123                } else {
124                    let upper_inclusive = random.next_i32_between(min + inner, max);
125                    let biased_upper_inclusive = random.next_i32_between(min, upper_inclusive - 1);
126                    random.next_i32_between(min, biased_upper_inclusive - 1 + inner)
127                }
128            }
129        }
130    }
131}
132
133impl<'de> Deserialize<'de> for HeightProvider {
134    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
135        #[derive(Deserialize)]
136        #[serde(tag = "type", deny_unknown_fields)]
137        enum Tagged {
138            #[serde(rename = "minecraft:constant")]
139            Constant { value: VerticalAnchor },
140            #[serde(rename = "minecraft:uniform")]
141            Uniform {
142                min_inclusive: VerticalAnchor,
143                max_inclusive: VerticalAnchor,
144            },
145            #[serde(rename = "minecraft:trapezoid")]
146            Trapezoid {
147                min_inclusive: VerticalAnchor,
148                max_inclusive: VerticalAnchor,
149                #[serde(default)]
150                plateau: i32,
151            },
152            #[serde(rename = "minecraft:biased_to_bottom")]
153            BiasedToBottom {
154                min_inclusive: VerticalAnchor,
155                max_inclusive: VerticalAnchor,
156                #[serde(default = "default_inner")]
157                inner: i32,
158            },
159            #[serde(rename = "minecraft:very_biased_to_bottom")]
160            VeryBiasedToBottom {
161                min_inclusive: VerticalAnchor,
162                max_inclusive: VerticalAnchor,
163                #[serde(default = "default_inner")]
164                inner: i32,
165            },
166        }
167
168        const fn default_inner() -> i32 {
169            1
170        }
171
172        let value = serde_json::Value::deserialize(d)?;
173        let has_type = value
174            .as_object()
175            .is_some_and(|object| object.contains_key("type"));
176
177        if !has_type {
178            let anchor = VerticalAnchor::deserialize(value).map_err(D::Error::custom)?;
179            return Ok(Self::Constant(anchor));
180        }
181
182        Ok(
183            match serde_json::from_value(value).map_err(D::Error::custom)? {
184                Tagged::Constant { value } => Self::Constant(value),
185                Tagged::Uniform {
186                    min_inclusive,
187                    max_inclusive,
188                } => Self::Uniform {
189                    min_inclusive,
190                    max_inclusive,
191                },
192                Tagged::Trapezoid {
193                    min_inclusive,
194                    max_inclusive,
195                    plateau,
196                } => Self::Trapezoid {
197                    min_inclusive,
198                    max_inclusive,
199                    plateau,
200                },
201                Tagged::BiasedToBottom {
202                    min_inclusive,
203                    max_inclusive,
204                    inner,
205                } => Self::BiasedToBottom {
206                    min_inclusive,
207                    max_inclusive,
208                    inner,
209                },
210                Tagged::VeryBiasedToBottom {
211                    min_inclusive,
212                    max_inclusive,
213                    inner,
214                } => Self::VeryBiasedToBottom {
215                    min_inclusive,
216                    max_inclusive,
217                    inner,
218                },
219            },
220        )
221    }
222}