diff --git a/crates/pumpkin-util/src/random/mod.rs b/crates/pumpkin-util/src/random/mod.rs index 8739702c7..8fdb3b04a 100644 --- a/crates/pumpkin-util/src/random/mod.rs +++ b/crates/pumpkin-util/src/random/mod.rs @@ -4,10 +4,12 @@ use std::{ }; 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. @@ -32,6 +34,8 @@ pub fn get_seed() -> u64 { 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), } @@ -215,6 +219,7 @@ impl RandomImpl for RandomGenerator { 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()), } } @@ -223,6 +228,7 @@ impl RandomImpl for RandomGenerator { 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(), } } @@ -231,6 +237,7 @@ impl RandomImpl for RandomGenerator { 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(), } } @@ -239,6 +246,7 @@ impl RandomImpl for RandomGenerator { 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), } } @@ -247,6 +255,7 @@ impl RandomImpl for RandomGenerator { 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(), } } @@ -255,6 +264,7 @@ impl RandomImpl for RandomGenerator { 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(), } } @@ -263,6 +273,7 @@ impl RandomImpl for RandomGenerator { 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(), } } @@ -271,6 +282,7 @@ impl RandomImpl for RandomGenerator { 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(), } } @@ -279,6 +291,7 @@ impl RandomImpl for RandomGenerator { 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(), } } @@ -287,6 +300,7 @@ impl RandomImpl for RandomGenerator { 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), } } diff --git a/crates/pumpkin-util/src/random/worldgen_random.rs b/crates/pumpkin-util/src/random/worldgen_random.rs new file mode 100644 index 000000000..7a3ed9bbd --- /dev/null +++ b/crates/pumpkin-util/src/random/worldgen_random.rs @@ -0,0 +1,134 @@ +use crate::population_seed_fn; + +use super::{RandomDeriver, RandomImpl, gaussian::GaussianGenerator, xoroshiro128::Xoroshiro}; + +/// A Xoroshiro source accessed through Java `WorldgenRandom` bit-source semantics. +/// +/// `WorldgenRandom::next(bits)` takes the high bits of a complete underlying +/// `nextLong()` draw. This differs from direct `XoroshiroRandomSource` methods. +pub struct WorldgenRandom { + source: Xoroshiro, + internal_next_gaussian: Option, +} + +impl WorldgenRandom { + population_seed_fn!(); + + #[must_use] + pub const fn from_seed(seed: u64) -> Self { + Self { + source: Xoroshiro::from_seed(seed), + internal_next_gaussian: None, + } + } + + const fn next(&mut self, bits: u64) -> u64 { + self.source.next(bits) + } +} + +impl GaussianGenerator for WorldgenRandom { + fn stored_next_gaussian(&self) -> Option { + self.internal_next_gaussian + } + + fn set_stored_next_gaussian(&mut self, value: Option) { + self.internal_next_gaussian = value; + } +} + +impl RandomImpl for WorldgenRandom { + fn split(&mut self) -> Self { + Self { + source: self.source.split(), + internal_next_gaussian: None, + } + } + + fn next_splitter(&mut self) -> RandomDeriver { + RandomDeriver::Xoroshiro(self.source.next_splitter()) + } + + fn next_i32(&mut self) -> i32 { + self.next(32) as i32 + } + + fn next_bounded_i32(&mut self, bound: i32) -> i32 { + assert!(bound > 0, "bound must be positive"); + if bound & (bound - 1) == 0 { + return ((i64::from(bound) * self.next(31) as i64) >> 31) as i32; + } + + loop { + let value = self.next(31) as i32; + let result = value % bound; + if value.wrapping_sub(result).wrapping_add(bound - 1) >= 0 { + return result; + } + } + } + + fn next_i64(&mut self) -> i64 { + let high = self.next_i32(); + let low = self.next_i32(); + (i64::from(high) << 32).wrapping_add(i64::from(low)) + } + + fn next_bool(&mut self) -> bool { + self.next(1) != 0 + } + + fn next_f32(&mut self) -> f32 { + self.next(24) as f32 * 5.960_464_5E-8f32 + } + + fn next_f64(&mut self) -> f64 { + let high = self.next(26); + let low = self.next(27); + ((high << 27) + low) as f64 * 1.110_223_024_625_156_5E-16 + } + + fn next_gaussian(&mut self) -> f64 { + self.calculate_gaussian() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::random::get_decorator_seed; + + const SEED: u64 = 1_786_192_857_164_469_025; + + #[test] + fn decoration_seed_matches_vanilla() { + let chunk_86 = WorldgenRandom::get_population_seed(SEED, 86 << 4, 600 << 4); + let chunk_87 = WorldgenRandom::get_population_seed(SEED, 87 << 4, 600 << 4); + assert_eq!(chunk_86, 0x36e9_895d_da2b_ad81); + assert_eq!(chunk_87, 0xcdc9_553d_e86a_6171); + assert_eq!(get_decorator_seed(chunk_86, 69, 9), 0x36e9_895d_da2d_0d56); + assert_eq!(get_decorator_seed(chunk_87, 69, 9), 0xcdc9_553d_e86b_c146); + } + + #[test] + fn dry_grass_modifier_draws_match_vanilla() { + let mut chunk_86 = WorldgenRandom::from_seed(0x36e9_895d_da2d_0d56); + assert_eq!(chunk_86.next_f32().to_bits(), 0x3db2_ba18); + assert_eq!(chunk_86.next_bounded_i32(16), 10); + assert_eq!(chunk_86.next_bounded_i32(16), 6); + assert_eq!( + [ + chunk_86.next_bounded_i32(8), + chunk_86.next_bounded_i32(8), + chunk_86.next_bounded_i32(4), + chunk_86.next_bounded_i32(4), + chunk_86.next_bounded_i32(8), + chunk_86.next_bounded_i32(8), + ], + [6, 5, 1, 3, 7, 7] + ); + + let mut chunk_87 = WorldgenRandom::from_seed(0xcdc9_553d_e86b_c146); + assert_eq!(chunk_87.next_f32().to_bits(), 0x3f64_187a); + } +} diff --git a/crates/pumpkin-util/src/random/xoroshiro128.rs b/crates/pumpkin-util/src/random/xoroshiro128.rs index e0e92f00c..e59ccb9b9 100644 --- a/crates/pumpkin-util/src/random/xoroshiro128.rs +++ b/crates/pumpkin-util/src/random/xoroshiro128.rs @@ -105,7 +105,7 @@ impl Xoroshiro { /// /// # Returns /// A random value with the given number of bits. - const fn next(&mut self, bits: u64) -> u64 { + pub(super) const fn next(&mut self, bits: u64) -> u64 { self.next_random() >> (64 - bits) } diff --git a/crates/pumpkin-world/src/chunk_system/chunk_state.rs b/crates/pumpkin-world/src/chunk_system/chunk_state.rs index 4c57e7916..aba026853 100644 --- a/crates/pumpkin-world/src/chunk_system/chunk_state.rs +++ b/crates/pumpkin-world/src/chunk_system/chunk_state.rs @@ -129,18 +129,25 @@ impl StagedChunkEnum { Self::Lighting, Self::Features, Self::Carvers, - Self::Surface, + Self::Biomes, ]; - pub const FULL_RADIUS: i32 = 4; + pub const FULL_RADIUS: i32 = 5; #[must_use] pub const fn get_direct_radius(self) -> i32 { // self exclude match self { - Self::Features | Self::Lighting | Self::Spawn | Self::Full => 1, + Self::Surface | Self::Features | Self::Lighting | Self::Spawn | Self::Full => 1, _ => 0, } } #[must_use] + pub const fn get_read_radius(self) -> i32 { + match self { + Self::Surface => 1, + _ => self.get_write_radius(), + } + } + #[must_use] pub const fn get_write_radius(self) -> i32 { // self exclude match self { @@ -167,7 +174,7 @@ impl StagedChunkEnum { Self::StructureStart, ], Self::Noise => &[Self::StructureReferences], - Self::Surface => &[Self::Noise], + Self::Surface => &[Self::Noise, Self::Biomes], Self::Carvers => &[Self::Surface], Self::Features => &[Self::Carvers, Self::Carvers], Self::Lighting => &[Self::Features, Self::Features], @@ -332,3 +339,19 @@ impl Chunk { *self = Self::Level(Arc::new(chunk)); } } + +#[cfg(test)] +mod tests { + use super::StagedChunkEnum; + + #[test] + fn surface_reads_neighbor_biomes_without_owning_neighbors() { + assert_eq!(StagedChunkEnum::Surface.get_direct_radius(), 1); + assert_eq!(StagedChunkEnum::Surface.get_read_radius(), 1); + assert_eq!(StagedChunkEnum::Surface.get_write_radius(), 0); + assert_eq!( + StagedChunkEnum::Surface.get_direct_dependencies(), + &[StagedChunkEnum::Noise, StagedChunkEnum::Biomes] + ); + } +} diff --git a/crates/pumpkin-world/src/chunk_system/generation_cache.rs b/crates/pumpkin-world/src/chunk_system/generation_cache.rs index 61b337dbc..c45bd3450 100644 --- a/crates/pumpkin-world/src/chunk_system/generation_cache.rs +++ b/crates/pumpkin-world/src/chunk_system/generation_cache.rs @@ -1,6 +1,7 @@ use super::chunk_state::{Chunk, StagedChunkEnum}; use crate::ProtoChunk; use crate::chunk::ChunkHeightmapType; +use crate::generation::biome_coords; use crate::generation::generator; use crate::generation::height_limit::HeightLimitView; use crate::generation::proto_chunk::GenerationCache; @@ -21,6 +22,121 @@ pub struct Cache { pub z: i32, pub size: i32, pub chunks: Vec, + surface_biomes: Option>, +} + +struct SurfaceBiomePalette { + chunk_x: i32, + chunk_z: i32, + bottom_quart_y: i32, + height_quarts: usize, + biomes: Box<[u8]>, +} + +impl SurfaceBiomePalette { + fn from_chunk(chunk: &Chunk) -> Option { + match chunk { + Chunk::Proto(chunk) => Some(Self { + chunk_x: chunk.x, + chunk_z: chunk.z, + bottom_quart_y: biome_coords::from_block(chunk.bottom_y() as i32), + height_quarts: chunk.height() as usize >> 2, + biomes: chunk.flat_biome_map.clone(), + }), + Chunk::Level(chunk) => { + let bottom_y = chunk.section.min_y; + let height_quarts = chunk.section.count * 4; + let bottom_quart_y = biome_coords::from_block(bottom_y); + let mut biomes = vec![0; 4 * height_quarts * 4]; + + for local_x in 0..4 { + for local_y in 0..height_quarts { + for local_z in 0..4 { + let index = height_quarts * 4 * local_x + 4 * local_y + local_z; + biomes[index] = chunk.section.get_rough_biome_absolute_y( + local_x << 2, + biome_coords::to_block(bottom_quart_y + local_y as i32), + local_z << 2, + )?; + } + } + } + + Some(Self { + chunk_x: chunk.x, + chunk_z: chunk.z, + bottom_quart_y, + height_quarts, + biomes: biomes.into_boxed_slice(), + }) + } + } + } + + fn get_biome_id(&self, quart_x: i32, quart_y: i32, quart_z: i32) -> Option { + if quart_x >> 2 != self.chunk_x || quart_z >> 2 != self.chunk_z { + return None; + } + let local_y = quart_y - self.bottom_quart_y; + if !(0..self.height_quarts as i32).contains(&local_y) { + return None; + } + let local_x = (quart_x & 3) as usize; + let local_z = (quart_z & 3) as usize; + let index = self.height_quarts * 4 * local_x + 4 * local_y as usize + local_z; + self.biomes.get(index).copied() + } +} + +pub(crate) struct SurfaceBiomeNeighborhood { + center_x: i32, + center_z: i32, + palettes: [Option; 9], +} + +impl SurfaceBiomeNeighborhood { + #[must_use] + pub(crate) fn new(center_x: i32, center_z: i32) -> Self { + Self { + center_x, + center_z, + palettes: std::array::from_fn(|_| None), + } + } + + pub(crate) fn push_chunk(&mut self, chunk: &Chunk) -> bool { + let Some(palette) = SurfaceBiomePalette::from_chunk(chunk) else { + return false; + }; + let dx = palette.chunk_x - self.center_x; + let dz = palette.chunk_z - self.center_z; + if !(-1..=1).contains(&dx) || !(-1..=1).contains(&dz) { + return false; + } + let slot = &mut self.palettes[((dx + 1) * 3 + dz + 1) as usize]; + if slot.is_some() { + return false; + } + *slot = Some(palette); + true + } + + #[must_use] + pub(crate) fn is_complete(&self) -> bool { + self.palettes.iter().all(Option::is_some) + } + + #[must_use] + pub(crate) fn get_biome_id(&self, quart_x: i32, quart_y: i32, quart_z: i32) -> Option { + let dx = (quart_x >> 2) - self.center_x; + let dz = (quart_z >> 2) - self.center_z; + if !(-1..=1).contains(&dx) || !(-1..=1).contains(&dz) { + return None; + } + self.palettes[((dx + 1) * 3 + dz + 1) as usize] + .as_ref() + .and_then(|palette| palette.get_biome_id(quart_x, quart_y, quart_z)) + } } impl HeightLimitView for Cache { @@ -300,26 +416,31 @@ impl GenerationCache for Cache { } fn get_biome_for_terrain_gen(&self, x: i32, y: i32, z: i32) -> &'static Biome { - let dx = (x >> 4) - self.x; - let dy = (z >> 4) - self.z; - let (dx, dy) = if dx < 0 || dy < 0 || dx >= self.size || dy >= self.size { + let biome_pos = self.get_center_chunk().get_terrain_gen_biome_pos(x, y, z); + let dx = (biome_pos.x >> 2) - self.x; + let dz = (biome_pos.z >> 2) - self.z; + let (dx, dz) = if dx < 0 || dz < 0 || dx >= self.size || dz >= self.size { // Position is outside the cache — fall back to the centre chunk's biome let mid = self.size / 2; (mid, mid) } else { - (dx, dy) + (dx, dz) }; - match &self.chunks[(dx * self.size + dy) as usize] { + match &self.chunks[(dx * self.size + dz) as usize] { Chunk::Level(data) => { // Could this happen? Biome::from_id( data.section - .get_rough_biome_absolute_y((x & 15) as usize, y, (z & 15) as usize) + .get_rough_biome_absolute_y( + (biome_coords::to_block(biome_pos.x) & 15) as usize, + biome_coords::to_block(biome_pos.y), + (biome_coords::to_block(biome_pos.z) & 15) as usize, + ) .unwrap_or(0), ) .unwrap_or(&Biome::PLAINS) } - Chunk::Proto(data) => data.get_terrain_gen_biome(x, y, z), + Chunk::Proto(data) => data.get_biome(biome_pos.x, biome_pos.y, biome_pos.z), } } @@ -420,8 +541,34 @@ impl Cache { z, size, chunks: Vec::with_capacity((size * size) as usize), + surface_biomes: None, } } + + pub(crate) fn set_surface_biomes(&mut self, biomes: SurfaceBiomeNeighborhood) { + debug_assert!(biomes.is_complete()); + self.surface_biomes = Some(Box::new(biomes)); + } + + fn prepare_surface_biomes(&mut self) { + if self.surface_biomes.is_some() || self.size < 3 { + return; + } + let center_x = self.x + self.size / 2; + let center_z = self.z + self.size / 2; + let mut neighborhood = SurfaceBiomeNeighborhood::new(center_x, center_z); + for chunk_x in center_x - 1..=center_x + 1 { + for chunk_z in center_z - 1..=center_z + 1 { + let dx = chunk_x - self.x; + let dz = chunk_z - self.z; + let index = (dx * self.size + dz) as usize; + if !neighborhood.push_chunk(&self.chunks[index]) { + return; + } + } + } + self.surface_biomes = Some(Box::new(neighborhood)); + } #[allow(clippy::too_many_lines)] pub fn advance( &mut self, @@ -430,6 +577,9 @@ impl Cache { block_registry: &dyn WorldPortalExt, lighting_config: &LightingEngineConfig, ) { + if stage == StagedChunkEnum::Surface { + self.prepare_surface_biomes(); + } let mid = ((self.size * self.size) >> 1) as usize; match &self.chunks[mid] { Chunk::Level(_) => return, @@ -496,9 +646,13 @@ impl Cache { }, StagedChunkEnum::Surface => match generator { generator::WorldGenerator::Noise(noise_gen) => { + let surface_biomes = self + .surface_biomes + .take() + .expect("surface stage requires a complete biome neighborhood"); self.chunks[mid] .get_proto_chunk_mut() - .step_to_surface(noise_gen); + .step_to_surface(noise_gen, &surface_biomes); } generator::WorldGenerator::Flat(flat_gen) => { flat_gen.step_to_surface(self.chunks[mid].get_proto_chunk_mut()); @@ -560,3 +714,24 @@ impl Cache { } } } + +#[cfg(test)] +mod tests { + use super::{Chunk, SurfaceBiomeNeighborhood}; + use crate::chunk::ChunkData; + use pumpkin_data::biome::Biome; + + #[test] + fn surface_biome_snapshot_copies_level_chunk_palettes() { + let chunk = ChunkData::empty_sync(12, -4); + chunk.section.set_relative_biome(3, 0, 2, Biome::DESERT.id); + + let mut neighborhood = SurfaceBiomeNeighborhood::new(12, -4); + assert!(neighborhood.push_chunk(&Chunk::Level(chunk))); + assert_eq!( + neighborhood.get_biome_id(12 * 4 + 3, -16, -4 * 4 + 2), + Some(Biome::DESERT.id) + ); + assert_eq!(neighborhood.get_biome_id(13 * 4, -16, -4 * 4), None); + } +} diff --git a/crates/pumpkin-world/src/chunk_system/schedule.rs b/crates/pumpkin-world/src/chunk_system/schedule.rs index 4616f032b..0b417809c 100644 --- a/crates/pumpkin-world/src/chunk_system/schedule.rs +++ b/crates/pumpkin-world/src/chunk_system/schedule.rs @@ -2,7 +2,7 @@ use super::channel::LevelChange; use super::chunk_holder::ChunkHolder; use super::chunk_state::{Chunk, StagedChunkEnum}; use super::dag::{DAG, EdgeKey, Node, NodeKey}; -use super::generation_cache::Cache; +use super::generation_cache::{Cache, SurfaceBiomeNeighborhood}; use super::worker_logic::{RecvChunk, io_read_work, io_write_work}; use super::{ ChunkLevel, ChunkListener, ChunkLoading, ChunkPos, HashMapType, HashSetType, IOLock, @@ -416,10 +416,10 @@ impl GenerationSchedule { let Some(node) = self.graph.nodes.get(node_key) else { return false; // node was dropped, discard silently }; - let write_radius = node.stage.get_write_radius(); + let read_radius = node.stage.get_read_radius(); let pos = node.pos; - let all_ready = (-write_radius..=write_radius).all(|dx| { - (-write_radius..=write_radius).all(|dy| { + let all_ready = (-read_radius..=read_radius).all(|dx| { + (-read_radius..=read_radius).all(|dy| { self.chunk_map .get(&pos.add_raw(dx, dy)) .is_some_and(|h| h.chunk.is_some()) @@ -1319,10 +1319,10 @@ impl GenerationSchedule { } let write_radius = node.stage.get_write_radius(); + let read_radius = node.stage.get_read_radius(); - // Pre-validate that every chunk in the write area (including the - // center for write_radius==0 stages like Biomes, StructureStart, - // Noise, Surface) has its data present before we swap anything out. + // Pre-validate that every chunk in the read area has its data present + // before we snapshot neighbors or swap the write area out. // // The dependency graph ensures predecessor *tasks* are complete, but // there is a brief window between a task completing on a generation @@ -1331,8 +1331,8 @@ impl GenerationSchedule { // see chunk==None in that window. We park here and let // check_waiting_tasks() re-queue once all data has arrived. { - let all_ready = (-write_radius..=write_radius).all(|dx| { - (-write_radius..=write_radius).all(|dy| { + let all_ready = (-read_radius..=read_radius).all(|dx| { + (-read_radius..=read_radius).all(|dy| { self.chunk_map .get(&node.pos.add_raw(dx, dy)) .is_some_and(|h| h.chunk.is_some()) @@ -1361,6 +1361,28 @@ impl GenerationSchedule { write_radius << 1 | 1, ); + if node.stage == StagedChunkEnum::Surface { + let mut neighborhood = + SurfaceBiomeNeighborhood::new(node.pos.x, node.pos.y); + for dx in -1..=1 { + for dz in -1..=1 { + let holder = self + .chunk_map + .get(&node.pos.add_raw(dx, dz)) + .expect("surface biome dependency holder exists"); + let chunk = holder + .chunk + .as_ref() + .expect("surface biome dependency is available"); + assert!( + neighborhood.push_chunk(chunk), + "surface biome dependency has an incomplete palette" + ); + } + } + cache.set_surface_biomes(neighborhood); + } + let occupy = self.graph.nodes.insert(Node::new( ChunkPos::new(i32::MAX, i32::MAX), StagedChunkEnum::None, diff --git a/crates/pumpkin-world/src/generation/feature/placed_features.rs b/crates/pumpkin-world/src/generation/feature/placed_features.rs index b2bdd9a4b..2f04ae932 100644 --- a/crates/pumpkin-world/src/generation/feature/placed_features.rs +++ b/crates/pumpkin-world/src/generation/feature/placed_features.rs @@ -91,51 +91,80 @@ impl PlacedFeature { random: &mut RandomGenerator, pos: BlockPos, ) -> bool { - let mut stream: Vec = vec![pos]; - for modifier in &self.placement { - let mut new_stream = Vec::with_capacity(stream.len()); + let feature = match &self.feature { + Feature::Named(name) => CONFIGURED_FEATURES.get(name), + Feature::Inlined(feature) => Some(feature.as_ref()), + }; - for block_pos in stream { - let positions = modifier.get_positions( + generate_with_modifiers( + chunk, + 0, + self.placement.len(), + random, + pos, + &|chunk, modifier_index, random, pos| { + self.placement[modifier_index].get_positions( chunk, block_registry, min_y, height, feature_name, random, - block_pos, - ); - new_stream.extend(positions); - } - - stream = new_stream; - } - - let Some(feature) = (match &self.feature { - Feature::Named(name) => CONFIGURED_FEATURES.get(name), - Feature::Inlined(feature) => Some(feature.as_ref()), - }) else { - return false; - }; - - let mut ret = false; - for pos in stream { - if feature.generate( - chunk, - block_registry, - min_y, - height, - feature_name, - random, - pos, - ) { - ret = true; - } - } - ret + pos, + ) + }, + &|chunk, random, pos| { + feature.is_some_and(|feature| { + feature.generate( + chunk, + block_registry, + min_y, + height, + feature_name, + random, + pos, + ) + }) + }, + ) } } +fn generate_with_modifiers( + state: &mut T, + modifier_index: usize, + modifier_count: usize, + random: &mut RandomGenerator, + pos: BlockPos, + get_positions: &P, + generate: &G, +) -> bool +where + P: Fn(&T, usize, &mut RandomGenerator, BlockPos) -> Box>, + G: Fn(&mut T, &mut RandomGenerator, BlockPos) -> bool, +{ + if modifier_index == modifier_count { + return generate(state, random, pos); + } + + let positions = get_positions(state, modifier_index, random, pos); + let mut generated = false; + for next_pos in positions { + if generate_with_modifiers( + state, + modifier_index + 1, + modifier_count, + random, + next_pos, + get_positions, + generate, + ) { + generated = true; + } + } + generated +} + pub enum PlacementModifier { BlockPredicateFilter(BlockFilterPlacementModifier), RarityFilter(RarityFilterPlacementModifier), @@ -570,3 +599,44 @@ pub const fn all_placed_feature_names() -> &'static [&'static str] { pumpkin_data::placed_feature::PlacedFeature::all_names() } include!("../../../../pumpkin-data/src/generated/placed_features_generated.rs"); + +#[cfg(test)] +mod tests { + use pumpkin_util::random::xoroshiro128::Xoroshiro; + + use super::*; + + #[test] + fn modifier_candidates_interleave_with_feature_rng() { + let seed = 0x5EED_CAFEu64; + let mut expected_random = RandomGenerator::Xoroshiro(Xoroshiro::from_seed(seed)); + let expected = vec![ + (expected_random.next_i32(), expected_random.next_i32()), + (expected_random.next_i32(), expected_random.next_i32()), + ]; + + let mut random = RandomGenerator::Xoroshiro(Xoroshiro::from_seed(seed)); + let mut generated = Vec::new(); + let result = generate_with_modifiers( + &mut generated, + 0, + 2, + &mut random, + BlockPos::new(0, 0, 0), + &|_, modifier_index, random, pos| -> Box> { + match modifier_index { + 0 => Box::new([pos, pos].into_iter()), + 1 => Box::new(iter::once(BlockPos::new(random.next_i32(), 0, 0))), + _ => unreachable!(), + } + }, + &|generated, random, pos| { + generated.push((pos.0.x, random.next_i32())); + true + }, + ); + + assert!(result); + assert_eq!(generated, expected); + } +} diff --git a/crates/pumpkin-world/src/generation/feature_order.rs b/crates/pumpkin-world/src/generation/feature_order.rs new file mode 100644 index 000000000..e4c5a102e --- /dev/null +++ b/crates/pumpkin-world/src/generation/feature_order.rs @@ -0,0 +1,256 @@ +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::sync::LazyLock; + +use pumpkin_data::chunk::Biome; +use pumpkin_data::placed_feature::PlacedFeature; + +// The order of biomes in this array is used by FeatureSorter to break ties between otherwise independent features. +const OVERWORLD_BIOMES: &[&Biome] = &[ + &Biome::MUSHROOM_FIELDS, + &Biome::DEEP_FROZEN_OCEAN, + &Biome::FROZEN_OCEAN, + &Biome::DEEP_COLD_OCEAN, + &Biome::COLD_OCEAN, + &Biome::DEEP_OCEAN, + &Biome::OCEAN, + &Biome::DEEP_LUKEWARM_OCEAN, + &Biome::LUKEWARM_OCEAN, + &Biome::WARM_OCEAN, + &Biome::STONY_SHORE, + &Biome::SWAMP, + &Biome::MANGROVE_SWAMP, + &Biome::SNOWY_SLOPES, + &Biome::SNOWY_PLAINS, + &Biome::SNOWY_BEACH, + &Biome::WINDSWEPT_GRAVELLY_HILLS, + &Biome::GROVE, + &Biome::WINDSWEPT_HILLS, + &Biome::SNOWY_TAIGA, + &Biome::WINDSWEPT_FOREST, + &Biome::TAIGA, + &Biome::PLAINS, + &Biome::MEADOW, + &Biome::BEACH, + &Biome::FOREST, + &Biome::OLD_GROWTH_SPRUCE_TAIGA, + &Biome::FLOWER_FOREST, + &Biome::BIRCH_FOREST, + &Biome::DARK_FOREST, + &Biome::PALE_GARDEN, + &Biome::SAVANNA_PLATEAU, + &Biome::SAVANNA, + &Biome::JUNGLE, + &Biome::BADLANDS, + &Biome::DESERT, + &Biome::WOODED_BADLANDS, + &Biome::JAGGED_PEAKS, + &Biome::STONY_PEAKS, + &Biome::FROZEN_RIVER, + &Biome::RIVER, + &Biome::ICE_SPIKES, + &Biome::OLD_GROWTH_PINE_TAIGA, + &Biome::SUNFLOWER_PLAINS, + &Biome::OLD_GROWTH_BIRCH_FOREST, + &Biome::SPARSE_JUNGLE, + &Biome::BAMBOO_JUNGLE, + &Biome::ERODED_BADLANDS, + &Biome::WINDSWEPT_SAVANNA, + &Biome::CHERRY_GROVE, + &Biome::FROZEN_PEAKS, + &Biome::DRIPSTONE_CAVES, + &Biome::LUSH_CAVES, + &Biome::SULFUR_CAVES, + &Biome::DEEP_DARK, +]; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct FeatureData { + step: usize, + encounter_index: usize, + feature: PlacedFeature, +} + +impl Ord for FeatureData { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + (self.step, self.encounter_index, self.feature).cmp(&( + other.step, + other.encounter_index, + other.feature, + )) + } +} + +impl PartialOrd for FeatureData { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +static OVERWORLD_FEATURES_PER_STEP: LazyLock>> = + LazyLock::new(|| sort_features_per_step(OVERWORLD_BIOMES)); +static OVERWORLD_BIOME_IDS: LazyLock> = + LazyLock::new(|| OVERWORLD_BIOMES.iter().map(|biome| biome.id).collect()); + +pub fn select_features(biome_ids: &[u8], step: usize) -> Vec<(usize, PlacedFeature)> { + // Feature generation does not currently carry its biome-source identity. + // TODO: Consider modeling the Nether and End biome feature orders + if !biome_ids + .iter() + .any(|biome_id| OVERWORLD_BIOME_IDS.contains(biome_id)) + { + let mut selected: Vec<_> = biome_ids + .iter() + .filter_map(|biome_id| Biome::from_id(*biome_id)) + .filter_map(|biome| biome.features.get(step)) + .flat_map(|features| features.iter().copied()) + .collect(); + selected.sort_unstable(); + selected.dedup(); + return selected.into_iter().enumerate().collect(); + } + + let mut selected = HashSet::new(); + + for biome_id in biome_ids { + if !OVERWORLD_BIOME_IDS.contains(biome_id) { + continue; + } + if let Some(features) = Biome::from_id(*biome_id).and_then(|biome| biome.features.get(step)) + { + selected.extend(features.iter().copied()); + } + } + + OVERWORLD_FEATURES_PER_STEP + .get(step) + .into_iter() + .flatten() + .copied() + .enumerate() + .filter(|(_, feature)| selected.contains(feature)) + .collect() +} + +fn sort_features_per_step(biomes: &[&Biome]) -> Vec> { + let mut feature_indices = HashMap::new(); + let mut edges: BTreeMap> = BTreeMap::new(); + let mut max_steps = 0; + + for biome in biomes { + max_steps = max_steps.max(biome.features.len()); + let mut biome_features = Vec::new(); + + for (step, features) in biome.features.iter().enumerate() { + for &feature in *features { + let next_index = feature_indices.len(); + let encounter_index = *feature_indices.entry(feature).or_insert(next_index); + let data = FeatureData { + step, + encounter_index, + feature, + }; + edges.entry(data).or_default(); + biome_features.push(data); + } + } + + for pair in biome_features.windows(2) { + edges.entry(pair[0]).or_default().insert(pair[1]); + } + } + + let mut discovered = BTreeSet::new(); + let mut visiting = BTreeSet::new(); + let mut sorted = Vec::with_capacity(edges.len()); + for &feature in edges.keys() { + visit_feature(feature, &edges, &mut discovered, &mut visiting, &mut sorted); + } + sorted.reverse(); + + let mut per_step = vec![Vec::new(); max_steps]; + for feature in sorted { + per_step[feature.step].push(feature.feature); + } + per_step +} + +fn visit_feature( + feature: FeatureData, + edges: &BTreeMap>, + discovered: &mut BTreeSet, + visiting: &mut BTreeSet, + sorted: &mut Vec, +) { + if discovered.contains(&feature) { + return; + } + assert!( + visiting.insert(feature), + "feature order cycle contains {feature:?}" + ); + if let Some(next_features) = edges.get(&feature) { + for &next in next_features { + visit_feature(next, edges, discovered, visiting, sorted); + } + } + visiting.remove(&feature); + discovered.insert(feature); + sorted.push(feature); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn dry_grass_uses_vanilla_global_feature_index() { + let step = &OVERWORLD_FEATURES_PER_STEP[9]; + assert_eq!( + step.iter() + .position(|feature| *feature == PlacedFeature::PatchDryGrassDesert), + Some(69) + ); + assert_eq!( + step.iter() + .position(|feature| *feature == PlacedFeature::PatchDryGrassBadlands), + Some(71) + ); + } + + #[test] + fn biome_selection_keeps_global_indices_and_order() { + let selected = select_features(&[Biome::SAVANNA.id, Biome::DESERT.id], 9); + assert!(selected.windows(2).all(|pair| pair[0].0 < pair[1].0)); + assert!(selected.contains(&(69, PlacedFeature::PatchDryGrassDesert))); + assert!( + !selected + .iter() + .any(|(_, feature)| { *feature == PlacedFeature::PatchDryGrassBadlands }) + ); + + let savanna_grass = selected + .iter() + .find(|(_, feature)| *feature == PlacedFeature::PatchGrassSavanna) + .expect("savanna grass must be selected"); + assert_eq!(savanna_grass.0, 12); + } + + #[test] + fn selection_deduplicates_biomes_from_neighbor_chunks() { + let once = select_features(&[Biome::DESERT.id], 9); + let repeated = select_features(&[Biome::DESERT.id, Biome::DESERT.id, Biome::DESERT.id], 9); + assert_eq!(once, repeated); + } + + #[test] + fn non_overworld_selection_preserves_local_indexing() { + let selected = select_features(&[Biome::NETHER_WASTES.id], 9); + assert!(!selected.is_empty()); + assert!( + selected + .iter() + .enumerate() + .all(|(index, (feature_index, _))| index == *feature_index) + ); + } +} diff --git a/crates/pumpkin-world/src/generation/mod.rs b/crates/pumpkin-world/src/generation/mod.rs index d3f37fe72..9fbd12c17 100644 --- a/crates/pumpkin-world/src/generation/mod.rs +++ b/crates/pumpkin-world/src/generation/mod.rs @@ -6,6 +6,7 @@ mod block_predicate; mod block_state_provider; pub mod carver; pub mod feature; +mod feature_order; pub mod generator; pub mod height_limit; pub mod height_provider; diff --git a/crates/pumpkin-world/src/generation/proto_chunk.rs b/crates/pumpkin-world/src/generation/proto_chunk.rs index 005b5e9d1..71a458b96 100644 --- a/crates/pumpkin-world/src/generation/proto_chunk.rs +++ b/crates/pumpkin-world/src/generation/proto_chunk.rs @@ -16,7 +16,10 @@ use pumpkin_util::random::{RandomImpl, get_carver_seed}; use pumpkin_util::{ HeightMap, math::{block_box::BlockBox, position::BlockPos, vector3::Vector3}, - random::{RandomGenerator, get_decorator_seed, xoroshiro128::Xoroshiro}, + random::{ + RandomGenerator, get_decorator_seed, worldgen_random::WorldgenRandom, + xoroshiro128::Xoroshiro, + }, }; use rustc_hash::FxHashMap; @@ -24,6 +27,7 @@ use super::{ GlobalRandomConfig, biome_coords, blender::{Blender, BlenderImpl}, feature::placed_features::PLACED_FEATURES, + feature_order::select_features, noise::router::{ multi_noise_sampler::MultiNoiseSampler, proto_noise_router::DoublePerlinNoiseBuilder, surface_height_sampler::SurfaceHeightEstimateSampler, @@ -35,7 +39,7 @@ use super::{ use crate::biome::{BiomeSupplier, MultiNoiseBiomeSupplier, end::TheEndBiomeSupplier}; use crate::chunk::format::LightContainer; use crate::chunk::{ChunkData, ChunkHeightmapType, ChunkLight}; -use crate::chunk_system::StagedChunkEnum; +use crate::chunk_system::{StagedChunkEnum, generation_cache::SurfaceBiomeNeighborhood}; use crate::generation::height_limit::HeightLimitView; use crate::generation::noise::aquifer_sampler::{FluidLevel, FluidLevelSamplerImpl}; use crate::generation::noise::perlin::DoublePerlinNoiseSampler; @@ -773,7 +777,11 @@ impl ProtoChunk { self.stage = StagedChunkEnum::Noise; } - pub fn step_to_surface(&mut self, generator: &super::generator::VanillaGenerator) { + pub(crate) fn step_to_surface( + &mut self, + generator: &super::generator::VanillaGenerator, + surface_biomes: &SurfaceBiomeNeighborhood, + ) { debug_assert_eq!(self.stage, StagedChunkEnum::Noise); let start_x = start_block_x(self.x); let start_z = start_block_z(self.z); @@ -796,7 +804,11 @@ impl ProtoChunk { &surface_config, ); - self.build_surface(generator, &mut surface_height_estimate_sampler); + self.build_surface( + generator, + surface_biomes, + &mut surface_height_estimate_sampler, + ); self.stage = StagedChunkEnum::Surface; } @@ -956,16 +968,37 @@ impl ProtoChunk { #[must_use] pub fn get_terrain_gen_biome_id(&self, x: i32, y: i32, z: i32) -> u8 { - let seed_biome_pos = biome::get_biome_blend( + let biome_pos = self.get_terrain_gen_biome_pos(x, y, z); + + self.get_biome_id(biome_pos.x, biome_pos.y, biome_pos.z) + } + + #[must_use] + pub(crate) fn get_terrain_gen_biome_pos(&self, x: i32, y: i32, z: i32) -> Vector3 { + biome::get_biome_blend( self.bottom_y(), self.height(), self.biome_mixer_seed, x, y, z, - ); + ) + } - self.get_biome_id(seed_biome_pos.x, seed_biome_pos.y, seed_biome_pos.z) + pub(crate) fn get_terrain_gen_biome_id_from_neighborhood( + &self, + surface_biomes: &SurfaceBiomeNeighborhood, + x: i32, + y: i32, + z: i32, + ) -> Option { + let biome_pos = self.get_terrain_gen_biome_pos(x, y, z); + + if biome_pos.x >> 2 == self.x && biome_pos.z >> 2 == self.z { + return Some(self.get_biome_id(biome_pos.x, biome_pos.y, biome_pos.z)); + } + + surface_biomes.get_biome_id(biome_pos.x, biome_pos.y, biome_pos.z) } #[must_use] @@ -974,9 +1007,14 @@ impl ProtoChunk { } #[expect(clippy::too_many_lines)] - pub fn build_surface( + #[expect( + clippy::panic, + reason = "surface scheduling guarantees a complete 3x3 biome neighborhood" + )] + pub(crate) fn build_surface( &mut self, generator: &super::generator::VanillaGenerator, + surface_biomes: &SurfaceBiomeNeighborhood, surface_height_estimate_sampler: &mut SurfaceHeightEstimateSampler, ) { let start_x = chunk_pos::start_block_x(self.x); @@ -1010,7 +1048,11 @@ impl ProtoChunk { top_block }; - let this_biome = self.get_terrain_gen_biome_id(x, biome_y, z); + let Some(this_biome) = + self.get_terrain_gen_biome_id_from_neighborhood(surface_biomes, x, biome_y, z) + else { + panic!("surface biome neighborhood must cover fuzzy biome lookup"); + }; if this_biome == Biome::ERODED_BADLANDS { terrain_cache .terrain_builder @@ -1067,11 +1109,15 @@ impl ProtoChunk { context.init_vertical(stone_depth_above, stone_depth_below, y, fluid_height); if state.id == self.default_block.id { - context.biome = self.get_terrain_gen_biome( + let Some(biome_id) = self.get_terrain_gen_biome_id_from_neighborhood( + surface_biomes, context.block_pos_x, context.block_pos_y, context.block_pos_z, - ); + ) else { + panic!("surface biome neighborhood must cover fuzzy biome lookup"); + }; + context.biome = Biome::from_id(biome_id).unwrap_or(&Biome::PLAINS); let new_state = try_apply_material_rule( &settings.surface_rule, self, @@ -1108,30 +1154,38 @@ impl ProtoChunk { block_registry: &dyn WorldPortalExt, random_config: &GlobalRandomConfig, ) { - let (center_x, center_z, min_y, generation_min_y, generation_height, biomes_in_chunk) = { + let (center_x, center_z, min_y, generation_min_y, generation_height) = { let chunk = cache.get_center_chunk(); - let mut unique_biomes = Vec::with_capacity(4); - for &biome_id in &chunk.flat_biome_map { - if !unique_biomes.contains(&biome_id) { - unique_biomes.push(biome_id); - } - } ( chunk.x, chunk.z, chunk.bottom_y() as i32, chunk.generation_bottom_y(), chunk.generation_height(), - unique_biomes, ) }; + // Vanilla gathers every biome stored in the 3x3 chunk neighborhood before selecting the + // globally ordered feature set. + let mut possible_biomes = Vec::new(); + for chunk_x in center_x - 1..=center_x + 1 { + for chunk_z in center_z - 1..=center_z + 1 { + if let Some(chunk) = cache.get_chunk(chunk_x, chunk_z) { + for &biome_id in &chunk.flat_biome_map { + if !possible_biomes.contains(&biome_id) { + possible_biomes.push(biome_id); + } + } + } + } + } + let start_block_x = chunk_pos::start_block_x(center_x); let start_block_z = chunk_pos::start_block_z(center_z); let origin_pos = BlockPos::new(start_block_x, min_y, start_block_z); let population_seed = - Xoroshiro::get_population_seed(random_config.seed, start_block_x, start_block_z); + WorldgenRandom::get_population_seed(random_config.seed, start_block_x, start_block_z); for step in 0..11 { Self::generate_structure_step( @@ -1142,25 +1196,12 @@ impl ProtoChunk { random_config.seed as i64, ); - let mut features_to_run = Vec::new(); - for biome_id in &biomes_in_chunk { - if let Some(biome) = Biome::from_id(*biome_id) - && let Some(features_at_step) = biome.features.get(step) - { - for &feature_id in *features_at_step { - features_to_run.push(feature_id); - } - } - } - - features_to_run.sort_unstable(); - features_to_run.dedup(); - - for (p, feature_enum) in features_to_run.into_iter().enumerate() { + for (global_index, feature_enum) in select_features(&possible_biomes, step) { if let Some(feature) = PLACED_FEATURES.get(&feature_enum) { - let decorator_seed = get_decorator_seed(population_seed, p as u64, step as u64); + let decorator_seed = + get_decorator_seed(population_seed, global_index as u64, step as u64); let mut random = - RandomGenerator::Xoroshiro(Xoroshiro::from_seed(decorator_seed)); + RandomGenerator::Worldgen(WorldgenRandom::from_seed(decorator_seed)); feature.generate( cache, @@ -1256,7 +1297,7 @@ impl ProtoChunk { } let decorator_seed = get_decorator_seed(population_seed, 0, step as u64); - let mut random = RandomGenerator::Xoroshiro(Xoroshiro::from_seed(decorator_seed)); + let mut random = RandomGenerator::Worldgen(WorldgenRandom::from_seed(decorator_seed)); let chunk = cache.get_center_chunk_mut(); for collector_arc in tasks { diff --git a/crates/pumpkin-world/src/generation/proto_chunk_test.rs b/crates/pumpkin-world/src/generation/proto_chunk_test.rs index 0ba3d6553..3c26b3015 100644 --- a/crates/pumpkin-world/src/generation/proto_chunk_test.rs +++ b/crates/pumpkin-world/src/generation/proto_chunk_test.rs @@ -1,11 +1,107 @@ #[cfg(test)] mod test { #![allow(clippy::print_stdout, clippy::needless_pass_by_value)] - use crate::chunk_system::chunk_state::StagedChunkEnum; - use crate::generation::{generator::WorldGenerator, get_world_gen, proto_chunk::ProtoChunk}; + use crate::chunk_system::{ + Chunk, chunk_state::StagedChunkEnum, generation_cache::SurfaceBiomeNeighborhood, + }; + use crate::generation::{ + biome_coords, generator::WorldGenerator, get_world_gen, proto_chunk::ProtoChunk, + }; use pumpkin_data::dimension::Dimension; use pumpkin_util::world_seed::Seed; + fn surface_biomes( + world_gen: &WorldGenerator, + center_x: i32, + center_z: i32, + ) -> crate::chunk_system::generation_cache::SurfaceBiomeNeighborhood { + let WorldGenerator::Noise(generator) = world_gen else { + unreachable!() + }; + let mut neighborhood = SurfaceBiomeNeighborhood::new(center_x, center_z); + for chunk_x in center_x - 1..=center_x + 1 { + for chunk_z in center_z - 1..=center_z + 1 { + let mut chunk = ProtoChunk::new(chunk_x, chunk_z, world_gen); + chunk.step_to_biomes(generator); + assert!(neighborhood.push_chunk(&Chunk::Proto(Box::new(chunk)))); + } + } + assert!(neighborhood.is_complete()); + neighborhood + } + + #[test] + fn terrain_biome_lookup_crosses_chunk_boundary() { + use pumpkin_data::chunk::Biome; + + let seed = Seed(1_786_192_857_164_469_025); + let world_gen = get_world_gen(seed, Dimension::OVERWORLD, false, Vec::new(), String::new()); + let WorldGenerator::Noise(generator) = &*world_gen else { + unreachable!() + }; + + let mut north = ProtoChunk::new(84, 599, &world_gen); + let mut south = ProtoChunk::new(84, 600, &world_gen); + north.step_to_biomes(generator); + south.step_to_biomes(generator); + + // Biome zoom selects absolute quart (338, 17, 2399) on both sides of z=9600. + // Vanilla resolves that quart through the owning chunk. It must not wrap the quart + // coordinate into the chunk whose surface is currently being generated. + let expected = north.get_biome_id(338, 17, 2399); + assert_eq!(expected, Biome::SAVANNA.id); + assert_eq!(north.get_terrain_gen_biome_id(1354, 68, 9599), expected); + + let mut surface_biomes = SurfaceBiomeNeighborhood::new(south.x, south.z); + for chunk_x in south.x - 1..=south.x + 1 { + for chunk_z in south.z - 1..=south.z + 1 { + let mut chunk = ProtoChunk::new(chunk_x, chunk_z, &world_gen); + chunk.step_to_biomes(generator); + if (chunk_x, chunk_z) == (north.x, north.z) { + // Make stored authority observably differ from a fresh biome-source sample. + let index = chunk.local_biome_pos_to_biome_index( + 338i32.rem_euclid(4), + 17 - biome_coords::from_block(chunk.bottom_y() as i32), + 2399i32.rem_euclid(4), + ); + chunk.flat_biome_map[index] = Biome::DESERT.id; + } + assert!(surface_biomes.push_chunk(&Chunk::Proto(Box::new(chunk)))); + } + } + assert_eq!( + south.get_terrain_gen_biome_id_from_neighborhood(&surface_biomes, 1354, 68, 9600), + Some(Biome::DESERT.id) + ); + } + + #[test] + fn generation_cache_resolves_blended_biome_through_owning_chunk() { + use crate::chunk_system::{Chunk, generation_cache::Cache}; + use crate::generation::proto_chunk::GenerationCache; + use pumpkin_data::chunk::Biome; + + let seed = Seed(1_786_192_857_164_469_025); + let world_gen = get_world_gen(seed, Dimension::OVERWORLD, false, Vec::new(), String::new()); + let WorldGenerator::Noise(generator) = &*world_gen else { + unreachable!() + }; + + let mut cache = Cache::new(83, 599, 3); + for chunk_x in 83..=85 { + for chunk_z in 599..=601 { + let mut chunk = ProtoChunk::new(chunk_x, chunk_z, &world_gen); + chunk.step_to_biomes(generator); + cache.chunks.push(Chunk::Proto(Box::new(chunk))); + } + } + + assert_eq!( + cache.get_biome_for_terrain_gen(1354, 68, 9600).id, + Biome::SAVANNA.id + ); + } + #[test] fn structure_references_are_rebuilt_when_resuming_generation() { use crate::chunk_system::chunk_state::Chunk; @@ -89,14 +185,15 @@ mod test { "heightmap corrupted by save/load roundtrip (transposed or lost)" ); - resumed.step_to_surface(generator); + let surface_biomes = surface_biomes(&world_gen, cx, cz); + resumed.step_to_surface(generator, &surface_biomes); let mut fresh = ProtoChunk::new(cx, cz, &world_gen); fresh.step_to_biomes(generator); fresh.set_structure_starts(generator); fresh.set_structure_references(generator); fresh.step_to_noise(generator); - fresh.step_to_surface(generator); + fresh.step_to_surface(generator, &surface_biomes); let bottom = fresh.bottom_y() as i32; let top = bottom + fresh.height() as i32; @@ -226,7 +323,8 @@ mod test { chunk.step_to_biomes(generator); chunk.stage = StagedChunkEnum::StructureReferences; chunk.step_to_noise(generator); - chunk.step_to_surface(generator); + let surface_biomes = surface_biomes(&world_gen, chunk_x, chunk_z); + chunk.step_to_surface(generator, &surface_biomes); let mismatches = count_dump_mismatches(&chunk, expected_data, test_name); assert_air_above_dumped_window(&chunk, expected_data, test_name); diff --git a/crates/pumpkin-world/src/lib.rs b/crates/pumpkin-world/src/lib.rs index e1957af5b..074568e59 100644 --- a/crates/pumpkin-world/src/lib.rs +++ b/crates/pumpkin-world/src/lib.rs @@ -165,6 +165,7 @@ pub fn bench_create_and_populate_biome(random_config: &GlobalRandomConfig) { } pub fn bench_create_and_populate_noise_with_surface(random_config: &GlobalRandomConfig) { + use crate::chunk_system::{Chunk, generation_cache::SurfaceBiomeNeighborhood}; use crate::generation::generator::{GeneratorInit, VanillaGenerator, WorldGenerator}; use crate::generation::noise::router::{ multi_noise_sampler::{MultiNoiseSampler, MultiNoiseSamplerBuilderOptions}, @@ -244,11 +245,35 @@ pub fn bench_create_and_populate_noise_with_surface(random_config: &GlobalRandom ); chunk.populate_biomes(generator, &mut multi_noise_sampler); + + // Surface biome zoom may select a quart from an immediate neighbor. Build the same read-only + // palette snapshot that the runtime scheduler supplies to a Surface task. + let mut surface_biomes = SurfaceBiomeNeighborhood::new(0, 0); + for chunk_x in -1..=1 { + for chunk_z in -1..=1 { + if chunk_x == 0 && chunk_z == 0 { + continue; + } + let mut neighbor = ProtoChunk::new(chunk_x, chunk_z, &world_gen); + neighbor.step_to_biomes(generator); + assert!(surface_biomes.push_chunk(&Chunk::Proto(Box::new(neighbor)))); + } + } + let center = Chunk::Proto(Box::new(chunk)); + assert!(surface_biomes.push_chunk(¢er)); + let Chunk::Proto(mut chunk) = center else { + unreachable!() + }; + chunk.populate_noise( generator, &mut noise_sampler, &generator.random_config.ore_random_deriver, &mut surface_height_estimate_sampler, ); - chunk.build_surface(generator, &mut surface_height_estimate_sampler); + chunk.build_surface( + generator, + &surface_biomes, + &mut surface_height_estimate_sampler, + ); }