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