From c9c05bb6ec728e64312c7ca21dd391b8b6d01ba5 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Wed, 6 May 2026 17:52:21 +0200 Subject: [PATCH] feat(chunk-gen): add RootSystem feature --- pumpkin-codegen/src/configured_feature.rs | 44 ++++- .../configured_features_generated.rs | 55 +++++- .../generation/feature/configured_features.rs | 9 + .../src/generation/feature/features/lake.rs | 2 +- .../feature/features/root_system.rs | 183 +++++++++++++++++- 5 files changed, 289 insertions(+), 4 deletions(-) diff --git a/pumpkin-codegen/src/configured_feature.rs b/pumpkin-codegen/src/configured_feature.rs index e5f532e6f..34d0429c2 100644 --- a/pumpkin-codegen/src/configured_feature.rs +++ b/pumpkin-codegen/src/configured_feature.rs @@ -632,7 +632,49 @@ pub fn value_to_configured_feature(v: &Value) -> TokenStream { quote! { ConfiguredFeature::Vines(crate::generation::feature::features::vines::VinesFeature) } } "minecraft:root_system" => { - quote! { ConfiguredFeature::RootSystem(crate::generation::feature::features::root_system::RootSystemFeature {}) } + let feature = value_to_inline_placed_feature(&config["feature"]); + let required_vertical_space_for_tree = config["required_vertical_space_for_tree"] + .as_i64() + .unwrap_or(0) as i32; + let root_radius = config["root_radius"].as_i64().unwrap_or(0) as i32; + let root_replaceable = value_to_block_predicate(&config["root_replaceable"]); + let root_state_provider = value_to_block_state_provider(&config["root_state_provider"]); + let root_placement_attempts = config["root_placement_attempts"].as_i64().unwrap_or(0) + as i32; + let root_column_max_height = config["root_column_max_height"].as_i64().unwrap_or(0) + as i32; + let hanging_root_radius = config["hanging_root_radius"].as_i64().unwrap_or(0) as i32; + let hanging_roots_vertical_span = config["hanging_roots_vertical_span"] + .as_i64() + .or(config["hanging_root_vertical_span"].as_i64()) + .unwrap_or(0) as i32; + let hanging_root_state_provider = + value_to_block_state_provider(&config["hanging_root_state_provider"]); + let hanging_root_placement_attempts = config["hanging_root_placement_attempts"] + .as_i64() + .unwrap_or(0) as i32; + let allowed_vertical_water_for_tree = config["allowed_vertical_water_for_tree"] + .as_i64() + .unwrap_or(0) as i32; + let allowed_tree_position = value_to_block_predicate(&config["allowed_tree_position"]); + + quote! { + ConfiguredFeature::RootSystem(crate::generation::feature::features::root_system::RootSystemFeature { + feature: Box::new(#feature), + required_vertical_space_for_tree: #required_vertical_space_for_tree, + root_radius: #root_radius, + root_replaceable: #root_replaceable, + root_state_provider: #root_state_provider, + root_placement_attempts: #root_placement_attempts, + root_column_max_height: #root_column_max_height, + hanging_root_radius: #hanging_root_radius, + hanging_roots_vertical_span: #hanging_roots_vertical_span, + hanging_root_state_provider: #hanging_root_state_provider, + hanging_root_placement_attempts: #hanging_root_placement_attempts, + allowed_vertical_water_for_tree: #allowed_vertical_water_for_tree, + allowed_tree_position: #allowed_tree_position, + }) + } } "minecraft:multiface_growth" => { quote! { ConfiguredFeature::MultifaceGrowth(crate::generation::feature::features::multiface_growth::MultifaceGrowthFeature {}) } diff --git a/pumpkin-data/src/generated/configured_features_generated.rs b/pumpkin-data/src/generated/configured_features_generated.rs index 254ec7df9..7484e2d44 100644 --- a/pumpkin-data/src/generated/configured_features_generated.rs +++ b/pumpkin-data/src/generated/configured_features_generated.rs @@ -3595,7 +3595,60 @@ fn build_configured_features() -> std::collections::HashMap feature.generate(block_registry, chunk, random, pos), + Self::RootSystem(feature) => feature.generate( + chunk, + block_registry, + min_y, + height, + feature_name, + random, + pos, + ), _ => false, // TODO } } diff --git a/pumpkin-world/src/generation/feature/features/lake.rs b/pumpkin-world/src/generation/feature/features/lake.rs index ca0e1f4bb..947b6afe3 100644 --- a/pumpkin-world/src/generation/feature/features/lake.rs +++ b/pumpkin-world/src/generation/feature/features/lake.rs @@ -140,7 +140,7 @@ impl LakeFeature { if Block::from_state_id(fluid_state.id).id == Block::WATER.id { for xx in 0..16 { for zz in 0..16 { - let freeze_pos = origin.add(xx as i32, 4, zz as i32); + let freeze_pos = origin.add(xx, 4, zz); let biome = chunk.get_biome_for_terrain_gen( freeze_pos.0.x, freeze_pos.0.y, diff --git a/pumpkin-world/src/generation/feature/features/root_system.rs b/pumpkin-world/src/generation/feature/features/root_system.rs index 182d84702..76a1fdd9b 100644 --- a/pumpkin-world/src/generation/feature/features/root_system.rs +++ b/pumpkin-world/src/generation/feature/features/root_system.rs @@ -1,3 +1,184 @@ +use pumpkin_util::{ + math::position::BlockPos, + 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 RootSystemFeature { - // TODO + pub feature: Box, + pub required_vertical_space_for_tree: i32, + pub root_radius: i32, + pub root_replaceable: BlockPredicate, + pub root_state_provider: BlockStateProvider, + pub root_placement_attempts: i32, + pub root_column_max_height: i32, + pub hanging_root_radius: i32, + pub hanging_roots_vertical_span: i32, + pub hanging_root_state_provider: BlockStateProvider, + pub hanging_root_placement_attempts: i32, + pub allowed_vertical_water_for_tree: i32, + pub allowed_tree_position: BlockPredicate, +} + +impl RootSystemFeature { + #[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 { + if !self.allowed_tree_position.test(block_registry, chunk, &pos) { + return false; + } + + if !self.can_place_tree(chunk, pos) { + return false; + } + + if !self.feature.generate( + chunk, + block_registry, + min_y, + height, + feature_name, + random, + pos, + ) { + return false; + } + + self.place_roots(chunk, block_registry, random, pos); + true + } + + fn can_place_tree(&self, chunk: &T, pos: BlockPos) -> bool { + let mut mutable_pos = pos; + for i in 1..=self.required_vertical_space_for_tree { + mutable_pos = mutable_pos.add(0, 1, 0); + if !self.is_allowed_tree_space(chunk, mutable_pos, i) { + return false; + } + } + true + } + + fn is_allowed_tree_space( + &self, + chunk: &T, + pos: BlockPos, + vertical_space: i32, + ) -> bool { + if chunk.is_air(&pos.0) { + return true; + } + // TODO: check for water. + vertical_space <= self.allowed_vertical_water_for_tree + } + + fn place_roots( + &self, + chunk: &mut T, + block_registry: &dyn BlockRegistryExt, + random: &mut RandomGenerator, + pos: BlockPos, + ) { + self.place_rooted_dirt(chunk, block_registry, random, pos); + self.place_hanging_roots(chunk, block_registry, random, pos); + } + + fn place_rooted_dirt( + &self, + chunk: &mut T, + block_registry: &dyn BlockRegistryExt, + random: &mut RandomGenerator, + pos: BlockPos, + ) { + for _ in 0..self.root_placement_attempts { + let mut mutable_pos = pos.add( + random.next_bounded_i32(self.root_radius.max(1)) + - random.next_bounded_i32(self.root_radius.max(1)), + 0, + random.next_bounded_i32(self.root_radius.max(1)) + - random.next_bounded_i32(self.root_radius.max(1)), + ); + + if self + .root_replaceable + .test(block_registry, chunk, &mutable_pos) + { + chunk.set_block_state( + &mutable_pos.0, + self.root_state_provider.get_with_context( + block_registry, + chunk, + random, + mutable_pos, + ), + ); + } + + for _ in 0..self.root_column_max_height { + mutable_pos = mutable_pos.add(0, -1, 0); + if chunk.out_of_height(mutable_pos.0.y as i16) + || !self + .root_replaceable + .test(block_registry, chunk, &mutable_pos) + { + break; + } + chunk.set_block_state( + &mutable_pos.0, + self.root_state_provider.get_with_context( + block_registry, + chunk, + random, + mutable_pos, + ), + ); + } + } + } + + fn place_hanging_roots( + &self, + chunk: &mut T, + block_registry: &dyn BlockRegistryExt, + random: &mut RandomGenerator, + pos: BlockPos, + ) { + for _ in 0..self.hanging_root_placement_attempts { + let mutable_pos = pos.add( + random.next_bounded_i32(self.hanging_root_radius.max(1)) + - random.next_bounded_i32(self.hanging_root_radius.max(1)), + random.next_bounded_i32(self.hanging_roots_vertical_span.max(1)) + - random.next_bounded_i32(self.hanging_roots_vertical_span.max(1)), + random.next_bounded_i32(self.hanging_root_radius.max(1)) + - random.next_bounded_i32(self.hanging_root_radius.max(1)), + ); + + if chunk.is_air(&mutable_pos.0) { + let state = self.hanging_root_state_provider.get_with_context( + block_registry, + chunk, + random, + mutable_pos, + ); + // In vanilla, it checks if it can survive and if the block above is sturdy. + // For now, let's just check if the block above is NOT air. + let above = mutable_pos.add(0, 1, 0); + if !chunk.is_air(&above.0) { + chunk.set_block_state(&mutable_pos.0, state); + } + } + } + } }