use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; use crate::command::argument_types::block::BlockArgumentType; use crate::command::argument_types::coordinates::block_pos::BlockPosArgumentType; 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::{Block, BlockStateId, translation}; use pumpkin_nbt::compound::NbtCompound; use pumpkin_util::PermissionLvl; use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::vector2::Vector2; use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; use pumpkin_world::world::BlockFlags; const DESCRIPTION: &str = "Clones blocks from one region to another."; const PERMISSION: &str = "minecraft:command.clone"; const ARG_BEGIN: &str = "begin"; const ARG_END: &str = "end"; const ARG_DEST: &str = "destination"; const ARG_FILTER: &str = "filter"; const OVERLAP_ERROR: CommandErrorType<0> = CommandErrorType::new( translation::java::COMMANDS_CLONE_OVERLAP, translation::java::COMMANDS_CLONE_OVERLAP, ); const TOOBIG_ERROR: CommandErrorType<2> = CommandErrorType::new( translation::java::COMMANDS_CLONE_TOOBIG, translation::java::COMMANDS_CLONE_TOOBIG, ); const FAILED_ERROR: CommandErrorType<0> = CommandErrorType::new( translation::java::COMMANDS_CLONE_FAILED, translation::java::COMMANDS_CLONE_FAILED, ); const NOT_LOADED_ERROR: CommandErrorType<0> = CommandErrorType::new( translation::java::ARGUMENT_POS_UNLOADED, translation::java::ARGUMENT_POS_UNLOADED, ); const OUT_OF_WORLD_ERROR: CommandErrorType<0> = CommandErrorType::new( translation::java::ARGUMENT_POS_OUTOFWORLD, translation::java::ARGUMENT_POS_OUTOFWORLD, ); #[derive(Clone, Copy, PartialEq, Eq)] enum MaskMode { Replace, Masked, Filtered, } #[derive(Clone, Copy, PartialEq, Eq)] enum CloneMode { Normal, Force, Move, } struct CloneExecutor { mask_mode: MaskMode, clone_mode: CloneMode, has_filter: bool, } struct ClonedBlock { src_pos: BlockPos, dest_pos: BlockPos, state_id: BlockStateId, block_entity_nbt: Option, } impl CommandExecutor for CloneExecutor { #[expect(clippy::too_many_lines)] fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> { Box::pin(async move { let begin = BlockPosArgumentType::get_block_pos(context, ARG_BEGIN)?; let end = BlockPosArgumentType::get_block_pos(context, ARG_END)?; let dest = BlockPosArgumentType::get_block_pos(context, ARG_DEST)?; let filter_block = if self.has_filter { Some(BlockArgumentType::get(context, ARG_FILTER)?) } else { None }; let min_x = begin.0.x.min(end.0.x); let max_x = begin.0.x.max(end.0.x); let min_y = begin.0.y.min(end.0.y); let max_y = begin.0.y.max(end.0.y); let min_z = begin.0.z.min(end.0.z); let max_z = begin.0.z.max(end.0.z); let size_x = max_x - min_x + 1; let size_y = max_y - min_y + 1; let size_z = max_z - min_z + 1; let volume = size_x * size_y * size_z; if volume > 32768 { return Err(TOOBIG_ERROR.create_without_context_args_slice(&[ TextComponent::text("32768"), TextComponent::text(volume.to_string()), ])); } let dest_min_x = dest.0.x; let dest_min_y = dest.0.y; let dest_min_z = dest.0.z; let dest_max_x = dest_min_x + size_x - 1; let dest_max_y = dest_min_y + size_y - 1; let dest_max_z = dest_min_z + size_z - 1; let overlap = !(dest_max_x < min_x || dest_min_x > max_x || dest_max_y < min_y || dest_min_y > max_y || dest_max_z < min_z || dest_min_z > max_z); if overlap && self.clone_mode != CloneMode::Force { return Err(OVERLAP_ERROR.create_without_context()); } let world = context.source.world(); let limit_check_pos_1 = BlockPos::new(min_x, min_y, min_z); let limit_check_pos_2 = BlockPos::new(max_x, max_y, max_z); let limit_check_pos_3 = BlockPos::new(dest_min_x, dest_min_y, dest_min_z); let limit_check_pos_4 = BlockPos::new(dest_max_x, dest_max_y, dest_max_z); if !world.is_in_build_limit(limit_check_pos_1) || !world.is_in_build_limit(limit_check_pos_2) || !world.is_in_build_limit(limit_check_pos_3) || !world.is_in_build_limit(limit_check_pos_4) { return Err(OUT_OF_WORLD_ERROR.create_without_context()); } for cx in (min_x >> 4)..=(max_x >> 4) { for cz in (min_z >> 4)..=(max_z >> 4) { let chunk_pos = Vector2::new(cx, cz); if world.level.read_chunk_sync(&chunk_pos, |_| ()).is_none() { return Err(NOT_LOADED_ERROR.create_without_context()); } } } for cx in (dest_min_x >> 4)..=(dest_max_x >> 4) { for cz in (dest_min_z >> 4)..=(dest_max_z >> 4) { let chunk_pos = Vector2::new(cx, cz); if world.level.read_chunk_sync(&chunk_pos, |_| ()).is_none() { return Err(NOT_LOADED_ERROR.create_without_context()); } } } let mut blocks_to_clone = Vec::new(); for x in 0..size_x { for y in 0..size_y { for z in 0..size_z { let src_pos = BlockPos::new(min_x + x, min_y + y, min_z + z); let dest_pos = BlockPos::new(dest_min_x + x, dest_min_y + y, dest_min_z + z); let state_id = world.get_block_state_id(&src_pos); let should_clone = match self.mask_mode { MaskMode::Replace => true, MaskMode::Masked => !pumpkin_data::block_properties::is_air(state_id), MaskMode::Filtered => { let block = Block::from_state_id(state_id); block.id == filter_block.unwrap().id } }; if should_clone { let block_entity_nbt = if let Some(be) = world.get_block_entity(&src_pos) { let mut nbt = NbtCompound::new(); be.write_internal(&mut nbt).await; Some(nbt) } else { None }; blocks_to_clone.push(ClonedBlock { src_pos, dest_pos, state_id, block_entity_nbt, }); } } } } let mut count = 0; for block in &blocks_to_clone { world .set_block_state(&block.dest_pos, block.state_id, BlockFlags::NOTIFY_ALL) .await; if let Some(nbt) = &block.block_entity_nbt { let mut new_nbt = nbt.clone(); new_nbt.put_int("x", block.dest_pos.0.x); new_nbt.put_int("y", block.dest_pos.0.y); new_nbt.put_int("z", block.dest_pos.0.z); if let Some(be) = crate::block::entities::block_entity_from_nbt(&new_nbt) { world.add_block_entity(be); } } count += 1; } if count == 0 { return Err(FAILED_ERROR.create_without_context()); } let is_dest_pos = |pos: &BlockPos| { pos.0.x >= dest_min_x && pos.0.x <= dest_max_x && pos.0.y >= dest_min_y && pos.0.y <= dest_max_y && pos.0.z >= dest_min_z && pos.0.z <= dest_max_z }; if self.clone_mode == CloneMode::Move { for block in &blocks_to_clone { if !is_dest_pos(&block.src_pos) { world .set_block_state( &block.src_pos, BlockStateId::AIR, BlockFlags::NOTIFY_ALL, ) .await; } } } context .source .send_feedback( TextComponent::translate_cross( translation::java::COMMANDS_CLONE_SUCCESS, translation::java::COMMANDS_CLONE_SUCCESS, [TextComponent::text(count.to_string())], ), true, ) .await; Ok(count) }) } } pub fn register(dispatcher: &mut CommandDispatcher, registry: &mut PermissionRegistry) { registry.register_permission_or_panic(Permission::new( PERMISSION, DESCRIPTION, PermissionDefault::Op(PermissionLvl::Two), )); dispatcher.register( command("clone", DESCRIPTION).requires(PERMISSION).then( argument(ARG_BEGIN, BlockPosArgumentType).then( argument(ARG_END, BlockPosArgumentType).then( argument(ARG_DEST, BlockPosArgumentType) .executes(CloneExecutor { mask_mode: MaskMode::Replace, clone_mode: CloneMode::Normal, has_filter: false, }) .then( literal("replace") .executes(CloneExecutor { mask_mode: MaskMode::Replace, clone_mode: CloneMode::Normal, has_filter: false, }) .then(literal("force").executes(CloneExecutor { mask_mode: MaskMode::Replace, clone_mode: CloneMode::Force, has_filter: false, })) .then(literal("move").executes(CloneExecutor { mask_mode: MaskMode::Replace, clone_mode: CloneMode::Move, has_filter: false, })) .then(literal("normal").executes(CloneExecutor { mask_mode: MaskMode::Replace, clone_mode: CloneMode::Normal, has_filter: false, })), ) .then( literal("masked") .executes(CloneExecutor { mask_mode: MaskMode::Masked, clone_mode: CloneMode::Normal, has_filter: false, }) .then(literal("force").executes(CloneExecutor { mask_mode: MaskMode::Masked, clone_mode: CloneMode::Force, has_filter: false, })) .then(literal("move").executes(CloneExecutor { mask_mode: MaskMode::Masked, clone_mode: CloneMode::Move, has_filter: false, })) .then(literal("normal").executes(CloneExecutor { mask_mode: MaskMode::Masked, clone_mode: CloneMode::Normal, has_filter: false, })), ) .then( literal("filtered").then( argument(ARG_FILTER, BlockArgumentType) .executes(CloneExecutor { mask_mode: MaskMode::Filtered, clone_mode: CloneMode::Normal, has_filter: true, }) .then(literal("force").executes(CloneExecutor { mask_mode: MaskMode::Filtered, clone_mode: CloneMode::Force, has_filter: true, })) .then(literal("move").executes(CloneExecutor { mask_mode: MaskMode::Filtered, clone_mode: CloneMode::Move, has_filter: true, })) .then(literal("normal").executes(CloneExecutor { mask_mode: MaskMode::Filtered, clone_mode: CloneMode::Normal, has_filter: true, })), ), ), ), ), ), ); }