steel_utils/random/
worldgen_random.rs1use crate::random::{
2 Random, RandomSplitter, gaussian::MarsagliaPolarGaussian, xoroshiro::Xoroshiro,
3};
4
5pub struct WorldgenRandom {
11 source: Xoroshiro,
12 next_gaussian: Option<f64>,
13}
14
15impl WorldgenRandom {
16 #[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 pub const fn set_seed(&mut self, seed: i64) {
32 self.source.set_seed(seed);
33 }
34
35 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 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}