diff --git a/pumpkin-codegen/src/biome.rs b/pumpkin-codegen/src/biome.rs index 24458ba2c..81ed411e0 100644 --- a/pumpkin-codegen/src/biome.rs +++ b/pumpkin-codegen/src/biome.rs @@ -226,6 +226,7 @@ pub fn build() -> TokenStream { let mut variants = TokenStream::new(); let mut name_to_type = TokenStream::new(); let mut id_to_type = TokenStream::new(); + let mut all_variants = TokenStream::new(); for (name, biome) in biomes { let full_name = format!("minecraft:{name}"); @@ -359,6 +360,7 @@ pub fn build() -> TokenStream { name_to_type.extend(quote! { #name => Some(&Self::#format_name), }); id_to_type.extend(quote! { #index => Some(&Self::#format_name), }); + all_variants.extend(quote! { &Self::#format_name, }); } let overworld_tree = biome_trees.overworld.into_token_stream(); @@ -464,6 +466,8 @@ pub fn build() -> TokenStream { impl Biome { #variants + pub const ALL: &'static [&'static Self] = &[#all_variants]; + pub fn from_name(name: &str) -> Option<&'static Self> { match name { #name_to_type diff --git a/pumpkin-data/src/generated/biome.rs b/pumpkin-data/src/generated/biome.rs index f83a2865f..df9c7534b 100644 --- a/pumpkin-data/src/generated/biome.rs +++ b/pumpkin-data/src/generated/biome.rs @@ -8869,6 +8869,74 @@ impl Biome { }, spawn_costs: phf::phf_map! {}, }; + pub const ALL: &'static [&'static Self] = &[ + &Self::BADLANDS, + &Self::BAMBOO_JUNGLE, + &Self::BASALT_DELTAS, + &Self::BEACH, + &Self::BIRCH_FOREST, + &Self::CHERRY_GROVE, + &Self::COLD_OCEAN, + &Self::CRIMSON_FOREST, + &Self::DARK_FOREST, + &Self::DEEP_COLD_OCEAN, + &Self::DEEP_DARK, + &Self::DEEP_FROZEN_OCEAN, + &Self::DEEP_LUKEWARM_OCEAN, + &Self::DEEP_OCEAN, + &Self::DESERT, + &Self::DRIPSTONE_CAVES, + &Self::END_BARRENS, + &Self::END_HIGHLANDS, + &Self::END_MIDLANDS, + &Self::ERODED_BADLANDS, + &Self::FLOWER_FOREST, + &Self::FOREST, + &Self::FROZEN_OCEAN, + &Self::FROZEN_PEAKS, + &Self::FROZEN_RIVER, + &Self::GROVE, + &Self::ICE_SPIKES, + &Self::JAGGED_PEAKS, + &Self::JUNGLE, + &Self::LUKEWARM_OCEAN, + &Self::LUSH_CAVES, + &Self::MANGROVE_SWAMP, + &Self::MEADOW, + &Self::MUSHROOM_FIELDS, + &Self::NETHER_WASTES, + &Self::OCEAN, + &Self::OLD_GROWTH_BIRCH_FOREST, + &Self::OLD_GROWTH_PINE_TAIGA, + &Self::OLD_GROWTH_SPRUCE_TAIGA, + &Self::PALE_GARDEN, + &Self::PLAINS, + &Self::RIVER, + &Self::SAVANNA, + &Self::SAVANNA_PLATEAU, + &Self::SMALL_END_ISLANDS, + &Self::SNOWY_BEACH, + &Self::SNOWY_PLAINS, + &Self::SNOWY_SLOPES, + &Self::SNOWY_TAIGA, + &Self::SOUL_SAND_VALLEY, + &Self::SPARSE_JUNGLE, + &Self::STONY_PEAKS, + &Self::STONY_SHORE, + &Self::SULFUR_CAVES, + &Self::SUNFLOWER_PLAINS, + &Self::SWAMP, + &Self::TAIGA, + &Self::THE_END, + &Self::THE_VOID, + &Self::WARM_OCEAN, + &Self::WARPED_FOREST, + &Self::WINDSWEPT_FOREST, + &Self::WINDSWEPT_GRAVELLY_HILLS, + &Self::WINDSWEPT_HILLS, + &Self::WINDSWEPT_SAVANNA, + &Self::WOODED_BADLANDS, + ]; pub fn from_name(name: &str) -> Option<&'static Self> { match name { "badlands" => Some(&Self::BADLANDS), diff --git a/pumpkin-world/src/chunk/mod.rs b/pumpkin-world/src/chunk/mod.rs index 837aed36c..52b4ca45a 100644 --- a/pumpkin-world/src/chunk/mod.rs +++ b/pumpkin-world/src/chunk/mod.rs @@ -522,7 +522,7 @@ impl ChunkSections { } pub fn set_relative_biome( - &mut self, + &self, relative_x: usize, relative_y: usize, relative_z: usize, diff --git a/pumpkin/src/command/argument_types/resource_key.rs b/pumpkin/src/command/argument_types/resource_key.rs index 0860000cd..e7b9585a9 100644 --- a/pumpkin/src/command/argument_types/resource_key.rs +++ b/pumpkin/src/command/argument_types/resource_key.rs @@ -13,12 +13,16 @@ use std::pin::Pin; use std::string::ToString; pub static ADVANCEMENT_REGISTRY: Identifier = Identifier::vanilla_static("advancement"); +pub static BIOME_REGISTRY: Identifier = Identifier::vanilla_static("worldgen/biome"); pub const ERROR_INVALID_ADVANCEMENT: CommandErrorType<1> = CommandErrorType::new( translation::java::ADVANCEMENT_ADVANCEMENTNOTFOUND, translation::java::ADVANCEMENT_ADVANCEMENTNOTFOUND, ); +pub const ERROR_INVALID_BIOME: CommandErrorType<1> = + CommandErrorType::new("commands.fillbiome.invalid", "commands.fillbiome.invalid"); + /// Represents an argument type used to get a resource key from an identifier. /// /// if you want an [`Advancement`] put the [`ADVANCEMENT_REGISTRY`] @@ -54,6 +58,13 @@ impl ArgumentType for ResourceKeyArgument { .filter_and_suggest_iter(advancements.iter().map(ToString::to_string)) .build() }) + } else if self.0 == BIOME_REGISTRY { + Box::pin(async move { + let biomes = pumpkin_data::biome::Biome::ALL + .iter() + .map(|biome| format!("minecraft:{}", biome.registry_id)); + suggestions_builder.filter_and_suggest_iter(biomes).build() + }) } else { Box::pin(async move { Suggestions::empty() }) } @@ -94,6 +105,20 @@ impl ResourceKeyArgument { }) } + /// Returns a [`CommandContext`]'s parsed resource key argument as a [`Biome`]. + pub fn get_biome( + context: &CommandContext, + name: &str, + ) -> Result<&'static pumpkin_data::biome::Biome, CommandSyntaxError> { + let resource_key: &ResourceKey = + Self::get_registry_key(context, name, &BIOME_REGISTRY, &ERROR_INVALID_BIOME)?; + let path = resource_key.identifier.path(); + pumpkin_data::biome::Biome::from_name(path).ok_or_else(|| { + ERROR_INVALID_BIOME + .create_without_context(TextComponent::text(resource_key.identifier.to_string())) + }) + } + /// Returns a [`CommandContext`]'s parsed resource key argument in the form of a [`ResourceKey`]. /// /// # Arguments diff --git a/pumpkin/src/command/commands/fillbiome.rs b/pumpkin/src/command/commands/fillbiome.rs new file mode 100644 index 000000000..92ba2be4d --- /dev/null +++ b/pumpkin/src/command/commands/fillbiome.rs @@ -0,0 +1,182 @@ +use crate::command::argument_builder::{ArgumentBuilder, argument, command}; +use crate::command::argument_types::coordinates::block_pos::{ + BlockPosArgumentType, OUT_OF_BOUNDS_ERROR_TYPE, +}; +use crate::command::argument_types::resource_key::{BIOME_REGISTRY, ResourceKeyArgument}; +use crate::command::context::command_context::CommandContext; +use crate::command::errors::error_types::CommandErrorType; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; +use pumpkin_data::translation; +use pumpkin_protocol::java::client::play::CChunkData; +use pumpkin_util::PermissionLvl; +use pumpkin_util::math::vector2::Vector2; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; +use pumpkin_util::text::TextComponent; +use std::collections::HashMap; + +const DESCRIPTION: &str = "Changes biomes of an area."; +const PERMISSION: &str = "minecraft:command.fillbiome"; + +const MAX_BIOME_BLOCKS: i64 = 32768; + +static ERROR_TOOBIG: CommandErrorType<2> = CommandErrorType::new( + translation::java::COMMANDS_FILLBIOME_TOOBIG, + translation::java::COMMANDS_FILLBIOME_TOOBIG, +); + +struct FillBiomeExecutor { + has_replace: bool, +} + +impl CommandExecutor for FillBiomeExecutor { + #[expect(clippy::too_many_lines)] + fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> { + Box::pin(async move { + let from_pos = BlockPosArgumentType::get_block_pos(context, "from")?; + let to_pos = BlockPosArgumentType::get_block_pos(context, "to")?; + + let world = context.world(); + if !world.is_in_build_limit(from_pos) || !world.is_in_build_limit(to_pos) { + return Err(OUT_OF_BOUNDS_ERROR_TYPE.create_without_context()); + } + + let min_x = from_pos.0.x.min(to_pos.0.x); + let max_x = from_pos.0.x.max(to_pos.0.x); + let min_y = from_pos.0.y.min(to_pos.0.y); + let max_y = from_pos.0.y.max(to_pos.0.y); + let min_z = from_pos.0.z.min(to_pos.0.z); + let max_z = from_pos.0.z.max(to_pos.0.z); + + let biome_min_x = min_x >> 2; + let biome_max_x = max_x >> 2; + let biome_min_y = min_y >> 2; + let biome_max_y = max_y >> 2; + let biome_min_z = min_z >> 2; + let biome_max_z = max_z >> 2; + + let volume = (biome_max_x - biome_min_x + 1) as i64 + * (biome_max_y - biome_min_y + 1) as i64 + * (biome_max_z - biome_min_z + 1) as i64; + + if volume > MAX_BIOME_BLOCKS { + return Err(ERROR_TOOBIG.create_without_context_args_slice(&[ + TextComponent::text(MAX_BIOME_BLOCKS.to_string()), + TextComponent::text(volume.to_string()), + ])); + } + + let target_biome = ResourceKeyArgument::get_biome(context, "biome")?; + + let replace_biome = if self.has_replace { + Some(ResourceKeyArgument::get_biome(context, "replace_biome")?) + } else { + None + }; + + let mut chunk_modifications: HashMap, Vec<(usize, usize, usize)>> = + HashMap::new(); + for y in biome_min_y..=biome_max_y { + for z in biome_min_z..=biome_max_z { + for x in biome_min_x..=biome_max_x { + let chunk_pos = Vector2::new(x >> 2, z >> 2); + let rel_x = (x & 3) as usize; + let rel_z = (z & 3) as usize; + let rel_y = (y - (world.min_y >> 2)) as usize; + chunk_modifications + .entry(chunk_pos) + .or_default() + .push((rel_x, rel_y, rel_z)); + } + } + } + + let target_biome_id = target_biome.id; + let replace_biome_id = replace_biome.map(|b| b.id); + let mut changed_count = 0; + + for (chunk_pos, mods) in chunk_modifications { + let (has_replaced, count) = world + .level + .get_or_fetch_chunk(chunk_pos, |chunk| { + let mut local_count = 0; + let mut modified = false; + for &(rel_x, rel_y, rel_z) in &mods { + let section_index = rel_y / 4; + let scale_y = rel_y % 4; + if let Some(current_id) = + chunk + .section + .get_noise_biome(section_index, rel_x, scale_y, rel_z) + { + if let Some(replace_id) = replace_biome_id { + if current_id == replace_id { + chunk.section.set_relative_biome( + rel_x, + rel_y, + rel_z, + target_biome_id, + ); + local_count += 1; + modified = true; + } + } else { + chunk.section.set_relative_biome( + rel_x, + rel_y, + rel_z, + target_biome_id, + ); + local_count += 1; + modified = true; + } + } + } + (modified, local_count) + }) + .await; + + if has_replaced { + changed_count += count; + let chunk = world + .level + .get_or_fetch_chunk(chunk_pos, std::clone::Clone::clone) + .await; + world.broadcast_to_chunk_except(chunk_pos, &[], &CChunkData(&chunk)); + } + } + + let msg = TextComponent::translate_cross( + translation::java::COMMANDS_FILLBIOME_SUCCESS_COUNT, + translation::java::COMMANDS_FILLBIOME_SUCCESS_COUNT, + [TextComponent::text(changed_count.to_string())], + ); + context.source.send_feedback(msg, true).await; + + Ok(changed_count) + }) + } +} + +pub fn register(dispatcher: &mut CommandDispatcher, registry: &mut PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + let builder = command("fillbiome", DESCRIPTION).requires(PERMISSION).then( + argument("from", BlockPosArgumentType).then( + argument("to", BlockPosArgumentType).then( + argument("biome", ResourceKeyArgument(BIOME_REGISTRY.clone())) + .executes(FillBiomeExecutor { has_replace: false }) + .then( + argument("replace_biome", ResourceKeyArgument(BIOME_REGISTRY.clone())) + .executes(FillBiomeExecutor { has_replace: true }), + ), + ), + ), + ); + + dispatcher.register(builder); +} diff --git a/pumpkin/src/command/commands/mod.rs b/pumpkin/src/command/commands/mod.rs index 3d37b6ce2..dd91953bb 100644 --- a/pumpkin/src/command/commands/mod.rs +++ b/pumpkin/src/command/commands/mod.rs @@ -25,6 +25,7 @@ mod enchant; mod execute; mod experience; mod fill; +mod fillbiome; mod forceload; mod gamemode; mod gamerule; @@ -173,6 +174,7 @@ pub async fn default_dispatcher( difficulty::register(&mut dispatcher, registry); dialog::register(&mut dispatcher, registry); execute::register(&mut dispatcher, registry); + fillbiome::register(&mut dispatcher, registry); forceload::register(&mut dispatcher, registry); ride::register(&mut dispatcher, registry); recipe::register(&mut dispatcher, registry);