Skip to main content

steel_utils/random/
worldgen_random.rs

1use crate::random::{
2    Random, RandomSplitter, gaussian::MarsagliaPolarGaussian, xoroshiro::Xoroshiro,
3};
4
5/// Vanilla's `WorldgenRandom` when constructed for biome decoration.
6///
7/// Feature decoration always constructs `WorldgenRandom(new XoroshiroRandomSource(...))`.
8/// Sampling then goes through `BitRandomSource.next*`, so it does not match raw
9/// `XoroshiroRandomSource` for `nextInt`, bounded ints, doubles, longs, or gaussians.
10pub struct WorldgenRandom {
11    source: Xoroshiro,
12    next_gaussian: Option<f64>,
13}
14
15impl WorldgenRandom {
16    /// Creates a new `WorldgenRandom` backed by vanilla's `XoroshiroRandomSource`.
17    #[must_use]
18    pub const fn from_seed(seed: u64) -> Self {
19        Self {
20            source: Xoroshiro::from_seed(seed),
21            next_gaussian: None,
22        }
23    }
24
25    /// Re-seeds the backing `XoroshiroRandomSource`.
26    ///
27    /// Vanilla `WorldgenRandom` inherits its gaussian cache from
28    /// `LegacyRandomSource`, but overrides `setSeed` to only reseed the
29    /// wrapped source. That means `setDecorationSeed` / `setFeatureSeed`
30    /// intentionally preserve a pending gaussian value.
31    pub const fn set_seed(&mut self, seed: i64) {
32        self.source.set_seed(seed);
33    }
34
35    /// Vanilla's `WorldgenRandom.setDecorationSeed`.
36    pub fn set_decoration_seed(&mut self, seed: i64, block_x: i32, block_z: i32) -> i64 {
37        self.set_seed(seed);
38        let x_scale = self.next_i64() | 1;
39        let z_scale = self.next_i64() | 1;
40        let decoration_seed = i64::from(block_x)
41            .wrapping_mul(x_scale)
42            .wrapping_add(i64::from(block_z).wrapping_mul(z_scale))
43            ^ seed;
44        self.set_seed(decoration_seed);
45        decoration_seed
46    }
47
48    /// Vanilla's `WorldgenRandom.setFeatureSeed`.
49    pub const fn set_feature_seed(&mut self, decoration_seed: i64, feature_index: i32, step: i32) {
50        let feature_seed = decoration_seed
51            .wrapping_add(feature_index as i64)
52            .wrapping_add(10_000_i64.wrapping_mul(step as i64));
53        self.set_seed(feature_seed);
54    }
55
56    fn next_bits(&mut self, bits: u64) -> u64 {
57        self.source.next_i64() as u64 >> (64 - bits)
58    }
59}
60
61impl MarsagliaPolarGaussian for WorldgenRandom {
62    fn stored_next_gaussian(&self) -> Option<f64> {
63        self.next_gaussian
64    }
65
66    fn set_stored_next_gaussian(&mut self, value: Option<f64>) {
67        self.next_gaussian = value;
68    }
69}
70
71impl Random for WorldgenRandom {
72    fn fork(&mut self) -> Self {
73        Self {
74            source: self.source.fork(),
75            next_gaussian: None,
76        }
77    }
78
79    fn next_i32(&mut self) -> i32 {
80        self.next_bits(32) as i32
81    }
82
83    fn next_i32_bounded(&mut self, bound: i32) -> i32 {
84        if bound & bound.wrapping_sub(1) == 0 {
85            (i64::from(bound).wrapping_mul(i64::from(self.next_bits(31) as i32)) >> 31) as i32
86        } else {
87            loop {
88                let sample = self.next_bits(31) as i32;
89                let modulo = sample % bound;
90                if sample
91                    .wrapping_sub(modulo)
92                    .wrapping_add(bound.wrapping_sub(1))
93                    >= 0
94                {
95                    return modulo;
96                }
97            }
98        }
99    }
100
101    fn next_i64(&mut self) -> i64 {
102        let upper = self.next_i32();
103        let lower = self.next_i32();
104        (i64::from(upper) << 32).wrapping_add(i64::from(lower))
105    }
106
107    fn next_f32(&mut self) -> f32 {
108        self.next_bits(24) as f32 * 5.960_464_5e-8_f32
109    }
110
111    fn next_f64(&mut self) -> f64 {
112        let combined = ((self.next_bits(26) as i64) << 27) + self.next_bits(27) as i64;
113        combined as f64 * (1.0 / (1_i64 << 53) as f64)
114    }
115
116    fn next_bool(&mut self) -> bool {
117        self.next_bits(1) != 0
118    }
119
120    fn next_gaussian(&mut self) -> f64 {
121        self.calculate_gaussian()
122    }
123
124    fn next_positional(&mut self) -> RandomSplitter {
125        self.source.next_positional()
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::WorldgenRandom;
132    use crate::random::Random;
133
134    #[test]
135    fn set_decoration_seed_matches_vanilla_trace() {
136        let mut random = WorldgenRandom::from_seed(0);
137        assert_eq!(
138            random.set_decoration_seed(13_579, -6_695_392, 5_868_656),
139            7_632_291_757_650_236_667,
140        );
141    }
142
143    #[test]
144    fn feature_seed_matches_vanilla_first_ore_dirt_origin() {
145        let mut random = WorldgenRandom::from_seed(0);
146        let decoration_seed = random.set_decoration_seed(13_579, -6_695_392, 5_868_656);
147        random.set_feature_seed(decoration_seed, 0, 6);
148
149        let x = -6_695_392 + random.next_i32_bounded(16);
150        let z = 5_868_656 + random.next_i32_bounded(16);
151        let y = random.next_i32_bounded(161);
152        assert_eq!((x, y, z), (-6_695_386, 149, 5_868_662));
153    }
154
155    #[test]
156    #[expect(
157        clippy::float_cmp,
158        reason = "gaussian cache parity must match vanilla exactly"
159    )]
160    fn feature_seed_preserves_pending_gaussian() {
161        let mut random = WorldgenRandom::from_seed(123);
162        let _ = random.next_gaussian();
163        random.set_feature_seed(456, 7, 8);
164
165        let mut cached_reference = WorldgenRandom::from_seed(123);
166        let _ = cached_reference.next_gaussian();
167        assert_eq!(random.next_gaussian(), cached_reference.next_gaussian());
168
169        let mut reseeded_reference = WorldgenRandom::from_seed(0);
170        reseeded_reference.set_feature_seed(456, 7, 8);
171        assert_eq!(random.next_gaussian(), reseeded_reference.next_gaussian());
172    }
173}