mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
feat(command): add fillbiome command
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -522,7 +522,7 @@ impl ChunkSections {
|
||||
}
|
||||
|
||||
pub fn set_relative_biome(
|
||||
&mut self,
|
||||
&self,
|
||||
relative_x: usize,
|
||||
relative_y: usize,
|
||||
relative_z: usize,
|
||||
|
||||
@@ -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
|
||||
|
||||
182
pumpkin/src/command/commands/fillbiome.rs
Normal file
182
pumpkin/src/command/commands/fillbiome.rs
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user