feat(chunk-gen): add RootSystem feature

This commit is contained in:
Alexander Medvedev 2026-05-06 17:52:21 +02:00
parent ed12db09ce
commit c9c05bb6ec
5 changed files with 289 additions and 4 deletions

View File

@ -632,7 +632,49 @@ pub fn value_to_configured_feature(v: &Value) -> TokenStream {
quote! { ConfiguredFeature::Vines(crate::generation::feature::features::vines::VinesFeature) } quote! { ConfiguredFeature::Vines(crate::generation::feature::features::vines::VinesFeature) }
} }
"minecraft:root_system" => { "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" => { "minecraft:multiface_growth" => {
quote! { ConfiguredFeature::MultifaceGrowth(crate::generation::feature::features::multiface_growth::MultifaceGrowthFeature {}) } quote! { ConfiguredFeature::MultifaceGrowth(crate::generation::feature::features::multiface_growth::MultifaceGrowthFeature {}) }

View File

@ -3595,7 +3595,60 @@ fn build_configured_features() -> std::collections::HashMap<String, ConfiguredFe
map.insert( map.insert(
"rooted_azalea_tree".to_string(), "rooted_azalea_tree".to_string(),
ConfiguredFeature::RootSystem( ConfiguredFeature::RootSystem(
crate::generation::feature::features::root_system::RootSystemFeature {}, crate::generation::feature::features::root_system::RootSystemFeature {
feature: Box::new(PlacedFeature {
feature: Feature::Named("azalea_tree".to_string()),
placement: vec![],
}),
required_vertical_space_for_tree: 3i32,
root_radius: 3i32,
root_replaceable: BlockPredicate::MatchingBlockTag(MatchingBlockTagPredicate {
offset: OffsetBlocksBlockPredicate { offset: None },
tag: pumpkin_data::tag::Block::MINECRAFT_AZALEA_ROOT_REPLACEABLE,
}),
root_state_provider: BlockStateProvider::Simple(SimpleStateProvider {
state: pumpkin_data::Block::ROOTED_DIRT.default_state,
}),
root_placement_attempts: 20i32,
root_column_max_height: 100i32,
hanging_root_radius: 3i32,
hanging_roots_vertical_span: 2i32,
hanging_root_state_provider: BlockStateProvider::Simple(SimpleStateProvider {
state: {
let mut props = std::collections::HashMap::new();
props.insert("waterlogged".to_string(), "false".to_string());
BlockStateCodec {
name: &pumpkin_data::Block::HANGING_ROOTS,
properties: Some(props),
}
.get_state()
},
}),
hanging_root_placement_attempts: 20i32,
allowed_vertical_water_for_tree: 2i32,
allowed_tree_position: BlockPredicate::AllOf(AllOfBlockPredicate {
predicates: vec![
BlockPredicate::AnyOf(AnyOfBlockPredicate {
predicates: vec![
BlockPredicate::MatchingBlockTag(MatchingBlockTagPredicate {
offset: OffsetBlocksBlockPredicate { offset: None },
tag: pumpkin_data::tag::Block::MINECRAFT_AIR,
}),
BlockPredicate::MatchingBlockTag(MatchingBlockTagPredicate {
offset: OffsetBlocksBlockPredicate { offset: None },
tag: pumpkin_data::tag::Block::MINECRAFT_REPLACEABLE_BY_TREES,
}),
],
}),
BlockPredicate::MatchingBlockTag(MatchingBlockTagPredicate {
offset: OffsetBlocksBlockPredicate {
offset: Some(Vector3::new(0i32, -1i32, 0i32)),
},
tag: pumpkin_data::tag::Block::MINECRAFT_AZALEA_GROWS_ON,
}),
],
}),
},
), ),
); );
map.insert( map.insert(

View File

@ -387,6 +387,15 @@ impl ConfiguredFeature {
feature.generate(chunk, min_y, height, feature_name, random, pos) feature.generate(chunk, min_y, height, feature_name, random, pos)
} }
Self::SculkPatch(feature) => feature.generate(block_registry, chunk, random, pos), Self::SculkPatch(feature) => feature.generate(block_registry, chunk, random, pos),
Self::RootSystem(feature) => feature.generate(
chunk,
block_registry,
min_y,
height,
feature_name,
random,
pos,
),
_ => false, // TODO _ => false, // TODO
} }
} }

View File

@ -140,7 +140,7 @@ impl LakeFeature {
if Block::from_state_id(fluid_state.id).id == Block::WATER.id { if Block::from_state_id(fluid_state.id).id == Block::WATER.id {
for xx in 0..16 { for xx in 0..16 {
for zz 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( let biome = chunk.get_biome_for_terrain_gen(
freeze_pos.0.x, freeze_pos.0.x,
freeze_pos.0.y, freeze_pos.0.y,

View File

@ -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 { pub struct RootSystemFeature {
// TODO pub feature: Box<crate::generation::feature::placed_features::PlacedFeature>,
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<T: GenerationCache>(
&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<T: GenerationCache>(&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<T: GenerationCache>(
&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<T: GenerationCache>(
&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<T: GenerationCache>(
&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<T: GenerationCache>(
&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);
}
}
}
}
} }