From af1050713793c8de5d42804dbe84a77d89958ae3 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Sun, 3 May 2026 11:11:42 +0200 Subject: [PATCH] feat: add chunk blender --- pumpkin-util/src/noise/perlin.rs | 2 + pumpkin-util/src/random/mod.rs | 21 +- pumpkin-world/src/chunk/format/mod.rs | 1 + pumpkin-world/src/chunk/mod.rs | 1 + pumpkin-world/src/chunk_system/chunk_state.rs | 2 + .../src/chunk_system/generation_cache.rs | 18 + .../src/generation/blender/blending_data.rs | 72 ++++ pumpkin-world/src/generation/blender/mod.rs | 361 ++++++++++++++++-- pumpkin-world/src/generation/carver/canyon.rs | 40 +- pumpkin-world/src/generation/carver/cave.rs | 46 +-- pumpkin-world/src/generation/carver/mod.rs | 21 +- pumpkin-world/src/generation/mod.rs | 2 +- pumpkin-world/src/generation/noise/perlin.rs | 1 + pumpkin-world/src/generation/proto_chunk.rs | 18 +- .../src/generation/structure/placement.rs | 3 +- .../generation/structure/structures/mod.rs | 3 +- 16 files changed, 496 insertions(+), 116 deletions(-) create mode 100644 pumpkin-world/src/generation/blender/blending_data.rs diff --git a/pumpkin-util/src/noise/perlin.rs b/pumpkin-util/src/noise/perlin.rs index 909535220..0dab6a6b1 100644 --- a/pumpkin-util/src/noise/perlin.rs +++ b/pumpkin-util/src/noise/perlin.rs @@ -226,6 +226,7 @@ impl PerlinNoiseSampler { } /// Data for a single octave in an octave Perlin noise sampler. +#[derive(Clone)] pub struct SamplerData { /// The Perlin noise sampler for this octave. pub sampler: PerlinNoiseSampler, @@ -242,6 +243,7 @@ pub struct SamplerData { /// Octave noise, also known as fractal noise, combines multiple octaves of Perlin noise /// with different frequencies and amplitudes to create more complex, natural-looking patterns. /// Each octave adds finer detail to the overall noise. +#[derive(Clone)] pub struct OctavePerlinNoiseSampler { /// The list of samplers for each octave, with their associated parameters. pub samplers: Box<[SamplerData]>, diff --git a/pumpkin-util/src/random/mod.rs b/pumpkin-util/src/random/mod.rs index 009c6af49..c7ba7fbe1 100644 --- a/pumpkin-util/src/random/mod.rs +++ b/pumpkin-util/src/random/mod.rs @@ -149,23 +149,22 @@ pub fn get_region_seed(world_seed: u64, region_x: i32, region_z: i32, salt: u32) /// Carver seeds are used for terrain carving features like caves and ravines. /// /// # Arguments -/// - `random` – The random number generator to use. -/// - `world_seed` – The base world seed. +/// - `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] -pub fn get_carver_seed( - random: &mut RandomGenerator, - world_seed: u64, - chunk_x: i32, - chunk_z: i32, -) -> u64 { - let x = random.next_i64(); - let z = random.next_i64(); - (chunk_x as u64).wrapping_mul(x as u64) ^ (chunk_z as u64).wrapping_mul(z as u64) ^ world_seed +#[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] diff --git a/pumpkin-world/src/chunk/format/mod.rs b/pumpkin-world/src/chunk/format/mod.rs index 32a159874..e4328d7fd 100644 --- a/pumpkin-world/src/chunk/format/mod.rs +++ b/pumpkin-world/src/chunk/format/mod.rs @@ -193,6 +193,7 @@ impl ChunkData { light_engine: std::sync::Mutex::new(light_engine), light_populated: AtomicBool::new(chunk_data.light_correct), status: chunk_data.status, + blending_data: None, }) } diff --git a/pumpkin-world/src/chunk/mod.rs b/pumpkin-world/src/chunk/mod.rs index a5636bbad..d780c9eeb 100644 --- a/pumpkin-world/src/chunk/mod.rs +++ b/pumpkin-world/src/chunk/mod.rs @@ -80,6 +80,7 @@ pub struct ChunkData { pub light_engine: std::sync::Mutex, pub light_populated: AtomicBool, pub status: ChunkStatus, + pub blending_data: Option, pub dirty: AtomicBool, } diff --git a/pumpkin-world/src/chunk_system/chunk_state.rs b/pumpkin-world/src/chunk_system/chunk_state.rs index 71be9aa08..309493f8b 100644 --- a/pumpkin-world/src/chunk_system/chunk_state.rs +++ b/pumpkin-world/src/chunk_system/chunk_state.rs @@ -229,6 +229,7 @@ impl Chunk { light_engine: Mutex::new(ChunkLight::default()), light_populated: AtomicBool::new(false), status: ChunkStatus::Empty, + blending_data: None, dirty: AtomicBool::new(false), })), ) { @@ -317,6 +318,7 @@ impl Chunk { fluid_ticks: Default::default(), block_entities: Mutex::new(block_entities), status: proto_chunk.stage.into(), + blending_data: proto_chunk.blending_data.clone(), }; chunk.heightmap = Mutex::new(chunk.calculate_heightmap()); diff --git a/pumpkin-world/src/chunk_system/generation_cache.rs b/pumpkin-world/src/chunk_system/generation_cache.rs index 65487c0ad..15e1851f6 100644 --- a/pumpkin-world/src/chunk_system/generation_cache.rs +++ b/pumpkin-world/src/chunk_system/generation_cache.rs @@ -323,6 +323,24 @@ impl GenerationCache for Cache { } } + fn get_blending_data( + &self, + chunk_x: i32, + chunk_z: i32, + ) -> Option<&crate::generation::blender::blending_data::BlendingData> { + let dx = chunk_x - self.x; + let dz = chunk_z - self.z; + + if dx < 0 || dx >= self.size || dz < 0 || dz >= self.size { + return None; + } + + match &self.chunks[(dx * self.size + dz) as usize] { + Chunk::Proto(chunk) => chunk.blending_data.as_ref(), + Chunk::Level(data) => data.blending_data.as_ref(), + } + } + fn is_air(&self, local_pos: &Vector3) -> bool { is_air(GenerationCache::get_block_state(self, local_pos).0) } diff --git a/pumpkin-world/src/generation/blender/blending_data.rs b/pumpkin-world/src/generation/blender/blending_data.rs new file mode 100644 index 000000000..1f60b10de --- /dev/null +++ b/pumpkin-world/src/generation/blender/blending_data.rs @@ -0,0 +1,72 @@ +use pumpkin_data::chunk::Biome; + +#[derive(Clone)] +pub struct BlendingData { + pub min_y: i32, + pub max_y: i32, + // Heights at quart positions (16x16 per chunk) + pub heights: Vec, + // Densities at quart positions (16x16 per chunk, but at what Y levels? 1.18+ uses 4 blocks quart) + // Simplified: only store for current chunk's Y levels + pub densities: Vec, + // Biomes at quart positions (16x16 per chunk) + pub biomes: Vec<&'static Biome>, +} + +impl BlendingData { + pub fn get_height(&self, cell_x: i32, _cell_y: i32, cell_z: i32) -> f64 { + if !(0..16).contains(&cell_x) || !(0..16).contains(&cell_z) { + return f64::MAX; + } + self.heights[(cell_z * 16 + cell_x) as usize] + } + + pub fn get_density(&self, cell_x: i32, _cell_y: i32, cell_z: i32) -> f64 { + // cell_y is block_y / 8 in Blender.java + // We'll need a better storage for this if we want full fidelity + if !(0..16).contains(&cell_x) || !(0..16).contains(&cell_z) { + return f64::MAX; + } + // Dummy implementation + f64::MAX + } + + pub fn iterate_heights(&self, quart_x: i32, quart_z: i32, mut consumer: F) + where + F: FnMut(i32, i32, f64), + { + for z in 0..16 { + for x in 0..16 { + let h = self.heights[z * 16 + x]; + if h != f64::MAX { + consumer(quart_x + x as i32, quart_z + z as i32, h); + } + } + } + } + + pub fn iterate_densities( + &self, + _quart_x: i32, + _quart_z: i32, + _min_cell_y: i32, + _max_cell_y: i32, + _consumer: F, + ) where + F: FnMut(i32, i32, i32, f64), + { + // TODO: implement density iteration + } + + pub fn iterate_biomes(&self, quart_x: i32, _quart_y: i32, quart_z: i32, mut consumer: F) + where + F: FnMut(i32, i32, &'static Biome), + { + for z in 0..16 { + for x in 0..16 { + let biome = self.biomes[z * 16 + x]; + consumer(quart_x + x as i32, quart_z + z as i32, biome); + } + } + } +} diff --git a/pumpkin-world/src/generation/blender/mod.rs b/pumpkin-world/src/generation/blender/mod.rs index dad2fbf51..f99e75379 100644 --- a/pumpkin-world/src/generation/blender/mod.rs +++ b/pumpkin-world/src/generation/blender/mod.rs @@ -1,62 +1,361 @@ -use enum_dispatch::enum_dispatch; +pub mod blending_data; + +use std::f64; + +use crate::biome::BiomeSupplier; +use crate::generation::biome_coords; +use crate::generation::noise::perlin::DoublePerlinNoiseSampler; +use crate::generation::noise::router::multi_noise_sampler::MultiNoiseSampler; +use crate::generation::proto_chunk::GenerationCache; +use blending_data::BlendingData; use pumpkin_data::chunk::Biome; +use pumpkin_data::noise_parameter::DoublePerlinNoiseParameters; use pumpkin_util::math::vector3::Vector3; +use pumpkin_util::random::xoroshiro128::Xoroshiro; +use rustc_hash::FxHashMap; -use crate::{ - biome::BiomeSupplier, generation::noise::router::multi_noise_sampler::MultiNoiseSampler, -}; - -pub struct BlendResult { - alpha: f64, - offset: f64, +pub struct BlendingOutput { + pub alpha: f64, + pub blending_offset: f64, } -impl BlendResult { - pub const fn new(alpha: f64, offset: f64) -> Self { - Self { alpha, offset } - } +pub struct Blender { + height_and_biome_blending_data: FxHashMap, + density_blending_data: FxHashMap, } -#[enum_dispatch(BlenderImpl)] -pub enum Blender { - NoBlend(NoBlendBlender), -} +const HEIGHT_BLENDING_RANGE_CELLS: i32 = (7 << 2) - 1; // QuartPos.fromSection(7) - 1 +const HEIGHT_BLENDING_RANGE_CHUNKS: i32 = (HEIGHT_BLENDING_RANGE_CELLS + 3) >> 2; // QuartPos.toSection(...) +const DENSITY_BLENDING_RANGE_CELLS: i32 = 2; +const DENSITY_BLENDING_RANGE_CHUNKS: i32 = 5 >> 2; // QuartPos.toSection(5) impl Blender { - pub const NO_BLEND: Self = Self::NoBlend(NoBlendBlender {}); + pub fn empty() -> Self { + Self { + height_and_biome_blending_data: FxHashMap::default(), + density_blending_data: FxHashMap::default(), + } + } + + pub fn of(cache: &C) -> Self { + let center_chunk = cache.get_center_chunk(); + let center_x = center_chunk.x; + let center_z = center_chunk.z; + + let mut height_and_biome_data = FxHashMap::default(); + let mut density_data = FxHashMap::default(); + + let max_dist_sq = (HEIGHT_BLENDING_RANGE_CHUNKS + 1) * (HEIGHT_BLENDING_RANGE_CHUNKS + 1); + + for dx in -HEIGHT_BLENDING_RANGE_CHUNKS..=HEIGHT_BLENDING_RANGE_CHUNKS { + for dz in -HEIGHT_BLENDING_RANGE_CHUNKS..=HEIGHT_BLENDING_RANGE_CHUNKS { + if dx * dx + dz * dz <= max_dist_sq { + let chunk_x = center_x + dx; + let chunk_z = center_z + dz; + + if let Some(blending_data) = cache.get_blending_data(chunk_x, chunk_z) { + let packed = (chunk_x as u32 as u64) | ((chunk_z as u32 as u64) << 32); + height_and_biome_data.insert(packed, blending_data.clone()); + + if (-DENSITY_BLENDING_RANGE_CHUNKS..=DENSITY_BLENDING_RANGE_CHUNKS) + .contains(&dx) + && (-DENSITY_BLENDING_RANGE_CHUNKS..=DENSITY_BLENDING_RANGE_CHUNKS) + .contains(&dz) + { + density_data.insert(packed, blending_data.clone()); + } + } + } + } + } + + if height_and_biome_data.is_empty() && density_data.is_empty() { + Self::empty() + } else { + Self { + height_and_biome_blending_data: height_and_biome_data, + density_blending_data: density_data, + } + } + } + + pub fn is_empty(&self) -> bool { + self.height_and_biome_blending_data.is_empty() && self.density_blending_data.is_empty() + } + + pub fn blend_offset_and_factor(&self, block_x: i32, block_z: i32) -> BlendingOutput { + let cell_x = biome_coords::from_block(block_x); + let cell_z = biome_coords::from_block(block_z); + + let fixed_height = self + .get_blending_data_value(cell_x, 0, cell_z, |data, x, y, z| data.get_height(x, y, z)); + + if fixed_height != f64::MAX { + return BlendingOutput { + alpha: 0.0, + blending_offset: Self::height_to_offset(fixed_height), + }; + } + + let mut total_weight = 0.0; + let mut weighted_heights = 0.0; + let mut closest_distance = f64::INFINITY; + + for (&packed_pos, blending_data) in &self.height_and_biome_blending_data { + let chunk_x = (packed_pos & 0xFFFFFFFF) as i32; + let chunk_z = (packed_pos >> 32) as i32; + + blending_data.iterate_heights( + biome_coords::from_chunk(chunk_x), + biome_coords::from_chunk(chunk_z), + |test_cell_x, test_cell_z, height| { + let dx = (cell_x - test_cell_x) as f64; + let dz = (cell_z - test_cell_z) as f64; + let distance = (dx * dx + dz * dz).sqrt(); + + if distance <= HEIGHT_BLENDING_RANGE_CELLS as f64 { + if distance < closest_distance { + closest_distance = distance; + } + + let weight = 1.0 / (distance * distance * distance * distance); + weighted_heights += height * weight; + total_weight += weight; + } + }, + ); + } + + if closest_distance == f64::INFINITY { + BlendingOutput { + alpha: 1.0, + blending_offset: 0.0, + } + } else { + let average_height = weighted_heights / total_weight; + let mut alpha = + (closest_distance / (HEIGHT_BLENDING_RANGE_CELLS + 1) as f64).clamp(0.0, 1.0); + alpha = 3.0 * alpha * alpha - 2.0 * alpha * alpha * alpha; + BlendingOutput { + alpha, + blending_offset: Self::height_to_offset(average_height), + } + } + } + + fn height_to_offset(height: f64) -> f64 { + let target_y = height + 0.5; + let target_y_mod = target_y.rem_euclid(8.0); + (32.0 * (target_y - 128.0) - 3.0 * (target_y - 120.0) * target_y_mod + + 3.0 * target_y_mod * target_y_mod) + / (128.0 * (32.0 - 3.0 * target_y_mod)) + } + + pub fn blend_density(&self, block_x: i32, block_y: i32, block_z: i32, noise_value: f64) -> f64 { + let cell_x = biome_coords::from_block(block_x); + let cell_y = block_y / 8; + let cell_z = biome_coords::from_block(block_z); + + let fixed_density = + self.get_blending_data_value(cell_x, cell_y, cell_z, |data, x, y, z| { + data.get_density(x, y, z) + }); + + if fixed_density != f64::MAX { + return fixed_density; + } + + let mut total_weight = 0.0; + let mut weighted_densities = 0.0; + let mut closest_distance = f64::INFINITY; + + for (&packed_pos, blending_data) in &self.density_blending_data { + let chunk_x = (packed_pos & 0xFFFFFFFF) as i32; + let chunk_z = (packed_pos >> 32) as i32; + + blending_data.iterate_densities( + biome_coords::from_chunk(chunk_x), + biome_coords::from_chunk(chunk_z), + cell_y - 1, + cell_y + 1, + |test_cell_x, test_cell_y, test_cell_z, density| { + let dx = (cell_x - test_cell_x) as f64; + let dy = ((cell_y - test_cell_y) * 2) as f64; + let dz = (cell_z - test_cell_z) as f64; + let distance = (dx * dx + dy * dy + dz * dz).sqrt(); + + if distance <= 2.0 { + if distance < closest_distance { + closest_distance = distance; + } + + let weight = 1.0 / (distance * distance * distance * distance); + weighted_densities += density * weight; + total_weight += weight; + } + }, + ); + } + + if closest_distance == f64::INFINITY { + noise_value + } else { + let average_density = weighted_densities / total_weight; + let alpha = (closest_distance / 3.0).clamp(0.0, 1.0); + alpha * noise_value + (1.0 - alpha) * average_density + } + } + + fn get_blending_data_value(&self, cell_x: i32, cell_y: i32, cell_z: i32, getter: F) -> f64 + where + F: Fn(&BlendingData, i32, i32, i32) -> f64, + { + let chunk_x = biome_coords::to_chunk(cell_x); + let chunk_z = biome_coords::to_chunk(cell_z); + let min_x = (cell_x & 3) == 0; + let min_z = (cell_z & 3) == 0; + + let mut value = self.get_data_value(&getter, chunk_x, chunk_z, cell_x, cell_y, cell_z); + if value == f64::MAX { + if min_x && min_z { + value = + self.get_data_value(&getter, chunk_x - 1, chunk_z - 1, cell_x, cell_y, cell_z); + } + + if value == f64::MAX { + if min_x { + value = + self.get_data_value(&getter, chunk_x - 1, chunk_z, cell_x, cell_y, cell_z); + } + + if value == f64::MAX && min_z { + value = + self.get_data_value(&getter, chunk_x, chunk_z - 1, cell_x, cell_y, cell_z); + } + } + } + + value + } + + fn get_data_value( + &self, + getter: &F, + chunk_x: i32, + chunk_z: i32, + cell_x: i32, + cell_y: i32, + cell_z: i32, + ) -> f64 + where + F: Fn(&BlendingData, i32, i32, i32) -> f64, + { + let packed = (chunk_x as u32 as u64) | ((chunk_z as u32 as u64) << 32); + if let Some(data) = self.height_and_biome_blending_data.get(&packed) { + getter( + data, + cell_x - biome_coords::from_chunk(chunk_x), + cell_y, + cell_z - biome_coords::from_chunk(chunk_z), + ) + } else { + f64::MAX + } + } + + pub fn blend_biome( + &self, + quart_x: i32, + quart_y: i32, + quart_z: i32, + shift_noise: &DoublePerlinNoiseSampler, + ) -> Option<&'static Biome> { + let mut closest_distance = f64::INFINITY; + let mut closest_biome = None; + + for (&packed_pos, blending_data) in &self.height_and_biome_blending_data { + let chunk_x = (packed_pos & 0xFFFFFFFF) as i32; + let chunk_z = (packed_pos >> 32) as i32; + + blending_data.iterate_biomes( + biome_coords::from_chunk(chunk_x), + quart_y, + biome_coords::from_chunk(chunk_z), + |test_cell_x, test_cell_z, biome| { + let dx = (quart_x - test_cell_x) as f64; + let dz = (quart_z - test_cell_z) as f64; + let distance = (dx * dx + dz * dz).sqrt(); + + if distance <= HEIGHT_BLENDING_RANGE_CELLS as f64 && distance < closest_distance + { + closest_biome = Some(biome); + closest_distance = distance; + } + }, + ); + } + + if closest_distance == f64::INFINITY { + None + } else { + let shift = shift_noise.sample(quart_x as f64, 0.0, quart_z as f64) * 12.0; + let alpha = ((closest_distance + shift) / (HEIGHT_BLENDING_RANGE_CELLS + 1) as f64) + .clamp(0.0, 1.0); + if alpha > 0.5 { None } else { closest_biome } + } + } } pub struct BlenderBiomeSupplier<'a> { base: &'a dyn BiomeSupplier, + blender: &'a Blender, + shift_noise: DoublePerlinNoiseSampler, } impl BiomeSupplier for BlenderBiomeSupplier<'_> { fn biome(&self, x: i32, y: i32, z: i32, sampler: &mut MultiNoiseSampler<'_>) -> &'static Biome { - self.base.biome(x, y, z, sampler) + if let Some(blended) = self.blender.blend_biome(x, y, z, &self.shift_noise) { + blended + } else { + self.base.biome(x, y, z, sampler) + } } } -#[enum_dispatch] pub trait BlenderImpl { - fn calculate(&self, block_x: i32, block_z: i32) -> BlendResult; + fn blend_offset_and_factor(&self, block_x: i32, block_z: i32) -> BlendingOutput; - fn apply_blend_density(&self, pos: &Vector3, density: f64) -> f64; + fn blend_density(&self, pos: &Vector3, density: f64) -> f64; - fn get_biome_supplier<'a>(&self, supplier: &'a dyn BiomeSupplier) -> BlenderBiomeSupplier<'a>; + fn get_biome_supplier<'a>( + &'a self, + supplier: &'a dyn BiomeSupplier, + ) -> BlenderBiomeSupplier<'a>; } -pub struct NoBlendBlender {} - -impl BlenderImpl for NoBlendBlender { - fn calculate(&self, _block_x: i32, _block_z: i32) -> BlendResult { - BlendResult::new(1f64, 1f64) +impl BlenderImpl for Blender { + fn blend_offset_and_factor(&self, block_x: i32, block_z: i32) -> BlendingOutput { + self.blend_offset_and_factor(block_x, block_z) } - fn apply_blend_density(&self, _pos: &Vector3, density: f64) -> f64 { - density + fn blend_density(&self, pos: &Vector3, density: f64) -> f64 { + self.blend_density(pos.x, pos.y, pos.z, density) } - fn get_biome_supplier<'a>(&self, supplier: &'a dyn BiomeSupplier) -> BlenderBiomeSupplier<'a> { - BlenderBiomeSupplier { base: supplier } + fn get_biome_supplier<'a>( + &'a self, + supplier: &'a dyn BiomeSupplier, + ) -> BlenderBiomeSupplier<'a> { + let mut random = Xoroshiro::from_seed(42); + let shift_noise = DoublePerlinNoiseSampler::from_params( + &mut random, + &DoublePerlinNoiseParameters::OFFSET, + false, + ); + BlenderBiomeSupplier { + base: supplier, + blender: self, + shift_noise, + } } } diff --git a/pumpkin-world/src/generation/carver/canyon.rs b/pumpkin-world/src/generation/carver/canyon.rs index 7dea1e03e..86272e8b9 100644 --- a/pumpkin-world/src/generation/carver/canyon.rs +++ b/pumpkin-world/src/generation/carver/canyon.rs @@ -17,6 +17,7 @@ impl Carver for CanyonCarver { random: &mut RandomGenerator, _chunk_pos: &Vector2, carver_chunk_pos: &Vector2, + legacy_random_source: bool, ) { let CarverAdditionalConfig::Canyon(ref canyon_config) = config.additional else { return; @@ -51,6 +52,7 @@ impl Carver for CanyonCarver { 0, distance, y_scale, + legacy_random_source, ); } } @@ -71,10 +73,17 @@ impl CanyonCarver { step: i32, distance: i32, y_scale: f64, + legacy_random_source: bool, ) { - let mut random = RandomGenerator::Xoroshiro( - pumpkin_util::random::xoroshiro128::Xoroshiro::from_seed(tunnel_seed as u64), - ); + let mut random = if legacy_random_source { + RandomGenerator::Legacy(pumpkin_util::random::legacy_rand::LegacyRand::from_seed( + tunnel_seed as u64, + )) + } else { + RandomGenerator::Xoroshiro(pumpkin_util::random::xoroshiro128::Xoroshiro::from_seed( + tunnel_seed as u64, + )) + }; let width_factor_per_height = self.init_width_factors(chunk.height() as usize, config, &mut random); let mut y_rota = 0.0f32; @@ -234,8 +243,7 @@ impl CanyonCarver { let zd = (world_z as f64 + 0.5 - z) / horizontal_radius; if xd * xd + zd * zd < 1.0 { - let mut has_grass = false; - for world_y in (min_y..=max_y).rev() { + for world_y in (min_y + 1..=max_y).rev() { let yd = (world_y as f64 - 0.5 - y) / vertical_radius; if !self.should_skip( @@ -248,8 +256,7 @@ impl CanyonCarver { ) && !chunk.carving_mask.get(world_x, world_y, world_z) { chunk.carving_mask.set(world_x, world_y, world_z); - has_grass |= self - .carve_block(chunk, config, world_x, world_y, world_z, has_grass); + self.carve_block(chunk, config, world_x, world_y, world_z); } } } @@ -280,7 +287,6 @@ impl CanyonCarver { x: i32, y: i32, z: i32, - mut has_grass: bool, ) -> bool { let local_y = y - chunk.bottom_y() as i32; let state_id = chunk.get_block_state_raw(x & 15, local_y, z & 15); @@ -290,12 +296,6 @@ impl CanyonCarver { return false; } - if block.id == pumpkin_data::Block::GRASS_BLOCK.id - || block.id == pumpkin_data::Block::MYCELIUM.id - { - has_grass = true; - } - if config.replaceable.1.contains(&block.id) { let air = BlockState::from_id(pumpkin_data::Block::AIR.default_state.id); let lava = BlockState::from_id(pumpkin_data::Block::LAVA.default_state.id); @@ -310,18 +310,6 @@ impl CanyonCarver { chunk.set_block_state(x & 15, local_y, z & 15, air); } - if has_grass { - let down_y = y - 1; - let local_down_y = down_y - chunk.bottom_y() as i32; - if (0..chunk.height() as i32).contains(&local_down_y) { - let down_state_id = chunk.get_block_state_raw(x & 15, local_down_y, z & 15); - if pumpkin_data::Block::from_state_id(down_state_id).id - == pumpkin_data::Block::DIRT.id - { - // dirt replacement skipped - } - } - } return true; } false diff --git a/pumpkin-world/src/generation/carver/cave.rs b/pumpkin-world/src/generation/carver/cave.rs index 0e7eeff3b..aaddba41a 100644 --- a/pumpkin-world/src/generation/carver/cave.rs +++ b/pumpkin-world/src/generation/carver/cave.rs @@ -16,6 +16,7 @@ impl Carver for CaveCarver { random: &mut RandomGenerator, _chunk_pos: &Vector2, carver_chunk_pos: &Vector2, + legacy_random_source: bool, ) { let (is_nether, cave_config) = match config.additional { CarverAdditionalConfig::Cave(ref c) => (false, c), @@ -85,6 +86,7 @@ impl Carver for CaveCarver { if is_nether { 5.0 } else { 1.0 }, // this.getYScale() floor_level, is_nether, + legacy_random_source, ); } } @@ -151,10 +153,17 @@ impl CaveCarver { y_scale: f64, floor_level: f64, is_nether: bool, + legacy_random_source: bool, ) { - let mut random = RandomGenerator::Xoroshiro( - pumpkin_util::random::xoroshiro128::Xoroshiro::from_seed(tunnel_seed as u64), - ); + let mut random = if legacy_random_source { + RandomGenerator::Legacy(pumpkin_util::random::legacy_rand::LegacyRand::from_seed( + tunnel_seed as u64, + )) + } else { + RandomGenerator::Xoroshiro(pumpkin_util::random::xoroshiro128::Xoroshiro::from_seed( + tunnel_seed as u64, + )) + }; let split_point = random.next_bounded_i32(dist / 2) + dist / 4; let steep = random.next_bounded_i32(6) == 0; let mut y_rota = 0.0f32; @@ -195,6 +204,7 @@ impl CaveCarver { 1.0, floor_level, is_nether, + legacy_random_source, ); self.create_tunnel( config, @@ -213,6 +223,7 @@ impl CaveCarver { 1.0, floor_level, is_nether, + legacy_random_source, ); return; } @@ -301,17 +312,14 @@ impl CaveCarver { let zd = (world_z as f64 + 0.5 - z) / horizontal_radius; if xd * xd + zd * zd < 1.0 { - let mut has_grass = false; - for world_y in (min_y..=max_y).rev() { - let yd = (world_y as f64 + 0.5 - y) / vertical_radius; + for world_y in (min_y + 1..=max_y).rev() { + let yd = (world_y as f64 - 0.5 - y) / vertical_radius; if !self.should_skip(xd, yd, zd, floor_level) && !chunk.carving_mask.get(world_x, world_y, world_z) { chunk.carving_mask.set(world_x, world_y, world_z); - has_grass |= self.carve_block( - chunk, config, world_x, world_y, world_z, is_nether, has_grass, - ); + self.carve_block(chunk, config, world_x, world_y, world_z, is_nether); } } } @@ -336,7 +344,6 @@ impl CaveCarver { y: i32, z: i32, is_nether: bool, - mut has_grass: bool, ) -> bool { let local_y = y - chunk.bottom_y() as i32; let state_id = chunk.get_block_state_raw(x & 15, local_y, z & 15); @@ -346,12 +353,6 @@ impl CaveCarver { return false; } - if block.id == pumpkin_data::Block::GRASS_BLOCK.id - || block.id == pumpkin_data::Block::MYCELIUM.id - { - has_grass = true; - } - // Only carve if it's replaceable if config.replaceable.1.contains(&block.id) { // Replace with air or lava @@ -372,19 +373,6 @@ impl CaveCarver { chunk.set_block_state(x & 15, local_y, z & 15, air); } - if has_grass { - let down_y = y - 1; - let local_down_y = down_y - chunk.bottom_y() as i32; - if (0..chunk.height() as i32).contains(&local_down_y) { - let down_state_id = chunk.get_block_state_raw(x & 15, local_down_y, z & 15); - if pumpkin_data::Block::from_state_id(down_state_id).id - == pumpkin_data::Block::DIRT.id - { - // dirt replacement skipped - } - } - } - return true; } false diff --git a/pumpkin-world/src/generation/carver/mod.rs b/pumpkin-world/src/generation/carver/mod.rs index f75068893..fd1773278 100644 --- a/pumpkin-world/src/generation/carver/mod.rs +++ b/pumpkin-world/src/generation/carver/mod.rs @@ -17,14 +17,11 @@ pub trait Carver { random: &mut RandomGenerator, chunk_pos: &Vector2, carver_chunk_pos: &Vector2, + legacy_random_source: bool, ); } pub fn carve(chunk: &mut ProtoChunk, generator: &VanillaGenerator) { - let mut random = RandomGenerator::Xoroshiro( - pumpkin_util::random::xoroshiro128::Xoroshiro::from_seed(generator.random_config.seed), - ); - // Vanilla applyCarvers uses a range of 8 chunks (17x17 area) let radius = 8; let chunk_x = chunk.x; @@ -32,7 +29,6 @@ pub fn carve(chunk: &mut ProtoChunk, generator: &VanillaGenerator) { let chunk_pos = Vector2::new(chunk_x, chunk_z); let overworld_carvers = [&CAVE, &CAVE_EXTRA_UNDERGROUND, &CANYON]; - let nether_carvers = [&NETHER_CAVE]; let carvers_to_use = if generator.dimension == pumpkin_data::dimension::Dimension::OVERWORLD { @@ -56,14 +52,19 @@ pub fn carve(chunk: &mut ProtoChunk, generator: &VanillaGenerator) { // maintain the random seed logic. for (index, &config) in carvers_to_use.iter().enumerate() { let seed = get_carver_seed( - &mut random, generator.random_config.seed + index as u64, carver_x, carver_z, ); - let mut carver_random = RandomGenerator::Xoroshiro( - pumpkin_util::random::xoroshiro128::Xoroshiro::from_seed(seed), - ); + let mut carver_random = if generator.settings.legacy_random_source { + RandomGenerator::Legacy( + pumpkin_util::random::legacy_rand::LegacyRand::from_seed(seed), + ) + } else { + RandomGenerator::Xoroshiro( + pumpkin_util::random::xoroshiro128::Xoroshiro::from_seed(seed), + ) + }; if should_carve(config, &mut carver_random) { match config.additional { @@ -74,6 +75,7 @@ pub fn carve(chunk: &mut ProtoChunk, generator: &VanillaGenerator) { &mut carver_random, &chunk_pos, &carver_chunk_pos, + generator.settings.legacy_random_source, ); } CarverAdditionalConfig::Canyon(_) => { @@ -83,6 +85,7 @@ pub fn carve(chunk: &mut ProtoChunk, generator: &VanillaGenerator) { &mut carver_random, &chunk_pos, &carver_chunk_pos, + generator.settings.legacy_random_source, ); } } diff --git a/pumpkin-world/src/generation/mod.rs b/pumpkin-world/src/generation/mod.rs index 50f980798..75ade7957 100644 --- a/pumpkin-world/src/generation/mod.rs +++ b/pumpkin-world/src/generation/mod.rs @@ -1,7 +1,7 @@ #![allow(dead_code)] mod biome; -mod blender; +pub mod blender; mod block_predicate; mod block_state_provider; pub mod carver; diff --git a/pumpkin-world/src/generation/noise/perlin.rs b/pumpkin-world/src/generation/noise/perlin.rs index 8bdda39ee..4e3635eee 100644 --- a/pumpkin-world/src/generation/noise/perlin.rs +++ b/pumpkin-world/src/generation/noise/perlin.rs @@ -1,6 +1,7 @@ use pumpkin_data::chunk::DoublePerlinNoiseParameters; use pumpkin_util::{noise::perlin::OctavePerlinNoiseSampler, random::RandomImpl}; +#[derive(Clone)] pub struct DoublePerlinNoiseSampler { first_sampler: OctavePerlinNoiseSampler, second_sampler: OctavePerlinNoiseSampler, diff --git a/pumpkin-world/src/generation/proto_chunk.rs b/pumpkin-world/src/generation/proto_chunk.rs index ffe102635..c4cae2193 100644 --- a/pumpkin-world/src/generation/proto_chunk.rs +++ b/pumpkin-world/src/generation/proto_chunk.rs @@ -85,6 +85,11 @@ pub trait GenerationCache: HeightLimitView + BlockAccessor { fn ocean_floor_height_exclusive(&self, x: i32, z: i32) -> i32; fn is_air(&self, local_pos: &Vector3) -> bool; fn get_biome_for_terrain_gen(&self, x: i32, y: i32, z: i32) -> &'static Biome; + fn get_blending_data( + &self, + chunk_x: i32, + chunk_z: i32, + ) -> Option<&crate::generation::blender::blending_data::BlendingData>; } const AIR_BLOCK: Block = Block::AIR; @@ -169,6 +174,7 @@ pub struct ProtoChunk { pub stage: StagedChunkEnum, pub light: ChunkLight, pub carving_mask: crate::generation::carver::mask::CarvingMask, + pub blending_data: Option, /// Block entities pending creation when the chunk is finalized. /// These are created from structure templates during world generation. pub pending_block_entities: Vec, @@ -251,6 +257,7 @@ impl ProtoChunk { height as i32, dimension.min_y, ), + blending_data: None, pending_block_entities: Vec::new(), } } @@ -262,6 +269,7 @@ impl ProtoChunk { let mut proto_chunk = Self::new(chunk_data.x, chunk_data.z, generator); proto_chunk.light = chunk_data.light_engine.lock().unwrap().clone(); + proto_chunk.blending_data = chunk_data.blending_data.clone(); let section_data = &chunk_data.section; let heightmap_data = chunk_data.heightmap.lock().unwrap(); @@ -663,7 +671,8 @@ impl ProtoChunk { ActiveSupplier::Nether(s) => s, ActiveSupplier::Overworld(s) => s, }; - let biome_supplier = Blender::NO_BLEND.get_biome_supplier(base_supplier); + let blender = Blender::empty(); + let biome_supplier = blender.get_biome_supplier(base_supplier); let min_y = self.bottom_y(); let bottom_section = section_coords::block_to_section(min_y as i32); let top_section = section_coords::block_to_section(min_y as i32 + self.height() as i32 - 1); @@ -1135,9 +1144,7 @@ impl ProtoChunk { } let mut candidates = set.structures.to_vec(); - let mut random: RandomGenerator = - RandomGenerator::Xoroshiro(Xoroshiro::from_seed(seed)); - let carver_seed = get_carver_seed(&mut random, seed, self.x, self.z); + let carver_seed = get_carver_seed(seed, self.x, self.z); let mut random: RandomGenerator = RandomGenerator::Xoroshiro(Xoroshiro::from_seed(carver_seed)); @@ -1219,7 +1226,8 @@ impl ProtoChunk { ActiveSupplier::Nether(s) => s, ActiveSupplier::Overworld(s) => s, }; - let biome_supplier = Blender::NO_BLEND.get_biome_supplier(base_supplier); + let blender = Blender::empty(); + let biome_supplier = blender.get_biome_supplier(base_supplier); // Use an empty offset sampler since we are querying arbitrary world coordinates let multi_noise_config = MultiNoiseSamplerBuilderOptions::new(0, 0, 0); let mut multi_noise_sampler = diff --git a/pumpkin-world/src/generation/structure/placement.rs b/pumpkin-world/src/generation/structure/placement.rs index dc7d82d9f..fd5c26f07 100644 --- a/pumpkin-world/src/generation/structure/placement.rs +++ b/pumpkin-world/src/generation/structure/placement.rs @@ -213,8 +213,7 @@ fn should_generate_frequency( random.next_f32() < frequency } FrequencyReductionMethod::LegacyType3 => { - let mut random = RandomGenerator::Xoroshiro(Xoroshiro::from_seed(seed as u64)); - let carver_seed = get_carver_seed(&mut random, seed as u64, chunk_x, chunk_z); + let carver_seed = get_carver_seed(seed as u64, chunk_x, chunk_z); let mut random = RandomGenerator::Xoroshiro(Xoroshiro::from_seed(carver_seed)); random.next_f64() < f64::from(frequency) } diff --git a/pumpkin-world/src/generation/structure/structures/mod.rs b/pumpkin-world/src/generation/structure/structures/mod.rs index 3deca769c..f2cae9efc 100644 --- a/pumpkin-world/src/generation/structure/structures/mod.rs +++ b/pumpkin-world/src/generation/structure/structures/mod.rs @@ -578,8 +578,7 @@ pub struct StructureGeneratorContext { #[must_use] pub fn create_chunk_random(seed: i64, chunk_x: i32, chunk_z: i32) -> RandomGenerator { - let mut random: RandomGenerator = RandomGenerator::Xoroshiro(Xoroshiro::from_seed(seed as u64)); - let carver_seed = get_carver_seed(&mut random, seed as u64, chunk_x, chunk_z); + let carver_seed = get_carver_seed(seed as u64, chunk_x, chunk_z); RandomGenerator::Xoroshiro(Xoroshiro::from_seed(carver_seed)) }