Pumpkin/crates/pumpkin-util/src/random/mod.rs
2026-09-09 18:01:31 +02:00

426 lines
12 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 legacy_rand::{LegacyRand, LegacySplitter};
use worldgen_random::WorldgenRandom;
use xoroshiro128::{Xoroshiro, XoroshiroSplitter};
mod gaussian;
pub mod legacy_rand;
pub mod worldgen_random;
pub mod xoroshiro128;
/// Global seed uniquifier used to generate unique seeds based on time.
static SEED_UNIQUIFIER: AtomicU64 = AtomicU64::new(8682522807148012u64);
pub fn get_seed() -> u64 {
let seed = SEED_UNIQUIFIER
.try_update(Ordering::Relaxed, Ordering::Relaxed, |val| {
Some(val.wrapping_mul(1181783497276652981u64))
})
.unwrap_or(0);
let nanos = time::SystemTime::now()
.duration_since(time::SystemTime::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos());
let nano_upper = (nanos >> 8) as u64;
let nano_lower = nanos as u64;
seed ^ nano_upper ^ nano_lower
}
pub enum RandomGenerator {
/// Xoroshiro128+ random number generator (modern, fast implementation).
Xoroshiro(Xoroshiro),
/// Xoroshiro wrapped with Java `WorldgenRandom` bit-source semantics.
Worldgen(WorldgenRandom),
/// Legacy random number generator (compatible with older Minecraft versions).
Legacy(LegacyRand),
}
/// Unified random number deriver enum for creating child RNGs.
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 seed for slime chunk determination.
///
/// This follows Minecraft's specific formula for identifying if a chunk
/// is a "slime chunk" where slimes can spawn regardless of light levels.
///
/// # Arguments
/// - `x` – The X chunk coordinate.
/// - `z` – The Z chunk coordinate.
/// - `seed` – The world seed.
/// - `salt` – A salt value (default is 987234911).
///
/// # Returns
/// A seed value to be used with a legacy random generator.
#[inline]
#[must_use]
pub const fn seed_slime_chunk(x: i32, z: i32, seed: u64, salt: u64) -> u64 {
(seed
.wrapping_add((x.wrapping_mul(x).wrapping_mul(4_987_142)) as i64 as u64)
.wrapping_add((x.wrapping_mul(5_947_611)) as i64 as u64)
.wrapping_add((z.wrapping_mul(z) as i64).wrapping_mul(4_392_871) as u64)
.wrapping_add((z.wrapping_mul(389_711)) as i64 as u64))
^ salt
}
/// 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
}
/// Generates a large feature seed for structure selection and placement.
///
/// Matches vanilla Minecraft's `WorldgenRandom.setLargeFeatureSeed`.
///
/// # Arguments
/// - `world_seed` – The base world seed.
/// - `chunk_x` – The X chunk coordinate.
/// - `chunk_z` – The Z chunk coordinate.
///
/// # Returns
/// A large feature seed for the given chunk.
#[inline]
#[must_use]
pub fn get_large_feature_seed(world_seed: u64, chunk_x: i32, chunk_z: i32) -> u64 {
let mut random = LegacyRand::from_seed(world_seed);
let x_scale = random.next_i64();
let z_scale = random.next_i64();
((chunk_x as i64).wrapping_mul(x_scale)
^ (chunk_z as i64).wrapping_mul(z_scale)
^ (world_seed as i64)) as u64
}
#[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 {
if min >= max {
return min;
}
self.next_bounded_i32(max - min + 1) + min
}
fn next_inbetween_f32(&mut self, min: f32, max: f32) -> f32 {
self.next_f32() * (max - min) + 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 {
if min >= max {
return min;
}
min + self.next_bounded_i32(max - min)
}
}
impl RandomImpl for RandomGenerator {
#[inline]
fn split(&mut self) -> Self {
match self {
Self::Xoroshiro(x) => Self::Xoroshiro(x.split()),
Self::Worldgen(x) => Self::Worldgen(x.split()),
Self::Legacy(l) => Self::Legacy(l.split()),
}
}
#[inline]
fn next_splitter(&mut self) -> RandomDeriver {
match self {
Self::Xoroshiro(x) => RandomDeriver::Xoroshiro(x.next_splitter()),
Self::Worldgen(x) => x.next_splitter(),
Self::Legacy(l) => l.next_splitter(),
}
}
#[inline]
fn next_i32(&mut self) -> i32 {
match self {
Self::Xoroshiro(x) => x.next_i32(),
Self::Worldgen(x) => x.next_i32(),
Self::Legacy(l) => l.next_i32(),
}
}
#[inline]
fn next_bounded_i32(&mut self, bound: i32) -> i32 {
match self {
Self::Xoroshiro(x) => x.next_bounded_i32(bound),
Self::Worldgen(x) => x.next_bounded_i32(bound),
Self::Legacy(l) => l.next_bounded_i32(bound),
}
}
#[inline]
fn next_i64(&mut self) -> i64 {
match self {
Self::Xoroshiro(x) => x.next_i64(),
Self::Worldgen(x) => x.next_i64(),
Self::Legacy(l) => l.next_i64(),
}
}
#[inline]
fn next_bool(&mut self) -> bool {
match self {
Self::Xoroshiro(x) => x.next_bool(),
Self::Worldgen(x) => x.next_bool(),
Self::Legacy(l) => l.next_bool(),
}
}
#[inline]
fn next_f32(&mut self) -> f32 {
match self {
Self::Xoroshiro(x) => x.next_f32(),
Self::Worldgen(x) => x.next_f32(),
Self::Legacy(l) => l.next_f32(),
}
}
#[inline]
fn next_f64(&mut self) -> f64 {
match self {
Self::Xoroshiro(x) => x.next_f64(),
Self::Worldgen(x) => x.next_f64(),
Self::Legacy(l) => l.next_f64(),
}
}
#[inline]
fn next_gaussian(&mut self) -> f64 {
match self {
Self::Xoroshiro(x) => x.next_gaussian(),
Self::Worldgen(x) => x.next_gaussian(),
Self::Legacy(l) => l.next_gaussian(),
}
}
#[inline]
fn skip(&mut self, count: i32) {
match self {
Self::Xoroshiro(x) => x.skip(count),
Self::Worldgen(x) => x.skip(count),
Self::Legacy(l) => l.skip(count),
}
}
}
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;
}
impl RandomDeriverImpl for RandomDeriver {
#[inline]
fn split_string(&self, seed: &str) -> RandomGenerator {
match self {
Self::Xoroshiro(x) => RandomGenerator::Xoroshiro(x.split_string(seed)),
Self::Legacy(l) => l.split_string(seed),
}
}
#[inline]
fn split_u64(&self, seed: u64) -> RandomGenerator {
match self {
Self::Xoroshiro(x) => x.split_u64(seed),
Self::Legacy(l) => l.split_u64(seed),
}
}
#[inline]
fn split_pos(&self, x: i32, y: i32, z: i32) -> RandomGenerator {
match self {
Self::Xoroshiro(s) => RandomGenerator::Xoroshiro(s.split_pos(x, y, z)),
Self::Legacy(s) => s.split_pos(x, y, z),
}
}
}
/// 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);
}
}
}