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:
MCbabel
2026-08-07 10:10:00 +02:00
committed by GitHub
parent b0d2dfa434
commit 69e3ff7c7c
8 changed files with 175 additions and 105 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
&reg,
bottom_y,
height,
generation_bottom_y,
generation_height,
key,
&mut random,
block_pos,