fix(worldgen): match vanilla biome decoration (#3136)

This commit is contained in:
CyanStarlight
2026-08-29 15:29:33 +03:00
committed by GitHub
parent 3d4edce3b5
commit f6d0b4ab20
12 changed files with 960 additions and 101 deletions

View File

@@ -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),
}
}

View File

@@ -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<f64>,
}
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<f64> {
self.internal_next_gaussian
}
fn set_stored_next_gaussian(&mut self, value: Option<f64>) {
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);
}
}

View File

@@ -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)
}

View File

@@ -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]
);
}
}

View File

@@ -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<Chunk>,
surface_biomes: Option<Box<SurfaceBiomeNeighborhood>>,
}
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<Self> {
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<u8> {
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<SurfaceBiomePalette>; 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<u8> {
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);
}
}

View File

@@ -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,

View File

@@ -91,51 +91,80 @@ impl PlacedFeature {
random: &mut RandomGenerator,
pos: BlockPos,
) -> bool {
let mut stream: Vec<BlockPos> = 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<T, P, G>(
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<dyn Iterator<Item = BlockPos>>,
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<dyn Iterator<Item = BlockPos>> {
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);
}
}

View File

@@ -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<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
static OVERWORLD_FEATURES_PER_STEP: LazyLock<Vec<Vec<PlacedFeature>>> =
LazyLock::new(|| sort_features_per_step(OVERWORLD_BIOMES));
static OVERWORLD_BIOME_IDS: LazyLock<HashSet<u8>> =
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<Vec<PlacedFeature>> {
let mut feature_indices = HashMap::new();
let mut edges: BTreeMap<FeatureData, BTreeSet<FeatureData>> = 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<FeatureData, BTreeSet<FeatureData>>,
discovered: &mut BTreeSet<FeatureData>,
visiting: &mut BTreeSet<FeatureData>,
sorted: &mut Vec<FeatureData>,
) {
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)
);
}
}

View File

@@ -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;

View File

@@ -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<i32> {
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<u8> {
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 {

View File

@@ -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);

View File

@@ -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(&center));
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,
);
}