From 655e69fc66bdf030ca60b740883b6bee7a051137 Mon Sep 17 00:00:00 2001 From: RB007 <36972202+RoosterBooster007@users.noreply.github.com> Date: Fri, 20 Mar 2026 04:43:28 -0400 Subject: [PATCH] feat: vegetation patch features (#1708) * feat: vegetation features * refactor: surface_direction helpers * fix: several issues * refactor: add with_waterlogged helper to Block/BlockState * refactor: small changes and added helpful comments --- pumpkin-codegen/src/configured_feature.rs | 57 ++++- pumpkin-codegen/src/placed_feature.rs | 14 ++ pumpkin-data/src/block_state.rs | 9 + pumpkin-data/src/blocks.rs | 33 +++ .../configured_features_generated.rs | 215 +++++++++++++++-- .../generation/feature/configured_features.rs | 20 ++ .../feature/features/vegetation_patch.rs | 222 +++++++++++++++++- .../features/waterlogged_vegetation_patch.rs | 153 +++++++++++- 8 files changed, 696 insertions(+), 27 deletions(-) diff --git a/pumpkin-codegen/src/configured_feature.rs b/pumpkin-codegen/src/configured_feature.rs index 4233ef3da..c245bbc50 100644 --- a/pumpkin-codegen/src/configured_feature.rs +++ b/pumpkin-codegen/src/configured_feature.rs @@ -92,6 +92,8 @@ pub fn build() -> TokenStream { spring_feature::{BlockWrapper, SpringFeatureFeature}, geode::GeodeFeature, tree::TreeFeature, + vegetation_patch::VegetationPatchFeature, + waterlogged_vegetation_patch::WaterloggedVegetationPatchFeature, tree::trunk::{TrunkPlacer, TrunkType, bending::BendingTrunkPlacer, cherry::CherryTrunkPlacer, @@ -487,6 +489,55 @@ pub fn value_to_configured_feature(v: &Value) -> TokenStream { ) } } + "minecraft:vegetation_patch" | "minecraft:waterlogged_vegetation_patch" => { + let replaceable = value_to_block_predicate(&config["replaceable"]); + let ground_state = value_to_block_state_provider(&config["ground_state"]); + let vegetation_feature = value_to_inline_placed_feature(&config["vegetation_feature"]); + let surface = match config["surface"].as_str().unwrap_or("floor") { + "ceiling" => quote! { pumpkin_util::math::vertical_surface_type::VerticalSurfaceType::Ceiling }, + _ => quote! { pumpkin_util::math::vertical_surface_type::VerticalSurfaceType::Floor }, + }; + let depth = value_to_int_provider(&config["depth"]); + let extra_bottom = config["extra_bottom_block_chance"].as_f64().unwrap_or(0.0) as f32; + let vert_range = config["vertical_range"].as_i64().unwrap_or(0) as i32; + let veg_chance = config["vegetation_chance"].as_f64().unwrap_or(0.0) as f32; + let xz = value_to_int_provider(&config["xz_radius"]); + let extra_edge = config["extra_edge_column_chance"].as_f64().unwrap_or(0.0) as f32; + + if type_str == "minecraft:vegetation_patch" { + quote! { + ConfiguredFeature::VegetationPatch(vegetation_patch::VegetationPatchFeature { + replaceable: #replaceable, + ground_state: #ground_state, + vegetation_feature: Box::new(#vegetation_feature), + surface: #surface, + depth: #depth, + extra_bottom_block_chance: #extra_bottom, + vertical_range: #vert_range, + vegetation_chance: #veg_chance, + xz_radius: #xz, + extra_edge_column_chance: #extra_edge, + }) + } + } else { + quote! { + ConfiguredFeature::WaterloggedVegetationPatch(waterlogged_vegetation_patch::WaterloggedVegetationPatchFeature { + base: vegetation_patch::VegetationPatchFeature { + replaceable: #replaceable, + ground_state: #ground_state, + vegetation_feature: Box::new(#vegetation_feature), + surface: #surface, + depth: #depth, + extra_bottom_block_chance: #extra_bottom, + vertical_range: #vert_range, + vegetation_chance: #veg_chance, + xz_radius: #xz, + extra_edge_column_chance: #extra_edge, + } + }) + } + } + } "minecraft:glowstone_blob" => { quote! { ConfiguredFeature::GlowstoneBlob(crate::generation::feature::features::glowstone_blob::GlowstoneBlobFeature {}) } } @@ -516,12 +567,6 @@ pub fn value_to_configured_feature(v: &Value) -> TokenStream { "minecraft:vines" => { quote! { ConfiguredFeature::Vines(crate::generation::feature::features::vines::VinesFeature) } } - "minecraft:vegetation_patch" => { - quote! { ConfiguredFeature::VegetationPatch(crate::generation::feature::features::vegetation_patch::VegetationPatchFeature {}) } - } - "minecraft:waterlogged_vegetation_patch" => { - quote! { ConfiguredFeature::WaterloggedVegetationPatch(crate::generation::feature::features::waterlogged_vegetation_patch::WaterloggedVegetationPatchFeature {}) } - } "minecraft:root_system" => { quote! { ConfiguredFeature::RootSystem(crate::generation::feature::features::root_system::RootSystemFeature {}) } } diff --git a/pumpkin-codegen/src/placed_feature.rs b/pumpkin-codegen/src/placed_feature.rs index 3a6e1c3bf..0b162f377 100644 --- a/pumpkin-codegen/src/placed_feature.rs +++ b/pumpkin-codegen/src/placed_feature.rs @@ -253,6 +253,20 @@ fn value_to_placement_modifier(v: &Value) -> TokenStream { /// # Returns /// A `BlockPredicate` variant token stream, or `compile_error!` for unknown types. pub fn value_to_block_predicate(v: &Value) -> TokenStream { + // Handle bare string values: "#minecraft:some_tag" is a block-tag predicate, + // "true" (or any non-# string) is AlwaysTrue. + if let Some(s) = v.as_str() { + if let Some(tag) = s.strip_prefix('#') { + return quote! { + BlockPredicate::MatchingBlockTag(MatchingBlockTagPredicate { + offset: OffsetBlocksBlockPredicate { offset: None }, + tag: #tag.to_string(), + }) + }; + } + return quote! { BlockPredicate::AlwaysTrue }; + } + let type_str = v["type"].as_str().unwrap_or(""); match type_str { "minecraft:true" | "" => quote! { BlockPredicate::AlwaysTrue }, diff --git a/pumpkin-data/src/block_state.rs b/pumpkin-data/src/block_state.rs index 52d4050b2..527fbd414 100644 --- a/pumpkin-data/src/block_state.rs +++ b/pumpkin-data/src/block_state.rs @@ -143,6 +143,15 @@ impl BlockState { }) } + /// Produce a new state identical to `self` except the waterlogged property + /// is set to `true`. If the block type does not support waterlogging or + /// the state was already waterlogged, `None` is returned. + #[must_use] + pub fn with_waterlogged(&self) -> Option<&'static BlockState> { + let block = Block::from_state_id(self.id); + block.with_waterlogged(self.id) + } + pub fn get_block_collision_shapes(&self) -> impl Iterator + '_ { self.collision_shapes .iter() diff --git a/pumpkin-data/src/blocks.rs b/pumpkin-data/src/blocks.rs index 3629aa058..517789d8c 100644 --- a/pumpkin-data/src/blocks.rs +++ b/pumpkin-data/src/blocks.rs @@ -116,6 +116,39 @@ impl Block { }) } + /// Returns a new [`BlockState`] reference for the given `state_id` with the + /// `waterlogged` property forced to `true` if the block supports that + /// property. If the state is already waterlogged or the block does not + /// expose a `waterlogged` property then `None` is returned. + #[must_use] + pub fn with_waterlogged(&self, state_id: u16) -> Option<&'static BlockState> { + // Check if already waterlogged + if self.is_waterlogged(state_id) { + return Some(BlockState::from_id(state_id)); + } + + // Modify the property list if available + if let Some(props_source) = self.properties(state_id) { + let mut props: Vec<(&str, &str)> = props_source + .to_props() + .iter() + .map(|(k, v)| (*k, *v)) + .collect(); + + // Look for an existing waterlogged key or add one + if let Some(idx) = props.iter().position(|(k, _)| *k == "waterlogged") { + props[idx] = ("waterlogged", "true"); + } else { + props.push(("waterlogged", "true")); + } + + let new_state_id = self.from_properties(&props).to_state_id(self); + return Some(BlockState::from_id(new_state_id)); + } + + None + } + /// Returns whether this block is solid (based on default state) #[must_use] pub const fn is_solid(&self) -> bool { diff --git a/pumpkin-data/src/generated/configured_features_generated.rs b/pumpkin-data/src/generated/configured_features_generated.rs index 00b291863..60d44fc55 100644 --- a/pumpkin-data/src/generated/configured_features_generated.rs +++ b/pumpkin-data/src/generated/configured_features_generated.rs @@ -54,6 +54,8 @@ fn build_configured_features() -> std::collections::HashMap std::collections::HashMap std::collections::HashMap std::collections::HashMap feature.generate( + chunk, + block_registry, + min_y, + height, + feature_name, + random, + pos, + ), + Self::WaterloggedVegetationPatch(feature) => feature.generate( + chunk, + block_registry, + min_y, + height, + feature_name, + random, + pos, + ), Self::PointedDripstone(feature) => feature.generate(chunk, random, pos), Self::CoralMushroom(feature) => { feature.generate(chunk, min_y, height, feature_name, random, pos) diff --git a/pumpkin-world/src/generation/feature/features/vegetation_patch.rs b/pumpkin-world/src/generation/feature/features/vegetation_patch.rs index a7c9e3738..fe2f99612 100644 --- a/pumpkin-world/src/generation/feature/features/vegetation_patch.rs +++ b/pumpkin-world/src/generation/feature/features/vegetation_patch.rs @@ -1,3 +1,223 @@ +use std::collections::HashSet; + +use pumpkin_util::{ + math::{position::BlockPos, vector3::Vector3, vertical_surface_type::VerticalSurfaceType}, + random::{RandomGenerator, RandomImpl}, +}; + +use crate::generation::block_predicate::BlockPredicate; +use crate::generation::block_state_provider::BlockStateProvider; +use crate::generation::proto_chunk::GenerationCache; +use crate::world::BlockRegistryExt; + pub struct VegetationPatchFeature { - // TODO + pub replaceable: BlockPredicate, + pub ground_state: BlockStateProvider, + pub vegetation_feature: Box, + pub surface: VerticalSurfaceType, + pub depth: pumpkin_util::math::int_provider::IntProvider, + pub extra_bottom_block_chance: f32, + pub vertical_range: i32, + pub vegetation_chance: f32, + pub xz_radius: pumpkin_util::math::int_provider::IntProvider, + pub extra_edge_column_chance: f32, +} + +impl VegetationPatchFeature { + /// Returns the block direction that points "into" the surface (down for floor, up for ceiling). + pub(crate) fn surface_direction(&self) -> pumpkin_data::BlockDirection { + match self.surface { + VerticalSurfaceType::Floor => pumpkin_data::BlockDirection::Down, + VerticalSurfaceType::Ceiling => pumpkin_data::BlockDirection::Up, + } + } + + /// Shortcut for `self.surface_direction().to_offset()`. + pub(crate) fn surface_offset(&self) -> Vector3 { + self.surface_direction().to_offset() + } + + #[allow(clippy::too_many_arguments)] + pub fn generate( + &self, + chunk: &mut T, + block_registry: &dyn BlockRegistryExt, + min_y: i8, + height: u16, + feature_name: &str, + random: &mut RandomGenerator, + pos: BlockPos, + ) -> bool { + // Convert radius providers + let x_radius = self.xz_radius.get(random) + 1; + let z_radius = self.xz_radius.get(random) + 1; + + let surface = self.place_ground_patch( + chunk, + block_registry, + random, + pos, + &self.replaceable, + x_radius, + z_radius, + ); + + self.distribute_vegetation( + chunk, + block_registry, + random, + min_y, + height, + feature_name, + &surface, + ); + + !surface.is_empty() + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn place_ground_patch( + &self, + chunk: &mut T, + block_registry: &dyn BlockRegistryExt, + random: &mut RandomGenerator, + origin: BlockPos, + replaceable: &BlockPredicate, + x_radius: i32, + z_radius: i32, + ) -> HashSet { + let mut surface = HashSet::new(); + + // Determine "inwards" and "outwards" directions based on the surface + let inwards = self.surface_direction(); + let outwards = inwards.opposite(); + + for dx in -x_radius..=x_radius { + let is_x_edge = dx == -x_radius || dx == x_radius; + for dz in -z_radius..=z_radius { + let is_z_edge = dz == -z_radius || dz == z_radius; + let is_corner = is_x_edge && is_z_edge; + let is_edge = is_x_edge || is_z_edge; + let is_edge_but_not_corner = is_edge && !is_corner; + + if is_corner { + continue; + } + + if is_edge_but_not_corner + && (self.extra_edge_column_chance == 0.0 + || random.next_f32() > self.extra_edge_column_chance) + { + continue; + } + + let mut pos = origin.offset(Vector3::new(dx, 0, dz)); + + // Move down until we hit non-air or exceed vertical range + for _ in 0..self.vertical_range { + if !chunk.is_air(&pos.0) { + break; + } + pos = pos.offset(inwards.to_offset()); + } + + // Now back the other way until we reach air again + for _ in 0..self.vertical_range { + if chunk.is_air(&pos.0) { + break; + } + pos = pos.offset(outwards.to_offset()); + } + + let below_pos = pos.offset(self.surface_offset()); + + let below_state_raw = GenerationCache::get_block_state(chunk, &below_pos.0); + if chunk.is_air(&pos.0) + && below_state_raw + .to_state() + .is_side_solid(self.surface_direction().opposite()) + { + // Compute depth variation + let mut depth = self.depth.get(random); + if self.extra_bottom_block_chance > 0.0 + && random.next_f32() < self.extra_bottom_block_chance + { + depth += 1; + } + + let ground_pos = below_pos; + if self.place_ground( + chunk, + block_registry, + replaceable, + random, + ground_pos, + depth, + ) { + surface.insert(ground_pos); + } + } + } + } + + surface + } + + fn place_ground( + &self, + chunk: &mut T, + block_registry: &dyn BlockRegistryExt, + replaceable: &BlockPredicate, + random: &mut RandomGenerator, + mut below_pos: BlockPos, + depth: i32, + ) -> bool { + for i in 0..depth { + let state_to_place = self.ground_state.get(random, below_pos); + let below_state_raw = GenerationCache::get_block_state(chunk, &below_pos.0); + + let state_block_id = pumpkin_data::Block::from_state_id(state_to_place.id).id; + let below_block_id = below_state_raw.to_block_id(); + + if state_block_id != below_block_id { + if !replaceable.test(block_registry, chunk, &below_pos) { + return i != 0; + } + + chunk.set_block_state(&below_pos.0, state_to_place); + // Move in direction of surface + below_pos = below_pos.offset(self.surface_offset()); + } + } + true + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn distribute_vegetation( + &self, + chunk: &mut T, + block_registry: &dyn BlockRegistryExt, + random: &mut RandomGenerator, + min_y: i8, + height: u16, + feature_name: &str, + surface: &HashSet, + ) { + let opposite_dir = self.surface_direction().opposite(); + + for &surface_pos in surface { + if self.vegetation_chance > 0.0 && random.next_f32() < self.vegetation_chance { + let placement_pos = surface_pos.offset(opposite_dir.to_offset()); + let _ = self.vegetation_feature.generate( + chunk, + block_registry, + min_y, + height, + feature_name, + random, + placement_pos, + ); + } + } + } } diff --git a/pumpkin-world/src/generation/feature/features/waterlogged_vegetation_patch.rs b/pumpkin-world/src/generation/feature/features/waterlogged_vegetation_patch.rs index 4a6f2b8ab..45c45a9a7 100644 --- a/pumpkin-world/src/generation/feature/features/waterlogged_vegetation_patch.rs +++ b/pumpkin-world/src/generation/feature/features/waterlogged_vegetation_patch.rs @@ -1,3 +1,154 @@ +use std::collections::HashSet; + +use pumpkin_util::{ + math::position::BlockPos, + random::{RandomGenerator, RandomImpl}, +}; + +use crate::generation::proto_chunk::GenerationCache; +use crate::world::BlockRegistryExt; +use pumpkin_data::BlockDirection; + +use super::vegetation_patch::VegetationPatchFeature; + pub struct WaterloggedVegetationPatchFeature { - // TODO + pub base: VegetationPatchFeature, +} + +impl WaterloggedVegetationPatchFeature { + #[allow(clippy::too_many_arguments)] + pub fn generate( + &self, + chunk: &mut T, + block_registry: &dyn BlockRegistryExt, + min_y: i8, + height: u16, + feature_name: &str, + random: &mut RandomGenerator, + pos: BlockPos, + ) -> bool { + let x_radius = self.base.xz_radius.get(random) + 1; + let z_radius = self.base.xz_radius.get(random) + 1; + + let water_surface = self.place_ground_patch( + chunk, + block_registry, + random, + pos, + &self.base.replaceable, + x_radius, + z_radius, + ); + + // Waterlogged vegetation should occupy the water block itself rather + // than sitting above it. We pass the water surface position directly. + for &surface_pos in &water_surface { + if self.base.vegetation_chance > 0.0 && random.next_f32() < self.base.vegetation_chance + { + self.place_vegetation( + chunk, + block_registry, + min_y, + height, + feature_name, + random, + surface_pos, + ); + } + } + + !water_surface.is_empty() + } + + #[allow(clippy::too_many_arguments)] + fn place_ground_patch( + &self, + chunk: &mut T, + block_registry: &dyn BlockRegistryExt, + random: &mut RandomGenerator, + origin: BlockPos, + replaceable: &crate::generation::block_predicate::BlockPredicate, + x_radius: i32, + z_radius: i32, + ) -> HashSet { + let surface = self.base.place_ground_patch( + chunk, + block_registry, + random, + origin, + replaceable, + x_radius, + z_radius, + ); + + // Filter the surface to only include unexposed positions, turning them into water + let water_surface: HashSet = surface + .into_iter() + .filter(|&pos| !is_exposed(chunk, pos)) + .collect(); + + for pos in &water_surface { + chunk.set_block_state(&pos.0, pumpkin_data::Block::WATER.default_state); + } + + water_surface + } + + #[allow(clippy::too_many_arguments)] + fn place_vegetation( + &self, + chunk: &mut T, + block_registry: &dyn BlockRegistryExt, + min_y: i8, + height: u16, + feature_name: &str, + random: &mut RandomGenerator, + placement_pos: BlockPos, + ) -> bool { + if self.base.vegetation_feature.generate( + chunk, + block_registry, + min_y, + height, + feature_name, + random, + placement_pos, + ) { + let placed_raw = GenerationCache::get_block_state(chunk, &placement_pos.0); + let placed_state = placed_raw.to_state(); + + if !placed_state.is_waterlogged() + && let Some(new_state) = placed_raw.to_block().with_waterlogged(placed_raw.0) + { + chunk.set_block_state(&placement_pos.0, new_state); + } + + true + } else { + false + } + } +} + +fn is_exposed(chunk: &T, pos: BlockPos) -> bool { + [ + BlockDirection::North, + BlockDirection::East, + BlockDirection::South, + BlockDirection::West, + BlockDirection::Down, + ] + .into_iter() + .any(|dir| is_exposed_direction(chunk, pos, dir)) +} + +fn is_exposed_direction( + chunk: &T, + pos: BlockPos, + direction: pumpkin_data::BlockDirection, +) -> bool { + let test_pos = pos.offset(direction.to_offset()); + !GenerationCache::get_block_state(chunk, &test_pos.0) + .to_state() + .is_side_solid(direction.opposite()) }