feat(command): add fillbiome command

This commit is contained in:
Alexander Medvedev
2026-07-14 19:19:03 +02:00
parent 06fa1db1d7
commit 37bed1c8b5
6 changed files with 282 additions and 1 deletions

View File

@@ -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

View File

@@ -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),

View File

@@ -522,7 +522,7 @@ impl ChunkSections {
}
pub fn set_relative_biome(
&mut self,
&self,
relative_x: usize,
relative_y: usize,
relative_z: usize,

View File

@@ -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

View File

@@ -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<Vector2<i32>, 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);
}

View File

@@ -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);