mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
fix(world): size proto chunks by the dimension, not by the noise settings (#2803)
`ProtoChunk::new` took its height from the noise settings while
`Chunk::build_level_sections` builds and reads back `dimension.height / 16`
sections. The End and the Nether are the two dimensions taller than their noise
settings (256 vs 128), so their proto chunks held 8 sections for the 16 the
level chunk asks for and `get_block_state_raw` indexed past the end of
`flat_block_map`, panicking on every generation thread. The same mismatch left
light data covering half the column and dropped features placed above y = 128.
A chunk spans its dimension, as in vanilla's `LevelHeightAccessor`; the noise
settings only bound the vertical window the generator writes into. `ProtoChunk`
now carries both, storage sized by the dimension plus a generation window built
with `GenerationShapeConfig::trim_height`, so surface rules, carver height
providers and feature placement keep resolving against y < 128 there.
The panic quoted in the issue (`chunk_density_function.rs`, len 99 index 129)
was the same conflation in `populate_noise` and is already fixed by 3c43f971.
This commit is contained in:
@@ -151,6 +151,51 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dimensions_taller_than_their_noise_settings_generate_all_sections() {
|
||||
for (dimension, terrain_state) in [
|
||||
(
|
||||
Dimension::THE_NETHER,
|
||||
pumpkin_data::Block::NETHERRACK.default_state.id,
|
||||
),
|
||||
(
|
||||
Dimension::THE_END,
|
||||
pumpkin_data::Block::END_STONE.default_state.id,
|
||||
),
|
||||
] {
|
||||
let seed = Seed(42);
|
||||
let block_registry = Arc::new(BlockRegistry);
|
||||
let world_gen =
|
||||
get_world_gen(seed, dimension.clone(), false, Vec::new(), String::new());
|
||||
let biome_mixer_seed = hash_seed(world_gen.seed());
|
||||
|
||||
let chunk = generate_single_chunk(
|
||||
&dimension,
|
||||
biome_mixer_seed,
|
||||
&world_gen,
|
||||
block_registry.as_ref(),
|
||||
0,
|
||||
0,
|
||||
StagedChunkEnum::Full,
|
||||
);
|
||||
let Chunk::Level(chunk) = chunk else {
|
||||
panic!("full generation must return a level chunk");
|
||||
};
|
||||
|
||||
assert_eq!(chunk.section.min_y, dimension.min_y);
|
||||
assert_eq!(chunk.section.count, dimension.height as usize / 16);
|
||||
assert_eq!(
|
||||
chunk.light_engine.lock().unwrap().sky_light.len(),
|
||||
chunk.section.count
|
||||
);
|
||||
|
||||
let dumped = chunk.section.dump_blocks();
|
||||
assert!(dumped.contains(&terrain_state));
|
||||
let top_section = &dumped[dumped.len() - 16 * 16 * 16..];
|
||||
assert!(top_section.iter().all(|&state| state == BlockStateId::AIR));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_chunk_should_return() {
|
||||
let dimension = Dimension::OVERWORLD;
|
||||
@@ -325,7 +370,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(non_air, 14);
|
||||
assert_eq!(non_air, 59);
|
||||
assert!(chunk.pending_block_entities.iter().any(|nbt| {
|
||||
nbt.get_string("id") == Some("minecraft:skull")
|
||||
&& nbt.get_int("x") == Some(-4888)
|
||||
|
||||
@@ -21,8 +21,8 @@ impl Carver for CanyonCarver {
|
||||
return;
|
||||
};
|
||||
|
||||
let min_y = run.chunk.bottom_y() as i32;
|
||||
let height = run.chunk.height();
|
||||
let min_y = run.chunk.generation_bottom_y() as i32;
|
||||
let height = run.chunk.generation_height();
|
||||
|
||||
let max_distance = (4 * 2 - 1) * 16;
|
||||
|
||||
@@ -74,7 +74,7 @@ impl CanyonCarver {
|
||||
) {
|
||||
let mut random = super::new_carver_random(tunnel_seed as u64, legacy_random_source);
|
||||
let width_factor_per_height =
|
||||
Self::init_width_factors(run.chunk.height() as usize, config, &mut random);
|
||||
Self::init_width_factors(run.chunk.generation_height() as usize, config, &mut random);
|
||||
let mut y_rota = 0.0f32;
|
||||
let mut x_rota = 0.0f32;
|
||||
|
||||
@@ -220,10 +220,13 @@ impl CanyonCarver {
|
||||
let x_index_min = ((x - horizontal_radius).floor() as i32 - chunk_min_x - 1).max(0);
|
||||
let x_index_max = ((x + horizontal_radius).floor() as i32 - chunk_min_x).min(15);
|
||||
|
||||
let min_y = ((y - vertical_radius).floor() as i32 - 1).max(run.chunk.bottom_y() as i32 + 1);
|
||||
let min_y = ((y - vertical_radius).floor() as i32 - 1)
|
||||
.max(run.chunk.generation_bottom_y() as i32 + 1);
|
||||
let protected_blocks_on_top = 7;
|
||||
let max_y = ((y + vertical_radius).floor() as i32 + 1).min(
|
||||
run.chunk.bottom_y() as i32 + run.chunk.height() as i32 - 1 - protected_blocks_on_top,
|
||||
run.chunk.generation_bottom_y() as i32 + run.chunk.generation_height() as i32
|
||||
- 1
|
||||
- protected_blocks_on_top,
|
||||
);
|
||||
|
||||
let z_index_min = ((z - horizontal_radius).floor() as i32 - chunk_min_z - 1).max(0);
|
||||
@@ -249,7 +252,7 @@ impl CanyonCarver {
|
||||
yd,
|
||||
zd,
|
||||
world_y,
|
||||
run.chunk.bottom_y() as i32,
|
||||
run.chunk.generation_bottom_y() as i32,
|
||||
) && !run.chunk.carving_mask.get(world_x, world_y, world_z)
|
||||
{
|
||||
run.chunk.carving_mask.set(world_x, world_y, world_z);
|
||||
|
||||
@@ -23,8 +23,8 @@ impl Carver for CaveCarver {
|
||||
CarverAdditionalConfig::Canyon(_) => return,
|
||||
};
|
||||
|
||||
let min_y = run.chunk.bottom_y() as i32;
|
||||
let height = run.chunk.height();
|
||||
let min_y = run.chunk.generation_bottom_y() as i32;
|
||||
let height = run.chunk.generation_height();
|
||||
|
||||
let max_distance = (4 * 2 - 1) << 4;
|
||||
|
||||
@@ -292,10 +292,13 @@ impl CaveCarver {
|
||||
let x_index_min = ((x - horizontal_radius).floor() as i32 - chunk_min_x - 1).max(0);
|
||||
let x_index_max = ((x + horizontal_radius).floor() as i32 - chunk_min_x).min(15);
|
||||
|
||||
let min_y = ((y - vertical_radius).floor() as i32 - 1).max(run.chunk.bottom_y() as i32 + 1);
|
||||
let min_y = ((y - vertical_radius).floor() as i32 - 1)
|
||||
.max(run.chunk.generation_bottom_y() as i32 + 1);
|
||||
let protected_blocks_on_top = 7;
|
||||
let max_y = ((y + vertical_radius).floor() as i32 + 1).min(
|
||||
run.chunk.bottom_y() as i32 + run.chunk.height() as i32 - 1 - protected_blocks_on_top,
|
||||
run.chunk.generation_bottom_y() as i32 + run.chunk.generation_height() as i32
|
||||
- 1
|
||||
- protected_blocks_on_top,
|
||||
);
|
||||
|
||||
let z_index_min = ((z - horizontal_radius).floor() as i32 - chunk_min_z - 1).max(0);
|
||||
@@ -499,9 +502,10 @@ mod tests {
|
||||
run: &mut Run,
|
||||
predicate: impl Fn(&'static pumpkin_data::BlockState, bool) -> bool,
|
||||
) -> Option<(i32, i32, i32, &'static pumpkin_data::BlockState)> {
|
||||
let lava_y = CAVE
|
||||
.lava_level
|
||||
.get_y(run.chunk.bottom_y() as i16, run.chunk.height());
|
||||
let lava_y = CAVE.lava_level.get_y(
|
||||
run.chunk.generation_bottom_y() as i16,
|
||||
run.chunk.generation_height(),
|
||||
);
|
||||
|
||||
for y in (lava_y + 1)..=63 {
|
||||
for x in 0..16 {
|
||||
|
||||
@@ -299,9 +299,10 @@ fn overworld_carve_state(
|
||||
y: i32,
|
||||
z: i32,
|
||||
) -> Option<(&'static BlockState, bool)> {
|
||||
let lava_y = config
|
||||
.lava_level
|
||||
.get_y(run.chunk.bottom_y() as i16, run.chunk.height());
|
||||
let lava_y = config.lava_level.get_y(
|
||||
run.chunk.generation_bottom_y() as i16,
|
||||
run.chunk.generation_height(),
|
||||
);
|
||||
|
||||
if y <= lava_y {
|
||||
return Some((run.ids.lava, false));
|
||||
|
||||
@@ -65,8 +65,8 @@ impl PlacedFeature {
|
||||
if let ConfiguredFeature::SculkPatch(feature) = feature {
|
||||
feature.generate_in_proto_chunk(chunk, random, pos)
|
||||
} else {
|
||||
let min_y = chunk.bottom_y();
|
||||
let height = chunk.height();
|
||||
let min_y = chunk.generation_bottom_y();
|
||||
let height = chunk.generation_height();
|
||||
self.generate(
|
||||
chunk,
|
||||
block_registry,
|
||||
|
||||
@@ -141,6 +141,8 @@ pub struct ProtoChunk {
|
||||
|
||||
height: u16,
|
||||
bottom_y: i8,
|
||||
generation_height: u16,
|
||||
generation_bottom_y: i8,
|
||||
pub stage: StagedChunkEnum,
|
||||
pub light: ChunkLight,
|
||||
pub carving_mask: crate::generation::carver::mask::CarvingMask,
|
||||
@@ -180,18 +182,22 @@ impl TerrainCache {
|
||||
impl ProtoChunk {
|
||||
#[must_use]
|
||||
pub fn new(x: i32, z: i32, generator: &super::generator::WorldGenerator) -> Self {
|
||||
let (height, bottom_y) = match generator {
|
||||
super::generator::WorldGenerator::Noise(noise_gen) => (
|
||||
noise_gen.settings.shape.height,
|
||||
noise_gen.settings.shape.min_y,
|
||||
),
|
||||
super::generator::WorldGenerator::Flat(flat_gen) => (
|
||||
flat_gen.dimension.logical_height as u16,
|
||||
flat_gen.dimension.min_y as i8,
|
||||
),
|
||||
};
|
||||
let dimension = generator.dimension();
|
||||
let height = dimension.height as u16;
|
||||
let bottom_y = dimension.min_y as i8;
|
||||
let section_count = (height as usize) / 16;
|
||||
|
||||
let (generation_height, generation_bottom_y) = match generator {
|
||||
super::generator::WorldGenerator::Noise(noise_gen) => {
|
||||
let shape = noise_gen
|
||||
.settings
|
||||
.shape
|
||||
.trim_height(bottom_y, (dimension.min_y + dimension.height) as u16);
|
||||
(shape.height, shape.min_y)
|
||||
}
|
||||
super::generator::WorldGenerator::Flat(_) => (height, bottom_y),
|
||||
};
|
||||
|
||||
let default_block = match generator {
|
||||
super::generator::WorldGenerator::Noise(noise_gen) => noise_gen.default_block,
|
||||
super::generator::WorldGenerator::Flat(_) => Block::AIR.default_state,
|
||||
@@ -225,6 +231,8 @@ impl ProtoChunk {
|
||||
structure_starts: FxHashMap::default(),
|
||||
height,
|
||||
bottom_y,
|
||||
generation_height,
|
||||
generation_bottom_y,
|
||||
stage: StagedChunkEnum::Empty,
|
||||
light: ChunkLight {
|
||||
sky_light: (0..section_count)
|
||||
@@ -347,6 +355,16 @@ impl ProtoChunk {
|
||||
self.bottom_y
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn generation_height(&self) -> u16 {
|
||||
self.generation_height
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn generation_bottom_y(&self) -> i8 {
|
||||
self.generation_bottom_y
|
||||
}
|
||||
|
||||
pub fn add_block_entity(&mut self, nbt: NbtCompound) {
|
||||
self.pending_block_entities.push(nbt);
|
||||
}
|
||||
@@ -934,8 +952,8 @@ impl ProtoChunk {
|
||||
|
||||
let random = &random_config.base_random_deriver;
|
||||
let mut context = MaterialRuleContext::new(
|
||||
min_y,
|
||||
self.height(),
|
||||
self.generation_bottom_y(),
|
||||
self.generation_height(),
|
||||
random,
|
||||
&terrain_cache.terrain_builder,
|
||||
&terrain_cache.surface_noise,
|
||||
@@ -1053,7 +1071,7 @@ impl ProtoChunk {
|
||||
block_registry: &dyn WorldPortalExt,
|
||||
random_config: &GlobalRandomConfig,
|
||||
) {
|
||||
let (center_x, center_z, min_y, height, biomes_in_chunk) = {
|
||||
let (center_x, center_z, min_y, generation_min_y, generation_height, biomes_in_chunk) = {
|
||||
let chunk = cache.get_center_chunk();
|
||||
let mut unique_biomes = Vec::with_capacity(4);
|
||||
for &biome_id in &chunk.flat_biome_map {
|
||||
@@ -1065,7 +1083,8 @@ impl ProtoChunk {
|
||||
chunk.x,
|
||||
chunk.z,
|
||||
chunk.bottom_y() as i32,
|
||||
chunk.height() as i32,
|
||||
chunk.generation_bottom_y(),
|
||||
chunk.generation_height(),
|
||||
unique_biomes,
|
||||
)
|
||||
};
|
||||
@@ -1109,8 +1128,8 @@ impl ProtoChunk {
|
||||
feature.generate(
|
||||
cache,
|
||||
block_registry,
|
||||
min_y as i8,
|
||||
height as u16,
|
||||
generation_min_y,
|
||||
generation_height,
|
||||
feature_enum,
|
||||
&mut random,
|
||||
origin_pos,
|
||||
|
||||
@@ -112,44 +112,72 @@ mod test {
|
||||
chunk.stage = StagedChunkEnum::StructureReferences;
|
||||
chunk.step_to_noise(generator);
|
||||
|
||||
assert_eq!(chunk.flat_block_map.len(), expected_data.len());
|
||||
let min_y = chunk.bottom_y() as i32;
|
||||
let height = chunk.height() as usize;
|
||||
let mut mismatches = 0;
|
||||
for (i, (&actual, &expected)) in chunk
|
||||
.flat_block_map
|
||||
.iter()
|
||||
.zip(expected_data.iter())
|
||||
.enumerate()
|
||||
{
|
||||
if actual.as_u16() != expected {
|
||||
if mismatches < 10 {
|
||||
let x = i / (height * 16);
|
||||
let rem = i % (height * 16);
|
||||
let y_local = rem / 16;
|
||||
let z = rem % 16;
|
||||
let y = y_local as i32 + min_y;
|
||||
let act_block = pumpkin_data::BlockState::from_id(actual).id.to_block().name;
|
||||
let exp_block = pumpkin_data::BlockState::from_id(
|
||||
pumpkin_data::BlockStateId::new(expected).unwrap(),
|
||||
)
|
||||
.id
|
||||
.to_block()
|
||||
.name;
|
||||
println!(
|
||||
"[{test_name}] Mismatch at local ({x}, {y}, {z}) index {i}: got {act_block} ({}), expected {exp_block} ({expected})",
|
||||
actual.as_u16()
|
||||
);
|
||||
}
|
||||
mismatches += 1;
|
||||
}
|
||||
}
|
||||
let mismatches = count_dump_mismatches(&chunk, expected_data, test_name);
|
||||
assert_air_above_dumped_window(&chunk, expected_data, test_name);
|
||||
assert_eq!(
|
||||
mismatches, 0,
|
||||
"[{test_name}] Chunk noise generation mismatches vanilla!"
|
||||
);
|
||||
}
|
||||
|
||||
fn dumped_window_height(expected_data: &[u16]) -> usize {
|
||||
let columns = 16 * 16;
|
||||
assert_eq!(expected_data.len() % columns, 0);
|
||||
expected_data.len() / columns
|
||||
}
|
||||
|
||||
fn count_dump_mismatches(chunk: &ProtoChunk, expected_data: &[u16], test_name: &str) -> usize {
|
||||
let dumped_height = dumped_window_height(expected_data);
|
||||
assert!(dumped_height <= chunk.height() as usize);
|
||||
|
||||
let min_y = chunk.bottom_y() as i32;
|
||||
let mut mismatches = 0;
|
||||
for x in 0..16usize {
|
||||
for local_y in 0..dumped_height {
|
||||
for z in 0..16usize {
|
||||
let expected = expected_data[(x * dumped_height + local_y) * 16 + z];
|
||||
let actual = chunk.get_block_state_raw(x as i32, local_y as i32, z as i32);
|
||||
if actual.as_u16() == expected {
|
||||
continue;
|
||||
}
|
||||
if mismatches < 10 {
|
||||
let y = local_y as i32 + min_y;
|
||||
let act_block =
|
||||
pumpkin_data::BlockState::from_id(actual).id.to_block().name;
|
||||
let exp_block = pumpkin_data::BlockState::from_id(
|
||||
pumpkin_data::BlockStateId::new(expected).unwrap(),
|
||||
)
|
||||
.id
|
||||
.to_block()
|
||||
.name;
|
||||
println!(
|
||||
"[{test_name}] Mismatch at local ({x}, {y}, {z}): got {act_block} ({}), expected {exp_block} ({expected})",
|
||||
actual.as_u16()
|
||||
);
|
||||
}
|
||||
mismatches += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
mismatches
|
||||
}
|
||||
|
||||
fn assert_air_above_dumped_window(chunk: &ProtoChunk, expected_data: &[u16], test_name: &str) {
|
||||
let min_y = chunk.bottom_y() as i32;
|
||||
for x in 0..16usize {
|
||||
for local_y in dumped_window_height(expected_data)..chunk.height() as usize {
|
||||
for z in 0..16usize {
|
||||
let actual = chunk.get_block_state_raw(x as i32, local_y as i32, z as i32);
|
||||
assert!(
|
||||
pumpkin_data::BlockState::from_id(actual).is_air(),
|
||||
"[{test_name}] Block above the noise window at local ({x}, {}, {z}) is not air",
|
||||
local_y as i32 + min_y
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_chunk_surface(
|
||||
seed: u64,
|
||||
dimension: Dimension,
|
||||
@@ -170,38 +198,8 @@ mod test {
|
||||
chunk.step_to_noise(generator);
|
||||
chunk.step_to_surface(generator);
|
||||
|
||||
assert_eq!(chunk.flat_block_map.len(), expected_data.len());
|
||||
let min_y = chunk.bottom_y() as i32;
|
||||
let height = chunk.height() as usize;
|
||||
let mut mismatches = 0;
|
||||
for (i, (&actual, &expected)) in chunk
|
||||
.flat_block_map
|
||||
.iter()
|
||||
.zip(expected_data.iter())
|
||||
.enumerate()
|
||||
{
|
||||
if actual.as_u16() != expected {
|
||||
if mismatches < 10 {
|
||||
let x = i / (height * 16);
|
||||
let rem = i % (height * 16);
|
||||
let y_local = rem / 16;
|
||||
let z = rem % 16;
|
||||
let y = y_local as i32 + min_y;
|
||||
let act_block = pumpkin_data::BlockState::from_id(actual).id.to_block().name;
|
||||
let exp_block = pumpkin_data::BlockState::from_id(
|
||||
pumpkin_data::BlockStateId::new(expected).unwrap(),
|
||||
)
|
||||
.id
|
||||
.to_block()
|
||||
.name;
|
||||
println!(
|
||||
"[{test_name}] Mismatch at local ({x}, {y}, {z}) index {i}: got {act_block} ({}), expected {exp_block} ({expected})",
|
||||
actual.as_u16()
|
||||
);
|
||||
}
|
||||
mismatches += 1;
|
||||
}
|
||||
}
|
||||
let mismatches = count_dump_mismatches(&chunk, expected_data, test_name);
|
||||
assert_air_above_dumped_window(&chunk, expected_data, test_name);
|
||||
let allowed_mismatches = 1060;
|
||||
assert!(
|
||||
mismatches <= allowed_mismatches,
|
||||
|
||||
@@ -568,10 +568,10 @@ impl CommandExecutor for PlaceFeatureExecutor {
|
||||
let cx = block_pos.0.x >> 4;
|
||||
let cz = block_pos.0.z >> 4;
|
||||
let mut chunk = ProtoChunk::new(cx, cz, &world_gen);
|
||||
let bottom_y = chunk.bottom_y();
|
||||
let height = chunk.height();
|
||||
let chunk_min_y = bottom_y as i32;
|
||||
let chunk_height = height as i32;
|
||||
let generation_bottom_y = chunk.generation_bottom_y();
|
||||
let generation_height = chunk.generation_height();
|
||||
let chunk_min_y = chunk.bottom_y() as i32;
|
||||
let chunk_height = chunk.height() as i32;
|
||||
let surface_y = ground_y(block_pos.0.y, chunk_min_y, chunk_height);
|
||||
|
||||
let ground = surface_y as i16;
|
||||
@@ -601,8 +601,8 @@ impl CommandExecutor for PlaceFeatureExecutor {
|
||||
configured.generate(
|
||||
&mut chunk,
|
||||
®,
|
||||
bottom_y,
|
||||
height,
|
||||
generation_bottom_y,
|
||||
generation_height,
|
||||
key,
|
||||
&mut random,
|
||||
block_pos,
|
||||
|
||||
Reference in New Issue
Block a user