mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
fix(worldgen): match vanilla biome decoration (#3136)
This commit is contained in:
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
134
crates/pumpkin-util/src/random/worldgen_random.rs
Normal file
134
crates/pumpkin-util/src/random/worldgen_random.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
256
crates/pumpkin-world/src/generation/feature_order.rs
Normal file
256
crates/pumpkin-world/src/generation/feature_order.rs
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user