Skip to main content

steel_utils/random/
legacy_random.rs

1use crate::random::{
2    PositionalRandom, Random, RandomSource, RandomSplitter, gaussian::MarsagliaPolarGaussian,
3    get_seed, name_hash::NameHash,
4};
5
6/// Legacy Minecraft random number generator based on a Linear Congruential Generator (LCG).
7/// This implementation mirrors Java's `java.util.Random` which Minecraft originally used.
8pub struct LegacyRandom {
9    seed: i64,
10    next_gaussian: Option<f64>,
11}
12
13/// A positional random number generator factory for the legacy Minecraft LCG algorithm.
14/// This can create random sources based on position, hash, or seed.
15#[derive(Clone)]
16pub struct LegacyRandomSplitter {
17    seed: i64,
18}
19
20impl LegacyRandom {
21    /// Creates a new `LegacyRandom` instance from the given seed.
22    /// The seed is `XORed` with the LCG multiplier and masked to 48 bits, matching Java's behavior.
23    #[must_use]
24    pub const fn from_seed(seed: u64) -> Self {
25        Self {
26            seed: (seed as i64 ^ 0x0005_DEEC_E66D) & 0xFFFF_FFFF_FFFF,
27            next_gaussian: None,
28        }
29    }
30
31    /// Returns the internal seed (for debugging/checkpointing).
32    #[must_use]
33    pub const fn get_seed(&self) -> i64 {
34        self.seed
35    }
36
37    /// Re-seeds this generator, matching Java's `Random.setSeed`.
38    pub const fn set_seed(&mut self, seed: i64) {
39        self.seed = (seed ^ 0x0005_DEEC_E66D) & 0xFFFF_FFFF_FFFF;
40        self.next_gaussian = None;
41    }
42
43    /// Matches vanilla's `WorldgenRandom.setLargeFeatureSeed`.
44    pub fn set_large_feature_seed(&mut self, seed: i64, chunk_x: i32, chunk_z: i32) {
45        self.set_seed(seed);
46        let x_mul = self.next_i64();
47        let z_mul = self.next_i64();
48        self.set_seed(
49            i64::from(chunk_x).wrapping_mul(x_mul) ^ i64::from(chunk_z).wrapping_mul(z_mul) ^ seed,
50        );
51    }
52
53    /// Matches vanilla's `WorldgenRandom.setLargeFeatureWithSalt`.
54    pub fn set_large_feature_with_salt(&mut self, seed: i64, x: i32, z: i32, salt: i32) {
55        self.set_seed(
56            i64::from(x)
57                .wrapping_mul(341_873_128_712)
58                .wrapping_add(i64::from(z).wrapping_mul(132_897_987_541))
59                .wrapping_add(seed)
60                .wrapping_add(i64::from(salt)),
61        );
62    }
63
64    const fn next(&mut self, bits: u64) -> i32 {
65        (self.next_random() >> (48 - bits)) as i32
66    }
67
68    const fn next_random(&mut self) -> i64 {
69        let l = self.seed;
70        let m = l.wrapping_mul(0x0005_DEEC_E66D).wrapping_add(0xB) & 0xFFFF_FFFF_FFFF;
71        self.seed = m;
72        m
73    }
74}
75
76impl MarsagliaPolarGaussian for LegacyRandom {
77    fn stored_next_gaussian(&self) -> Option<f64> {
78        self.next_gaussian
79    }
80
81    fn set_stored_next_gaussian(&mut self, value: Option<f64>) {
82        self.next_gaussian = value;
83    }
84}
85
86impl Random for LegacyRandom {
87    fn fork(&mut self) -> Self {
88        Self::from_seed(self.next_i64() as u64)
89    }
90
91    fn next_i32(&mut self) -> i32 {
92        self.next(32)
93    }
94
95    fn next_i32_bounded(&mut self, bound: i32) -> i32 {
96        if bound & bound.wrapping_sub(1) == 0 {
97            (i64::from(bound).wrapping_mul(i64::from(self.next(31))) >> 31) as i32
98        } else {
99            loop {
100                let i = self.next(31);
101                let j = i % bound;
102                if i.wrapping_sub(j).wrapping_add(bound.wrapping_sub(1)) >= 0 {
103                    return j;
104                }
105            }
106        }
107    }
108
109    fn next_i64(&mut self) -> i64 {
110        let i = self.next_i32();
111        let j = self.next_i32();
112        (i64::from(i) << 32).wrapping_add(i64::from(j))
113    }
114
115    fn next_f32(&mut self) -> f32 {
116        self.next(24) as f32 * 5.960_464_5e-8_f32
117    }
118
119    fn next_f64(&mut self) -> f64 {
120        // Matches vanilla's BitRandomSource.nextDouble():
121        //   double DOUBLE_MULTIPLIER = 1.110223E-16F;  // stored as double = 2^-53
122        //   return combined * DOUBLE_MULTIPLIER;
123        // The field is declared double; the float literal `1.110223E-16F` is widened to
124        // double at compile time (= exactly 2^-53). javac inlines static final interface
125        // fields, so the bytecode uses `ldc2_w (double)` → double multiplication.
126        let combined = (i64::from(self.next(26)) << 27) + i64::from(self.next(27));
127        combined as f64 * (1.0 / (1_i64 << 53) as f64)
128    }
129
130    fn next_bool(&mut self) -> bool {
131        self.next(1) != 0
132    }
133
134    fn next_gaussian(&mut self) -> f64 {
135        self.calculate_gaussian()
136    }
137
138    fn next_positional(&mut self) -> RandomSplitter {
139        RandomSplitter::Legacy(LegacyRandomSplitter::new(self.next_i64()))
140    }
141}
142
143impl LegacyRandomSplitter {
144    /// Creates a new `LegacyRandomSplitter` with the given seed.
145    /// This seed is used to initialize positional random sources.
146    #[must_use]
147    pub const fn new(seed: i64) -> Self {
148        Self { seed }
149    }
150}
151
152impl PositionalRandom for LegacyRandomSplitter {
153    fn at(&self, x: i32, y: i32, z: i32) -> RandomSource {
154        let seed = get_seed(x, y, z);
155        RandomSource::Legacy(LegacyRandom::from_seed((seed as u64) ^ (self.seed as u64)))
156    }
157
158    fn with_hash_of(&self, hash: &NameHash) -> RandomSource {
159        RandomSource::Legacy(LegacyRandom::from_seed(
160            (hash.java_hash as u64) ^ (self.seed as u64),
161        ))
162    }
163
164    fn with_seed(&self, seed: u64) -> RandomSource {
165        RandomSource::Legacy(LegacyRandom::from_seed(seed))
166    }
167}
168
169#[cfg(test)]
170mod test {
171    use crate::random::{PositionalRandom, Random, RandomSplitter, name_hash::NameHash};
172
173    use super::LegacyRandom;
174
175    #[test]
176    fn test_next_i32() {
177        let mut rand = LegacyRandom::from_seed(0);
178
179        let values = [
180            -1_155_484_576,
181            -723_955_400,
182            1_033_096_058,
183            -1_690_734_402,
184            -1_557_280_266,
185            1_327_362_106,
186            -1_930_858_313,
187            502_539_523,
188            -1_728_529_858,
189            -938_301_587,
190        ];
191
192        for value in values {
193            assert_eq!(rand.next_i32(), value);
194        }
195    }
196
197    #[test]
198    fn test_next_i32_bounded() {
199        let mut rand = LegacyRandom::from_seed(0);
200
201        let values = [0, 13, 4, 2, 5, 8, 11, 6, 9, 14];
202
203        for value in values {
204            assert_eq!(rand.next_i32_bounded(0xf), value);
205        }
206
207        let mut rand = LegacyRandom::from_seed(0);
208        for _ in 0..10 {
209            assert_eq!(rand.next_i32_bounded(1), 0);
210        }
211
212        let mut rand = LegacyRandom::from_seed(0);
213        let values = [1, 1, 0, 1, 1, 0, 1, 0, 1, 1];
214        for value in values {
215            assert_eq!(rand.next_i32_bounded(2), value);
216        }
217    }
218
219    #[test]
220    fn test_next_i32_between() {
221        let mut rand = LegacyRandom::from_seed(0);
222
223        let values = [1, 5, 2, 12, 12, 6, 12, 10, 4, 3];
224
225        for value in values {
226            assert_eq!(rand.next_i32_between(1, 12), value);
227        }
228    }
229
230    #[test]
231    fn test_next_i32_between_exclusive() {
232        let mut rand = LegacyRandom::from_seed(0);
233
234        let values = [1, 7, 9, 6, 7, 3, 3, 7, 3, 1];
235
236        for value in values {
237            assert_eq!(rand.next_i32_between_exclusive(1, 12), value);
238        }
239    }
240
241    #[test]
242    #[expect(clippy::float_cmp, reason = "exact match against vanilla test vectors")]
243    fn test_next_f64() {
244        let mut rand = LegacyRandom::from_seed(0);
245
246        // Values match vanilla's BitRandomSource.nextDouble():
247        //   double DOUBLE_MULTIPLIER = 1.110223E-16F;  // stored as double = 2^-53
248        //   return combined * DOUBLE_MULTIPLIER;        // double multiplication
249        let values = [
250            0.730_967_787_376_657,
251            0.240_536_415_671_485_87,
252            0.637_417_425_350_108_3,
253            0.550_437_005_117_633_9,
254            0.597_545_277_797_201_8,
255            0.333_218_399_476_649_8,
256            0.385_189_184_740_718_5,
257            0.984_841_540_199_809,
258            0.879_182_517_872_480_1,
259            0.941_249_179_482_114_4,
260        ];
261
262        for value in values {
263            assert_eq!(rand.next_f64(), value);
264        }
265    }
266
267    #[test]
268    #[expect(clippy::float_cmp, reason = "exact match against vanilla test vectors")]
269    fn test_next_f32() {
270        let mut rand = LegacyRandom::from_seed(0);
271
272        let values: [f32; 10] = [
273            0.730_967_76,
274            0.831_441,
275            0.240_536_39,
276            0.606_345_2,
277            0.637_417_4,
278            0.309_050_56,
279            0.550_437,
280            0.117_006_6,
281            0.597_545_27,
282            0.781_534_6,
283        ];
284
285        for value in values {
286            assert_eq!(rand.next_f32(), value);
287        }
288    }
289
290    #[test]
291    fn test_next_i64() {
292        let mut rand = LegacyRandom::from_seed(0);
293
294        let values: [i64; 10] = [
295            -4_962_768_465_676_381_896,
296            4_437_113_781_045_784_766,
297            -6_688_467_811_848_818_630,
298            -8_292_973_307_042_192_125,
299            -7_423_979_211_207_825_555,
300            6_146_794_652_083_548_235,
301            7_105_486_291_024_734_541,
302            -279_624_296_851_435_688,
303            -2_228_689_144_322_150_137,
304            -1_083_761_183_081_836_303,
305        ];
306
307        for value in values {
308            assert_eq!(rand.next_i64(), value);
309        }
310    }
311
312    #[test]
313    fn test_next_bool() {
314        let mut rand = LegacyRandom::from_seed(0);
315
316        let values = [
317            true, true, false, true, true, false, true, false, true, true,
318        ];
319
320        for value in values {
321            assert_eq!(rand.next_bool(), value);
322        }
323    }
324
325    #[test]
326    #[expect(clippy::float_cmp, reason = "exact match against vanilla test vectors")]
327    fn test_next_gaussian() {
328        let mut rand = LegacyRandom::from_seed(0);
329
330        let values = [
331            0.802_533_063_739_030_5,
332            -0.901_546_088_417_512_2,
333            2.080_920_790_428_163,
334            0.763_770_768_436_489_4,
335            0.984_574_532_882_512_8,
336            -1.683_412_258_767_342_8,
337            -0.027_290_262_907_887_285,
338            0.115_245_702_862_023_15,
339            -0.390_167_041_379_937_74,
340            -0.643_388_813_126_449,
341        ];
342
343        for value in values {
344            assert_eq!(rand.next_gaussian(), value);
345        }
346    }
347
348    #[test]
349    #[expect(clippy::float_cmp, reason = "exact match against vanilla test vectors")]
350    fn test_triangle() {
351        let mut rand = LegacyRandom::from_seed(0);
352
353        let values = [
354            124.521_568_585_258_56,
355            104.349_021_011_623_72,
356            113.216_343_916_027_6,
357            70.017_382_227_045_47,
358            96.896_666_919_518_28,
359            107.302_840_758_085_41,
360            106.168_176_758_131_44,
361            79.112_644_826_080_78,
362            73.967_216_139_270_62,
363            81.724_195_210_806_46,
364        ];
365
366        for value in values {
367            assert_eq!(rand.triangle(100_f64, 50_f64), value);
368        }
369    }
370
371    #[test]
372    fn test_fork() {
373        let mut original_rand = LegacyRandom::from_seed(0);
374        assert_eq!(original_rand.next_i64(), -4_962_768_465_676_381_896_i64);
375
376        let mut original_rand = LegacyRandom::from_seed(0);
377        {
378            let RandomSplitter::Legacy(splitter) = original_rand.next_positional() else {
379                unreachable!()
380            };
381            assert_eq!(splitter.seed, -4_962_768_465_676_381_896_i64);
382
383            let mut rand = splitter.with_hash_of(&NameHash::new("minecraft:offset"));
384            assert_eq!(rand.next_i32(), 103_436_829);
385        }
386
387        let mut original_rand = LegacyRandom::from_seed(0);
388        let mut new_rand = original_rand.fork();
389        {
390            let splitter = new_rand.next_positional();
391
392            let mut rand1 = splitter.with_hash_of(&NameHash::new("TEST STRING"));
393            assert_eq!(rand1.next_i32(), -1_170_413_697);
394
395            let mut rand2 = splitter.with_seed(10);
396            assert_eq!(rand2.next_i32(), -1_157_793_070);
397
398            let mut rand3 = splitter.at(1, 11, -111);
399            assert_eq!(rand3.next_i32(), -1_213_890_343);
400        }
401
402        assert_eq!(original_rand.next_i32(), 1_033_096_058);
403        assert_eq!(new_rand.next_i32(), -888_301_832);
404    }
405
406    #[test]
407    fn test_set_seed_matches_from_seed() {
408        let mut fresh = LegacyRandom::from_seed(12345);
409        let mut reseeded = LegacyRandom::from_seed(0);
410        reseeded.set_seed(12345);
411        for _ in 0..10 {
412            assert_eq!(fresh.next_i64(), reseeded.next_i64());
413        }
414    }
415
416    #[test]
417    fn test_set_large_feature_with_salt_trivial() {
418        let mut rng = LegacyRandom::from_seed(0);
419        rng.set_large_feature_with_salt(0, 0, 0, 10_387_312);
420        let mut expected = LegacyRandom::from_seed(0);
421        expected.set_seed(10_387_312);
422        for _ in 0..5 {
423            assert_eq!(rng.next_i32(), expected.next_i32());
424        }
425    }
426
427    #[test]
428    fn test_set_large_feature_with_salt() {
429        let mut rng = LegacyRandom::from_seed(0);
430        rng.set_large_feature_with_salt(123_456_789, 5, -3, 10_387_312);
431        let expected_seed: i64 =
432            5_i64 * 341_873_128_712 + (-3_i64) * 132_897_987_541 + 123_456_789 + 10_387_312;
433        let mut expected = LegacyRandom::from_seed(0);
434        expected.set_seed(expected_seed);
435        for _ in 0..5 {
436            assert_eq!(rng.next_i32(), expected.next_i32());
437        }
438    }
439
440    #[test]
441    fn test_set_large_feature_seed() {
442        let x_mul = -4_962_768_465_676_381_896_i64;
443        let z_mul = 4_437_113_781_045_784_766_i64;
444        let expected_seed = 3_i64.wrapping_mul(x_mul) ^ 5_i64.wrapping_mul(z_mul);
445
446        let mut rng = LegacyRandom::from_seed(0);
447        rng.set_large_feature_seed(0, 3, 5);
448        let mut expected = LegacyRandom::from_seed(0);
449        expected.set_seed(expected_seed);
450        for _ in 0..5 {
451            assert_eq!(rng.next_i32(), expected.next_i32());
452        }
453    }
454}