Pumpkin/pumpkin-util/src/random/mod.rs
2026-05-03 11:11:42 +02:00

275 lines
8.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use std::{
sync::atomic::{AtomicU64, Ordering},
time,
};
use enum_dispatch::enum_dispatch;
use legacy_rand::{LegacyRand, LegacySplitter};
use xoroshiro128::{Xoroshiro, XoroshiroSplitter};
mod gaussian;
pub mod legacy_rand;
pub mod xoroshiro128;
/// Global seed uniquifier used to generate unique seeds based on time.
///
/// This value is atomically updated each time a seed is requested to ensure
/// that seeds generated at the same time are still unique.
static SEED_UNIQUIFIER: AtomicU64 = AtomicU64::new(8682522807148012u64);
/// Generates a new random seed based on the current time and a uniquifier.
///
/// The seed is generated by combining:
/// - An atomic counter multiplied by a large constant.
/// - The current system time in nanoseconds.
///
/// This ensures that seeds are both time-dependent and unique across calls.
///
/// # Returns
/// A 64-bit seed value suitable for initializing random number generators.
pub fn get_seed() -> u64 {
let seed = SEED_UNIQUIFIER
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |val| {
Some(val.wrapping_mul(1181783497276652981u64))
})
.unwrap();
let nanos = time::SystemTime::now()
.duration_since(time::SystemTime::UNIX_EPOCH)
.unwrap()
.as_nanos();
let nano_upper = (nanos >> 8) as u64;
let nano_lower = nanos as u64;
seed ^ nano_upper ^ nano_lower
}
/// Unified random number generator enum that can represent different RNG implementations.
///
/// This enum allows for dynamic dispatch between different RNG implementations
/// while maintaining performance through `enum_dispatch`.
#[enum_dispatch(RandomImpl)]
pub enum RandomGenerator {
/// Xoroshiro128+ random number generator (modern, fast implementation).
Xoroshiro(Xoroshiro),
/// Legacy random number generator (compatible with older Minecraft versions).
Legacy(LegacyRand),
}
/// Unified random number deriver enum for creating child RNGs.
///
/// Derivers are used to create new independent random generators from a parent seed,
/// which is essential for deterministic world generation where different features
/// need their own RNGs.
#[enum_dispatch(RandomDeriverImpl)]
pub enum RandomDeriver {
/// Xoroshiro splitter implementation.
Xoroshiro(XoroshiroSplitter),
/// Legacy splitter implementation.
Legacy(LegacySplitter),
}
// TODO: Write unit test for this
#[macro_export]
macro_rules! population_seed_fn {
() => {
/// Generates a population seed for structure placement.
///
/// Population seeds are used to determine the placement of structures
/// like villages, temples, and other features. They combine the world seed
/// with block coordinates to create a unique seed for each chunk.
///
/// # Arguments
/// - `world_seed` – The base world seed.
/// - `block_x` – The X block coordinate.
/// - `block_z` – The Z block coordinate.
///
/// # Returns
/// A population seed for the given location.
pub fn get_population_seed(world_seed: u64, block_x: i32, block_z: i32) -> u64 {
let mut rand = Self::from_seed(world_seed);
let l = rand.next_i64() | 1;
let m = rand.next_i64() | 1;
let base = (block_x as i64)
.wrapping_mul(l)
.wrapping_add((block_z as i64).wrapping_mul(m));
(base as u64) ^ world_seed
}
};
}
/// Generates a decorator seed for feature placement.
///
/// Decorator seeds are used for placing individual features like trees,
/// flowers, and ores within a chunk.
///
/// # Arguments
/// - `population_seed` – The base population seed for the area.
/// - `index` – The index of the decorator.
/// - `step` – The decoration step number.
///
/// # Returns
/// A decorator seed for the given parameters.
// TODO: Write unit test for this
#[inline]
#[must_use]
pub const fn get_decorator_seed(population_seed: u64, index: u64, step: u64) -> u64 {
population_seed
.wrapping_add(index)
.wrapping_add(10_000u64.wrapping_mul(step))
}
/// Generates a region seed for large-scale feature placement.
///
/// Region seeds are used for features that span multiple chunks, such as
/// slime chunks or specific biome placements.
///
/// # Arguments
/// - `world_seed` – The base world seed.
/// - `region_x` – The X region coordinate.
/// - `region_z` – The Z region coordinate.
/// - `salt` – A salt value to make the seed unique for specific features.
///
/// # Returns
/// A region seed for the given location.
#[inline]
#[must_use]
pub fn get_region_seed(world_seed: u64, region_x: i32, region_z: i32, salt: u32) -> u64 {
let x_part = i64::from(region_x).wrapping_mul(341873128712) as u64;
let z_part = i64::from(region_z).wrapping_mul(132897987541) as u64;
world_seed
.wrapping_add(x_part)
.wrapping_add(z_part)
.wrapping_add(i64::from(salt) as u64)
}
/// Generates a carver seed for cave and ravine generation.
///
/// Carver seeds are used for terrain carving features like caves and ravines.
///
/// # Arguments
/// - `world_seed` – The base world seed (plus carver index).
/// - `chunk_x` – The X chunk coordinate.
/// - `chunk_z` – The Z chunk coordinate.
///
/// # Returns
/// A carver seed for the given chunk.
#[inline]
#[must_use]
pub fn get_carver_seed(world_seed: u64, chunk_x: i32, chunk_z: i32) -> u64 {
let mut random = LegacyRand::from_seed(world_seed);
let l = random.next_i64() | 1;
let m = random.next_i64() | 1;
((chunk_x as i64)
.wrapping_mul(l)
.wrapping_add((chunk_z as i64).wrapping_mul(m)) as u64)
^ world_seed
}
#[enum_dispatch]
#[expect(clippy::return_self_not_must_use)]
pub trait RandomImpl {
fn split(&mut self) -> Self;
fn next_splitter(&mut self) -> RandomDeriver;
fn next_i32(&mut self) -> i32;
fn next_bounded_i32(&mut self, bound: i32) -> i32;
fn next_inbetween_i32(&mut self, min: i32, max: i32) -> i32 {
self.next_bounded_i32(max - min + 1) + min
}
fn next_i64(&mut self) -> i64;
fn next_bool(&mut self) -> bool;
fn next_f32(&mut self) -> f32;
fn next_f64(&mut self) -> f64;
fn next_gaussian(&mut self) -> f64;
#[allow(clippy::suboptimal_flops)]
fn next_triangular(&mut self, mode: f64, deviation: f64) -> f64 {
mode + deviation * (self.next_f64() - self.next_f64())
}
fn skip(&mut self, count: i32) {
for _ in 0..count {
self.next_i64();
}
}
fn next_inbetween_i32_exclusive(&mut self, min: i32, max: i32) -> i32 {
min + self.next_bounded_i32(max - min)
}
}
#[enum_dispatch]
pub trait RandomDeriverImpl {
fn split_string(&self, seed: &str) -> RandomGenerator;
fn split_u64(&self, seed: u64) -> RandomGenerator;
fn split_pos(&self, x: i32, y: i32, z: i32) -> RandomGenerator;
}
/// Hashes a block position into a 64-bit value for use in RNG seeding.
///
/// This hash function is designed to produce well-distributed values for
/// use as seeds for position-dependent random generation.
///
/// # Arguments
/// - `x` – The X coordinate.
/// - `y` – The Y coordinate.
/// - `z` – The Z coordinate.
///
/// # Returns
/// A 64-bit hash value.
#[must_use]
pub const fn hash_block_pos(x: i32, y: i32, z: i32) -> i64 {
let l =
((x.wrapping_mul(3129871)) as i64) ^ ((z as i64).wrapping_mul(116129781i64)) ^ (y as i64);
let l = l
.wrapping_mul(l)
.wrapping_mul(42317861i64)
.wrapping_add(l.wrapping_mul(11i64));
l >> 16
}
#[cfg(test)]
mod tests {
use crate::random::get_region_seed;
use super::hash_block_pos;
#[test]
fn region_seed() {
let seed = get_region_seed(12345612, 1, 1, 14357620);
assert_eq!(seed, 474797819485);
}
#[test]
fn block_position_hash() {
let values: [((i32, i32, i32), i64); 8] = [
((0, 0, 0), 0),
((1, 1, 1), 60311958971344),
((4, 4, 4), 120566413180880),
((25, 25, 25), 111753446486209),
((676, 676, 676), 75210837988243),
((458329, 458329, 458329), -43764888250),
((-387008604, -387008604, -387008604), 8437923733503),
((176771161, 176771161, 176771161), 18421337580760),
];
for ((x, y, z), value) in values {
assert_eq!(hash_block_pos(x, y, z), value);
}
}
}