From fd56a5be7a7053d8fbc965287734cf447d0668e5 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Wed, 6 May 2026 18:59:36 +0200 Subject: [PATCH] feat(chunk-gen) add scattered ore, twisting vines, blue ice --- pumpkin-codegen/src/configured_feature.rs | 15 +- .../configured_features_generated.rs | 34 ++++- .../generation/feature/configured_features.rs | 13 ++ .../generation/feature/features/blue_ice.rs | 96 ++++++++++++- .../src/generation/feature/features/ore.rs | 19 ++- .../feature/features/scattered_ore.rs | 68 ++++++++- .../feature/features/twisting_vines.rs | 132 +++++++++++++++++- 7 files changed, 361 insertions(+), 16 deletions(-) diff --git a/pumpkin-codegen/src/configured_feature.rs b/pumpkin-codegen/src/configured_feature.rs index 34d0429c2..4a5a81cc7 100644 --- a/pumpkin-codegen/src/configured_feature.rs +++ b/pumpkin-codegen/src/configured_feature.rs @@ -226,7 +226,9 @@ pub fn value_to_configured_feature(v: &Value) -> TokenStream { if type_str == "minecraft:scattered_ore" { quote! { ConfiguredFeature::ScatteredOre(crate::generation::feature::features::scattered_ore::ScatteredOreFeature { - // TODO + size: #size, + discard_chance_on_air_exposure: #discard, + targets: vec![#(#targets),*], }) } } else { @@ -701,7 +703,16 @@ pub fn value_to_configured_feature(v: &Value) -> TokenStream { quote! { ConfiguredFeature::WeepingVines(crate::generation::feature::features::weeping_vines::WeepingVinesFeature {}) } } "minecraft:twisting_vines" => { - quote! { ConfiguredFeature::TwistingVines(crate::generation::feature::features::twisting_vines::TwistingVinesFeature {}) } + let spread_width = config["spread_width"].as_i64().unwrap_or(0) as i32; + let spread_height = config["spread_height"].as_i64().unwrap_or(0) as i32; + let max_height = config["max_height"].as_i64().unwrap_or(0) as i32; + quote! { + ConfiguredFeature::TwistingVines(crate::generation::feature::features::twisting_vines::TwistingVinesFeature { + spread_width: #spread_width, + spread_height: #spread_height, + max_height: #max_height, + }) + } } "minecraft:delta_feature" => { quote! { ConfiguredFeature::DeltaFeature(crate::generation::feature::features::delta_feature::DeltaFeatureFeature {}) } diff --git a/pumpkin-data/src/generated/configured_features_generated.rs b/pumpkin-data/src/generated/configured_features_generated.rs index 7484e2d44..4f8a38d96 100644 --- a/pumpkin-data/src/generated/configured_features_generated.rs +++ b/pumpkin-data/src/generated/configured_features_generated.rs @@ -2738,13 +2738,31 @@ fn build_configured_features() -> std::collections::HashMap std::collections::HashMap { feature.generate(chunk, min_y, height, feature_name, random, pos) } + Self::TwistingVines(feature) => { + feature.generate(chunk, min_y, height, feature_name, random, pos) + } Self::UnderwaterMagma(feature) => { feature.generate(chunk, min_y, height, feature_name, random, pos) } @@ -344,9 +347,19 @@ impl ConfiguredFeature { random, pos, ), + Self::ScatteredOre(feature) => feature.generate( + chunk, + block_registry, + min_y, + height, + feature_name, + random, + pos, + ), Self::MonsterRoom(feature) => { feature.generate(chunk, min_y, height, feature_name, random, pos) } + Self::BlueIce(feature) => feature.generate(chunk, random, pos), Self::GlowstoneBlob(feature) => { feature.generate(chunk, min_y, height, feature_name, random, pos) } diff --git a/pumpkin-world/src/generation/feature/features/blue_ice.rs b/pumpkin-world/src/generation/feature/features/blue_ice.rs index 8a797f5ac..a9b50e9dc 100644 --- a/pumpkin-world/src/generation/feature/features/blue_ice.rs +++ b/pumpkin-world/src/generation/feature/features/blue_ice.rs @@ -1,3 +1,95 @@ -pub struct BlueIceFeature { - // TODO +use pumpkin_data::Block; +use pumpkin_util::{ + math::position::BlockPos, + random::{RandomGenerator, RandomImpl}, +}; + +use crate::generation::proto_chunk::GenerationCache; + +const SEA_LEVEL: i32 = 63; // TODO: use getSeaLevel() instead of hardcoding + +pub struct BlueIceFeature {} + +impl BlueIceFeature { + pub fn generate( + &self, + chunk: &mut T, + random: &mut RandomGenerator, + pos: BlockPos, + ) -> bool { + if pos.0.y >= SEA_LEVEL - 1 { + return false; + } + + let block = GenerationCache::get_block_state(chunk, &pos.0).to_state(); + let block_below = GenerationCache::get_block_state(chunk, &pos.down().0).to_state(); + + if block != Block::WATER.default_state && block_below != Block::WATER.default_state { + return false; + } + + let mut has_ice_neighbor = false; + for neighbor_pos in [ + pos.up(), + pos.down(), + pos.north(), + pos.south(), + pos.east(), + pos.west(), + ] { + if GenerationCache::get_block_state(chunk, &neighbor_pos.0).to_state() + == Block::ICE.default_state + { + has_ice_neighbor = true; + break; + } + } + + if !has_ice_neighbor { + return false; + } + + chunk.set_block_state(&pos.0, Block::BLUE_ICE.default_state); + + for _ in 0..200 { + let offset_x = random.next_bounded_i32(8) - random.next_bounded_i32(8); + let offset_y = random.next_bounded_i32(8) - random.next_bounded_i32(8); + let offset_z = random.next_bounded_i32(8) - random.next_bounded_i32(8); + let target_pos = pos.add(offset_x, offset_y, offset_z); + + if chunk.out_of_height(target_pos.0.y as i16) { + continue; + } + + let target_state = GenerationCache::get_block_state(chunk, &target_pos.0).to_state(); + if target_state.is_air() + || target_state == Block::WATER.default_state + || target_state == Block::ICE.default_state + || target_state == Block::PACKED_ICE.default_state + { + let mut has_blue_ice_neighbor = false; + for neighbor_pos in [ + target_pos.up(), + target_pos.down(), + target_pos.north(), + target_pos.south(), + target_pos.east(), + target_pos.west(), + ] { + if GenerationCache::get_block_state(chunk, &neighbor_pos.0).to_state() + == Block::BLUE_ICE.default_state + { + has_blue_ice_neighbor = true; + break; + } + } + + if has_blue_ice_neighbor { + chunk.set_block_state(&target_pos.0, Block::BLUE_ICE.default_state); + } + } + } + + true + } } diff --git a/pumpkin-world/src/generation/feature/features/ore.rs b/pumpkin-world/src/generation/feature/features/ore.rs index e8ec2ed81..4e3f9494f 100644 --- a/pumpkin-world/src/generation/feature/features/ore.rs +++ b/pumpkin-world/src/generation/feature/features/ore.rs @@ -184,7 +184,14 @@ impl OreFeature { let block_state = GenerationCache::get_block_state(chunk, &pos_vec); for target in &self.targets { - if self.should_place(chunk, block_state, random, target, &mutable_pos) { + if Self::should_place( + self.discard_chance_on_air_exposure, + chunk, + block_state, + random, + target, + &mutable_pos, + ) { chunk.set_block_state(&pos_vec, target.state); placed_blocks_count += 1; break; // Equivalent to 'continue block11;' @@ -197,8 +204,8 @@ impl OreFeature { placed_blocks_count > 0 } - fn should_place( - &self, + pub fn should_place( + discard_chance: f32, chunk: &T, state: RawBlockState, random: &mut RandomGenerator, @@ -208,13 +215,13 @@ impl OreFeature { if !target.target.test(state, random) { return false; } - if Self::should_not_discard(random, self.discard_chance_on_air_exposure) { + if Self::should_not_discard(random, discard_chance) { return true; } !Self::is_exposed_to_air(chunk, pos) } - fn should_not_discard(random: &mut RandomGenerator, chance: f32) -> bool { + pub fn should_not_discard(random: &mut RandomGenerator, chance: f32) -> bool { if chance <= 0.0f32 { return true; } @@ -224,7 +231,7 @@ impl OreFeature { random.next_f32() >= chance } - fn is_exposed_to_air(chunk: &T, pos: &BlockPos) -> bool { + pub fn is_exposed_to_air(chunk: &T, pos: &BlockPos) -> bool { for dir in BlockDirection::all() { if GenerationCache::get_block_state(chunk, &pos.offset(dir.to_offset()).0) .to_state() diff --git a/pumpkin-world/src/generation/feature/features/scattered_ore.rs b/pumpkin-world/src/generation/feature/features/scattered_ore.rs index 5d3f9b7d2..f0f7f0d44 100644 --- a/pumpkin-world/src/generation/feature/features/scattered_ore.rs +++ b/pumpkin-world/src/generation/feature/features/scattered_ore.rs @@ -1,3 +1,69 @@ +use pumpkin_util::{ + math::position::BlockPos, + random::{RandomGenerator, RandomImpl}, +}; + +use crate::generation::proto_chunk::GenerationCache; +use crate::world::BlockRegistryExt; + +use super::ore::{OreFeature, OreTarget}; + pub struct ScatteredOreFeature { - // TODO + pub size: i32, + pub discard_chance_on_air_exposure: f32, + pub targets: Vec, +} + +impl ScatteredOreFeature { + #[expect(clippy::too_many_arguments)] + pub fn generate( + &self, + chunk: &mut T, + _block_registry: &dyn BlockRegistryExt, + _min_y: i8, + _height: u16, + _feature: &str, + random: &mut RandomGenerator, + pos: BlockPos, + ) -> bool { + let count = random.next_bounded_i32(self.size + 1); + let mut placed = false; + + for _ in 0..count { + let offset_x = self.get_random_offset(random); + let offset_y = self.get_random_offset(random); + let offset_z = self.get_random_offset(random); + + let target_pos = pos.add(offset_x, offset_y, offset_z); + + if chunk.out_of_height(target_pos.0.y as i16) { + continue; + } + + let block_state = GenerationCache::get_block_state(chunk, &target_pos.0); + + for target in &self.targets { + if OreFeature::should_place( + self.discard_chance_on_air_exposure, + chunk, + block_state, + random, + target, + &target_pos, + ) { + chunk.set_block_state(&target_pos.0, target.state); + placed = true; + break; + } + } + } + + placed + } + + fn get_random_offset(&self, random: &mut RandomGenerator) -> i32 { + let f1 = random.next_f32(); + let f2 = random.next_f32(); + ((f1 - f2) * self.size as f32).round() as i32 + } } diff --git a/pumpkin-world/src/generation/feature/features/twisting_vines.rs b/pumpkin-world/src/generation/feature/features/twisting_vines.rs index 4e3a7563e..0d7ba1c6b 100644 --- a/pumpkin-world/src/generation/feature/features/twisting_vines.rs +++ b/pumpkin-world/src/generation/feature/features/twisting_vines.rs @@ -1,3 +1,133 @@ +use pumpkin_data::Block; +use pumpkin_util::{ + math::position::BlockPos, + random::{RandomGenerator, RandomImpl}, +}; + +use crate::generation::proto_chunk::GenerationCache; + pub struct TwistingVinesFeature { - // TODO + pub spread_width: i32, + pub spread_height: i32, + pub max_height: i32, +} + +impl TwistingVinesFeature { + pub fn generate( + &self, + chunk: &mut T, + _min_y: i8, + _height: u16, + _feature_name: &str, + random: &mut RandomGenerator, + pos: BlockPos, + ) -> bool { + if self.is_invalid_location(chunk, &pos) { + return false; + } + + let mut placed = false; + + for _ in 0..self.spread_width * self.spread_width { + let offset_x = random.next_bounded_i32(self.spread_width) + - random.next_bounded_i32(self.spread_width); + let offset_y = random.next_bounded_i32(self.spread_height) + - random.next_bounded_i32(self.spread_height); + let offset_z = random.next_bounded_i32(self.spread_width) + - random.next_bounded_i32(self.spread_width); + + let mut mutable_pos = pos.add(offset_x, offset_y, offset_z); + + if self.find_target_y(chunk, &mut mutable_pos) + && !self.is_invalid_location(chunk, &mutable_pos) + { + let mut height = random.next_bounded_i32(self.max_height) + 1; + if random.next_bounded_i32(6) == 0 { + height *= 2; + } + if random.next_bounded_i32(10) == 0 { + height = 1; + } + + self.generate_column(chunk, random, &mutable_pos, height); + placed = true; + } + } + + placed + } + + fn generate_column( + &self, + chunk: &mut T, + random: &mut RandomGenerator, + pos: &BlockPos, + height: i32, + ) { + let mut current_pos = *pos; + for i in 0..height { + if !GenerationCache::get_block_state(chunk, ¤t_pos.0) + .to_state() + .is_air() + { + break; + } + + if i == height - 1 + || !GenerationCache::get_block_state(chunk, ¤t_pos.up().0) + .to_state() + .is_air() + { + // Top part + let _age = 17 + random.next_bounded_i32(25 - 17 + 1); + // We should set the age property here, but Pumpkin's BlockState might not support it easily yet or we just use default + // For now, let's just use the block. + // TODO: Set age property + chunk.set_block_state(¤t_pos.0, Block::TWISTING_VINES.default_state); + break; + } else { + chunk.set_block_state(¤t_pos.0, Block::TWISTING_VINES_PLANT.default_state); + } + current_pos = current_pos.up(); + } + } + + fn is_invalid_location(&self, chunk: &T, pos: &BlockPos) -> bool { + if !GenerationCache::get_block_state(chunk, &pos.0) + .to_state() + .is_air() + { + return true; + } + + let block_below = GenerationCache::get_block_state(chunk, &pos.down().0).to_state(); + block_below != Block::WARPED_NYLIUM.default_state + && block_below != Block::WARPED_WART_BLOCK.default_state + && block_below != Block::TWISTING_VINES.default_state + && block_below != Block::TWISTING_VINES_PLANT.default_state + } + + fn find_target_y(&self, chunk: &T, pos: &mut BlockPos) -> bool { + // Try to find a valid floor by looking down + let mut current = *pos; + for _ in 0..self.spread_height { + if GenerationCache::get_block_state(chunk, ¤t.0) + .to_state() + .is_air() + { + let below = current.down(); + let block_below = GenerationCache::get_block_state(chunk, &below.0).to_state(); + if block_below == Block::WARPED_NYLIUM.default_state + || block_below == Block::WARPED_WART_BLOCK.default_state + || block_below == Block::TWISTING_VINES.default_state + || block_below == Block::TWISTING_VINES_PLANT.default_state + { + *pos = current; + return true; + } + } + current = current.down(); + } + false + } }