From e393751b8d441fda01710f242f6e4c610ea3c193 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Sun, 30 Aug 2026 23:10:34 +0200 Subject: [PATCH] chore: port to new command system --- .../java/client/play/player_chat_message.rs | 13 +- crates/pumpkin-world/benches/chunk_gen.rs | 6 +- crates/pumpkin/src/block/blocks/signs.rs | 329 +++-- .../src/block/entities/hanging_sign.rs | 4 +- crates/pumpkin/src/block/entities/sign.rs | 288 ++++- crates/pumpkin/src/command/args/block.rs | 150 --- crates/pumpkin/src/command/args/bool.rs | 46 - .../pumpkin/src/command/args/bossbar_color.rs | 73 -- .../pumpkin/src/command/args/bossbar_style.rs | 77 -- .../pumpkin/src/command/args/bounded_num.rs | 329 ----- crates/pumpkin/src/command/args/command.rs | 79 -- crates/pumpkin/src/command/args/coordinate.rs | 75 -- crates/pumpkin/src/command/args/difficulty.rs | 69 - crates/pumpkin/src/command/args/entities.rs | 558 -------- crates/pumpkin/src/command/args/entity.rs | 101 -- .../pumpkin/src/command/args/entity_anchor.rs | 77 -- crates/pumpkin/src/command/args/gamemode.rs | 61 - .../pumpkin/src/command/args/gameprofile.rs | 463 ------- crates/pumpkin/src/command/args/hex_color.rs | 122 -- crates/pumpkin/src/command/args/message.rs | 66 - crates/pumpkin/src/command/args/mod.rs | 224 ---- crates/pumpkin/src/command/args/players.rs | 103 -- .../pumpkin/src/command/args/position_2d.rs | 79 -- .../pumpkin/src/command/args/position_3d.rs | 83 -- .../src/command/args/position_block.rs | 193 --- .../src/command/args/resource/damage_type.rs | 57 - .../src/command/args/resource/effect.rs | 58 - .../src/command/args/resource/enchantment.rs | 56 - .../pumpkin/src/command/args/resource/item.rs | 297 ----- .../pumpkin/src/command/args/resource/mod.rs | 5 - .../src/command/args/resource/particle.rs | 56 - .../src/command/args/resource_location.rs | 52 - crates/pumpkin/src/command/args/rotation.rs | 82 -- crates/pumpkin/src/command/args/simple.rs | 51 - crates/pumpkin/src/command/args/slot.rs | 99 -- crates/pumpkin/src/command/args/sound.rs | 67 - .../src/command/args/sound_category.rs | 84 -- .../src/command/args/summonable_entities.rs | 67 - crates/pumpkin/src/command/args/team_color.rs | 120 -- .../pumpkin/src/command/args/textcomponent.rs | 61 - crates/pumpkin/src/command/args/time.rs | 117 -- .../pumpkin/src/command/argument_builder.rs | 10 + .../command/argument_types/block_predicate.rs | 228 ++++ .../src/command/argument_types/component.rs | 54 + .../argument_types/coordinates/angle.rs | 88 ++ .../command/argument_types/coordinates/mod.rs | 3 + .../argument_types/coordinates/swizzle.rs | 88 ++ .../argument_types/coordinates/vec2.rs | 111 ++ .../argument_types/coordinates/vec3.rs | 136 +- .../argument_types/entity_selector/mod.rs | 9 +- .../src/command/argument_types/identifier.rs | 9 + .../src/command/argument_types/item.rs | 114 ++ .../command/argument_types/item_predicate.rs | 170 +++ .../pumpkin/src/command/argument_types/mod.rs | 8 + .../pumpkin/src/command/argument_types/nbt.rs | 6 +- .../src/command/argument_types/nbt_path.rs | 802 ++++++++++++ .../src/command/argument_types/particle.rs | 61 + .../src/command/argument_types/resource.rs | 161 +++ .../command/argument_types/sound_category.rs | 68 + .../pumpkin/src/command/client_suggestions.rs | 346 +---- crates/pumpkin/src/command/commands/ban.rs | 183 ++- crates/pumpkin/src/command/commands/banip.rs | 202 ++- .../pumpkin/src/command/commands/bossbar.rs | 799 +++++++----- crates/pumpkin/src/command/commands/clear.rs | 403 +++--- crates/pumpkin/src/command/commands/damage.rs | 220 ++-- crates/pumpkin/src/command/commands/data.rs | 1129 ++++++++++++++--- .../src/command/commands/defaultgamemode.rs | 70 +- crates/pumpkin/src/command/commands/deop.rs | 81 +- crates/pumpkin/src/command/commands/effect.rs | 512 ++++---- .../pumpkin/src/command/commands/enchant.rs | 260 ++-- .../pumpkin/src/command/commands/execute.rs | 1109 +++++++++++++++- .../src/command/commands/experience.rs | 218 ++-- crates/pumpkin/src/command/commands/fill.rs | 546 ++++---- .../pumpkin/src/command/commands/gamemode.rs | 119 +- .../pumpkin/src/command/commands/gamerule.rs | 177 +-- crates/pumpkin/src/command/commands/give.rs | 84 +- crates/pumpkin/src/command/commands/item.rs | 259 ++-- crates/pumpkin/src/command/commands/kick.rs | 86 +- crates/pumpkin/src/command/commands/me.rs | 65 +- crates/pumpkin/src/command/commands/mod.rs | 484 +------ crates/pumpkin/src/command/commands/msg.rs | 102 +- crates/pumpkin/src/command/commands/pardon.rs | 89 +- .../pumpkin/src/command/commands/pardonip.rs | 95 +- .../pumpkin/src/command/commands/particle.rs | 323 +++-- .../pumpkin/src/command/commands/playsound.rs | 497 +++++--- crates/pumpkin/src/command/commands/plugin.rs | 203 ++- .../pumpkin/src/command/commands/plugins.rs | 49 +- .../pumpkin/src/command/commands/pumpkin.rs | 38 +- crates/pumpkin/src/command/commands/raid.rs | 236 ++-- crates/pumpkin/src/command/commands/return.rs | 44 + crates/pumpkin/src/command/commands/rotate.rs | 240 ++-- .../pumpkin/src/command/commands/schedule.rs | 196 +++ .../pumpkin/src/command/commands/setblock.rs | 99 +- .../src/command/commands/setworldspawn.rs | 273 ++-- .../src/command/commands/spawnpoint.rs | 230 ++-- .../pumpkin/src/command/commands/spectate.rs | 234 ++-- .../src/command/commands/spreadplayers.rs | 83 +- .../pumpkin/src/command/commands/stopsound.rs | 124 +- .../pumpkin/src/command/commands/stopwatch.rs | 213 ++++ crates/pumpkin/src/command/commands/summon.rs | 123 +- .../pumpkin/src/command/commands/teleport.rs | 599 ++++----- .../pumpkin/src/command/commands/tellraw.rs | 56 +- crates/pumpkin/src/command/commands/time.rs | 386 +++--- crates/pumpkin/src/command/commands/title.rs | 176 ++- crates/pumpkin/src/command/commands/tps.rs | 38 +- .../pumpkin/src/command/commands/transfer.rs | 244 ++-- .../pumpkin/src/command/commands/waypoint.rs | 170 +-- .../pumpkin/src/command/commands/weather.rs | 141 +- .../pumpkin/src/command/commands/whitelist.rs | 280 ++-- .../src/command/commands/worldborder.rs | 877 ++++++------- .../src/command/context/command_source.rs | 6 + crates/pumpkin/src/command/dispatcher.rs | 969 +------------- crates/pumpkin/src/command/mod.rs | 37 +- crates/pumpkin/src/command/node/detached.rs | 4 + crates/pumpkin/src/command/node/dispatcher.rs | 154 +-- crates/pumpkin/src/command/node/mod.rs | 2 + crates/pumpkin/src/command/node/tree.rs | 12 + crates/pumpkin/src/command/tree/builder.rs | 206 --- crates/pumpkin/src/command/tree/format.rs | 122 -- crates/pumpkin/src/command/tree/mod.rs | 150 --- crates/pumpkin/src/entity/combat.rs | 494 +++++++- crates/pumpkin/src/entity/living.rs | 463 ++++--- crates/pumpkin/src/entity/player.rs | 33 +- crates/pumpkin/src/item/items/dye.rs | 3 + crates/pumpkin/src/item/items/honeycomb.rs | 9 +- crates/pumpkin/src/logging.rs | 111 +- crates/pumpkin/src/net/chat/mod.rs | 432 +++++++ .../pumpkin/src/net/java/play/chat_message.rs | 19 +- crates/pumpkin/src/net/java/play/mod.rs | 7 +- .../pumpkin/src/net/java/play/update_sign.rs | 24 +- crates/pumpkin/src/net/mod.rs | 1 + crates/pumpkin/src/plugin/api/context.rs | 54 +- .../src/plugin/loader/wasm/wasm_host/args.rs | 109 +- .../src/plugin/loader/wasm/wasm_host/state.rs | 100 +- .../wasm_host/wit/v0_1/commands/executor.rs | 122 +- .../wasm/wasm_host/wit/v0_1/commands/mod.rs | 211 +-- .../loader/wasm/wasm_host/wit/v0_1/context.rs | 14 +- crates/pumpkin/src/server/mod.rs | 246 +--- crates/pumpkin/src/server/scheduler.rs | 121 ++ crates/pumpkin/src/world/custom_bossbar.rs | 3 +- crates/pumpkin/src/world/mod.rs | 133 +- crates/pumpkin/src/world/stopwatches.rs | 168 +++ 142 files changed, 12194 insertions(+), 13278 deletions(-) delete mode 100644 crates/pumpkin/src/command/args/block.rs delete mode 100644 crates/pumpkin/src/command/args/bool.rs delete mode 100644 crates/pumpkin/src/command/args/bossbar_color.rs delete mode 100644 crates/pumpkin/src/command/args/bossbar_style.rs delete mode 100644 crates/pumpkin/src/command/args/bounded_num.rs delete mode 100644 crates/pumpkin/src/command/args/command.rs delete mode 100644 crates/pumpkin/src/command/args/coordinate.rs delete mode 100644 crates/pumpkin/src/command/args/difficulty.rs delete mode 100644 crates/pumpkin/src/command/args/entities.rs delete mode 100644 crates/pumpkin/src/command/args/entity.rs delete mode 100644 crates/pumpkin/src/command/args/entity_anchor.rs delete mode 100644 crates/pumpkin/src/command/args/gamemode.rs delete mode 100644 crates/pumpkin/src/command/args/gameprofile.rs delete mode 100644 crates/pumpkin/src/command/args/hex_color.rs delete mode 100644 crates/pumpkin/src/command/args/message.rs delete mode 100644 crates/pumpkin/src/command/args/mod.rs delete mode 100644 crates/pumpkin/src/command/args/players.rs delete mode 100644 crates/pumpkin/src/command/args/position_2d.rs delete mode 100644 crates/pumpkin/src/command/args/position_3d.rs delete mode 100644 crates/pumpkin/src/command/args/position_block.rs delete mode 100644 crates/pumpkin/src/command/args/resource/damage_type.rs delete mode 100644 crates/pumpkin/src/command/args/resource/effect.rs delete mode 100644 crates/pumpkin/src/command/args/resource/enchantment.rs delete mode 100644 crates/pumpkin/src/command/args/resource/item.rs delete mode 100644 crates/pumpkin/src/command/args/resource/mod.rs delete mode 100644 crates/pumpkin/src/command/args/resource/particle.rs delete mode 100644 crates/pumpkin/src/command/args/resource_location.rs delete mode 100644 crates/pumpkin/src/command/args/rotation.rs delete mode 100644 crates/pumpkin/src/command/args/simple.rs delete mode 100644 crates/pumpkin/src/command/args/slot.rs delete mode 100644 crates/pumpkin/src/command/args/sound.rs delete mode 100644 crates/pumpkin/src/command/args/sound_category.rs delete mode 100644 crates/pumpkin/src/command/args/summonable_entities.rs delete mode 100644 crates/pumpkin/src/command/args/team_color.rs delete mode 100644 crates/pumpkin/src/command/args/textcomponent.rs delete mode 100644 crates/pumpkin/src/command/args/time.rs create mode 100644 crates/pumpkin/src/command/argument_types/block_predicate.rs create mode 100644 crates/pumpkin/src/command/argument_types/component.rs create mode 100644 crates/pumpkin/src/command/argument_types/coordinates/angle.rs create mode 100644 crates/pumpkin/src/command/argument_types/coordinates/swizzle.rs create mode 100644 crates/pumpkin/src/command/argument_types/coordinates/vec2.rs create mode 100644 crates/pumpkin/src/command/argument_types/item.rs create mode 100644 crates/pumpkin/src/command/argument_types/item_predicate.rs create mode 100644 crates/pumpkin/src/command/argument_types/nbt_path.rs create mode 100644 crates/pumpkin/src/command/argument_types/particle.rs create mode 100644 crates/pumpkin/src/command/argument_types/sound_category.rs create mode 100644 crates/pumpkin/src/command/commands/return.rs create mode 100644 crates/pumpkin/src/command/commands/schedule.rs create mode 100644 crates/pumpkin/src/command/commands/stopwatch.rs delete mode 100644 crates/pumpkin/src/command/tree/builder.rs delete mode 100644 crates/pumpkin/src/command/tree/format.rs delete mode 100644 crates/pumpkin/src/command/tree/mod.rs create mode 100644 crates/pumpkin/src/net/chat/mod.rs create mode 100644 crates/pumpkin/src/world/stopwatches.rs diff --git a/crates/pumpkin-protocol/src/java/client/play/player_chat_message.rs b/crates/pumpkin-protocol/src/java/client/play/player_chat_message.rs index b14022065..d96030590 100644 --- a/crates/pumpkin-protocol/src/java/client/play/player_chat_message.rs +++ b/crates/pumpkin-protocol/src/java/client/play/player_chat_message.rs @@ -108,11 +108,14 @@ impl ClientPacket for CPlayerChatMessage { Ok(()) })?; write.write_option(&self.unsigned_content, |p, v| p.write_component(v, version))?; - write.write_var_int(&VarInt(match self.filter_type { - FilterType::PassThrough => 0, - FilterType::FullyFiltered => 1, - FilterType::PartiallyFiltered(_) => 2, - }))?; + match &self.filter_type { + FilterType::PassThrough => write.write_var_int(&VarInt(0))?, + FilterType::FullyFiltered => write.write_var_int(&VarInt(1))?, + FilterType::PartiallyFiltered(bit_set) => { + write.write_var_int(&VarInt(2))?; + bit_set.encode(&mut write)?; + } + } write.write_var_int(&self.chat_type)?; write.write_component(&self.sender_name, version)?; write.write_option(&self.target_name, |p, v| p.write_component(v, version))?; diff --git a/crates/pumpkin-world/benches/chunk_gen.rs b/crates/pumpkin-world/benches/chunk_gen.rs index 84684c850..999074271 100644 --- a/crates/pumpkin-world/benches/chunk_gen.rs +++ b/crates/pumpkin-world/benches/chunk_gen.rs @@ -64,7 +64,11 @@ fn setup_cache( world_gen: &WorldGenerator, block_registry: &dyn WorldPortalExt, ) -> Cache { - let radius = target_stage.get_direct_radius(); + let radius = if target_stage as u8 >= StagedChunkEnum::Surface as u8 { + 1 + } else { + target_stage.get_direct_radius() + }; let mut cache = Cache::new(-radius, -radius, radius * 2 + 1); for dx in -radius..=radius { diff --git a/crates/pumpkin/src/block/blocks/signs.rs b/crates/pumpkin/src/block/blocks/signs.rs index 32acba43a..e0ad4ddad 100644 --- a/crates/pumpkin/src/block/blocks/signs.rs +++ b/crates/pumpkin/src/block/blocks/signs.rs @@ -1,8 +1,9 @@ -use std::sync::Arc; -use std::sync::atomic::Ordering; +use std::sync::{Arc, Mutex}; use crate::block::entities::hanging_sign::HangingSignBlockEntity; -use crate::block::entities::sign::SignBlockEntity; +use crate::block::entities::sign::{SignBlockEntity, SignEntityRef, Text}; +use crate::command::CommandSender; +use crate::command::context::command_source::CommandSource; use pumpkin_data::Block; use pumpkin_data::BlockDirection; use pumpkin_data::BlockId; @@ -13,7 +14,10 @@ use pumpkin_data::tag::Taggable; use pumpkin_inventory::screen_handler::InventoryPlayer; use pumpkin_macros::pumpkin_block_from_tag; use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector2::Vector2; use pumpkin_util::math::vector3::Vector3; +use pumpkin_util::text::TextComponent; +use pumpkin_util::text::click::ClickEvent; use uuid::Uuid; use crate::block::BlockBehaviour; @@ -291,7 +295,6 @@ impl SignBlock { } } -//TODO: add support for click commands impl BlockBehaviour for SignBlock { fn on_place(&self, args: OnPlaceArgs<'_>) -> BlockStateId { let support = Self::detect_support(args.world, args.position); @@ -315,6 +318,17 @@ impl BlockBehaviour for SignBlock { } fn player_placed(&self, args: PlayerPlacedArgs<'_>) { + if let Some(block_entity) = args.world.get_block_entity(args.position) + && let Some(sign) = SignEntityRef::from_block_entity(&*block_entity) + { + open_text_edit( + args.player, + sign.currently_editing_player(), + args.position, + true, + ); + return; + } args.player .try_send_client_packet(&COpenSignEditor::new(*args.position, true)); } @@ -421,38 +435,50 @@ impl BlockBehaviour for SignBlock { let Some(block_entity) = args.world.get_block_entity(args.position) else { return BlockActionResult::Pass; }; - let Some(sign_entity) = block_entity.as_any().downcast_ref::() else { + let Some(sign_entity) = SignEntityRef::from_block_entity(&*block_entity) else { return BlockActionResult::Pass; }; - if sign_entity.is_waxed.load(Ordering::Relaxed) { + let is_front_text = + is_facing_front_text(args.world, args.position, args.block, args.player); + let text = sign_entity.get_text(is_front_text); + + let executed_click_command = + execute_click_commands_if_present(args.world, args.player, args.position, text); + + if sign_entity.is_waxed() { + let is_hanging = args.block.name.contains("hanging"); + let sound = if is_hanging { + pumpkin_data::sound::Sound::BlockHangingSignWaxedInteractFail + } else { + pumpkin_data::sound::Sound::BlockSignWaxedInteractFail + }; args.world.play_block_sound( - pumpkin_data::sound::Sound::BlockSignWaxedInteractFail, + sound, pumpkin_data::sound::SoundCategory::Blocks, *args.position, ); - return BlockActionResult::SuccessServer; - } - - let mut currently_editing = sign_entity - .currently_editing_player - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - if !try_claim_sign( - &mut currently_editing, - &args.player.gameprofile.id, + BlockActionResult::SuccessServer + } else if executed_click_command { + BlockActionResult::SuccessServer + } else if !other_player_is_editing_sign( + args.player, + sign_entity.currently_editing_player(), args.world, args.position, - ) { - return BlockActionResult::Pass; + ) && args.player.may_build() + && has_editable_text(text) + { + open_text_edit( + args.player, + sign_entity.currently_editing_player(), + args.position, + is_front_text, + ); + BlockActionResult::SuccessServer + } else { + BlockActionResult::Pass } - - let is_facing_front_text = - is_facing_front_text(args.world, args.position, args.block, args.player); - args.player - .try_send_client_packet(&COpenSignEditor::new(*args.position, is_facing_front_text)); - - BlockActionResult::SuccessServer } /// Handles use with an item on the sign block. @@ -460,33 +486,10 @@ impl BlockBehaviour for SignBlock { let Some(block_entity) = args.world.get_block_entity(args.position) else { return BlockActionResult::Pass; }; - let Some(sign_entity) = block_entity.as_any().downcast_ref::() else { + let Some(sign_entity) = SignEntityRef::from_block_entity(&*block_entity) else { return BlockActionResult::Pass; }; - if sign_entity.is_waxed.load(Ordering::Relaxed) { - return BlockActionResult::PassToDefaultBlockAction; - } - - let mut currently_editing = sign_entity - .currently_editing_player - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - if !try_claim_sign( - &mut currently_editing, - &args.player.gameprofile.id, - args.world, - args.position, - ) { - return BlockActionResult::PassToDefaultBlockAction; - } - - let text = if is_facing_front_text(args.world, args.position, args.block, args.player) { - &sign_entity.front_text - } else { - &sign_entity.back_text - }; - let Some(pumpkin_item) = args .server .item_registry @@ -495,62 +498,172 @@ impl BlockBehaviour for SignBlock { return BlockActionResult::PassToDefaultBlockAction; }; - let result = pumpkin_item - .as_any() - .downcast_ref::() - .map_or_else( - || { - pumpkin_item - .as_any() - .downcast_ref::() - .map_or_else( - || { - if let Some(ink_sac_item) = - pumpkin_item.as_any().downcast_ref::() - { - ink_sac_item.apply_to_sign(&args, &block_entity, text) - } else if let Some(dye) = - pumpkin_item.as_any().downcast_ref::() - { - let color_name = args - .item_stack - .item - .registry_key - .strip_suffix("_dye") - .unwrap_or(args.item_stack.item.registry_key); - dye.apply_to_sign(&args, &block_entity, text, color_name) - } else { - BlockActionResult::PassToDefaultBlockAction - } - }, - |g_ink_sac_item| { - g_ink_sac_item.apply_to_sign(&args, &block_entity, text) - }, - ) - }, - |honeycomb_item| honeycomb_item.apply_to_sign(&args, &block_entity, sign_entity), - ); + let is_applicator = pumpkin_item.as_any().is::() + || pumpkin_item.as_any().is::() + || pumpkin_item.as_any().is::() + || pumpkin_item.as_any().is::(); - if result == BlockActionResult::Success { - if pumpkin_item - .as_any() - .downcast_ref::() - .is_some() + let has_applicator_to_use = is_applicator && args.player.may_build(); + + if has_applicator_to_use + && !sign_entity.is_waxed() + && !other_player_is_editing_sign( + args.player, + sign_entity.currently_editing_player(), + args.world, + args.position, + ) + { + let is_front_text = + is_facing_front_text(args.world, args.position, args.block, args.player); + let text = sign_entity.get_text(is_front_text); + + let result = if let Some(honeycomb_item) = + pumpkin_item.as_any().downcast_ref::() { - args.player.trigger_advancement( - crate::entity::player::advancement::trigger::AdvancementTrigger::GlowedSign, - ); + honeycomb_item.apply_to_sign(&args, &block_entity, &sign_entity) + } else if let Some(g_ink_sac_item) = + pumpkin_item.as_any().downcast_ref::() + { + g_ink_sac_item.apply_to_sign(&args, &block_entity, text) + } else if let Some(ink_sac_item) = pumpkin_item.as_any().downcast_ref::() { + ink_sac_item.apply_to_sign(&args, &block_entity, text) + } else if let Some(dye) = pumpkin_item.as_any().downcast_ref::() { + let color_name = args + .item_stack + .item + .registry_key + .strip_suffix("_dye") + .unwrap_or(args.item_stack.item.registry_key); + dye.apply_to_sign(&args, &block_entity, text, color_name) + } else { + BlockActionResult::PassToDefaultBlockAction + }; + + if result == BlockActionResult::Success { + execute_click_commands_if_present(args.world, args.player, args.position, text); + if pumpkin_item.as_any().is::() { + args.player.trigger_advancement( + crate::entity::player::advancement::trigger::AdvancementTrigger::GlowedSign, + ); + } + if !args.player.has_infinite_materials() { + args.item_stack.decrement(1); + } + return BlockActionResult::Success; } - if !args.player.has_infinite_materials() { - args.item_stack.decrement(1); - } - *currently_editing = None; } - result + BlockActionResult::PassToDefaultBlockAction } } +/// Opens the sign text edit interface for the player and registers them as allowed editor. +fn open_text_edit( + player: &Player, + currently_editing_player: &Arc>>, + position: &BlockPos, + is_front_text: bool, +) { + *currently_editing_player + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(player.gameprofile.id); + player.try_send_client_packet(&COpenSignEditor::new(*position, is_front_text)); +} + +/// Checks if another player is actively editing the sign within reach. +fn other_player_is_editing_sign( + player: &Player, + currently_editing_player: &Arc>>, + world: &World, + position: &BlockPos, +) -> bool { + let currently_editing = currently_editing_player + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + if let Some(editing_player_id) = *currently_editing + && editing_player_id != player.gameprofile.id + && let Some(editing_player) = world.get_player_by_uuid(editing_player_id) + && editing_player.can_interact_with_block_at(position, 4.0) + { + return true; + } + false +} + +/// Checks whether all messages on the given sign text face are plain text or empty. +fn has_editable_text(text: &Text) -> bool { + let messages = text + .messages + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + messages.iter().all(|msg| is_plain_or_empty_text(msg)) +} + +fn is_plain_or_empty_text(text: &str) -> bool { + if text.is_empty() { + return true; + } + if !text.starts_with('{') { + return true; + } + match serde_json::from_str::(text) { + Ok(component) => { + component.0.style.click_event.is_none() + && component.0.style.hover_event.is_none() + && component.0.extra.is_empty() + && matches!( + *component.0.content, + pumpkin_util::text::TextContent::Text { .. } + ) + } + Err(_) => true, + } +} + +/// Executes any `run_command` click events defined in the sign's text messages. +fn execute_click_commands_if_present( + world: &Arc, + player: &Arc, + position: &BlockPos, + text: &Text, +) -> bool { + let Some(server) = world.server.upgrade() else { + return false; + }; + + let messages = text + .messages + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + + let mut has_run_command = false; + for msg in messages.iter() { + if msg.is_empty() || !msg.starts_with('{') { + continue; + } + if let Ok(component) = serde_json::from_str::(msg) + && let Some(ClickEvent::RunCommand { command }) = &component.0.style.click_event + { + let source = CommandSource::new( + CommandSender::Dummy, + world.clone(), + Some(player.clone()), + position.to_centered_f64(), + Vector2::new(0.0, 0.0), + player.gameprofile.name.clone(), + player.get_display_name(), + server.clone(), + ); + let command_str = command.strip_prefix('/').unwrap_or(command); + let dispatcher = server.command_dispatcher.load(); + dispatcher.handle_command(&source, command_str); + has_run_command = true; + } + } + has_run_command +} + /// Returns the direction of the block supporting the wall sign. fn get_wall_support_direction(block: &Block, state_id: BlockStateId) -> Option { block.properties(state_id).and_then(|props| { @@ -625,23 +738,3 @@ fn is_facing_front_text( fn get_yaw_from_rotation_16(rotation: u8) -> f32 { f32::from(rotation) * 22.5 } - -fn try_claim_sign( - currently_editing: &mut Option, - uuid: &Uuid, - world: &World, - position: &BlockPos, -) -> bool { - if let Some(editing_player_id) = *currently_editing - && editing_player_id != *uuid - && let Some(editing_player) = world.get_player_by_uuid(editing_player_id) - && editing_player - .as_ref() - .can_interact_with_block_at(position, 4.0f64) - { - return false; - } - - *currently_editing = Some(*uuid); - true -} diff --git a/crates/pumpkin/src/block/entities/hanging_sign.rs b/crates/pumpkin/src/block/entities/hanging_sign.rs index cd4080a9c..2bfbfe19d 100644 --- a/crates/pumpkin/src/block/entities/hanging_sign.rs +++ b/crates/pumpkin/src/block/entities/hanging_sign.rs @@ -77,14 +77,14 @@ impl HangingSignBlockEntity { position, is_waxed: AtomicBool::new(false), front_text: if is_front { - Text::new(messages.clone()) + Text::from_messages(messages.clone()) } else { Text::default() }, back_text: if is_front { Text::default() } else { - Text::new(messages) + Text::from_messages(messages) }, currently_editing_player: Arc::new(Mutex::new(None)), } diff --git a/crates/pumpkin/src/block/entities/sign.rs b/crates/pumpkin/src/block/entities/sign.rs index 2f9e88973..248dd418a 100644 --- a/crates/pumpkin/src/block/entities/sign.rs +++ b/crates/pumpkin/src/block/entities/sign.rs @@ -1,73 +1,93 @@ use std::sync::{ - Arc, + Arc, Mutex, atomic::{AtomicBool, AtomicI8, Ordering}, }; use super::BlockEntity; use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag}; use pumpkin_util::math::position::BlockPos; -use std::sync::Mutex; +use pumpkin_util::text::TextComponent; +use pumpkin_util::text::click::ClickEvent; pub use pumpkin_data::dye_color::DyeColor; pub struct SignBlockEntity { - pub front_text: Text, - pub back_text: Text, + pub front_text: SignText, + pub back_text: SignText, pub is_waxed: AtomicBool, position: BlockPos, pub currently_editing_player: Arc>>, } -pub struct Text { +pub type Text = SignText; + +pub struct SignText { pub has_glowing_text: AtomicBool, color: AtomicI8, - pub messages: Arc; 4]>>, + pub messages: Arc; 4]>>, + pub filtered_messages: Arc; 4]>>, } -impl Clone for Text { +impl Clone for SignText { fn clone(&self) -> Self { Self { has_glowing_text: AtomicBool::new(self.has_glowing_text.load(Ordering::Relaxed)), color: AtomicI8::new(self.color.load(Ordering::Relaxed)), messages: self.messages.clone(), + filtered_messages: self.filtered_messages.clone(), } } } -impl Default for Text { +impl Default for SignText { fn default() -> Self { Self { has_glowing_text: AtomicBool::new(false), - color: AtomicI8::new(DyeColor::default() as i8), - messages: Arc::default(), + color: AtomicI8::new(DyeColor::Black as i8), + messages: Arc::new(Mutex::new(Self::empty_messages())), + filtered_messages: Arc::new(Mutex::new(Self::empty_messages())), } } } #[allow(clippy::fallible_impl_from)] -impl From for NbtTag { - fn from(value: Text) -> Self { +impl From for NbtTag { + fn from(value: SignText) -> Self { let mut nbt = NbtCompound::new(); nbt.put_bool( "has_glowing_text", value.has_glowing_text.load(Ordering::Relaxed), ); nbt.put_string("color", value.get_color().name().to_string()); + + let messages = value + .messages + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); nbt.put_list( "messages", - value - .messages - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .iter() - .map(|s| Self::String(s.clone())) - .collect(), + messages.iter().map(|s| Self::String(s.clone())).collect(), ); + + let filtered_messages = value + .filtered_messages + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + if *filtered_messages != *messages { + nbt.put_list( + "filtered_messages", + filtered_messages + .iter() + .map(|s| Self::String(s.clone())) + .collect(), + ); + } + Self::Compound(nbt) } } -impl From for Text { +impl From for SignText { fn from(tag: NbtTag) -> Self { let Some(nbt) = tag.extract_compound() else { return Self::default(); @@ -82,41 +102,155 @@ impl From for Text { let get_message = |i: usize| -> Box { messages.get(i).cloned().unwrap_or_else(|| Box::from("")) }; + let parsed_messages = [ + get_message(0), + get_message(1), + get_message(2), + get_message(3), + ]; + + let filtered_messages: Option>> = + nbt.get_list("filtered_messages").map(|list| { + list.iter() + .filter_map(|tag| tag.extract_string().map(Box::from)) + .collect() + }); + + let parsed_filtered = filtered_messages.map_or_else( + || parsed_messages.clone(), + |filtered| { + let get_filtered = |i: usize| -> Box { + filtered.get(i).cloned().unwrap_or_else(|| Box::from("")) + }; + [ + get_filtered(0), + get_filtered(1), + get_filtered(2), + get_filtered(3), + ] + }, + ); + Self { has_glowing_text: AtomicBool::new(has_glowing_text), - color: AtomicI8::new(DyeColor::by_name(color).unwrap_or_default().id() as i8), - messages: Arc::new(std::sync::Mutex::new([ - get_message(0), - get_message(1), - get_message(2), - get_message(3), - ])), + color: AtomicI8::new(DyeColor::by_name(color).unwrap_or(DyeColor::Black).id() as i8), + messages: Arc::new(Mutex::new(parsed_messages)), + filtered_messages: Arc::new(Mutex::new(parsed_filtered)), } } } -impl Text { +impl SignText { + pub const LINES: usize = 4; + #[must_use] - pub fn new(messages: [Box; 4]) -> Self { + pub fn empty_messages() -> [Box; 4] { + [Box::from(""), Box::from(""), Box::from(""), Box::from("")] + } + + #[must_use] + pub fn new( + messages: [Box; 4], + filtered_messages: Option<[Box; 4]>, + color: DyeColor, + has_glowing_text: bool, + ) -> Self { + let filtered = filtered_messages.unwrap_or_else(|| messages.clone()); Self { - has_glowing_text: AtomicBool::new(false), - color: AtomicI8::new(DyeColor::default().id() as i8), - messages: Arc::new(std::sync::Mutex::new(messages)), + has_glowing_text: AtomicBool::new(has_glowing_text), + color: AtomicI8::new(color.id() as i8), + messages: Arc::new(Mutex::new(messages)), + filtered_messages: Arc::new(Mutex::new(filtered)), } } + #[must_use] + pub fn from_messages(messages: [Box; 4]) -> Self { + Self::new(messages, None, DyeColor::Black, false) + } + + #[must_use] + pub fn has_glowing_text(&self) -> bool { + self.has_glowing_text.load(Ordering::Relaxed) + } + + pub fn set_has_glowing_text(&self, has_glowing_text: bool) { + self.has_glowing_text + .store(has_glowing_text, Ordering::Relaxed); + } + + #[must_use] pub fn get_color(&self) -> DyeColor { let c = self.color.load(Ordering::Relaxed); if c >= 0 { - DyeColor::by_id(c as u8).unwrap_or_default() + DyeColor::by_id(c as u8).unwrap_or(DyeColor::Black) } else { - DyeColor::default() + DyeColor::Black } } pub fn set_color(&self, color: DyeColor) { self.color.store(color.id() as i8, Ordering::Relaxed); } + + #[must_use] + pub fn get_message(&self, index: usize, should_filter: bool) -> Box { + if index >= Self::LINES { + return Box::from(""); + } + let lock = if should_filter { + self.filtered_messages.lock() + } else { + self.messages.lock() + }; + lock.unwrap_or_else(std::sync::PoisonError::into_inner)[index].clone() + } + + pub fn set_message(&self, index: usize, message: Box, filtered_message: Option>) { + if index >= Self::LINES { + return; + } + let filtered = filtered_message.unwrap_or_else(|| message.clone()); + self.messages + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner)[index] = message; + self.filtered_messages + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner)[index] = filtered; + } + + #[must_use] + pub fn get_messages(&self, should_filter: bool) -> [Box; 4] { + let lock = if should_filter { + self.filtered_messages.lock() + } else { + self.messages.lock() + }; + lock.unwrap_or_else(std::sync::PoisonError::into_inner) + .clone() + } + + #[must_use] + pub fn has_message(&self, should_filter: bool) -> bool { + let messages = self.get_messages(should_filter); + messages.iter().any(|msg| !msg.is_empty()) + } + + #[must_use] + pub fn has_any_click_commands(&self, should_filter: bool) -> bool { + let messages = self.get_messages(should_filter); + for msg in &messages { + if msg.is_empty() || !msg.starts_with('{') { + continue; + } + if let Ok(component) = serde_json::from_str::(msg) + && let Some(ClickEvent::RunCommand { .. }) = &component.0.style.click_event + { + return true; + } + } + false + } } impl BlockEntity for SignBlockEntity { @@ -135,12 +269,12 @@ impl BlockEntity for SignBlockEntity { let front_text = nbt .get("front_text") .cloned() - .map(Text::from) + .map(SignText::from) .unwrap_or_default(); let back_text = nbt .get("back_text") .cloned() - .map(Text::from) + .map(SignText::from) .unwrap_or_default(); let is_waxed = nbt.get_bool("is_waxed").unwrap_or(false); Self { @@ -173,32 +307,102 @@ impl BlockEntity for SignBlockEntity { impl SignBlockEntity { pub const ID: &'static str = "minecraft:sign"; + #[must_use] pub fn new(position: BlockPos, is_front: bool, messages: [Box; 4]) -> Self { Self { position, is_waxed: AtomicBool::new(false), front_text: if is_front { - Text::new(messages.clone()) + SignText::from_messages(messages.clone()) } else { - Text::default() + SignText::default() }, back_text: if is_front { - Text::default() + SignText::default() } else { - Text::new(messages) + SignText::from_messages(messages) }, currently_editing_player: Arc::new(Mutex::new(None)), } } + #[must_use] pub fn empty(position: BlockPos) -> Self { Self { position, is_waxed: AtomicBool::new(false), - front_text: Text::default(), - back_text: Text::default(), + front_text: SignText::default(), + back_text: SignText::default(), currently_editing_player: Arc::new(Mutex::new(None)), } } } + +pub enum SignEntityRef<'a> { + Sign(&'a SignBlockEntity), + Hanging(&'a super::hanging_sign::HangingSignBlockEntity), +} + +impl<'a> SignEntityRef<'a> { + pub fn from_block_entity(entity: &'a dyn BlockEntity) -> Option { + entity + .as_any() + .downcast_ref::() + .map(Self::Sign) + .or_else(|| { + entity + .as_any() + .downcast_ref::() + .map(Self::Hanging) + }) + } + + #[must_use] + pub const fn front_text(&self) -> &'a SignText { + match self { + Self::Sign(s) => &s.front_text, + Self::Hanging(s) => &s.front_text, + } + } + + #[must_use] + pub const fn back_text(&self) -> &'a SignText { + match self { + Self::Sign(s) => &s.back_text, + Self::Hanging(s) => &s.back_text, + } + } + + #[must_use] + pub const fn get_text(&self, is_front: bool) -> &'a SignText { + if is_front { + self.front_text() + } else { + self.back_text() + } + } + + #[must_use] + pub fn is_waxed(&self) -> bool { + match self { + Self::Sign(s) => s.is_waxed.load(Ordering::Relaxed), + Self::Hanging(s) => s.is_waxed.load(Ordering::Relaxed), + } + } + + pub fn set_waxed(&self, waxed: bool) { + match self { + Self::Sign(s) => s.is_waxed.store(waxed, Ordering::Relaxed), + Self::Hanging(s) => s.is_waxed.store(waxed, Ordering::Relaxed), + } + } + + #[must_use] + pub const fn currently_editing_player(&self) -> &'a Arc>> { + match self { + Self::Sign(s) => &s.currently_editing_player, + Self::Hanging(s) => &s.currently_editing_player, + } + } +} diff --git a/crates/pumpkin/src/command/args/block.rs b/crates/pumpkin/src/command/args/block.rs deleted file mode 100644 index 31b7df439..000000000 --- a/crates/pumpkin/src/command/args/block.rs +++ /dev/null @@ -1,150 +0,0 @@ -use pumpkin_data::tag::{RegistryKey, get_tag_ids}; -use pumpkin_data::{Block, BlockId, translation}; -use pumpkin_protocol::java::client::play::{ArgumentType, SuggestionProviders}; -use pumpkin_util::text::TextComponent; - -use crate::command::args::ConsumeResult; -use crate::{command::dispatcher::CommandError, server::Server}; - -use super::{ - super::{ - CommandSender, - args::{ArgumentConsumer, RawArgs}, - }, - Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser, -}; - -pub struct BlockArgumentConsumer; - -impl GetClientSideArgParser for BlockArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::BlockState - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - None - } -} - -impl ArgumentConsumer for BlockArgumentConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - args.pop().map(|arg| Arg::Block(arg.value)) - } -} - -impl DefaultNameArgConsumer for BlockArgumentConsumer { - fn default_name(&self) -> &'static str { - "block" - } -} - -impl<'a> FindArg<'a> for BlockArgumentConsumer { - type Data = &'static Block; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::Block(name)) => Block::from_name(name).map_or_else( - || { - if name.starts_with("minecraft:") { - Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::ARGUMENT_BLOCK_ID_INVALID, - translation::java::ARGUMENT_BLOCK_ID_INVALID, - [TextComponent::text((*name).to_string())], - ))) - } else { - Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::ARGUMENT_BLOCK_ID_INVALID, - translation::java::ARGUMENT_BLOCK_ID_INVALID, - [TextComponent::text("minecraft:".to_string() + *name)], - ))) - } - }, - Result::Ok, - ), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} - -pub struct BlockPredicateArgumentConsumer; -#[derive(Debug)] -pub enum BlockPredicate { - Tag(Vec), - Block(BlockId), -} - -impl GetClientSideArgParser for BlockPredicateArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::BlockPredicate - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - None - } -} - -impl ArgumentConsumer for BlockPredicateArgumentConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - args.pop().map(|arg| Arg::BlockPredicate(arg.value)) - } -} - -impl DefaultNameArgConsumer for BlockPredicateArgumentConsumer { - fn default_name(&self) -> &'static str { - "filter" - } -} - -impl<'a> FindArg<'a> for BlockPredicateArgumentConsumer { - type Data = Option; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::BlockPredicate(name)) => name.strip_prefix("#").map_or_else( - || { - Block::from_name(name).map_or_else( - || { - if name.starts_with("minecraft:") { - Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::ARGUMENT_BLOCK_ID_INVALID, - translation::java::ARGUMENT_BLOCK_ID_INVALID, - [TextComponent::text((*name).to_string())], - ))) - } else { - Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::ARGUMENT_BLOCK_ID_INVALID, - translation::java::ARGUMENT_BLOCK_ID_INVALID, - [TextComponent::text("minecraft:".to_string() + *name)], - ))) - } - }, - |block| Ok(Some(BlockPredicate::Block(block.id))), - ) - }, - |tag| { - get_tag_ids(RegistryKey::Block, tag).map_or_else( - || { - Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::ARGUMENTS_BLOCK_TAG_UNKNOWN, - translation::java::ARGUMENTS_BLOCK_TAG_UNKNOWN, - [TextComponent::text((*tag).to_string())], - ))) - }, - |blocks| Ok(Some(BlockPredicate::Tag(blocks.to_vec()))), - ) - }, - ), - _ => Ok(None), - } - } -} diff --git a/crates/pumpkin/src/command/args/bool.rs b/crates/pumpkin/src/command/args/bool.rs deleted file mode 100644 index 1301959da..000000000 --- a/crates/pumpkin/src/command/args/bool.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::command::CommandSender; -use crate::command::args::{Arg, ArgumentConsumer, ConsumeResult, FindArg, GetClientSideArgParser}; -use crate::command::dispatcher::CommandError; -use crate::command::tree::RawArgs; -use crate::server::Server; -use pumpkin_protocol::java::client::play::{ArgumentType, SuggestionProviders}; - -pub struct BoolArgConsumer; - -impl GetClientSideArgParser for BoolArgConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::Bool - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - None - } -} - -impl ArgumentConsumer for BoolArgConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let s_opt: Option<&'a str> = args.pop().map(|arg| arg.value); - - s_opt.and_then(|s| match s { - "false" => Some(Arg::Bool(false)), - "true" => Some(Arg::Bool(true)), - _ => None, - }) - } -} - -impl<'a> FindArg<'a> for BoolArgConsumer { - type Data = bool; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::Bool(data)) => Ok(*data), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} diff --git a/crates/pumpkin/src/command/args/bossbar_color.rs b/crates/pumpkin/src/command/args/bossbar_color.rs deleted file mode 100644 index fc360d652..000000000 --- a/crates/pumpkin/src/command/args/bossbar_color.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::command::CommandSender; -use crate::command::args::{ - Arg, ArgumentConsumer, ConsumeResult, DefaultNameArgConsumer, FindArg, GetClientSideArgParser, - SuggestResult, -}; -use crate::command::dispatcher::CommandError; -use crate::command::tree::RawArgs; -use crate::server::Server; -use crate::world::bossbar::BossbarColor; -use pumpkin_protocol::java::client::play::{ArgumentType, CommandSuggestion, SuggestionProviders}; - -pub struct BossbarColorArgumentConsumer; - -impl GetClientSideArgParser for BossbarColorArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - // Not sure if this is right... - ArgumentType::ResourceLocation - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - Some(SuggestionProviders::AskServer) - } -} - -impl ArgumentConsumer for BossbarColorArgumentConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let s_opt: Option<&'a str> = args.pop().map(|arg| arg.value); - - let color = match s_opt? { - "blue" => Some(BossbarColor::Blue), - "green" => Some(BossbarColor::Green), - "pink" => Some(BossbarColor::Pink), - "purple" => Some(BossbarColor::Purple), - "red" => Some(BossbarColor::Red), - "white" => Some(BossbarColor::White), - "yellow" => Some(BossbarColor::Yellow), - _ => None, - }; - - color.map(Arg::BossbarColor) - } - - fn suggest(&self, _sender: &CommandSender, _server: &Server, _input: &str) -> SuggestResult { - let colors = ["blue", "green", "pink", "purple", "red", "white", "yellow"]; - let suggestions: Vec = colors - .iter() - .map(|color| CommandSuggestion::new((*color).to_string(), None)) - .collect(); - Ok(Some(suggestions)) - } -} - -impl DefaultNameArgConsumer for BossbarColorArgumentConsumer { - fn default_name(&self) -> &'static str { - "color" - } -} - -impl<'a> FindArg<'a> for BossbarColorArgumentConsumer { - type Data = &'a BossbarColor; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::BossbarColor(data)) => Ok(data), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} diff --git a/crates/pumpkin/src/command/args/bossbar_style.rs b/crates/pumpkin/src/command/args/bossbar_style.rs deleted file mode 100644 index 9df88047b..000000000 --- a/crates/pumpkin/src/command/args/bossbar_style.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::command::CommandSender; -use crate::command::args::{ - Arg, ArgumentConsumer, ConsumeResult, DefaultNameArgConsumer, FindArg, GetClientSideArgParser, - SuggestResult, -}; -use crate::command::dispatcher::CommandError; -use crate::command::tree::RawArgs; -use crate::server::Server; -use crate::world::bossbar::BossbarDivisions; -use pumpkin_protocol::java::client::play::{ArgumentType, CommandSuggestion, SuggestionProviders}; - -pub struct BossbarStyleArgumentConsumer; - -impl GetClientSideArgParser for BossbarStyleArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - // Not sure if this is right... - ArgumentType::ResourceLocation - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - Some(SuggestionProviders::AskServer) - } -} - -impl ArgumentConsumer for BossbarStyleArgumentConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let s_opt: Option<&'a str> = args.pop().map(|arg| arg.value); - - let style = match s_opt? { - "notched_10" => Some(BossbarDivisions::Notches10), - "notched_12" => Some(BossbarDivisions::Notches12), - "notched_20" => Some(BossbarDivisions::Notches20), - "notched_6" => Some(BossbarDivisions::Notches6), - "progress" => Some(BossbarDivisions::NoDivision), - _ => None, - }; - - style.map(Arg::BossbarStyle) - } - - fn suggest(&self, _sender: &CommandSender, _server: &Server, _input: &str) -> SuggestResult { - let styles = [ - "notched_10", - "notched_12", - "notched_20", - "notched_6", - "progress", - ]; - let suggestions: Vec = styles - .iter() - .map(|style| CommandSuggestion::new((*style).to_string(), None)) - .collect(); - Ok(Some(suggestions)) - } -} - -impl DefaultNameArgConsumer for BossbarStyleArgumentConsumer { - fn default_name(&self) -> &'static str { - "style" - } -} - -impl<'a> FindArg<'a> for BossbarStyleArgumentConsumer { - type Data = &'a BossbarDivisions; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::BossbarStyle(data)) => Ok(data), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} diff --git a/crates/pumpkin/src/command/args/bounded_num.rs b/crates/pumpkin/src/command/args/bounded_num.rs deleted file mode 100644 index b45da14ee..000000000 --- a/crates/pumpkin/src/command/args/bounded_num.rs +++ /dev/null @@ -1,329 +0,0 @@ -use core::f64; -use std::fmt::{Display, Formatter}; -use std::str::FromStr; - -use pumpkin_protocol::java::client::play::ArgumentType; -use pumpkin_util::text::TextComponent; - -use crate::command::CommandSender; -use crate::command::args::ConsumeResult; -use crate::command::dispatcher::CommandError; -use crate::command::tree::RawArgs; -use crate::server::Server; - -use super::super::args::ArgumentConsumer; -use super::{Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser}; - -/// Consumes a single generic num, but only if it's in bounds. -pub struct BoundedNumArgumentConsumer { - min_inclusive: Option, - max_inclusive: Option, - name: Option<&'static str>, -} - -impl ArgumentConsumer for BoundedNumArgumentConsumer -where - Self: GetClientSideArgParser, -{ - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let s_opt: Option<&'a str> = args.pop().map(|arg| arg.value); - - s_opt - // Replace args.pop()?.parse::().ok()? - .and_then(|s| s.parse::().ok()) - .map(|x| { - // Check Upper Bound (max_inclusive) - if let Some(max) = self.max_inclusive - && x > max - { - return Arg::Num(Err(NotInBounds::UpperBound(x.to_number(), max.to_number()))); - } - - // Check Lower Bound (min_inclusive) - if let Some(min) = self.min_inclusive - && x < min - { - return Arg::Num(Err(NotInBounds::LowerBound(x.to_number(), min.to_number()))); - } - - // Success case - Arg::Num(Ok(x.to_number())) - }) - } -} - -impl<'a, T: 'static + ToFromNumber> FindArg<'a> for BoundedNumArgumentConsumer { - type Data = Result; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::Num(data)) => match data { - Ok(num) => T::from_number(num).map_or_else( - || Err(CommandError::InvalidConsumption(Some(name.to_string()))), - |x| Ok(Ok(x)), - ), - Err(err) => Ok(Err(*err)), - }, - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} - -#[derive(Debug, Copy, Clone)] -pub enum NotInBounds { - /// Number is lower than the lower bound - LowerBound(Number, Number), - /// Number is higher then the upper bound - UpperBound(Number, Number), -} - -impl From for CommandError { - fn from(value: NotInBounds) -> Self { - match value { - NotInBounds::LowerBound(val, min) => { - let (key, min_text, val_text) = match val { - Number::F64(_) | Number::F32(_) => ( - pumpkin_data::translation::java::ARGUMENT_DOUBLE_LOW, - min.to_string(), - val.to_string(), - ), - Number::I32(_) => ( - pumpkin_data::translation::java::ARGUMENT_INTEGER_LOW, - min.to_string(), - val.to_string(), - ), - Number::I64(_) => ( - pumpkin_data::translation::java::ARGUMENT_LONG_LOW, - min.to_string(), - val.to_string(), - ), - }; - Self::CommandFailed(TextComponent::translate_cross( - key, - key, - [TextComponent::text(min_text), TextComponent::text(val_text)], - )) - } - NotInBounds::UpperBound(val, max) => { - let (key, max_text, val_text) = match val { - Number::F64(_) | Number::F32(_) => ( - pumpkin_data::translation::java::ARGUMENT_DOUBLE_BIG, - max.to_string(), - val.to_string(), - ), - Number::I32(_) => ( - pumpkin_data::translation::java::ARGUMENT_INTEGER_BIG, - max.to_string(), - val.to_string(), - ), - Number::I64(_) => ( - pumpkin_data::translation::java::ARGUMENT_LONG_BIG, - max.to_string(), - val.to_string(), - ), - }; - Self::CommandFailed(TextComponent::translate_cross( - key, - key, - [TextComponent::text(max_text), TextComponent::text(val_text)], - )) - } - } - } -} - -#[derive(Clone, Copy, Debug)] -pub enum Number { - F64(f64), - F32(f32), - I32(i32), - I64(i64), -} - -impl Number { - #[must_use] - pub const fn qualifier(&self) -> &'static str { - match self { - Self::F64(_) | Self::F32(_) => "Float", - Self::I32(_) | Self::I64(_) => "Integer", - } - } -} - -impl Display for Number { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::F64(v) => write!(f, "{v}"), - Self::F32(v) => write!(f, "{v}"), - Self::I32(v) => write!(f, "{v}"), - Self::I64(v) => write!(f, "{v}"), - } - } -} - -impl BoundedNumArgumentConsumer { - #[must_use] - pub const fn new() -> Self { - Self { - min_inclusive: None, - max_inclusive: None, - name: None, - } - } - - #[must_use] - pub const fn min(mut self, min_inclusive: T) -> Self { - self.min_inclusive = Some(min_inclusive); - self - } - - #[must_use] - pub const fn max(mut self, max_inclusive: T) -> Self { - self.max_inclusive = Some(max_inclusive); - self - } - - #[must_use] - pub const fn name(mut self, name: &'static str) -> Self { - self.name = Some(name); - self - } -} - -pub trait ToFromNumber: PartialOrd + Copy + Send + Sync + FromStr { - fn to_number(self) -> Number; - fn from_number(arg: &Number) -> Option; -} - -impl Default for BoundedNumArgumentConsumer { - fn default() -> Self { - Self::new() - } -} - -impl ToFromNumber for f64 { - fn to_number(self) -> Number { - Number::F64(self) - } - - fn from_number(arg: &Number) -> Option { - match arg { - Number::F64(x) => Some(*x), - _ => None, - } - } -} - -impl GetClientSideArgParser for BoundedNumArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::Double { - min: self.min_inclusive, - max: self.max_inclusive, - } - } - - fn get_client_side_suggestion_type_override( - &self, - ) -> Option { - None - } -} - -impl ToFromNumber for f32 { - fn to_number(self) -> Number { - Number::F32(self) - } - - fn from_number(arg: &Number) -> Option { - match arg { - Number::F32(x) => Some(*x), - _ => None, - } - } -} - -impl GetClientSideArgParser for BoundedNumArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::Float { - min: self.min_inclusive, - max: self.max_inclusive, - } - } - - fn get_client_side_suggestion_type_override( - &self, - ) -> Option { - None - } -} - -impl ToFromNumber for i32 { - fn to_number(self) -> Number { - Number::I32(self) - } - - fn from_number(arg: &Number) -> Option { - match arg { - Number::I32(x) => Some(*x), - _ => None, - } - } -} - -impl GetClientSideArgParser for BoundedNumArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::Integer { - min: self.min_inclusive, - max: self.max_inclusive, - } - } - - fn get_client_side_suggestion_type_override( - &self, - ) -> Option { - None - } -} - -impl ToFromNumber for i64 { - fn to_number(self) -> Number { - Number::I64(self) - } - - fn from_number(arg: &Number) -> Option { - match arg { - Number::I64(x) => Some(*x), - _ => None, - } - } -} - -impl GetClientSideArgParser for BoundedNumArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::Long { - min: self.min_inclusive, - max: self.max_inclusive, - } - } - - fn get_client_side_suggestion_type_override( - &self, - ) -> Option { - None - } -} - -impl DefaultNameArgConsumer for BoundedNumArgumentConsumer -where - Self: ArgumentConsumer, -{ - fn default_name(&self) -> &'static str { - // setting a single default name for all BoundedNumArgumentConsumer variants is probably a bad idea since it would lead to confusion - self.name.expect("Only use *_default variants of methods with a BoundedNumArgumentConsumer that has a name.") - } -} diff --git a/crates/pumpkin/src/command/args/command.rs b/crates/pumpkin/src/command/args/command.rs deleted file mode 100644 index 52cc6fcf1..000000000 --- a/crates/pumpkin/src/command/args/command.rs +++ /dev/null @@ -1,79 +0,0 @@ -use pumpkin_protocol::java::client::play::{ - ArgumentType, CommandSuggestion, StringProtoArgBehavior, SuggestionProviders, -}; - -use crate::{ - command::{ - CommandSender, - args::{ConsumeResult, SplitSingleWhitespaceIncludingEmptyParts, SuggestResult}, - dispatcher::CommandError, - tree::{CommandTree, RawArgs}, - }, - server::Server, -}; - -use super::{Arg, ArgumentConsumer, DefaultNameArgConsumer, FindArg, GetClientSideArgParser}; - -pub struct CommandTreeArgumentConsumer; - -impl GetClientSideArgParser for CommandTreeArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::String(StringProtoArgBehavior::SingleWord) - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - Some(SuggestionProviders::AskServer) - } -} - -impl ArgumentConsumer for CommandTreeArgumentConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let s = args.pop().map(|arg| arg.value)?; - - let dispatcher = server.command_dispatcher.load(); - - dispatcher - .fallback_dispatcher - .get_tree(s) - .ok() - .map(|tree| Arg::CommandTree(tree.clone())) - } - - fn suggest(&self, _sender: &CommandSender, server: &Server, input: &str) -> SuggestResult { - let Some(input) = input.split_single_whitespace_including_empty_parts().last() else { - return Ok(None); - }; - - let dispatcher = server.command_dispatcher.load(); - let suggestions = dispatcher - .fallback_dispatcher - .commands - .keys() - .filter(|suggestion| suggestion.starts_with(input)) - .map(|suggestion| CommandSuggestion::new(suggestion.clone(), None)) - .collect(); - Ok(Some(suggestions)) - } -} - -impl DefaultNameArgConsumer for CommandTreeArgumentConsumer { - fn default_name(&self) -> &'static str { - "cmd" - } -} - -impl<'a> FindArg<'a> for CommandTreeArgumentConsumer { - type Data = &'a CommandTree; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::CommandTree(tree)) => Ok(tree), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} diff --git a/crates/pumpkin/src/command/args/coordinate.rs b/crates/pumpkin/src/command/args/coordinate.rs deleted file mode 100644 index 46d8c39dd..000000000 --- a/crates/pumpkin/src/command/args/coordinate.rs +++ /dev/null @@ -1,75 +0,0 @@ -use std::str::FromStr; - -pub enum MaybeRelativeCoordinate { - Absolute(f64), - Relative(f64), -} - -impl TryFrom<&str> for MaybeRelativeCoordinate { - type Error = ::Err; - - fn try_from(s: &str) -> Result { - if let Some(s) = s.strip_prefix('~') { - let offset = if s.is_empty() { 0.0 } else { s.parse()? }; - Ok(Self::Relative(offset)) - } else { - let mut v = s.parse()?; - - // set position to block center if no decimal place is given - if !IS_Y && !s.contains('.') { - v += 0.5; - } - - Ok(Self::Absolute(v)) - } - } -} - -impl MaybeRelativeCoordinate { - pub fn into_absolute(self, origin: Option) -> Option { - match self { - Self::Absolute(v) => Some(v), - Self::Relative(offset) => Some(origin? + offset), - } - } -} - -#[derive(Debug)] -pub enum MaybeRelativeBlockCoordinate { - Absolute(i32), - Relative(f64), -} - -#[derive(Debug)] -pub enum ParseMaybeRelativeBlockCoordinateError { - InvalidAbsolute, - InvalidRelative, -} - -impl TryFrom<&str> for MaybeRelativeBlockCoordinate { - type Error = ParseMaybeRelativeBlockCoordinateError; - - fn try_from(s: &str) -> Result { - if let Some(s) = s.strip_prefix('~') { - let offset = if s.is_empty() { - 0.0 - } else { - s.parse().map_err(|_| Self::Error::InvalidRelative)? - }; - Ok(Self::Relative(offset)) - } else { - Ok(Self::Absolute( - s.parse().map_err(|_| Self::Error::InvalidAbsolute)?, - )) - } - } -} - -impl MaybeRelativeBlockCoordinate { - pub fn into_absolute(self, origin: Option) -> Option { - Some(match self { - Self::Absolute(v) => v, - Self::Relative(offset) => (origin? + offset).floor() as i32, - }) - } -} diff --git a/crates/pumpkin/src/command/args/difficulty.rs b/crates/pumpkin/src/command/args/difficulty.rs deleted file mode 100644 index b9e69b0e6..000000000 --- a/crates/pumpkin/src/command/args/difficulty.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::str::FromStr; - -use pumpkin_protocol::java::client::play::{ArgumentType, CommandSuggestion, SuggestionProviders}; -use pumpkin_util::Difficulty; - -use crate::{ - command::{ - CommandSender, - args::{ConsumeResult, SuggestResult}, - dispatcher::CommandError, - tree::RawArgs, - }, - server::Server, -}; - -use super::{Arg, ArgumentConsumer, DefaultNameArgConsumer, FindArg, GetClientSideArgParser}; - -pub struct DifficultyArgumentConsumer; - -impl GetClientSideArgParser for DifficultyArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::String( - pumpkin_protocol::java::client::play::StringProtoArgBehavior::SingleWord, - ) - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - Some(SuggestionProviders::AskServer) - } -} - -impl ArgumentConsumer for DifficultyArgumentConsumer { - fn consume<'a>( - &'a self, - _sender: &CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let s_opt: Option<&'a str> = args.pop().map(|arg| arg.value); - - s_opt.and_then(|s| Difficulty::from_str(s).map(Arg::Difficulty).ok()) - } - - fn suggest(&self, _sender: &CommandSender, _server: &Server, _input: &str) -> SuggestResult { - let difficulties = ["easy", "normal", "hard", "peaceful"]; - let suggestions: Vec = difficulties - .iter() - .map(|difficulty| CommandSuggestion::new((*difficulty).to_string(), None)) - .collect(); - Ok(Some(suggestions)) - } -} - -impl DefaultNameArgConsumer for DifficultyArgumentConsumer { - fn default_name(&self) -> &'static str { - "difficulty" - } -} - -impl<'a> FindArg<'a> for DifficultyArgumentConsumer { - type Data = Difficulty; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::Difficulty(data)) => Ok(*data), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} diff --git a/crates/pumpkin/src/command/args/entities.rs b/crates/pumpkin/src/command/args/entities.rs deleted file mode 100644 index 586dfeb2e..000000000 --- a/crates/pumpkin/src/command/args/entities.rs +++ /dev/null @@ -1,558 +0,0 @@ -use std::str::FromStr; -use std::sync::Arc; - -use crate::command::CommandSender; -use crate::command::args::{ConsumeResult, ConsumeResultWithSyntax}; -use crate::command::dispatcher::CommandError; -use crate::command::errors::command_syntax_error::{CommandSyntaxError, CommandSyntaxErrorContext}; -use crate::command::errors::error_types; -use crate::command::tree::{RawArg, RawArgs}; -use crate::entity::EntityBase; -use crate::server::Server; -use pumpkin_data::{entity::EntityType, translation}; -use pumpkin_nbt::compound::NbtCompound; -use pumpkin_protocol::java::client::play::{ArgumentType, SuggestionProviders}; -use pumpkin_util::GameMode; -use pumpkin_util::text::TextComponent; -use tracing::debug; -use uuid::Uuid; - -use super::super::args::ArgumentConsumer; -use super::{Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser}; - -pub enum EntitySelectorType { - Source, - NearestPlayer, - NearestEntity, - RandomPlayer, - AllPlayers, - AllEntities, - NamedPlayer(String), - Uuid(Uuid), -} - -// todo tags -pub enum ValueCondition { - Equals(T), - NotEquals(T), -} - -pub enum ComparableValueCondition { - Equals(T), - NotEquals(T), - GreaterThan(T), - LessThan(T), - GreaterThanOrEquals(T), - LessThanOrEquals(T), - Between(T, T), -} - -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum EntityFilterSort { - Arbitrary, - Nearest, - Furthest, - Random, -} - -pub enum EntityFilter { - X(ComparableValueCondition), - Y(ComparableValueCondition), - Z(ComparableValueCondition), - Distance(ComparableValueCondition), - Dx(ComparableValueCondition), - Dy(ComparableValueCondition), - Dz(ComparableValueCondition), - XRotation(ComparableValueCondition), - YRotation(ComparableValueCondition), - Score(ComparableValueCondition), - Tag(ValueCondition), - Team(ValueCondition), - Name(ValueCondition), - Type(ValueCondition<&'static EntityType>), - Nbt(NbtCompound), - Gamemode(ValueCondition), - Limit(usize), - Sort(EntityFilterSort), -} - -impl FromStr for EntityFilter { - type Err = String; - - fn from_str(s: &str) -> Result { - let mut parts = s.splitn(2, '='); - let key = parts.next().ok_or("Missing key in entity filter")?; - let mut value = parts.next().ok_or("Missing value in entity filter")?; - let negate = value.starts_with('!'); - if negate { - value = &value[1..]; - } - - match key { - "type" => { - let name = value.strip_prefix("minecraft:").unwrap_or(value); - let entity_type = - EntityType::from_name(name).ok_or(format!("Invalid entity type {value}"))?; - Ok(Self::Type(if negate { - ValueCondition::NotEquals(entity_type) - } else { - ValueCondition::Equals(entity_type) - })) - } - "limit" => { - let limit = value - .parse::() - .map_err(|_| "Invalid limit value".to_string())?; - if negate { - return Err("Negation of limit is not allowed".to_string()); - } - Ok(Self::Limit(limit)) - } - "sort" => { - let sort = match value { - "arbitrary" => EntityFilterSort::Arbitrary, - "nearest" => EntityFilterSort::Nearest, - "furthest" => EntityFilterSort::Furthest, - "random" => EntityFilterSort::Random, - _ => return Err(format!("Invalid sort type {value}")), - }; - if negate { - return Err("Negation of sort is not allowed".to_string()); - } - Ok(Self::Sort(sort)) - } - "name" => Ok(Self::Name(if negate { - ValueCondition::NotEquals(value.to_string()) - } else { - ValueCondition::Equals(value.to_string()) - })), - "tag" => Ok(Self::Tag(if negate { - ValueCondition::NotEquals(value.to_string()) - } else { - ValueCondition::Equals(value.to_string()) - })), - "team" => Ok(Self::Team(if negate { - ValueCondition::NotEquals(value.to_string()) - } else { - ValueCondition::Equals(value.to_string()) - })), - "scores" => Ok(Self::Score(ComparableValueCondition::Equals(0))), - "advancements" | "predicate" => Ok(Self::Sort(EntityFilterSort::Arbitrary)), - "nbt" => Ok(Self::Nbt(NbtCompound::default())), - _ => Err(format!("Unimplemented key: {key}")), - } - } -} - -/// -pub struct TargetSelector { - pub selector_type: EntitySelectorType, - pub conditions: Vec, -} - -impl TargetSelector { - /// Creates a new target selector with the specified type and default conditions. - #[must_use] - pub fn new(selector_type: EntitySelectorType) -> Self { - let mut filter = Vec::new(); - match selector_type { - EntitySelectorType::Source => filter.push(EntityFilter::Limit(1)), - EntitySelectorType::NearestPlayer | EntitySelectorType::NearestEntity => { - filter.push(EntityFilter::Sort(EntityFilterSort::Nearest)); - filter.push(EntityFilter::Limit(1)); - } - EntitySelectorType::RandomPlayer => { - filter.push(EntityFilter::Sort(EntityFilterSort::Random)); - filter.push(EntityFilter::Limit(1)); - } - EntitySelectorType::NamedPlayer(_) | EntitySelectorType::Uuid(_) => { - // Named or UUID selectors should only return one entity - filter.push(EntityFilter::Limit(1)); - } - _ => {} - } - Self { - selector_type, - conditions: filter, - } - } - - const fn base_includes_entities(&self) -> bool { - matches!( - self.selector_type, - EntitySelectorType::AllEntities | EntitySelectorType::NearestEntity - ) - } - - #[must_use] - pub fn includes_entities(&self) -> bool { - let player_type = &EntityType::PLAYER; - let mut includes_entities = self.base_includes_entities(); - - for condition in &self.conditions { - if let EntityFilter::Type(ValueCondition::Equals(entity_type)) = condition { - includes_entities = *entity_type != player_type; - } else if let EntityFilter::Type(ValueCondition::NotEquals(entity_type)) = condition - && *entity_type == player_type - { - includes_entities = true; - } - } - - includes_entities - } - - #[must_use] - pub fn get_sort(&self) -> Option { - self.conditions.iter().rev().find_map(|f| { - if let EntityFilter::Sort(sort) = f { - Some(*sort) - } else { - None - } - }) - } - - #[must_use] - pub fn get_limit(&self) -> usize { - self.conditions - .iter() - .rev() - .find_map(|f| { - if let EntityFilter::Limit(limit) = f { - Some(*limit) - } else { - None - } - }) - .unwrap_or(usize::MAX) - } -} - -impl FromStr for TargetSelector { - type Err = String; - - fn from_str(arg: &str) -> Result { - parse_target_selector(arg).map_err(|error| error.message) - } -} - -#[derive(Debug)] -struct TargetSelectorParseError { - message: String, - cursor: usize, -} - -fn parse_target_selector(arg: &str) -> Result { - if !arg.starts_with('@') { - return Uuid::parse_str(arg).map_or_else( - |_| { - Ok(TargetSelector::new(EntitySelectorType::NamedPlayer( - arg.to_string(), - ))) - }, - |uuid| Ok(TargetSelector::new(EntitySelectorType::Uuid(uuid))), - ); - } - - let selector_type_end = arg.find('[').unwrap_or(arg.len()); - let type_str = &arg[..selector_type_end]; - let selector_type = match type_str { - "@a" => EntitySelectorType::AllPlayers, - "@e" => EntitySelectorType::AllEntities, - "@s" => EntitySelectorType::Source, - "@p" => EntitySelectorType::NearestPlayer, - "@r" => EntitySelectorType::RandomPlayer, - "@n" => EntitySelectorType::NearestEntity, - _ => { - return Err(TargetSelectorParseError { - message: format!("Invalid target selector type {type_str}"), - cursor: selector_type_end.saturating_sub(1), - }); - } - }; - - let mut selector = TargetSelector::new(selector_type); - if selector_type_end == arg.len() { - return Ok(selector); - } - - if !arg.ends_with(']') { - return Err(TargetSelectorParseError { - message: "Target selector must end with ]".to_string(), - cursor: arg.len(), - }); - } - - let args_content = &arg[selector_type_end + 1..arg.len() - 1]; - let mut filter_start = 0usize; - let mut curly_depth = 0; - for (i, c) in args_content.char_indices() { - if c == '{' { - curly_depth += 1; - } else if c == '}' { - curly_depth -= 1; - } else if c == ',' && curly_depth == 0 { - parse_selector_filter( - &mut selector, - &args_content[filter_start..i], - selector_type_end + 1 + filter_start, - )?; - filter_start = i + 1; - } - } - parse_selector_filter( - &mut selector, - &args_content[filter_start..], - selector_type_end + 1 + filter_start, - )?; - - Ok(selector) -} - -fn parse_selector_filter( - selector: &mut TargetSelector, - raw_filter: &str, - filter_offset: usize, -) -> Result<(), TargetSelectorParseError> { - let trimmed_filter = raw_filter.trim(); - if trimmed_filter.is_empty() { - return Ok(()); - } - - let local_trimmed_start = raw_filter - .char_indices() - .find_map(|(index, c)| (!c.is_whitespace()).then_some(index)) - .unwrap_or(0); - let filter_cursor = filter_offset + local_trimmed_start; - - let parsed_filter = - EntityFilter::from_str(trimmed_filter).map_err(|message| TargetSelectorParseError { - message, - cursor: filter_cursor, - })?; - selector.conditions.push(parsed_filter); - Ok(()) -} - -/// todo: implement (currently just calls `PlayerArgumentConsumer`) -/// -/// For selecting zero, one or multiple entities, eg. using @s, a player name, @a or @e -pub struct EntitiesArgumentConsumer; - -impl GetClientSideArgParser for EntitiesArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - // todo: investigate why this does not accept target selectors - ArgumentType::Entity { flags: 0 } - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - None - } -} - -impl ArgumentConsumer for EntitiesArgumentConsumer { - fn consume<'a>( - &'a self, - sender: &'a CommandSender, - server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let s = args.pop().map(|arg| arg.value)?; - - let entity_selector = match s.parse::() { - Ok(selector) => selector, - Err(e) => { - debug!("Failed to parse target selector '{s}': {e}"); - return None; - } - }; - - // todo: command context - let entities = server.select_entities(&entity_selector, Some(sender)); - - Some(Arg::Entities(entities)) - } - - fn consume_with_syntax<'a>( - &'a self, - sender: &'a CommandSender, - server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResultWithSyntax<'a> { - let Some(raw_arg) = args.pop() else { - return Ok(None); - }; - - let selector = parse_target_selector_with_context(raw_arg)?; - let entities = server.select_entities(&selector, Some(sender)); - Ok(Some(Arg::Entities(entities))) - } -} - -impl DefaultNameArgConsumer for EntitiesArgumentConsumer { - fn default_name(&self) -> &'static str { - "targets" - } -} - -impl<'a> FindArg<'a> for EntitiesArgumentConsumer { - type Data = &'a [Arc]; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::Entities(data)) => Ok(data), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} - -pub(crate) fn parse_target_selector_with_context( - raw_arg: RawArg<'_>, -) -> Result { - parse_target_selector(raw_arg.value).map_err(|error| { - syntax_error_for_arg_with_cursor( - raw_arg, - TextComponent::translate_cross( - translation::java::ARGUMENT_ENTITY_INVALID, - translation::java::ARGUMENT_ENTITY_INVALID, - [], - ), - error.cursor, - ) - }) -} - -pub(crate) fn ensure_player_only_selector( - selector: &TargetSelector, - raw_arg: RawArg<'_>, -) -> Result<(), CommandSyntaxError> { - if selector.includes_entities() { - Err(syntax_error_for_arg_with_cursor( - raw_arg, - TextComponent::translate_cross( - translation::java::ARGUMENT_PLAYER_ENTITIES, - translation::java::ARGUMENT_PLAYER_ENTITIES, - [], - ), - 0, - )) - } else { - Ok(()) - } -} - -fn syntax_error_for_arg_with_cursor( - raw_arg: RawArg<'_>, - message: TextComponent, - local_cursor: usize, -) -> CommandSyntaxError { - let mut clamped_local_cursor = local_cursor.min(raw_arg.value.len()); - while clamped_local_cursor > 0 && !raw_arg.value.is_char_boundary(clamped_local_cursor) { - clamped_local_cursor -= 1; - } - - CommandSyntaxError { - error_type: &error_types::DISPATCHER_UNKNOWN_ARGUMENT, - message, - context: Some(CommandSyntaxErrorContext { - input: raw_arg.input.to_string(), - cursor: raw_arg.start + clamped_local_cursor, - }), - } -} - -#[cfg(test)] -mod test { - use pumpkin_data::translation; - - use super::{TargetSelector, ensure_player_only_selector, parse_target_selector_with_context}; - use crate::command::tree::RawArg; - - #[test] - fn selector_parse_error_points_inside_token() { - let input = "ban @e[sort=invalid]"; - let raw_arg = RawArg { - value: "@e[sort=invalid]", - start: 4, - end: input.len(), - input, - }; - - let Err(error) = parse_target_selector_with_context(raw_arg) else { - panic!("expected selector parsing to fail"); - }; - let cursor = error.context.expect("Error should have context").cursor; - assert_eq!(cursor, 7); - } - - #[test] - fn player_only_error_points_to_selector_start() { - let input = "ban @e"; - let raw_arg = RawArg { - value: "@e", - start: 4, - end: input.len(), - input, - }; - let selector = "@e" - .parse::() - .expect("Selector should be valid"); - - let error = ensure_player_only_selector(&selector, raw_arg).unwrap_err(); - let translate_key = match error.message.0.content.as_ref() { - pumpkin_util::text::TextContent::Translate { translate, .. } => translate.as_ref(), - _ => "", - }; - assert_eq!(translate_key, translation::java::ARGUMENT_PLAYER_ENTITIES); - assert_eq!(error.context.expect("Error should have context").cursor, 4); - } - - #[test] - fn parse_type_and_inverted_type() { - let s = "@e[type=!player,type=minecraft:zombie]" - .parse::() - .expect("should parse"); - assert!(s.includes_entities()); - assert_eq!(s.conditions.len(), 2); - } - - #[test] - fn parse_advanced_selectors() { - // Test name and inverted name - let s = "@e[name=Alex,name=!Steve]" - .parse::() - .expect("should parse name selectors"); - assert_eq!(s.conditions.len(), 2); - - // Test tag selector - let s = "@e[tag=admin,tag=!vip]" - .parse::() - .expect("should parse tag selectors"); - assert_eq!(s.conditions.len(), 2); - - // Test team selector - let s = "@e[team=red,team=!blue]" - .parse::() - .expect("should parse team selectors"); - assert_eq!(s.conditions.len(), 2); - - // Test scores selector - let s = "@e[scores={kills=1..,deaths=..5}]" - .parse::() - .expect("should parse scores selectors"); - assert_eq!(s.conditions.len(), 1); - - // Test advancements selector - let s = "@e[advancements={story/mine_stone=true}]" - .parse::() - .expect("should parse advancements selectors"); - assert_eq!(s.conditions.len(), 1); - - // Test NBT selector - let s = "@e[nbt={OnGround:1b}]" - .parse::() - .expect("should parse NBT selectors"); - assert_eq!(s.conditions.len(), 1); - } -} diff --git a/crates/pumpkin/src/command/args/entity.rs b/crates/pumpkin/src/command/args/entity.rs deleted file mode 100644 index 330975f24..000000000 --- a/crates/pumpkin/src/command/args/entity.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::sync::Arc; - -use crate::command::CommandSender; -use crate::command::args::entities::TargetSelector; -use crate::command::args::{ConsumeResult, ConsumeResultWithSyntax}; -use crate::command::dispatcher::CommandError; -use crate::command::tree::RawArgs; -use crate::entity::EntityBase; -use crate::server::Server; -use pumpkin_protocol::java::client::play::{ArgumentType, SuggestionProviders}; -use tracing::debug; - -use super::super::args::ArgumentConsumer; -use super::entities::parse_target_selector_with_context; -use super::{Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser}; - -/// todo: implement for entities that aren't players -/// -/// For selecting a single entity, eg. using @s, a player name or entity uuid. -/// -/// Use [`super::entities::EntitiesArgumentConsumer`] when there may be multiple targets. -pub struct EntityArgumentConsumer; - -impl GetClientSideArgParser for EntityArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - // todo: investigate why this does not accept target selectors - ArgumentType::Entity { - flags: ArgumentType::ENTITY_FLAG_ONLY_SINGLE, - } - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - None - } -} - -impl ArgumentConsumer for EntityArgumentConsumer { - fn consume<'a>( - &'a self, - sender: &'a CommandSender, - server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let s = args.pop().map(|arg| arg.value)?; - - let entity_selector = match s.parse::() { - Ok(selector) => selector, - Err(e) => { - debug!("Failed to parse target selector '{s}': {e}"); - return None; - } - }; - - if entity_selector.get_limit() > 1 { - debug!("Target selector '{s}' has limit > 1, expected single entity"); - return None; - } - - // todo: command context - let entities = server.select_entities(&entity_selector, Some(sender)); - - entities.into_iter().next().map(Arg::Entity) - } - - fn consume_with_syntax<'a>( - &'a self, - sender: &'a CommandSender, - server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResultWithSyntax<'a> { - let Some(raw_arg) = args.pop() else { - return Ok(None); - }; - - let selector = parse_target_selector_with_context(raw_arg)?; - - if selector.get_limit() > 1 { - return Ok(None); - } - - let entities = server.select_entities(&selector, Some(sender)); - Ok(entities.into_iter().next().map(Arg::Entity)) - } -} - -impl DefaultNameArgConsumer for EntityArgumentConsumer { - fn default_name(&self) -> &'static str { - "target" - } -} - -impl<'a> FindArg<'a> for EntityArgumentConsumer { - type Data = Arc; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::Entity(data)) => Ok(data.clone()), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} diff --git a/crates/pumpkin/src/command/args/entity_anchor.rs b/crates/pumpkin/src/command/args/entity_anchor.rs deleted file mode 100644 index 289ade66d..000000000 --- a/crates/pumpkin/src/command/args/entity_anchor.rs +++ /dev/null @@ -1,77 +0,0 @@ -use pumpkin_protocol::java::client::play::{ArgumentType, SuggestionProviders}; - -use crate::command::CommandSender; -use crate::command::args::ConsumeResult; -use crate::command::dispatcher::CommandError; -use crate::command::tree::RawArgs; -use crate::server::Server; - -use super::super::args::ArgumentConsumer; -use super::{Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser}; - -/// Entity anchor point for facing commands (eyes or feet). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum EntityAnchor { - Feet, - Eyes, -} - -impl EntityAnchor { - /// Returns the name used in commands. - #[must_use] - pub const fn as_str(&self) -> &'static str { - match self { - Self::Feet => "feet", - Self::Eyes => "eyes", - } - } -} - -/// Argument consumer for entity anchor (eyes/feet). -pub struct EntityAnchorArgumentConsumer; - -impl GetClientSideArgParser for EntityAnchorArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::EntityAnchor - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - None - } -} - -impl ArgumentConsumer for EntityAnchorArgumentConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let anchor_str = args.pop().map(|arg| arg.value)?; - - let anchor = match anchor_str.to_lowercase().as_str() { - "feet" => EntityAnchor::Feet, - "eyes" => EntityAnchor::Eyes, - _ => return None, - }; - - Some(Arg::EntityAnchor(anchor)) - } -} - -impl DefaultNameArgConsumer for EntityAnchorArgumentConsumer { - fn default_name(&self) -> &'static str { - "anchor" - } -} - -impl<'a> FindArg<'a> for EntityAnchorArgumentConsumer { - type Data = EntityAnchor; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::EntityAnchor(anchor)) => Ok(*anchor), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} diff --git a/crates/pumpkin/src/command/args/gamemode.rs b/crates/pumpkin/src/command/args/gamemode.rs deleted file mode 100644 index 94d34099f..000000000 --- a/crates/pumpkin/src/command/args/gamemode.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::str::FromStr; - -use pumpkin_protocol::java::client::play::{ArgumentType, SuggestionProviders}; -use pumpkin_util::GameMode; - -use crate::{ - command::{CommandSender, args::ConsumeResult, dispatcher::CommandError, tree::RawArgs}, - server::Server, -}; - -use super::{Arg, ArgumentConsumer, DefaultNameArgConsumer, FindArg, GetClientSideArgParser}; - -pub struct GamemodeArgumentConsumer; - -impl GetClientSideArgParser for GamemodeArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::Gamemode - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - None - } -} - -impl ArgumentConsumer for GamemodeArgumentConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let s_opt: Option<&'a str> = args.pop().map(|arg| arg.value); - - s_opt.and_then(|s| { - if let Ok(id) = s.parse::() - && let Ok(gamemode) = GameMode::try_from(id) - { - return Some(Arg::GameMode(gamemode)); - } - - GameMode::from_str(s).map(Arg::GameMode).ok() // Convert Result to Option - }) - } -} - -impl DefaultNameArgConsumer for GamemodeArgumentConsumer { - fn default_name(&self) -> &'static str { - "gamemode" - } -} - -impl<'a> FindArg<'a> for GamemodeArgumentConsumer { - type Data = GameMode; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::GameMode(data)) => Ok(*data), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} diff --git a/crates/pumpkin/src/command/args/gameprofile.rs b/crates/pumpkin/src/command/args/gameprofile.rs deleted file mode 100644 index 27d8e7aea..000000000 --- a/crates/pumpkin/src/command/args/gameprofile.rs +++ /dev/null @@ -1,463 +0,0 @@ -use arc_swap::ArcSwap; -use pumpkin_data::translation; -use pumpkin_protocol::java::client::play::{ArgumentType, CommandSuggestion, SuggestionProviders}; -use pumpkin_util::text::TextComponent; -use std::sync::Arc; -use uuid::Uuid; - -use crate::command::errors::command_syntax_error::{CommandSyntaxError, CommandSyntaxErrorContext}; -use crate::command::errors::error_types; -use crate::{ - command::{ - CommandSender, - args::{ConsumeResult, ConsumeResultWithSyntax, SuggestResult}, - dispatcher::CommandError, - tree::{RawArg, RawArgs}, - }, - net::authentication::lookup_profile_by_name_blocking, - net::{GameProfile, offline_uuid}, - server::Server, -}; - -use super::entities::{ensure_player_only_selector, parse_target_selector_with_context}; -use super::{Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser}; -use crate::command::args::ArgumentConsumer; - -#[derive(Clone, Copy)] -pub enum GameProfileSuggestionMode { - OnlinePlayers, - NonOpOnlinePlayers, - OpNames, - BannedNames, - NonWhitelistedOnlinePlayers, - WhitelistedNames, -} - -pub struct GameProfilesArgumentConsumer { - suggestion_mode: GameProfileSuggestionMode, - suggest_selectors: bool, -} - -impl GameProfilesArgumentConsumer { - #[must_use] - pub const fn new(suggestion_mode: GameProfileSuggestionMode, suggest_selectors: bool) -> Self { - Self { - suggestion_mode, - suggest_selectors, - } - } - - #[must_use] - pub const fn online_players_with_selectors() -> Self { - Self::new(GameProfileSuggestionMode::OnlinePlayers, true) - } -} - -impl Default for GameProfilesArgumentConsumer { - fn default() -> Self { - Self::online_players_with_selectors() - } -} - -impl GetClientSideArgParser for GameProfilesArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::GameProfile - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - Some(SuggestionProviders::AskServer) - } -} - -impl ArgumentConsumer for GameProfilesArgumentConsumer { - fn consume<'a>( - &'a self, - sender: &'a CommandSender, - server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let raw_arg = args.pop()?; - - resolve_profiles_from_token(sender, server, raw_arg) - .ok() - .map(Arg::GameProfiles) - } - - fn consume_with_syntax<'a>( - &'a self, - sender: &'a CommandSender, - server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResultWithSyntax<'a> { - let Some(raw_arg) = args.pop() else { - return Ok(None); - }; - - let resolved = resolve_profiles_from_token(sender, server, raw_arg)?; - Ok(Some(Arg::GameProfiles(resolved))) - } - - fn suggest(&self, _sender: &CommandSender, server: &Server, _input: &str) -> SuggestResult { - let mut suggestions = Vec::new(); - if self.suggest_selectors { - suggestions.extend(selector_suggestions()); - } - - let mut names = Vec::new(); - match self.suggestion_mode { - GameProfileSuggestionMode::OnlinePlayers => { - for player in server.get_all_players() { - push_name_if_missing(&mut names, player.gameprofile.name.clone()); - } - } - GameProfileSuggestionMode::NonOpOnlinePlayers => { - let ops = server - .data - .operator_config - .read() - .unwrap_or_else(std::sync::PoisonError::into_inner); - for player in server.get_all_players() { - if ops.ops.iter().all(|op| op.uuid != player.gameprofile.id) { - push_name_if_missing(&mut names, player.gameprofile.name.clone()); - } - } - } - GameProfileSuggestionMode::OpNames => { - let ops = server - .data - .operator_config - .read() - .unwrap_or_else(std::sync::PoisonError::into_inner); - for op in &ops.ops { - push_name_if_missing(&mut names, op.name.clone()); - } - } - GameProfileSuggestionMode::BannedNames => { - let banned = server - .data - .banned_player_list - .read() - .unwrap_or_else(std::sync::PoisonError::into_inner); - for entry in &banned.banned_players { - push_name_if_missing(&mut names, entry.name.clone()); - } - } - GameProfileSuggestionMode::NonWhitelistedOnlinePlayers => { - let whitelist = server - .data - .whitelist_config - .read() - .unwrap_or_else(std::sync::PoisonError::into_inner); - for player in server.get_all_players() { - if !whitelist.is_whitelisted(&player.gameprofile) { - push_name_if_missing(&mut names, player.gameprofile.name.clone()); - } - } - } - GameProfileSuggestionMode::WhitelistedNames => { - let whitelist = server - .data - .whitelist_config - .read() - .unwrap_or_else(std::sync::PoisonError::into_inner); - for entry in &whitelist.whitelist { - push_name_if_missing(&mut names, entry.name.clone()); - } - } - } - - suggestions.extend( - names - .into_iter() - .map(|name| CommandSuggestion::new(name, None)), - ); - - Ok(Some(suggestions)) - } -} - -impl DefaultNameArgConsumer for GameProfilesArgumentConsumer { - fn default_name(&self) -> &'static str { - "targets" - } -} - -impl<'a> FindArg<'a> for GameProfilesArgumentConsumer { - type Data = &'a [GameProfile]; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::GameProfiles(data)) => Ok(data), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} - -fn resolve_profiles_from_token( - sender: &CommandSender, - server: &Server, - raw_arg: RawArg<'_>, -) -> Result, CommandSyntaxError> { - if raw_arg.value.starts_with('@') { - let selector = parse_target_selector_with_context(raw_arg)?; - ensure_player_only_selector(&selector, raw_arg)?; - - let players = server.select_players(&selector, Some(sender)); - if players.is_empty() { - return Err(syntax_player_unknown(raw_arg)); - } - - return Ok(players - .into_iter() - .map(|player| player.gameprofile.clone()) - .collect()); - } - - if let Ok(uuid) = Uuid::parse_str(raw_arg.value) { - if let Some(player) = server.get_player_by_uuid(uuid) { - return Ok(vec![player.gameprofile.clone()]); - } - - let cached_entry = server - .data - .user_cache - .write() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .get_by_uuid(uuid); - if let Some(entry) = cached_entry { - return Ok(vec![profile_from_uuid_name(entry.uuid, entry.name)]); - } - - if let Some(profile) = resolve_known_profile_by_uuid(server, uuid) { - return Ok(vec![profile]); - } - - return Err(syntax_player_unknown(raw_arg)); - } - - if let Some(player) = server.get_player_by_name(raw_arg.value) { - return Ok(vec![player.gameprofile.clone()]); - } - - let cached_entry = server - .data - .user_cache - .write() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .get_by_name(raw_arg.value); - if let Some(entry) = cached_entry { - return Ok(vec![profile_from_uuid_name(entry.uuid, entry.name)]); - } - - if let Some(profile) = resolve_known_profile_by_name(server, raw_arg.value) { - return Ok(vec![profile]); - } - - if server.advanced_config.networking.java.online_mode { - match lookup_profile_by_name_blocking( - raw_arg.value, - &server.advanced_config.networking.java.authentication, - ) { - Ok(Some((uuid, resolved_name))) => { - server - .data - .user_cache - .write() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .upsert(uuid, resolved_name.clone()); - return Ok(vec![profile_from_uuid_name(uuid, resolved_name)]); - } - Ok(None) | Err(_) => return Err(syntax_player_unknown(raw_arg)), - } - } - - if let Ok(uuid) = offline_uuid(raw_arg.value) { - let profile = profile_from_uuid_name(uuid, raw_arg.value.to_string()); - server - .data - .user_cache - .write() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .upsert(profile.id, profile.name.clone()); - return Ok(vec![profile]); - } - - Err(syntax_player_unknown(raw_arg)) -} - -fn resolve_known_profile_by_name(server: &Server, name: &str) -> Option { - { - let ops = server - .data - .operator_config - .read() - .unwrap_or_else(std::sync::PoisonError::into_inner); - if let Some(op) = ops.ops.iter().find(|op| op.name.eq_ignore_ascii_case(name)) { - return Some(profile_from_uuid_name(op.uuid, op.name.clone())); - } - } - - { - let banned_players = server - .data - .banned_player_list - .read() - .unwrap_or_else(std::sync::PoisonError::into_inner); - if let Some(entry) = banned_players - .banned_players - .iter() - .find(|entry| entry.name.eq_ignore_ascii_case(name)) - { - return Some(profile_from_uuid_name(entry.uuid, entry.name.clone())); - } - } - - { - let whitelist = server - .data - .whitelist_config - .read() - .unwrap_or_else(std::sync::PoisonError::into_inner); - if let Some(entry) = whitelist - .whitelist - .iter() - .find(|entry| entry.name.eq_ignore_ascii_case(name)) - { - return Some(profile_from_uuid_name(entry.uuid, entry.name.clone())); - } - } - - None -} - -fn resolve_known_profile_by_uuid(server: &Server, uuid: Uuid) -> Option { - { - let ops = server - .data - .operator_config - .read() - .unwrap_or_else(std::sync::PoisonError::into_inner); - if let Some(op) = ops.ops.iter().find(|op| op.uuid == uuid) { - return Some(profile_from_uuid_name(op.uuid, op.name.clone())); - } - } - - { - let banned_players = server - .data - .banned_player_list - .read() - .unwrap_or_else(std::sync::PoisonError::into_inner); - if let Some(entry) = banned_players - .banned_players - .iter() - .find(|entry| entry.uuid == uuid) - { - return Some(profile_from_uuid_name(entry.uuid, entry.name.clone())); - } - } - - { - let whitelist = server - .data - .whitelist_config - .read() - .unwrap_or_else(std::sync::PoisonError::into_inner); - if let Some(entry) = whitelist.whitelist.iter().find(|entry| entry.uuid == uuid) { - return Some(profile_from_uuid_name(entry.uuid, entry.name.clone())); - } - } - - None -} - -#[allow(clippy::missing_const_for_fn)] -fn profile_from_uuid_name(uuid: Uuid, name: String) -> GameProfile { - GameProfile { - id: uuid, - name, - properties: ArcSwap::new(Arc::from(vec![])), - profile_actions: None, - } -} - -fn push_name_if_missing(names: &mut Vec, name: String) { - if names - .iter() - .any(|known_name| known_name.eq_ignore_ascii_case(&name)) - { - return; - } - names.push(name); -} - -fn selector_suggestions() -> Vec { - vec![ - CommandSuggestion::new("@s".to_string(), None), - CommandSuggestion::new("@p".to_string(), None), - CommandSuggestion::new("@r".to_string(), None), - CommandSuggestion::new("@a".to_string(), None), - CommandSuggestion::new("@e".to_string(), None), - CommandSuggestion::new("@n".to_string(), None), - ] -} - -fn syntax_player_unknown(raw_arg: RawArg<'_>) -> CommandSyntaxError { - syntax_error_for_arg_with_cursor( - raw_arg, - TextComponent::translate_cross( - translation::java::ARGUMENT_PLAYER_UNKNOWN, - translation::java::ARGUMENT_PLAYER_UNKNOWN, - [], - ), - 0, - ) -} - -fn syntax_error_for_arg_with_cursor( - raw_arg: RawArg<'_>, - message: TextComponent, - local_cursor: usize, -) -> CommandSyntaxError { - let mut clamped_local_cursor = local_cursor.min(raw_arg.value.len()); - while clamped_local_cursor > 0 && !raw_arg.value.is_char_boundary(clamped_local_cursor) { - clamped_local_cursor -= 1; - } - - CommandSyntaxError { - error_type: &error_types::DISPATCHER_UNKNOWN_ARGUMENT, - message, - context: Some(CommandSyntaxErrorContext { - input: raw_arg.input.to_string(), - cursor: raw_arg.start + clamped_local_cursor, - }), - } -} - -#[cfg(test)] -mod test { - use pumpkin_data::translation; - use pumpkin_util::text::TextContent; - - use super::syntax_player_unknown; - use crate::command::tree::RawArg; - - #[test] - fn unknown_player_error_uses_translation_and_arg_start_cursor() { - let input = "ban missing_player"; - let raw_arg = RawArg { - value: "missing_player", - start: 4, - end: input.len(), - input, - }; - - let error = syntax_player_unknown(raw_arg); - let translate_key = match error.message.0.content.as_ref() { - TextContent::Translate { translate, .. } => translate.as_ref(), - _ => "", - }; - assert_eq!(translate_key, translation::java::ARGUMENT_PLAYER_UNKNOWN); - assert_eq!(error.context.expect("Error should have context").cursor, 4); - } -} diff --git a/crates/pumpkin/src/command/args/hex_color.rs b/crates/pumpkin/src/command/args/hex_color.rs deleted file mode 100644 index 8acfef70e..000000000 --- a/crates/pumpkin/src/command/args/hex_color.rs +++ /dev/null @@ -1,122 +0,0 @@ -use pumpkin_data::translation; -use pumpkin_protocol::java::client::play::{ArgumentType, CommandSuggestion, SuggestionProviders}; -use pumpkin_util::text::TextComponent; - -use crate::command::CommandSender; -use crate::command::args::{ - Arg, ArgumentConsumer, ConsumeResult, ConsumeResultWithSyntax, DefaultNameArgConsumer, FindArg, - GetClientSideArgParser, SuggestResult, -}; -use crate::command::dispatcher::CommandError; -use crate::command::errors::error_types::CommandErrorType; -use crate::command::tree::RawArgs; -use crate::server::Server; - -pub const INVALID_HEX_ERROR_TYPE: CommandErrorType<1> = CommandErrorType::new( - translation::java::ARGUMENT_HEXCOLOR_INVALID, - translation::java::ARGUMENT_HEXCOLOR_INVALID, -); - -pub struct HexColorArgumentConsumer; - -fn parse_hex_color(color_str: &str) -> Option { - match color_str.len() { - 3 => { - let r = u32::from_str_radix(&color_str[0..1], 16).ok()? * 17; - let g = u32::from_str_radix(&color_str[1..2], 16).ok()? * 17; - let b = u32::from_str_radix(&color_str[2..3], 16).ok()? * 17; - Some((r << 16) | (g << 8) | b) - } - 6 => { - let r = u32::from_str_radix(&color_str[0..2], 16).ok()?; - let g = u32::from_str_radix(&color_str[2..4], 16).ok()?; - let b = u32::from_str_radix(&color_str[4..6], 16).ok()?; - Some((r << 16) | (g << 8) | b) - } - _ => None, - } -} - -impl GetClientSideArgParser for HexColorArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::HexColor - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - None - } -} - -impl ArgumentConsumer for HexColorArgumentConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let s_opt: Option<&'a str> = args.pop().map(|arg| arg.value); - - s_opt.and_then(|s| parse_hex_color(s).map(Arg::HexColor)) - } - - fn consume_with_syntax<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResultWithSyntax<'a> { - let Some(raw_arg) = args.pop() else { - return Ok(None); - }; - - parse_hex_color(raw_arg.value) - .map(|color| Some(Arg::HexColor(color))) - .ok_or_else(|| { - INVALID_HEX_ERROR_TYPE.create_without_context_args_slice(&[TextComponent::text( - raw_arg.value.to_string(), - )]) - }) - } - - fn suggest(&self, _sender: &CommandSender, _server: &Server, _input: &str) -> SuggestResult { - let suggestions = vec![ - CommandSuggestion::new("F00".to_string(), None), - CommandSuggestion::new("FF0000".to_string(), None), - ]; - Ok(Some(suggestions)) - } -} - -impl DefaultNameArgConsumer for HexColorArgumentConsumer { - fn default_name(&self) -> &'static str { - "color" - } -} - -impl<'a> FindArg<'a> for HexColorArgumentConsumer { - type Data = u32; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::HexColor(color)) => Ok(*color), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse_hex_color_works() { - assert_eq!(parse_hex_color("F00"), Some(0xFF0000)); - assert_eq!(parse_hex_color("0F0"), Some(0x00FF00)); - assert_eq!(parse_hex_color("00F"), Some(0x0000FF)); - assert_eq!(parse_hex_color("FF0000"), Some(0xFF0000)); - assert_eq!(parse_hex_color("123456"), Some(0x123456)); - assert_eq!(parse_hex_color("FFFFF"), None); - assert_eq!(parse_hex_color("F"), None); - assert_eq!(parse_hex_color("GGGGGG"), None); - } -} diff --git a/crates/pumpkin/src/command/args/message.rs b/crates/pumpkin/src/command/args/message.rs deleted file mode 100644 index 12a42e825..000000000 --- a/crates/pumpkin/src/command/args/message.rs +++ /dev/null @@ -1,66 +0,0 @@ -use pumpkin_protocol::java::client::play::{ - ArgumentType, StringProtoArgBehavior, SuggestionProviders, -}; - -use crate::{ - command::{args::ConsumeResult, dispatcher::CommandError}, - server::Server, -}; - -use super::{ - super::{ - CommandSender, - args::{ArgumentConsumer, RawArgs}, - }, - Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser, -}; - -/// Consumes all remaining words/args. Does not consume if there is no word. -pub struct MsgArgConsumer; - -impl GetClientSideArgParser for MsgArgConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::String(StringProtoArgBehavior::GreedyPhrase) - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - None - } -} - -impl ArgumentConsumer for MsgArgConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let first_word = args.pop()?; - - let mut msg = first_word.value.to_string(); - - while let Some(word) = args.pop() { - msg.push(' '); - msg.push_str(word.value); - } - - Some(Arg::Msg(msg)) - } -} - -impl DefaultNameArgConsumer for MsgArgConsumer { - fn default_name(&self) -> &'static str { - "msg" - } -} - -impl<'a> FindArg<'a> for MsgArgConsumer { - type Data = &'a str; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::Msg(data)) => Ok(data), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} diff --git a/crates/pumpkin/src/command/args/mod.rs b/crates/pumpkin/src/command/args/mod.rs deleted file mode 100644 index 26d6f8fff..000000000 --- a/crates/pumpkin/src/command/args/mod.rs +++ /dev/null @@ -1,224 +0,0 @@ -use std::{collections::HashMap, hash::Hash, sync::Arc}; - -use bounded_num::{NotInBounds, Number}; -use pumpkin_data::Enchantment; -use pumpkin_data::damage::DamageType; -use pumpkin_data::effect::StatusEffect; -use pumpkin_data::particle::Particle; -use pumpkin_data::sound::SoundCategory; -use pumpkin_protocol::java::client::play::{ArgumentType, CommandSuggestion, SuggestionProviders}; -use pumpkin_util::Difficulty; -use pumpkin_util::text::TextComponent; -use pumpkin_util::{ - GameMode, - math::{position::BlockPos, vector2::Vector2, vector3::Vector3}, -}; - -use super::{ - CommandSender, - dispatcher::CommandError, - errors::command_syntax_error::CommandSyntaxError, - tree::{CommandTree, RawArgs}, -}; -use crate::entity::EntityBase; -use crate::world::bossbar::{BossbarColor, BossbarDivisions}; -use crate::{entity::player::Player, server::Server}; - -pub use entity_anchor::EntityAnchor; -use pumpkin_data::advancement::Advancement; - -pub mod block; -pub mod bool; -pub mod bossbar_color; -pub mod bossbar_style; -pub mod bounded_num; -pub mod command; -mod coordinate; -pub mod difficulty; -pub mod entities; -pub mod entity; -pub mod entity_anchor; -pub mod gamemode; -pub mod gameprofile; -pub mod hex_color; -pub mod message; -pub mod players; -pub mod position_2d; -pub mod position_3d; -pub mod position_block; -pub mod resource; -pub mod resource_location; -pub mod rotation; -pub mod simple; -pub mod slot; -pub mod sound; -pub mod sound_category; -pub mod summonable_entities; -pub mod team_color; -pub mod textcomponent; -pub mod time; - -/// see [`crate::command::tree::builder::argument`] -pub type ConsumeResult<'a> = Option>; -pub type ConsumeResultWithSyntax<'a> = Result>, CommandSyntaxError>; - -pub type SuggestResult = Result>, CommandError>; - -pub trait ArgumentConsumer: Sync + Send + GetClientSideArgParser { - fn consume<'a>( - &'a self, - sender: &'a CommandSender, - server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a>; - - fn consume_with_syntax<'a>( - &'a self, - sender: &'a CommandSender, - server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResultWithSyntax<'a> { - Ok(self.consume(sender, server, args)) - } - - /// Used for tab completion (but only if argument suggestion type is "`minecraft:ask_server`"!). - /// - /// NOTE: This is called after this consumer's [`ArgumentConsumer::consume`] method returned None, so if args is used here, make sure [`ArgumentConsumer::consume`] never returns None after mutating args. - fn suggest(&self, _sender: &CommandSender, _server: &Server, _input: &str) -> SuggestResult { - Ok(None) - } -} - -pub trait GetClientSideArgParser { - /// Return the parser the client should use while typing a command in chat. - fn get_client_side_parser(&self) -> ArgumentType; - /// Usually this should return None. This can be used to force suggestions to be processed on serverside. - fn get_client_side_suggestion_type_override(&self) -> Option; -} - -pub trait DefaultNameArgConsumer: ArgumentConsumer { - fn default_name(&self) -> &str; -} - -#[derive(Clone)] -pub enum Arg<'a> { - Entities(Vec>), - Entity(Arc), - Players(Vec>), - GameProfiles(Vec), - BlockPos(BlockPos), - Pos3D(Vector3), - Pos2D(Vector2), - /// (yaw, `is_yaw_relative`, pitch, `is_pitch_relative`) - Rotation(f32, bool, f32, bool), - GameMode(GameMode), - Difficulty(Difficulty), - CommandTree(CommandTree), - Item(&'a str), - ItemPredicate(&'a str), - ResourceLocation(pumpkin_util::identifier::Identifier), - Block(&'a str), - BlockPredicate(&'a str), - BossbarColor(BossbarColor), - BossbarStyle(BossbarDivisions), - Particle(Particle), - Msg(String), - TextComponent(TextComponent), - Time(i32), - Num(Result), - Bool(bool), - Simple(&'a str), - SoundCategory(SoundCategory), - DamageType(DamageType), - Effect(&'static StatusEffect), - Enchantment(&'static Enchantment), - Advancement(&'static Advancement), - EntityAnchor(EntityAnchor), - Slot(usize, String), - Slots(&'static [usize], String), - TeamColor(pumpkin_util::text::color::NamedColor), - HexColor(u32), -} - -/// see [`crate::command::tree::builder::argument`] and [`CommandTree::execute`]/[`crate::command::tree::builder::NonLeafNodeBuilder::execute`] -pub type ConsumedArgs<'a> = HashMap<&'a str, Arg<'a>>; - -pub trait GetCloned { - fn get_cloned(&self, key: &K) -> Option; -} - -impl GetCloned for HashMap { - fn get_cloned(&self, key: &K) -> Option { - self.get(key).cloned() - } -} - -pub trait FindArg<'a> { - type Data; - - fn find_arg(args: &'a ConsumedArgs, name: &str) -> Result; -} - -pub trait FindArgDefaultName<'a, T> { - fn find_arg_default_name(&self, args: &'a ConsumedArgs) -> Result; -} - -impl<'a, T, C: FindArg<'a, Data = T> + DefaultNameArgConsumer> FindArgDefaultName<'a, T> for C { - fn find_arg_default_name(&self, args: &'a ConsumedArgs) -> Result { - C::find_arg(args, self.default_name()) - } -} - -pub(crate) trait SplitSingleWhitespaceIncludingEmptyParts<'a> { - /// Splits a string at every single unicode whitespace. Therefore the returned iterator sometimes contains empty strings. This is useful for command suggestions. - /// - /// Note: Vanilla does this only for command suggestions, for execution consecutive whitespaces are treated as one. - fn split_single_whitespace_including_empty_parts(self) -> impl Iterator; -} - -impl<'a> SplitSingleWhitespaceIncludingEmptyParts<'a> for &'a str { - fn split_single_whitespace_including_empty_parts(self) -> impl Iterator { - SplitSingleWhitespaceIncludingEmptyPartsIter { - s: self, - pos: 0, - chars_iter: self.char_indices(), - is_complete: false, - } - } -} - -struct SplitSingleWhitespaceIncludingEmptyPartsIter<'a, T: Iterator> { - s: &'a str, - pos: usize, - chars_iter: T, - is_complete: bool, -} - -impl<'a, T: DoubleEndedIterator> Iterator - for SplitSingleWhitespaceIncludingEmptyPartsIter<'a, T> -{ - type Item = &'a str; - - fn next(&mut self) -> Option { - if self.is_complete { - return None; - } - - let start = self.pos; - - loop { - match self.chars_iter.next() { - Some((i, c)) if c.is_whitespace() => { - let whitespace_len = c.len_utf8(); - self.pos = i + whitespace_len; - return Some(&self.s[start..i]); - } - Some(_) => {} - None => { - self.is_complete = true; - return Some(&self.s[start..self.pos]); - } - } - } - } -} diff --git a/crates/pumpkin/src/command/args/players.rs b/crates/pumpkin/src/command/args/players.rs deleted file mode 100644 index 9fd9a965c..000000000 --- a/crates/pumpkin/src/command/args/players.rs +++ /dev/null @@ -1,103 +0,0 @@ -use std::sync::Arc; - -use pumpkin_protocol::java::client::play::{ArgumentType, SuggestionProviders}; - -use crate::command::CommandSender; -use crate::command::args::{ConsumeResult, ConsumeResultWithSyntax}; -use crate::command::dispatcher::CommandError; -use crate::command::tree::RawArgs; -use crate::entity::player::Player; -use crate::server::Server; - -use super::super::args::ArgumentConsumer; -use super::entities::{ensure_player_only_selector, parse_target_selector_with_context}; -use super::{Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser}; - -/// Select zero, one or multiple players -pub struct PlayersArgumentConsumer; - -impl GetClientSideArgParser for PlayersArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - // todo: investigate why this does not accept target selectors - ArgumentType::Entity { - flags: ArgumentType::ENTITY_FLAG_PLAYERS_ONLY, - } - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - None - } -} - -impl ArgumentConsumer for PlayersArgumentConsumer { - fn consume<'a>( - &'a self, - sender: &'a CommandSender, - server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let s = args.pop().map(|arg| arg.value)?; - - let sync_result: Option>> = match s { - "@s" => match sender { - CommandSender::Player(p) => Some(vec![p.clone()]), - _ => None, - }, - #[expect(clippy::match_same_arms)] - // todo: implement for non-players and remove this line - "@n" | "@p" => match sender { - CommandSender::Player(p) => Some(vec![p.clone()]), - _ => None, - }, - _ => None, - }; - - if let Some(players) = sync_result { - return Some(Arg::Players(players)); - } - - let players = match s { - "@r" => server - .get_random_player() - .map_or_else(|| Some(vec![]), |p| Some(vec![p])), - "@a" | "@e" => Some(server.get_all_players()), - name => server.get_player_by_name(name).map(|p| vec![p]), - }; - - players.map(Arg::Players) - } - - fn consume_with_syntax<'a>( - &'a self, - sender: &'a CommandSender, - server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResultWithSyntax<'a> { - let Some(raw_arg) = args.pop() else { - return Ok(None); - }; - - let selector = parse_target_selector_with_context(raw_arg)?; - ensure_player_only_selector(&selector, raw_arg)?; - - let players = server.select_players(&selector, Some(sender)); - Ok(Some(Arg::Players(players))) - } -} - -impl DefaultNameArgConsumer for PlayersArgumentConsumer { - fn default_name(&self) -> &'static str { - "target" - } -} - -impl<'a> FindArg<'a> for PlayersArgumentConsumer { - type Data = &'a [Arc]; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::Players(data)) => Ok(data), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} diff --git a/crates/pumpkin/src/command/args/position_2d.rs b/crates/pumpkin/src/command/args/position_2d.rs deleted file mode 100644 index 9eef609c2..000000000 --- a/crates/pumpkin/src/command/args/position_2d.rs +++ /dev/null @@ -1,79 +0,0 @@ -use pumpkin_protocol::java::client::play::{ArgumentType, SuggestionProviders}; -use pumpkin_util::math::vector2::Vector2; -use pumpkin_util::math::vector3::Vector3; - -use crate::command::CommandSender; -use crate::command::args::ConsumeResult; -use crate::command::dispatcher::CommandError; -use crate::command::tree::RawArgs; -use crate::server::Server; - -use super::super::args::ArgumentConsumer; -use super::coordinate::MaybeRelativeCoordinate; -use super::{Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser}; - -/// x and z coordinates only -/// -/// todo: implement ~ ^ notations -pub struct Position2DArgumentConsumer; - -impl GetClientSideArgParser for Position2DArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::Vec2 - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - None - } -} - -impl ArgumentConsumer for Position2DArgumentConsumer { - fn consume<'a>( - &'a self, - sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let x_str = args.pop()?.value; - let z_str = args.pop()?.value; - - MaybeRelativePosition2D::try_new(x_str, z_str) - .and_then(|pos| pos.try_to_absolute(sender.position())) - .map(Arg::Pos2D) - } -} - -struct MaybeRelativePosition2D( - MaybeRelativeCoordinate, - MaybeRelativeCoordinate, -); - -impl MaybeRelativePosition2D { - fn try_new(x: &str, z: &str) -> Option { - Some(Self(x.try_into().ok()?, z.try_into().ok()?)) - } - - fn try_to_absolute(self, origin: Option>) -> Option> { - Some(Vector2::new( - self.0.into_absolute(origin.map(|o| o.x))?, - self.1.into_absolute(origin.map(|o| o.z))?, - )) - } -} - -impl DefaultNameArgConsumer for Position2DArgumentConsumer { - fn default_name(&self) -> &'static str { - "pos" - } -} - -impl<'a> FindArg<'a> for Position2DArgumentConsumer { - type Data = Vector2; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::Pos2D(data)) => Ok(*data), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} diff --git a/crates/pumpkin/src/command/args/position_3d.rs b/crates/pumpkin/src/command/args/position_3d.rs deleted file mode 100644 index 7cadbba93..000000000 --- a/crates/pumpkin/src/command/args/position_3d.rs +++ /dev/null @@ -1,83 +0,0 @@ -use pumpkin_protocol::java::client::play::{ArgumentType, SuggestionProviders}; -use pumpkin_util::math::vector3::Vector3; - -use crate::command::CommandSender; -use crate::command::args::ConsumeResult; -use crate::command::dispatcher::CommandError; -use crate::command::tree::RawArgs; -use crate::server::Server; - -use super::super::args::ArgumentConsumer; -use super::coordinate::MaybeRelativeCoordinate; -use super::{Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser}; - -/// x, y and z coordinates -pub struct Position3DArgumentConsumer; - -impl GetClientSideArgParser for Position3DArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::Vec3 - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - None - } -} - -impl ArgumentConsumer for Position3DArgumentConsumer { - fn consume<'a>( - &'a self, - sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let x_str = args.pop()?.value; - let y_str = args.pop()?.value; - let z_str = args.pop()?.value; - - MaybeRelativePosition3D::try_new(x_str, y_str, z_str) - .and_then(|pos| pos.try_to_absolute(sender.position())) - .map(Arg::Pos3D) - } -} - -struct MaybeRelativePosition3D( - MaybeRelativeCoordinate, - MaybeRelativeCoordinate, - MaybeRelativeCoordinate, -); - -impl MaybeRelativePosition3D { - fn try_new(x: &str, y: &str, z: &str) -> Option { - Some(Self( - x.try_into().ok()?, - y.try_into().ok()?, - z.try_into().ok()?, - )) - } - - fn try_to_absolute(self, origin: Option>) -> Option> { - Some(Vector3::new( - self.0.into_absolute(origin.map(|o| o.x))?, - self.1.into_absolute(origin.map(|o| o.y))?, - self.2.into_absolute(origin.map(|o| o.z))?, - )) - } -} - -impl DefaultNameArgConsumer for Position3DArgumentConsumer { - fn default_name(&self) -> &'static str { - "pos" - } -} - -impl<'a> FindArg<'a> for Position3DArgumentConsumer { - type Data = Vector3; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::Pos3D(data)) => Ok(*data), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} diff --git a/crates/pumpkin/src/command/args/position_block.rs b/crates/pumpkin/src/command/args/position_block.rs deleted file mode 100644 index 41d540eb3..000000000 --- a/crates/pumpkin/src/command/args/position_block.rs +++ /dev/null @@ -1,193 +0,0 @@ -use pumpkin_protocol::java::client::play::{ArgumentType, SuggestionProviders}; -use pumpkin_util::math::position::BlockPos; -use pumpkin_util::math::vector3::Vector3; -use pumpkin_util::text::TextComponent; - -use crate::command::CommandSender; -use crate::command::args::ConsumeResult; -use crate::command::dispatcher::CommandError; -use crate::command::tree::RawArgs; -use crate::server::Server; -use crate::world::World; - -use super::super::args::ArgumentConsumer; -use super::coordinate::MaybeRelativeBlockCoordinate; -use super::{Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser}; - -/// x, y and z coordinates -pub struct BlockPosArgumentConsumer; - -impl GetClientSideArgParser for BlockPosArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::BlockPos - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - None - } -} - -impl ArgumentConsumer for BlockPosArgumentConsumer { - fn consume<'a>( - &'a self, - sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let x_str = args.pop()?.value; - let y_str = args.pop()?.value; - let z_str = args.pop()?.value; - - MaybeRelativeBlockPos::try_new(x_str, y_str, z_str) - .and_then(|pos| pos.try_to_absolute(sender)) - .map(Arg::BlockPos) - } -} - -enum MaybeRelativeBlockPos { - World( - MaybeRelativeBlockCoordinate, - MaybeRelativeBlockCoordinate, - MaybeRelativeBlockCoordinate, - ), - Local(f64, f64, f64), -} - -impl MaybeRelativeBlockPos { - fn try_new(x: &str, y: &str, z: &str) -> Option { - if x.starts_with('^') || y.starts_with('^') || z.starts_with('^') { - Some(Self::Local( - parse_local_coordinate(x)?, - parse_local_coordinate(y)?, - parse_local_coordinate(z)?, - )) - } else { - Some(Self::World( - x.try_into().ok()?, - y.try_into().ok()?, - z.try_into().ok()?, - )) - } - } - - fn try_to_absolute(self, sender: &CommandSender) -> Option { - match self { - Self::World(x, y, z) => { - let origin = sender.position(); - Some(BlockPos(Vector3::new( - x.into_absolute(origin.map(|o| o.x))?, - y.into_absolute(origin.map(|o| o.y))?, - z.into_absolute(origin.map(|o| o.z))?, - ))) - } - Self::Local(left, up, forwards) => { - let origin = sender.position()?; - let rotation = sender.rotation()?; - let offset = apply_local_coordinates(rotation, Vector3::new(left, up, forwards)); - Some(BlockPos::floored_v(origin.add(&offset))) - } - } - } -} - -fn parse_local_coordinate(s: &str) -> Option { - let value = s.strip_prefix('^')?; - if value.is_empty() { - Some(0.0) - } else { - value.parse().ok() - } -} - -fn apply_local_coordinates(rotation: (f32, f32), direction: Vector3) -> Vector3 { - let (yaw, pitch) = rotation; - let yaw = f64::from(yaw); - let pitch = f64::from(pitch); - - let y_cos = (yaw + 90.0).to_radians().cos(); - let y_sin = (yaw + 90.0).to_radians().sin(); - let x_cos = (-pitch).to_radians().cos(); - let x_sin = (-pitch).to_radians().sin(); - let x_cos_up = (-pitch + 90.0).to_radians().cos(); - let x_sin_up = (-pitch + 90.0).to_radians().sin(); - - let forwards = Vector3::new(y_cos * x_cos, x_sin, y_sin * x_cos); - let up = Vector3::new(y_cos * x_cos_up, x_sin_up, y_sin * x_cos_up); - let left = Vector3::new( - forwards.y * up.z - forwards.z * up.y, - forwards.z * up.x - forwards.x * up.z, - forwards.x * up.y - forwards.y * up.x, - ) * -1.0; - - Vector3::new( - forwards.x * direction.z + up.x * direction.y + left.x * direction.x, - forwards.y * direction.z + up.y * direction.y + left.y * direction.x, - forwards.z * direction.z + up.z * direction.y + left.z * direction.x, - ) -} - -impl DefaultNameArgConsumer for BlockPosArgumentConsumer { - fn default_name(&self) -> &'static str { - "block_pos" - } -} - -impl<'a> FindArg<'a> for BlockPosArgumentConsumer { - type Data = BlockPos; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::BlockPos(data)) => Ok(*data), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} - -impl BlockPosArgumentConsumer { - pub fn find_loaded_arg( - args: &super::ConsumedArgs, - name: &str, - world: &World, - ) -> Result { - let pos = Self::find_arg(args, name)?; - - if world - .level - .read_chunk_sync(&pos.chunk_position(), |_| ()) - .is_none() - { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - "argument.pos.unloaded", - "argument.pos.unloaded", - [], - ))); - } - - if !world.is_in_build_limit(pos) { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - "argument.pos.outofworld", - "argument.pos.outofworld", - [], - ))); - } - - Ok(pos) - } - - pub fn find_spawnable_arg( - args: &super::ConsumedArgs, - name: &str, - ) -> Result { - let pos = Self::find_arg(args, name)?; - - if !World::is_valid(pos) { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - "argument.pos.outofbounds", - "argument.pos.outofbounds", - [], - ))); - } - - Ok(pos) - } -} diff --git a/crates/pumpkin/src/command/args/resource/damage_type.rs b/crates/pumpkin/src/command/args/resource/damage_type.rs deleted file mode 100644 index 600dd5d30..000000000 --- a/crates/pumpkin/src/command/args/resource/damage_type.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::command::{ - CommandSender, - args::{ - Arg, ArgumentConsumer, ConsumeResult, ConsumedArgs, DefaultNameArgConsumer, FindArg, - GetClientSideArgParser, - }, - dispatcher::CommandError, - tree::RawArgs, -}; -use crate::server::Server; -use pumpkin_data::damage::DamageType; -use pumpkin_protocol::java::client::play::{ArgumentType, SuggestionProviders}; -use pumpkin_util::identifier::Identifier; - -pub struct DamageTypeArgumentConsumer; - -impl GetClientSideArgParser for DamageTypeArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::Resource { - identifier: Identifier::vanilla_static("damage_type"), - } - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - None - } -} - -impl ArgumentConsumer for DamageTypeArgumentConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let name = args.pop().map(|arg| arg.value)?; - let name = name.strip_prefix("minecraft:").unwrap_or(name); - DamageType::from_name(name).map(Arg::DamageType) - } -} - -impl DefaultNameArgConsumer for DamageTypeArgumentConsumer { - fn default_name(&self) -> &'static str { - "damage_type" - } -} - -impl<'a> FindArg<'a> for DamageTypeArgumentConsumer { - type Data = &'a DamageType; - - fn find_arg(args: &'a ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::DamageType(data)) => Ok(data), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} diff --git a/crates/pumpkin/src/command/args/resource/effect.rs b/crates/pumpkin/src/command/args/resource/effect.rs deleted file mode 100644 index fee469fa8..000000000 --- a/crates/pumpkin/src/command/args/resource/effect.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::command::{ - CommandSender, - args::{ - Arg, ArgumentConsumer, ConsumeResult, ConsumedArgs, DefaultNameArgConsumer, FindArg, - GetClientSideArgParser, - }, - dispatcher::CommandError, - tree::RawArgs, -}; -use crate::server::Server; -use pumpkin_data::effect::StatusEffect; -use pumpkin_protocol::java::client::play::{ArgumentType, SuggestionProviders}; -use pumpkin_util::identifier::Identifier; - -pub struct EffectTypeArgumentConsumer; - -impl GetClientSideArgParser for EffectTypeArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::Resource { - identifier: Identifier::vanilla_static("mob_effect"), - } - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - None - } -} - -impl ArgumentConsumer for EffectTypeArgumentConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let name = args.pop().map(|arg| arg.value)?; - let status_effect = - StatusEffect::from_name(name.strip_prefix("minecraft:").unwrap_or(name))?; - Some(Arg::Effect(status_effect)) - } -} - -impl DefaultNameArgConsumer for EffectTypeArgumentConsumer { - fn default_name(&self) -> &'static str { - "mob_effect" - } -} - -impl<'a> FindArg<'a> for EffectTypeArgumentConsumer { - type Data = &'static StatusEffect; - - fn find_arg(args: &'a ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::Effect(data)) => Ok(data), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} diff --git a/crates/pumpkin/src/command/args/resource/enchantment.rs b/crates/pumpkin/src/command/args/resource/enchantment.rs deleted file mode 100644 index b8ad89557..000000000 --- a/crates/pumpkin/src/command/args/resource/enchantment.rs +++ /dev/null @@ -1,56 +0,0 @@ -use crate::command::{ - CommandSender, - args::{ - Arg, ArgumentConsumer, ConsumeResult, ConsumedArgs, DefaultNameArgConsumer, FindArg, - GetClientSideArgParser, - }, - dispatcher::CommandError, - tree::RawArgs, -}; -use crate::server::Server; -use pumpkin_data::Enchantment; -use pumpkin_protocol::java::client::play::{ArgumentType, SuggestionProviders}; -use pumpkin_util::identifier::Identifier; - -pub struct EnchantmentArgumentConsumer; - -impl GetClientSideArgParser for EnchantmentArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::Resource { - identifier: Identifier::vanilla_static("enchantment"), - } - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - None - } -} - -impl ArgumentConsumer for EnchantmentArgumentConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let name = args.pop().map(|arg| arg.value)?; - Enchantment::from_name(name).map(Arg::Enchantment) - } -} - -impl DefaultNameArgConsumer for EnchantmentArgumentConsumer { - fn default_name(&self) -> &'static str { - "enchantment" - } -} - -impl<'a> FindArg<'a> for EnchantmentArgumentConsumer { - type Data = &'static Enchantment; - - fn find_arg(args: &'a ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::Enchantment(data)) => Ok(data), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} diff --git a/crates/pumpkin/src/command/args/resource/item.rs b/crates/pumpkin/src/command/args/resource/item.rs deleted file mode 100644 index fee100be2..000000000 --- a/crates/pumpkin/src/command/args/resource/item.rs +++ /dev/null @@ -1,297 +0,0 @@ -use pumpkin_data::item_stack::ItemStack; -use pumpkin_data::{ - data_component::DataComponent, - item::Item, - tag::{RegistryKey, get_tag_ids}, -}; -use pumpkin_protocol::java::client::play::{ArgumentType, SuggestionProviders}; -use pumpkin_util::text::TextComponent; - -use crate::command::{ - CommandSender, - args::{ - Arg, ArgumentConsumer, ConsumeResult, ConsumedArgs, DefaultNameArgConsumer, FindArg, - GetClientSideArgParser, - }, - dispatcher::CommandError, - tree::RawArgs, -}; -use crate::server::Server; - -pub struct ItemArgumentConsumer; - -impl GetClientSideArgParser for ItemArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::ItemStack - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - None - } -} - -impl ArgumentConsumer for ItemArgumentConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - args.pop().map(|arg| Arg::Item(arg.value)) - } -} - -impl DefaultNameArgConsumer for ItemArgumentConsumer { - fn default_name(&self) -> &'static str { - "item" - } -} - -impl<'a> FindArg<'a> for ItemArgumentConsumer { - type Data = (&'a str, ItemStack); - - fn find_arg(args: &'a ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::Item(raw_name)) => { - let item_id = raw_name.split('[').next().unwrap_or(raw_name); - let item = Item::from_registry_key(item_id).ok_or_else(|| { - if item_id.starts_with("minecraft:") { - CommandError::CommandFailed(TextComponent::translate_cross( - "argument.item.id.invalid", - "argument.item.id.invalid", - [TextComponent::text((*item_id).to_string())], - )) - } else { - CommandError::CommandFailed(TextComponent::translate_cross( - "argument.item.id.invalid", - "argument.item.id.invalid", - [TextComponent::text("minecraft:".to_string() + item_id)], - )) - } - })?; - - let mut patch = Vec::new(); - // Parse optional components inside brackets `[...]` - if let Some(start_idx) = raw_name.find('[').filter(|_| raw_name.ends_with(']')) { - let inner = &raw_name[start_idx + 1..raw_name.len() - 1]; - // Split components by comma, but we must ignore commas inside curly braces/brackets! - let mut components = Vec::new(); - let mut filter_start = 0usize; - let mut curly_depth = 0; - let mut bracket_depth = 0; - for (i, c) in inner.char_indices() { - if c == '{' { - curly_depth += 1; - } else if c == '}' { - curly_depth -= 1; - } else if c == '[' { - bracket_depth += 1; - } else if c == ']' { - bracket_depth -= 1; - } else if c == ',' && curly_depth == 0 && bracket_depth == 0 { - components.push(&inner[filter_start..i]); - filter_start = i + 1; - } - } - components.push(&inner[filter_start..]); - - for comp in components { - let comp = comp.trim(); - if comp.is_empty() { - continue; - } - let mut parts = comp.splitn(2, '='); - if let Some(comp_key) = parts.next() { - let comp_key = comp_key.trim(); - if let Some(comp_val_str) = parts.next() { - let comp_val_str = comp_val_str.trim(); - // Parse value string as SNBT - let mut reader = - crate::command::string_reader::StringReader::new(comp_val_str); - if let Ok(nbt_tag) = - crate::command::snbt::SnbtParser::parse_for_commands( - &mut reader, - ) - { - // Match the DataComponent key - if let (Some(data_comp), Some(comp_impl)) = ( - DataComponent::try_from_name(comp_key), - pumpkin_data::data_component_impl::read_data( - DataComponent::try_from_name(comp_key) - .unwrap_or(DataComponent::CustomData), - &nbt_tag, - ), - ) { - patch.push((data_comp, Some(comp_impl))); - } - } - } - } - } - } - - let mut stack = ItemStack::new(1, item); - if !patch.is_empty() { - stack.patch = patch; - } - Ok((*raw_name, stack)) - } - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} - -pub struct ItemPredicateArgumentConsumer; - -pub enum ItemPredicate { - Item(&'static Item), - Tag(Vec), - Any, -} - -impl ItemPredicate { - #[must_use] - pub fn test_item_stack(&self, stack: &ItemStack) -> bool { - match self { - Self::Any => true, - Self::Item(item) => stack.get_item() == *item, - Self::Tag(tag) => tag.contains(&stack.get_item().id), - } - } -} - -impl GetClientSideArgParser for ItemPredicateArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::ItemPredicate - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - None - } -} - -impl ArgumentConsumer for ItemPredicateArgumentConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - args.pop().map(|arg| Arg::Item(arg.value)) - } -} - -impl DefaultNameArgConsumer for ItemPredicateArgumentConsumer { - fn default_name(&self) -> &'static str { - "item" - } -} - -impl<'a> FindArg<'a> for ItemPredicateArgumentConsumer { - type Data = ItemPredicate; - - fn find_arg(args: &'a ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::Item(raw_name)) => { - if *raw_name == "*" { - return Ok(ItemPredicate::Any); - } - let clean_name = raw_name.split('[').next().unwrap_or(raw_name); - clean_name.strip_prefix("#").map_or_else( - || { - Item::from_registry_key(clean_name).map_or_else( - || { - if clean_name.starts_with("minecraft:") { - Err(CommandError::CommandFailed( - TextComponent::translate_cross( - "argument.item.id.invalid", - "argument.item.id.invalid", - [TextComponent::text((*clean_name).to_string())], - ), - )) - } else { - Err(CommandError::CommandFailed( - TextComponent::translate_cross( - "argument.item.id.invalid", - "argument.item.id.invalid", - [TextComponent::text( - "minecraft:".to_string() + clean_name, - )], - ), - )) - } - }, - |item| Ok(ItemPredicate::Item(item)), - ) - }, - |tag| { - get_tag_ids(RegistryKey::Item, tag).map_or_else( - || { - Err(CommandError::CommandFailed(TextComponent::translate_cross( - "arguments.item.tag.unknown", - "arguments.item.tag.unknown", - [TextComponent::text((*tag).to_string())], - ))) - }, - |items| Ok(ItemPredicate::Tag(items.to_vec())), - ) - }, - ) - } - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} - -#[cfg(test)] -mod test { - use super::*; - use std::collections::HashMap; - - #[test] - fn parse_plain_item() { - let mut args = HashMap::new(); - args.insert("item", Arg::Item("minecraft:stick")); - - let res = ItemArgumentConsumer::find_arg(&args, "item"); - assert!(res.is_ok()); - let (name, stack) = res.unwrap(); - assert_eq!(name, "minecraft:stick"); - assert_eq!(stack.item.registry_key, "stick"); - assert!(stack.patch.is_empty()); - } - - #[test] - fn parse_item_with_profile_component() { - let mut args = HashMap::new(); - args.insert( - "item", - Arg::Item("player_head[minecraft:profile={name:\"Username\"}]"), - ); - - let res = ItemArgumentConsumer::find_arg(&args, "item"); - assert!(res.is_ok()); - let (name, stack) = res.unwrap(); - assert_eq!(name, "player_head[minecraft:profile={name:\"Username\"}]"); - assert_eq!(stack.item.registry_key, "player_head"); - assert_eq!(stack.patch.len(), 1); - assert_eq!(stack.patch[0].0.to_id(), DataComponent::Profile.to_id()); - } - - #[test] - fn parse_item_with_custom_name() { - let mut args = HashMap::new(); - args.insert( - "item", - Arg::Item("stick[minecraft:custom_name=\"Magic wand\"]"), - ); - - let res = ItemArgumentConsumer::find_arg(&args, "item"); - assert!(res.is_ok()); - let (name, stack) = res.unwrap(); - assert_eq!(name, "stick[minecraft:custom_name=\"Magic wand\"]"); - assert_eq!(stack.item.registry_key, "stick"); - assert_eq!(stack.patch.len(), 1); - assert_eq!(stack.patch[0].0.to_id(), DataComponent::CustomName.to_id()); - } -} diff --git a/crates/pumpkin/src/command/args/resource/mod.rs b/crates/pumpkin/src/command/args/resource/mod.rs deleted file mode 100644 index 1a22fc261..000000000 --- a/crates/pumpkin/src/command/args/resource/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod damage_type; -pub mod effect; -pub mod enchantment; -pub mod item; -pub mod particle; diff --git a/crates/pumpkin/src/command/args/resource/particle.rs b/crates/pumpkin/src/command/args/resource/particle.rs deleted file mode 100644 index 65abca24e..000000000 --- a/crates/pumpkin/src/command/args/resource/particle.rs +++ /dev/null @@ -1,56 +0,0 @@ -use crate::command::{ - CommandSender, - args::{ - Arg, ArgumentConsumer, ConsumeResult, ConsumedArgs, DefaultNameArgConsumer, FindArg, - GetClientSideArgParser, - }, - dispatcher::CommandError, - tree::RawArgs, -}; -use crate::server::Server; -use pumpkin_data::particle::Particle; -use pumpkin_protocol::java::client::play::{ArgumentType, SuggestionProviders}; -use pumpkin_util::identifier::Identifier; - -pub struct ParticleArgumentConsumer; - -impl GetClientSideArgParser for ParticleArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::Resource { - identifier: Identifier::vanilla_static("particle_type"), - } - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - None - } -} - -impl ArgumentConsumer for ParticleArgumentConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let name = args.pop().map(|arg| arg.value)?; - Particle::from_name(name.strip_prefix("minecraft:").unwrap_or(name)).map(Arg::Particle) - } -} - -impl DefaultNameArgConsumer for ParticleArgumentConsumer { - fn default_name(&self) -> &'static str { - "particle_type" - } -} - -impl<'a> FindArg<'a> for ParticleArgumentConsumer { - type Data = &'a Particle; - - fn find_arg(args: &'a ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::Particle(data)) => Ok(data), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} diff --git a/crates/pumpkin/src/command/args/resource_location.rs b/crates/pumpkin/src/command/args/resource_location.rs deleted file mode 100644 index cdafcbbc2..000000000 --- a/crates/pumpkin/src/command/args/resource_location.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::command::CommandSender; -use crate::command::args::{ - Arg, ArgumentConsumer, ConsumeResult, DefaultNameArgConsumer, FindArg, GetClientSideArgParser, -}; -use crate::command::dispatcher::CommandError; -use crate::command::tree::RawArgs; -use crate::server::Server; -use pumpkin_protocol::java::client::play::{ArgumentType, SuggestionProviders}; -use pumpkin_util::identifier::Identifier; - -// TODO: Add proper autocomplete -pub struct ResourceLocationArgumentConsumer; - -impl GetClientSideArgParser for ResourceLocationArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::ResourceLocation - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - Some(SuggestionProviders::AskServer) - } -} - -impl ArgumentConsumer for ResourceLocationArgumentConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let identifier = args.pop().and_then(|arg| Identifier::parse(arg.value).ok()); - - identifier.map(Arg::ResourceLocation) - } -} - -impl DefaultNameArgConsumer for ResourceLocationArgumentConsumer { - fn default_name(&self) -> &'static str { - "id" - } -} - -impl<'a> FindArg<'a> for ResourceLocationArgumentConsumer { - type Data = &'a Identifier; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::ResourceLocation(data)) => Ok(data), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} diff --git a/crates/pumpkin/src/command/args/rotation.rs b/crates/pumpkin/src/command/args/rotation.rs deleted file mode 100644 index 3f9681350..000000000 --- a/crates/pumpkin/src/command/args/rotation.rs +++ /dev/null @@ -1,82 +0,0 @@ -use pumpkin_protocol::java::client::play::{ArgumentType, SuggestionProviders}; - -use crate::command::CommandSender; -use crate::command::args::ConsumeResult; -use crate::command::dispatcher::CommandError; -use crate::command::tree::RawArgs; -use crate::server::Server; - -use super::super::args::ArgumentConsumer; -use super::{Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser}; - -/// Parses a rotation value that may be relative (prefixed with ~). -/// Returns (value, `is_relative`). -fn parse_rotation_component(s: &str) -> Option<(f32, bool)> { - if let Some(rest) = s.strip_prefix('~') { - // Relative rotation - let value = if rest.is_empty() { - 0.0 - } else { - rest.parse::().ok()? - }; - Some((value, true)) - } else { - // Absolute rotation - let mut value = s.parse::().ok()?; - value %= 360.0; - if value >= 180.0 { - value -= 360.0; - } - Some((value, false)) - } -} - -/// yaw and pitch with optional relative prefix (~) -pub struct RotationArgumentConsumer; - -impl GetClientSideArgParser for RotationArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::Rotation - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - None - } -} - -impl ArgumentConsumer for RotationArgumentConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let yaw_str = args.pop()?.value; - let pitch_str = args.pop()?.value; - - let (yaw, yaw_rel) = parse_rotation_component(yaw_str)?; - let (pitch, pitch_rel) = parse_rotation_component(pitch_str)?; - - Some(Arg::Rotation(yaw, yaw_rel, pitch, pitch_rel)) - } -} - -impl DefaultNameArgConsumer for RotationArgumentConsumer { - fn default_name(&self) -> &'static str { - "rotation" - } -} - -impl<'a> FindArg<'a> for RotationArgumentConsumer { - /// (yaw, `is_yaw_relative`, pitch, `is_pitch_relative`) - type Data = (f32, bool, f32, bool); - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::Rotation(yaw, yaw_rel, pitch, pitch_rel)) => { - Ok((*yaw, *yaw_rel, *pitch, *pitch_rel)) - } - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} diff --git a/crates/pumpkin/src/command/args/simple.rs b/crates/pumpkin/src/command/args/simple.rs deleted file mode 100644 index df68a27b1..000000000 --- a/crates/pumpkin/src/command/args/simple.rs +++ /dev/null @@ -1,51 +0,0 @@ -use pumpkin_protocol::java::client::play::{ - ArgumentType, StringProtoArgBehavior, SuggestionProviders, -}; - -use crate::{ - command::{args::ConsumeResult, dispatcher::CommandError}, - server::Server, -}; - -use super::{ - super::{ - CommandSender, - args::{ArgumentConsumer, RawArgs}, - }, - Arg, FindArg, GetClientSideArgParser, -}; - -/// Should never be a permanent solution -pub struct SimpleArgConsumer; - -impl GetClientSideArgParser for SimpleArgConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::String(StringProtoArgBehavior::SingleWord) - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - None - } -} - -impl ArgumentConsumer for SimpleArgConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - args.pop().map(|arg| Arg::Simple(arg.value)) - } -} - -impl<'a> FindArg<'a> for SimpleArgConsumer { - type Data = &'a str; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::Simple(data)) => Ok(data), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} diff --git a/crates/pumpkin/src/command/args/slot.rs b/crates/pumpkin/src/command/args/slot.rs deleted file mode 100644 index 5a79e4328..000000000 --- a/crates/pumpkin/src/command/args/slot.rs +++ /dev/null @@ -1,99 +0,0 @@ -use pumpkin_data::slot_ranges::get_slot_range; -use pumpkin_protocol::java::client::play::{ArgumentType, SuggestionProviders}; - -use crate::{ - command::{CommandSender, args::ConsumeResult, dispatcher::CommandError, tree::RawArgs}, - server::Server, -}; - -use super::{Arg, ArgumentConsumer, DefaultNameArgConsumer, FindArg, GetClientSideArgParser}; - -pub struct SlotArgumentConsumer; - -impl GetClientSideArgParser for SlotArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::ItemSlot - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - None - } -} - -impl ArgumentConsumer for SlotArgumentConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let s = args.pop()?.value; - if let Some(range) = get_slot_range(s) - && range.len() == 1 - { - return Some(Arg::Slot(range[0], s.to_string())); - } - None - } -} - -impl DefaultNameArgConsumer for SlotArgumentConsumer { - fn default_name(&self) -> &'static str { - "slot" - } -} - -impl<'a> FindArg<'a> for SlotArgumentConsumer { - type Data = (usize, String); - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::Slot(slot, name)) => Ok((*slot, name.clone())), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} - -pub struct SlotsArgumentConsumer; - -impl GetClientSideArgParser for SlotsArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::ItemSlots - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - None - } -} - -impl ArgumentConsumer for SlotsArgumentConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let s = args.pop()?.value; - if let Some(range) = get_slot_range(s) { - return Some(Arg::Slots(range, s.to_string())); - } - None - } -} - -impl DefaultNameArgConsumer for SlotsArgumentConsumer { - fn default_name(&self) -> &'static str { - "slots" - } -} - -impl<'a> FindArg<'a> for SlotsArgumentConsumer { - type Data = (&'static [usize], String); - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::Slots(slots, name)) => Ok((*slots, name.clone())), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} diff --git a/crates/pumpkin/src/command/args/sound.rs b/crates/pumpkin/src/command/args/sound.rs deleted file mode 100644 index 2a71926b4..000000000 --- a/crates/pumpkin/src/command/args/sound.rs +++ /dev/null @@ -1,67 +0,0 @@ -use pumpkin_data::sound::Sound; -use pumpkin_protocol::java::client::play::{ArgumentType, SuggestionProviders}; -use pumpkin_util::text::TextComponent; - -use crate::{ - command::{args::ConsumeResult, dispatcher::CommandError}, - server::Server, -}; - -use super::{ - super::{ - CommandSender, - args::{ArgumentConsumer, RawArgs}, - }, - Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser, -}; - -pub struct SoundArgumentConsumer; - -impl GetClientSideArgParser for SoundArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::ResourceLocation - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - Some(SuggestionProviders::AvailableSounds) - } -} - -impl ArgumentConsumer for SoundArgumentConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - args.pop().map(|arg| Arg::Block(arg.value)) - } -} - -impl DefaultNameArgConsumer for SoundArgumentConsumer { - fn default_name(&self) -> &'static str { - "available_sounds" - } -} - -impl<'a> FindArg<'a> for SoundArgumentConsumer { - type Data = Sound; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::Block(name)) => { - Sound::from_name(name.strip_prefix("minecraft:").unwrap_or(name)).map_or_else( - || { - Err(CommandError::CommandFailed(TextComponent::translate_cross( - pumpkin_data::translation::java::ARGUMENT_ID_INVALID, - pumpkin_data::translation::java::ARGUMENT_ID_INVALID, - [], - ))) - }, - Result::Ok, - ) - } - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} diff --git a/crates/pumpkin/src/command/args/sound_category.rs b/crates/pumpkin/src/command/args/sound_category.rs deleted file mode 100644 index 05a785829..000000000 --- a/crates/pumpkin/src/command/args/sound_category.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::command::CommandSender; -use crate::command::args::{ - Arg, ArgumentConsumer, ConsumeResult, DefaultNameArgConsumer, FindArg, GetClientSideArgParser, - SuggestResult, -}; -use crate::command::dispatcher::CommandError; -use crate::command::tree::RawArgs; -use crate::server::Server; -use pumpkin_data::sound::SoundCategory; -use pumpkin_protocol::java::client::play::{ArgumentType, CommandSuggestion, SuggestionProviders}; - -/// `ArgumentConsumer` for Minecraft sound categories (master, music, record, etc.) -pub struct SoundCategoryArgumentConsumer; - -impl GetClientSideArgParser for SoundCategoryArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - // ResourceLocation is used for enumerated string values - ArgumentType::ResourceLocation - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - // Force server-side suggestions to show available sound categories - Some(SuggestionProviders::AskServer) - } -} - -impl ArgumentConsumer for SoundCategoryArgumentConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let s_opt: Option<&'a str> = args.pop().map(|arg| arg.value); - - s_opt.and_then(|s| { - let category = match s.to_lowercase().as_str() { - "master" => Some(SoundCategory::Master), - "music" => Some(SoundCategory::Music), - // i don't use SoundCategory::from_name because its is record and not records :c - "record" => Some(SoundCategory::Records), - "weather" => Some(SoundCategory::Weather), - "block" => Some(SoundCategory::Blocks), - "hostile" => Some(SoundCategory::Hostile), - "neutral" => Some(SoundCategory::Neutral), - "player" => Some(SoundCategory::Players), - "ambient" => Some(SoundCategory::Ambient), - "voice" => Some(SoundCategory::Voice), - _ => None, - }; - - category.map(Arg::SoundCategory) - }) - } - - fn suggest(&self, _sender: &CommandSender, _server: &Server, _input: &str) -> SuggestResult { - let categories = [ - "master", "music", "record", "weather", "block", "hostile", "neutral", "player", - "ambient", "voice", - ]; - let suggestions: Vec = categories - .iter() - .map(|cat| CommandSuggestion::new((*cat).to_string(), None)) - .collect(); - Ok(Some(suggestions)) - } -} - -impl DefaultNameArgConsumer for SoundCategoryArgumentConsumer { - fn default_name(&self) -> &'static str { - "source" - } -} - -impl<'a> FindArg<'a> for SoundCategoryArgumentConsumer { - type Data = &'a SoundCategory; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::SoundCategory(data)) => Ok(data), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} diff --git a/crates/pumpkin/src/command/args/summonable_entities.rs b/crates/pumpkin/src/command/args/summonable_entities.rs deleted file mode 100644 index 562bdafca..000000000 --- a/crates/pumpkin/src/command/args/summonable_entities.rs +++ /dev/null @@ -1,67 +0,0 @@ -use pumpkin_data::entity::EntityType; -use pumpkin_protocol::java::client::play::{ArgumentType, SuggestionProviders}; -use pumpkin_util::text::TextComponent; - -use crate::{ - command::{args::ConsumeResult, dispatcher::CommandError}, - server::Server, -}; - -use super::{ - super::{ - CommandSender, - args::{ArgumentConsumer, RawArgs}, - }, - Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser, -}; - -pub struct SummonableEntitiesArgumentConsumer; - -impl GetClientSideArgParser for SummonableEntitiesArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::ResourceLocation - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - Some(SuggestionProviders::SummonableEntities) - } -} - -impl ArgumentConsumer for SummonableEntitiesArgumentConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - args.pop().map(|arg| Arg::Block(arg.value)) - } -} - -impl DefaultNameArgConsumer for SummonableEntitiesArgumentConsumer { - fn default_name(&self) -> &'static str { - "summonable_entities" - } -} - -impl<'a> FindArg<'a> for SummonableEntitiesArgumentConsumer { - type Data = &'static EntityType; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::Block(name)) => { - EntityType::from_name(name.strip_prefix("minecraft:").unwrap_or(name)).map_or_else( - || { - Err(CommandError::CommandFailed(TextComponent::translate_cross( - pumpkin_data::translation::java::ARGUMENT_ENTITY_INVALID, - pumpkin_data::translation::java::ARGUMENT_ENTITY_INVALID, - [], - ))) - }, - Result::Ok, - ) - } - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} diff --git a/crates/pumpkin/src/command/args/team_color.rs b/crates/pumpkin/src/command/args/team_color.rs deleted file mode 100644 index 2a068a6ea..000000000 --- a/crates/pumpkin/src/command/args/team_color.rs +++ /dev/null @@ -1,120 +0,0 @@ -use pumpkin_data::translation; -use pumpkin_protocol::java::client::play::{ArgumentType, CommandSuggestion, SuggestionProviders}; -use pumpkin_util::text::TextComponent; -use pumpkin_util::text::color::NamedColor; - -use crate::command::CommandSender; -use crate::command::args::{ - Arg, ArgumentConsumer, ConsumeResult, ConsumeResultWithSyntax, DefaultNameArgConsumer, FindArg, - GetClientSideArgParser, SuggestResult, -}; -use crate::command::dispatcher::CommandError; -use crate::command::errors::error_types::CommandErrorType; -use crate::command::tree::RawArgs; -use crate::server::Server; - -pub const INVALID_COLOR_ERROR_TYPE: CommandErrorType<1> = CommandErrorType::new( - translation::java::ARGUMENT_COLOR_INVALID, - translation::java::ARGUMENT_COLOR_INVALID, -); - -const TEAM_COLORS: [&str; 16] = [ - "black", - "dark_blue", - "dark_green", - "dark_aqua", - "dark_red", - "dark_purple", - "gold", - "gray", - "dark_gray", - "blue", - "green", - "aqua", - "red", - "light_purple", - "yellow", - "white", -]; - -pub struct TeamColorArgumentConsumer; - -impl GetClientSideArgParser for TeamColorArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::Color - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - Some(SuggestionProviders::AskServer) - } -} - -impl ArgumentConsumer for TeamColorArgumentConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let s_opt: Option<&'a str> = args.pop().map(|arg| arg.value); - - s_opt.and_then(|s| NamedColor::try_from(s).ok().map(Arg::TeamColor)) - } - - fn consume_with_syntax<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResultWithSyntax<'a> { - let Some(raw_arg) = args.pop() else { - return Ok(None); - }; - - NamedColor::try_from(raw_arg.value) - .map(|color| Some(Arg::TeamColor(color))) - .map_err(|()| { - INVALID_COLOR_ERROR_TYPE.create_without_context_args_slice(&[TextComponent::text( - raw_arg.value.to_string(), - )]) - }) - } - - fn suggest(&self, _sender: &CommandSender, _server: &Server, input: &str) -> SuggestResult { - let suggestions: Vec = TEAM_COLORS - .iter() - .filter(|color| color.starts_with(input)) - .map(|color| CommandSuggestion::new((*color).to_string(), None)) - .collect(); - Ok(Some(suggestions)) - } -} - -impl DefaultNameArgConsumer for TeamColorArgumentConsumer { - fn default_name(&self) -> &'static str { - "color" - } -} - -impl<'a> FindArg<'a> for TeamColorArgumentConsumer { - type Data = NamedColor; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::TeamColor(color)) => Ok(*color), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse_team_color() { - assert_eq!(NamedColor::try_from("red"), Ok(NamedColor::Red)); - assert_eq!(NamedColor::try_from("dark_blue"), Ok(NamedColor::DarkBlue)); - assert_eq!(NamedColor::try_from("invalid"), Err(())); - } -} diff --git a/crates/pumpkin/src/command/args/textcomponent.rs b/crates/pumpkin/src/command/args/textcomponent.rs deleted file mode 100644 index 5753032a9..000000000 --- a/crates/pumpkin/src/command/args/textcomponent.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::command::CommandSender; -use crate::command::args::{Arg, ArgumentConsumer, ConsumeResult, FindArg, GetClientSideArgParser}; -use crate::command::dispatcher::CommandError; -use crate::command::tree::RawArgs; -use crate::server::Server; -use pumpkin_protocol::java::client::play::{ArgumentType, SuggestionProviders}; -use pumpkin_util::text::TextComponent; -use tracing::debug; - -pub struct TextComponentArgConsumer; - -impl GetClientSideArgParser for TextComponentArgConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::Component - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - None - } -} - -impl ArgumentConsumer for TextComponentArgConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let s = args.pop().map(|arg| arg.value)?; - - let text_component_opt = parse_text_component(s); - - // TODO: Allow identifiers (starting with alphabetic or _, then alphanumeric+-_.) as display names - text_component_opt.map_or_else( - || { - (s.starts_with('"') && s.ends_with('"')).then(|| { - let s_owned = s.replace('"', ""); - Arg::TextComponent(TextComponent::text(s_owned)) - }) - }, - |text_component| Some(Arg::TextComponent(text_component)), - ) - } -} - -impl FindArg<'_> for TextComponentArgConsumer { - type Data = TextComponent; - - fn find_arg(args: &super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::TextComponent(data)) => Ok(data.clone()), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} - -fn parse_text_component(input: &str) -> Option { - serde_json::from_str(input) - .map_err(|e| debug!("Failed to parse text component: {e}")) - .ok() -} diff --git a/crates/pumpkin/src/command/args/time.rs b/crates/pumpkin/src/command/args/time.rs deleted file mode 100644 index a02e845a3..000000000 --- a/crates/pumpkin/src/command/args/time.rs +++ /dev/null @@ -1,117 +0,0 @@ -use pumpkin_protocol::java::client::play::{ArgumentType, SuggestionProviders}; - -use crate::command::{ - CommandSender, - args::{ - Arg, ArgumentConsumer, ConsumeResult, ConsumeResultWithSyntax, DefaultNameArgConsumer, - FindArg, GetClientSideArgParser, - }, - argument_types::{argument_type::ArgumentType as _, time::TimeArgumentType}, - dispatcher::CommandError, - errors::command_syntax_error::{CommandSyntaxError, CommandSyntaxErrorContext}, - string_reader::StringReader, - tree::{RawArg, RawArgs}, -}; -use crate::server::Server; - -#[derive(Clone, Copy, Debug)] -pub struct TimeArgumentConsumer { - min: i32, -} - -impl Default for TimeArgumentConsumer { - fn default() -> Self { - Self::new() - } -} - -impl TimeArgumentConsumer { - #[must_use] - pub const fn new() -> Self { - Self { min: 0 } - } - - #[must_use] - pub const fn min(min: i32) -> Self { - Self { min } - } -} - -fn map_local_syntax_error(error: CommandSyntaxError, raw_arg: RawArg<'_>) -> CommandSyntaxError { - let local_cursor = error.context.map_or(0, |context| context.cursor); - let mut clamped_local_cursor = local_cursor.min(raw_arg.value.len()); - while clamped_local_cursor > 0 && !raw_arg.value.is_char_boundary(clamped_local_cursor) { - clamped_local_cursor -= 1; - } - - CommandSyntaxError { - error_type: error.error_type, - message: error.message, - context: Some(CommandSyntaxErrorContext { - input: raw_arg.input.to_string(), - cursor: raw_arg.start + clamped_local_cursor, - }), - } -} - -impl GetClientSideArgParser for TimeArgumentConsumer { - fn get_client_side_parser(&self) -> ArgumentType { - ArgumentType::Time { min: self.min } - } - - fn get_client_side_suggestion_type_override(&self) -> Option { - None - } -} - -impl ArgumentConsumer for TimeArgumentConsumer { - fn consume<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResult<'a> { - let s_opt: Option<&'a str> = args.pop().map(|arg| arg.value); - - s_opt.and_then(|s| { - let mut reader = StringReader::new(s); - let parser = TimeArgumentType::new(self.min); - parser.parse(&mut reader).ok().map(Arg::Time) - }) - } - - fn consume_with_syntax<'a>( - &'a self, - _sender: &'a CommandSender, - _server: &'a Server, - args: &mut RawArgs<'a>, - ) -> ConsumeResultWithSyntax<'a> { - let Some(raw_arg) = args.pop() else { - return Ok(None); - }; - - let mut reader = StringReader::new(raw_arg.value); - let parser = TimeArgumentType::new(self.min); - parser - .parse(&mut reader) - .map(|ticks| Some(Arg::Time(ticks))) - .map_err(|error| map_local_syntax_error(error, raw_arg)) - } -} - -impl DefaultNameArgConsumer for TimeArgumentConsumer { - fn default_name(&self) -> &'static str { - "time" - } -} - -impl<'a> FindArg<'a> for TimeArgumentConsumer { - type Data = i32; - - fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(Arg::Time(ticks)) => Ok(*ticks), - _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), - } - } -} diff --git a/crates/pumpkin/src/command/argument_builder.rs b/crates/pumpkin/src/command/argument_builder.rs index 628046310..834c50a73 100644 --- a/crates/pumpkin/src/command/argument_builder.rs +++ b/crates/pumpkin/src/command/argument_builder.rs @@ -113,6 +113,7 @@ pub struct CommandArgumentBuilder { common: CommonArgumentBuilder, literal: Cow<'static, str>, description: Cow<'static, str>, + source: Option, } /// A builder that builds an argument [`DetachedNode`]. @@ -344,8 +345,16 @@ impl CommandArgumentBuilder { common: CommonArgumentBuilder::new(), literal: literal.into(), description: description.into(), + source: None, } } + + /// Sets the source (e.g. plugin name) that registered this command. + #[must_use] + pub fn with_source(mut self, source: impl Into) -> Self { + self.source = Some(source.into()); + self + } } impl RequiredArgumentBuilder { @@ -408,6 +417,7 @@ impl ArgumentBuilder for CommandArgumentBuilder { self.common.modifier, self.common.forks, ); + node.meta.source = self.source; node.children = self.common.arguments; node } diff --git a/crates/pumpkin/src/command/argument_types/block_predicate.rs b/crates/pumpkin/src/command/argument_types/block_predicate.rs new file mode 100644 index 000000000..81ab2e91b --- /dev/null +++ b/crates/pumpkin/src/command/argument_types/block_predicate.rs @@ -0,0 +1,228 @@ +use std::collections::HashMap; + +use pumpkin_data::tag::{RegistryKey, get_tag_ids}; +use pumpkin_data::{Block, translation}; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_util::text::TextComponent; + +use crate::command::argument_types::argument_type::{ArgumentType, JavaClientArgumentType}; +use crate::command::argument_types::block::INVALID_BLOCK_ERROR_TYPE; +use crate::command::argument_types::nbt::NbtCompoundArgumentType; +use crate::command::context::command_context::CommandContext; +use crate::command::errors::command_syntax_error::CommandSyntaxError; +use crate::command::errors::error_types::CommandErrorType; +use crate::command::string_reader::StringReader; +use crate::command::suggestion::suggestions::{Suggestions, SuggestionsBuilder}; + +pub const ERROR_UNKNOWN_TAG: CommandErrorType<1> = CommandErrorType::new( + translation::java::ARGUMENTS_BLOCK_TAG_UNKNOWN, + translation::java::ARGUMENTS_BLOCK_TAG_UNKNOWN, +); + +pub const ERROR_NO_VALUE: CommandErrorType<1> = CommandErrorType::new( + translation::java::ARGUMENT_BLOCK_PROPERTY_NOVALUE, + translation::java::ARGUMENT_BLOCK_PROPERTY_NOVALUE, +); + +pub const ERROR_UNCLOSED_PROPERTIES: CommandErrorType<0> = CommandErrorType::new( + translation::java::ARGUMENT_BLOCK_PROPERTY_UNCLOSED, + translation::java::ARGUMENT_BLOCK_PROPERTY_UNCLOSED, +); + +#[derive(Clone, Debug)] +pub enum BlockPredicate { + Block { + block: &'static Block, + properties: HashMap, + nbt: Option, + }, + Tag { + tag_name: String, + block_ids: Vec, + properties: HashMap, + nbt: Option, + }, +} + +impl BlockPredicate { + #[must_use] + pub fn test(&self, block: &Block) -> bool { + match self { + Self::Block { + block: expected, .. + } => block.id == expected.id, + Self::Tag { block_ids, .. } => block_ids.contains(&block.id.as_u16()), + } + } + + #[must_use] + pub const fn requires_nbt(&self) -> bool { + match self { + Self::Block { nbt, .. } | Self::Tag { nbt, .. } => nbt.is_some(), + } + } +} + +fn parse_properties( + reader: &mut StringReader, +) -> Result, CommandSyntaxError> { + let mut properties = HashMap::new(); + if reader.peek() == Some('[') { + reader.skip(); + reader.skip_whitespace(); + while reader.can_read_char() && reader.peek() != Some(']') { + let start_key = reader.cursor(); + while let Some(c) = reader.peek() { + if c.is_alphanumeric() || c == '_' || c == '-' { + reader.skip(); + } else { + break; + } + } + let key = reader.string()[start_key..reader.cursor()].to_string(); + reader.skip_whitespace(); + if reader.peek() != Some('=') { + return Err(ERROR_NO_VALUE.create(reader, TextComponent::text(key))); + } + reader.skip(); + reader.skip_whitespace(); + let start_val = reader.cursor(); + while let Some(c) = reader.peek() { + if c.is_alphanumeric() || c == '_' || c == '-' { + reader.skip(); + } else { + break; + } + } + let val = reader.string()[start_val..reader.cursor()].to_string(); + properties.insert(key, val); + reader.skip_whitespace(); + if reader.peek() == Some(',') { + reader.skip(); + reader.skip_whitespace(); + } else if reader.peek() == Some(']') { + break; + } else { + return Err(ERROR_UNCLOSED_PROPERTIES.create_without_context()); + } + } + if reader.peek() == Some(']') { + reader.skip(); + } else { + return Err(ERROR_UNCLOSED_PROPERTIES.create_without_context()); + } + } + Ok(properties) +} + +fn parse_nbt(reader: &mut StringReader) -> Result, CommandSyntaxError> { + if reader.peek() == Some('{') { + let tag = NbtCompoundArgumentType.parse(reader)?; + Ok(Some(tag)) + } else { + Ok(None) + } +} + +pub struct BlockPredicateArgumentType; + +impl ArgumentType for BlockPredicateArgumentType { + type Item = BlockPredicate; + + fn parse(&self, reader: &mut StringReader) -> Result { + if reader.peek() == Some('#') { + reader.skip(); + let start = reader.cursor(); + while let Some(c) = reader.peek() { + if c.is_alphanumeric() || c == '_' || c == ':' || c == '/' || c == '.' || c == '-' { + reader.skip(); + } else { + break; + } + } + let tag_str = &reader.string()[start..reader.cursor()]; + let normalized = if tag_str.contains(':') { + tag_str.to_string() + } else { + format!("minecraft:{tag_str}") + }; + let stripped = normalized.strip_prefix("minecraft:").unwrap_or(&normalized); + + let tag_ids = get_tag_ids(RegistryKey::Block, &normalized) + .or_else(|| get_tag_ids(RegistryKey::Block, stripped)); + + let block_ids = match tag_ids { + Some(ids) => ids.to_vec(), + None => { + return Err(ERROR_UNKNOWN_TAG.create(reader, TextComponent::text(normalized))); + } + }; + + let properties = parse_properties(reader)?; + let nbt = parse_nbt(reader)?; + + Ok(BlockPredicate::Tag { + tag_name: normalized, + block_ids, + properties, + nbt, + }) + } else { + let start = reader.cursor(); + while let Some(c) = reader.peek() { + if c.is_alphanumeric() || c == '_' || c == ':' || c == '/' || c == '.' || c == '-' { + reader.skip(); + } else { + break; + } + } + let block_str = &reader.string()[start..reader.cursor()]; + let normalized = if block_str.contains(':') { + block_str.to_string() + } else { + format!("minecraft:{block_str}") + }; + + let block = Block::from_name(&normalized).ok_or_else(|| { + INVALID_BLOCK_ERROR_TYPE.create(reader, TextComponent::text(normalized)) + })?; + + let properties = parse_properties(reader)?; + let nbt = parse_nbt(reader)?; + + Ok(BlockPredicate::Block { + block, + properties, + nbt, + }) + } + } + + fn client_side_parser(&self) -> JavaClientArgumentType { + JavaClientArgumentType::BlockPredicate + } + + fn examples(&self) -> Vec { + vec![ + "stone".to_string(), + "minecraft:stone".to_string(), + "stone[foo=bar]".to_string(), + "#stone".to_string(), + "#stone[foo=bar]{baz=nbt}".to_string(), + ] + } + + fn list_suggestions( + &self, + _context: &CommandContext, + builder: SuggestionsBuilder, + ) -> Suggestions { + builder.build() + } +} + +impl BlockPredicateArgumentType { + pub fn get(context: &CommandContext, name: &str) -> Result { + Ok(context.get_argument::(name)?.clone()) + } +} diff --git a/crates/pumpkin/src/command/argument_types/component.rs b/crates/pumpkin/src/command/argument_types/component.rs new file mode 100644 index 000000000..17cb4430a --- /dev/null +++ b/crates/pumpkin/src/command/argument_types/component.rs @@ -0,0 +1,54 @@ +use pumpkin_data::translation; +use pumpkin_util::text::TextComponent; + +use crate::command::argument_types::argument_type::{ArgumentType, JavaClientArgumentType}; +use crate::command::context::command_context::CommandContext; +use crate::command::errors::command_syntax_error::CommandSyntaxError; +use crate::command::errors::error_types::CommandErrorType; +use crate::command::string_reader::StringReader; +use crate::command::suggestion::suggestions::{Suggestions, SuggestionsBuilder}; + +pub const ERROR_INVALID_JSON: CommandErrorType<1> = CommandErrorType::new( + translation::java::ARGUMENT_COMPONENT_INVALID, + translation::java::ARGUMENT_COMPONENT_INVALID, +); + +#[derive(Clone, Copy)] +pub struct ComponentArgumentType; + +impl ArgumentType for ComponentArgumentType { + type Item = TextComponent; + + fn parse(&self, reader: &mut StringReader) -> Result { + let remaining = reader.remaining_part(); + if let Ok(comp) = serde_json::from_str::(remaining) { + reader.set_cursor(reader.string().len()); + return Ok(comp); + } + + let s = reader.read_string()?; + if let Ok(comp) = serde_json::from_str::(&s) { + return Ok(comp); + } + + Ok(TextComponent::text(s)) + } + + fn client_side_parser(&'_ self) -> JavaClientArgumentType { + JavaClientArgumentType::Component + } + + fn list_suggestions( + &self, + _context: &CommandContext, + builder: SuggestionsBuilder, + ) -> Suggestions { + builder.build() + } +} + +impl ComponentArgumentType { + pub fn get(context: &CommandContext, name: &str) -> Result { + context.get_argument::(name).cloned() + } +} diff --git a/crates/pumpkin/src/command/argument_types/coordinates/angle.rs b/crates/pumpkin/src/command/argument_types/coordinates/angle.rs new file mode 100644 index 000000000..3c386dec1 --- /dev/null +++ b/crates/pumpkin/src/command/argument_types/coordinates/angle.rs @@ -0,0 +1,88 @@ +use pumpkin_data::translation; +use pumpkin_util::math::wrap_degrees; + +use crate::command::argument_types::argument_type::{ArgumentType, JavaClientArgumentType}; +use crate::command::context::command_context::CommandContext; +use crate::command::context::command_source::CommandSource; +use crate::command::errors::command_syntax_error::CommandSyntaxError; +use crate::command::errors::error_types::CommandErrorType; +use crate::command::string_reader::StringReader; + +pub const ERROR_NOT_COMPLETE: CommandErrorType<0> = CommandErrorType::new( + translation::java::ARGUMENT_ANGLE_INCOMPLETE, + translation::java::ARGUMENT_ANGLE_INCOMPLETE, +); + +pub const ERROR_INVALID_ANGLE: CommandErrorType<0> = CommandErrorType::new( + translation::java::ARGUMENT_ANGLE_INVALID, + translation::java::ARGUMENT_ANGLE_INVALID, +); + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Angle { + pub angle: f32, + pub is_relative: bool, +} + +impl Angle { + #[must_use] + pub const fn new(angle: f32, is_relative: bool) -> Self { + Self { angle, is_relative } + } + + #[must_use] + pub fn get_angle(&self, source: &CommandSource) -> f32 { + let base = if self.is_relative { + source.rotation.y + } else { + 0.0 + }; + wrap_degrees(base + self.angle) + } +} + +pub struct AngleArgumentType; + +impl ArgumentType for AngleArgumentType { + type Item = Angle; + + fn parse(&self, reader: &mut StringReader) -> Result { + if !reader.can_read_char() { + return Err(ERROR_NOT_COMPLETE.create(reader)); + } + + let mut is_relative = false; + if reader.peek() == Some('~') { + is_relative = true; + reader.skip(); + } + + let angle = if reader.can_read_char() && reader.peek() != Some(' ') { + reader.read_float()? + } else if is_relative { + 0.0 + } else { + return Err(ERROR_NOT_COMPLETE.create(reader)); + }; + + if angle.is_nan() || angle.is_infinite() { + return Err(ERROR_INVALID_ANGLE.create(reader)); + } + + Ok(Angle::new(angle, is_relative)) + } + + fn client_side_parser(&'_ self) -> JavaClientArgumentType { + JavaClientArgumentType::Angle + } + + fn examples(&self) -> Vec { + examples!("0", "~", "~-5") + } +} + +impl AngleArgumentType { + pub fn get(context: &CommandContext, name: &str) -> Result { + context.get_argument::(name).copied() + } +} diff --git a/crates/pumpkin/src/command/argument_types/coordinates/mod.rs b/crates/pumpkin/src/command/argument_types/coordinates/mod.rs index c676579ac..b34b10a1c 100644 --- a/crates/pumpkin/src/command/argument_types/coordinates/mod.rs +++ b/crates/pumpkin/src/command/argument_types/coordinates/mod.rs @@ -8,9 +8,12 @@ use pumpkin_data::translation; use pumpkin_util::math::vector2::Vector2; use pumpkin_util::math::vector3::{Axis, Vector3}; +pub mod angle; pub mod block_pos; pub mod column_pos; pub mod rotation; +pub mod swizzle; +pub mod vec2; pub mod vec3; pub const MIXED_TYPE_ERROR_TYPE: CommandErrorType<0> = CommandErrorType::new( diff --git a/crates/pumpkin/src/command/argument_types/coordinates/swizzle.rs b/crates/pumpkin/src/command/argument_types/coordinates/swizzle.rs new file mode 100644 index 000000000..29b679838 --- /dev/null +++ b/crates/pumpkin/src/command/argument_types/coordinates/swizzle.rs @@ -0,0 +1,88 @@ +use pumpkin_data::translation; +use pumpkin_util::math::vector3::Vector3; + +use crate::command::argument_types::argument_type::{ArgumentType, JavaClientArgumentType}; +use crate::command::context::command_context::CommandContext; +use crate::command::errors::command_syntax_error::CommandSyntaxError; +use crate::command::errors::error_types::CommandErrorType; +use crate::command::string_reader::StringReader; + +pub const ERROR_INVALID_SWIZZLE: CommandErrorType<0> = CommandErrorType::new( + translation::java::ARGUMENTS_SWIZZLE_INVALID, + translation::java::ARGUMENTS_SWIZZLE_INVALID, +); + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub struct Swizzle { + pub x: bool, + pub y: bool, + pub z: bool, +} + +impl Swizzle { + #[must_use] + pub const fn align(&self, pos: Vector3) -> Vector3 { + Vector3::new( + if self.x { pos.x.floor() } else { pos.x }, + if self.y { pos.y.floor() } else { pos.y }, + if self.z { pos.z.floor() } else { pos.z }, + ) + } +} + +pub struct SwizzleArgumentType; + +impl ArgumentType for SwizzleArgumentType { + type Item = Swizzle; + + fn parse(&self, reader: &mut StringReader) -> Result { + let mut swizzle = Swizzle::default(); + let mut count = 0; + + while reader.can_read_char() && reader.peek() != Some(' ') { + let c = reader.read().unwrap(); + match c { + 'x' => { + if swizzle.x { + return Err(ERROR_INVALID_SWIZZLE.create(reader)); + } + swizzle.x = true; + } + 'y' => { + if swizzle.y { + return Err(ERROR_INVALID_SWIZZLE.create(reader)); + } + swizzle.y = true; + } + 'z' => { + if swizzle.z { + return Err(ERROR_INVALID_SWIZZLE.create(reader)); + } + swizzle.z = true; + } + _ => return Err(ERROR_INVALID_SWIZZLE.create(reader)), + } + count += 1; + } + + if count == 0 { + return Err(ERROR_INVALID_SWIZZLE.create(reader)); + } + + Ok(swizzle) + } + + fn client_side_parser(&self) -> JavaClientArgumentType { + JavaClientArgumentType::Swizzle + } + + fn examples(&self) -> Vec { + vec!["xyz".to_string(), "x".to_string()] + } +} + +impl SwizzleArgumentType { + pub fn get(context: &CommandContext, name: &str) -> Result { + Ok(*context.get_argument::(name)?) + } +} diff --git a/crates/pumpkin/src/command/argument_types/coordinates/vec2.rs b/crates/pumpkin/src/command/argument_types/coordinates/vec2.rs new file mode 100644 index 000000000..32fdf9a19 --- /dev/null +++ b/crates/pumpkin/src/command/argument_types/coordinates/vec2.rs @@ -0,0 +1,111 @@ +use crate::command::argument_types::argument_type::{ArgumentType, JavaClientArgumentType}; +use crate::command::argument_types::coordinates::{Coordinates, WorldCoordinate}; +use crate::command::context::command_context::CommandContext; +use crate::command::errors::command_syntax_error::CommandSyntaxError; +use crate::command::errors::error_types::CommandErrorType; +use crate::command::string_reader::StringReader; +use crate::command::suggestion::suggestions::{Suggestions, SuggestionsBuilder, TextCoordinates}; +use pumpkin_data::translation; +use pumpkin_util::math::vector2::Vector2; +use pumpkin_util::math::vector3::Vector3; + +pub const INCOMPLETE_2D_ERROR_TYPE: CommandErrorType<0> = CommandErrorType::new( + translation::java::ARGUMENT_POS2D_INCOMPLETE, + translation::java::ARGUMENT_POS2D_INCOMPLETE, +); + +#[derive(Debug, Default)] +pub enum Vec2ArgumentType { + #[default] + Default, + Uncorrected, +} + +impl Vec2ArgumentType { + #[must_use] + pub const fn centers_integers(&self) -> bool { + matches!(self, Self::Default) + } + + #[must_use] + pub const fn new(centers_integers: bool) -> Self { + if centers_integers { + Self::Default + } else { + Self::Uncorrected + } + } +} + +impl ArgumentType for Vec2ArgumentType { + type Item = Coordinates; + + fn parse(&self, reader: &mut StringReader) -> Result { + let start = reader.cursor(); + if !reader.can_read_char() { + return Err(INCOMPLETE_2D_ERROR_TYPE.create(reader)); + } + + let x = WorldCoordinate::parse(reader, self.centers_integers())?; + if reader.can_read_char() && reader.peek() == Some(' ') { + reader.skip(); + let z = WorldCoordinate::parse(reader, self.centers_integers())?; + Ok(Coordinates::World(Vector3::new( + x, + WorldCoordinate::Relative(0.0), + z, + ))) + } else { + reader.set_cursor(start); + Err(INCOMPLETE_2D_ERROR_TYPE.create(reader)) + } + } + + fn client_side_parser(&self) -> JavaClientArgumentType { + JavaClientArgumentType::Vec2 + } + + fn examples(&self) -> Vec { + vec![ + "0 0".to_string(), + "~ ~".to_string(), + "0.1 -0.5".to_string(), + "~1 ~-2".to_string(), + ] + } + + fn list_suggestions( + &self, + _context: &CommandContext, + builder: SuggestionsBuilder, + ) -> Suggestions { + let remainder = builder.remaining(); + + let suggested_coordinates = if remainder.bytes().next() == Some(b'^') { + TextCoordinates::Local + } else { + TextCoordinates::Global + }; + + builder.suggest_2d_coordinates(suggested_coordinates, |value| { + self.parse(&mut StringReader::new(value)).is_ok() + }) + } +} + +impl Vec2ArgumentType { + pub fn get_coordinates( + context: &CommandContext, + name: &str, + ) -> Result { + Ok(*context.get_argument(name)?) + } + + pub fn get_vector2( + context: &CommandContext, + name: &str, + ) -> Result, CommandSyntaxError> { + let resolved = Self::get_coordinates(context, name)?.resolve(context.source.as_ref()); + Ok(Vector2::new(resolved.x, resolved.z)) + } +} diff --git a/crates/pumpkin/src/command/argument_types/coordinates/vec3.rs b/crates/pumpkin/src/command/argument_types/coordinates/vec3.rs index c5ec6a0df..7d5dc477f 100644 --- a/crates/pumpkin/src/command/argument_types/coordinates/vec3.rs +++ b/crates/pumpkin/src/command/argument_types/coordinates/vec3.rs @@ -1,5 +1,5 @@ use crate::command::argument_types::argument_type::{ArgumentType, JavaClientArgumentType}; -use crate::command::argument_types::coordinates::Coordinates; +use crate::command::argument_types::coordinates::{Coordinates, MIXED_TYPE_ERROR_TYPE}; use crate::command::context::command_context::CommandContext; use crate::command::errors::command_syntax_error::CommandSyntaxError; use crate::command::errors::error_types::CommandErrorType; @@ -12,8 +12,10 @@ pub const INCOMPLETE_ERROR_TYPE: CommandErrorType<0> = CommandErrorType::new( translation::java::ARGUMENT_POS3D_INCOMPLETE, translation::java::ARGUMENT_POS3D_INCOMPLETE, ); +pub const ERROR_NOT_COMPLETE: CommandErrorType<0> = INCOMPLETE_ERROR_TYPE; +pub const ERROR_MIXED_TYPE: CommandErrorType<0> = MIXED_TYPE_ERROR_TYPE; -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone, Copy)] /// An argument type for a 3-dimensional vector. pub enum Vec3ArgumentType { /// The default `Vec3ArgumentType` variant. @@ -32,11 +34,21 @@ pub enum Vec3ArgumentType { } impl Vec3ArgumentType { - /// Returns whether this argument type centers integers.\ + /// Returns whether this argument type centers integers. #[must_use] pub const fn centers_integers(&self) -> bool { matches!(self, Self::Default) } + + /// Creates a new `Vec3ArgumentType` with the given center correction. + #[must_use] + pub const fn new(centers_integers: bool) -> Self { + if centers_integers { + Self::Default + } else { + Self::Uncorrected + } + } } impl ArgumentType for Vec3ArgumentType { @@ -55,7 +67,14 @@ impl ArgumentType for Vec3ArgumentType { } fn examples(&self) -> Vec { - examples!("1 1 1", "3 ~34 ~-2", "40 50 60", "^ ^4 ^3") + vec![ + "0 0 0".to_string(), + "~ ~ ~".to_string(), + "^ ^ ^".to_string(), + "^1 ^ ^-5".to_string(), + "0.1 -0.5 .9".to_string(), + "~0.5 ~1 ~-5".to_string(), + ] } fn list_suggestions( @@ -94,112 +113,3 @@ impl Vec3ArgumentType { Ok(Self::get_coordinates(context, name)?.resolve(context.source.as_ref())) } } - -#[cfg(test)] -mod test { - use crate::command::argument_types::argument_type::ArgumentType; - use crate::command::argument_types::coordinates::vec3::{ - INCOMPLETE_ERROR_TYPE, Vec3ArgumentType, - }; - use crate::command::argument_types::coordinates::{ - Coordinates, MIXED_TYPE_ERROR_TYPE, WorldCoordinate, - }; - use crate::command::string_reader::StringReader; - use pumpkin_util::math::vector3::Vector3; - - macro_rules! world_coordinate { - ($( ($variant:ident, $value:expr) ),+) => { - Coordinates::World(Vector3::new( - $( WorldCoordinate::$variant($value), )+ - )) - }; - } - - #[test] - fn parse_test() { - let mut reader = StringReader::new("0 0 0"); - - // The default type centers both the X and Z coordinates. - assert_parse_ok_reset!( - reader, - Vec3ArgumentType::Default, - world_coordinate!((Absolute, 0.5), (Absolute, 0.0), (Absolute, 0.5)) - ); - assert_parse_ok_reset!( - reader, - Vec3ArgumentType::Uncorrected, - world_coordinate!((Absolute, 0.0), (Absolute, 0.0), (Absolute, 0.0)) - ); - - let mut reader = StringReader::new("~ ~4 8"); - - assert_parse_ok_reset!( - reader, - Vec3ArgumentType::Default, - world_coordinate!( - (Relative, 0.0), - (Relative, 4.0), - // Only the Z coordinate is centered. - (Absolute, 8.5) - ) - ); - assert_parse_ok_reset!( - reader, - Vec3ArgumentType::Uncorrected, - world_coordinate!((Relative, 0.0), (Relative, 4.0), (Absolute, 8.0)) - ); - - let mut reader = StringReader::new("~-1 ~-2.5 ~-3"); - - assert_parse_ok_reset!( - reader, - Vec3ArgumentType::Default, - world_coordinate!((Relative, -1.0), (Relative, -2.5), (Relative, -3.0)) - ); - assert_parse_ok_reset!( - reader, - Vec3ArgumentType::Uncorrected, - world_coordinate!((Relative, -1.0), (Relative, -2.5), (Relative, -3.0)) - ); - - let mut reader = StringReader::new("-3 4 5"); - - assert_parse_ok_reset!( - reader, - Vec3ArgumentType::Default, - world_coordinate!((Absolute, -2.5), (Absolute, 4.0), (Absolute, 5.5)) - ); - - let mut reader = StringReader::new("1000 2000"); - - assert_parse_err_reset!(reader, Vec3ArgumentType::Default, &INCOMPLETE_ERROR_TYPE); - } - - #[test] - fn parse_local_coordinates() { - let mut reader = StringReader::new("^1 ^10 ^30"); - - assert_parse_ok_reset!( - reader, - Vec3ArgumentType::Default, - Coordinates::Local { - left: 1.0, - up: 10.0, - forward: 30.0 - } - ); - - // We can't mix world & local coordinates. - let mut reader = StringReader::new("^1 ^10 ~20"); - - assert_parse_err_reset!(reader, Vec3ArgumentType::Default, &MIXED_TYPE_ERROR_TYPE); - - let mut reader = StringReader::new("^-3 ^-7 10"); - - assert_parse_err_reset!(reader, Vec3ArgumentType::Default, &MIXED_TYPE_ERROR_TYPE); - - let mut reader = StringReader::new("^1 ^2"); - - assert_parse_err_reset!(reader, Vec3ArgumentType::Default, &INCOMPLETE_ERROR_TYPE); - } -} diff --git a/crates/pumpkin/src/command/argument_types/entity_selector/mod.rs b/crates/pumpkin/src/command/argument_types/entity_selector/mod.rs index 4448e6a2c..e5c806430 100644 --- a/crates/pumpkin/src/command/argument_types/entity_selector/mod.rs +++ b/crates/pumpkin/src/command/argument_types/entity_selector/mod.rs @@ -161,11 +161,10 @@ impl EntitySelector { source: &CommandSource, ) -> Result, CommandSyntaxError> { let mut list = self.find_players(source)?; - if list.len() == 1 { - list.pop() - .ok_or_else(|| entity::NO_PLAYERS_ERROR_TYPE.create_without_context()) - } else { - Err(entity::NO_PLAYERS_ERROR_TYPE.create_without_context()) + match list.len() { + 0 => Err(entity::NO_PLAYERS_ERROR_TYPE.create_without_context()), + 1 => Ok(list.pop().unwrap()), + _ => Err(entity::NOT_SINGLE_PLAYER_ERROR_TYPE.create_without_context()), } } diff --git a/crates/pumpkin/src/command/argument_types/identifier.rs b/crates/pumpkin/src/command/argument_types/identifier.rs index 45b5bbb49..bb09d7e95 100644 --- a/crates/pumpkin/src/command/argument_types/identifier.rs +++ b/crates/pumpkin/src/command/argument_types/identifier.rs @@ -27,3 +27,12 @@ impl ArgumentType for IdentifierArgumentType { examples!("foo", "foo:bar") } } + +impl IdentifierArgumentType { + pub fn get( + context: &crate::command::context::command_context::CommandContext, + name: &str, + ) -> Result { + Ok(context.get_argument::(name)?.clone()) + } +} diff --git a/crates/pumpkin/src/command/argument_types/item.rs b/crates/pumpkin/src/command/argument_types/item.rs new file mode 100644 index 000000000..565515dae --- /dev/null +++ b/crates/pumpkin/src/command/argument_types/item.rs @@ -0,0 +1,114 @@ +use pumpkin_data::data_component::DataComponent; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::translation; +use pumpkin_util::text::TextComponent; + +use crate::command::argument_types::argument_type::{ArgumentType, JavaClientArgumentType}; +use crate::command::context::command_context::CommandContext; +use crate::command::errors::command_syntax_error::CommandSyntaxError; +use crate::command::errors::error_types::CommandErrorType; +use crate::command::snbt::SnbtParser; +use crate::command::string_reader::StringReader; +use crate::command::suggestion::suggestions::{Suggestions, SuggestionsBuilder}; + +pub const ERROR_UNKNOWN_ITEM: CommandErrorType<1> = CommandErrorType::new( + translation::java::ARGUMENT_ITEM_ID_INVALID, + translation::java::ARGUMENT_ITEM_ID_INVALID, +); + +#[derive(Clone, Copy)] +pub struct ItemStackArgumentType; + +impl ArgumentType for ItemStackArgumentType { + type Item = ItemStack; + + fn parse(&self, reader: &mut StringReader) -> Result { + let start = reader.cursor(); + while let Some(c) = reader.peek() { + if c.is_alphanumeric() || c == '_' || c == ':' || c == '/' || c == '.' || c == '-' { + reader.skip(); + } else { + break; + } + } + let raw_id = &reader.string()[start..reader.cursor()]; + let item = Item::from_registry_key(raw_id).ok_or_else(|| { + let full_name = if raw_id.contains(':') { + raw_id.to_string() + } else { + format!("minecraft:{raw_id}") + }; + ERROR_UNKNOWN_ITEM.create(reader, TextComponent::text(full_name)) + })?; + + let mut stack = ItemStack::new(1, item); + + // Optional components [...] + if reader.peek() == Some('[') { + reader.skip(); + let mut patch = Vec::new(); + while reader.can_read_char() && reader.peek() != Some(']') { + let key_start = reader.cursor(); + while let Some(c) = reader.peek() { + if c.is_alphanumeric() || c == '_' || c == ':' || c == '.' || c == '-' { + reader.skip(); + } else { + break; + } + } + let key_str = reader.string()[key_start..reader.cursor()] + .trim() + .to_string(); + + reader.skip_whitespace(); + if reader.peek() == Some('=') { + reader.skip(); + reader.skip_whitespace(); + let nbt_tag = SnbtParser::parse_for_commands(reader)?; + if let (Some(data_comp), Some(comp_impl)) = ( + DataComponent::try_from_name(&key_str), + pumpkin_data::data_component_impl::read_data( + DataComponent::try_from_name(&key_str) + .unwrap_or(DataComponent::CustomData), + &nbt_tag, + ), + ) { + patch.push((data_comp, Some(comp_impl))); + } + } + reader.skip_whitespace(); + if reader.peek() == Some(',') { + reader.skip(); + reader.skip_whitespace(); + } + } + if reader.peek() == Some(']') { + reader.skip(); + } + if !patch.is_empty() { + stack.patch = patch; + } + } + + Ok(stack) + } + + fn client_side_parser(&'_ self) -> JavaClientArgumentType { + JavaClientArgumentType::ItemStack + } + + fn list_suggestions( + &self, + _context: &CommandContext, + builder: SuggestionsBuilder, + ) -> Suggestions { + builder.build() + } +} + +impl ItemStackArgumentType { + pub fn get(context: &CommandContext, name: &str) -> Result { + context.get_argument::(name).cloned() + } +} diff --git a/crates/pumpkin/src/command/argument_types/item_predicate.rs b/crates/pumpkin/src/command/argument_types/item_predicate.rs new file mode 100644 index 000000000..b972f202e --- /dev/null +++ b/crates/pumpkin/src/command/argument_types/item_predicate.rs @@ -0,0 +1,170 @@ +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::tag::{RegistryKey, get_tag_ids}; +use pumpkin_data::translation; +use pumpkin_util::text::TextComponent; + +use crate::command::argument_types::argument_type::{ArgumentType, JavaClientArgumentType}; +use crate::command::context::command_context::CommandContext; +use crate::command::errors::command_syntax_error::CommandSyntaxError; +use crate::command::errors::error_types::CommandErrorType; +use crate::command::string_reader::StringReader; +use crate::command::suggestion::suggestions::{Suggestions, SuggestionsBuilder}; + +pub const ERROR_UNKNOWN_ITEM: CommandErrorType<1> = CommandErrorType::new( + translation::java::ARGUMENT_ITEM_ID_INVALID, + translation::java::ARGUMENT_ITEM_ID_INVALID, +); + +pub const ERROR_UNKNOWN_TAG: CommandErrorType<1> = CommandErrorType::new( + translation::java::ARGUMENTS_ITEM_TAG_UNKNOWN, + translation::java::ARGUMENTS_ITEM_TAG_UNKNOWN, +); + +#[derive(Clone)] +pub enum ItemPredicate { + Item(&'static Item), + Tag(Vec), + Any, +} + +impl ItemPredicate { + #[must_use] + pub fn test(&self, stack: &ItemStack) -> bool { + match self { + Self::Any => true, + Self::Item(item) => stack.get_item().id == item.id, + Self::Tag(tag) => tag.contains(&stack.get_item().id), + } + } +} + +pub struct ItemPredicateArgumentType; + +impl ArgumentType for ItemPredicateArgumentType { + type Item = ItemPredicate; + + fn parse(&self, reader: &mut StringReader) -> Result { + if reader.peek() == Some('#') { + reader.skip(); + let start = reader.cursor(); + while let Some(c) = reader.peek() { + if c.is_alphanumeric() || c == '_' || c == ':' || c == '/' || c == '.' || c == '-' { + reader.skip(); + } else { + break; + } + } + let tag_str = &reader.string()[start..reader.cursor()]; + let normalized = if tag_str.contains(':') { + tag_str.to_string() + } else { + format!("minecraft:{tag_str}") + }; + let stripped = normalized.strip_prefix("minecraft:").unwrap_or(&normalized); + + let tag_ids = get_tag_ids(RegistryKey::Item, &normalized) + .or_else(|| get_tag_ids(RegistryKey::Item, stripped)); + + let items = match tag_ids { + Some(ids) => ids.to_vec(), + None => { + return Err(ERROR_UNKNOWN_TAG.create(reader, TextComponent::text(normalized))); + } + }; + + skip_components_and_nbt(reader); + + Ok(ItemPredicate::Tag(items)) + } else { + let start = reader.cursor(); + while let Some(c) = reader.peek() { + if c.is_alphanumeric() + || c == '_' + || c == ':' + || c == '/' + || c == '.' + || c == '-' + || c == '*' + { + reader.skip(); + } else { + break; + } + } + let item_str = &reader.string()[start..reader.cursor()]; + if item_str == "*" { + return Ok(ItemPredicate::Any); + } + + let normalized = if item_str.contains(':') { + item_str.to_string() + } else { + format!("minecraft:{item_str}") + }; + let stripped = normalized.strip_prefix("minecraft:").unwrap_or(&normalized); + + let item = Item::from_registry_key(&normalized) + .or_else(|| Item::from_registry_key(stripped)) + .ok_or_else(|| { + ERROR_UNKNOWN_ITEM.create(reader, TextComponent::text(normalized)) + })?; + + skip_components_and_nbt(reader); + + Ok(ItemPredicate::Item(item)) + } + } + + fn client_side_parser(&self) -> JavaClientArgumentType { + JavaClientArgumentType::ItemPredicate + } + + fn examples(&self) -> Vec { + vec![ + "stick".to_string(), + "minecraft:stick".to_string(), + "#stick".to_string(), + "#stick{foo:'bar'}".to_string(), + ] + } + + fn list_suggestions( + &self, + _context: &CommandContext, + builder: SuggestionsBuilder, + ) -> Suggestions { + builder.build() + } +} + +fn skip_components_and_nbt(reader: &mut StringReader) { + if reader.peek() == Some('[') { + reader.skip(); + let mut depth = 1; + while reader.can_read_char() && depth > 0 { + match reader.read() { + Some('[') => depth += 1, + Some(']') => depth -= 1, + _ => {} + } + } + } + if reader.peek() == Some('{') { + reader.skip(); + let mut depth = 1; + while reader.can_read_char() && depth > 0 { + match reader.read() { + Some('{') => depth += 1, + Some('}') => depth -= 1, + _ => {} + } + } + } +} + +impl ItemPredicateArgumentType { + pub fn get(context: &CommandContext, name: &str) -> Result { + Ok(context.get_argument::(name)?.clone()) + } +} diff --git a/crates/pumpkin/src/command/argument_types/mod.rs b/crates/pumpkin/src/command/argument_types/mod.rs index cf4ec0daf..1b07389c3 100644 --- a/crates/pumpkin/src/command/argument_types/mod.rs +++ b/crates/pumpkin/src/command/argument_types/mod.rs @@ -219,6 +219,8 @@ impl FromStringReader for Identifier { pub mod argument_type; pub mod attribute; pub mod block; +pub mod block_predicate; +pub mod component; pub mod coordinates; pub mod core; pub mod dialog; @@ -226,10 +228,15 @@ pub mod entity; pub mod entity_anchor; pub mod entity_selector; pub mod game_profile; +pub mod gamemode; pub mod hex_color; pub mod identifier; +pub mod item; +pub mod item_predicate; pub mod nbt; +pub mod nbt_path; pub mod objective; +pub mod particle; pub mod placed_feature; pub mod pool; pub mod range; @@ -237,6 +244,7 @@ pub mod resource; pub mod resource_key; pub mod resource_or_tag; pub mod slot; +pub mod sound_category; pub mod structure; pub mod team; pub mod team_color; diff --git a/crates/pumpkin/src/command/argument_types/nbt.rs b/crates/pumpkin/src/command/argument_types/nbt.rs index 99541f5a1..df5582fcd 100644 --- a/crates/pumpkin/src/command/argument_types/nbt.rs +++ b/crates/pumpkin/src/command/argument_types/nbt.rs @@ -35,9 +35,7 @@ impl ArgumentType for NbtTagArgumentType { } fn examples(&self) -> Vec { - examples!( - "5", "7b", "1.6", "\"hi\"", "'bye'", "[2, 3]", "[L; 4]", "{x: 3}" - ) + examples!("0", "0b", "0l", "0.0", "\"foo\"", "{foo=bar}", "[0]") } } @@ -90,3 +88,5 @@ impl NbtCompoundArgumentType { context.get_argument(name) } } + +pub use super::nbt_path::{NbtPath, NbtPathArgumentType, NbtPathNode}; diff --git a/crates/pumpkin/src/command/argument_types/nbt_path.rs b/crates/pumpkin/src/command/argument_types/nbt_path.rs new file mode 100644 index 000000000..958a6fe94 --- /dev/null +++ b/crates/pumpkin/src/command/argument_types/nbt_path.rs @@ -0,0 +1,802 @@ +#![allow( + clippy::borrow_as_ptr, + clippy::ref_as_ptr, + clippy::manual_let_else, + clippy::undocumented_unsafe_blocks, + clippy::explicit_counter_loop, + clippy::collapsible_if, + clippy::must_use_candidate, + clippy::needless_pass_by_value, + clippy::redundant_closure_for_method_calls, + clippy::cast_possible_wrap, + clippy::cast_sign_loss, + clippy::cast_possible_truncation, + clippy::missing_const_for_fn, + clippy::bool_to_int_with_if, + clippy::if_not_else +)] + +use pumpkin_data::translation; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_nbt::tag::NbtTag; +use pumpkin_util::text::TextComponent; + +use crate::command::argument_types::argument_type::{ArgumentType, JavaClientArgumentType}; +use crate::command::context::command_context::CommandContext; +use crate::command::errors::command_syntax_error::CommandSyntaxError; +use crate::command::errors::error_types::CommandErrorType; +use crate::command::snbt::SnbtParser; +use crate::command::string_reader::StringReader; + +pub const ERROR_INVALID_NODE: CommandErrorType<0> = CommandErrorType::new( + translation::java::ARGUMENTS_NBTPATH_NODE_INVALID, + translation::java::ARGUMENTS_NBTPATH_NODE_INVALID, +); + +pub const ERROR_DATA_TOO_DEEP: CommandErrorType<0> = CommandErrorType::new( + translation::java::ARGUMENTS_NBTPATH_TOO_DEEP, + translation::java::ARGUMENTS_NBTPATH_TOO_DEEP, +); + +pub const ERROR_NOTHING_FOUND: CommandErrorType<1> = CommandErrorType::new( + translation::java::ARGUMENTS_NBTPATH_NOTHING_FOUND, + translation::java::ARGUMENTS_NBTPATH_NOTHING_FOUND, +); + +pub const ERROR_EXPECTED_LIST: CommandErrorType<1> = CommandErrorType::new( + translation::java::COMMANDS_DATA_MODIFY_EXPECTED_LIST, + translation::java::COMMANDS_DATA_MODIFY_EXPECTED_LIST, +); + +pub const ERROR_INVALID_INDEX: CommandErrorType<1> = CommandErrorType::new( + translation::java::COMMANDS_DATA_MODIFY_INVALID_INDEX, + translation::java::COMMANDS_DATA_MODIFY_INVALID_INDEX, +); + +/// Checks if an NBT tag exceeds the maximum nesting depth (512). +pub fn is_too_deep(tag: &NbtTag, depth: usize) -> bool { + if depth >= 512 { + return true; + } + match tag { + NbtTag::Compound(compound) => { + for child in compound.child_tags.values() { + if is_too_deep(child, depth + 1) { + return true; + } + } + } + NbtTag::List(list) => { + for child in list { + if is_too_deep(child, depth + 1) { + return true; + } + } + } + _ => {} + } + false +} + +/// Recursively compares whether `target` satisfies the `pattern`. +pub fn compare_nbt(pattern: &NbtTag, target: &NbtTag) -> bool { + if pattern == target { + return true; + } + match (pattern, target) { + (NbtTag::Compound(pattern_compound), NbtTag::Compound(target_compound)) => { + for (key, pattern_value) in &pattern_compound.child_tags { + match target_compound.child_tags.get(key) { + Some(target_value) => { + if !compare_nbt(pattern_value, target_value) { + return false; + } + } + None => return false, + } + } + true + } + (NbtTag::List(pattern_list), NbtTag::List(target_list)) => { + if pattern_list.is_empty() { + return target_list.is_empty(); + } + for pattern_elem in pattern_list { + let mut matched = false; + for target_elem in target_list { + if compare_nbt(pattern_elem, target_elem) { + matched = true; + break; + } + } + if !matched { + return false; + } + } + true + } + _ => pattern == target, + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum NbtPathNode { + AllElements, + CompoundChild(String), + IndexedElement(i32), + MatchElement(NbtCompound), + MatchObject(String, NbtCompound), + MatchRootObject(NbtCompound), +} + +impl NbtPathNode { + pub fn get_tag(&self, parent: &NbtTag, output: &mut Vec) { + match self { + Self::AllElements => match parent { + NbtTag::List(list) => output.extend(list.iter().cloned()), + NbtTag::ByteArray(arr) => { + output.extend(arr.iter().map(|&b| NbtTag::Byte(b))); + } + NbtTag::IntArray(arr) => { + output.extend(arr.iter().map(|&i| NbtTag::Int(i))); + } + NbtTag::LongArray(arr) => { + output.extend(arr.iter().map(|&l| NbtTag::Long(l))); + } + _ => {} + }, + Self::CompoundChild(name) => { + if let NbtTag::Compound(compound) = parent { + if let Some(tag) = compound.child_tags.get(name.as_str()) { + output.push(tag.clone()); + } + } + } + Self::IndexedElement(index) => match parent { + NbtTag::List(list) => { + let size = list.len() as i32; + let actual_index = if *index < 0 { size + index } else { *index }; + if actual_index >= 0 && (actual_index as usize) < list.len() { + output.push(list[actual_index as usize].clone()); + } + } + NbtTag::ByteArray(arr) => { + let size = arr.len() as i32; + let actual_index = if *index < 0 { size + index } else { *index }; + if actual_index >= 0 && (actual_index as usize) < arr.len() { + output.push(NbtTag::Byte(arr[actual_index as usize])); + } + } + NbtTag::IntArray(arr) => { + let size = arr.len() as i32; + let actual_index = if *index < 0 { size + index } else { *index }; + if actual_index >= 0 && (actual_index as usize) < arr.len() { + output.push(NbtTag::Int(arr[actual_index as usize])); + } + } + NbtTag::LongArray(arr) => { + let size = arr.len() as i32; + let actual_index = if *index < 0 { size + index } else { *index }; + if actual_index >= 0 && (actual_index as usize) < arr.len() { + output.push(NbtTag::Long(arr[actual_index as usize])); + } + } + _ => {} + }, + Self::MatchElement(pattern) => { + if let NbtTag::List(list) = parent { + let pattern_tag = NbtTag::Compound(pattern.clone()); + for elem in list { + if compare_nbt(&pattern_tag, elem) { + output.push(elem.clone()); + } + } + } + } + Self::MatchObject(name, pattern) => { + if let NbtTag::Compound(compound) = parent { + if let Some(tag) = compound.child_tags.get(name.as_str()) { + let pattern_tag = NbtTag::Compound(pattern.clone()); + if compare_nbt(&pattern_tag, tag) { + output.push(tag.clone()); + } + } + } + } + Self::MatchRootObject(pattern) => { + if let NbtTag::Compound(_) = parent { + let pattern_tag = NbtTag::Compound(pattern.clone()); + if compare_nbt(&pattern_tag, parent) { + output.push(parent.clone()); + } + } + } + } + } + + pub fn create_preferred_parent_tag(&self) -> NbtTag { + match self { + Self::AllElements | Self::IndexedElement(_) | Self::MatchElement(_) => { + NbtTag::List(Vec::new()) + } + Self::CompoundChild(_) | Self::MatchObject(_, _) | Self::MatchRootObject(_) => { + NbtTag::Compound(NbtCompound::new()) + } + } + } + + pub fn set_tag(&self, parent: &mut NbtTag, to_add: &mut impl FnMut() -> NbtTag) -> i32 { + match self { + Self::AllElements => { + if let NbtTag::List(list) = parent { + let size = list.len(); + if size == 0 { + list.push(to_add()); + return 1; + } + let new_val = to_add(); + let changed_count = list.iter().filter(|&x| x != &new_val).count() as i32; + if changed_count == 0 { + return 0; + } + list.clear(); + list.push(new_val); + for _ in 1..size { + list.push(to_add()); + } + changed_count + } else { + 0 + } + } + Self::CompoundChild(name) => { + if let NbtTag::Compound(compound) = parent { + let new_val = to_add(); + let prev = compound + .child_tags + .insert(name.clone().into(), new_val.clone()); + i32::from(prev.as_ref() != Some(&new_val)) + } else { + 0 + } + } + Self::IndexedElement(index) => { + if let NbtTag::List(list) = parent { + let size = list.len() as i32; + let actual_index = if *index < 0 { size + index } else { *index }; + if actual_index >= 0 && (actual_index as usize) < list.len() { + let new_val = to_add(); + if list[actual_index as usize] != new_val { + list[actual_index as usize] = new_val; + return 1; + } + } + } + 0 + } + Self::MatchElement(pattern) => { + let mut changed_count = 0; + if let NbtTag::List(list) = parent { + let pattern_tag = NbtTag::Compound(pattern.clone()); + if list.is_empty() { + list.push(to_add()); + changed_count += 1; + } else { + for elem in list.iter_mut() { + if compare_nbt(&pattern_tag, elem) { + let new_val = to_add(); + if *elem != new_val { + *elem = new_val; + changed_count += 1; + } + } + } + } + } + changed_count + } + Self::MatchObject(name, pattern) => { + if let NbtTag::Compound(compound) = parent { + let pattern_tag = NbtTag::Compound(pattern.clone()); + if let Some(curr) = compound.child_tags.get_mut(name.as_str()) { + if compare_nbt(&pattern_tag, curr) { + let new_val = to_add(); + if *curr != new_val { + *curr = new_val; + return 1; + } + } + } + } + 0 + } + Self::MatchRootObject(_) => 0, + } + } + + pub fn remove_tag(&self, parent: &mut NbtTag) -> i32 { + match self { + Self::AllElements => { + if let NbtTag::List(list) = parent { + let size = list.len() as i32; + if size > 0 { + list.clear(); + return size; + } + } + 0 + } + Self::CompoundChild(name) => { + if let NbtTag::Compound(compound) = parent { + if compound.child_tags.remove(name.as_str()).is_some() { + return 1; + } + } + 0 + } + Self::IndexedElement(index) => { + if let NbtTag::List(list) = parent { + let size = list.len() as i32; + let actual_index = if *index < 0 { size + index } else { *index }; + if actual_index >= 0 && (actual_index as usize) < list.len() { + list.remove(actual_index as usize); + return 1; + } + } + 0 + } + Self::MatchElement(pattern) => { + let mut changed_count = 0; + if let NbtTag::List(list) = parent { + let pattern_tag = NbtTag::Compound(pattern.clone()); + let mut i = 0; + while i < list.len() { + if compare_nbt(&pattern_tag, &list[i]) { + list.remove(i); + changed_count += 1; + } else { + i += 1; + } + } + } + changed_count + } + Self::MatchObject(name, pattern) => { + if let NbtTag::Compound(compound) = parent { + let pattern_tag = NbtTag::Compound(pattern.clone()); + if let Some(curr) = compound.child_tags.get(name.as_str()) { + if compare_nbt(&pattern_tag, curr) { + compound.child_tags.remove(name.as_str()); + return 1; + } + } + } + 0 + } + Self::MatchRootObject(_) => 0, + } + } +} + +/// Represents a parsed NBT path. +#[derive(Clone, Debug, PartialEq)] +pub struct NbtPath { + original: String, + nodes: Vec, + node_to_original_position: Vec, +} + +impl NbtPath { + #[must_use] + pub fn new( + original: String, + nodes: Vec, + node_to_original_position: Vec, + ) -> Self { + Self { + original, + nodes, + node_to_original_position, + } + } + + #[must_use] + pub fn as_str(&self) -> &str { + &self.original + } + + #[must_use] + pub fn nodes(&self) -> &[NbtPathNode] { + &self.nodes + } + + fn create_not_found_exception(&self, node_index: usize) -> CommandSyntaxError { + let index = self + .node_to_original_position + .get(node_index) + .copied() + .unwrap_or(self.original.len()); + ERROR_NOTHING_FOUND + .create_without_context(TextComponent::text(self.original[..index].to_string())) + } + + /// Gets matching tags from the given root tag. + pub fn get(&self, tag: &NbtTag) -> Result, CommandSyntaxError> { + let mut current = vec![tag.clone()]; + for (i, node) in self.nodes.iter().enumerate() { + let mut next = Vec::new(); + for parent in ¤t { + node.get_tag(parent, &mut next); + } + if next.is_empty() { + return Err(self.create_not_found_exception(i)); + } + current = next; + } + Ok(current) + } + + /// Counts how many elements match the path. + pub fn count_matching(&self, tag: &NbtTag) -> usize { + let mut current = vec![tag.clone()]; + for node in &self.nodes { + let mut next = Vec::new(); + for parent in ¤t { + node.get_tag(parent, &mut next); + } + if next.is_empty() { + return 0; + } + current = next; + } + current.len() + } + + fn get_or_create_parents( + &self, + tag: &mut NbtTag, + ) -> Result, CommandSyntaxError> { + let mut current: Vec<*mut NbtTag> = vec![tag as *mut NbtTag]; + for i in 0..self.nodes.len().saturating_sub(1) { + let node = &self.nodes[i]; + let next_node = &self.nodes[i + 1]; + let mut next = Vec::new(); + for &parent_ptr in ¤t { + // SAFETY: We traverse disjoint paths in the NBT tree. + let parent = unsafe { &mut *parent_ptr }; + match node { + NbtPathNode::CompoundChild(name) => { + if let NbtTag::Compound(compound) = parent { + if !compound.child_tags.contains_key(name.as_str()) { + compound.child_tags.insert( + name.clone().into(), + next_node.create_preferred_parent_tag(), + ); + } + if let Some(child) = compound.child_tags.get_mut(name.as_str()) { + next.push(child as *mut NbtTag); + } + } + } + NbtPathNode::AllElements => { + if let NbtTag::List(list) = parent { + if list.is_empty() { + list.push(next_node.create_preferred_parent_tag()); + } + for elem in list.iter_mut() { + next.push(elem as *mut NbtTag); + } + } + } + NbtPathNode::IndexedElement(index) => { + if let NbtTag::List(list) = parent { + let size = list.len() as i32; + let actual_index = if *index < 0 { size + index } else { *index }; + if actual_index >= 0 && (actual_index as usize) < list.len() { + next.push(&mut list[actual_index as usize] as *mut NbtTag); + } + } + } + NbtPathNode::MatchElement(pattern) => { + if let NbtTag::List(list) = parent { + let pattern_tag = NbtTag::Compound(pattern.clone()); + let mut found = false; + for elem in list.iter_mut() { + if compare_nbt(&pattern_tag, elem) { + next.push(elem as *mut NbtTag); + found = true; + } + } + if !found { + list.push(pattern_tag.clone()); + if let Some(last) = list.last_mut() { + next.push(last as *mut NbtTag); + } + } + } + } + NbtPathNode::MatchObject(name, pattern) => { + if let NbtTag::Compound(compound) = parent { + let pattern_tag = NbtTag::Compound(pattern.clone()); + if !compound.child_tags.contains_key(name.as_str()) { + compound + .child_tags + .insert(name.clone().into(), pattern_tag.clone()); + } + if let Some(child) = compound.child_tags.get_mut(name.as_str()) { + if compare_nbt(&pattern_tag, child) { + next.push(child as *mut NbtTag); + } + } + } + } + NbtPathNode::MatchRootObject(pattern) => { + let pattern_tag = NbtTag::Compound(pattern.clone()); + if compare_nbt(&pattern_tag, parent) { + next.push(parent as *mut NbtTag); + } + } + } + } + if next.is_empty() { + return Err(self.create_not_found_exception(i)); + } + current = next; + } + Ok(current) + } + + /// Sets the value at this path on the given root tag. + pub fn set(&self, tag: &mut NbtTag, to_add: NbtTag) -> Result { + if is_too_deep(&to_add, self.nodes.len()) { + return Err(ERROR_DATA_TOO_DEEP.create_without_context()); + } + let parents = self.get_or_create_parents(tag)?; + if parents.is_empty() { + return Ok(0); + } + let last_node = match self.nodes.last() { + Some(node) => node, + None => return Ok(0), + }; + let mut changed_count = 0; + for &parent_ptr in &parents { + let parent = unsafe { &mut *parent_ptr }; + let val_clone = to_add.clone(); + changed_count += last_node.set_tag(parent, &mut || val_clone.clone()); + } + Ok(changed_count) + } + + /// Inserts elements at the given index into list targets matching this path. + pub fn insert( + &self, + index: i32, + target: &mut NbtTag, + to_insert: &[NbtTag], + ) -> Result { + for tag in to_insert { + if is_too_deep(tag, self.nodes.len()) { + return Err(ERROR_DATA_TOO_DEEP.create_without_context()); + } + } + let parents = self.get_or_create_parents(target)?; + let last_node = match self.nodes.last() { + Some(node) => node, + None => return Ok(0), + }; + + let mut modified_count = 0; + for &parent_ptr in &parents { + let parent = unsafe { &mut *parent_ptr }; + let target_tags = match last_node { + NbtPathNode::CompoundChild(name) => { + if let NbtTag::Compound(compound) = parent { + if !compound.child_tags.contains_key(name.as_str()) { + compound + .child_tags + .insert(name.clone().into(), NbtTag::List(Vec::new())); + } + compound.child_tags.get_mut(name.as_str()) + } else { + None + } + } + NbtPathNode::MatchRootObject(_) => Some(parent), + _ => None, + }; + + if let Some(target_tag) = target_tags { + match target_tag { + NbtTag::List(list) => { + let mut modified = false; + let size = list.len() as i32; + let mut actual_index = if index < 0 { size + index + 1 } else { index }; + for source_tag in to_insert { + if actual_index < 0 || (actual_index as usize) > list.len() { + return Err(ERROR_INVALID_INDEX.create_without_context( + TextComponent::text(actual_index.to_string()), + )); + } + list.insert(actual_index as usize, source_tag.clone()); + actual_index += 1; + modified = true; + } + if modified { + modified_count += 1; + } + } + _ => { + return Err(ERROR_EXPECTED_LIST.create_without_context( + TextComponent::text(format!("{target_tag:?}")), + )); + } + } + } + } + + Ok(modified_count) + } + + /// Removes tags at this path from the given root tag. + pub fn remove(&self, tag: &mut NbtTag) -> i32 { + let parents = match self.get_or_create_parents(tag) { + Ok(p) => p, + Err(_) => return 0, + }; + let last_node = match self.nodes.last() { + Some(node) => node, + None => return 0, + }; + let mut total_removed = 0; + for &parent_ptr in &parents { + let parent = unsafe { &mut *parent_ptr }; + total_removed += last_node.remove_tag(parent); + } + total_removed + } +} + +impl std::fmt::Display for NbtPath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.original) + } +} + +/// Argument type for parsing NBT Paths. +pub struct NbtPathArgumentType; + +impl ArgumentType for NbtPathArgumentType { + type Item = NbtPath; + + fn parse(&self, reader: &mut StringReader) -> Result { + let mut nodes = Vec::new(); + let start = reader.cursor(); + let mut node_to_original_position = Vec::new(); + let mut first_node = true; + + while reader.can_read_char() && reader.peek() != Some(' ') { + let node = parse_node(reader, first_node)?; + nodes.push(node); + node_to_original_position.push(reader.cursor() - start); + first_node = false; + if reader.can_read_char() { + let next = reader.peek().unwrap(); + if next != ' ' && next != '[' && next != '{' { + reader.expect('.')?; + } + } + } + + if nodes.is_empty() { + return Err(ERROR_INVALID_NODE.create(reader)); + } + + let original = reader.string()[start..reader.cursor()].to_string(); + Ok(NbtPath::new(original, nodes, node_to_original_position)) + } + + fn client_side_parser(&'_ self) -> JavaClientArgumentType { + JavaClientArgumentType::NbtPath + } + + fn examples(&self) -> Vec { + examples!("foo", "foo.bar", "foo[0]", "[0]", "[]", "{foo:\"bar\"}") + } +} + +impl NbtPathArgumentType { + pub fn get<'a>( + context: &'a CommandContext, + name: &'_ str, + ) -> Result<&'a NbtPath, CommandSyntaxError> { + context.get_argument(name) + } +} + +fn parse_node( + reader: &mut StringReader, + first_node: bool, +) -> Result { + let peek = match reader.peek() { + Some(c) => c, + None => return Err(ERROR_INVALID_NODE.create(reader)), + }; + match peek { + '"' | '\'' => { + let name = reader.read_string()?; + read_object_node(reader, name) + } + '[' => { + reader.skip(); + let next = match reader.peek() { + Some(c) => c, + None => return Err(ERROR_INVALID_NODE.create(reader)), + }; + if next == '{' { + let pattern = parse_compound_pattern(reader)?; + reader.expect(']')?; + Ok(NbtPathNode::MatchElement(pattern)) + } else if next == ']' { + reader.skip(); + Ok(NbtPathNode::AllElements) + } else { + let index = reader.read_int()?; + reader.expect(']')?; + Ok(NbtPathNode::IndexedElement(index)) + } + } + '{' => { + if !first_node { + return Err(ERROR_INVALID_NODE.create(reader)); + } + let pattern = parse_compound_pattern(reader)?; + Ok(NbtPathNode::MatchRootObject(pattern)) + } + _ => { + let name = read_unquoted_name(reader)?; + read_object_node(reader, name) + } + } +} + +fn read_object_node( + reader: &mut StringReader, + name: String, +) -> Result { + if name.is_empty() { + return Err(ERROR_INVALID_NODE.create(reader)); + } + if reader.peek() == Some('{') { + let pattern = parse_compound_pattern(reader)?; + Ok(NbtPathNode::MatchObject(name, pattern)) + } else { + Ok(NbtPathNode::CompoundChild(name)) + } +} + +fn read_unquoted_name(reader: &mut StringReader) -> Result { + let start = reader.cursor(); + while let Some(c) = reader.peek() { + if is_allowed_in_unquoted_name(c) { + reader.skip(); + } else { + break; + } + } + if reader.cursor() == start { + return Err(ERROR_INVALID_NODE.create(reader)); + } + Ok(reader.string()[start..reader.cursor()].to_string()) +} + +const fn is_allowed_in_unquoted_name(c: char) -> bool { + !matches!(c, ' ' | '"' | '\'' | '[' | ']' | '.' | '{' | '}') +} + +fn parse_compound_pattern(reader: &mut StringReader) -> Result { + match SnbtParser::parse_for_commands(reader)? { + NbtTag::Compound(compound) => Ok(compound), + _ => Err(ERROR_INVALID_NODE.create(reader)), + } +} diff --git a/crates/pumpkin/src/command/argument_types/particle.rs b/crates/pumpkin/src/command/argument_types/particle.rs new file mode 100644 index 000000000..0c6f969a9 --- /dev/null +++ b/crates/pumpkin/src/command/argument_types/particle.rs @@ -0,0 +1,61 @@ +use crate::command::argument_types::FromStringReader; +use crate::command::argument_types::argument_type::{ArgumentType, JavaClientArgumentType}; +use crate::command::context::command_context::CommandContext; +use crate::command::errors::command_syntax_error::CommandSyntaxError; +use crate::command::errors::error_types::CommandErrorType; +use crate::command::string_reader::StringReader; +use crate::command::suggestion::suggestions::{Suggestions, SuggestionsBuilder}; +use pumpkin_data::particle::Particle; +use pumpkin_data::translation; +use pumpkin_util::identifier::Identifier; +use pumpkin_util::text::TextComponent; + +pub const ERROR_UNKNOWN_PARTICLE: CommandErrorType<1> = CommandErrorType::new( + translation::java::PARTICLE_NOTFOUND, + translation::bedrock::COMMANDS_PARTICLE_NOTFOUND, +); + +pub struct ParticleArgumentType; + +impl ArgumentType for ParticleArgumentType { + type Item = Particle; + + fn parse(&self, reader: &mut StringReader) -> Result { + let identifier = Identifier::from_reader(reader)?; + Particle::from_name(identifier.path()) + .or_else(|| Particle::from_name(&identifier.to_string())) + .ok_or_else(|| { + ERROR_UNKNOWN_PARTICLE.create(reader, TextComponent::text(identifier.to_string())) + }) + } + + fn client_side_parser(&self) -> JavaClientArgumentType { + JavaClientArgumentType::Particle + } + + fn examples(&self) -> Vec { + vec![ + "foo".to_string(), + "foo:bar".to_string(), + "particle".to_string(), + ] + } + + fn list_suggestions( + &self, + _context: &CommandContext, + builder: SuggestionsBuilder, + ) -> Suggestions { + let particles = (0..=124u16) + .filter_map(Particle::from_id) + .map(|p| format!("{p:?}").to_lowercase()) + .collect(); + builder.filter_and_suggest_lowercase(particles).build() + } +} + +impl ParticleArgumentType { + pub fn get(context: &CommandContext, name: &str) -> Result { + Ok(*context.get_argument::(name)?) + } +} diff --git a/crates/pumpkin/src/command/argument_types/resource.rs b/crates/pumpkin/src/command/argument_types/resource.rs index 2382c698e..31b002591 100644 --- a/crates/pumpkin/src/command/argument_types/resource.rs +++ b/crates/pumpkin/src/command/argument_types/resource.rs @@ -14,6 +14,64 @@ use std::any::Any; use std::iter::Iterator; pub static ENTITY_TYPE_REGISTRY: &Identifier = &Identifier::vanilla_static("entity_type"); +pub static ENCHANTMENT_REGISTRY: &Identifier = &Identifier::vanilla_static("enchantment"); +pub static MOB_EFFECT_REGISTRY: &Identifier = &Identifier::vanilla_static("mob_effect"); +pub static DAMAGE_TYPE_REGISTRY: &Identifier = &Identifier::vanilla_static("damage_type"); + +static DAMAGE_TYPES: [pumpkin_data::damage::DamageType; 51] = [ + pumpkin_data::damage::DamageType::ARROW, + pumpkin_data::damage::DamageType::BAD_RESPAWN_POINT, + pumpkin_data::damage::DamageType::CACTUS, + pumpkin_data::damage::DamageType::CAMPFIRE, + pumpkin_data::damage::DamageType::CRAMMING, + pumpkin_data::damage::DamageType::DRAGON_BREATH, + pumpkin_data::damage::DamageType::DROWN, + pumpkin_data::damage::DamageType::DRY_OUT, + pumpkin_data::damage::DamageType::ENDER_PEARL, + pumpkin_data::damage::DamageType::EXPLOSION, + pumpkin_data::damage::DamageType::FALL, + pumpkin_data::damage::DamageType::FALLING_ANVIL, + pumpkin_data::damage::DamageType::FALLING_BLOCK, + pumpkin_data::damage::DamageType::FALLING_STALACTITE, + pumpkin_data::damage::DamageType::FIREBALL, + pumpkin_data::damage::DamageType::FIREWORKS, + pumpkin_data::damage::DamageType::FLY_INTO_WALL, + pumpkin_data::damage::DamageType::FREEZE, + pumpkin_data::damage::DamageType::GENERIC, + pumpkin_data::damage::DamageType::GENERIC_KILL, + pumpkin_data::damage::DamageType::HOT_FLOOR, + pumpkin_data::damage::DamageType::IN_FIRE, + pumpkin_data::damage::DamageType::IN_WALL, + pumpkin_data::damage::DamageType::INDIRECT_MAGIC, + pumpkin_data::damage::DamageType::LAVA, + pumpkin_data::damage::DamageType::LIGHTNING_BOLT, + pumpkin_data::damage::DamageType::MACE_SMASH, + pumpkin_data::damage::DamageType::MAGIC, + pumpkin_data::damage::DamageType::MOB_ATTACK, + pumpkin_data::damage::DamageType::MOB_ATTACK_NO_AGGRO, + pumpkin_data::damage::DamageType::MOB_PROJECTILE, + pumpkin_data::damage::DamageType::ON_FIRE, + pumpkin_data::damage::DamageType::OUT_OF_WORLD, + pumpkin_data::damage::DamageType::OUTSIDE_BORDER, + pumpkin_data::damage::DamageType::PLAYER_ATTACK, + pumpkin_data::damage::DamageType::PLAYER_EXPLOSION, + pumpkin_data::damage::DamageType::SONIC_BOOM, + pumpkin_data::damage::DamageType::SPEAR, + pumpkin_data::damage::DamageType::SPIT, + pumpkin_data::damage::DamageType::STALAGMITE, + pumpkin_data::damage::DamageType::STARVE, + pumpkin_data::damage::DamageType::STING, + pumpkin_data::damage::DamageType::SULFUR_CUBE_HOT, + pumpkin_data::damage::DamageType::SWEET_BERRY_BUSH, + pumpkin_data::damage::DamageType::THORNS, + pumpkin_data::damage::DamageType::THROWN, + pumpkin_data::damage::DamageType::TRIDENT, + pumpkin_data::damage::DamageType::UNATTRIBUTED_FIREBALL, + pumpkin_data::damage::DamageType::WIND_CHARGE, + pumpkin_data::damage::DamageType::WITHER, + pumpkin_data::damage::DamageType::WITHER_SKULL, +]; + static ERROR_UNKNOWN_RESOURCE: CommandErrorType<2> = CommandErrorType::new( translation::java::ARGUMENT_RESOURCE_NOT_FOUND, translation::java::ARGUMENT_RESOURCE_NOT_FOUND, @@ -34,6 +92,30 @@ pub static ENTITY_TYPE_ARGUMENT: ResourceArgument = EntityType::from_name(id.path()).map(|value| value as &'static (dyn Any + Send + Sync)) }); +pub static ENCHANTMENT_ARGUMENT: ResourceArgument = + ResourceArgument(ENCHANTMENT_REGISTRY, &|id: Identifier| { + pumpkin_data::Enchantment::from_name(id.path()) + .or_else(|| pumpkin_data::Enchantment::from_name(&id.to_string())) + .map(|value| value as &'static (dyn Any + Send + Sync)) + }); + +pub static MOB_EFFECT_ARGUMENT: ResourceArgument = + ResourceArgument(MOB_EFFECT_REGISTRY, &|id: Identifier| { + pumpkin_data::effect::StatusEffect::from_name(id.path()) + .or_else(|| pumpkin_data::effect::StatusEffect::from_minecraft_name(&id.to_string())) + .map(|value| value as &'static (dyn Any + Send + Sync)) + }); + +pub static DAMAGE_TYPE_ARGUMENT: ResourceArgument = + ResourceArgument(DAMAGE_TYPE_REGISTRY, &|id: Identifier| { + let dt = pumpkin_data::damage::DamageType::from_name(id.path()) + .or_else(|| pumpkin_data::damage::DamageType::from_name(&id.to_string()))?; + let idx = dt.id as usize; + DAMAGE_TYPES + .get(idx) + .map(|val| val as &'static (dyn Any + Send + Sync)) + }); + #[derive(Clone)] pub struct ResourceArgument( pub &'static Identifier, @@ -66,6 +148,64 @@ impl ArgumentType for ResourceArgument { suggestions_builder .filter_and_suggest_iter(entity_types) .build() + } else if self.0 == ENCHANTMENT_REGISTRY { + let enchantments = pumpkin_data::Enchantment::ALL + .iter() + .map(|e| format!("minecraft:{}", e.name)); + suggestions_builder + .filter_and_suggest_iter(enchantments) + .build() + } else if self.0 == MOB_EFFECT_REGISTRY { + let effects = [ + &pumpkin_data::effect::StatusEffect::ABSORPTION, + &pumpkin_data::effect::StatusEffect::BAD_OMEN, + &pumpkin_data::effect::StatusEffect::BLINDNESS, + &pumpkin_data::effect::StatusEffect::BREATH_OF_THE_NAUTILUS, + &pumpkin_data::effect::StatusEffect::CONDUIT_POWER, + &pumpkin_data::effect::StatusEffect::DARKNESS, + &pumpkin_data::effect::StatusEffect::DOLPHINS_GRACE, + &pumpkin_data::effect::StatusEffect::FIRE_RESISTANCE, + &pumpkin_data::effect::StatusEffect::GLOWING, + &pumpkin_data::effect::StatusEffect::HASTE, + &pumpkin_data::effect::StatusEffect::HEALTH_BOOST, + &pumpkin_data::effect::StatusEffect::HERO_OF_THE_VILLAGE, + &pumpkin_data::effect::StatusEffect::HUNGER, + &pumpkin_data::effect::StatusEffect::INFESTED, + &pumpkin_data::effect::StatusEffect::INSTANT_DAMAGE, + &pumpkin_data::effect::StatusEffect::INSTANT_HEALTH, + &pumpkin_data::effect::StatusEffect::INVISIBILITY, + &pumpkin_data::effect::StatusEffect::JUMP_BOOST, + &pumpkin_data::effect::StatusEffect::LEVITATION, + &pumpkin_data::effect::StatusEffect::LUCK, + &pumpkin_data::effect::StatusEffect::MINING_FATIGUE, + &pumpkin_data::effect::StatusEffect::NAUSEA, + &pumpkin_data::effect::StatusEffect::NIGHT_VISION, + &pumpkin_data::effect::StatusEffect::OOZING, + &pumpkin_data::effect::StatusEffect::POISON, + &pumpkin_data::effect::StatusEffect::RAID_OMEN, + &pumpkin_data::effect::StatusEffect::REGENERATION, + &pumpkin_data::effect::StatusEffect::RESISTANCE, + &pumpkin_data::effect::StatusEffect::SATURATION, + &pumpkin_data::effect::StatusEffect::SLOW_FALLING, + &pumpkin_data::effect::StatusEffect::SLOWNESS, + &pumpkin_data::effect::StatusEffect::SPEED, + &pumpkin_data::effect::StatusEffect::STRENGTH, + &pumpkin_data::effect::StatusEffect::TRIAL_OMEN, + &pumpkin_data::effect::StatusEffect::UNLUCK, + &pumpkin_data::effect::StatusEffect::WATER_BREATHING, + &pumpkin_data::effect::StatusEffect::WEAKNESS, + &pumpkin_data::effect::StatusEffect::WEAVING, + &pumpkin_data::effect::StatusEffect::WIND_CHARGED, + &pumpkin_data::effect::StatusEffect::WITHER, + ] + .iter() + .map(|e| e.minecraft_name.to_string()); + suggestions_builder.filter_and_suggest_iter(effects).build() + } else if self.0 == DAMAGE_TYPE_REGISTRY { + let types = DAMAGE_TYPES + .iter() + .map(|dt| format!("minecraft:{}", dt.message_id)); + suggestions_builder.filter_and_suggest_iter(types).build() } else { Suggestions::empty() } @@ -140,6 +280,27 @@ impl ResourceArgument { Self::get_resource(context, name, ENTITY_TYPE_REGISTRY) } + pub fn get_enchantment( + context: &CommandContext, + name: &str, + ) -> Result<&'static pumpkin_data::Enchantment, CommandSyntaxError> { + Self::get_resource(context, name, ENCHANTMENT_REGISTRY) + } + + pub fn get_mob_effect( + context: &CommandContext, + name: &str, + ) -> Result<&'static pumpkin_data::effect::StatusEffect, CommandSyntaxError> { + Self::get_resource(context, name, MOB_EFFECT_REGISTRY) + } + + pub fn get_damage_type( + context: &CommandContext, + name: &str, + ) -> Result<&'static pumpkin_data::damage::DamageType, CommandSyntaxError> { + Self::get_resource(context, name, DAMAGE_TYPE_REGISTRY) + } + pub fn get_summonable_entity_type( context: &CommandContext, name: &str, diff --git a/crates/pumpkin/src/command/argument_types/sound_category.rs b/crates/pumpkin/src/command/argument_types/sound_category.rs new file mode 100644 index 000000000..eca44c77f --- /dev/null +++ b/crates/pumpkin/src/command/argument_types/sound_category.rs @@ -0,0 +1,68 @@ +use pumpkin_data::sound::SoundCategory; +use pumpkin_data::translation; +use pumpkin_util::text::TextComponent; + +use crate::command::argument_types::argument_type::{ArgumentType, JavaClientArgumentType}; +use crate::command::context::command_context::CommandContext; +use crate::command::errors::command_syntax_error::CommandSyntaxError; +use crate::command::errors::error_types::CommandErrorType; +use crate::command::string_reader::StringReader; +use crate::command::suggestion::suggestions::{Suggestions, SuggestionsBuilder}; + +pub const ERROR_INVALID_SOURCE: CommandErrorType<1> = CommandErrorType::new( + translation::java::ARGUMENT_ENUM_INVALID, + translation::java::ARGUMENT_ENUM_INVALID, +); + +const CATEGORIES: [&str; 10] = [ + "master", "music", "record", "weather", "block", "hostile", "neutral", "player", "ambient", + "voice", +]; + +#[derive(Clone, Copy)] +pub struct SoundCategoryArgumentType; + +impl ArgumentType for SoundCategoryArgumentType { + type Item = SoundCategory; + + fn parse(&self, reader: &mut StringReader) -> Result { + let start = reader.cursor(); + let s = reader.read_unquoted_string(); + match s.to_lowercase().as_str() { + "master" => Ok(SoundCategory::Master), + "music" => Ok(SoundCategory::Music), + "record" => Ok(SoundCategory::Records), + "weather" => Ok(SoundCategory::Weather), + "block" => Ok(SoundCategory::Blocks), + "hostile" => Ok(SoundCategory::Hostile), + "neutral" => Ok(SoundCategory::Neutral), + "player" => Ok(SoundCategory::Players), + "ambient" => Ok(SoundCategory::Ambient), + "voice" => Ok(SoundCategory::Voice), + _ => { + reader.set_cursor(start); + Err(ERROR_INVALID_SOURCE.create(reader, TextComponent::text(s))) + } + } + } + + fn client_side_parser(&'_ self) -> JavaClientArgumentType { + JavaClientArgumentType::String( + pumpkin_protocol::java::client::play::StringProtoArgBehavior::SingleWord, + ) + } + + fn list_suggestions( + &self, + _context: &CommandContext, + builder: SuggestionsBuilder, + ) -> Suggestions { + builder.filter_and_suggest(&CATEGORIES).build() + } +} + +impl SoundCategoryArgumentType { + pub fn get(context: &CommandContext, name: &str) -> Result { + context.get_argument::(name).copied() + } +} diff --git a/crates/pumpkin/src/command/client_suggestions.rs b/crates/pumpkin/src/command/client_suggestions.rs index 4e4d192f5..255b41373 100644 --- a/crates/pumpkin/src/command/client_suggestions.rs +++ b/crates/pumpkin/src/command/client_suggestions.rs @@ -7,7 +7,6 @@ use pumpkin_protocol::{ }; use std::sync::Arc; -use super::tree::{Node, NodeType}; use crate::command::node::{ attached::{AttachedNode, NodeId}, dispatcher::CommandDispatcher, @@ -20,99 +19,21 @@ use pumpkin_protocol::bedrock::client::available_commands::{ }; use pumpkin_protocol::java::client::play::SuggestionProviders; -#[expect(clippy::too_many_lines)] +#[allow(clippy::too_many_lines)] pub fn send_c_commands_packet( player: &Arc, - server: &Server, + _server: &Server, dispatcher: &CommandDispatcher, ) { - let cmd_src = super::CommandSender::Player(player.clone()); - - let mut first_level = Vec::new(); - - let fallback_dispatcher = &dispatcher.fallback_dispatcher; - for key in fallback_dispatcher.commands.keys() { - if dispatcher.is_disabled(key) { - continue; - } - - // For double-slash commands, if the single-slash alias (/set) exists, only - // register the single-slash literal for the Java client. - if key.starts_with("//") && fallback_dispatcher.commands.contains_key(&key[1..]) { - continue; - } - - let Ok(tree) = fallback_dispatcher.get_tree(key) else { - continue; - }; - - let Some(permission) = fallback_dispatcher.permissions.get(key) else { - continue; - }; - - if !cmd_src.has_permission(server, permission.as_str()) { - continue; - } - - let (is_executable, child_nodes) = - nodes_to_proto_node_builders(&cmd_src, &tree.nodes, &tree.children); - - let name = if key.starts_with("//") { - &key[1..] - } else { - key.as_str() - }; - - let proto_node = ProtoNodeBuilder { - child_nodes, - node_type: ProtoNodeType::Literal { - name, - is_executable, - redirect_target: None, - restricted: false, - }, - }; - - first_level.push(proto_node); - } - - let root = ProtoNodeBuilder { - child_nodes: first_level, - node_type: ProtoNodeType::Root, - }; - - let mut proto_nodes = Vec::new(); - let root_node_index = root.build(&mut proto_nodes); - - let node_id_offset = proto_nodes.len(); - - let mut root_node_children_second: Box<[VarInt]> = Box::new([]); + let mut proto_nodes: Vec = Vec::with_capacity(dispatcher.tree.len()); for node in &dispatcher.tree { - let children: Box<[VarInt]> = node - .children_ref() - .values() - .copied() - .map(|id| resolve_node_id(id, node_id_offset, root_node_index)) - .map(|i| VarInt(i as i32)) - .collect(); - - let redirect_target = node - .redirect() - .and_then(|redirection| dispatcher.tree.resolve(redirection)) - .map(|id| resolve_node_id(id, node_id_offset, root_node_index) as i32); - - let satisfies_requirements = true; - - match node { + let children: Box<[VarInt]> = match node { AttachedNode::Root(_) => { // Drop disabled commands from the root's child list so they // disappear from the client's command graph (and tab-completion) - // entirely. The nodes themselves stay in `proto_nodes` to keep - // every other node's indices valid; they simply become - // unreachable. - root_node_children_second = node - .children_ref() + // entirely. + node.children_ref() .values() .copied() .filter(|id| { @@ -135,9 +56,30 @@ pub fn send_c_commands_packet( } true }) - .map(|id| resolve_node_id(id, node_id_offset, root_node_index)) - .map(|i| VarInt(i as i32)) - .collect(); + .map(|id| VarInt((id.0.get() - 1) as i32)) + .collect() + } + _ => node + .children_ref() + .values() + .copied() + .map(|id| VarInt((id.0.get() - 1) as i32)) + .collect(), + }; + + let redirect_target = node + .redirect() + .and_then(|redirection| dispatcher.tree.resolve(redirection)) + .map(|id| (id.0.get() - 1) as i32); + + let satisfies_requirements = true; + + match node { + AttachedNode::Root(_) => { + proto_nodes.push(ProtoNode { + children, + node_type: ProtoNodeType::Root, + }); } AttachedNode::Literal(literal_attached_node) => { let name = if literal_attached_node.meta.literal.starts_with("//") { @@ -200,176 +142,25 @@ pub fn send_c_commands_packet( } } - if !root_node_children_second.is_empty() { - let root_node = &mut proto_nodes[root_node_index]; - let mut first = std::mem::take(&mut root_node.children).into_vec(); - first.append(&mut root_node_children_second.into_vec()); - root_node.children = first.into_boxed_slice(); - } - + let root_node_index = ROOT_NODE_ID.0.get() - 1; let packet = CCommands::new(proto_nodes.into(), VarInt(root_node_index as i32)); player.try_send_client_packet(&packet); } -fn resolve_node_id(node_id: NodeId, node_id_offset: usize, root_node_index: usize) -> usize { - if node_id == ROOT_NODE_ID { - root_node_index - } else { - const FIRST_NONROOT_ID: usize = 2; - debug_assert!( - node_id.0.get() >= FIRST_NONROOT_ID, - "Root node should have been handled in the if body" - ); - node_id_offset + node_id.0.get() - FIRST_NONROOT_ID - } -} - -struct ProtoNodeBuilder<'a> { - child_nodes: Vec, - node_type: ProtoNodeType<'a>, -} - -impl<'a> ProtoNodeBuilder<'a> { - fn build(self, buffer: &mut Vec>) -> usize { - let children: Box<[VarInt]> = self - .child_nodes - .into_iter() - .map(|node| VarInt(node.build(buffer) as i32)) - .collect(); - - let i = buffer.len(); - buffer.push(ProtoNode { - children, - node_type: self.node_type, - }); - i - } -} - -fn nodes_to_proto_node_builders<'a>( - cmd_src: &super::CommandSender, - nodes: &'a [Node], - children: &[usize], -) -> (bool, Vec>) { - let mut child_nodes = Vec::new(); - let mut is_executable = false; - - for i in children { - let node = &nodes[*i]; - match &node.node_type { - NodeType::Argument { - name, - consumer, - suggestion_provider, - } => { - let (node_is_executable, node_children) = - nodes_to_proto_node_builders(cmd_src, nodes, &node.children); - child_nodes.push(ProtoNodeBuilder { - child_nodes: node_children, - node_type: ProtoNodeType::Argument { - name, - is_executable: node_is_executable, - redirect_target: None, - parser: consumer.get_client_side_parser(), - override_suggestion_type: if suggestion_provider.is_some() { - Some(SuggestionProviders::AskServer) - } else { - consumer.get_client_side_suggestion_type_override() - }, - restricted: false, - }, - }); - } - - NodeType::Literal { string, .. } => { - let (node_is_executable, node_children) = - nodes_to_proto_node_builders(cmd_src, nodes, &node.children); - child_nodes.push(ProtoNodeBuilder { - child_nodes: node_children, - node_type: ProtoNodeType::Literal { - name: string, - is_executable: node_is_executable, - redirect_target: None, - restricted: false, - }, - }); - } - - NodeType::ExecuteLeaf { .. } => is_executable = true, - - NodeType::Require { predicate } => { - if predicate(cmd_src) { - let (node_is_executable, node_children) = - nodes_to_proto_node_builders(cmd_src, nodes, &node.children); - if node_is_executable { - is_executable = true; - } - child_nodes.extend(node_children); - } - } - } - } - - (is_executable, child_nodes) -} - struct BuilderContext<'a> { enum_values: &'a mut Vec, enums: &'a mut Vec, } -#[expect(clippy::too_many_lines)] pub fn send_bedrock_commands_packet( player: &Arc, - server: &Server, + _server: &Server, dispatcher: &CommandDispatcher, ) { - let cmd_src = super::CommandSender::Player(player.clone()); - let mut enum_values: Vec = Vec::new(); let mut enums: Vec = Vec::new(); let mut commands: Vec = Vec::new(); - let fallback_dispatcher = &dispatcher.fallback_dispatcher; - for key in fallback_dispatcher.commands.keys() { - if dispatcher.is_disabled(key) { - continue; - } - - if key.starts_with("//") && fallback_dispatcher.commands.contains_key(&key[1..]) { - continue; - } - - let Ok(tree) = fallback_dispatcher.get_tree(key) else { - continue; - }; - - let Some(permission) = fallback_dispatcher.permissions.get(key) else { - continue; - }; - - if !cmd_src.has_permission(server, permission.as_str()) { - continue; - } - - let mut ctx = BuilderContext { - enum_values: &mut enum_values, - enums: &mut enums, - }; - - let overloads = build_overloads_from_nodes(&tree.nodes, &tree.children, &mut ctx); - - commands.push(CommandData { - name: key.clone(), - description: String::new(), - flags: 0, - permission_level: CommandPermissionLevel::Any.into(), - alias_enum: -1, - command_data_chained_subcommand_indexes: Vec::new(), - overloads, - }); - } - let tree_nodes: Vec<&AttachedNode> = dispatcher.tree.iter().collect(); let root_child_ids: Vec = tree_nodes @@ -448,79 +239,6 @@ pub fn send_bedrock_commands_packet( } } -fn build_overloads_from_nodes( - nodes: &[Node], - children: &[usize], - ctx: &mut BuilderContext, -) -> Vec { - let mut overloads = Vec::new(); - collect_overloads_from_nodes(nodes, children, &mut Vec::new(), &mut overloads, ctx); - if overloads.is_empty() { - overloads.push(OverloadData { - is_chaining: false, - parameter_data: Vec::new(), - }); - } - overloads -} - -fn collect_overloads_from_nodes( - nodes: &[Node], - children: &[usize], - current_params: &mut Vec, - overloads: &mut Vec, - ctx: &mut BuilderContext, -) { - let mut has_executable = false; - - for &i in children { - let node = &nodes[i]; - match &node.node_type { - NodeType::ExecuteLeaf { .. } => { - has_executable = true; - } - NodeType::Literal { string, .. } => { - let enum_idx = ensure_command_enum( - ctx.enums, - ctx.enum_values, - &format!("SubCommand_{string}"), - std::slice::from_ref(string), - ); - let mut params = current_params.clone(); - params.push(ParamData { - name: string.clone(), - parse_symbol: arg_flags::ARG_FLAG_VALID - | arg_flags::ARG_FLAG_ENUM - | enum_idx as u32, - is_optional: false, - options: 0, - }); - collect_overloads_from_nodes(nodes, &node.children, &mut params, overloads, ctx); - } - NodeType::Argument { name, consumer, .. } => { - let mut params = current_params.clone(); - params.push(ParamData { - name: name.clone(), - parse_symbol: bedrock_param_type(&consumer.get_client_side_parser()), - is_optional: false, - options: 0, - }); - collect_overloads_from_nodes(nodes, &node.children, &mut params, overloads, ctx); - } - NodeType::Require { .. } => { - collect_overloads_from_nodes(nodes, &node.children, current_params, overloads, ctx); - } - } - } - - if has_executable { - overloads.push(OverloadData { - is_chaining: false, - parameter_data: current_params.clone(), - }); - } -} - fn build_overloads_from_attached_nodes( tree: &[&AttachedNode], child_ids: &[NodeId], diff --git a/crates/pumpkin/src/command/commands/ban.rs b/crates/pumpkin/src/command/commands/ban.rs index abc9ef25c..662ae70d8 100644 --- a/crates/pumpkin/src/command/commands/ban.rs +++ b/crates/pumpkin/src/command/commands/ban.rs @@ -1,96 +1,29 @@ -use crate::{ - command::{ - CommandError, CommandExecutor, CommandResult, CommandSender, - args::{ - Arg, ConsumedArgs, - gameprofile::{GameProfileSuggestionMode, GameProfilesArgumentConsumer}, - message::MsgArgConsumer, - }, - tree::{CommandTree, builder::argument}, - }, - data::{SaveJSONConfiguration, banlist_serializer::BannedPlayerEntry}, - net::{DisconnectReason, GameProfile}, -}; -use CommandError::InvalidConsumption; use pumpkin_data::translation; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; -const NAMES: [&str; 1] = ["ban"]; +use crate::command::argument_builder::{ArgumentBuilder, argument, command}; +use crate::command::argument_types::core::string::StringArgumentType; +use crate::command::argument_types::game_profile::GameProfileArgumentType; +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 crate::data::SaveJSONConfiguration; +use crate::data::banlist_serializer::BannedPlayerEntry; +use crate::net::{DisconnectReason, GameProfile}; + const DESCRIPTION: &str = "bans a player"; +const PERMISSION: &str = "minecraft:command.ban"; -const ARG_TARGET: &str = "player"; -const ARG_REASON: &str = "reason"; +const ERROR_BAN_FAILED: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_BAN_FAILED, + translation::bedrock::COMMANDS_BAN_FAILED, +); -struct NoReasonExecutor; - -impl CommandExecutor for NoReasonExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::GameProfiles(targets)) = args.get(&ARG_TARGET) else { - return Err(InvalidConsumption(Some(ARG_TARGET.into()))); - }; - - ban_players(sender, server, targets.as_slice(), None) - } -} - -struct ReasonExecutor; - -impl CommandExecutor for ReasonExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::GameProfiles(targets)) = args.get(&ARG_TARGET) else { - return Err(InvalidConsumption(Some(ARG_TARGET.into()))); - }; - - let Some(Arg::Msg(reason)) = args.get(ARG_REASON) else { - return Err(InvalidConsumption(Some(ARG_REASON.into()))); - }; - - ban_players(sender, server, targets.as_slice(), Some(reason)) - } -} - -/// Returns the number of players successfully banned. -fn ban_players( - sender: &CommandSender, - server: &crate::server::Server, - targets: &[GameProfile], - reason: Option<&String>, -) -> Result { - let mut count: usize = 0; - for target in targets { - if ban_profile(sender, server, target, reason.cloned()) { - count += 1; - } - } - - if count == 0 { - Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_BAN_FAILED, - translation::bedrock::COMMANDS_BAN_FAILED, - [], - ))) - } else { - Ok(count as i32) - } -} - -/// Returns `true` if the player was successfully banned. -fn ban_profile( - sender: &CommandSender, - server: &crate::server::Server, - profile: &GameProfile, - reason: Option, -) -> bool { +fn ban_profile(context: &CommandContext, profile: &GameProfile, reason: Option) -> bool { + let server = context.source.server(); let mut banned_players = server.data.banned_player_list.write().unwrap(); let reason = reason.unwrap_or_else(|| "Banned by an operator.".to_string()); @@ -109,7 +42,7 @@ fn ban_profile( banned_players.banned_players.push(BannedPlayerEntry::new( profile, - sender.to_string(), + context.source.name.clone(), None, reason.clone(), )); @@ -117,15 +50,17 @@ fn ban_profile( banned_players.save(); drop(banned_players); - // Send messages - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_BAN_SUCCESS, - translation::bedrock::COMMANDS_BAN_SUCCESS, - [ - TextComponent::text(profile.name.clone()), - TextComponent::text(reason), - ], - )); + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_BAN_SUCCESS, + translation::bedrock::COMMANDS_BAN_SUCCESS, + [ + TextComponent::text(profile.name.clone()), + TextComponent::text(reason), + ], + ), + true, + ); if let Some(player) = server.get_player_by_uuid(profile.id) { let kick_msg = TextComponent::translate_cross( @@ -139,13 +74,49 @@ fn ban_profile( true } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).then( - argument( - ARG_TARGET, - GameProfilesArgumentConsumer::new(GameProfileSuggestionMode::OnlinePlayers, true), - ) - .execute(NoReasonExecutor) - .then(argument(ARG_REASON, MsgArgConsumer).execute(ReasonExecutor)), - ) +struct BanExecutor { + has_reason: bool, +} + +impl CommandExecutor for BanExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let targets = GameProfileArgumentType::get(context, "player")?; + let reason = if self.has_reason { + Some(StringArgumentType::get(context, "reason")?.to_string()) + } else { + None + }; + + let mut count: usize = 0; + for target in &targets { + if ban_profile(context, target, reason.clone()) { + count += 1; + } + } + + if count == 0 { + Err(ERROR_BAN_FAILED.create_without_context()) + } else { + Ok(count as i32) + } + } +} + +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Three), + )); + + dispatcher.register( + command("ban", DESCRIPTION).requires(PERMISSION).then( + argument("player", GameProfileArgumentType) + .executes(BanExecutor { has_reason: false }) + .then( + argument("reason", StringArgumentType::GreedyPhrase) + .executes(BanExecutor { has_reason: true }), + ), + ), + ); } diff --git a/crates/pumpkin/src/command/commands/banip.rs b/crates/pumpkin/src/command/commands/banip.rs index f78634aec..7b2c468d7 100644 --- a/crates/pumpkin/src/command/commands/banip.rs +++ b/crates/pumpkin/src/command/commands/banip.rs @@ -1,99 +1,59 @@ -use std::{net::IpAddr, str::FromStr}; +use std::net::IpAddr; +use std::str::FromStr; -use crate::{ - command::{ - CommandError, CommandExecutor, CommandResult, CommandSender, - args::{Arg, ConsumedArgs, message::MsgArgConsumer, simple::SimpleArgConsumer}, - tree::{CommandTree, builder::argument}, - }, - data::{SaveJSONConfiguration, banlist_serializer::BannedIpEntry}, - net::DisconnectReason, - server::Server, -}; -use CommandError::InvalidConsumption; use pumpkin_data::translation; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; -const NAMES: [&str; 1] = ["ban-ip"]; -const DESCRIPTION: &str = "bans a player-ip"; +use crate::command::argument_builder::{ArgumentBuilder, argument, command}; +use crate::command::argument_types::core::string::StringArgumentType; +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 crate::data::SaveJSONConfiguration; +use crate::data::banlist_serializer::BannedIpEntry; +use crate::net::DisconnectReason; +use crate::server::Server; -const ARG_TARGET: &str = "ip"; -const ARG_REASON: &str = "reason"; +const DESCRIPTION: &str = "bans a player-ip"; +const PERMISSION: &str = "minecraft:command.banip"; + +const ERROR_BANIP_INVALID: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_BANIP_INVALID, + translation::bedrock::COMMANDS_BANIP_INVALID, +); + +const ERROR_BANIP_FAILED: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_BANIP_FAILED, + translation::java::COMMANDS_BANIP_FAILED, +); fn parse_ip(target: &str, server: &Server) -> Option { - Some(match IpAddr::from_str(target) { - Ok(ip) => ip, - Err(_) => server.get_player_by_name(target)?.client.address().ip(), + IpAddr::from_str(target).ok().or_else(|| { + server + .get_player_by_name(target) + .map(|p| p.client.address().ip()) }) } -struct NoReasonExecutor; - -impl CommandExecutor for NoReasonExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::Simple(target)) = args.get(&ARG_TARGET) else { - return Err(InvalidConsumption(Some(ARG_TARGET.into()))); - }; - - ban_ip(sender, server, target, None) - } -} - -struct ReasonExecutor; - -impl CommandExecutor for ReasonExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::Simple(target)) = args.get(&ARG_TARGET) else { - return Err(InvalidConsumption(Some(ARG_TARGET.into()))); - }; - - let Some(Arg::Msg(reason)) = args.get(ARG_REASON) else { - return Err(InvalidConsumption(Some(ARG_REASON.into()))); - }; - - ban_ip(sender, server, target, Some(reason.clone())) - } -} - -fn ban_ip( - sender: &CommandSender, - server: &Server, - target: &str, - reason: Option, -) -> Result { +fn ban_ip(context: &CommandContext, target: &str, reason: Option) -> CommandExecutorResult { + let server = context.source.server(); let reason = reason.unwrap_or_else(|| "Banned by an operator.".to_string()); - let Some(target_ip) = parse_ip(target, server) else { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_BANIP_INVALID, - translation::bedrock::COMMANDS_BANIP_INVALID, - [], - ))); - }; + let target_ip = + parse_ip(target, server).ok_or_else(|| ERROR_BANIP_INVALID.create_without_context())?; let mut banned_ips = server.data.banned_ip_list.write().unwrap(); if banned_ips.get_entry(&target_ip).is_some() { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_BANIP_FAILED, - translation::java::COMMANDS_BANIP_FAILED, - [], - ))); + return Err(ERROR_BANIP_FAILED.create_without_context()); } banned_ips.banned_ips.push(BannedIpEntry::new( target_ip, - sender.to_string(), + context.source.name.clone(), None, reason.clone(), )); @@ -101,49 +61,69 @@ fn ban_ip( banned_ips.save(); drop(banned_ips); - // Send messages - let affected = server.get_players_by_ip(target_ip); - let names = affected + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_BANIP_SUCCESS, + translation::bedrock::COMMANDS_BANIP_SUCCESS, + [ + TextComponent::text(target_ip.to_string()), + TextComponent::text(reason), + ], + ), + true, + ); + + let players_to_kick: Vec<_> = server + .get_all_players() .iter() - .map(|p| p.gameprofile.name.clone()) - .collect::>() - .join(" "); + .filter(|player| player.client.address().ip() == target_ip) + .cloned() + .collect(); - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_BANIP_SUCCESS, - translation::bedrock::COMMANDS_BANIP_SUCCESS, - [ - TextComponent::text(target_ip.to_string()), - TextComponent::text(reason), - ], - )); - - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_BANIP_INFO, - translation::java::COMMANDS_BANIP_INFO, - [ - TextComponent::text(affected.len().to_string()), - TextComponent::text(names), - ], - )); - - let count = affected.len(); - for target in affected { + for player in players_to_kick { let kick_msg = TextComponent::translate_cross( translation::java::MULTIPLAYER_DISCONNECT_IP_BANNED, - translation::java::MULTIPLAYER_DISCONNECT_IP_BANNED, + translation::bedrock::DISCONNECTIONSCREEN_TITLE_BANNEDBYHOST, [], ); - target.kick(DisconnectReason::Kicked, &kick_msg); + player.kick(DisconnectReason::Kicked, &kick_msg); } - Ok(count as i32) + Ok(1) } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).then( - argument(ARG_TARGET, SimpleArgConsumer) - .execute(NoReasonExecutor) - .then(argument(ARG_REASON, MsgArgConsumer).execute(ReasonExecutor)), - ) +struct BanIpExecutor { + has_reason: bool, +} + +impl CommandExecutor for BanIpExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let target = StringArgumentType::get(context, "ip")?; + let reason = if self.has_reason { + Some(StringArgumentType::get(context, "reason")?.to_string()) + } else { + None + }; + + ban_ip(context, target, reason) + } +} + +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Three), + )); + + let cmd = command("ban-ip", DESCRIPTION).requires(PERMISSION).then( + argument("ip", StringArgumentType::SingleWord) + .executes(BanIpExecutor { has_reason: false }) + .then( + argument("reason", StringArgumentType::GreedyPhrase) + .executes(BanIpExecutor { has_reason: true }), + ), + ); + + dispatcher.register_with_aliases(cmd, &["banip"]); } diff --git a/crates/pumpkin/src/command/commands/bossbar.rs b/crates/pumpkin/src/command/commands/bossbar.rs index 037e41045..b7a7e36ab 100644 --- a/crates/pumpkin/src/command/commands/bossbar.rs +++ b/crates/pumpkin/src/command/commands/bossbar.rs @@ -1,57 +1,207 @@ -use crate::command::args::bool::BoolArgConsumer; -use crate::command::args::bossbar_color::BossbarColorArgumentConsumer; -use crate::command::args::bossbar_style::BossbarStyleArgumentConsumer; -use crate::command::args::bounded_num::BoundedNumArgumentConsumer; -use crate::command::args::players::PlayersArgumentConsumer; -use crate::command::args::resource_location::ResourceLocationArgumentConsumer; - -use crate::command::args::{ConsumedArgs, FindArg, FindArgDefaultName}; - -use crate::command::args::textcomponent::TextComponentArgConsumer; -use crate::command::dispatcher::CommandError; -use crate::command::suggestion::suggestions::SuggestionsBuilder; -use crate::command::tree::builder::{argument, argument_default_name, literal}; -use crate::command::tree::{CommandSuggestionProvider, CommandSuggestionResult, CommandTree}; -use crate::command::{CommandExecutor, CommandResult, CommandSender}; -use crate::server::Server; -use crate::world::bossbar::Bossbar; -use crate::world::custom_bossbar::BossbarUpdateError; -use pumpkin_data::translation; -use pumpkin_util::text::TextComponent; -use pumpkin_util::text::hover::HoverEvent; use uuid::Uuid; -const NAMES: [&str; 1] = ["bossbar"]; +use pumpkin_data::translation; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; +use pumpkin_util::text::TextComponent; +use pumpkin_util::text::hover::HoverEvent; + +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::FromStringReader; +use crate::command::argument_types::argument_type::{ArgumentType, JavaClientArgumentType}; +use crate::command::argument_types::component::ComponentArgumentType; +use crate::command::argument_types::core::bool::BoolArgumentType; +use crate::command::argument_types::core::integer::IntegerArgumentType; +use crate::command::argument_types::entity::EntityArgumentType; +use crate::command::argument_types::identifier::IdentifierArgumentType; +use crate::command::context::command_context::CommandContext; +use crate::command::errors::command_syntax_error::CommandSyntaxError; +use crate::command::errors::error_types::CommandErrorType; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; +use crate::command::string_reader::StringReader; +use crate::command::suggestion::suggestions::{Suggestions, SuggestionsBuilder}; +use crate::world::bossbar::{Bossbar, BossbarColor, BossbarDivisions}; +use crate::world::custom_bossbar::BossbarUpdateError; + const DESCRIPTION: &str = "Creates and modifies boss bars"; +const PERMISSION: &str = "minecraft:command.bossbar"; -const ARG_NAME: &str = "name"; +const ERROR_INVALID_COLOR: CommandErrorType<1> = CommandErrorType::new( + translation::java::ARGUMENT_ENUM_INVALID, + translation::java::ARGUMENT_ENUM_INVALID, +); -const ARG_VISIBLE: &str = "visible"; +const ERROR_INVALID_STYLE: CommandErrorType<1> = CommandErrorType::new( + translation::java::ARGUMENT_ENUM_INVALID, + translation::java::ARGUMENT_ENUM_INVALID, +); -const fn autocomplete_consumer() -> ResourceLocationArgumentConsumer { - ResourceLocationArgumentConsumer +const ERROR_UNKNOWN_BOSSBAR: CommandErrorType<1> = CommandErrorType::new( + translation::java::COMMANDS_BOSSBAR_UNKNOWN, + translation::bedrock::COMMANDS_BOSSBAR_NOTFOUND, +); + +const ERROR_CREATE_FAILED: CommandErrorType<1> = CommandErrorType::new( + translation::java::COMMANDS_BOSSBAR_CREATE_FAILED, + translation::bedrock::COMMANDS_BOSSBAR_ADD_FAILURE_EXISTS, +); + +#[derive(Clone, Copy)] +pub struct BossbarIdArgumentType; + +impl ArgumentType for BossbarIdArgumentType { + type Item = String; + + fn parse(&self, reader: &mut StringReader) -> Result { + let ident = pumpkin_util::identifier::Identifier::from_reader(reader)?; + Ok(ident.to_string()) + } + + fn client_side_parser(&'_ self) -> JavaClientArgumentType { + JavaClientArgumentType::ResourceLocation + } + + fn list_suggestions( + &self, + context: &CommandContext, + builder: SuggestionsBuilder, + ) -> Suggestions { + let bossbars = context.source.server().bossbars.lock().unwrap(); + builder + .filter_and_suggest_iter(bossbars.custom_bossbars.keys().cloned()) + .build() + } } -struct BossbarSuggestionProvider; +impl BossbarIdArgumentType { + pub fn get(context: &CommandContext, name: &str) -> Result { + context.get_argument::(name).cloned() + } +} -impl CommandSuggestionProvider for BossbarSuggestionProvider { - fn suggest( - &self, - _src: &CommandSender, - server: &Server, - input: &str, - start: usize, - _end: usize, - ) -> CommandSuggestionResult { - let mut builder = SuggestionsBuilder::new(input, start); - let bossbars = server.bossbars.lock().unwrap(); - let remaining = builder.remaining_lowercase().to_string(); - for key in bossbars.custom_bossbars.keys() { - if key.to_lowercase().starts_with(&remaining) { - builder = builder.suggest(key.clone()); +#[derive(Clone, Copy)] +pub struct BossbarColorArgumentType; + +const COLORS: [&str; 7] = ["blue", "green", "pink", "purple", "red", "white", "yellow"]; + +impl ArgumentType for BossbarColorArgumentType { + type Item = BossbarColor; + + fn parse(&self, reader: &mut StringReader) -> Result { + let start = reader.cursor(); + let s = reader.read_unquoted_string(); + match s.to_lowercase().as_str() { + "blue" => Ok(BossbarColor::Blue), + "green" => Ok(BossbarColor::Green), + "pink" => Ok(BossbarColor::Pink), + "purple" => Ok(BossbarColor::Purple), + "red" => Ok(BossbarColor::Red), + "white" => Ok(BossbarColor::White), + "yellow" => Ok(BossbarColor::Yellow), + _ => { + reader.set_cursor(start); + Err(ERROR_INVALID_COLOR.create(reader, TextComponent::text(s))) } } - builder.build() + } + + fn client_side_parser(&'_ self) -> JavaClientArgumentType { + JavaClientArgumentType::String( + pumpkin_protocol::java::client::play::StringProtoArgBehavior::SingleWord, + ) + } + + fn list_suggestions( + &self, + _context: &CommandContext, + builder: SuggestionsBuilder, + ) -> Suggestions { + builder.filter_and_suggest(&COLORS).build() + } +} + +impl BossbarColorArgumentType { + pub fn get(context: &CommandContext, name: &str) -> Result { + context.get_argument::(name).copied() + } +} + +#[derive(Clone, Copy)] +pub struct BossbarStyleArgumentType; + +const STYLES: [&str; 5] = [ + "notched_10", + "notched_12", + "notched_20", + "notched_6", + "progress", +]; + +impl ArgumentType for BossbarStyleArgumentType { + type Item = BossbarDivisions; + + fn parse(&self, reader: &mut StringReader) -> Result { + let start = reader.cursor(); + let s = reader.read_unquoted_string(); + match s.to_lowercase().as_str() { + "notched_10" => Ok(BossbarDivisions::Notches10), + "notched_12" => Ok(BossbarDivisions::Notches12), + "notched_20" => Ok(BossbarDivisions::Notches20), + "notched_6" => Ok(BossbarDivisions::Notches6), + "progress" => Ok(BossbarDivisions::NoDivision), + _ => { + reader.set_cursor(start); + Err(ERROR_INVALID_STYLE.create(reader, TextComponent::text(s))) + } + } + } + + fn client_side_parser(&'_ self) -> JavaClientArgumentType { + JavaClientArgumentType::String( + pumpkin_protocol::java::client::play::StringProtoArgBehavior::SingleWord, + ) + } + + fn list_suggestions( + &self, + _context: &CommandContext, + builder: SuggestionsBuilder, + ) -> Suggestions { + builder.filter_and_suggest(&STYLES).build() + } +} + +impl BossbarStyleArgumentType { + pub fn get( + context: &CommandContext, + name: &str, + ) -> Result { + context.get_argument::(name).copied() + } +} + +fn bossbar_prefix(title: TextComponent, namespace: String) -> TextComponent { + TextComponent::text("[") + .add_child(title) + .add_child(TextComponent::text("]")) + .hover_event(HoverEvent::show_text(TextComponent::text(namespace))) +} + +fn handle_bossbar_error(error: BossbarUpdateError) -> CommandSyntaxError { + match error { + BossbarUpdateError::InvalidResourceLocation(location) => { + ERROR_UNKNOWN_BOSSBAR.create_without_context(TextComponent::text(location)) + } + BossbarUpdateError::NoChanges(value, variation) => { + let key = variation.map_or_else( + || format!("commands.bossbar.set.{value}.unchanged"), + |var| format!("commands.bossbar.set.{value}.unchanged.{var}"), + ); + + crate::command::errors::error_types::DISPATCHER_PARSE_EXCEPTION + .create_without_context(TextComponent::translate_cross(key.clone(), key, [])) + } } } @@ -75,24 +225,13 @@ enum CommandValueSet { struct AddExecutor; impl CommandExecutor for AddExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let namespace = autocomplete_consumer() - .find_arg_default_name(args)? - .to_string(); - - let text_component = TextComponentArgConsumer::find_arg(args, ARG_NAME)?; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let namespace = IdentifierArgumentType::get(context, "id")?.to_string(); + let text_component = ComponentArgumentType::get(context, "name")?; + let server = context.source.server(); if server.bossbars.lock().unwrap().has_bossbar(&namespace) { - return Result::Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_BOSSBAR_CREATE_FAILED, - translation::bedrock::COMMANDS_BOSSBAR_ADD_FAILURE_EXISTS, - [TextComponent::text(namespace)], - ))); + return Err(ERROR_CREATE_FAILED.create_without_context(TextComponent::text(namespace))); } let bossbar = Bossbar::new(text_component); @@ -102,11 +241,14 @@ impl CommandExecutor for AddExecutor { let new_size = bossbars.get_bossbars_len(); drop(bossbars); - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_BOSSBAR_CREATE_SUCCESS, - translation::bedrock::COMMANDS_BOSSBAR_ADD_SUCCESS, - [bossbar_prefix(bossbar.title, namespace)], - )); + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_BOSSBAR_CREATE_SUCCESS, + translation::bedrock::COMMANDS_BOSSBAR_ADD_SUCCESS, + [bossbar_prefix(bossbar.title, namespace)], + ), + true, + ); Ok(new_size as i32) } @@ -115,15 +257,9 @@ impl CommandExecutor for AddExecutor { struct GetExecutor(CommandValueGet); impl CommandExecutor for GetExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let namespace = autocomplete_consumer() - .find_arg_default_name(args)? - .to_string(); + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let namespace = BossbarIdArgumentType::get(context, "id")?; + let server = context.source.server(); let Some(bossbar) = server.bossbars.lock().unwrap().get_bossbar(&namespace) else { return Err(handle_bossbar_error( @@ -133,14 +269,17 @@ impl CommandExecutor for GetExecutor { match self.0 { CommandValueGet::Max => { - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_BOSSBAR_GET_MAX, - translation::bedrock::COMMANDS_BOSSBAR_GET_MAX, - [ - bossbar_prefix(bossbar.bossbar_data.title.clone(), namespace), - TextComponent::text(bossbar.max.to_string()), - ], - )); + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_BOSSBAR_GET_MAX, + translation::bedrock::COMMANDS_BOSSBAR_GET_MAX, + [ + bossbar_prefix(bossbar.bossbar_data.title.clone(), namespace), + TextComponent::text(bossbar.max.to_string()), + ], + ), + false, + ); Ok(bossbar.max) } CommandValueGet::Players => { @@ -153,37 +292,46 @@ impl CommandExecutor for GetExecutor { let count = online_players.len() as i32; if count == 0 { - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_BOSSBAR_GET_PLAYERS_NONE, - translation::bedrock::COMMANDS_BOSSBAR_GET_PLAYERS_NONE, - [bossbar_prefix(bossbar.bossbar_data.title, namespace)], - )); + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_BOSSBAR_GET_PLAYERS_NONE, + translation::bedrock::COMMANDS_BOSSBAR_GET_PLAYERS_NONE, + [bossbar_prefix(bossbar.bossbar_data.title, namespace)], + ), + false, + ); } else { - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_BOSSBAR_GET_PLAYERS_SOME, - if count == 1 { - translation::bedrock::COMMANDS_BOSSBAR_GET_PLAYERS_ONE - } else { - translation::bedrock::COMMANDS_BOSSBAR_GET_PLAYERS - }, - [ - bossbar_prefix(bossbar.bossbar_data.title, namespace), - TextComponent::text(count.to_string()), - TextComponent::text(online_players.join(", ")), - ], - )); + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_BOSSBAR_GET_PLAYERS_SOME, + if count == 1 { + translation::bedrock::COMMANDS_BOSSBAR_GET_PLAYERS_ONE + } else { + translation::bedrock::COMMANDS_BOSSBAR_GET_PLAYERS + }, + [ + bossbar_prefix(bossbar.bossbar_data.title, namespace), + TextComponent::text(count.to_string()), + TextComponent::text(online_players.join(", ")), + ], + ), + false, + ); } Ok(count) } CommandValueGet::Value => { - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_BOSSBAR_GET_VALUE, - translation::bedrock::COMMANDS_BOSSBAR_GET_VALUE, - [ - bossbar_prefix(bossbar.bossbar_data.title.clone(), namespace), - TextComponent::text(bossbar.value.to_string()), - ], - )); + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_BOSSBAR_GET_VALUE, + translation::bedrock::COMMANDS_BOSSBAR_GET_VALUE, + [ + bossbar_prefix(bossbar.bossbar_data.title.clone(), namespace), + TextComponent::text(bossbar.value.to_string()), + ], + ), + false, + ); Ok(bossbar.value) } CommandValueGet::Visible => { @@ -198,14 +346,17 @@ impl CommandExecutor for GetExecutor { translation::bedrock::COMMANDS_BOSSBAR_GET_VISIBLE_FALSE, ) }; - sender.send_message(TextComponent::translate_cross( - java_key, - bedrock_key, - [bossbar_prefix( - bossbar.bossbar_data.title.clone(), - namespace, - )], - )); + context.source.send_feedback( + TextComponent::translate_cross( + java_key, + bedrock_key, + [bossbar_prefix( + bossbar.bossbar_data.title.clone(), + namespace, + )], + ), + false, + ); Ok(bossbar.visible as i32) } } @@ -215,20 +366,19 @@ impl CommandExecutor for GetExecutor { struct ListExecutor; impl CommandExecutor for ListExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - _args: &ConsumedArgs, - ) -> CommandResult { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let server = context.source.server(); let bossbars = server.bossbars.lock().unwrap().get_all_bossbars(); if bossbars.is_empty() { - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_BOSSBAR_LIST_BARS_NONE, - translation::bedrock::COMMANDS_BOSSBAR_LIST_NONE, - [], - )); + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_BOSSBAR_LIST_BARS_NONE, + translation::bedrock::COMMANDS_BOSSBAR_LIST_NONE, + [], + ), + false, + ); return Ok(0); } @@ -248,14 +398,17 @@ impl CommandExecutor for ListExecutor { } } - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_BOSSBAR_LIST_BARS_SOME, - translation::bedrock::COMMANDS_BOSSBAR_LIST, - [ - TextComponent::text(bossbars.len().to_string()), - bossbars_text, - ], - )); + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_BOSSBAR_LIST_BARS_SOME, + translation::bedrock::COMMANDS_BOSSBAR_LIST, + [ + TextComponent::text(bossbars.len().to_string()), + bossbars_text, + ], + ), + false, + ); Ok(bossbars.len() as i32) } @@ -264,15 +417,9 @@ impl CommandExecutor for ListExecutor { struct RemoveExecutor; impl CommandExecutor for RemoveExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let namespace = autocomplete_consumer() - .find_arg_default_name(args)? - .to_string(); + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let namespace = BossbarIdArgumentType::get(context, "id")?; + let server = context.source.server(); let Some(bossbar) = server.bossbars.lock().unwrap().get_bossbar(&namespace) else { return Err(handle_bossbar_error( @@ -280,14 +427,17 @@ impl CommandExecutor for RemoveExecutor { )); }; - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_BOSSBAR_REMOVE_SUCCESS, - translation::bedrock::COMMANDS_BOSSBAR_REMOVE, - [bossbar_prefix( - bossbar.bossbar_data.title, - namespace.clone(), - )], - )); + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_BOSSBAR_REMOVE_SUCCESS, + translation::bedrock::COMMANDS_BOSSBAR_REMOVE, + [bossbar_prefix( + bossbar.bossbar_data.title, + namespace.clone(), + )], + ), + true, + ); let res = server .bossbars @@ -304,16 +454,10 @@ impl CommandExecutor for RemoveExecutor { struct SetExecutor(CommandValueSet); impl CommandExecutor for SetExecutor { - #[expect(clippy::too_many_lines)] - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let namespace = autocomplete_consumer() - .find_arg_default_name(args)? - .to_string(); + #[allow(clippy::too_many_lines)] + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let namespace = BossbarIdArgumentType::get(context, "id")?; + let server = context.source.server(); let Some(bossbar) = server.bossbars.lock().unwrap().get_bossbar(&namespace) else { return Err(handle_bossbar_error( @@ -323,31 +467,28 @@ impl CommandExecutor for SetExecutor { match self.0 { CommandValueSet::Color => { - let color = BossbarColorArgumentConsumer.find_arg_default_name(args)?; + let color = BossbarColorArgumentType::get(context, "color")?; server .bossbars .lock() .unwrap() - .update_color(server, &namespace, *color) + .update_color(server, &namespace, color) .map_err(handle_bossbar_error)?; - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_BOSSBAR_SET_COLOR_SUCCESS, - translation::java::COMMANDS_BOSSBAR_SET_COLOR_SUCCESS, - [bossbar_prefix(bossbar.bossbar_data.title, namespace)], - )); + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_BOSSBAR_SET_COLOR_SUCCESS, + translation::java::COMMANDS_BOSSBAR_SET_COLOR_SUCCESS, + [bossbar_prefix(bossbar.bossbar_data.title, namespace)], + ), + true, + ); Ok(0) } CommandValueSet::Max => { - let Ok(max_value) = max_value_consumer().find_arg_default_name(args)? else { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - "parsing.int.invalid", - "parsing.int.invalid", - [TextComponent::text(i32::MAX.to_string())], - ))); - }; + let max_value = IntegerArgumentType::get(context, "max")?; server .bossbars @@ -356,19 +497,22 @@ impl CommandExecutor for SetExecutor { .update_max(server, namespace.clone(), max_value) .map_err(handle_bossbar_error)?; - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_BOSSBAR_SET_MAX_SUCCESS, - translation::java::COMMANDS_BOSSBAR_SET_MAX_SUCCESS, - [ - bossbar_prefix(bossbar.bossbar_data.title, namespace), - TextComponent::text(max_value.to_string()), - ], - )); + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_BOSSBAR_SET_MAX_SUCCESS, + translation::java::COMMANDS_BOSSBAR_SET_MAX_SUCCESS, + [ + bossbar_prefix(bossbar.bossbar_data.title, namespace), + TextComponent::text(max_value.to_string()), + ], + ), + true, + ); Ok(0) } CommandValueSet::Name => { - let name = TextComponentArgConsumer::find_arg(args, ARG_NAME)?; + let name = ComponentArgumentType::get(context, "name")?; server .bossbars .lock() @@ -376,11 +520,14 @@ impl CommandExecutor for SetExecutor { .update_name(server, &namespace, &name) .map_err(handle_bossbar_error)?; - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_BOSSBAR_SET_NAME_SUCCESS, - translation::java::COMMANDS_BOSSBAR_SET_NAME_SUCCESS, - [bossbar_prefix(name, namespace)], - )); + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_BOSSBAR_SET_NAME_SUCCESS, + translation::java::COMMANDS_BOSSBAR_SET_NAME_SUCCESS, + [bossbar_prefix(name, namespace)], + ), + true, + ); Ok(0) } @@ -393,16 +540,19 @@ impl CommandExecutor for SetExecutor { .set_players(server, namespace.clone(), vec![]) .map_err(handle_bossbar_error)?; - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_BOSSBAR_SET_PLAYERS_SUCCESS_NONE, - translation::java::COMMANDS_BOSSBAR_SET_PLAYERS_SUCCESS_NONE, - [bossbar_prefix(bossbar.bossbar_data.title, namespace)], - )); + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_BOSSBAR_SET_PLAYERS_SUCCESS_NONE, + translation::java::COMMANDS_BOSSBAR_SET_PLAYERS_SUCCESS_NONE, + [bossbar_prefix(bossbar.bossbar_data.title, namespace)], + ), + true, + ); return Ok(0); } - let targets = PlayersArgumentConsumer.find_arg_default_name(args)?; + let targets = EntityArgumentType::get_players(context, "targets")?; let players: Vec = targets.iter().map(|player| player.gameprofile.id).collect(); let count = players.len(); @@ -420,42 +570,42 @@ impl CommandExecutor for SetExecutor { .collect::>() .join(", "); - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_BOSSBAR_SET_PLAYERS_SUCCESS_SOME, - translation::java::COMMANDS_BOSSBAR_SET_PLAYERS_SUCCESS_SOME, - [ - bossbar_prefix(bossbar.bossbar_data.title, namespace), - TextComponent::text(count.to_string()), - TextComponent::text(player_names), - ], - )); + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_BOSSBAR_SET_PLAYERS_SUCCESS_SOME, + translation::java::COMMANDS_BOSSBAR_SET_PLAYERS_SUCCESS_SOME, + [ + bossbar_prefix(bossbar.bossbar_data.title, namespace), + TextComponent::text(count.to_string()), + TextComponent::text(player_names), + ], + ), + true, + ); Ok(count as i32) } CommandValueSet::Style => { - let style = BossbarStyleArgumentConsumer.find_arg_default_name(args)?; + let style = BossbarStyleArgumentType::get(context, "style")?; server .bossbars .lock() .unwrap() - .update_style(server, &namespace, *style) + .update_style(server, &namespace, style) .map_err(handle_bossbar_error)?; - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_BOSSBAR_SET_STYLE_SUCCESS, - translation::java::COMMANDS_BOSSBAR_SET_STYLE_SUCCESS, - [bossbar_prefix(bossbar.bossbar_data.title, namespace)], - )); + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_BOSSBAR_SET_STYLE_SUCCESS, + translation::java::COMMANDS_BOSSBAR_SET_STYLE_SUCCESS, + [bossbar_prefix(bossbar.bossbar_data.title, namespace)], + ), + true, + ); Ok(0) } CommandValueSet::Value => { - let Ok(value) = value_consumer().find_arg_default_name(args)? else { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - "parsing.int.invalid", - "parsing.int.invalid", - [TextComponent::text(i32::MAX.to_string())], - ))); - }; + let value = IntegerArgumentType::get(context, "value")?; server .bossbars @@ -464,19 +614,22 @@ impl CommandExecutor for SetExecutor { .update_value(server, namespace.clone(), value) .map_err(handle_bossbar_error)?; - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_BOSSBAR_SET_VALUE_SUCCESS, - translation::java::COMMANDS_BOSSBAR_SET_VALUE_SUCCESS, - [ - bossbar_prefix(bossbar.bossbar_data.title, namespace), - TextComponent::text(value.to_string()), - ], - )); + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_BOSSBAR_SET_VALUE_SUCCESS, + translation::java::COMMANDS_BOSSBAR_SET_VALUE_SUCCESS, + [ + bossbar_prefix(bossbar.bossbar_data.title, namespace), + TextComponent::text(value.to_string()), + ], + ), + true, + ); Ok(value) } CommandValueSet::Visible => { - let visibility = BoolArgConsumer::find_arg(args, ARG_VISIBLE)?; + let visibility = BoolArgumentType::get(context, "visible")?; server .bossbars @@ -491,11 +644,14 @@ impl CommandExecutor for SetExecutor { translation::java::COMMANDS_BOSSBAR_SET_VISIBLE_SUCCESS_HIDDEN }; - sender.send_message(TextComponent::translate_cross( - state, - state, - [bossbar_prefix(bossbar.bossbar_data.title, namespace)], - )); + context.source.send_feedback( + TextComponent::translate_cross( + state, + state, + [bossbar_prefix(bossbar.bossbar_data.title, namespace)], + ), + true, + ); Ok(0) } @@ -503,115 +659,84 @@ impl CommandExecutor for SetExecutor { } } -const fn max_value_consumer() -> BoundedNumArgumentConsumer { - BoundedNumArgumentConsumer::new().min(1).name("max") -} +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); -const fn value_consumer() -> BoundedNumArgumentConsumer { - BoundedNumArgumentConsumer::new().min(0).name("value") -} + let add_node = literal("add").then( + argument("id", IdentifierArgumentType) + .then(argument("name", ComponentArgumentType).executes(AddExecutor)), + ); -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION) - .then( - literal("add").then( - argument_default_name(autocomplete_consumer()) - .then(argument(ARG_NAME, TextComponentArgConsumer).execute(AddExecutor)), - ), - ) - .then( - literal("get").then( - argument_default_name(autocomplete_consumer()) - .suggests(BossbarSuggestionProvider) - .then(literal("max").execute(GetExecutor(CommandValueGet::Max))) - .then(literal("players").execute(GetExecutor(CommandValueGet::Players))) - .then(literal("value").execute(GetExecutor(CommandValueGet::Value))) - .then(literal("visible").execute(GetExecutor(CommandValueGet::Visible))), - ), - ) - .then(literal("list").execute(ListExecutor)) - .then( - literal("remove").then( - argument_default_name(autocomplete_consumer()) - .suggests(BossbarSuggestionProvider) - .execute(RemoveExecutor), - ), - ) - .then( - literal("set").then( - argument_default_name(autocomplete_consumer()) - .suggests(BossbarSuggestionProvider) + let get_node = literal("get").then( + argument("id", BossbarIdArgumentType) + .then(literal("max").executes(GetExecutor(CommandValueGet::Max))) + .then(literal("players").executes(GetExecutor(CommandValueGet::Players))) + .then(literal("value").executes(GetExecutor(CommandValueGet::Value))) + .then(literal("visible").executes(GetExecutor(CommandValueGet::Visible))), + ); + + let remove_node = + literal("remove").then(argument("id", BossbarIdArgumentType).executes(RemoveExecutor)); + + let set_node = literal("set").then( + argument("id", BossbarIdArgumentType) + .then( + literal("color").then( + argument("color", BossbarColorArgumentType) + .executes(SetExecutor(CommandValueSet::Color)), + ), + ) + .then( + literal("max").then( + argument("max", IntegerArgumentType::with_min(1)) + .executes(SetExecutor(CommandValueSet::Max)), + ), + ) + .then( + literal("name").then( + argument("name", ComponentArgumentType) + .executes(SetExecutor(CommandValueSet::Name)), + ), + ) + .then( + literal("players") + .executes(SetExecutor(CommandValueSet::Players(false))) .then( - literal("color").then( - argument_default_name(BossbarColorArgumentConsumer) - .execute(SetExecutor(CommandValueSet::Color)), - ), - ) - .then( - literal("max").then( - argument_default_name(max_value_consumer()) - .execute(SetExecutor(CommandValueSet::Max)), - ), - ) - .then( - literal("name").then( - argument(ARG_NAME, TextComponentArgConsumer) - .execute(SetExecutor(CommandValueSet::Name)), - ), - ) - .then( - literal("players") - .then( - argument_default_name(PlayersArgumentConsumer) - .execute(SetExecutor(CommandValueSet::Players(true))), - ) - .execute(SetExecutor(CommandValueSet::Players(false))), - ) - .then( - literal("style").then( - argument_default_name(BossbarStyleArgumentConsumer) - .execute(SetExecutor(CommandValueSet::Style)), - ), - ) - .then( - literal("value").then( - argument_default_name(value_consumer()) - .execute(SetExecutor(CommandValueSet::Value)), - ), - ) - .then( - literal("visible").then( - argument(ARG_VISIBLE, BoolArgConsumer) - .execute(SetExecutor(CommandValueSet::Visible)), - ), + argument("targets", EntityArgumentType::Players) + .executes(SetExecutor(CommandValueSet::Players(true))), ), + ) + .then( + literal("style").then( + argument("style", BossbarStyleArgumentType) + .executes(SetExecutor(CommandValueSet::Style)), + ), + ) + .then( + literal("value").then( + argument("value", IntegerArgumentType::with_min(0)) + .executes(SetExecutor(CommandValueSet::Value)), + ), + ) + .then( + literal("visible").then( + argument("visible", BoolArgumentType) + .executes(SetExecutor(CommandValueSet::Visible)), + ), ), - ) -} + ); -fn bossbar_prefix(title: TextComponent, namespace: String) -> TextComponent { - TextComponent::text("[") - .add_child(title) - .add_child(TextComponent::text("]")) - .hover_event(HoverEvent::show_text(TextComponent::text(namespace))) -} - -fn handle_bossbar_error(error: BossbarUpdateError) -> CommandError { - match error { - BossbarUpdateError::InvalidResourceLocation(location) => { - CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_BOSSBAR_UNKNOWN, - translation::bedrock::COMMANDS_BOSSBAR_NOTFOUND, - [TextComponent::text(location)], - )) - } - BossbarUpdateError::NoChanges(value, variation) => { - let key = variation.map_or_else( - || format!("commands.bossbar.set.{value}.unchanged"), - |var| format!("commands.bossbar.set.{value}.unchanged.{var}"), - ); - - CommandError::CommandFailed(TextComponent::translate_cross(key.clone(), key, [])) - } - } + dispatcher.register( + command("bossbar", DESCRIPTION) + .requires(PERMISSION) + .then(add_node) + .then(get_node) + .then(literal("list").executes(ListExecutor)) + .then(remove_node) + .then(set_node), + ); } diff --git a/crates/pumpkin/src/command/commands/clear.rs b/crates/pumpkin/src/command/commands/clear.rs index 28aef6924..97374a0cb 100644 --- a/crates/pumpkin/src/command/commands/clear.rs +++ b/crates/pumpkin/src/command/commands/clear.rs @@ -2,37 +2,43 @@ use std::sync::Arc; use pumpkin_data::item_stack::ItemStack; use pumpkin_data::translation; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; -use crate::command::args::bounded_num::BoundedNumArgumentConsumer; -use crate::command::args::players::PlayersArgumentConsumer; -use crate::command::args::resource::item::{ItemPredicate, ItemPredicateArgumentConsumer}; -use crate::command::args::{Arg, ConsumedArgs, FindArg, FindArgDefaultName}; -use crate::command::tree::CommandTree; -use crate::command::tree::builder::{argument, require}; -use crate::command::{CommandError, CommandExecutor, CommandResult, CommandSender}; -use crate::entity::EntityBase; +use crate::command::argument_builder::{ArgumentBuilder, argument, command}; +use crate::command::argument_types::core::integer::IntegerArgumentType; +use crate::command::argument_types::entity::EntityArgumentType; +use crate::command::argument_types::item_predicate::{ItemPredicate, ItemPredicateArgumentType}; +use crate::command::context::command_context::CommandContext; +use crate::command::context::command_source::CommandSource; +use crate::command::errors::command_syntax_error::CommandSyntaxError; +use crate::command::errors::error_types::CommandErrorType; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; use crate::entity::player::Player; -use CommandError::InvalidConsumption; -const NAMES: [&str; 1] = ["clear"]; -const DESCRIPTION: &str = "Clear your inventory or that of target(s)"; +const DESCRIPTION: &str = "Clear your inventory or that of target(s)."; +const PERMISSION: &str = "minecraft:command.clear"; -const ARG_TARGETS: &str = "targets"; -const ARG_ITEM: &str = "item"; -const ARG_MAX_COUNT: &str = "max_count"; +const ERROR_SINGLE: CommandErrorType<1> = CommandErrorType::new( + translation::java::CLEAR_FAILED_SINGLE, + translation::java::CLEAR_FAILED_SINGLE, +); + +const ERROR_MULTIPLE: CommandErrorType<1> = CommandErrorType::new( + translation::java::CLEAR_FAILED_MULTIPLE, + translation::java::CLEAR_FAILED_MULTIPLE, +); + +const ERROR_NOT_PLAYER: CommandErrorType<0> = CommandErrorType::new( + translation::java::PERMISSIONS_REQUIRES_PLAYER, + translation::java::PERMISSIONS_REQUIRES_PLAYER, +); const MAX_NO_UPPER_LIMIT: i32 = -1; const MAX_NO_CLEAR_BUT_SIMULATE: i32 = 0; -/// Returns the number of items actually cleared. -/// -/// If `max` provided is equal to [`MAX_NO_CLEAR_BUT_SIMULATE`] (`0`), then no items are cleared, -/// but instead returns the items that *could* be cleared. -/// -/// If `max` provided is [`MAX_NO_UPPER_LIMIT`] (`-1`), then there is no limit in clearing. -/// -/// Otherwise, at most `max` items are cleared. fn clear_player(target: &Player, item: &ItemPredicate, max: i32) -> i32 { let inventory = target.inventory(); let mut count: i32 = 0; @@ -75,7 +81,7 @@ fn test_and_clear( slot_lock: &mut ItemStack, is_done: &mut bool, ) { - if item.test_item_stack(slot_lock) { + if item.test(slot_lock) { let item_count = slot_lock.item_count as i32; if *max == MAX_NO_CLEAR_BUT_SIMULATE { *count += item_count; @@ -83,207 +89,176 @@ fn test_and_clear( *count += item_count; *slot_lock = ItemStack::EMPTY.clone(); } else { - // We need more complex logic for this one. let taken = i32::min(*max, item_count); - - // Take all that we can. *count += taken; if taken == item_count { *slot_lock = ItemStack::EMPTY.clone(); - *max -= taken; - - // Set `is_done` flag if required. - *is_done = *max == 0; } else { - // As `slot_lock.item_count` is limited to `u8`, this should be fine. slot_lock.decrement(taken as u8); - *max -= taken; + } + *max -= taken; + *is_done = *max == 0; + } + } +} - // Set `is_done` flag if required. - *is_done = true; +fn clear_inventory( + source: &CommandSource, + players: &[Arc], + predicate: &ItemPredicate, + max_count: i32, +) -> Result { + let mut total_count = 0; + + for player in players { + total_count += clear_player(player, predicate, max_count); + } + + if total_count == 0 { + if players.len() == 1 { + let player_name = players[0].gameprofile.name.clone(); + Err(ERROR_SINGLE.create_without_context(TextComponent::text(player_name))) + } else { + Err(ERROR_MULTIPLE + .create_without_context(TextComponent::text(players.len().to_string()))) + } + } else { + if max_count == 0 { + if players.len() == 1 { + let player_name = players[0].gameprofile.name.clone(); + source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_CLEAR_TEST_SINGLE, + translation::java::COMMANDS_CLEAR_TEST_SINGLE, + [ + TextComponent::text(total_count.to_string()), + TextComponent::text(player_name), + ], + ), + true, + ); + } else { + source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_CLEAR_TEST_MULTIPLE, + translation::java::COMMANDS_CLEAR_TEST_MULTIPLE, + [ + TextComponent::text(total_count.to_string()), + TextComponent::text(players.len().to_string()), + ], + ), + true, + ); + } + } else if players.len() == 1 { + let player_name = players[0].gameprofile.name.clone(); + source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_CLEAR_SUCCESS_SINGLE, + translation::java::COMMANDS_CLEAR_SUCCESS_SINGLE, + [ + TextComponent::text(total_count.to_string()), + TextComponent::text(player_name), + ], + ), + true, + ); + } else { + source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_CLEAR_SUCCESS_MULTIPLE, + translation::java::COMMANDS_CLEAR_SUCCESS_MULTIPLE, + [ + TextComponent::text(total_count.to_string()), + TextComponent::text(players.len().to_string()), + ], + ), + true, + ); + } + + Ok(total_count) + } +} + +#[derive(Clone, Copy)] +enum ClearStep { + CallerOnly, + TargetsOnly, + WithItem, + WithMaxCount, +} + +struct ClearExecutor { + step: ClearStep, +} + +impl CommandExecutor for ClearExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + match self.step { + ClearStep::CallerOnly => { + let player = context + .source + .output + .as_player() + .ok_or_else(|| ERROR_NOT_PLAYER.create_without_context())?; + clear_inventory( + &context.source, + std::slice::from_ref(&player), + &ItemPredicate::Any, + -1, + ) + } + ClearStep::TargetsOnly => { + let targets = EntityArgumentType::get_players(context, "targets")?; + clear_inventory(&context.source, &targets, &ItemPredicate::Any, -1) + } + ClearStep::WithItem => { + let targets = EntityArgumentType::get_players(context, "targets")?; + let item = ItemPredicateArgumentType::get(context, "item")?; + clear_inventory(&context.source, &targets, &item, -1) + } + ClearStep::WithMaxCount => { + let targets = EntityArgumentType::get_players(context, "targets")?; + let item = ItemPredicateArgumentType::get(context, "item")?; + let max_count = IntegerArgumentType::get(context, "maxCount")?; + clear_inventory(&context.source, &targets, &item, max_count) } } } } -fn command_result( - sender: &CommandSender, - item_count: i32, - max_count: i32, - targets: &[Arc], -) -> Result { - match clear_command_text_output(item_count, max_count, targets) { - Ok(success) => { - sender.send_message(success); - Ok(item_count) - } - Err(failure) => Err(CommandError::CommandFailed(failure)), - } -} - -fn clear_command_text_output( - item_count: i32, - max_count: i32, - targets: &[Arc], -) -> Result { - match (targets, item_count == 0, max_count == 0) { - ([target], true, _) => Err(TextComponent::translate_cross( - translation::java::CLEAR_FAILED_SINGLE, - translation::bedrock::COMMANDS_CLEAR_FAILURE, - [target.get_display_name()], - )), - (targets, true, _) => Err(TextComponent::translate_cross( - translation::java::CLEAR_FAILED_MULTIPLE, - translation::bedrock::COMMANDS_CLEAR_FAILURE, - [TextComponent::text(targets.len().to_string())], - )), - ([target], false, false) => Ok(TextComponent::translate_cross( - translation::java::COMMANDS_CLEAR_SUCCESS_SINGLE, - translation::java::COMMANDS_CLEAR_SUCCESS_SINGLE, - [ - TextComponent::text(item_count.to_string()), - target.get_display_name(), - ], - )), - (targets, false, false) => Ok(TextComponent::translate_cross( - translation::java::COMMANDS_CLEAR_SUCCESS_MULTIPLE, - translation::java::COMMANDS_CLEAR_SUCCESS_MULTIPLE, - [ - TextComponent::text(item_count.to_string()), - TextComponent::text(targets.len().to_string()), - ], - )), - ([target], false, true) => Ok(TextComponent::translate_cross( - translation::java::COMMANDS_CLEAR_TEST_SINGLE, - translation::java::COMMANDS_CLEAR_TEST_SINGLE, - [ - TextComponent::text(item_count.to_string()), - target.get_display_name(), - ], - )), - (targets, false, true) => Ok(TextComponent::translate_cross( - translation::java::COMMANDS_CLEAR_TEST_MULTIPLE, - translation::java::COMMANDS_CLEAR_TEST_MULTIPLE, - [ - TextComponent::text(item_count.to_string()), - TextComponent::text(targets.len().to_string()), - ], - )), - } -} - -const fn count_consumer() -> BoundedNumArgumentConsumer { - BoundedNumArgumentConsumer::new().min(0).name(ARG_MAX_COUNT) -} - -struct SelfExecutor; - -impl CommandExecutor for SelfExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - _args: &ConsumedArgs, - ) -> CommandResult { - let target = sender.as_player().ok_or(CommandError::InvalidRequirement)?; - - let items_cleared = clear_player(&target, &ItemPredicate::Any, MAX_NO_UPPER_LIMIT); - - command_result(sender, items_cleared, MAX_NO_UPPER_LIMIT, &[target]) - } -} - -struct Executor; - -impl CommandExecutor for Executor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::Players(targets)) = args.get(&ARG_TARGETS) else { - return Err(InvalidConsumption(Some(ARG_TARGETS.into()))); - }; - - let mut total_items_cleared = 0; - for target in targets { - total_items_cleared += clear_player(target, &ItemPredicate::Any, MAX_NO_UPPER_LIMIT); - } - - command_result(sender, total_items_cleared, MAX_NO_UPPER_LIMIT, targets) - } -} - -struct ItemExecutor; - -impl CommandExecutor for ItemExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::Players(targets)) = args.get(&ARG_TARGETS) else { - return Err(InvalidConsumption(Some(ARG_TARGETS.into()))); - }; - - let item = ItemPredicateArgumentConsumer::find_arg(args, ARG_ITEM)?; - - let mut total_items_cleared = 0; - for target in targets { - total_items_cleared += clear_player(target, &item, MAX_NO_UPPER_LIMIT); - } - - command_result(sender, total_items_cleared, MAX_NO_UPPER_LIMIT, targets) - } -} - -struct ItemCountExecutor; - -impl CommandExecutor for ItemCountExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::Players(targets)) = args.get(&ARG_TARGETS) else { - return Err(InvalidConsumption(Some(ARG_TARGETS.into()))); - }; - - let item = ItemPredicateArgumentConsumer::find_arg(args, ARG_ITEM)?; - let Ok(max) = count_consumer().find_arg_default_name(args)? else { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::PARSING_INT_INVALID, - translation::java::PARSING_INT_INVALID, - [TextComponent::text(i32::MAX.to_string())], - ))); - }; - - let mut total_items_cleared = 0; - for target in targets { - total_items_cleared += clear_player(target, &item, max); - } - - command_result(sender, total_items_cleared, max, targets) - } -} - -// #[expect(clippy::redundant_closure_for_method_calls)] // causes lifetime issues -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION) - .then( - argument(ARG_TARGETS, PlayersArgumentConsumer) - .execute(Executor) - .then( - argument(ARG_ITEM, ItemPredicateArgumentConsumer) - .execute(ItemExecutor) - .then( - argument(ARG_MAX_COUNT, count_consumer().name(ARG_MAX_COUNT)) - .execute(ItemCountExecutor), - ), - ), - ) - .then(require(super::super::CommandSender::is_player).execute(SelfExecutor)) +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + dispatcher.register( + command("clear", DESCRIPTION) + .requires(PERMISSION) + .executes(ClearExecutor { + step: ClearStep::CallerOnly, + }) + .then( + argument("targets", EntityArgumentType::Players) + .executes(ClearExecutor { + step: ClearStep::TargetsOnly, + }) + .then( + argument("item", ItemPredicateArgumentType) + .executes(ClearExecutor { + step: ClearStep::WithItem, + }) + .then( + argument("maxCount", IntegerArgumentType::with_min(0)).executes( + ClearExecutor { + step: ClearStep::WithMaxCount, + }, + ), + ), + ), + ), + ); } diff --git a/crates/pumpkin/src/command/commands/damage.rs b/crates/pumpkin/src/command/commands/damage.rs index 42c36f141..084a2a51b 100644 --- a/crates/pumpkin/src/command/commands/damage.rs +++ b/crates/pumpkin/src/command/commands/damage.rs @@ -1,110 +1,100 @@ -use pumpkin_data::{damage::DamageType, translation}; +use pumpkin_data::damage::DamageType; +use pumpkin_data::translation; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; -use crate::command::{ - CommandExecutor, CommandResult, CommandSender, - args::{ - Arg, ConsumedArgs, FindArg, bounded_num::BoundedNumArgumentConsumer, - entity::EntityArgumentConsumer, position_3d::Position3DArgumentConsumer, - resource::damage_type::DamageTypeArgumentConsumer, - }, - dispatcher::CommandError, - tree::{ - CommandTree, - builder::{argument, literal}, - }, -}; +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::coordinates::vec3::Vec3ArgumentType; +use crate::command::argument_types::core::float::FloatArgumentType; +use crate::command::argument_types::entity::EntityArgumentType; +use crate::command::argument_types::resource::{DAMAGE_TYPE_ARGUMENT, ResourceArgument}; +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 crate::entity::EntityBase; -const NAMES: [&str; 1] = ["damage"]; const DESCRIPTION: &str = "Deals damage to entities"; -const ARG_TARGET: &str = "target"; -const ARG_AMOUNT: &str = "amount"; -const ARG_DAMAGE_TYPE: &str = "damageType"; -const ARG_LOCATION: &str = "location"; -const ARG_ENTITY: &str = "entity"; -const ARG_CAUSE: &str = "cause"; +const PERMISSION: &str = "minecraft:command.damage"; -const fn amount_consumer() -> BoundedNumArgumentConsumer { - BoundedNumArgumentConsumer::new().name(ARG_AMOUNT).min(0.0) -} - -struct LocationExecutor; -struct EntityExecutor(bool); +const ERROR_INVULNERABLE: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_DAMAGE_INVULNERABLE, + translation::java::COMMANDS_DAMAGE_INVULNERABLE, +); fn send_damage_result( - sender: &CommandSender, + context: &CommandContext, success: bool, amount: f32, target_name: TextComponent, -) -> Result { +) -> CommandExecutorResult { if !success { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_DAMAGE_INVULNERABLE, - translation::java::COMMANDS_DAMAGE_INVULNERABLE, - [], - ))); + return Err(ERROR_INVULNERABLE.create_without_context()); } - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_DAMAGE_SUCCESS, - translation::bedrock::COMMANDS_DAMAGE_SUCCESS, - [TextComponent::text(amount.to_string()), target_name], - )); + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_DAMAGE_SUCCESS, + translation::bedrock::COMMANDS_DAMAGE_SUCCESS, + [TextComponent::text(amount.to_string()), target_name], + ), + true, + ); - Ok(1) // not arbitrary, this is what vanilla does + Ok(1) } -impl CommandExecutor for LocationExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let target = EntityArgumentConsumer::find_arg(args, ARG_TARGET)?; +struct DamageLocationExecutor; - let amount = BoundedNumArgumentConsumer::::find_arg(args, ARG_AMOUNT)??; - - let damage_type = args - .get(ARG_DAMAGE_TYPE) - .map_or(DamageType::GENERIC, |arg| match arg { - Arg::DamageType(dt) => *dt, - _ => DamageType::GENERIC, - }); - - let location = Position3DArgumentConsumer::find_arg(args, ARG_LOCATION)?; +impl CommandExecutor for DamageLocationExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let target = EntityArgumentType::get_entity(context, "target")?; + let amount = FloatArgumentType::get(context, "amount")?; + let damage_type = ResourceArgument::get_damage_type(context, "damageType")?; + let location = + Vec3ArgumentType::get_coordinates(context, "location")?.resolve(&context.source); let success = - target.damage_with_context(&*target, amount, damage_type, Some(location), None, None); + target.damage_with_context(&*target, amount, *damage_type, Some(location), None, None); - send_damage_result(sender, success, amount, target.get_display_name()) + send_damage_result(context, success, amount, target.get_display_name()) } } -impl CommandExecutor for EntityExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let target = EntityArgumentConsumer::find_arg(args, ARG_TARGET)?; +enum EntityMode { + Basic, + WithType, + WithSource, + WithSourceAndCause, +} - let amount = BoundedNumArgumentConsumer::::find_arg(args, ARG_AMOUNT)??; +struct DamageEntityExecutor(EntityMode); - let damage_type = args - .get(ARG_DAMAGE_TYPE) - .map_or(DamageType::GENERIC, |arg| match arg { - Arg::DamageType(dt) => *dt, - _ => DamageType::GENERIC, - }); +impl CommandExecutor for DamageEntityExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let target = EntityArgumentType::get_entity(context, "target")?; + let amount = FloatArgumentType::get(context, "amount")?; - let source = EntityArgumentConsumer::find_arg(args, ARG_ENTITY).ok(); - let cause = if self.0 { - EntityArgumentConsumer::find_arg(args, ARG_CAUSE).ok() - } else { - None + let damage_type = match self.0 { + EntityMode::Basic => DamageType::GENERIC, + EntityMode::WithType | EntityMode::WithSource | EntityMode::WithSourceAndCause => { + *ResourceArgument::get_damage_type(context, "damageType")? + } + }; + + let source = match self.0 { + EntityMode::WithSource | EntityMode::WithSourceAndCause => { + Some(EntityArgumentType::get_entity(context, "source")?) + } + EntityMode::Basic | EntityMode::WithType => None, + }; + + let cause = match self.0 { + EntityMode::WithSourceAndCause => { + Some(EntityArgumentType::get_entity(context, "cause")?) + } + EntityMode::Basic | EntityMode::WithType | EntityMode::WithSource => None, }; let success = target.damage_with_context( @@ -116,42 +106,46 @@ impl CommandExecutor for EntityExecutor { cause.as_ref().map(|e| e.as_ref() as &dyn EntityBase), ); - send_damage_result(sender, success, amount, target.get_display_name()) + send_damage_result(context, success, amount, target.get_display_name()) } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).then( - argument(ARG_TARGET, EntityArgumentConsumer).then( - argument(ARG_AMOUNT, amount_consumer()) - // Basic damage - .execute(EntityExecutor(false)) - // With damage type - .then( - argument(ARG_DAMAGE_TYPE, DamageTypeArgumentConsumer) - .execute(EntityExecutor(false)) - // At location - .then( - literal("at").then( - argument(ARG_LOCATION, Position3DArgumentConsumer) - .execute(LocationExecutor), +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + dispatcher.register( + command("damage", DESCRIPTION).requires(PERMISSION).then( + argument("target", EntityArgumentType::Entity).then( + argument("amount", FloatArgumentType::with_min(0.0)) + .executes(DamageEntityExecutor(EntityMode::Basic)) + .then( + argument("damageType", DAMAGE_TYPE_ARGUMENT.clone()) + .executes(DamageEntityExecutor(EntityMode::WithType)) + .then( + literal("at").then( + argument("location", Vec3ArgumentType::Default) + .executes(DamageLocationExecutor), + ), + ) + .then( + literal("by").then( + argument("source", EntityArgumentType::Entity) + .executes(DamageEntityExecutor(EntityMode::WithSource)) + .then(literal("from").then( + argument("cause", EntityArgumentType::Entity).executes( + DamageEntityExecutor( + EntityMode::WithSourceAndCause, + ), + ), + )), + ), ), - ) - // By entity - .then( - literal("by").then( - argument(ARG_ENTITY, EntityArgumentConsumer) - .execute(EntityExecutor(false)) - // From cause - .then( - literal("from").then( - argument(ARG_CAUSE, EntityArgumentConsumer) - .execute(EntityExecutor(true)), - ), - ), - ), - ), - ), + ), + ), ), - ) + ); } diff --git a/crates/pumpkin/src/command/commands/data.rs b/crates/pumpkin/src/command/commands/data.rs index be068237a..fb1be28f9 100644 --- a/crates/pumpkin/src/command/commands/data.rs +++ b/crates/pumpkin/src/command/commands/data.rs @@ -1,88 +1,124 @@ -use crate::command::CommandResult; -use crate::command::args::entity::EntityArgumentConsumer; -use crate::command::tree::builder::literal; -use crate::command::{ - CommandError, CommandExecutor, CommandSender, - args::{Arg, ConsumedArgs}, - tree::{CommandTree, builder::argument}, -}; -use crate::entity::EntityBase; -use CommandError::InvalidConsumption; +#![allow( + clippy::too_many_lines, + clippy::manual_let_else, + clippy::map_unwrap_or, + clippy::option_if_let_else, + clippy::trivially_copy_pass_by_ref, + clippy::cast_possible_wrap, + clippy::cast_sign_loss, + clippy::cast_possible_truncation, + clippy::missing_const_for_fn, + clippy::redundant_closure_for_method_calls +)] + +use std::sync::Arc; + use pumpkin_data::translation; use pumpkin_nbt::compound::NbtCompound; use pumpkin_nbt::tag::NbtTag; +use pumpkin_util::PermissionLvl; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; use pumpkin_util::text::color::NamedColor; -const NAMES: [&str; 1] = ["data"]; -const DESCRIPTION: &str = "Query and modify data of entities and blocks"; +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::coordinates::block_pos::BlockPosArgumentType; +use crate::command::argument_types::core::double::DoubleArgumentType; +use crate::command::argument_types::core::integer::IntegerArgumentType; +use crate::command::argument_types::entity::EntityArgumentType; +use crate::command::argument_types::identifier::IdentifierArgumentType; +use crate::command::argument_types::nbt::{NbtCompoundArgumentType, NbtTagArgumentType}; +use crate::command::argument_types::nbt_path::{ + ERROR_DATA_TOO_DEEP, NbtPath, NbtPathArgumentType, is_too_deep, +}; +use crate::command::context::command_context::CommandContext; +use crate::command::errors::command_syntax_error::CommandSyntaxError; +use crate::command::errors::error_types::CommandErrorType; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; +use crate::entity::EntityBase; +use crate::server::Server; +use crate::world::World; -const ARG_ENTITY: &str = "entity"; +const DESCRIPTION: &str = "Queries or modifies NBT data of entities, blocks, and storages."; +const PERMISSION: &str = "minecraft:command.data"; -struct GetEntityDataExecutor; +pub const ERROR_MERGE_UNCHANGED: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_DATA_MERGE_FAILED, + translation::java::COMMANDS_DATA_MERGE_FAILED, +); -impl CommandExecutor for GetEntityDataExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::Entity(entity)) = args.get(&ARG_ENTITY) else { - return Err(InvalidConsumption(Some(ARG_ENTITY.into()))); - }; - display_data(entity.as_ref(), entity.get_display_name(), sender) - } -} +pub const ERROR_GET_NOT_NUMBER: CommandErrorType<1> = CommandErrorType::new( + translation::java::COMMANDS_DATA_GET_INVALID, + translation::java::COMMANDS_DATA_GET_INVALID, +); -#[expect(clippy::too_many_lines)] -pub fn snbt_colorful_display(tag: &NbtTag, depth: usize) -> Result { +pub const ERROR_GET_NON_EXISTENT: CommandErrorType<1> = CommandErrorType::new( + translation::java::COMMANDS_DATA_GET_UNKNOWN, + translation::java::COMMANDS_DATA_GET_UNKNOWN, +); + +pub const ERROR_MULTIPLE_TAGS: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_DATA_GET_MULTIPLE, + translation::java::COMMANDS_DATA_GET_MULTIPLE, +); + +pub const ERROR_EXPECTED_OBJECT: CommandErrorType<1> = CommandErrorType::new( + translation::java::COMMANDS_DATA_MODIFY_EXPECTED_OBJECT, + translation::java::COMMANDS_DATA_MODIFY_EXPECTED_OBJECT, +); + +pub const ERROR_EXPECTED_VALUE: CommandErrorType<1> = CommandErrorType::new( + translation::java::COMMANDS_DATA_MODIFY_EXPECTED_VALUE, + translation::java::COMMANDS_DATA_MODIFY_EXPECTED_VALUE, +); + +pub const ERROR_INVALID_SUBSTRING: CommandErrorType<2> = CommandErrorType::new( + translation::java::COMMANDS_DATA_MODIFY_INVALID_SUBSTRING, + translation::java::COMMANDS_DATA_MODIFY_INVALID_SUBSTRING, +); + +pub const ERROR_BLOCK_INVALID: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_DATA_BLOCK_INVALID, + translation::java::COMMANDS_DATA_BLOCK_INVALID, +); + +pub const ERROR_ENTITY_INVALID: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_DATA_ENTITY_INVALID, + translation::java::COMMANDS_DATA_ENTITY_INVALID, +); + +#[must_use] +pub fn snbt_colorful_display(tag: &NbtTag, _depth: usize) -> TextComponent { let folded = TextComponent::text("<...>").color_named(NamedColor::Gray); match tag { - NbtTag::End => Err("Unexpected end tag".into()), - NbtTag::Byte(value) => { - let byte_format = TextComponent::text("b").color_named(NamedColor::Red); - Ok(TextComponent::text(format!("{value}")) - .color_named(NamedColor::Gold) - .add_child(byte_format)) - } - NbtTag::Short(value) => { - let short_format = TextComponent::text("s").color_named(NamedColor::Red); - Ok(TextComponent::text(format!("{value}")) - .color_named(NamedColor::Gold) - .add_child(short_format)) - } - NbtTag::Int(value) => { - Ok(TextComponent::text(format!("{value}")).color_named(NamedColor::Gold)) - } - NbtTag::Long(value) => { - let long_format = TextComponent::text("L").color_named(NamedColor::Red); - Ok(TextComponent::text(format!("{value}")) - .color_named(NamedColor::Gold) - .add_child(long_format)) - } - NbtTag::Float(value) => { - let float_format = TextComponent::text("f").color_named(NamedColor::Red); - Ok(TextComponent::text(format!("{value}")) - .color_named(NamedColor::Gold) - .add_child(float_format)) - } - NbtTag::Double(value) => { - let double_format = TextComponent::text("d").color_named(NamedColor::Red); - Ok(TextComponent::text(format!("{value}")) - .color_named(NamedColor::Gold) - .add_child(double_format)) - } + NbtTag::End => TextComponent::text(""), + NbtTag::Byte(value) => TextComponent::text(format!("{value}")) + .color_named(NamedColor::Gold) + .add_child(TextComponent::text("b").color_named(NamedColor::Red)), + NbtTag::Short(value) => TextComponent::text(format!("{value}")) + .color_named(NamedColor::Gold) + .add_child(TextComponent::text("s").color_named(NamedColor::Red)), + NbtTag::Int(value) => TextComponent::text(format!("{value}")).color_named(NamedColor::Gold), + NbtTag::Long(value) => TextComponent::text(format!("{value}")) + .color_named(NamedColor::Gold) + .add_child(TextComponent::text("L").color_named(NamedColor::Red)), + NbtTag::Float(value) => TextComponent::text(format!("{value}")) + .color_named(NamedColor::Gold) + .add_child(TextComponent::text("f").color_named(NamedColor::Red)), + NbtTag::Double(value) => TextComponent::text(format!("{value}")) + .color_named(NamedColor::Gold) + .add_child(TextComponent::text("d").color_named(NamedColor::Red)), NbtTag::ByteArray(value) => { - let byte_array_format = TextComponent::text("B").color_named(NamedColor::Red); let mut content = TextComponent::text("[") - .add_child(byte_array_format.clone()) + .add_child(TextComponent::text("B").color_named(NamedColor::Red)) .add_child(TextComponent::text("; ")); for (index, byte) in value.iter().take(128).enumerate() { content = content .add_child(TextComponent::text(format!("{byte}"))) - .add_child(byte_array_format.clone()); + .add_child(TextComponent::text("b").color_named(NamedColor::Red)); if index < value.len() - 1 { content = content.add_child(TextComponent::text(", ")); } @@ -92,88 +128,49 @@ pub fn snbt_colorful_display(tag: &NbtTag, depth: usize) -> Result { - let escaped_value = value - .replace('"', "\\\"") - .replace('\\', "\\\\") - .replace('\n', "\\n") - .replace('\t', "\\t") - .replace('\r', "\\r") - .replace('\x0c', "\\f") - .replace('\x08', "\\b"); - - Ok(TextComponent::text(format!("\"{escaped_value}\"")).color_named(NamedColor::Green)) + let escaped = value.replace('"', "\\\""); + TextComponent::text(format!("\"{escaped}\"")).color_named(NamedColor::Green) } NbtTag::List(value) => { - if value.is_empty() { - Ok(TextComponent::text("[]")) - } else if depth >= 64 { - Ok(TextComponent::text("[") - .add_child(folded) - .add_child(TextComponent::text("]"))) - } else { - let mut content = TextComponent::text("["); - - for (index, item) in value.iter().take(128).enumerate() { - let item_display = snbt_colorful_display(item, depth + 1) - .map_err(|string| format!("Error displaying item.[{index}]: {string}"))?; - content = content.add_child(item_display); - - if index < value.len() - 1 { - content = content.add_child(TextComponent::text(", ")); - } + let mut content = TextComponent::text("["); + for (index, tag) in value.iter().take(128).enumerate() { + content = content.add_child(snbt_colorful_display(tag, 0)); + if index < value.len() - 1 { + content = content.add_child(TextComponent::text(", ")); } - - if value.len() > 128 { - content = content.add_child(folded); - } - - content = content.add_child(TextComponent::text("]")); - Ok(content) } + if value.len() > 128 { + content = content.add_child(folded); + } + content.add_child(TextComponent::text("]")) } NbtTag::Compound(value) => { - if value.is_empty() { - Ok(TextComponent::text("{}")) - } else if depth >= 64 { - Ok(TextComponent::text("{") - .add_child(folded) - .add_child(TextComponent::text("}"))) - } else { - let mut content = TextComponent::text("{"); - - for (index, (key, item)) in value.child_tags.iter().take(128).enumerate() { - let item_display = snbt_colorful_display(item, depth + 1) - .map_err(|string| format!("Error displaying item.{key}: {string}"))?; - content = content - .add_child( - TextComponent::text(key.to_string()).color_named(NamedColor::Aqua), - ) - .add_child(TextComponent::text(": ")) - .add_child(item_display); - - if index < value.child_tags.len() - 1 { - content = content.add_child(TextComponent::text(", ")); - } + let mut content = TextComponent::text("{"); + let mut keys: Vec<&Box> = value.child_tags.keys().collect(); + keys.sort(); + for (index, key) in keys.into_iter().take(128).enumerate() { + let tag = &value.child_tags[key]; + content = content + .add_child( + TextComponent::text(format!("{key}: ")).color_named(NamedColor::Aqua), + ) + .add_child(snbt_colorful_display(tag, 0)); + if index < value.child_tags.len() - 1 { + content = content.add_child(TextComponent::text(", ")); } - - if value.child_tags.len() > 128 { - content = content.add_child(folded); - } - - content = content.add_child(TextComponent::text("}")); - Ok(content) } + if value.child_tags.len() > 128 { + content = content.add_child(folded); + } + content.add_child(TextComponent::text("}")) } NbtTag::IntArray(value) => { - let int_array_format = TextComponent::text("I").color_named(NamedColor::Red); let mut content = TextComponent::text("[") - .add_child(int_array_format) + .add_child(TextComponent::text("I").color_named(NamedColor::Red)) .add_child(TextComponent::text("; ")); - for (index, int) in value.iter().take(128).enumerate() { content = content .add_child(TextComponent::text(format!("{int}")).color_named(NamedColor::Gold)); @@ -181,96 +178,830 @@ pub fn snbt_colorful_display(tag: &NbtTag, depth: usize) -> Result 128 { content = content.add_child(folded); } - - content = content.add_child(TextComponent::text("]")); - Ok(content) + content.add_child(TextComponent::text("]")) } NbtTag::LongArray(value) => { - let long_array_format = TextComponent::text("L").color_named(NamedColor::Red); let mut content = TextComponent::text("[") - .add_child(long_array_format.clone()) + .add_child(TextComponent::text("L").color_named(NamedColor::Red)) .add_child(TextComponent::text("; ")); - for (index, long) in value.iter().take(128).enumerate() { content = content - .add_child(TextComponent::text(format!("{long}"))) - .add_child(long_array_format.clone()); + .add_child(TextComponent::text(format!("{long}")).color_named(NamedColor::Gold)) + .add_child(TextComponent::text("L").color_named(NamedColor::Red)); if index < value.len() - 1 { content = content.add_child(TextComponent::text(", ")); } } - if value.len() > 128 { content = content.add_child(folded); } - - content = content.add_child(TextComponent::text("]")); - Ok(content) + content.add_child(TextComponent::text("]")) } } } -fn display_data( - entity: &dyn EntityBase, - target_name: TextComponent, - sender: &CommandSender, -) -> Result { - let mut nbt = NbtCompound::new(); - entity.write_nbt(&mut nbt); - let tag = NbtTag::Compound(nbt); +// ---------------- DATA ACCESSOR ABSTRACTION ---------------- - let result = get_i32_result(&tag)?; - let display = snbt_colorful_display(&tag, 0) - .map_err(|string| CommandError::CommandFailed(TextComponent::text(string)))?; - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_DATA_ENTITY_QUERY, - translation::java::COMMANDS_DATA_ENTITY_QUERY, - [target_name, display], - )); - - Ok(result) +pub trait DataAccessor: Send + Sync { + fn set_data(&self, data: &NbtCompound) -> Result<(), CommandSyntaxError>; + fn get_data(&self) -> Result; + fn get_modified_success(&self) -> TextComponent; + fn get_print_success(&self, data: &NbtTag) -> TextComponent; + fn get_print_scaled_success(&self, path: &NbtPath, scale: f64, value: i32) -> TextComponent; } -fn get_i32_result(tag: &NbtTag) -> Result { - match tag { - NbtTag::End => Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_DATA_GET_UNKNOWN, - translation::java::COMMANDS_DATA_GET_UNKNOWN, - [], - ))), +pub struct EntityDataAccessor { + entity: Arc, +} - NbtTag::Byte(b) => Ok(*b as i32), - NbtTag::Short(s) => Ok(*s as i32), - NbtTag::Int(i) => Ok(*i), - NbtTag::Long(l) => Ok((*l).clamp(i32::MIN as i64, i32::MAX as i64) as i32), - NbtTag::Float(f) => Ok({ - let i = *f as i32; - if *f < i as f32 { i - 1 } else { i } - }), - NbtTag::Double(d) => Ok({ - let i = *d as i32; - if *d < i as f64 { i - 1 } else { i } - }), - - NbtTag::ByteArray(items) => Ok(items.len() as i32), - NbtTag::IntArray(items) => Ok(items.len() as i32), - NbtTag::LongArray(items) => Ok(items.len() as i32), - - NbtTag::String(string) => Ok(string.len() as i32), - NbtTag::List(nbt_tags) => Ok(nbt_tags.len() as i32), - NbtTag::Compound(nbt_compound) => Ok(nbt_compound.child_tags.len() as i32), +impl EntityDataAccessor { + #[must_use] + pub fn new(entity: Arc) -> Self { + Self { entity } } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).then( - literal("get").then( - literal("entity") - .then(argument(ARG_ENTITY, EntityArgumentConsumer).execute(GetEntityDataExecutor)), - ), - ) +impl DataAccessor for EntityDataAccessor { + fn set_data(&self, tag: &NbtCompound) -> Result<(), CommandSyntaxError> { + if self.entity.get_player().is_some() { + return Err(ERROR_ENTITY_INVALID.create_without_context()); + } + self.entity.read_nbt_non_mut(tag); + Ok(()) + } + + fn get_data(&self) -> Result { + let mut nbt = NbtCompound::new(); + self.entity.write_nbt(&mut nbt); + Ok(nbt) + } + + fn get_modified_success(&self) -> TextComponent { + TextComponent::translate_cross( + translation::java::COMMANDS_DATA_ENTITY_MODIFIED, + translation::java::COMMANDS_DATA_ENTITY_MODIFIED, + [self.entity.get_display_name()], + ) + } + + fn get_print_success(&self, data: &NbtTag) -> TextComponent { + TextComponent::translate_cross( + translation::java::COMMANDS_DATA_ENTITY_QUERY, + translation::java::COMMANDS_DATA_ENTITY_QUERY, + [ + self.entity.get_display_name(), + snbt_colorful_display(data, 0), + ], + ) + } + + fn get_print_scaled_success(&self, path: &NbtPath, scale: f64, value: i32) -> TextComponent { + TextComponent::translate_cross( + translation::java::COMMANDS_DATA_ENTITY_GET, + translation::java::COMMANDS_DATA_ENTITY_GET, + [ + TextComponent::text(path.as_str().to_string()), + self.entity.get_display_name(), + TextComponent::text(format!("{scale:.2}")), + TextComponent::text(value.to_string()), + ], + ) + } +} + +pub struct BlockDataAccessor { + pos: BlockPos, + world: Arc, +} + +impl BlockDataAccessor { + pub fn new(pos: BlockPos, world: Arc) -> Result { + if world.get_block_entity(&pos).is_none() { + return Err(ERROR_BLOCK_INVALID.create_without_context()); + } + Ok(Self { pos, world }) + } +} + +impl DataAccessor for BlockDataAccessor { + fn set_data(&self, tag: &NbtCompound) -> Result<(), CommandSyntaxError> { + if self.world.get_block_entity(&self.pos).is_some() { + self.world.add_block_entity_nbt(self.pos, tag); + Ok(()) + } else { + Err(ERROR_BLOCK_INVALID.create_without_context()) + } + } + + fn get_data(&self) -> Result { + if let Some(block_entity) = self.world.get_block_entity(&self.pos) { + let mut nbt = NbtCompound::new(); + block_entity.write_internal(&mut nbt); + Ok(nbt) + } else { + Err(ERROR_BLOCK_INVALID.create_without_context()) + } + } + + fn get_modified_success(&self) -> TextComponent { + TextComponent::translate_cross( + translation::java::COMMANDS_DATA_BLOCK_MODIFIED, + translation::java::COMMANDS_DATA_BLOCK_MODIFIED, + [TextComponent::text(format!( + "{}, {}, {}", + self.pos.0.x, self.pos.0.y, self.pos.0.z + ))], + ) + } + + fn get_print_success(&self, data: &NbtTag) -> TextComponent { + TextComponent::translate_cross( + translation::java::COMMANDS_DATA_BLOCK_QUERY, + translation::java::COMMANDS_DATA_BLOCK_QUERY, + [ + TextComponent::text(format!( + "{}, {}, {}", + self.pos.0.x, self.pos.0.y, self.pos.0.z + )), + snbt_colorful_display(data, 0), + ], + ) + } + + fn get_print_scaled_success(&self, path: &NbtPath, scale: f64, value: i32) -> TextComponent { + TextComponent::translate_cross( + translation::java::COMMANDS_DATA_BLOCK_GET, + translation::java::COMMANDS_DATA_BLOCK_GET, + [ + TextComponent::text(path.as_str().to_string()), + TextComponent::text(format!("{}", self.pos.0.x)), + TextComponent::text(format!("{}", self.pos.0.y)), + TextComponent::text(format!("{}", self.pos.0.z)), + TextComponent::text(format!("{scale:.2}")), + TextComponent::text(value.to_string()), + ], + ) + } +} + +pub struct StorageDataAccessor { + id: String, + server: Arc, +} + +impl StorageDataAccessor { + #[must_use] + pub fn new(id: String, server: Arc) -> Self { + Self { id, server } + } +} + +impl DataAccessor for StorageDataAccessor { + fn set_data(&self, tag: &NbtCompound) -> Result<(), CommandSyntaxError> { + let mut storage = self + .server + .command_storage + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + storage.insert(self.id.clone(), tag.clone()); + Ok(()) + } + + fn get_data(&self) -> Result { + let storage = self + .server + .command_storage + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + Ok(storage.get(&self.id).cloned().unwrap_or_default()) + } + + fn get_modified_success(&self) -> TextComponent { + TextComponent::translate_cross( + translation::java::COMMANDS_DATA_STORAGE_MODIFIED, + translation::java::COMMANDS_DATA_STORAGE_MODIFIED, + [TextComponent::text(self.id.clone())], + ) + } + + fn get_print_success(&self, data: &NbtTag) -> TextComponent { + TextComponent::translate_cross( + translation::java::COMMANDS_DATA_STORAGE_QUERY, + translation::java::COMMANDS_DATA_STORAGE_QUERY, + [ + TextComponent::text(self.id.clone()), + snbt_colorful_display(data, 0), + ], + ) + } + + fn get_print_scaled_success(&self, path: &NbtPath, scale: f64, value: i32) -> TextComponent { + TextComponent::translate_cross( + translation::java::COMMANDS_DATA_STORAGE_GET, + translation::java::COMMANDS_DATA_STORAGE_GET, + [ + TextComponent::text(path.as_str().to_string()), + TextComponent::text(self.id.clone()), + TextComponent::text(format!("{scale:.2}")), + TextComponent::text(value.to_string()), + ], + ) + } +} + +pub fn get_single_tag( + path: &NbtPath, + accessor: &dyn DataAccessor, +) -> Result { + let data = accessor.get_data()?; + let root = NbtTag::Compound(data); + let tags = path.get(&root)?; + match tags.len() { + 0 => Err(ERROR_GET_NON_EXISTENT + .create_without_context(TextComponent::text(path.as_str().to_string()))), + 1 => Ok(tags.into_iter().next().unwrap()), + _ => Err(ERROR_MULTIPLE_TAGS.create_without_context()), + } +} + +fn get_as_text(tag: &NbtTag) -> Result { + match tag { + NbtTag::String(s) => Ok(s.to_string()), + NbtTag::Byte(b) => Ok(b.to_string()), + NbtTag::Short(s) => Ok(s.to_string()), + NbtTag::Int(i) => Ok(i.to_string()), + NbtTag::Long(l) => Ok(l.to_string()), + NbtTag::Float(f) => Ok(f.to_string()), + NbtTag::Double(d) => Ok(d.to_string()), + _ => { + Err(ERROR_EXPECTED_VALUE + .create_without_context(TextComponent::text(format!("{tag:?}")))) + } + } +} + +fn substring(input: &str, start: i32, end: Option) -> Result { + let len = input.chars().count() as i32; + let abs_start = if start >= 0 { start } else { len + start }; + let abs_end = match end { + Some(e) => { + if e >= 0 { + e + } else { + len + e + } + } + None => len, + }; + if abs_start >= 0 && abs_end <= len && abs_start <= abs_end { + let chars: Vec = input.chars().collect(); + Ok(chars[(abs_start as usize)..(abs_end as usize)] + .iter() + .collect()) + } else { + Err(ERROR_INVALID_SUBSTRING.create_without_context( + TextComponent::text(start.to_string()), + TextComponent::text(end.unwrap_or(len).to_string()), + )) + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum TargetKind { + Entity, + Block, + Storage, +} + +impl TargetKind { + pub fn access( + &self, + context: &CommandContext, + prefix: &str, + ) -> Result, CommandSyntaxError> { + match self { + Self::Entity => { + let entity = EntityArgumentType::get_entity(context, &format!("{prefix}_target"))?; + Ok(Box::new(EntityDataAccessor::new(entity))) + } + Self::Block => { + let pos = + BlockPosArgumentType::get_block_pos(context, &format!("{prefix}_target"))?; + let world = context.world().clone(); + let accessor = BlockDataAccessor::new(pos, world)?; + Ok(Box::new(accessor)) + } + Self::Storage => { + let id = context.get_argument::(&format!( + "{prefix}_target" + ))?; + let server = context.server().clone(); + Ok(Box::new(StorageDataAccessor::new(id.to_string(), server))) + } + } + } +} + +// ---------------- EXECUTION LOGIC ---------------- + +fn execute_get( + context: &CommandContext, + target_kind: TargetKind, + has_path: bool, + has_scale: bool, +) -> CommandExecutorResult { + let accessor = target_kind.access(context, "target")?; + if !has_path { + let data = accessor.get_data()?; + let result = data.child_tags.len() as i32; + context + .source + .send_feedback(accessor.get_print_success(&NbtTag::Compound(data)), false); + return Ok(result); + } + + let path = context.get_argument::("path")?; + if has_scale { + let scale = DoubleArgumentType::get(context, "scale")?; + let tag = get_single_tag(path, accessor.as_ref())?; + let num = match &tag { + NbtTag::Byte(b) => *b as f64, + NbtTag::Short(s) => *s as f64, + NbtTag::Int(i) => *i as f64, + NbtTag::Long(l) => *l as f64, + NbtTag::Float(f) => *f as f64, + NbtTag::Double(d) => *d, + _ => { + return Err(ERROR_GET_NOT_NUMBER + .create_without_context(TextComponent::text(path.as_str().to_string()))); + } + }; + let result = (num * scale).floor() as i32; + context.source.send_feedback( + accessor.get_print_scaled_success(path, scale, result), + false, + ); + return Ok(result); + } + + let tag = get_single_tag(path, accessor.as_ref())?; + let result = match &tag { + NbtTag::Byte(b) => *b as i32, + NbtTag::Short(s) => *s as i32, + NbtTag::Int(i) => *i, + NbtTag::Long(l) => *l as i32, + NbtTag::Float(f) => f.floor() as i32, + NbtTag::Double(d) => d.floor() as i32, + NbtTag::ByteArray(arr) => arr.len() as i32, + NbtTag::IntArray(arr) => arr.len() as i32, + NbtTag::LongArray(arr) => arr.len() as i32, + NbtTag::String(s) => s.len() as i32, + NbtTag::List(list) => list.len() as i32, + NbtTag::Compound(compound) => compound.child_tags.len() as i32, + NbtTag::End => { + return Err(ERROR_GET_NON_EXISTENT + .create_without_context(TextComponent::text(path.as_str().to_string()))); + } + }; + context + .source + .send_feedback(accessor.get_print_success(&tag), false); + Ok(result) +} + +fn execute_merge(context: &CommandContext, target_kind: TargetKind) -> CommandExecutorResult { + let accessor = target_kind.access(context, "target")?; + let nbt = NbtCompoundArgumentType::get(context, "nbt")?.clone(); + if is_too_deep(&NbtTag::Compound(nbt.clone()), 0) { + return Err(ERROR_DATA_TOO_DEEP.create_without_context()); + } + let old = accessor.get_data()?; + let mut updated = old.clone(); + for (k, v) in nbt.child_tags { + updated.child_tags.insert(k, v); + } + if old == updated { + return Err(ERROR_MERGE_UNCHANGED.create_without_context()); + } + accessor.set_data(&updated)?; + context + .source + .send_feedback(accessor.get_modified_success(), true); + Ok(1) +} + +fn execute_remove(context: &CommandContext, target_kind: TargetKind) -> CommandExecutorResult { + let accessor = target_kind.access(context, "target")?; + let path = context.get_argument::("path")?; + let mut data = accessor.get_data()?; + let mut root = NbtTag::Compound(data.clone()); + let count = path.remove(&mut root); + if count == 0 { + return Err(ERROR_MERGE_UNCHANGED.create_without_context()); + } + if let NbtTag::Compound(new_compound) = root { + data = new_compound; + } + accessor.set_data(&data)?; + context + .source + .send_feedback(accessor.get_modified_success(), true); + Ok(count) +} + +#[derive(Clone, Copy)] +pub enum ModifyMode { + Insert(i32), + Prepend, + Append, + Set, + Merge, +} + +#[derive(Clone, Copy)] +pub enum SourceMode { + From { + has_path: bool, + }, + String { + has_path: bool, + has_start: bool, + has_end: bool, + }, + Value, +} + +fn resolve_source_tags( + context: &CommandContext, + source_kind: Option, + source_mode: SourceMode, +) -> Result, CommandSyntaxError> { + match source_mode { + SourceMode::Value => { + let val = NbtTagArgumentType::get(context, "value")?.clone(); + Ok(vec![val]) + } + SourceMode::From { has_path } => { + let source_accessor = source_kind.unwrap().access(context, "source")?; + let data = source_accessor.get_data()?; + if has_path { + let source_path = context.get_argument::("sourcePath")?; + source_path.get(&NbtTag::Compound(data)) + } else { + Ok(vec![NbtTag::Compound(data)]) + } + } + SourceMode::String { + has_path, + has_start, + has_end, + } => { + let source_accessor = source_kind.unwrap().access(context, "source")?; + let data = source_accessor.get_data()?; + let tags = if has_path { + let source_path = context.get_argument::("sourcePath")?; + source_path.get(&NbtTag::Compound(data))? + } else { + vec![NbtTag::Compound(data)] + }; + + let start = if has_start { + IntegerArgumentType::get(context, "start")? + } else { + 0 + }; + let end = if has_end { + Some(IntegerArgumentType::get(context, "end")?) + } else { + None + }; + + let mut string_tags = Vec::with_capacity(tags.len()); + for tag in tags { + let text = get_as_text(&tag)?; + let sub = substring(&text, start, end)?; + string_tags.push(NbtTag::String(sub.into_boxed_str())); + } + Ok(string_tags) + } + } +} + +fn execute_modify( + context: &CommandContext, + target_kind: TargetKind, + modify_mode: ModifyMode, + source_kind: Option, + source_mode: SourceMode, +) -> CommandExecutorResult { + let target_accessor = target_kind.access(context, "target")?; + let target_path = context.get_argument::("targetPath")?; + let mut target_data = target_accessor.get_data()?; + let mut root = NbtTag::Compound(target_data.clone()); + let source_tags = resolve_source_tags(context, source_kind, source_mode)?; + + let result = match modify_mode { + ModifyMode::Insert(idx) => target_path.insert(idx, &mut root, &source_tags)?, + ModifyMode::Prepend => target_path.insert(0, &mut root, &source_tags)?, + ModifyMode::Append => target_path.insert(-1, &mut root, &source_tags)?, + ModifyMode::Set => { + let last_tag = source_tags.last().cloned().unwrap_or(NbtTag::End); + target_path.set(&mut root, last_tag)? + } + ModifyMode::Merge => { + let mut combined = NbtCompound::new(); + for tag in &source_tags { + if is_too_deep(tag, 0) { + return Err(ERROR_DATA_TOO_DEEP.create_without_context()); + } + if let NbtTag::Compound(c) = tag { + for (k, v) in &c.child_tags { + combined.child_tags.insert(k.clone(), v.clone()); + } + } else { + return Err(ERROR_EXPECTED_OBJECT + .create_without_context(TextComponent::text(format!("{tag:?}")))); + } + } + + target_path.set(&mut root, NbtTag::Compound(combined))? + } + }; + + if result == 0 { + return Err(ERROR_MERGE_UNCHANGED.create_without_context()); + } + + if let NbtTag::Compound(new_compound) = root { + target_data = new_compound; + } + target_accessor.set_data(&target_data)?; + context + .source + .send_feedback(target_accessor.get_modified_success(), true); + Ok(result) +} + +// ---------------- EXECUTOR STRUCTS ---------------- + +struct GetExecutor { + target_kind: TargetKind, + has_path: bool, + has_scale: bool, +} + +impl CommandExecutor for GetExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + execute_get(context, self.target_kind, self.has_path, self.has_scale) + } +} + +struct MergeExecutor { + target_kind: TargetKind, +} + +impl CommandExecutor for MergeExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + execute_merge(context, self.target_kind) + } +} + +struct RemoveExecutor { + target_kind: TargetKind, +} + +impl CommandExecutor for RemoveExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + execute_remove(context, self.target_kind) + } +} + +struct ModifyExecutor { + target_kind: TargetKind, + modify_mode: ModifyMode, + source_kind: Option, + source_mode: SourceMode, +} + +impl CommandExecutor for ModifyExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let modify_mode = match self.modify_mode { + ModifyMode::Insert(_) => { + let idx = IntegerArgumentType::get(context, "index")?; + ModifyMode::Insert(idx) + } + other => other, + }; + execute_modify( + context, + self.target_kind, + modify_mode, + self.source_kind, + self.source_mode, + ) + } +} + +// ---------------- REGISTRATION ---------------- + +fn make_target_arg( + kind: TargetKind, + name: &'static str, +) -> crate::command::argument_builder::RequiredArgumentBuilder { + match kind { + TargetKind::Entity => argument(name, EntityArgumentType::Entity), + TargetKind::Block => argument(name, BlockPosArgumentType), + TargetKind::Storage => argument(name, IdentifierArgumentType), + } +} + +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + let all_target_kinds = [ + (TargetKind::Entity, "entity"), + (TargetKind::Block, "block"), + (TargetKind::Storage, "storage"), + ]; + + let mut data_cmd = command("data", DESCRIPTION).requires(PERMISSION); + + // Merge & Get & Remove + for &(target_kind, name) in &all_target_kinds { + // data merge + data_cmd = data_cmd.then(literal("merge").then(literal(name).then( + make_target_arg(target_kind, "target_target").then( + argument("nbt", NbtCompoundArgumentType).executes(MergeExecutor { target_kind }), + ), + ))); + + // data get [] [] + data_cmd = data_cmd.then( + literal("get").then( + literal(name).then( + make_target_arg(target_kind, "target_target") + .executes(GetExecutor { + target_kind, + has_path: false, + has_scale: false, + }) + .then( + argument("path", NbtPathArgumentType) + .executes(GetExecutor { + target_kind, + has_path: true, + has_scale: false, + }) + .then(argument("scale", DoubleArgumentType::any()).executes( + GetExecutor { + target_kind, + has_path: true, + has_scale: true, + }, + )), + ), + ), + ), + ); + + // data remove + data_cmd = data_cmd.then(literal("remove").then(literal(name).then( + make_target_arg(target_kind, "target_target").then( + argument("path", NbtPathArgumentType).executes(RemoveExecutor { target_kind }), + ), + ))); + } + + // data modify (insert | prepend | append | set | merge) ... + let modify_modes = [ + ("insert", ModifyMode::Insert(0)), + ("prepend", ModifyMode::Prepend), + ("append", ModifyMode::Append), + ("set", ModifyMode::Set), + ("merge", ModifyMode::Merge), + ]; + + for &(target_kind, target_name) in &all_target_kinds { + let mut target_path_arg = argument("targetPath", NbtPathArgumentType); + + for &(mod_name, mod_mode) in &modify_modes { + let mut mod_node = if mod_name == "insert" { + literal("insert").then(argument("index", IntegerArgumentType::any())) + } else { + literal(mod_name) + }; + + // value + mod_node = mod_node.then(literal("value").then( + argument("value", NbtTagArgumentType).executes(ModifyExecutor { + target_kind, + modify_mode: mod_mode, + source_kind: None, + source_mode: SourceMode::Value, + }), + )); + + // from & string sources + for &(source_kind, source_name) in &all_target_kinds { + // from [] + mod_node = mod_node.then( + literal("from").then( + literal(source_name).then( + make_target_arg(source_kind, "source_target") + .executes(ModifyExecutor { + target_kind, + modify_mode: mod_mode, + source_kind: Some(source_kind), + source_mode: SourceMode::From { has_path: false }, + }) + .then(argument("sourcePath", NbtPathArgumentType).executes( + ModifyExecutor { + target_kind, + modify_mode: mod_mode, + source_kind: Some(source_kind), + source_mode: SourceMode::From { has_path: true }, + }, + )), + ), + ), + ); + + // string [] [] [] + mod_node = mod_node.then( + literal("string").then( + literal(source_name).then( + make_target_arg(source_kind, "source_target") + .executes(ModifyExecutor { + target_kind, + modify_mode: mod_mode, + source_kind: Some(source_kind), + source_mode: SourceMode::String { + has_path: false, + has_start: false, + has_end: false, + }, + }) + .then( + argument("sourcePath", NbtPathArgumentType) + .executes(ModifyExecutor { + target_kind, + modify_mode: mod_mode, + source_kind: Some(source_kind), + source_mode: SourceMode::String { + has_path: true, + has_start: false, + has_end: false, + }, + }) + .then( + argument("start", IntegerArgumentType::any()) + .executes(ModifyExecutor { + target_kind, + modify_mode: mod_mode, + source_kind: Some(source_kind), + source_mode: SourceMode::String { + has_path: true, + has_start: true, + has_end: false, + }, + }) + .then( + argument("end", IntegerArgumentType::any()) + .executes(ModifyExecutor { + target_kind, + modify_mode: mod_mode, + source_kind: Some(source_kind), + source_mode: SourceMode::String { + has_path: true, + has_start: true, + has_end: true, + }, + }), + ), + ), + ), + ), + ), + ); + } + + target_path_arg = target_path_arg.then(mod_node); + } + + data_cmd = data_cmd.then( + literal("modify").then( + literal(target_name) + .then(make_target_arg(target_kind, "target_target").then(target_path_arg)), + ), + ); + } + + dispatcher.register(data_cmd); } diff --git a/crates/pumpkin/src/command/commands/defaultgamemode.rs b/crates/pumpkin/src/command/commands/defaultgamemode.rs index 0071a5f8b..dc1d18eb0 100644 --- a/crates/pumpkin/src/command/commands/defaultgamemode.rs +++ b/crates/pumpkin/src/command/commands/defaultgamemode.rs @@ -1,17 +1,15 @@ -use crate::command::CommandResult; -use crate::command::args::gamemode::GamemodeArgumentConsumer; -use crate::command::args::{Arg, GetCloned}; -use crate::command::dispatcher::CommandError::InvalidConsumption; -use crate::command::tree::builder::argument; -use crate::command::{CommandExecutor, CommandSender, args::ConsumedArgs, tree::CommandTree}; -use pumpkin_util::GameMode; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; +use pumpkin_util::{GameMode, PermissionLvl}; -const NAMES: [&str; 1] = ["defaultgamemode"]; +use crate::command::argument_builder::{ArgumentBuilder, argument, command}; +use crate::command::argument_types::gamemode::GameModeArgumentType; +use crate::command::context::command_context::CommandContext; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; -const DESCRIPTION: &str = "Change the default gamemode"; - -pub const ARG_GAMEMODE: &str = "gamemode"; +const DESCRIPTION: &str = "Change the default gamemode."; +const PERMISSION: &str = "minecraft:command.defaultgamemode"; pub struct DefaultGamemode { pub gamemode: GameMode, @@ -20,15 +18,9 @@ pub struct DefaultGamemode { struct DefaultGamemodeExecutor; impl CommandExecutor for DefaultGamemodeExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::GameMode(gamemode)) = args.get_cloned(&ARG_GAMEMODE) else { - return Err(InvalidConsumption(Some(ARG_GAMEMODE.into()))); - }; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let gamemode = GameModeArgumentType::get(context, "gamemode")?; + let server = context.source.server(); let mut successful_changes: i32 = 0; if server.basic_config.force_gamemode { @@ -43,25 +35,35 @@ impl CommandExecutor for DefaultGamemodeExecutor { let gamemode_string = format!("{gamemode:?}").to_lowercase(); let gamemode_string = format!("gameMode.{gamemode_string}"); - sender.send_message(TextComponent::translate_cross( - pumpkin_data::translation::java::COMMANDS_DEFAULTGAMEMODE_SUCCESS, - pumpkin_data::translation::bedrock::COMMANDS_DEFAULTGAMEMODE_SUCCESS, - [TextComponent::translate_cross( - gamemode_string.clone(), - gamemode_string, - [], - )], - )); + context.source.send_feedback( + TextComponent::translate_cross( + pumpkin_data::translation::java::COMMANDS_DEFAULTGAMEMODE_SUCCESS, + pumpkin_data::translation::bedrock::COMMANDS_DEFAULTGAMEMODE_SUCCESS, + [TextComponent::translate_cross( + gamemode_string.clone(), + gamemode_string, + [], + )], + ), + true, + ); - //Change the default gamemode (not in configuration.toml) server.defaultgamemode.lock().unwrap().gamemode = gamemode; Ok(successful_changes) } } -#[must_use] -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION) - .then(argument(ARG_GAMEMODE, GamemodeArgumentConsumer).execute(DefaultGamemodeExecutor)) +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + dispatcher.register( + command("defaultgamemode", DESCRIPTION) + .requires(PERMISSION) + .then(argument("gamemode", GameModeArgumentType).executes(DefaultGamemodeExecutor)), + ); } diff --git a/crates/pumpkin/src/command/commands/deop.rs b/crates/pumpkin/src/command/commands/deop.rs index fca9d5807..1b67b07f4 100644 --- a/crates/pumpkin/src/command/commands/deop.rs +++ b/crates/pumpkin/src/command/commands/deop.rs @@ -1,40 +1,33 @@ -use crate::command::CommandResult; -use crate::{ - command::{ - CommandError, CommandExecutor, CommandSender, - args::{ - Arg, ConsumedArgs, - gameprofile::{GameProfileSuggestionMode, GameProfilesArgumentConsumer}, - }, - tree::CommandTree, - tree::builder::argument, - }, - data::SaveJSONConfiguration, -}; -use CommandError::InvalidConsumption; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; -const NAMES: [&str; 1] = ["deop"]; +use crate::command::argument_builder::{ArgumentBuilder, argument, command}; +use crate::command::argument_types::game_profile::GameProfileArgumentType; +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 crate::data::SaveJSONConfiguration; + const DESCRIPTION: &str = "Revokes operator status from a player."; -const ARG_TARGETS: &str = "targets"; +const PERMISSION: &str = "minecraft:command.deop"; -struct Executor; +const ERROR_DEOP_FAILED: CommandErrorType<0> = CommandErrorType::new( + pumpkin_data::translation::java::COMMANDS_DEOP_FAILED, + pumpkin_data::translation::bedrock::COMMANDS_DEOP_FAILED, +); -impl CommandExecutor for Executor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { +struct DeopExecutor; + +impl CommandExecutor for DeopExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let targets = GameProfileArgumentType::get(context, "targets")?; + let server = context.source.server(); let mut config = server.data.operator_config.write().unwrap(); - let Some(Arg::GameProfiles(targets)) = args.get(&ARG_TARGETS) else { - return Err(InvalidConsumption(Some(ARG_TARGETS.into()))); - }; - let mut succeeded_deops: i32 = 0; - for profile in targets { + for profile in &targets { if let Some(op_index) = config.ops.iter().position(|o| o.uuid == profile.id) { config.ops.remove(op_index); succeeded_deops += 1; @@ -45,7 +38,7 @@ impl CommandExecutor for Executor { let command_dispatcher = server_arc.command_dispatcher.load(); player.set_permission_lvl( &server_arc, - pumpkin_util::PermissionLvl::Zero, + PermissionLvl::Zero, &command_dispatcher, ); } @@ -55,7 +48,7 @@ impl CommandExecutor for Executor { pumpkin_data::translation::bedrock::COMMANDS_DEOP_SUCCESS, [TextComponent::text(profile.name.clone())], ); - sender.send_message(msg); + context.source.send_feedback(msg, true); } } @@ -64,23 +57,23 @@ impl CommandExecutor for Executor { } if succeeded_deops == 0 { - Err(CommandError::CommandFailed(TextComponent::translate_cross( - pumpkin_data::translation::java::COMMANDS_DEOP_FAILED, - pumpkin_data::translation::bedrock::COMMANDS_DEOP_FAILED, - [], - ))) + Err(ERROR_DEOP_FAILED.create_without_context()) } else { Ok(succeeded_deops) } } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).then( - argument( - ARG_TARGETS, - GameProfilesArgumentConsumer::new(GameProfileSuggestionMode::OpNames, false), - ) - .execute(Executor), - ) +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Three), + )); + + dispatcher.register( + command("deop", DESCRIPTION) + .requires(PERMISSION) + .then(argument("targets", GameProfileArgumentType).executes(DeopExecutor)), + ); } diff --git a/crates/pumpkin/src/command/commands/effect.rs b/crates/pumpkin/src/command/commands/effect.rs index 66280b971..55fc938e7 100644 --- a/crates/pumpkin/src/command/commands/effect.rs +++ b/crates/pumpkin/src/command/commands/effect.rs @@ -1,92 +1,82 @@ -use crate::TextComponent; -use crate::command::args::bool::BoolArgConsumer; -use crate::command::args::bounded_num::BoundedNumArgumentConsumer; -use crate::command::args::players::PlayersArgumentConsumer; -use crate::command::args::resource::effect::EffectTypeArgumentConsumer; -use crate::command::args::{Arg, ConsumedArgs, FindArgDefaultName}; -use crate::command::dispatcher::CommandError::{self, InvalidConsumption}; -use crate::command::tree::CommandTree; -use crate::command::tree::builder::{argument, literal}; -use crate::command::{CommandExecutor, CommandResult, CommandSender}; -use crate::entity::EntityBase; use pumpkin_data::potion::Effect; +use pumpkin_data::translation; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; +use pumpkin_util::text::TextComponent; -const NAMES: [&str; 1] = ["effect"]; +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::core::bool::BoolArgumentType; +use crate::command::argument_types::core::integer::IntegerArgumentType; +use crate::command::argument_types::entity::EntityArgumentType; +use crate::command::argument_types::resource::{MOB_EFFECT_ARGUMENT, ResourceArgument}; +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 crate::entity::EntityBase; const DESCRIPTION: &str = "Adds or removes the status effects of players and other entities."; +const PERMISSION: &str = "minecraft:command.effect"; -const ARG_CLEAR: &str = "clear"; -const ARG_GIVE: &str = "give"; -const ARG_EFFECT: &str = "effect"; -const ARG_TARGET: &str = "target"; -const ARG_SECOND: &str = "seconds"; -const ARG_INFINITE: &str = "infinite"; -const ARG_AMPLIFIER: &str = "amplifier"; -const ARG_HIDE_PARTICLE: &str = "hideParticles"; +const ERROR_NOT_PLAYER: CommandErrorType<0> = CommandErrorType::new( + translation::java::PERMISSIONS_REQUIRES_PLAYER, + translation::java::PERMISSIONS_REQUIRES_PLAYER, +); -enum Time { - Base, +const ERROR_GIVE_FAILED: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_EFFECT_GIVE_FAILED, + translation::java::COMMANDS_EFFECT_GIVE_FAILED, +); + +const ERROR_CLEAR_EVERYTHING_FAILED: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_EFFECT_CLEAR_EVERYTHING_FAILED, + translation::java::COMMANDS_EFFECT_CLEAR_EVERYTHING_FAILED, +); + +const ERROR_CLEAR_SPECIFIC_FAILED: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_EFFECT_CLEAR_SPECIFIC_FAILED, + translation::java::COMMANDS_EFFECT_CLEAR_SPECIFIC_FAILED, +); + +#[derive(Clone, Copy)] +enum Duration { + Default, Specified, Infinite, } -enum Amplifier { - Base, - Specified, +struct GiveExecutor { + duration: Duration, + has_amplifier: bool, + has_hide_particles: bool, } -struct GiveExecutor(Time, Amplifier, bool); - impl CommandExecutor for GiveExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::Players(targets)) = args.get(ARG_TARGET) else { - return Err(InvalidConsumption(Some(ARG_TARGET.into()))); - }; - let Some(Arg::Effect(effect)) = args.get(ARG_EFFECT) else { - return Err(InvalidConsumption(Some(ARG_EFFECT.into()))); + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let targets = EntityArgumentType::get_players(context, "targets")?; + let effect = ResourceArgument::get_mob_effect(context, "effect")?; + + let duration_ticks = match self.duration { + Duration::Default => 30 * 20, + Duration::Specified => IntegerArgumentType::get(context, "seconds")? * 20, + Duration::Infinite => -1, }; - //duration is in tick, so * 20 (not for the infinite because -1*20 cause visual glitch) - let second = match self.0 { - Time::Base => 30 * 20, - Time::Specified => { - BoundedNumArgumentConsumer::new() - .name("seconds") - .min(1) - .max(1_000_000) - .find_arg_default_name(args)?? - * 20 - } - Time::Infinite => -1, + let amplifier = if self.has_amplifier { + IntegerArgumentType::get(context, "amplifier")? as u8 + } else { + 0 }; - let amplifier: u8 = match self.1 { - Amplifier::Base => 0, - Amplifier::Specified => BoundedNumArgumentConsumer::::new() - .name("amplifier") - .min(0) - .max(255) - .find_arg_default_name(args)?? as u8, + let hide_particles = if self.has_hide_particles { + BoolArgumentType::get(context, "hideParticles")? + } else { + false }; - let mut hide_particles = self.2; - //if false -> parameter is referred - if !hide_particles { - let Some(Arg::Bool(hide_particle)) = args.get(ARG_HIDE_PARTICLE) else { - return Err(InvalidConsumption(Some(ARG_HIDE_PARTICLE.into()))); - }; - - hide_particles = *hide_particle; - } - let mut successes = 0; - for target in targets { + for target in &targets { let should_skip = target .living_entity .get_effect(effect) @@ -95,9 +85,9 @@ impl CommandExecutor for GiveExecutor { if !should_skip { target.add_effect(Effect { effect_type: effect, - duration: second, + duration: duration_ticks, amplifier, - ambient: false, //this is not a beacon effect + ambient: false, show_particles: !hide_particles, show_icon: true, blend: false, @@ -113,209 +103,233 @@ impl CommandExecutor for GiveExecutor { ); if successes == 0 { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - "commands.effect.give.failed", - "commands.effect.give.failed", - [], - ))); - } else if targets.len() == 1 { - sender.send_message(TextComponent::translate_cross( - "commands.effect.give.success.single", - "commands.effect.give.success.single", - [translation_name, targets[0].get_display_name()], - )); + return Err(ERROR_GIVE_FAILED.create_without_context()); + } + + if targets.len() == 1 { + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_EFFECT_GIVE_SUCCESS_SINGLE, + translation::java::COMMANDS_EFFECT_GIVE_SUCCESS_SINGLE, + [translation_name, targets[0].as_ref().get_display_name()], + ), + true, + ); } else { - sender.send_message(TextComponent::translate_cross( - "commands.effect.give.success.multiple", - "commands.effect.give.success.multiple", - [ - translation_name, - TextComponent::text(targets.len().to_string()), - ], - )); + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_EFFECT_GIVE_SUCCESS_MULTIPLE, + translation::java::COMMANDS_EFFECT_GIVE_SUCCESS_MULTIPLE, + [ + translation_name, + TextComponent::text(targets.len().to_string()), + ], + ), + true, + ); } Ok(successes) } } -struct ClearExecutor(bool); //the param -> true = delete every effect, false = only one +enum ClearMode { + SelfAll, + TargetsAll, + TargetsSpecific, +} + +struct ClearExecutor(ClearMode); impl CommandExecutor for ClearExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::Players(targets)) = args.get(ARG_TARGET) else { - return Err(InvalidConsumption(Some(ARG_TARGET.into()))); + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let targets = match self.0 { + ClearMode::SelfAll => { + let player = context + .source + .output + .as_player() + .ok_or_else(|| ERROR_NOT_PLAYER.create_without_context())?; + vec![player] + } + ClearMode::TargetsAll | ClearMode::TargetsSpecific => { + EntityArgumentType::get_players(context, "targets")? + } }; - let effect; - //Only one effect - if self.0 { - let mut succeeded_clears: i32 = 0; - for target in targets { - let has_effects = !target - .living_entity - .active_effects - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .is_empty(); - if has_effects { - target.remove_all_effects(); - succeeded_clears += 1; + match self.0 { + ClearMode::SelfAll | ClearMode::TargetsAll => { + let mut succeeded_clears = 0; + for target in &targets { + let has_effects = !target + .living_entity + .active_effects + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .is_empty(); + if has_effects { + target.remove_all_effects(); + succeeded_clears += 1; + } } - } - //if the player or everyplayer don't have any effect - if succeeded_clears == 0 { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - "commands.effect.clear.everything.failed", - "commands.effect.clear.everything.failed", - [], - ))); - } - //a player have at least 1 effect - else if targets.len() == 1 { - sender.send_message(TextComponent::translate_cross( - "commands.effect.clear.everything.success.single", - "commands.effect.clear.everything.success.single", - [targets[0].get_display_name()], - )); - } else { - sender.send_message(TextComponent::translate_cross( - "commands.effect.clear.everything.success.multiple", - "commands.effect.clear.everything.success.multiple", - [TextComponent::text(targets.len().to_string())], - )); - } - Ok(succeeded_clears) - } else { - let Some(Arg::Effect(effect_type)) = args.get(ARG_EFFECT) else { - return Err(InvalidConsumption(Some(ARG_EFFECT.into()))); - }; - - effect = *effect_type; - - let mut succeeded_clears: i32 = 0; - for target in targets { - if target.living_entity.has_effect(effect) { - target.remove_effect(effect); - succeeded_clears += 1; + if succeeded_clears == 0 { + return Err(ERROR_CLEAR_EVERYTHING_FAILED.create_without_context()); } - } - if succeeded_clears == 0 { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - "commands.effect.clear.specific.failed", - "commands.effect.clear.specific.failed", - [], - ))); - } else if targets.len() == 1 { - sender.send_message(TextComponent::translate_cross( - "commands.effect.clear.specific.success.single", - "commands.effect.clear.specific.success.single", - [ + if targets.len() == 1 { + context.source.send_feedback( TextComponent::translate_cross( - effect.translation_key, - effect.translation_key, - [], + translation::java::COMMANDS_EFFECT_CLEAR_EVERYTHING_SUCCESS_SINGLE, + translation::java::COMMANDS_EFFECT_CLEAR_EVERYTHING_SUCCESS_SINGLE, + [targets[0].as_ref().get_display_name()], ), - targets[0].get_display_name(), - ], - )); - } else { - sender.send_message(TextComponent::translate_cross( - "commands.effect.clear.specific.success.multiple", - "commands.effect.clear.specific.success.multiple", - [ + true, + ); + } else { + context.source.send_feedback( TextComponent::translate_cross( - effect.translation_key, - effect.translation_key, - [], + translation::java::COMMANDS_EFFECT_CLEAR_EVERYTHING_SUCCESS_MULTIPLE, + translation::java::COMMANDS_EFFECT_CLEAR_EVERYTHING_SUCCESS_MULTIPLE, + [TextComponent::text(targets.len().to_string())], ), - TextComponent::text(targets.len().to_string()), - ], - )); + true, + ); + } + Ok(succeeded_clears) + } + ClearMode::TargetsSpecific => { + let effect = ResourceArgument::get_mob_effect(context, "effect")?; + let mut succeeded_clears = 0; + for target in &targets { + if target.living_entity.has_effect(effect) { + target.remove_effect(effect); + succeeded_clears += 1; + } + } + + if succeeded_clears == 0 { + return Err(ERROR_CLEAR_SPECIFIC_FAILED.create_without_context()); + } + + if targets.len() == 1 { + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_EFFECT_CLEAR_SPECIFIC_SUCCESS_SINGLE, + translation::java::COMMANDS_EFFECT_CLEAR_SPECIFIC_SUCCESS_SINGLE, + [ + TextComponent::translate_cross( + effect.translation_key, + effect.translation_key, + [], + ), + targets[0].as_ref().get_display_name(), + ], + ), + true, + ); + } else { + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_EFFECT_CLEAR_SPECIFIC_SUCCESS_MULTIPLE, + translation::java::COMMANDS_EFFECT_CLEAR_SPECIFIC_SUCCESS_MULTIPLE, + [ + TextComponent::translate_cross( + effect.translation_key, + effect.translation_key, + [], + ), + TextComponent::text(targets.len().to_string()), + ], + ), + true, + ); + } + Ok(succeeded_clears) } - Ok(succeeded_clears) } } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION) +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + let seconds_node = argument("seconds", IntegerArgumentType::new(0, 1_000_000)) + .executes(GiveExecutor { + duration: Duration::Specified, + has_amplifier: false, + has_hide_particles: false, + }) .then( - literal(ARG_CLEAR).then( - argument(ARG_TARGET, PlayersArgumentConsumer) - .execute(ClearExecutor(true)) - .then( - argument(ARG_EFFECT, EffectTypeArgumentConsumer) - .execute(ClearExecutor(false)), - ), - ), - ) - .then( - literal(ARG_GIVE).then( - argument(ARG_TARGET, PlayersArgumentConsumer).then( - argument(ARG_EFFECT, EffectTypeArgumentConsumer) - .execute(GiveExecutor(Time::Base, Amplifier::Base, true)) - //for specified time - .then( - argument( - ARG_SECOND, - BoundedNumArgumentConsumer::new() - .name("seconds") - .min(0) - .max(1_000_000), - ) - .execute(GiveExecutor(Time::Specified, Amplifier::Base, true)) - .then( - argument( - ARG_AMPLIFIER, - BoundedNumArgumentConsumer::new() - .name("amplifier") - .min(1) - .max(255), - ) - .execute(GiveExecutor(Time::Specified, Amplifier::Specified, true)) - .then( - argument(ARG_HIDE_PARTICLE, BoolArgConsumer).execute( - GiveExecutor(Time::Specified, Amplifier::Specified, false), - ), - ), - ), - ) - .then( - literal(ARG_INFINITE) - .execute(GiveExecutor(Time::Infinite, Amplifier::Base, true)) - .then( - argument( - ARG_AMPLIFIER, - BoundedNumArgumentConsumer::new() - .name("amplifier") - .min(1) - .max(255), - ) - .execute(GiveExecutor( - Time::Infinite, - Amplifier::Specified, - true, - )) - .then( - argument(ARG_HIDE_PARTICLE, BoolArgConsumer).execute( - GiveExecutor( - Time::Infinite, - Amplifier::Specified, - false, - ), - ), - ), - ), - ), + argument("amplifier", IntegerArgumentType::new(0, 255)) + .executes(GiveExecutor { + duration: Duration::Specified, + has_amplifier: true, + has_hide_particles: false, + }) + .then( + argument("hideParticles", BoolArgumentType).executes(GiveExecutor { + duration: Duration::Specified, + has_amplifier: true, + has_hide_particles: true, + }), ), - ), - ) + ); + + let infinite_node = literal("infinite") + .executes(GiveExecutor { + duration: Duration::Infinite, + has_amplifier: false, + has_hide_particles: false, + }) + .then( + argument("amplifier", IntegerArgumentType::new(0, 255)) + .executes(GiveExecutor { + duration: Duration::Infinite, + has_amplifier: true, + has_hide_particles: false, + }) + .then( + argument("hideParticles", BoolArgumentType).executes(GiveExecutor { + duration: Duration::Infinite, + has_amplifier: true, + has_hide_particles: true, + }), + ), + ); + + let give_node = literal("give").then( + argument("targets", EntityArgumentType::Players).then( + argument("effect", MOB_EFFECT_ARGUMENT.clone()) + .executes(GiveExecutor { + duration: Duration::Default, + has_amplifier: false, + has_hide_particles: false, + }) + .then(seconds_node) + .then(infinite_node), + ), + ); + + let clear_node = literal("clear") + .executes(ClearExecutor(ClearMode::SelfAll)) + .then( + argument("targets", EntityArgumentType::Players) + .executes(ClearExecutor(ClearMode::TargetsAll)) + .then( + argument("effect", MOB_EFFECT_ARGUMENT.clone()) + .executes(ClearExecutor(ClearMode::TargetsSpecific)), + ), + ); + + dispatcher.register( + command("effect", DESCRIPTION) + .requires(PERMISSION) + .then(clear_node) + .then(give_node), + ); } diff --git a/crates/pumpkin/src/command/commands/enchant.rs b/crates/pumpkin/src/command/commands/enchant.rs index 6c7067f8b..796d6eb5b 100644 --- a/crates/pumpkin/src/command/commands/enchant.rs +++ b/crates/pumpkin/src/command/commands/enchant.rs @@ -1,167 +1,67 @@ -use pumpkin_data::{Enchantment, translation}; -use pumpkin_util::text::TextComponent; use std::sync::Arc; -use crate::command::args::bounded_num::{BoundedNumArgumentConsumer, NotInBounds}; -use crate::command::args::entities::EntitiesArgumentConsumer; -use crate::command::args::resource::enchantment::EnchantmentArgumentConsumer; -use crate::command::args::{ConsumedArgs, FindArgDefaultName}; -use crate::command::tree::CommandTree; -use crate::command::tree::builder::argument_default_name; -use crate::command::{CommandError, CommandExecutor, CommandResult, CommandSender}; -use crate::entity::EntityBase; use pumpkin_data::data_component_impl::EnchantmentsImpl; +use pumpkin_data::{Enchantment, translation}; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; +use pumpkin_util::text::TextComponent; + +use crate::command::argument_builder::{ArgumentBuilder, argument, command}; +use crate::command::argument_types::core::integer::IntegerArgumentType; +use crate::command::argument_types::entity::EntityArgumentType; +use crate::command::argument_types::resource::{ENCHANTMENT_ARGUMENT, ResourceArgument}; +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 crate::entity::EntityBase; -const NAMES: [&str; 1] = ["enchant"]; const DESCRIPTION: &str = "Adds an enchantment to a player's selected item, subject to the same restrictions as an anvil. Also works on any mob or entity holding a weapon/tool/armor in its main hand."; +const PERMISSION: &str = "minecraft:command.enchant"; -struct Executor; +const ERROR_FAILED: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_ENCHANT_FAILED, + translation::java::COMMANDS_ENCHANT_FAILED, +); -impl CommandExecutor for Executor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let targets = EntitiesArgumentConsumer.find_arg_default_name(args)?; - let enchantment = EnchantmentArgumentConsumer.find_arg_default_name(args)?; - let level = match enchantment_level_consumer().find_arg_default_name(args) { - Err(_) => 1, - Ok(Ok(level)) => level, - Ok(Err(err)) => { - let err_msg = match err { - NotInBounds::LowerBound(val, min) => TextComponent::translate_cross( - "argument.integer.low", - "argument.integer.low", - &[ - TextComponent::text(min.to_string()), - TextComponent::text(val.to_string()), - ], - ), - NotInBounds::UpperBound(val, max) => TextComponent::translate_cross( - "argument.integer.big", - "argument.integer.big", - &[ - TextComponent::text(max.to_string()), - TextComponent::text(val.to_string()), - ], - ), - }; +const ERROR_FAILED_LEVEL: CommandErrorType<2> = CommandErrorType::new( + translation::java::COMMANDS_ENCHANT_FAILED_LEVEL, + translation::java::COMMANDS_ENCHANT_FAILED_LEVEL, +); - return Err(CommandError::CommandFailed(err_msg)); - } - }; +const ERROR_FAILED_ITEMLESS: CommandErrorType<1> = CommandErrorType::new( + translation::java::COMMANDS_ENCHANT_FAILED_ITEMLESS, + translation::java::COMMANDS_ENCHANT_FAILED_ITEMLESS, +); - if level > enchantment.max_level { - let msg = TextComponent::translate_cross( - translation::java::COMMANDS_ENCHANT_FAILED_LEVEL, - translation::bedrock::COMMANDS_ENCHANT_INVALIDLEVEL, - [ - TextComponent::text(level.to_string()), - TextComponent::text(enchantment.max_level.to_string()), - ], - ); - return Err(CommandError::CommandFailed(msg)); - } - - let mut successful_targets = 0; - - if targets.len() == 1 { - return match enchant_target(&targets[0], enchantment, level) { - Ok(()) => { - let msg = TextComponent::translate_cross( - translation::java::COMMANDS_ENCHANT_SUCCESS_SINGLE, - translation::bedrock::COMMANDS_ENCHANT_SUCCESS, - [ - enchantment.get_fullname(level), - targets[0].get_display_name(), - ], - ); - sender.send_message(msg); - Ok(1) - } - Err(e) => Err(e), - }; - } - - for target in targets { - if enchant_target(target, enchantment, level).is_ok() { - successful_targets += 1; - } - } - - if successful_targets == 0 { - return Err(commands_enchant_failed()); - } - - let msg = TextComponent::translate_cross( - translation::java::COMMANDS_ENCHANT_SUCCESS_MULTIPLE, - translation::bedrock::COMMANDS_ENCHANT_SUCCESS, - [ - enchantment.get_fullname(level), - TextComponent::text(targets.len().to_string()), - ], - ); - sender.send_message(msg); - Ok(successful_targets) - } -} - -const fn enchantment_level_consumer() -> BoundedNumArgumentConsumer { - BoundedNumArgumentConsumer::new() - .name("level") - .min(0) - .max(i32::MAX) -} - -fn commands_enchant_failed() -> CommandError { - let msg = TextComponent::translate_cross( - translation::java::COMMANDS_ENCHANT_FAILED, - translation::bedrock::COMMANDS_ENCHANT_CANTENCHANT, - [TextComponent::text("")], - ); - CommandError::CommandFailed(msg) -} +const ERROR_FAILED_INCOMPATIBLE: CommandErrorType<1> = CommandErrorType::new( + translation::java::COMMANDS_ENCHANT_FAILED_INCOMPATIBLE, + translation::java::COMMANDS_ENCHANT_FAILED_INCOMPATIBLE, +); fn enchant_target( target: &Arc, enchantment: &'static Enchantment, level: i32, -) -> Result<(), CommandError> { +) -> Result<(), crate::command::errors::command_syntax_error::CommandSyntaxError> { let Some(player) = target.get_player() else { - return Err(commands_enchant_failed()); + return Err(ERROR_FAILED.create_without_context()); }; let mut item = player.inventory().held_item(); if item.is_empty() { - let msg = TextComponent::translate_cross( - translation::java::COMMANDS_ENCHANT_FAILED_ITEMLESS, - translation::bedrock::COMMANDS_ENCHANT_NOITEM, - [target.get_display_name()], - ); - return Err(CommandError::CommandFailed(msg)); + return Err(ERROR_FAILED_ITEMLESS.create_without_context(target.get_display_name())); } if !enchantment.can_enchant(item.item) { - let msg = TextComponent::translate_cross( - translation::java::COMMANDS_ENCHANT_FAILED_INCOMPATIBLE, - translation::bedrock::COMMANDS_ENCHANT_CANTENCHANT, - [item.item.translated_name()], - ); - return Err(CommandError::CommandFailed(msg)); + return Err(ERROR_FAILED_INCOMPATIBLE.create_without_context(item.item.translated_name())); } if let Some(data) = item.get_data_component::() && !enchantment.is_enchantment_compatible(data) { - let msg = TextComponent::translate_cross( - translation::java::COMMANDS_ENCHANT_FAILED_INCOMPATIBLE, - translation::bedrock::COMMANDS_ENCHANT_CANTENCHANT, - [item.item.translated_name()], - ); - return Err(CommandError::CommandFailed(msg)); + return Err(ERROR_FAILED_INCOMPATIBLE.create_without_context(item.item.translated_name())); } item.enchant(enchantment, level); @@ -173,12 +73,84 @@ fn enchant_target( Ok(()) } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).then( - argument_default_name(EntitiesArgumentConsumer).then( - argument_default_name(EnchantmentArgumentConsumer) - .then(argument_default_name(enchantment_level_consumer()).execute(Executor)) - .execute(Executor), - ), - ) +struct EnchantExecutor { + has_level: bool, +} + +impl CommandExecutor for EnchantExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let targets = EntityArgumentType::get_entities(context, "targets")?; + let enchantment = ResourceArgument::get_enchantment(context, "enchantment")?; + let level = if self.has_level { + IntegerArgumentType::get(context, "level")? + } else { + 1 + }; + + if level > enchantment.max_level { + return Err(ERROR_FAILED_LEVEL.create_without_context( + TextComponent::text(level.to_string()), + TextComponent::text(enchantment.max_level.to_string()), + )); + } + + let mut successful_targets = 0; + + if targets.len() == 1 { + enchant_target(&targets[0], enchantment, level)?; + let msg = TextComponent::translate_cross( + translation::java::COMMANDS_ENCHANT_SUCCESS_SINGLE, + translation::bedrock::COMMANDS_ENCHANT_SUCCESS, + [ + enchantment.get_fullname(level), + targets[0].as_ref().get_display_name(), + ], + ); + context.source.send_feedback(msg, true); + return Ok(1); + } + + for target in &targets { + if enchant_target(target, enchantment, level).is_ok() { + successful_targets += 1; + } + } + + if successful_targets == 0 { + return Err(ERROR_FAILED.create_without_context()); + } + + let msg = TextComponent::translate_cross( + translation::java::COMMANDS_ENCHANT_SUCCESS_MULTIPLE, + translation::bedrock::COMMANDS_ENCHANT_SUCCESS, + [ + enchantment.get_fullname(level), + TextComponent::text(targets.len().to_string()), + ], + ); + context.source.send_feedback(msg, true); + + Ok(successful_targets) + } +} + +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + dispatcher.register( + command("enchant", DESCRIPTION).requires(PERMISSION).then( + argument("targets", EntityArgumentType::Entities).then( + argument("enchantment", ENCHANTMENT_ARGUMENT.clone()) + .executes(EnchantExecutor { has_level: false }) + .then( + argument("level", IntegerArgumentType::with_min(0)) + .executes(EnchantExecutor { has_level: true }), + ), + ), + ), + ); } diff --git a/crates/pumpkin/src/command/commands/execute.rs b/crates/pumpkin/src/command/commands/execute.rs index e29ceb0fc..e6eeae05c 100644 --- a/crates/pumpkin/src/command/commands/execute.rs +++ b/crates/pumpkin/src/command/commands/execute.rs @@ -1,25 +1,57 @@ +#![allow( + clippy::too_many_lines, + clippy::collapsible_if, + clippy::unnecessary_wraps, + clippy::significant_drop_in_scrutinee, + clippy::nonminimal_bool, + clippy::if_not_else, + clippy::explicit_auto_deref, + clippy::cast_possible_wrap, + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + clippy::match_same_arms, + clippy::struct_excessive_bools +)] + 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::argument_types::coordinates::rotation::RotationArgumentType; +use crate::command::argument_types::coordinates::swizzle::SwizzleArgumentType; use crate::command::argument_types::coordinates::vec3::Vec3ArgumentType; use crate::command::argument_types::core::string::StringArgumentType; use crate::command::argument_types::entity::EntityArgumentType; use crate::command::argument_types::entity_anchor::EntityAnchorArgumentType; +use crate::command::argument_types::identifier::IdentifierArgumentType; +use crate::command::argument_types::nbt_path::{NbtPath, NbtPathArgumentType}; +use crate::command::argument_types::objective::ObjectiveArgumentType; +use crate::command::argument_types::range::{FloatRangeArgumentType, IntRangeArgumentType}; use crate::command::argument_types::resource::{ENTITY_TYPE_ARGUMENT, ResourceArgument}; -use crate::command::argument_types::resource_key::ResourceKeyArgument; +use crate::command::argument_types::resource_key::{BIOME_REGISTRY, ResourceKeyArgument}; +use crate::command::argument_types::resource_or_tag::{ResourceOrTag, ResourceOrTagArgument}; +use crate::command::commands::data::{ + BlockDataAccessor, DataAccessor, EntityDataAccessor, StorageDataAccessor, +}; use crate::command::context::command_context::CommandContext; +use crate::command::errors::command_syntax_error::CommandSyntaxError; use crate::command::errors::error_types::CommandErrorType; use crate::command::node::attached::{CommandNodeId, NodeId}; use crate::command::node::dispatcher::CommandDispatcher; use crate::command::node::tree::Tree; use crate::command::node::{RedirectModifier, Redirection}; +use crate::entity::EntityBase; use crate::entity::r#type::from_type; +use crate::world::stopwatches::Stopwatches; +use pumpkin_data::biome::Biome; +use pumpkin_data::tag::{self, RegistryKey}; +use pumpkin_nbt::tag::NbtTag; use pumpkin_util::PermissionLvl; use pumpkin_util::identifier::Identifier; +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 rustc_hash::FxHashSet; use std::sync::Arc; use uuid::Uuid; @@ -106,6 +138,21 @@ fn execute_positioned_as_modifier( Ok(sources) } +fn execute_positioned_over_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + let mut source = context.source.as_ref().clone(); + let pos = source.position; + let block_x = pos.x.floor() as i32; + let block_z = pos.z.floor() as i32; + + if let Some(ref world) = source.world { + let top_y = world.get_top_block(Vector2::new(block_x, block_z)); + source.position.y = (top_y + 1) as f64; + } + Ok(vec![Arc::new(source)]) +} + fn execute_rotated_modifier( context: &CommandContext, ) -> crate::command::node::RedirectModifierResult { @@ -155,22 +202,10 @@ fn execute_unless_entity_modifier( fn execute_align_modifier( context: &CommandContext, ) -> crate::command::node::RedirectModifierResult { - let axes = StringArgumentType::get(context, "axes")?; + let swizzle = SwizzleArgumentType::get(context, "axes")?; let mut source = context.source.as_ref().clone(); - let has_x = axes.contains('x'); - let has_y = axes.contains('y'); - let has_z = axes.contains('z'); - - if has_x { - source.position.x = source.position.x.floor(); - } - if has_y { - source.position.y = source.position.y.floor(); - } - if has_z { - source.position.z = source.position.z.floor(); - } + source.position = swizzle.align(source.position); Ok(vec![Arc::new(source)]) } @@ -327,6 +362,513 @@ fn execute_unless_dimension_modifier( Ok(vec![]) } +fn execute_if_biome_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + let pos = BlockPosArgumentType::get_block_pos(context, "pos")?; + let biome_arg = context.get_argument::("biome")?.clone(); + + if let Some(ref world) = context.source.world { + let biome = world.get_biome(&pos); + let targets: FxHashSet = match &biome_arg { + ResourceOrTag::Resource(id) => Biome::from_name(id.path()) + .map(|b| b.id) + .into_iter() + .collect(), + ResourceOrTag::Tag(id) => { + tag::get_tag_values(RegistryKey::WorldgenBiome, &id.to_string()) + .into_iter() + .flatten() + .filter_map(|name| Biome::from_name(*name)) + .map(|b| b.id) + .collect() + } + }; + + if targets.contains(&biome.id) { + return Ok(vec![context.source.clone()]); + } + } + Ok(vec![]) +} + +fn execute_unless_biome_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + let pos = BlockPosArgumentType::get_block_pos(context, "pos")?; + let biome_arg = context.get_argument::("biome")?.clone(); + + if let Some(ref world) = context.source.world { + let biome = world.get_biome(&pos); + let targets: FxHashSet = match &biome_arg { + ResourceOrTag::Resource(id) => Biome::from_name(id.path()) + .map(|b| b.id) + .into_iter() + .collect(), + ResourceOrTag::Tag(id) => { + tag::get_tag_values(RegistryKey::WorldgenBiome, &id.to_string()) + .into_iter() + .flatten() + .filter_map(|name| Biome::from_name(*name)) + .map(|b| b.id) + .collect() + } + }; + + if !targets.contains(&biome.id) { + return Ok(vec![context.source.clone()]); + } + } else { + return Ok(vec![context.source.clone()]); + } + Ok(vec![]) +} + +fn get_score(context: &CommandContext, target: &str, objective: &str) -> Option { + let world = context.source.world.as_ref()?; + let scoreboard = world + .scoreboard + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + scoreboard.get_score(target, objective).map(|s| s.value.0) +} + +fn execute_if_score_matches_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + let target = StringArgumentType::get(context, "target")?; + let target_obj = ObjectiveArgumentType::get(context, "target_obj")?; + let range = IntRangeArgumentType::get(context, "range")?; + + if let Some(score) = get_score(context, target, target_obj) { + if range.matches(score) { + return Ok(vec![context.source.clone()]); + } + } + Ok(vec![]) +} + +fn execute_unless_score_matches_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + let target = StringArgumentType::get(context, "target")?; + let target_obj = ObjectiveArgumentType::get(context, "target_obj")?; + let range = IntRangeArgumentType::get(context, "range")?; + + if let Some(score) = get_score(context, target, target_obj) { + if !range.matches(score) { + return Ok(vec![context.source.clone()]); + } + } else { + return Ok(vec![context.source.clone()]); + } + Ok(vec![]) +} + +fn execute_if_score_eq_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + let target = StringArgumentType::get(context, "target")?; + let target_obj = ObjectiveArgumentType::get(context, "target_obj")?; + let source = StringArgumentType::get(context, "source")?; + let source_obj = ObjectiveArgumentType::get(context, "source_obj")?; + + if let (Some(a), Some(b)) = ( + get_score(context, target, target_obj), + get_score(context, source, source_obj), + ) { + if a == b { + return Ok(vec![context.source.clone()]); + } + } + Ok(vec![]) +} + +fn execute_if_score_lt_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + let target = StringArgumentType::get(context, "target")?; + let target_obj = ObjectiveArgumentType::get(context, "target_obj")?; + let source = StringArgumentType::get(context, "source")?; + let source_obj = ObjectiveArgumentType::get(context, "source_obj")?; + + if let (Some(a), Some(b)) = ( + get_score(context, target, target_obj), + get_score(context, source, source_obj), + ) { + if a < b { + return Ok(vec![context.source.clone()]); + } + } + Ok(vec![]) +} + +fn execute_if_score_le_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + let target = StringArgumentType::get(context, "target")?; + let target_obj = ObjectiveArgumentType::get(context, "target_obj")?; + let source = StringArgumentType::get(context, "source")?; + let source_obj = ObjectiveArgumentType::get(context, "source_obj")?; + + if let (Some(a), Some(b)) = ( + get_score(context, target, target_obj), + get_score(context, source, source_obj), + ) { + if a <= b { + return Ok(vec![context.source.clone()]); + } + } + Ok(vec![]) +} + +fn execute_if_score_gt_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + let target = StringArgumentType::get(context, "target")?; + let target_obj = ObjectiveArgumentType::get(context, "target_obj")?; + let source = StringArgumentType::get(context, "source")?; + let source_obj = ObjectiveArgumentType::get(context, "source_obj")?; + + if let (Some(a), Some(b)) = ( + get_score(context, target, target_obj), + get_score(context, source, source_obj), + ) { + if a > b { + return Ok(vec![context.source.clone()]); + } + } + Ok(vec![]) +} + +fn execute_if_score_ge_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + let target = StringArgumentType::get(context, "target")?; + let target_obj = ObjectiveArgumentType::get(context, "target_obj")?; + let source = StringArgumentType::get(context, "source")?; + let source_obj = ObjectiveArgumentType::get(context, "source_obj")?; + + if let (Some(a), Some(b)) = ( + get_score(context, target, target_obj), + get_score(context, source, source_obj), + ) { + if a >= b { + return Ok(vec![context.source.clone()]); + } + } + Ok(vec![]) +} + +fn execute_unless_score_eq_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + let target = StringArgumentType::get(context, "target")?; + let target_obj = ObjectiveArgumentType::get(context, "target_obj")?; + let source = StringArgumentType::get(context, "source")?; + let source_obj = ObjectiveArgumentType::get(context, "source_obj")?; + + if let (Some(a), Some(b)) = ( + get_score(context, target, target_obj), + get_score(context, source, source_obj), + ) { + if a != b { + return Ok(vec![context.source.clone()]); + } + } else { + return Ok(vec![context.source.clone()]); + } + Ok(vec![]) +} + +fn execute_unless_score_lt_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + let target = StringArgumentType::get(context, "target")?; + let target_obj = ObjectiveArgumentType::get(context, "target_obj")?; + let source = StringArgumentType::get(context, "source")?; + let source_obj = ObjectiveArgumentType::get(context, "source_obj")?; + + if let (Some(a), Some(b)) = ( + get_score(context, target, target_obj), + get_score(context, source, source_obj), + ) { + if a >= b { + return Ok(vec![context.source.clone()]); + } + } else { + return Ok(vec![context.source.clone()]); + } + Ok(vec![]) +} + +fn execute_unless_score_le_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + let target = StringArgumentType::get(context, "target")?; + let target_obj = ObjectiveArgumentType::get(context, "target_obj")?; + let source = StringArgumentType::get(context, "source")?; + let source_obj = ObjectiveArgumentType::get(context, "source_obj")?; + + if let (Some(a), Some(b)) = ( + get_score(context, target, target_obj), + get_score(context, source, source_obj), + ) { + if a > b { + return Ok(vec![context.source.clone()]); + } + } else { + return Ok(vec![context.source.clone()]); + } + Ok(vec![]) +} + +fn execute_unless_score_gt_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + let target = StringArgumentType::get(context, "target")?; + let target_obj = ObjectiveArgumentType::get(context, "target_obj")?; + let source = StringArgumentType::get(context, "source")?; + let source_obj = ObjectiveArgumentType::get(context, "source_obj")?; + + if let (Some(a), Some(b)) = ( + get_score(context, target, target_obj), + get_score(context, source, source_obj), + ) { + if a <= b { + return Ok(vec![context.source.clone()]); + } + } else { + return Ok(vec![context.source.clone()]); + } + Ok(vec![]) +} + +fn execute_unless_score_ge_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + let target = StringArgumentType::get(context, "target")?; + let target_obj = ObjectiveArgumentType::get(context, "target_obj")?; + let source = StringArgumentType::get(context, "source")?; + let source_obj = ObjectiveArgumentType::get(context, "source_obj")?; + + if let (Some(a), Some(b)) = ( + get_score(context, target, target_obj), + get_score(context, source, source_obj), + ) { + if a < b { + return Ok(vec![context.source.clone()]); + } + } else { + return Ok(vec![context.source.clone()]); + } + Ok(vec![]) +} + +fn check_blocks_match(context: &CommandContext, masked: bool) -> Result { + let start = BlockPosArgumentType::get_block_pos(context, "start")?; + let end = BlockPosArgumentType::get_block_pos(context, "end")?; + let destination = BlockPosArgumentType::get_block_pos(context, "destination")?; + + if let Some(ref world) = context.source.world { + let min_x = start.0.x.min(end.0.x); + let max_x = start.0.x.max(end.0.x); + let min_y = start.0.y.min(end.0.y); + let max_y = start.0.y.max(end.0.y); + let min_z = start.0.z.min(end.0.z); + let max_z = start.0.z.max(end.0.z); + + let dx = destination.0.x - min_x; + let dy = destination.0.y - min_y; + let dz = destination.0.z - min_z; + + for y in min_y..=max_y { + for z in min_z..=max_z { + for x in min_x..=max_x { + let src_pos = BlockPos::new(x, y, z); + let dst_pos = BlockPos::new(x + dx, y + dy, z + dz); + let src_block = world.get_block(&src_pos); + let dst_block = world.get_block(&dst_pos); + + if masked && src_block.is_air() { + continue; + } + if src_block != dst_block { + return Ok(false); + } + } + } + } + return Ok(true); + } + Ok(false) +} + +fn execute_if_blocks_all_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + if check_blocks_match(context, false)? { + Ok(vec![context.source.clone()]) + } else { + Ok(vec![]) + } +} + +fn execute_if_blocks_masked_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + if check_blocks_match(context, true)? { + Ok(vec![context.source.clone()]) + } else { + Ok(vec![]) + } +} + +fn execute_unless_blocks_all_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + if !check_blocks_match(context, false)? { + Ok(vec![context.source.clone()]) + } else { + Ok(vec![]) + } +} + +fn execute_unless_blocks_masked_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + if !check_blocks_match(context, true)? { + Ok(vec![context.source.clone()]) + } else { + Ok(vec![]) + } +} + +fn check_data_matches( + context: &CommandContext, + is_block: bool, + is_entity: bool, +) -> Result { + let path = context.get_argument::("path")?; + let tags = if is_block { + let pos = BlockPosArgumentType::get_block_pos(context, "pos")?; + let world = context.source.world().clone(); + let accessor = BlockDataAccessor::new(pos, world)?; + let data = accessor.get_data()?; + path.get(&NbtTag::Compound(data))? + } else if is_entity { + let entity = EntityArgumentType::get_entity(context, "target")?; + let accessor = EntityDataAccessor::new(entity); + let data = accessor.get_data()?; + path.get(&NbtTag::Compound(data))? + } else { + let id = context.get_argument::("id")?; + let server = context.server().clone(); + let accessor = StorageDataAccessor::new(id.to_string(), server); + let data = accessor.get_data()?; + path.get(&NbtTag::Compound(data))? + }; + Ok(!tags.is_empty()) +} + +fn execute_if_data_block_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + if check_data_matches(context, true, false)? { + Ok(vec![context.source.clone()]) + } else { + Ok(vec![]) + } +} + +fn execute_if_data_entity_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + if check_data_matches(context, false, true)? { + Ok(vec![context.source.clone()]) + } else { + Ok(vec![]) + } +} + +fn execute_if_data_storage_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + if check_data_matches(context, false, false)? { + Ok(vec![context.source.clone()]) + } else { + Ok(vec![]) + } +} + +fn execute_unless_data_block_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + if !check_data_matches(context, true, false)? { + Ok(vec![context.source.clone()]) + } else { + Ok(vec![]) + } +} + +fn execute_unless_data_entity_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + if !check_data_matches(context, false, true)? { + Ok(vec![context.source.clone()]) + } else { + Ok(vec![]) + } +} + +fn execute_unless_data_storage_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + if !check_data_matches(context, false, false)? { + Ok(vec![context.source.clone()]) + } else { + Ok(vec![]) + } +} + +fn check_stopwatch_matches(context: &CommandContext) -> Result { + let id = context.get_argument::("id")?; + let range = FloatRangeArgumentType::get(context, "range")?; + let stopwatches = context + .server() + .stopwatches + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let id_str = id.to_string(); + let Some(stopwatch) = stopwatches.get(&id_str) else { + return Err(crate::command::commands::stopwatch::ERROR_DOES_NOT_EXIST + .create_without_context(TextComponent::text(id_str))); + }; + let now = Stopwatches::current_time(); + let elapsed = stopwatch.elapsed_seconds(now); + Ok(range.matches(elapsed)) +} + +fn execute_if_stopwatch_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + if check_stopwatch_matches(context)? { + Ok(vec![context.source.clone()]) + } else { + Ok(vec![]) + } +} + +fn execute_unless_stopwatch_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + if !check_stopwatch_matches(context)? { + Ok(vec![context.source.clone()]) + } else { + Ok(vec![]) + } +} + fn execute_summon_modifier( context: &CommandContext, ) -> crate::command::node::RedirectModifierResult { @@ -343,6 +885,178 @@ fn execute_summon_modifier( Ok(vec![Arc::new(source)]) } +fn execute_on_owner_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + let mut sources = Vec::new(); + if let Some(ref entity) = context.source.entity { + let world = context.source.world(); + if let Some(mob) = entity.get_mob() { + if let Some(owner_uuid) = mob.get_owner_uuid() { + if let Some(owner) = world.get_player_by_uuid(owner_uuid) { + let mut source = context.source.as_ref().clone(); + let name = owner.get_name().get_text(); + let display_name = owner.get_display_name(); + source.entity = Some(owner); + source.name = name; + source.display_name = display_name; + sources.push(Arc::new(source)); + } + } + } + } + Ok(sources) +} + +fn execute_on_leasher_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + let mut sources = Vec::new(); + if let Some(ref entity) = context.source.entity { + if let Some(leasher) = entity + .get_entity() + .leashed_to + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .clone() + { + let mut source = context.source.as_ref().clone(); + let name = leasher.get_name().get_text(); + let display_name = leasher.get_display_name(); + source.entity = Some(leasher); + source.name = name; + source.display_name = display_name; + sources.push(Arc::new(source)); + } + } + Ok(sources) +} + +fn execute_on_target_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + let mut sources = Vec::new(); + if let Some(ref entity) = context.source.entity { + if let Some(mob) = entity.get_mob() { + if let Some(target) = mob.get_mob_entity().get_target() { + let mut source = context.source.as_ref().clone(); + let name = target.get_name().get_text(); + let display_name = target.get_display_name(); + source.entity = Some(target); + source.name = name; + source.display_name = display_name; + sources.push(Arc::new(source)); + } + } + } + Ok(sources) +} + +fn execute_on_attacker_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + let mut sources = Vec::new(); + if let Some(ref entity) = context.source.entity { + if let Some(living) = entity.get_living_entity() { + let attacker_id = living + .last_attacker_id + .load(std::sync::atomic::Ordering::Relaxed); + if attacker_id != 0 { + let world = context.source.world(); + if let Some(attacker) = world.get_entity_by_id(attacker_id) { + let mut source = context.source.as_ref().clone(); + let name = attacker.get_name().get_text(); + let display_name = attacker.get_display_name(); + source.entity = Some(attacker); + source.name = name; + source.display_name = display_name; + sources.push(Arc::new(source)); + } + } + } + } + Ok(sources) +} + +fn execute_on_vehicle_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + let mut sources = Vec::new(); + if let Some(ref entity) = context.source.entity { + if let Some(vehicle) = entity + .get_entity() + .vehicle + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .clone() + { + let mut source = context.source.as_ref().clone(); + let name = vehicle.get_name().get_text(); + let display_name = vehicle.get_display_name(); + source.entity = Some(vehicle); + source.name = name; + source.display_name = display_name; + sources.push(Arc::new(source)); + } + } + Ok(sources) +} + +fn execute_on_controller_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + let mut sources = Vec::new(); + if let Some(ref entity) = context.source.entity { + if let Some(controller) = entity + .get_entity() + .passengers + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .first() + .cloned() + { + let mut source = context.source.as_ref().clone(); + let name = controller.get_name().get_text(); + let display_name = controller.get_display_name(); + source.entity = Some(controller); + source.name = name; + source.display_name = display_name; + sources.push(Arc::new(source)); + } + } + Ok(sources) +} + +fn execute_on_origin_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + execute_on_owner_modifier(context) +} + +fn execute_on_passengers_modifier( + context: &CommandContext, +) -> crate::command::node::RedirectModifierResult { + let mut sources = Vec::new(); + if let Some(ref entity) = context.source.entity { + let passengers = entity + .get_entity() + .passengers + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .clone(); + for passenger in passengers { + let mut source = context.source.as_ref().clone(); + let name = passenger.get_name().get_text(); + let display_name = passenger.get_display_name(); + source.entity = Some(passenger); + source.name = name; + source.display_name = display_name; + sources.push(Arc::new(source)); + } + } + Ok(sources) +} + #[allow(clippy::too_many_lines)] pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { registry.register_permission_or_panic(Permission::new( @@ -380,6 +1094,12 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr RedirectModifier::Custom(Arc::new(execute_positioned_as_modifier)), )), ) + .then(literal("over").then( + argument("heightmap", StringArgumentType::SingleWord).redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new(execute_positioned_over_modifier)), + ), + )) .then( argument("pos", Vec3ArgumentType::Default).redirect_with_modifier( Redirection::Root, @@ -403,7 +1123,7 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr ), ) .then(literal("align").then( - argument("axes", StringArgumentType::SingleWord).redirect_with_modifier( + argument("axes", SwizzleArgumentType).redirect_with_modifier( Redirection::Root, RedirectModifier::Custom(Arc::new(execute_align_modifier)), ), @@ -431,6 +1151,47 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr ), ), ) + .then(literal("summon").then( + argument("entity_type", ENTITY_TYPE_ARGUMENT.clone()).redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new(execute_summon_modifier)), + ), + )) + .then( + literal("on") + .then(literal("owner").fork( + Redirection::Root, + RedirectModifier::Custom(Arc::new(execute_on_owner_modifier)), + )) + .then(literal("leasher").fork( + Redirection::Root, + RedirectModifier::Custom(Arc::new(execute_on_leasher_modifier)), + )) + .then(literal("target").fork( + Redirection::Root, + RedirectModifier::Custom(Arc::new(execute_on_target_modifier)), + )) + .then(literal("attacker").fork( + Redirection::Root, + RedirectModifier::Custom(Arc::new(execute_on_attacker_modifier)), + )) + .then(literal("vehicle").fork( + Redirection::Root, + RedirectModifier::Custom(Arc::new(execute_on_vehicle_modifier)), + )) + .then(literal("controller").fork( + Redirection::Root, + RedirectModifier::Custom(Arc::new(execute_on_controller_modifier)), + )) + .then(literal("origin").fork( + Redirection::Root, + RedirectModifier::Custom(Arc::new(execute_on_origin_modifier)), + )) + .then(literal("passengers").fork( + Redirection::Root, + RedirectModifier::Custom(Arc::new(execute_on_passengers_modifier)), + )), + ) .then( literal("if") .then(literal("entity").then( @@ -461,6 +1222,159 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr RedirectModifier::Custom(Arc::new(execute_if_dimension_modifier)), ), ), + ) + .then( + literal("biome").then( + argument("pos", BlockPosArgumentType).then( + argument("biome", ResourceOrTagArgument(BIOME_REGISTRY.clone())) + .redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new(execute_if_biome_modifier)), + ), + ), + ), + ) + .then( + literal("blocks").then( + argument("start", BlockPosArgumentType).then( + argument("end", BlockPosArgumentType).then( + argument("destination", BlockPosArgumentType) + .then(literal("all").redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new( + execute_if_blocks_all_modifier, + )), + )) + .then(literal("masked").redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new( + execute_if_blocks_masked_modifier, + )), + )), + ), + ), + ), + ) + .then( + literal("score").then( + argument("target", StringArgumentType::SingleWord).then( + argument("target_obj", ObjectiveArgumentType) + .then(literal("matches").then( + argument("range", IntRangeArgumentType).redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new( + execute_if_score_matches_modifier, + )), + ), + )) + .then( + literal("=").then( + argument("source", StringArgumentType::SingleWord).then( + argument("source_obj", ObjectiveArgumentType) + .redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new( + execute_if_score_eq_modifier, + )), + ), + ), + ), + ) + .then( + literal("<").then( + argument("source", StringArgumentType::SingleWord).then( + argument("source_obj", ObjectiveArgumentType) + .redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new( + execute_if_score_lt_modifier, + )), + ), + ), + ), + ) + .then( + literal("<=").then( + argument("source", StringArgumentType::SingleWord).then( + argument("source_obj", ObjectiveArgumentType) + .redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new( + execute_if_score_le_modifier, + )), + ), + ), + ), + ) + .then( + literal(">").then( + argument("source", StringArgumentType::SingleWord).then( + argument("source_obj", ObjectiveArgumentType) + .redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new( + execute_if_score_gt_modifier, + )), + ), + ), + ), + ) + .then( + literal(">=").then( + argument("source", StringArgumentType::SingleWord).then( + argument("source_obj", ObjectiveArgumentType) + .redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new( + execute_if_score_ge_modifier, + )), + ), + ), + ), + ), + ), + ), + ) + .then( + literal("data") + .then( + literal("block").then(argument("pos", BlockPosArgumentType).then( + argument("path", NbtPathArgumentType).redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new( + execute_if_data_block_modifier, + )), + ), + )), + ) + .then(literal("entity").then( + argument("target", EntityArgumentType::Entity).then( + argument("path", NbtPathArgumentType).redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new( + execute_if_data_entity_modifier, + )), + ), + ), + )) + .then(literal("storage").then( + argument("id", IdentifierArgumentType).then( + argument("path", NbtPathArgumentType).redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new( + execute_if_data_storage_modifier, + )), + ), + ), + )), + ) + .then( + literal("stopwatch").then(argument("id", IdentifierArgumentType).then( + argument("range", FloatRangeArgumentType).redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new(execute_if_stopwatch_modifier)), + ), + )), ), ) .then( @@ -495,14 +1409,163 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr )), ), ), + ) + .then( + literal("biome").then( + argument("pos", BlockPosArgumentType).then( + argument("biome", ResourceOrTagArgument(BIOME_REGISTRY.clone())) + .redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new( + execute_unless_biome_modifier, + )), + ), + ), + ), + ) + .then( + literal("blocks").then( + argument("start", BlockPosArgumentType).then( + argument("end", BlockPosArgumentType).then( + argument("destination", BlockPosArgumentType) + .then(literal("all").redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new( + execute_unless_blocks_all_modifier, + )), + )) + .then(literal("masked").redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new( + execute_unless_blocks_masked_modifier, + )), + )), + ), + ), + ), + ) + .then( + literal("score").then( + argument("target", StringArgumentType::SingleWord).then( + argument("target_obj", ObjectiveArgumentType) + .then(literal("matches").then( + argument("range", IntRangeArgumentType).redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new( + execute_unless_score_matches_modifier, + )), + ), + )) + .then( + literal("=").then( + argument("source", StringArgumentType::SingleWord).then( + argument("source_obj", ObjectiveArgumentType) + .redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new( + execute_unless_score_eq_modifier, + )), + ), + ), + ), + ) + .then( + literal("<").then( + argument("source", StringArgumentType::SingleWord).then( + argument("source_obj", ObjectiveArgumentType) + .redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new( + execute_unless_score_lt_modifier, + )), + ), + ), + ), + ) + .then( + literal("<=").then( + argument("source", StringArgumentType::SingleWord).then( + argument("source_obj", ObjectiveArgumentType) + .redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new( + execute_unless_score_le_modifier, + )), + ), + ), + ), + ) + .then( + literal(">").then( + argument("source", StringArgumentType::SingleWord).then( + argument("source_obj", ObjectiveArgumentType) + .redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new( + execute_unless_score_gt_modifier, + )), + ), + ), + ), + ) + .then( + literal(">=").then( + argument("source", StringArgumentType::SingleWord).then( + argument("source_obj", ObjectiveArgumentType) + .redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new( + execute_unless_score_ge_modifier, + )), + ), + ), + ), + ), + ), + ), + ) + .then( + literal("data") + .then( + literal("block").then(argument("pos", BlockPosArgumentType).then( + argument("path", NbtPathArgumentType).redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new( + execute_unless_data_block_modifier, + )), + ), + )), + ) + .then(literal("entity").then( + argument("target", EntityArgumentType::Entity).then( + argument("path", NbtPathArgumentType).redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new( + execute_unless_data_entity_modifier, + )), + ), + ), + )) + .then(literal("storage").then( + argument("id", IdentifierArgumentType).then( + argument("path", NbtPathArgumentType).redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new( + execute_unless_data_storage_modifier, + )), + ), + ), + )), + ) + .then( + literal("stopwatch").then(argument("id", IdentifierArgumentType).then( + argument("range", FloatRangeArgumentType).redirect_with_modifier( + Redirection::Root, + RedirectModifier::Custom(Arc::new(execute_unless_stopwatch_modifier)), + ), + )), ), - ) - .then(literal("summon").then( - argument("entity_type", ENTITY_TYPE_ARGUMENT.clone()).redirect_with_modifier( - Redirection::Root, - RedirectModifier::Custom(Arc::new(execute_summon_modifier)), - ), - )); + ); let execute_node_id = dispatcher.register(builder); diff --git a/crates/pumpkin/src/command/commands/experience.rs b/crates/pumpkin/src/command/commands/experience.rs index a9295ded1..bc5c1f593 100644 --- a/crates/pumpkin/src/command/commands/experience.rs +++ b/crates/pumpkin/src/command/commands/experience.rs @@ -1,30 +1,28 @@ use std::sync::Arc; use std::sync::atomic::Ordering; +use pumpkin_util::PermissionLvl; use pumpkin_util::math::experience; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; -use crate::command::args::bounded_num::BoundedNumArgumentConsumer; -use crate::command::args::players::PlayersArgumentConsumer; -use crate::command::args::{ConsumedArgs, FindArg}; -use crate::command::dispatcher::CommandError; -use crate::command::tree::CommandTree; -use crate::command::tree::builder::{argument, literal}; -use crate::command::{CommandExecutor, CommandResult, CommandSender}; +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::core::integer::IntegerArgumentType; +use crate::command::argument_types::entity::EntityArgumentType; +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 crate::entity::EntityBase; use crate::entity::player::Player; -const NAMES: [&str; 2] = ["experience", "xp"]; const DESCRIPTION: &str = "Add, set or query player experience."; -const ARG_TARGETS: &str = "targets"; -const ARG_AMOUNT: &str = "amount"; +const PERMISSION: &str = "minecraft:command.experience"; -const fn xp_amount() -> BoundedNumArgumentConsumer { - BoundedNumArgumentConsumer::new() - .name(ARG_AMOUNT) - .min(0) - .max(i32::MAX) -} +const ERROR_SET_POINTS_INVALID: CommandErrorType<0> = CommandErrorType::new( + "commands.experience.set.points.invalid", + "commands.experience.set.points.invalid", +); #[derive(Clone, Copy, PartialEq)] enum Mode { @@ -39,13 +37,13 @@ enum ExpType { Levels, } -struct Executor { +struct ExperienceExecutor { mode: Mode, exp_type: ExpType, } -impl Executor { - fn handle_query(&self, sender: &CommandSender, target: &Player) -> i32 { +impl ExperienceExecutor { + fn handle_query(&self, context: &CommandContext, target: &Player) -> i32 { let (val, translation_key) = match self.exp_type { ExpType::Levels => ( target.experience_level.load(Ordering::Relaxed), @@ -57,14 +55,17 @@ impl Executor { ), }; - sender.send_message(TextComponent::translate_cross( - translation_key, - translation_key, - [ - target.get_display_name(), - TextComponent::text(val.to_string()), - ], - )); + context.source.send_feedback( + TextComponent::translate_cross( + translation_key, + translation_key, + [ + target.get_display_name(), + TextComponent::text(val.to_string()), + ], + ), + false, + ); val } @@ -119,7 +120,6 @@ impl Executor { } } - /// Returns `true` if successful. Otherwise, there was a problem setting the points of a player. fn handle_modify(&self, target: &Arc, amount: i32) -> bool { match self.exp_type { ExpType::Levels => { @@ -145,112 +145,96 @@ impl Executor { } } -impl CommandExecutor for Executor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let targets = PlayersArgumentConsumer::find_arg(args, ARG_TARGETS)?; +impl CommandExecutor for ExperienceExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let targets = EntityArgumentType::get_players(context, "targets")?; if self.mode == Mode::Query { - let target = targets.first().ok_or_else(|| { - CommandError::CommandFailed(TextComponent::translate_cross( - "argument.player.unknown", - "argument.player.unknown", - [], - )) - })?; - - if targets.len() > 1 { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - "argument.player.toomany", - "argument.player.toomany", - [], - ))); - } - return Ok(self.handle_query(sender, target)); + let target = targets[0].clone(); + return Ok(self.handle_query(context, &target)); } - // Handle Add/Set - let amount = BoundedNumArgumentConsumer::::find_arg(args, ARG_AMOUNT)??; + let amount = IntegerArgumentType::get(context, "amount")?; let mut successes = 0; - for target in targets { + for target in &targets { if self.handle_modify(target, amount) { successes += 1; } } if successes == 0 { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - "commands.experience.set.points.invalid", - "commands.experience.set.points.invalid", - [], - ))); + return Err(ERROR_SET_POINTS_INVALID.create_without_context()); } - // Safe to access first() because successes > 0 - let first_name = targets[0].get_display_name(); - let msg = self.get_success_message(amount, targets, first_name); - sender.send_message(msg); + let first_name = targets[0].as_ref().get_display_name(); + let msg = self.get_success_message(amount, &targets, first_name); + context.source.send_feedback(msg, true); Ok(successes) } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION) - .then( - literal("add").then( - argument(ARG_TARGETS, PlayersArgumentConsumer).then( - argument(ARG_AMOUNT, xp_amount()) - .then(literal("levels").execute(Executor { - mode: Mode::Add, - exp_type: ExpType::Levels, - })) - .then(literal("points").execute(Executor { - mode: Mode::Add, - exp_type: ExpType::Points, - })) - .execute(Executor { - mode: Mode::Add, - exp_type: ExpType::Points, - }), - ), - ), - ) - .then( - literal("set").then( - argument(ARG_TARGETS, PlayersArgumentConsumer).then( - argument(ARG_AMOUNT, xp_amount()) - .then(literal("levels").execute(Executor { - mode: Mode::Set, - exp_type: ExpType::Levels, - })) - .then(literal("points").execute(Executor { - mode: Mode::Set, - exp_type: ExpType::Points, - })) - .execute(Executor { - mode: Mode::Set, - exp_type: ExpType::Points, - }), - ), - ), - ) - .then( - literal("query").then( - argument(ARG_TARGETS, PlayersArgumentConsumer) - .then(literal("levels").execute(Executor { - mode: Mode::Query, - exp_type: ExpType::Levels, - })) - .then(literal("points").execute(Executor { - mode: Mode::Query, - exp_type: ExpType::Points, - })), - ), - ) +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + let add_node = literal("add").then( + argument("targets", EntityArgumentType::Players).then( + argument("amount", IntegerArgumentType::with_min(0)) + .executes(ExperienceExecutor { + mode: Mode::Add, + exp_type: ExpType::Points, + }) + .then(literal("levels").executes(ExperienceExecutor { + mode: Mode::Add, + exp_type: ExpType::Levels, + })) + .then(literal("points").executes(ExperienceExecutor { + mode: Mode::Add, + exp_type: ExpType::Points, + })), + ), + ); + + let set_node = literal("set").then( + argument("targets", EntityArgumentType::Players).then( + argument("amount", IntegerArgumentType::with_min(0)) + .executes(ExperienceExecutor { + mode: Mode::Set, + exp_type: ExpType::Points, + }) + .then(literal("levels").executes(ExperienceExecutor { + mode: Mode::Set, + exp_type: ExpType::Levels, + })) + .then(literal("points").executes(ExperienceExecutor { + mode: Mode::Set, + exp_type: ExpType::Points, + })), + ), + ); + + let query_node = literal("query").then( + argument("targets", EntityArgumentType::Player) + .then(literal("levels").executes(ExperienceExecutor { + mode: Mode::Query, + exp_type: ExpType::Levels, + })) + .then(literal("points").executes(ExperienceExecutor { + mode: Mode::Query, + exp_type: ExpType::Points, + })), + ); + + let cmd = command("experience", DESCRIPTION) + .requires(PERMISSION) + .then(add_node) + .then(set_node) + .then(query_node); + + dispatcher.register_with_aliases(cmd, &["xp"]); } diff --git a/crates/pumpkin/src/command/commands/fill.rs b/crates/pumpkin/src/command/commands/fill.rs index e7d7874fc..49074026e 100644 --- a/crates/pumpkin/src/command/commands/fill.rs +++ b/crates/pumpkin/src/command/commands/fill.rs @@ -1,355 +1,259 @@ -use std::sync::Arc; - -use crate::command::args::block::{ - BlockArgumentConsumer, BlockPredicate, BlockPredicateArgumentConsumer, -}; -use crate::command::args::position_block::BlockPosArgumentConsumer; -use crate::command::args::{ConsumedArgs, FindArg}; -use crate::command::tree::CommandTree; -use crate::command::tree::builder::{argument, literal}; -use crate::command::{CommandError, CommandExecutor, CommandResult, CommandSender}; -use crate::world::World; - use pumpkin_data::translation; use pumpkin_data::{Block, BlockStateId}; +use pumpkin_util::PermissionLvl; use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::vector3::Vector3; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; use pumpkin_world::world::BlockFlags; -const NAMES: [&str; 1] = ["fill"]; +use crate::command::argument_builder::{ + ArgumentBuilder, RequiredArgumentBuilder, argument, command, literal, +}; +use crate::command::argument_types::block::BlockArgumentType; +use crate::command::argument_types::block_predicate::{BlockPredicate, BlockPredicateArgumentType}; +use crate::command::argument_types::coordinates::block_pos::BlockPosArgumentType; +use crate::command::context::command_context::CommandContext; +use crate::command::context::command_source::CommandSource; +use crate::command::errors::command_syntax_error::CommandSyntaxError; +use crate::command::errors::error_types::CommandErrorType; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; const DESCRIPTION: &str = "Fills all or parts of a region with a specific block."; +const PERMISSION: &str = "minecraft:command.fill"; -const ARG_BLOCK: &str = "block"; -const ARG_FROM: &str = "from"; -const ARG_TO: &str = "to"; -const ARG_FILTER: &str = "filter"; +const ERROR_AREA_TOO_LARGE: CommandErrorType<2> = CommandErrorType::new( + translation::java::COMMANDS_FILL_TOOBIG, + translation::java::COMMANDS_FILL_TOOBIG, +); -#[derive(Clone, Copy, Default)] -enum Mode { - /// Destroys blocks with particles and item drops - Destroy, - /// Leaves only the outer layer of blocks, removes the inner ones (creates a hollow space) - Hollow, - /// Only replaces air blocks, keeping non-air blocks unchanged - Keep, - /// Like Hollow but doesn't replace inner blocks with air, just the outline - Outline, - /// Replaces all blocks with the new block state, without particles - #[default] +const ERROR_FAILED: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_FILL_FAILED, + translation::java::COMMANDS_FILL_FAILED, +); + +#[derive(Clone, Copy, PartialEq, Eq)] +enum FillMode { Replace, - /// Replaces all blocks with the new block state, without particles and neighbors update - Strict, + Outline, + Hollow, + Destroy, + Keep, } -struct Executor(Mode); +#[derive(Clone, Copy)] +enum FilterMode { + None, + WithFilter, + KeepAir, +} -fn not_in_filter(filter: &BlockPredicate, old_block: &Block) -> bool { - match filter { - BlockPredicate::Tag(tag) => !tag.contains(&old_block.id.as_u16()), - BlockPredicate::Block(block) => *block != old_block.id, +fn fill_blocks( + source: &CommandSource, + from: BlockPos, + to: BlockPos, + target_block: &'static Block, + mode: FillMode, + filter: Option<&BlockPredicate>, + _strict: bool, +) -> Result { + let min_x = from.0.x.min(to.0.x); + let min_y = from.0.y.min(to.0.y); + let min_z = from.0.z.min(to.0.z); + let max_x = from.0.x.max(to.0.x); + let max_y = from.0.y.max(to.0.y); + let max_z = from.0.z.max(to.0.z); + + let x_span = i64::from(max_x - min_x + 1); + let y_span = i64::from(max_y - min_y + 1); + let z_span = i64::from(max_z - min_z + 1); + let area = x_span * y_span * z_span; + + let world = source.world().clone(); + let max_block_modifications = { + let level_info = world.level_info.load(); + level_info.game_rules.max_block_modifications + }; + + if area > max_block_modifications { + return Err(ERROR_AREA_TOO_LARGE.create_without_context( + TextComponent::text(max_block_modifications.to_string()), + TextComponent::text(area.to_string()), + )); } -} -enum FillerResult { - DidNotPlaceBlock = 0, - PlacedBlock = 1, - PlacedBlockWithoutUpdate = 2, -} + let target_state_id = target_block.default_state.id; + let mut count = 0; -struct Context { - block_state_id: BlockStateId, - option_filter: Option, - world: Arc, - placed_blocks: i32, - to_update: Vec, + for x in min_x..=max_x { + for y in min_y..=max_y { + for z in min_z..=max_z { + let pos = BlockPos(Vector3::new(x, y, z)); + let current_block = world.get_block(&pos); - start_x: i32, - start_y: i32, - start_z: i32, - end_x: i32, - end_y: i32, - end_z: i32, -} - -impl Context { - /// Checks whether the block position is at the edge of the region stored by this context. - #[inline] - const fn is_edge(&self, block_position: BlockPos) -> bool { - let pos = block_position.0; - pos.x == self.start_x - || pos.x == self.end_x - || pos.y == self.start_y - || pos.y == self.end_y - || pos.z == self.start_z - || pos.z == self.end_z - } -} - -trait Filler { - fn execute_for_pos(context: &Context, block_position: BlockPos) -> FillerResult; - - fn execute_for_region(context: &mut Context) { - for x in context.start_x..=context.end_x { - for y in context.start_y..=context.end_y { - for z in context.start_z..=context.end_z { - let block_position = BlockPos(Vector3::new(x, y, z)); - let filler_result = Self::execute_for_pos(context, block_position); - match filler_result { - FillerResult::PlacedBlock => { - context.placed_blocks += 1; - context.to_update.push(block_position); + match mode { + FillMode::Keep => { + if !current_block.is_air() { + continue; } - FillerResult::PlacedBlockWithoutUpdate => { - context.placed_blocks += 1; - } - FillerResult::DidNotPlaceBlock => {} } + _ => { + if let Some(f) = filter + && !f.test(current_block) + { + continue; + } + } + } + + let is_edge = x == min_x + || x == max_x + || y == min_y + || y == max_y + || z == min_z + || z == max_z; + + let block_to_place = match mode { + FillMode::Outline => is_edge.then_some(target_state_id), + FillMode::Hollow => { + if is_edge { + Some(target_state_id) + } else { + Some(BlockStateId::AIR) + } + } + FillMode::Destroy => { + world.break_block( + &pos, + None, + BlockFlags::SKIP_DROPS | BlockFlags::FORCE_STATE, + ); + Some(target_state_id) + } + FillMode::Replace | FillMode::Keep => Some(target_state_id), + }; + + if let Some(state_id) = block_to_place { + world.set_block_state(&pos, state_id, BlockFlags::FORCE_STATE); + count += 1; } } } } -} -struct DestroyFiller; -impl Filler for DestroyFiller { - fn execute_for_pos(context: &Context, block_position: BlockPos) -> FillerResult { - if let Some(filter) = &context.option_filter - && not_in_filter(filter, context.world.get_block(&block_position)) - { - return FillerResult::DidNotPlaceBlock; - } - context.world.break_block( - &block_position, - None, - BlockFlags::SKIP_DROPS | BlockFlags::FORCE_STATE, - ); - context.world.set_block_state( - &block_position, - context.block_state_id, - BlockFlags::FORCE_STATE, - ); - FillerResult::PlacedBlock + if count == 0 { + return Err(ERROR_FAILED.create_without_context()); } -} -struct HollowFiller; -impl Filler for HollowFiller { - fn execute_for_pos(context: &Context, block_position: BlockPos) -> FillerResult { - if let Some(filter) = &context.option_filter - && not_in_filter(filter, context.world.get_block(&block_position)) - { - return FillerResult::DidNotPlaceBlock; - } - if context.is_edge(block_position) { - context.world.set_block_state( - &block_position, - context.block_state_id, - BlockFlags::FORCE_STATE, - ); - } else { - context.world.set_block_state( - &block_position, - BlockStateId::AIR, - BlockFlags::FORCE_STATE, - ); - } - FillerResult::PlacedBlock - } -} - -struct KeepFiller; -impl Filler for KeepFiller { - fn execute_for_pos(context: &Context, block_position: BlockPos) -> FillerResult { - let (old_block, old_state) = context.world.get_block_and_state(&block_position); - if old_state.is_air() { - if let Some(filter) = &context.option_filter - && not_in_filter(filter, old_block) - { - return FillerResult::DidNotPlaceBlock; - } - context.world.set_block_state( - &block_position, - context.block_state_id, - BlockFlags::FORCE_STATE, - ); - FillerResult::PlacedBlock - } else { - FillerResult::DidNotPlaceBlock - } - } -} - -struct OutlineFiller; -impl Filler for OutlineFiller { - fn execute_for_pos(context: &Context, block_position: BlockPos) -> FillerResult { - if !context.is_edge(block_position) { - return FillerResult::DidNotPlaceBlock; - } - if let Some(filter) = &context.option_filter - && not_in_filter(filter, context.world.get_block(&block_position)) - { - return FillerResult::DidNotPlaceBlock; - } - context.world.set_block_state( - &block_position, - context.block_state_id, - BlockFlags::FORCE_STATE, - ); - FillerResult::PlacedBlock - } -} - -struct ReplaceFiller; -impl Filler for ReplaceFiller { - fn execute_for_pos(context: &Context, block_position: BlockPos) -> FillerResult { - if let Some(filter) = &context.option_filter - && not_in_filter(filter, context.world.get_block(&block_position)) - { - return FillerResult::DidNotPlaceBlock; - } - context.world.set_block_state( - &block_position, - context.block_state_id, - BlockFlags::FORCE_STATE, - ); - FillerResult::PlacedBlock - } -} - -struct StrictFiller; -impl Filler for StrictFiller { - fn execute_for_pos(context: &Context, block_position: BlockPos) -> FillerResult { - if let Some(filter) = &context.option_filter - && not_in_filter(filter, context.world.get_block(&block_position)) - { - return FillerResult::DidNotPlaceBlock; - } - context.world.set_block_state( - &block_position, - context.block_state_id, - BlockFlags::SKIP_BLOCK_ADDED_CALLBACK, - ); - FillerResult::PlacedBlockWithoutUpdate - } -} - -impl CommandExecutor for Executor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let block = BlockArgumentConsumer::find_arg(args, ARG_BLOCK)?; - let block_state_id = block.default_state.id; - let from = BlockPosArgumentConsumer::find_arg(args, ARG_FROM)?; - let to = BlockPosArgumentConsumer::find_arg(args, ARG_TO)?; - let mode = self.0; - - let mut context = Context { - block_state_id, - option_filter: BlockPredicateArgumentConsumer::find_arg(args, ARG_FILTER)?, - world: sender - .world_or_first(server) - .ok_or(CommandError::InvalidRequirement)?, - placed_blocks: 0, - to_update: Vec::new(), - - start_x: from.0.x.min(to.0.x), - start_y: from.0.y.min(to.0.y), - start_z: from.0.z.min(to.0.z), - - end_x: from.0.x.max(to.0.x), - end_y: from.0.y.max(to.0.y), - end_z: from.0.z.max(to.0.z), - }; - - if !context.world.is_in_build_limit(from) || !context.world.is_in_build_limit(to) { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::ARGUMENT_POS_OUTOFBOUNDS, - translation::java::ARGUMENT_POS_OUTOFBOUNDS, - [], - ))); - } - - let max_block_modifications = { - let level_info = server.level_info.load(); - level_info.game_rules.max_block_modifications - }; - - let total_blocks = (context.end_x - context.start_x + 1) as i64 - * (context.end_y - context.start_y + 1) as i64 - * (context.end_z - context.start_z + 1) as i64; - - if total_blocks > max_block_modifications { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_FILL_TOOBIG, - translation::java::COMMANDS_FILL_TOOBIG, - [ - TextComponent::text(max_block_modifications.to_string()), - TextComponent::text(total_blocks.to_string()), - ], - ))); - } - - match mode { - Mode::Destroy => DestroyFiller::execute_for_region(&mut context), - Mode::Replace => ReplaceFiller::execute_for_region(&mut context), - Mode::Keep => KeepFiller::execute_for_region(&mut context), - Mode::Hollow => HollowFiller::execute_for_region(&mut context), - Mode::Outline => OutlineFiller::execute_for_region(&mut context), - Mode::Strict => StrictFiller::execute_for_region(&mut context), - } - - for i in context.to_update { - context.world.update_neighbors(&i, None); - } - - if context.placed_blocks == 0 { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_FILL_FAILED, - translation::bedrock::COMMANDS_FILL_FAILED, - [], - ))); - } - - sender.send_message(TextComponent::translate_cross( + source.send_feedback( + TextComponent::translate_cross( translation::java::COMMANDS_FILL_SUCCESS, - translation::bedrock::COMMANDS_FILL_SUCCESS, - [TextComponent::text(context.placed_blocks.to_string())], - )); + translation::java::COMMANDS_FILL_SUCCESS, + [TextComponent::text(count.to_string())], + ), + true, + ); - Ok(context.placed_blocks) + Ok(count) +} + +struct FillExecutor { + mode: FillMode, + filter_mode: FilterMode, + strict: bool, +} + +impl CommandExecutor for FillExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let from = BlockPosArgumentType::get_loaded_block_pos(context, "from")?; + let to = BlockPosArgumentType::get_loaded_block_pos(context, "to")?; + let block = BlockArgumentType::get(context, "block")?; + + let filter = if matches!(self.filter_mode, FilterMode::WithFilter) { + Some(BlockPredicateArgumentType::get(context, "filter")?) + } else { + None + }; + + fill_blocks( + &context.source, + from, + to, + block, + self.mode, + filter.as_ref(), + self.strict, + ) } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).then( - argument(ARG_FROM, BlockPosArgumentConsumer).then( - argument(ARG_TO, BlockPosArgumentConsumer).then( - argument(ARG_BLOCK, BlockArgumentConsumer) - .then(literal("destroy").execute(Executor(Mode::Destroy))) - .then(literal("hollow").execute(Executor(Mode::Hollow))) - .then(literal("keep").execute(Executor(Mode::Keep))) - .then(literal("outline").execute(Executor(Mode::Outline))) - .then( - literal("replace") - .then( - argument(ARG_FILTER, BlockPredicateArgumentConsumer) - .then(literal("destroy").execute(Executor(Mode::Destroy))) - .then(literal("hollow").execute(Executor(Mode::Hollow))) - .then(literal("keep").execute(Executor(Mode::Keep))) - .then(literal("outline").execute(Executor(Mode::Outline))) - .then(literal("strict").execute(Executor(Mode::Strict))) - .execute(Executor(Mode::Replace)), - ) - .execute(Executor(Mode::Replace)), - ) - .then(literal("strict").execute(Executor(Mode::Strict))) - .execute(Executor(Mode::Replace)), - ), - ), - ) +fn wrap_with_mode(builder: RequiredArgumentBuilder, has_filter: bool) -> RequiredArgumentBuilder { + let filter_mode = if has_filter { + FilterMode::WithFilter + } else { + FilterMode::None + }; + + builder + .executes(FillExecutor { + mode: FillMode::Replace, + filter_mode, + strict: false, + }) + .then(literal("outline").executes(FillExecutor { + mode: FillMode::Outline, + filter_mode, + strict: false, + })) + .then(literal("hollow").executes(FillExecutor { + mode: FillMode::Hollow, + filter_mode, + strict: false, + })) + .then(literal("destroy").executes(FillExecutor { + mode: FillMode::Destroy, + filter_mode, + strict: false, + })) + .then(literal("strict").executes(FillExecutor { + mode: FillMode::Replace, + filter_mode, + strict: true, + })) +} + +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + let filter_arg = wrap_with_mode(argument("filter", BlockPredicateArgumentType), true); + + let replace_literal = literal("replace") + .executes(FillExecutor { + mode: FillMode::Replace, + filter_mode: FilterMode::None, + strict: false, + }) + .then(filter_arg); + + let keep_literal = literal("keep").executes(FillExecutor { + mode: FillMode::Keep, + filter_mode: FilterMode::KeepAir, + strict: false, + }); + + let block_arg = wrap_with_mode(argument("block", BlockArgumentType), false) + .then(replace_literal) + .then(keep_literal); + + dispatcher.register( + command("fill", DESCRIPTION).requires(PERMISSION).then( + argument("from", BlockPosArgumentType) + .then(argument("to", BlockPosArgumentType).then(block_arg)), + ), + ); } diff --git a/crates/pumpkin/src/command/commands/gamemode.rs b/crates/pumpkin/src/command/commands/gamemode.rs index 0abc8a7b4..fc52d049b 100644 --- a/crates/pumpkin/src/command/commands/gamemode.rs +++ b/crates/pumpkin/src/command/commands/gamemode.rs @@ -1,66 +1,63 @@ -use crate::command::args::GetCloned; -use crate::command::args::gamemode::GamemodeArgumentConsumer; +use std::sync::Arc; -use crate::TextComponent; use pumpkin_data::translation; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; +use pumpkin_util::text::TextComponent; -use crate::command::args::players::PlayersArgumentConsumer; - -use crate::command::args::{Arg, ConsumedArgs}; -use crate::command::dispatcher::CommandError::{InvalidConsumption, InvalidRequirement}; -use crate::command::tree::CommandTree; -use crate::command::tree::builder::{argument, require}; -use crate::command::{CommandExecutor, CommandResult, CommandSender}; +use crate::command::argument_builder::{ArgumentBuilder, argument, command}; +use crate::command::argument_types::entity::EntityArgumentType; +use crate::command::argument_types::gamemode::GameModeArgumentType; +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 crate::entity::EntityBase; -const NAMES: [&str; 1] = ["gamemode"]; - const DESCRIPTION: &str = "Change a player's gamemode."; +const PERMISSION: &str = "minecraft:command.gamemode"; -const ARG_GAMEMODE: &str = "gamemode"; -const ARG_TARGET: &str = "target"; +const ERROR_NOT_PLAYER: CommandErrorType<0> = CommandErrorType::new( + translation::java::PERMISSIONS_REQUIRES_PLAYER, + translation::java::PERMISSIONS_REQUIRES_PLAYER, +); -struct TargetExecutor { +struct GamemodeExecutor { is_self: bool, } -impl CommandExecutor for TargetExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::GameMode(gamemode)) = args.get_cloned(&ARG_GAMEMODE) else { - return Err(InvalidConsumption(Some(ARG_GAMEMODE.into()))); - }; +impl CommandExecutor for GamemodeExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let gamemode = GameModeArgumentType::get(context, "gamemode")?; let targets = if self.is_self { - let Some(player) = sender.as_player() else { - return Err(InvalidRequirement); - }; - &[player] + let player = context + .source + .output + .as_player() + .ok_or_else(|| ERROR_NOT_PLAYER.create_without_context())?; + vec![player] } else { - let Some(Arg::Players(targets)) = args.get(ARG_TARGET) else { - return Err(InvalidConsumption(Some(ARG_TARGET.into()))); - }; - targets.as_slice() + EntityArgumentType::get_players(context, "target")? }; let mut succeeded: i32 = 0; - for target in targets { + let server = context.source.server(); + + for target in &targets { if target.gamemode.load() != gamemode { target.set_gamemode(gamemode); succeeded += 1; let gamemode_string = format!("{gamemode:?}").to_lowercase(); let gamemode_string = format!("gameMode.{gamemode_string}"); - // Checking if the target was the sender of this command. - let gamemode_comp = TextComponent::translate_cross( - gamemode_string.clone(), - gamemode_string.clone(), - [], - ); - if sender.as_player().as_ref() == Some(target) { + let gamemode_comp = + TextComponent::translate_cross(gamemode_string.clone(), gamemode_string, []); + let is_self = context + .source + .output + .as_player() + .is_some_and(|p| Arc::ptr_eq(&p, target)); + if is_self { target.send_system_message(&TextComponent::translate_cross( translation::java::COMMANDS_GAMEMODE_SUCCESS_SELF, translation::bedrock::COMMANDS_GAMEMODE_SUCCESS_SELF, @@ -74,11 +71,14 @@ impl CommandExecutor for TargetExecutor { [gamemode_comp.clone()], )); } - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_GAMEMODE_SUCCESS_OTHER, - translation::bedrock::COMMANDS_GAMEMODE_SUCCESS_OTHER, - [target.get_display_name(), gamemode_comp], - )); + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_GAMEMODE_SUCCESS_OTHER, + translation::bedrock::COMMANDS_GAMEMODE_SUCCESS_OTHER, + [target.as_ref().get_display_name(), gamemode_comp], + ), + true, + ); } } } @@ -87,14 +87,21 @@ impl CommandExecutor for TargetExecutor { } } -#[expect(clippy::redundant_closure_for_method_calls)] -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).then( - argument(ARG_GAMEMODE, GamemodeArgumentConsumer) - .then(require(|sender| sender.is_player()).execute(TargetExecutor { is_self: true })) - .then( - argument(ARG_TARGET, PlayersArgumentConsumer) - .execute(TargetExecutor { is_self: false }), - ), - ) +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + dispatcher.register( + command("gamemode", DESCRIPTION).requires(PERMISSION).then( + argument("gamemode", GameModeArgumentType) + .executes(GamemodeExecutor { is_self: true }) + .then( + argument("target", EntityArgumentType::Players) + .executes(GamemodeExecutor { is_self: false }), + ), + ), + ); } diff --git a/crates/pumpkin/src/command/commands/gamerule.rs b/crates/pumpkin/src/command/commands/gamerule.rs index b4b2c5b74..3f9953f53 100644 --- a/crates/pumpkin/src/command/commands/gamerule.rs +++ b/crates/pumpkin/src/command/commands/gamerule.rs @@ -1,33 +1,26 @@ +use std::sync::Arc; + use pumpkin_data::game_rules::{GameRule, GameRuleRegistry, GameRuleValue}; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; +use pumpkin_util::text::TextComponent; -use crate::command::args::FindArg; -use crate::command::args::bool::BoolArgConsumer; -use crate::command::args::bounded_num::BoundedNumArgumentConsumer; - -use crate::TextComponent; - -use crate::command::args::ConsumedArgs; -use crate::command::tree::CommandTree; -use crate::command::tree::builder::{argument, literal}; -use crate::command::{CommandExecutor, CommandResult, CommandSender}; - -const NAMES: [&str; 1] = ["gamerule"]; +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::core::bool::BoolArgumentType; +use crate::command::argument_types::core::integer::IntegerArgumentType; +use crate::command::context::command_context::CommandContext; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; const DESCRIPTION: &str = "Sets or queries a game rule value."; - -const ARG_NAME: &str = "value"; +const PERMISSION: &str = "minecraft:command.gamerule"; struct QueryExecutor(GameRule); impl CommandExecutor for QueryExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - _args: &ConsumedArgs, - ) -> CommandResult { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { let key = TextComponent::text(self.0.to_string()); - let level_info = server.level_info.load(); + let level_info = context.server().level_info.load(); let game_rule = level_info.game_rules.get(&self.0); let game_rule_i32_value = match game_rule { GameRuleValue::Int(value) => (*value).clamp(i32::MIN as i64, i32::MAX as i64) as i32, @@ -36,79 +29,101 @@ impl CommandExecutor for QueryExecutor { let value = TextComponent::text(game_rule.to_string()); drop(level_info); - sender.send_message(TextComponent::translate_cross( - "commands.gamerule.query", - "commands.gamerule.query", - [key, value], - )); + context.source.send_feedback( + TextComponent::translate_cross( + "commands.gamerule.query", + "commands.gamerule.query", + [key, value], + ), + false, + ); Ok(game_rule_i32_value) } } -struct SetExecutor(GameRule); +struct SetIntExecutor(GameRule); -impl CommandExecutor for SetExecutor { - #[expect(unused)] - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { +impl CommandExecutor for SetIntExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { let key = TextComponent::text(self.0.to_string()); - let current_info = server.level_info.load(); + let arg_value = IntegerArgumentType::get(context, "value")? as i64; + let current_info = context.server().level_info.load(); let mut new_info = (**current_info).clone(); - let mut output_value = String::new(); - let mut result_i32: i32; - - let raw_value = new_info.game_rules.get_mut(&self.0); - - match raw_value { - GameRuleValue::Int(value) => { - let arg_value = BoundedNumArgumentConsumer::::find_arg(args, ARG_NAME)??; - *value = arg_value; - output_value = arg_value.to_string(); - // TODO: Should integer gamerule values be kept as a `i64` or should it be changed to an `i32`? - // For now, we can cast it - result_i32 = arg_value.clamp(i32::MIN as i64, i32::MAX as i64) as i32; - } - GameRuleValue::Bool(value) => { - let arg_value = BoolArgConsumer::find_arg(args, ARG_NAME)?; - *value = arg_value; - output_value = arg_value.to_string(); - result_i32 = *value as i32; - } + if let GameRuleValue::Int(raw_value) = new_info.game_rules.get_mut(&self.0) { + *raw_value = arg_value; } - server.level_info.store(std::sync::Arc::new(new_info)); + context.server().level_info.store(Arc::new(new_info)); - let value_component = TextComponent::text(output_value); - sender.send_message(TextComponent::translate_cross( - "commands.gamerule.set", - "commands.gamerule.set", - [key, value_component], - )); - - Ok(result_i32) - } -} - -pub fn init_command_tree() -> CommandTree { - let mut command_tree = CommandTree::new(NAMES, DESCRIPTION); - let rule_registry = GameRuleRegistry::default(); - for rule in GameRule::all() { - let arg = match rule_registry.get(rule) { - GameRuleValue::Int(_) => argument(ARG_NAME, BoundedNumArgumentConsumer::::new()), - GameRuleValue::Bool(_) => argument(ARG_NAME, BoolArgConsumer), - }; - command_tree = command_tree.then( - literal(rule.to_string()) - .execute(QueryExecutor(rule.clone())) - .then(arg.execute(SetExecutor(rule.clone()))), + let value_component = TextComponent::text(arg_value.to_string()); + context.source.send_feedback( + TextComponent::translate_cross( + "commands.gamerule.set", + "commands.gamerule.set", + [key, value_component], + ), + true, ); + + Ok(arg_value.clamp(i32::MIN as i64, i32::MAX as i64) as i32) } - command_tree +} + +struct SetBoolExecutor(GameRule); + +impl CommandExecutor for SetBoolExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let key = TextComponent::text(self.0.to_string()); + let arg_value = BoolArgumentType::get(context, "value")?; + + let current_info = context.server().level_info.load(); + let mut new_info = (**current_info).clone(); + + if let GameRuleValue::Bool(raw_value) = new_info.game_rules.get_mut(&self.0) { + *raw_value = arg_value; + } + + context.server().level_info.store(Arc::new(new_info)); + + let value_component = TextComponent::text(arg_value.to_string()); + context.source.send_feedback( + TextComponent::translate_cross( + "commands.gamerule.set", + "commands.gamerule.set", + [key, value_component], + ), + true, + ); + + Ok(arg_value as i32) + } +} + +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + let mut cmd = command("gamerule", DESCRIPTION).requires(PERMISSION); + let rule_registry = GameRuleRegistry::default(); + + for rule in GameRule::all() { + let rule_literal = literal(rule.to_string()).executes(QueryExecutor(rule.clone())); + let branch = match rule_registry.get(rule) { + GameRuleValue::Int(_) => rule_literal.then( + argument("value", IntegerArgumentType::any()) + .executes(SetIntExecutor(rule.clone())), + ), + GameRuleValue::Bool(_) => rule_literal + .then(argument("value", BoolArgumentType).executes(SetBoolExecutor(rule.clone()))), + }; + cmd = cmd.then(branch); + } + + dispatcher.register(cmd); } diff --git a/crates/pumpkin/src/command/commands/give.rs b/crates/pumpkin/src/command/commands/give.rs index aab2c44c0..d4b0b5a96 100644 --- a/crates/pumpkin/src/command/commands/give.rs +++ b/crates/pumpkin/src/command/commands/give.rs @@ -1,49 +1,37 @@ +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; use pumpkin_util::text::hover::HoverEvent; -use crate::command::args::bounded_num::BoundedNumArgumentConsumer; -use crate::command::args::players::PlayersArgumentConsumer; -use crate::command::args::resource::item::ItemArgumentConsumer; -use crate::command::args::{ConsumedArgs, FindArg, FindArgDefaultName}; -use crate::command::tree::CommandTree; -use crate::command::tree::builder::{argument, argument_default_name}; -use crate::command::{CommandExecutor, CommandResult, CommandSender}; +use crate::command::argument_builder::{ArgumentBuilder, argument, command}; +use crate::command::argument_types::core::integer::IntegerArgumentType; +use crate::command::argument_types::entity::EntityArgumentType; +use crate::command::argument_types::item::ItemStackArgumentType; +use crate::command::context::command_context::CommandContext; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; use crate::entity::EntityBase; -const NAMES: [&str; 1] = ["give"]; - const DESCRIPTION: &str = "Give items to player(s)."; +const PERMISSION: &str = "minecraft:command.give"; -const ARG_ITEM: &str = "item"; - -const fn item_count_consumer() -> BoundedNumArgumentConsumer { - BoundedNumArgumentConsumer::new() - .name("count") - .min(1) - .max(i32::MAX) +struct GiveExecutor { + has_count: bool, } -struct Executor; - -impl CommandExecutor for Executor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let targets = PlayersArgumentConsumer.find_arg_default_name(args)?; - - let (item_name, parsed_stack) = ItemArgumentConsumer::find_arg(args, ARG_ITEM)?; +impl CommandExecutor for GiveExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let targets = EntityArgumentType::get_players(context, "targets")?; + let parsed_stack = ItemStackArgumentType::get(context, "item")?; let item = parsed_stack.item; - let item_count = match item_count_consumer().find_arg_default_name(args) { - Err(_) => 1, - Ok(Ok(count)) => count, - Ok(Err(err)) => return Err(err.into()), + let item_count = if self.has_count { + IntegerArgumentType::get(context, "count")? + } else { + 1 }; - for target in targets { + for target in &targets { let max_stack = i32::from(parsed_stack.get_max_stack_size()); let mut remaining = item_count; @@ -59,6 +47,7 @@ impl CommandExecutor for Executor { } } + let item_name = item.registry_key; let msg = if targets.len() == 1 { TextComponent::translate_cross( pumpkin_data::translation::java::COMMANDS_GIVE_SUCCESS_SINGLE, @@ -72,7 +61,7 @@ impl CommandExecutor for Executor { id: item_name.to_string().into(), count: Some(item_count.min(99)), }), - targets[0].get_display_name(), + targets[0].as_ref().get_display_name(), ], ) } else { @@ -92,18 +81,29 @@ impl CommandExecutor for Executor { ], ) }; - sender.send_message(msg); + context.source.send_feedback(msg, true); Ok(targets.len() as i32) } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).then( - argument_default_name(PlayersArgumentConsumer).then( - argument(ARG_ITEM, ItemArgumentConsumer) - .execute(Executor) - .then(argument_default_name(item_count_consumer()).execute(Executor)), +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + dispatcher.register( + command("give", DESCRIPTION).requires(PERMISSION).then( + argument("targets", EntityArgumentType::Players).then( + argument("item", ItemStackArgumentType) + .executes(GiveExecutor { has_count: false }) + .then( + argument("count", IntegerArgumentType::with_min(1)) + .executes(GiveExecutor { has_count: true }), + ), + ), ), - ) + ); } diff --git a/crates/pumpkin/src/command/commands/item.rs b/crates/pumpkin/src/command/commands/item.rs index ebd212244..d0052a8fc 100644 --- a/crates/pumpkin/src/command/commands/item.rs +++ b/crates/pumpkin/src/command/commands/item.rs @@ -3,103 +3,82 @@ use pumpkin_data::translation; use pumpkin_inventory::screen_handler::InventoryPlayer; use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer; use pumpkin_protocol::java::client::play::CSetContainerSlot; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; use pumpkin_util::text::hover::HoverEvent; use pumpkin_world::inventory::Inventory; -use crate::command::args::bounded_num::BoundedNumArgumentConsumer; -use crate::command::args::entities::EntitiesArgumentConsumer; -use crate::command::args::position_block::BlockPosArgumentConsumer; -use crate::command::args::resource::item::ItemArgumentConsumer; -use crate::command::args::slot::SlotArgumentConsumer; -use crate::command::args::{ConsumedArgs, FindArg, FindArgDefaultName}; -use crate::command::dispatcher::CommandError; -use crate::command::tree::CommandTree; -use crate::command::tree::builder::{argument, argument_default_name, literal}; -use crate::command::{CommandExecutor, CommandResult, CommandSender}; +use crate::command::argument_builder::{ + ArgumentBuilder, RequiredArgumentBuilder, argument, command, literal, +}; +use crate::command::argument_types::coordinates::block_pos::BlockPosArgumentType; +use crate::command::argument_types::core::integer::IntegerArgumentType; +use crate::command::argument_types::entity::EntityArgumentType; +use crate::command::argument_types::item::ItemStackArgumentType; +use crate::command::argument_types::slot::SlotArgumentType; +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}; -const NAMES: [&str; 1] = ["item"]; const DESCRIPTION: &str = "Modifies items in block or entity inventories."; +const PERMISSION: &str = "minecraft:command.item"; -const ARG_POS: &str = "pos"; -const ARG_SLOT: &str = "slot"; -const ARG_ITEM: &str = "item"; -const ARG_TARGETS: &str = "targets"; +const ERROR_NOT_CONTAINER: CommandErrorType<3> = CommandErrorType::new( + translation::java::COMMANDS_ITEM_TARGET_NOT_A_CONTAINER, + translation::java::COMMANDS_ITEM_TARGET_NOT_A_CONTAINER, +); -const fn count_consumer() -> BoundedNumArgumentConsumer { - BoundedNumArgumentConsumer::new() - .name("count") - .min(1) - .max(99) +const ERROR_NO_SUCH_SLOT: CommandErrorType<1> = CommandErrorType::new( + translation::java::COMMANDS_ITEM_TARGET_NO_SUCH_SLOT, + translation::java::COMMANDS_ITEM_TARGET_NO_SUCH_SLOT, +); + +struct BlockReplaceExecutor { + has_count: bool, } -struct BlockReplaceExecutor; - impl CommandExecutor for BlockReplaceExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let world = match sender { - CommandSender::Console | CommandSender::Rcon(_) | CommandSender::Dummy => { - let guard = server.worlds.load(); - guard - .first() - .cloned() - .ok_or(CommandError::InvalidRequirement)? - } - CommandSender::Player(player) => player.world(), - CommandSender::CommandBlock(_, w) => w.clone(), - }; - - let pos = BlockPosArgumentConsumer::find_loaded_arg(args, ARG_POS, &world)?; - let (slot, slot_name) = SlotArgumentConsumer::find_arg(args, ARG_SLOT)?; - let (item_name, parsed_stack) = ItemArgumentConsumer::find_arg(args, ARG_ITEM)?; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let world = context.source.world(); + let pos = BlockPosArgumentType::get_loaded_block_pos(context, "pos")?; + let slot = SlotArgumentType::get(context, "slot")?; + let parsed_stack = ItemStackArgumentType::get(context, "item")?; let item = parsed_stack.item; - let count = match count_consumer().find_arg_default_name(args) { - Ok(Ok(c)) => c, - Err(_) | Ok(Err(_)) => 1, + let count = if self.has_count { + IntegerArgumentType::get(context, "count")? + } else { + 1 }; let block_entity = world.get_block_entity(&pos).ok_or_else(|| { - CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_ITEM_TARGET_NOT_A_CONTAINER, - translation::java::COMMANDS_ITEM_TARGET_NOT_A_CONTAINER, - [ - TextComponent::text(pos.0.x.to_string()), - TextComponent::text(pos.0.y.to_string()), - TextComponent::text(pos.0.z.to_string()), - ], - )) + ERROR_NOT_CONTAINER.create_without_context( + TextComponent::text(pos.0.x.to_string()), + TextComponent::text(pos.0.y.to_string()), + TextComponent::text(pos.0.z.to_string()), + ) })?; let inventory = block_entity.get_inventory().ok_or_else(|| { - CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_ITEM_TARGET_NOT_A_CONTAINER, - translation::java::COMMANDS_ITEM_TARGET_NOT_A_CONTAINER, - [ - TextComponent::text(pos.0.x.to_string()), - TextComponent::text(pos.0.y.to_string()), - TextComponent::text(pos.0.z.to_string()), - ], - )) + ERROR_NOT_CONTAINER.create_without_context( + TextComponent::text(pos.0.x.to_string()), + TextComponent::text(pos.0.y.to_string()), + TextComponent::text(pos.0.z.to_string()), + ) })?; if slot >= inventory.size() { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_ITEM_TARGET_NO_SUCH_SLOT, - translation::java::COMMANDS_ITEM_TARGET_NO_SUCH_SLOT, - [TextComponent::text(slot_name)], - ))); + return Err( + ERROR_NO_SUCH_SLOT.create_without_context(TextComponent::text(slot.to_string())) + ); } let mut item_stack = parsed_stack.clone(); item_stack.item_count = count as u8; - inventory.set_stack(slot, item_stack); + let item_name = item.registry_key; let msg = TextComponent::translate_cross( translation::java::COMMANDS_ITEM_BLOCK_SET_SUCCESS, translation::java::COMMANDS_ITEM_BLOCK_SET_SUCCESS, @@ -116,36 +95,34 @@ impl CommandExecutor for BlockReplaceExecutor { }), ], ); - sender.send_message(msg); + context.source.send_feedback(msg, true); Ok(1) } } -struct EntityReplaceExecutor; +struct EntityReplaceExecutor { + has_count: bool, +} impl CommandExecutor for EntityReplaceExecutor { #[expect(clippy::too_many_lines)] - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let targets = EntitiesArgumentConsumer.find_arg_default_name(args)?; - let (mojang_slot, slot_name) = SlotArgumentConsumer::find_arg(args, ARG_SLOT)?; - let (item_name, parsed_stack) = ItemArgumentConsumer::find_arg(args, ARG_ITEM)?; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let targets = EntityArgumentType::get_entities(context, "targets")?; + let mojang_slot = SlotArgumentType::get(context, "slot")?; + let parsed_stack = ItemStackArgumentType::get(context, "item")?; let item = parsed_stack.item; - let count = match count_consumer().find_arg_default_name(args) { - Ok(Ok(c)) => c, - Err(_) | Ok(Err(_)) => 1, + let count = if self.has_count { + IntegerArgumentType::get(context, "count")? + } else { + 1 }; let mut modified_count = 0; let mut item_stack = parsed_stack.clone(); item_stack.item_count = count as u8; - for target in targets { + for target in &targets { if let Some(player) = target.get_player() { if let Some(player_arc) = player.world().get_player_by_uuid(player.gameprofile.id) { if (200..=226).contains(&mojang_slot) { @@ -246,19 +223,17 @@ impl CommandExecutor for EntityReplaceExecutor { } if modified_count == 0 { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_ITEM_TARGET_NO_SUCH_SLOT, - translation::java::COMMANDS_ITEM_TARGET_NO_SUCH_SLOT, - [TextComponent::text(slot_name)], - ))); + return Err(ERROR_NO_SUCH_SLOT + .create_without_context(TextComponent::text(mojang_slot.to_string()))); } + let item_name = item.registry_key; let msg = if targets.len() == 1 { TextComponent::translate_cross( translation::java::COMMANDS_ITEM_ENTITY_SET_SUCCESS_SINGLE, translation::java::COMMANDS_ITEM_ENTITY_SET_SUCCESS_SINGLE, [ - targets[0].get_display_name(), + targets[0].as_ref().get_display_name(), TextComponent::text("[") .add_child(item.translated_name()) .add_child(TextComponent::text("]")) @@ -284,64 +259,56 @@ impl CommandExecutor for EntityReplaceExecutor { ], ) }; - sender.send_message(msg); + context.source.send_feedback(msg, true); Ok(modified_count) } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).then( - literal("replace") - .then( - literal("block").then( - argument(ARG_POS, BlockPosArgumentConsumer).then( - argument(ARG_SLOT, SlotArgumentConsumer) - .then( - argument(ARG_ITEM, ItemArgumentConsumer) - .execute(BlockReplaceExecutor) - .then( - argument_default_name(count_consumer()) - .execute(BlockReplaceExecutor), - ), - ) - .then( - literal("with").then( - argument(ARG_ITEM, ItemArgumentConsumer) - .execute(BlockReplaceExecutor) - .then( - argument_default_name(count_consumer()) - .execute(BlockReplaceExecutor), - ), - ), - ), - ), - ), - ) - .then( - literal("entity").then( - argument(ARG_TARGETS, EntitiesArgumentConsumer).then( - argument(ARG_SLOT, SlotArgumentConsumer) - .then( - argument(ARG_ITEM, ItemArgumentConsumer) - .execute(EntityReplaceExecutor) - .then( - argument_default_name(count_consumer()) - .execute(EntityReplaceExecutor), - ), - ) - .then( - literal("with").then( - argument(ARG_ITEM, ItemArgumentConsumer) - .execute(EntityReplaceExecutor) - .then( - argument_default_name(count_consumer()) - .execute(EntityReplaceExecutor), - ), - ), - ), - ), - ), - ), - ) +fn block_item_node() -> RequiredArgumentBuilder { + argument("item", ItemStackArgumentType) + .executes(BlockReplaceExecutor { has_count: false }) + .then( + argument("count", IntegerArgumentType::new(1, 99)) + .executes(BlockReplaceExecutor { has_count: true }), + ) +} + +fn entity_item_node() -> RequiredArgumentBuilder { + argument("item", ItemStackArgumentType) + .executes(EntityReplaceExecutor { has_count: false }) + .then( + argument("count", IntegerArgumentType::new(1, 99)) + .executes(EntityReplaceExecutor { has_count: true }), + ) +} + +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + let block_node = literal("block").then( + argument("pos", BlockPosArgumentType).then( + argument("slot", SlotArgumentType) + .then(block_item_node()) + .then(literal("with").then(block_item_node())), + ), + ); + + let entity_node = literal("entity").then( + argument("targets", EntityArgumentType::Entities).then( + argument("slot", SlotArgumentType) + .then(entity_item_node()) + .then(literal("with").then(entity_item_node())), + ), + ); + + dispatcher.register( + command("item", DESCRIPTION) + .requires(PERMISSION) + .then(literal("replace").then(block_node).then(entity_node)), + ); } diff --git a/crates/pumpkin/src/command/commands/kick.rs b/crates/pumpkin/src/command/commands/kick.rs index 0bfa5606a..7c1146158 100644 --- a/crates/pumpkin/src/command/commands/kick.rs +++ b/crates/pumpkin/src/command/commands/kick.rs @@ -1,77 +1,81 @@ use pumpkin_data::translation; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; use pumpkin_util::text::color::NamedColor; -use crate::command::args::message::MsgArgConsumer; -use crate::command::args::players::PlayersArgumentConsumer; -use crate::command::args::{Arg, ConsumedArgs}; -use crate::command::tree::CommandTree; -use crate::command::tree::builder::argument; -use crate::command::{CommandError, CommandResult}; -use crate::command::{CommandExecutor, CommandSender}; +use crate::command::argument_builder::{ArgumentBuilder, argument, command}; +use crate::command::argument_types::core::string::StringArgumentType; +use crate::command::argument_types::entity::EntityArgumentType; +use crate::command::context::command_context::CommandContext; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; use crate::entity::EntityBase; use crate::net::DisconnectReason; -use CommandError::InvalidConsumption; -const NAMES: [&str; 1] = ["kick"]; const DESCRIPTION: &str = "Kicks the target player from the server."; +const PERMISSION: &str = "minecraft:command.kick"; -const ARG_TARGETS: &str = "targets"; +struct KickExecutor { + has_reason: bool, +} -const ARG_REASON: &str = "reason"; +impl CommandExecutor for KickExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let targets = EntityArgumentType::get_players(context, "targets")?; -struct Executor; - -impl CommandExecutor for Executor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::Players(targets)) = args.get(&ARG_TARGETS) else { - return Err(InvalidConsumption(Some(ARG_TARGETS.into()))); - }; - - let custom_reason = args.get(&ARG_REASON); - let reason = match custom_reason { - Some(Arg::Msg(r)) => TextComponent::text(r.clone()), - _ => TextComponent::translate_cross( + let reason = if self.has_reason { + let custom_reason = StringArgumentType::get(context, "reason")?; + TextComponent::text(custom_reason.to_string()) + } else { + TextComponent::translate_cross( translation::java::MULTIPLAYER_DISCONNECT_KICKED, translation::bedrock::DISCONNECT_KICKED, [], - ), + ) }; - for target in targets { + for target in &targets { target.kick(DisconnectReason::Kicked, &reason); - let feedback = if custom_reason.is_some() { + let feedback = if self.has_reason { TextComponent::translate_cross( translation::java::COMMANDS_KICK_SUCCESS, translation::bedrock::COMMANDS_KICK_SUCCESS_REASON, - [target.get_display_name(), reason.clone()], + [target.as_ref().get_display_name(), reason.clone()], ) } else { TextComponent::translate_cross( translation::java::COMMANDS_KICK_SUCCESS, translation::bedrock::COMMANDS_KICK_SUCCESS, - [target.get_display_name(), reason.clone()], + [target.as_ref().get_display_name(), reason.clone()], ) }; - sender.send_message(feedback.color_named(NamedColor::Blue)); + context + .source + .send_feedback(feedback.color_named(NamedColor::Blue), true); } Ok(targets.len() as i32) } } -// TODO: Permission -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).then( - argument(ARG_TARGETS, PlayersArgumentConsumer) - .execute(Executor) - .then(argument(ARG_REASON, MsgArgConsumer).execute(Executor)), - ) +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Three), + )); + + dispatcher.register( + command("kick", DESCRIPTION).requires(PERMISSION).then( + argument("targets", EntityArgumentType::Players) + .executes(KickExecutor { has_reason: false }) + .then( + argument("reason", StringArgumentType::GreedyPhrase) + .executes(KickExecutor { has_reason: true }), + ), + ), + ); } diff --git a/crates/pumpkin/src/command/commands/me.rs b/crates/pumpkin/src/command/commands/me.rs index 4424dfffc..c657a92a7 100644 --- a/crates/pumpkin/src/command/commands/me.rs +++ b/crates/pumpkin/src/command/commands/me.rs @@ -1,44 +1,28 @@ use pumpkin_data::world::EMOTE_COMMAND; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; -use crate::command::{ - CommandError, CommandExecutor, CommandResult, CommandSender, - args::{Arg, ConsumedArgs, message::MsgArgConsumer}, - tree::{CommandTree, builder::argument}, -}; -use CommandError::InvalidConsumption; - -const NAMES: [&str; 1] = ["me"]; +use crate::command::argument_builder::{ArgumentBuilder, argument, command}; +use crate::command::argument_types::core::string::StringArgumentType; +use crate::command::context::command_context::CommandContext; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; const DESCRIPTION: &str = "Broadcasts a narrative message about yourself."; +const PERMISSION: &str = "minecraft:command.me"; -const ARG_MESSAGE: &str = "action"; +struct MeExecutor; -struct Executor; +impl CommandExecutor for MeExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let msg = StringArgumentType::get(context, "action")?; + let sender = &context.source; + let server = sender.server(); -impl CommandExecutor for Executor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::Msg(msg)) = args.get(ARG_MESSAGE) else { - return Err(InvalidConsumption(Some(ARG_MESSAGE.into()))); - }; - - let Some(server_arc) = sender - .world_or_first(server) - .and_then(|w| w.server.upgrade()) - else { - return Err(CommandError::CommandFailed(TextComponent::text( - "Failed to get server instance", - ))); - }; - - server_arc.broadcast_message( - &TextComponent::text(msg.clone()), - &TextComponent::text(format!("{sender}")), + server.broadcast_message( + &TextComponent::text(msg.to_string()), + &context.source.display_name, EMOTE_COMMAND, None, ); @@ -47,7 +31,16 @@ impl CommandExecutor for Executor { } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION) - .then(argument(ARG_MESSAGE, MsgArgConsumer).execute(Executor)) +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Zero), + )); + + dispatcher.register( + command("me", DESCRIPTION) + .requires(PERMISSION) + .then(argument("action", StringArgumentType::GreedyPhrase).executes(MeExecutor)), + ); } diff --git a/crates/pumpkin/src/command/commands/mod.rs b/crates/pumpkin/src/command/commands/mod.rs index 4768e53f1..dc2116f24 100644 --- a/crates/pumpkin/src/command/commands/mod.rs +++ b/crates/pumpkin/src/command/commands/mod.rs @@ -1,5 +1,4 @@ use crate::command::node::dispatcher::CommandDispatcher; -use crate::command::tree::Command; use pumpkin_config::CommandsConfig; use pumpkin_util::{ PermissionLvl, @@ -57,12 +56,14 @@ mod raid; mod random; mod recipe; mod reload; +mod r#return; mod ride; mod rotate; mod saveall; mod saveoff; mod saveon; mod say; +mod schedule; mod scoreboard; mod seed; mod setblock; @@ -73,6 +74,7 @@ mod spectate; mod spreadplayers; mod stop; mod stopsound; +mod stopwatch; mod summon; mod tag; mod team; @@ -96,88 +98,11 @@ pub fn default_dispatcher( permission_manager: &PermissionManager, commands_config: &CommandsConfig, ) -> CommandDispatcher { - let mut dispatcher = crate::command::dispatcher::CommandDispatcher::default(); let registry = &permission_manager.registry; register_permissions(registry); - // Zero - dispatcher.register(pumpkin::init_command_tree(), "pumpkin:command.pumpkin"); - dispatcher.register(me::init_command_tree(), "minecraft:command.me"); - dispatcher.register(msg::init_command_tree(), "minecraft:command.msg"); - // Two - dispatcher.register( - worldborder::init_command_tree(), - "minecraft:command.worldborder", - ); - dispatcher.register(effect::init_command_tree(), "minecraft:command.effect"); - dispatcher.register(teleport::init_command_tree(), "minecraft:command.teleport"); - dispatcher.register(time::init_command_tree(), "minecraft:command.time"); - dispatcher.register(give::init_command_tree(), "minecraft:command.give"); - dispatcher.register(item::init_command_tree(), "minecraft:command.item"); - dispatcher.register(enchant::init_command_tree(), "minecraft:command.enchant"); - dispatcher.register(clear::init_command_tree(), "minecraft:command.clear"); - dispatcher.register(setblock::init_command_tree(), "minecraft:command.setblock"); - dispatcher.register(tps::init_command_tree(), "pumpkin:command.tps"); - dispatcher.register(fill::init_command_tree(), "minecraft:command.fill"); - dispatcher.register( - playsound::init_command_tree(), - "minecraft:command.playsound", - ); - dispatcher.register(tellraw::init_command_tree(), "minecraft:command.tellraw"); - dispatcher.register(title::init_command_tree(), "minecraft:command.title"); - dispatcher.register(summon::init_command_tree(), "minecraft:command.summon"); - dispatcher.register( - experience::init_command_tree(), - "minecraft:command.experience", - ); - dispatcher.register(weather::init_command_tree(), "minecraft:command.weather"); - dispatcher.register(particle::init_command_tree(), "minecraft:command.particle"); - dispatcher.register(rotate::init_command_tree(), "minecraft:command.rotate"); - dispatcher.register(damage::init_command_tree(), "minecraft:command.damage"); - dispatcher.register(bossbar::init_command_tree(), "minecraft:command.bossbar"); - dispatcher.register(gamemode::init_command_tree(), "minecraft:command.gamemode"); - dispatcher.register(gamerule::init_command_tree(), "minecraft:command.gamerule"); - dispatcher.register( - stopsound::init_command_tree(), - "minecraft:command.stopsound", - ); - dispatcher.register( - defaultgamemode::init_command_tree(), - "minecraft:command.defaultgamemode", - ); - dispatcher.register( - setworldspawn::init_command_tree(), - "minecraft:command.setworldspawn", - ); - dispatcher.register( - spawnpoint::init_command_tree(), - "minecraft:command.spawnpoint", - ); - dispatcher.register(spectate::init_command_tree(), "minecraft:command.spectate"); - dispatcher.register(data::init_command_tree(), "minecraft:command.data"); - dispatcher.register(waypoint::init_command_tree(), "minecraft:command.waypoint"); - dispatcher.register(raid::init_command_tree(), "minecraft:command.raid"); - // Three - dispatcher.register(deop::init_command_tree(), "minecraft:command.deop"); - dispatcher.register(kick::init_command_tree(), "minecraft:command.kick"); - dispatcher.register(plugin::init_command_tree(), "pumpkin:command.plugin"); - dispatcher.register(plugins::init_command_tree(), "pumpkin:command.plugins"); - dispatcher.register(ban::init_command_tree(), "minecraft:command.ban"); - dispatcher.register(banip::init_command_tree(), "minecraft:command.banip"); - dispatcher.register(pardon::init_command_tree(), "minecraft:command.pardon"); - dispatcher.register(pardonip::init_command_tree(), "minecraft:command.pardonip"); - dispatcher.register( - whitelist::init_command_tree(), - "minecraft:command.whitelist", - ); - dispatcher.register(transfer::init_command_tree(), "minecraft:command.transfer"); - - let mut dispatcher = { - let mut wrapper_dispatcher = CommandDispatcher::new(); - wrapper_dispatcher.fallback_dispatcher = dispatcher; - wrapper_dispatcher - }; + let mut dispatcher = CommandDispatcher::new(); say::register(&mut dispatcher, registry); banlist::register(&mut dispatcher, registry); @@ -208,6 +133,8 @@ pub fn default_dispatcher( tag::register(&mut dispatcher, registry); tick::register(&mut dispatcher, registry); advancement::register(&mut dispatcher, registry); + data::register(&mut dispatcher, registry); + stopwatch::register(&mut dispatcher, registry); trigger::register(&mut dispatcher, registry); scoreboard::register(&mut dispatcher, registry); team::register(&mut dispatcher, registry); @@ -216,7 +143,52 @@ pub fn default_dispatcher( attribute::register(&mut dispatcher, registry); datapack::register(&mut dispatcher, registry); function::register(&mut dispatcher, registry); + r#return::register(&mut dispatcher, registry); + schedule::register(&mut dispatcher, registry); fetchprofile::register(&mut dispatcher, registry); + worldborder::register(&mut dispatcher, registry); + particle::register(&mut dispatcher, registry); + playsound::register(&mut dispatcher, registry); + fill::register(&mut dispatcher, registry); + clear::register(&mut dispatcher, registry); + pumpkin::register(&mut dispatcher, registry); + me::register(&mut dispatcher, registry); + msg::register(&mut dispatcher, registry); + tps::register(&mut dispatcher, registry); + transfer::register(&mut dispatcher, registry); + gamemode::register(&mut dispatcher, registry); + defaultgamemode::register(&mut dispatcher, registry); + weather::register(&mut dispatcher, registry); + time::register(&mut dispatcher, registry); + teleport::register(&mut dispatcher, registry); + setworldspawn::register(&mut dispatcher, registry); + spawnpoint::register(&mut dispatcher, registry); + spectate::register(&mut dispatcher, registry); + setblock::register(&mut dispatcher, registry); + give::register(&mut dispatcher, registry); + item::register(&mut dispatcher, registry); + enchant::register(&mut dispatcher, registry); + effect::register(&mut dispatcher, registry); + summon::register(&mut dispatcher, registry); + damage::register(&mut dispatcher, registry); + rotate::register(&mut dispatcher, registry); + tellraw::register(&mut dispatcher, registry); + title::register(&mut dispatcher, registry); + experience::register(&mut dispatcher, registry); + bossbar::register(&mut dispatcher, registry); + gamerule::register(&mut dispatcher, registry); + stopsound::register(&mut dispatcher, registry); + waypoint::register(&mut dispatcher, registry); + raid::register(&mut dispatcher, registry); + deop::register(&mut dispatcher, registry); + kick::register(&mut dispatcher, registry); + plugin::register(&mut dispatcher, registry); + plugins::register(&mut dispatcher, registry); + ban::register(&mut dispatcher, registry); + banip::register(&mut dispatcher, registry); + pardon::register(&mut dispatcher, registry); + pardonip::register(&mut dispatcher, registry); + whitelist::register(&mut dispatcher, registry); apply_command_overrides(&mut dispatcher, registry, commands_config); @@ -256,22 +228,15 @@ fn apply_command_overrides( if !settings.enabled { // If the owner named an alias (e.g. `tp` for `teleport`), turn off // the whole command, not just that one alias. - let primary = match dispatcher.fallback_dispatcher.commands.get(&name) { - Some(Command::Alias(target)) => target.clone(), - _ => name.clone(), - }; + let primary = dispatcher.primary_command_name(&name); dispatcher.disable_command(name.clone()); dispatcher.disable_command(primary.clone()); // Node-based commands keep their aliases as redirecting root nodes, - // so flag those too. (Legacy aliases are handled by the unregister - // below, which removes them from the dispatcher outright.) + // so flag those too. for alias in dispatcher.tree_alias_names(&primary) { dispatcher.disable_command(alias); } - // Unregistering the primary name cascades to every alias in the - // legacy dispatcher. - dispatcher.fallback_dispatcher.unregister(&primary); info!("The /{primary} command has been turned off in the configuration"); // A disabled command can never be run, so its permission level is // irrelevant; skip the rest. @@ -307,18 +272,12 @@ fn apply_command_overrides( /// Finds the permission node associated with a command name. /// -/// Legacy commands record their node in the dispatcher directly. Node-based -/// commands do not, but they follow the `:command.` convention, -/// so we fall back to probing the registry for those. +/// Follows the `:command.` convention by probing the registry. fn resolve_permission_node( - dispatcher: &CommandDispatcher, + _dispatcher: &CommandDispatcher, registry: &PermissionRegistry, name: &str, ) -> Option { - if let Some(node) = dispatcher.fallback_dispatcher.permissions.get(name) { - return Some(node.clone()); - } - for namespace in ["minecraft", "pumpkin"] { let candidate = format!("{namespace}:command.{name}"); if registry.get_permission(&candidate).is_some() { @@ -330,15 +289,6 @@ fn resolve_permission_node( } fn register_permissions(registry: &PermissionRegistry) { - // Register level 0 permissions (allowed by default) - register_level_0_permissions(registry); - - // Register level 2 permissions (OP level 2) - register_level_2_permissions(registry); - - // Register level 3 permissions (OP level 3) - register_level_3_permissions(registry); - // Register our entity selector permission as well. registry .register_permission(Permission::new( @@ -349,320 +299,6 @@ fn register_permissions(registry: &PermissionRegistry) { .unwrap_or_else(|e| tracing::warn!("{e}")); } -fn register_level_0_permissions(registry: &PermissionRegistry) { - // Register permissions for builtin commands that are allowed for everyone - registry - .register_permission(Permission::new( - "pumpkin:command.pumpkin", - "Shows information about the Pumpkin server", - PermissionDefault::Allow, - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.me", - "Broadcasts a narrative message about the player", - PermissionDefault::Allow, - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.msg", - "Sends a private message to another player", - PermissionDefault::Allow, - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); -} - -#[expect(clippy::too_many_lines)] -fn register_level_2_permissions(registry: &PermissionRegistry) { - // Register permissions for commands with PermissionLvl::Two - registry - .register_permission(Permission::new( - "minecraft:command.worldborder", - "Manages the world border", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.effect", - "Adds or removes status effects", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.teleport", - "Teleports entities to other locations", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.time", - "Changes or queries the world's game time", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.waypoint", - "List or modify waypoints", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.give", - "Gives an item to a player", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.item", - "Replace items in inventories", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.clear", - "Clears items from player inventory", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.setblock", - "Changes a block to another block", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.fill", - "Fills a region with a specific block", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.playsound", - "Plays a sound to players", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.tellraw", - "Displays a JSON message to players", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.title", - "Controls screen titles displayed to players", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.summon", - "Summons an entity", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.experience", - "Adds, removes or queries player experience", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.weather", - "Sets the weather in the server", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.particle", - "Creates particles in the world", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.rotate", - "Changes the rotation of an entity", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.damage", - "Damages entities", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.bossbar", - "Creates and manages boss bars", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.gamemode", - "Sets a player's game mode", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.gamerule", - "Sets a player's game mode", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.stopsound", - "Stops sounds from playing", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.defaultgamemode", - "Sets the default game mode for new players", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.data", - "Query and modify data of entities and blocks", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.enchant", - "Adds an enchantment to a player's selected item, subject to the same restrictions as an anvil. Also works on any mob or entity holding a weapon/tool/armor in its main hand.", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.spawnpoint", - "Sets the spawn point for a player", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.spectate", - "Allows a player to spectate another entity", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "pumpkin:command.tps", - "Displays the server TPS and MSPT", - PermissionDefault::Op(PermissionLvl::Two), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); -} - -fn register_level_3_permissions(registry: &PermissionRegistry) { - // Register permissions for commands with PermissionLvl::Three - registry - .register_permission(Permission::new( - "minecraft:command.setworldspawn", - "Sets the world spawn point", - PermissionDefault::Op(PermissionLvl::Three), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.deop", - "Revokes operator status from a player", - PermissionDefault::Op(PermissionLvl::Three), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.kick", - "Removes players from the server", - PermissionDefault::Op(PermissionLvl::Three), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "pumpkin:command.plugin", - "Manages server plugins", - PermissionDefault::Op(PermissionLvl::Three), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "pumpkin:command.plugins", - "Lists all plugins loaded on the server", - PermissionDefault::Op(PermissionLvl::Three), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.ban", - "Adds players to banlist", - PermissionDefault::Op(PermissionLvl::Three), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.banip", - "Adds IP addresses to banlist", - PermissionDefault::Op(PermissionLvl::Three), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.pardon", - "Removes entries from the player banlist", - PermissionDefault::Op(PermissionLvl::Three), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.pardonip", - "Removes entries from the IP banlist", - PermissionDefault::Op(PermissionLvl::Three), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.whitelist", - "Manages server whitelist", - PermissionDefault::Op(PermissionLvl::Three), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); - registry - .register_permission(Permission::new( - "minecraft:command.transfer", - "Transfers the player to another server", - PermissionDefault::Op(PermissionLvl::Three), - )) - .unwrap_or_else(|e| tracing::warn!("{e}")); -} - #[cfg(test)] mod override_tests { use pumpkin_config::{CommandOverride, CommandsConfig}; @@ -694,18 +330,12 @@ mod override_tests { #[test] fn disabling_a_command_removes_and_hides_it() { let mut commands = CommandsConfig::default(); - // `gamemode` lives on the legacy dispatcher; disabling it should remove - // it there and flag it on the wrapper. disabled(&mut commands, "gamemode"); let manager = PermissionManager::new(); let dispatcher = default_dispatcher(&manager, &commands); assert!(dispatcher.is_disabled("gamemode")); - assert!( - dispatcher.fallback_dispatcher.get_tree("gamemode").is_err(), - "disabled command should be unregistered from the legacy dispatcher" - ); assert!( !dispatcher.is_disabled("give"), "untouched commands stay on" @@ -724,8 +354,6 @@ mod override_tests { assert!(dispatcher.is_disabled("tp")); assert!(dispatcher.is_disabled("teleport")); - assert!(dispatcher.fallback_dispatcher.get_tree("tp").is_err()); - assert!(dispatcher.fallback_dispatcher.get_tree("teleport").is_err()); } #[test] @@ -753,7 +381,7 @@ mod override_tests { let dispatcher = default_dispatcher(&manager, &commands); assert!(!dispatcher.is_disabled("notacommand")); - assert!(dispatcher.fallback_dispatcher.get_tree("gamemode").is_ok()); + assert!(dispatcher.has_command("gamemode")); } #[test] diff --git a/crates/pumpkin/src/command/commands/msg.rs b/crates/pumpkin/src/command/commands/msg.rs index a5e9d490e..a9191365d 100644 --- a/crates/pumpkin/src/command/commands/msg.rs +++ b/crates/pumpkin/src/command/commands/msg.rs @@ -1,64 +1,74 @@ use pumpkin_data::world::{MSG_COMMAND_INCOMING, MSG_COMMAND_OUTGOING}; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; -use crate::command::{ - CommandError, CommandExecutor, CommandResult, CommandSender, - args::{ - Arg, ConsumedArgs, FindArgDefaultName, message::MsgArgConsumer, - players::PlayersArgumentConsumer, - }, - tree::{ - CommandTree, - builder::{argument, argument_default_name}, - }, -}; +use crate::command::argument_builder::{ArgumentBuilder, argument, command}; +use crate::command::argument_types::core::string::StringArgumentType; +use crate::command::argument_types::entity::EntityArgumentType; +use crate::command::context::command_context::CommandContext; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; use crate::entity::EntityBase; -use CommandError::InvalidConsumption; const NAMES: [&str; 3] = ["msg", "tell", "w"]; - const DESCRIPTION: &str = "Sends a private message to one or more players."; +const PERMISSION: &str = "minecraft:command.msg"; -const ARG_MESSAGE: &str = "message"; +struct MsgExecutor; -struct Executor; +impl CommandExecutor for MsgExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let targets = EntityArgumentType::get_players(context, "targets")?; + let msg = StringArgumentType::get(context, "message")?; -impl CommandExecutor for Executor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::Msg(msg)) = args.get(ARG_MESSAGE) else { - return Err(InvalidConsumption(Some(ARG_MESSAGE.into()))); - }; - let targets = PlayersArgumentConsumer.find_arg_default_name(args)?; - let player = sender.as_player().ok_or(CommandError::InvalidRequirement)?; + let sender_name = &context.source.display_name; + let msg_text = TextComponent::text(msg.to_string()); - for target in targets { - let msg_text = TextComponent::text(msg.clone()); - player.send_message( - &msg_text, - MSG_COMMAND_OUTGOING, - &player.get_display_name(), - Some(&target.get_display_name()), - ); - target.send_message( - &msg_text, - MSG_COMMAND_INCOMING, - &player.get_display_name(), - Some(&target.get_display_name()), - ); + if let Some(player) = context.source.player_or_none() { + for target in &targets { + player.send_message( + &msg_text, + MSG_COMMAND_OUTGOING, + &player.get_display_name(), + Some(&target.get_display_name()), + ); + target.send_message( + &msg_text, + MSG_COMMAND_INCOMING, + &player.get_display_name(), + Some(&target.get_display_name()), + ); + } + } else { + for target in &targets { + target.send_message( + &msg_text, + MSG_COMMAND_INCOMING, + sender_name, + Some(&target.get_display_name()), + ); + } } Ok(targets.len() as i32) } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).then( - argument_default_name(PlayersArgumentConsumer) - .then(argument(ARG_MESSAGE, MsgArgConsumer).execute(Executor)), - ) +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Zero), + )); + + for name in NAMES { + dispatcher.register( + command(name, DESCRIPTION).requires(PERMISSION).then( + argument("targets", EntityArgumentType::Players).then( + argument("message", StringArgumentType::GreedyPhrase).executes(MsgExecutor), + ), + ), + ); + } } diff --git a/crates/pumpkin/src/command/commands/pardon.rs b/crates/pumpkin/src/command/commands/pardon.rs index 3f5c3f796..cdfdf5304 100644 --- a/crates/pumpkin/src/command/commands/pardon.rs +++ b/crates/pumpkin/src/command/commands/pardon.rs @@ -1,39 +1,34 @@ -use crate::{ - command::{ - CommandError, CommandExecutor, CommandResult, CommandSender, - args::{ - Arg, ConsumedArgs, - gameprofile::{GameProfileSuggestionMode, GameProfilesArgumentConsumer}, - }, - tree::{CommandTree, builder::argument}, - }, - data::SaveJSONConfiguration, -}; -use CommandError::InvalidConsumption; +use pumpkin_data::translation; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; -const NAMES: [&str; 1] = ["pardon"]; +use crate::command::argument_builder::{ArgumentBuilder, argument, command}; +use crate::command::argument_types::game_profile::GameProfileArgumentType; +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 crate::data::SaveJSONConfiguration; + const DESCRIPTION: &str = "unbans a player"; +const PERMISSION: &str = "minecraft:command.pardon"; -const ARG_TARGET: &str = "targets"; +const ERROR_PARDON_FAILED: CommandErrorType<1> = CommandErrorType::new( + translation::java::COMMANDS_PARDON_FAILED, + translation::bedrock::COMMANDS_UNBAN_FAILED, +); -struct Executor; - -impl CommandExecutor for Executor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::GameProfiles(targets)) = args.get(&ARG_TARGET) else { - return Err(InvalidConsumption(Some(ARG_TARGET.into()))); - }; +struct PardonExecutor; +impl CommandExecutor for PardonExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let targets = GameProfileArgumentType::get(context, "targets")?; + let server = context.source.server(); let mut lock = server.data.banned_player_list.write().unwrap(); let mut successes = 0; - for target in targets { + for target in &targets { let idx = lock .banned_players .iter() @@ -41,11 +36,14 @@ impl CommandExecutor for Executor { if let Some(idx) = idx { lock.banned_players.remove(idx); - sender.send_message(TextComponent::translate_cross( - pumpkin_data::translation::java::COMMANDS_PARDON_SUCCESS, - pumpkin_data::translation::bedrock::COMMANDS_UNBAN_SUCCESS, - [TextComponent::text(target.name.clone())], - )); + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_PARDON_SUCCESS, + translation::bedrock::COMMANDS_UNBAN_SUCCESS, + [TextComponent::text(target.name.clone())], + ), + true, + ); successes += 1; } } @@ -57,22 +55,21 @@ impl CommandExecutor for Executor { let err_target = targets .first() .map_or_else(String::new, |first_target| first_target.name.clone()); - Err(CommandError::CommandFailed(TextComponent::translate_cross( - pumpkin_data::translation::java::COMMANDS_PARDON_FAILED, - pumpkin_data::translation::bedrock::COMMANDS_UNBAN_FAILED, - [TextComponent::text(err_target)], - ))) + Err(ERROR_PARDON_FAILED.create_without_context(TextComponent::text(err_target))) } } } -#[allow(clippy::too_many_lines)] -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).then( - argument( - ARG_TARGET, - GameProfilesArgumentConsumer::new(GameProfileSuggestionMode::BannedNames, false), - ) - .execute(Executor), - ) +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Three), + )); + + dispatcher.register( + command("pardon", DESCRIPTION) + .requires(PERMISSION) + .then(argument("targets", GameProfileArgumentType).executes(PardonExecutor)), + ); } diff --git a/crates/pumpkin/src/command/commands/pardonip.rs b/crates/pumpkin/src/command/commands/pardonip.rs index 36727c003..c8c22229a 100644 --- a/crates/pumpkin/src/command/commands/pardonip.rs +++ b/crates/pumpkin/src/command/commands/pardonip.rs @@ -1,42 +1,41 @@ -use std::{net::IpAddr, str::FromStr}; +use std::net::IpAddr; +use std::str::FromStr; -use crate::{ - command::{ - CommandError, CommandExecutor, CommandResult, CommandSender, - args::{Arg, ConsumedArgs, simple::SimpleArgConsumer}, - tree::{CommandTree, builder::argument}, - }, - data::SaveJSONConfiguration, -}; -use CommandError::InvalidConsumption; +use pumpkin_data::translation; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; -const NAMES: [&str; 1] = ["pardon-ip"]; +use crate::command::argument_builder::{ArgumentBuilder, argument, command}; +use crate::command::argument_types::core::string::StringArgumentType; +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 crate::data::SaveJSONConfiguration; + const DESCRIPTION: &str = "unbans a ip"; +const PERMISSION: &str = "minecraft:command.pardonip"; -const ARG_TARGET: &str = "ip"; +const ERROR_PARDONIP_INVALID: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_PARDONIP_INVALID, + translation::bedrock::COMMANDS_UNBANIP_INVALID, +); -struct Executor; +const ERROR_PARDONIP_FAILED: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_PARDONIP_FAILED, + translation::java::COMMANDS_PARDONIP_FAILED, +); -impl CommandExecutor for Executor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::Simple(target)) = args.get(&ARG_TARGET) else { - return Err(InvalidConsumption(Some(ARG_TARGET.into()))); - }; +struct PardonIpExecutor; - let Ok(ip) = IpAddr::from_str(target) else { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - pumpkin_data::translation::java::COMMANDS_PARDONIP_INVALID, - pumpkin_data::translation::bedrock::COMMANDS_UNBANIP_INVALID, - [], - ))); - }; +impl CommandExecutor for PardonIpExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let target = StringArgumentType::get(context, "ip")?; + let ip = IpAddr::from_str(target) + .map_err(|_| ERROR_PARDONIP_INVALID.create_without_context())?; + let server = context.source.server(); let mut lock = server .data .banned_ip_list @@ -48,20 +47,17 @@ impl CommandExecutor for Executor { .iter() .position(|entry| entry.ip == ip) .map_or_else( - || { - Err(CommandError::CommandFailed(TextComponent::translate_cross( - pumpkin_data::translation::java::COMMANDS_PARDONIP_FAILED, - pumpkin_data::translation::java::COMMANDS_PARDONIP_FAILED, - [], - ))) - }, + || Err(ERROR_PARDONIP_FAILED.create_without_context()), |idx| { lock.banned_ips.remove(idx); - sender.send_message(TextComponent::translate_cross( - pumpkin_data::translation::java::COMMANDS_PARDONIP_SUCCESS, - pumpkin_data::translation::bedrock::COMMANDS_UNBANIP_SUCCESS, - [TextComponent::text(ip.to_string())], - )); + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_PARDONIP_SUCCESS, + translation::bedrock::COMMANDS_UNBANIP_SUCCESS, + [TextComponent::text(ip.to_string())], + ), + true, + ); Ok(1) }, ); @@ -72,7 +68,16 @@ impl CommandExecutor for Executor { } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION) - .then(argument(ARG_TARGET, SimpleArgConsumer).execute(Executor)) +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Three), + )); + + let cmd = command("pardon-ip", DESCRIPTION) + .requires(PERMISSION) + .then(argument("ip", StringArgumentType::SingleWord).executes(PardonIpExecutor)); + + dispatcher.register_with_aliases(cmd, &["pardonip"]); } diff --git a/crates/pumpkin/src/command/commands/particle.rs b/crates/pumpkin/src/command/commands/particle.rs index ee3208ec1..65dd13f73 100644 --- a/crates/pumpkin/src/command/commands/particle.rs +++ b/crates/pumpkin/src/command/commands/particle.rs @@ -1,114 +1,239 @@ -use crate::block::entities::BlockEntity; -use crate::command::{ - CommandError, CommandExecutor, CommandResult, CommandSender, - args::{ - ConsumedArgs, FindArg, bounded_num::BoundedNumArgumentConsumer, - position_3d::Position3DArgumentConsumer, resource::particle::ParticleArgumentConsumer, - }, - tree::{CommandTree, builder::argument}, -}; -use crate::entity::EntityBase; -use pumpkin_util::{math::vector3::Vector3, text::TextComponent}; -const NAMES: [&str; 1] = ["particle"]; +use std::sync::Arc; -const DESCRIPTION: &str = "Spawns a Particle at position."; +use pumpkin_data::particle::Particle; +use pumpkin_data::translation; +use pumpkin_protocol::codec::var_int::VarInt; +use pumpkin_protocol::java::client::play::CParticle; +use pumpkin_util::PermissionLvl; +use pumpkin_util::math::vector3::Vector3; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; +use pumpkin_util::text::TextComponent; -const ARG_NAME: &str = "name"; +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::coordinates::vec3::Vec3ArgumentType; +use crate::command::argument_types::core::float::FloatArgumentType; +use crate::command::argument_types::core::integer::IntegerArgumentType; +use crate::command::argument_types::entity::EntityArgumentType; +use crate::command::argument_types::particle::ParticleArgumentType; +use crate::command::context::command_context::CommandContext; +use crate::command::context::command_source::CommandSource; +use crate::command::errors::command_syntax_error::CommandSyntaxError; +use crate::command::errors::error_types::CommandErrorType; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; +use crate::entity::player::Player; -const ARG_POS: &str = "pos"; -const ARG_DELTA: &str = "delta"; -const ARG_SPEED: &str = "speed"; -const ARG_COUNT: &str = "count"; +const DESCRIPTION: &str = "Creates particles in the world."; +const PERMISSION: &str = "minecraft:command.particle"; -struct Executor; +const ERROR_FAILED: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_PARTICLE_FAILED, + translation::java::COMMANDS_PARTICLE_FAILED, +); -impl CommandExecutor for Executor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let particle = ParticleArgumentConsumer::find_arg(args, ARG_NAME)?; - let pos = Position3DArgumentConsumer::find_arg(args, ARG_POS); - let delta = Position3DArgumentConsumer::find_arg(args, ARG_DELTA); - let speed = BoundedNumArgumentConsumer::::find_arg(args, ARG_SPEED); - let count = BoundedNumArgumentConsumer::::find_arg(args, ARG_COUNT); +#[allow(clippy::too_many_arguments)] +fn send_particles( + source: &CommandSource, + particle: Particle, + pos: Vector3, + delta: Vector3, + speed: f32, + count: i32, + force: bool, + viewers: Option>>, +) -> Result { + let world = source.world().clone(); + let players: Vec> = viewers.unwrap_or_else(|| world.players.load().to_vec()); - let delta = delta.unwrap_or(Vector3::new(0.0, 0.0, 0.0)); - let delta: Vector3 = Vector3::new(delta.x as f32, delta.y as f32, delta.z as f32); - let speed = speed.unwrap_or(Ok(0.0))?; - let count = count.unwrap_or(Ok(0))?; - let (world, pos) = match sender { - CommandSender::Console | CommandSender::Rcon(_) | CommandSender::Dummy => { - let guard = server.worlds.load(); - let world = guard - .first() - .cloned() - .ok_or(CommandError::InvalidRequirement)?; - // default position for spawning a player, in this case for particle - let pos = { - let info = &world.level_info.load(); - // default position for spawning a player, in this case for mob - pos.unwrap_or(Vector3::new( - f64::from(info.spawn_x) + 0.5, - f64::from(info.spawn_y) + 1.0, - f64::from(info.spawn_z) + 0.5, - )) + let offset = Vector3::new(delta.x as f32, delta.y as f32, delta.z as f32); + let packet = CParticle::new( + force, + false, + pos, + offset, + speed, + count, + VarInt(i32::from(particle.to_id())), + &[], + ); + + let max_dist_sq = if force { 512.0 * 512.0 } else { 32.0 * 32.0 }; + let mut sent_count = 0; + + for player in &players { + if !Arc::ptr_eq(&player.world(), &world) { + continue; + } + let player_pos = player.position(); + let dx = player_pos.x - pos.x; + let dy = player_pos.y - pos.y; + let dz = player_pos.z - pos.z; + let dist_sq = dx * dx + dy * dy + dz * dz; + if dist_sq <= max_dist_sq { + player.try_send_client_packet(&packet); + sent_count += 1; + } + } + + if sent_count == 0 { + return Err(ERROR_FAILED.create_without_context()); + } + + let particle_name = format!("{particle:?}").to_lowercase(); + source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_PARTICLE_SUCCESS, + translation::java::COMMANDS_PARTICLE_SUCCESS, + [TextComponent::text(particle_name)], + ), + true, + ); + + Ok(sent_count) +} + +#[derive(Clone, Copy)] +enum ParticleMode { + Simple, + WithPos, + Full { force: bool, has_viewers: bool }, +} + +struct ParticleExecutor { + mode: ParticleMode, +} + +impl CommandExecutor for ParticleExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let particle = ParticleArgumentType::get(context, "name")?; + + match self.mode { + ParticleMode::Simple => { + let pos = context.source.position; + send_particles( + &context.source, + particle, + pos, + Vector3::new(0.0, 0.0, 0.0), + 0.0, + 0, + false, + None, + ) + } + ParticleMode::WithPos => { + let pos = Vec3ArgumentType::get_vector3(context, "pos")?; + send_particles( + &context.source, + particle, + pos, + Vector3::new(0.0, 0.0, 0.0), + 0.0, + 0, + false, + None, + ) + } + ParticleMode::Full { force, has_viewers } => { + let pos = Vec3ArgumentType::get_vector3(context, "pos")?; + let delta = Vec3ArgumentType::get_vector3(context, "delta")?; + let speed = FloatArgumentType::get(context, "speed")?; + let count = IntegerArgumentType::get(context, "count")?; + let viewers = if has_viewers { + Some(EntityArgumentType::get_players(context, "viewers")?) + } else { + None }; - (world, pos) + send_particles( + &context.source, + particle, + pos, + delta, + speed, + count, + force, + viewers, + ) } - CommandSender::Player(player) => { - let pos = pos.unwrap_or(player.get_entity().pos.load()); - - (player.world(), pos) - } - CommandSender::CommandBlock(c, w) => (w.clone(), c.get_position().to_centered_f64()), - }; - - world.spawn_particle(pos, delta, speed, count, *particle); - - sender.send_message(TextComponent::translate_cross( - pumpkin_data::translation::java::COMMANDS_PARTICLE_SUCCESS, - pumpkin_data::translation::bedrock::COMMANDS_PARTICLE_SUCCESS, - [ - TextComponent::text(format!("{particle:?}")), - TextComponent::text(count.to_string()), - ], - )); - - // TODO: Add `viewers` arguments and change the logic for this result - Ok(1) + } } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).then( - argument(ARG_NAME, ParticleArgumentConsumer) - .execute(Executor) - .then( - argument(ARG_POS, Position3DArgumentConsumer) - .execute(Executor) - .then( - argument(ARG_DELTA, Position3DArgumentConsumer) - .execute(Executor) - .then( - argument( - ARG_SPEED, - BoundedNumArgumentConsumer::::new().min(0.0), - ) - .execute(Executor) - .then( - argument( - ARG_COUNT, - BoundedNumArgumentConsumer::::new().min(0), - ) - .execute(Executor), +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + dispatcher.register( + command("particle", DESCRIPTION).requires(PERMISSION).then( + argument("name", ParticleArgumentType) + .executes(ParticleExecutor { + mode: ParticleMode::Simple, + }) + .then( + argument("pos", Vec3ArgumentType::Default) + .executes(ParticleExecutor { + mode: ParticleMode::WithPos, + }) + .then( + argument("delta", Vec3ArgumentType::Uncorrected).then( + argument("speed", FloatArgumentType::with_min(0.0)).then( + argument("count", IntegerArgumentType::with_min(0)) + .executes(ParticleExecutor { + mode: ParticleMode::Full { + force: false, + has_viewers: false, + }, + }) + .then( + literal("force") + .executes(ParticleExecutor { + mode: ParticleMode::Full { + force: true, + has_viewers: false, + }, + }) + .then( + argument( + "viewers", + EntityArgumentType::Players, + ) + .executes(ParticleExecutor { + mode: ParticleMode::Full { + force: true, + has_viewers: true, + }, + }), + ), + ) + .then( + literal("normal") + .executes(ParticleExecutor { + mode: ParticleMode::Full { + force: false, + has_viewers: false, + }, + }) + .then( + argument( + "viewers", + EntityArgumentType::Players, + ) + .executes(ParticleExecutor { + mode: ParticleMode::Full { + force: false, + has_viewers: true, + }, + }), + ), + ), ), ), - ), - ), - // TODO: Add NBT - ) + ), + ), + ), + ); } diff --git a/crates/pumpkin/src/command/commands/playsound.rs b/crates/pumpkin/src/command/commands/playsound.rs index 2470b034d..67c731aad 100644 --- a/crates/pumpkin/src/command/commands/playsound.rs +++ b/crates/pumpkin/src/command/commands/playsound.rs @@ -1,204 +1,337 @@ use std::sync::Arc; -use pumpkin_data::sound::SoundCategory; +use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_data::translation; +use pumpkin_protocol::IdOr; +use pumpkin_protocol::java::client::play::CSoundEffect; +use pumpkin_util::PermissionLvl; +use pumpkin_util::identifier::Identifier; +use pumpkin_util::math::vector3::Vector3; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; -use rand::{RngExt, rng}; -use crate::entity::EntityBase; -use crate::{ - command::{ - CommandExecutor, CommandResult, CommandSender, - args::{ - Arg, ConsumedArgs, FindArg, bounded_num::BoundedNumArgumentConsumer, - players::PlayersArgumentConsumer, position_3d::Position3DArgumentConsumer, - sound::SoundArgumentConsumer, sound_category::SoundCategoryArgumentConsumer, - }, - dispatcher::CommandError, - tree::{CommandTree, builder::argument}, - }, - entity::player::Player, +use crate::command::argument_builder::{ + ArgumentBuilder, LiteralArgumentBuilder, argument, command, literal, }; +use crate::command::argument_types::coordinates::vec3::Vec3ArgumentType; +use crate::command::argument_types::core::float::FloatArgumentType; +use crate::command::argument_types::entity::EntityArgumentType; +use crate::command::argument_types::identifier::IdentifierArgumentType; +use crate::command::context::command_context::CommandContext; +use crate::command::context::command_source::CommandSource; +use crate::command::errors::command_syntax_error::CommandSyntaxError; +use crate::command::errors::error_types::CommandErrorType; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; +use crate::entity::player::Player; -/// Command: playsound [] [] [] [] [] [] -/// -/// Plays a sound at specified position for target players. -/// - sound: The sound resource location to play -/// - source: Sound category (master, music, record, etc.) -/// - targets: Players who will hear the sound -/// - pos: Position to play the sound from -/// - volume: Sound volume (>=0, default: 1.0) -/// - pitch: Sound pitch (0.5-2.0, default: 1.0) -/// - minVolume: Minimum volume for distant players (0.0-1.0, default: 0.0) -const NAMES: [&str; 1] = ["playsound"]; const DESCRIPTION: &str = "Plays a sound at a position."; -const ARG_SOUND: &str = "sound"; -const ARG_SOURCE: &str = "source"; -const ARG_TARGETS: &str = "targets"; -const ARG_POS: &str = "pos"; -const ARG_VOLUME: &str = "volume"; -const ARG_PITCH: &str = "pitch"; -const ARG_MIN_VOLUME: &str = "minVolume"; +const PERMISSION: &str = "minecraft:command.playsound"; -// Volume must be >= 0, no upper limit -const fn volume_consumer() -> BoundedNumArgumentConsumer { - BoundedNumArgumentConsumer::new().name(ARG_VOLUME).min(0.0) -} +const ERROR_TOO_FAR: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_PLAYSOUND_FAILED, + translation::java::COMMANDS_PLAYSOUND_FAILED, +); -// Pitch must be between 0.0 and 2.0 -// Values below 0.5 are treated as 0.5 -const fn pitch_consumer() -> BoundedNumArgumentConsumer { - BoundedNumArgumentConsumer::new() - .name(ARG_PITCH) - .min(0.0) - .max(2.0) -} +const SOUND_SOURCES: [(&str, SoundCategory); 10] = [ + ("master", SoundCategory::Master), + ("music", SoundCategory::Music), + ("record", SoundCategory::Records), + ("weather", SoundCategory::Weather), + ("block", SoundCategory::Blocks), + ("hostile", SoundCategory::Hostile), + ("neutral", SoundCategory::Neutral), + ("player", SoundCategory::Players), + ("ambient", SoundCategory::Ambient), + ("voice", SoundCategory::Voice), +]; -// Minimum volume must be between 0.0 and 1.0 -// Controls the volume for players outside normal hearing range -const fn min_volume_consumer() -> BoundedNumArgumentConsumer { - BoundedNumArgumentConsumer::new() - .name(ARG_MIN_VOLUME) - .min(0.0) - .max(1.0) -} +#[allow(clippy::too_many_arguments)] +fn play_sound( + source: &CommandSource, + players: &[Arc], + sound: &Identifier, + sound_source: SoundCategory, + position: Vector3, + volume: f32, + pitch: f32, + min_volume: f32, +) -> Result { + let range = 16.0 * if volume > 1.0 { volume } else { 1.0 }; + let max_dist_sq = f64::from(range * range); + let world = source.world().clone(); + let seed = rand::random::(); + let mut played_for = Vec::new(); -struct Executor; + let sound_event = Sound::from_name(sound.path()) + .or_else(|| Sound::from_name(&sound.to_string())) + .map_or_else( + || { + IdOr::Value(pumpkin_protocol::SoundEvent { + sound_name: sound.to_string(), + range: None, + }) + }, + |sound_enum| IdOr::Id(sound_enum as u16), + ); -impl CommandExecutor for Executor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - // Get required sound argument - let sound = SoundArgumentConsumer::find_arg(args, ARG_SOUND)?; - - // Get optional sound category, defaults to Master - let source = args - .get(ARG_SOURCE) - .map_or(SoundCategory::Master, |arg| match arg { - Arg::SoundCategory(category) => *category, - _ => SoundCategory::Master, - }); - - // Get target players, defaults to sender if not specified - let targets: &[Arc] = match ( - PlayersArgumentConsumer::find_arg(args, ARG_TARGETS), - sender.as_player(), - ) { - (Ok(players), _) => players, - (_, Some(player)) => &[player], - (_, _) => &[], - }; - - // Get optional position, defaults to target's position - let position = Position3DArgumentConsumer::find_arg(args, ARG_POS).ok(); - - // Get optional volume parameter - let volume = match BoundedNumArgumentConsumer::::find_arg(args, ARG_VOLUME) { - Ok(Ok(v)) => v, - _ => 1.0, // Default volume - }; - - // Get optional pitch parameter - let pitch = match BoundedNumArgumentConsumer::::find_arg(args, ARG_PITCH) { - Ok(Ok(p)) => p.max(0.5), // Values below 0.5 are clamped - _ => 1.0, // Default pitch - }; - - // Get optional minimum volume (currently unused in implementation) - let min_volume = match BoundedNumArgumentConsumer::::find_arg(args, ARG_MIN_VOLUME) { - Ok(Ok(v)) => v, - _ => 0.0, // Default minimum volume - }; - - // Use same random seed for all targets to ensure sound synchronization - let seed = rng().random::(); - - // Track how many players actually received the sound - let mut players_who_heard = 0; - - // Play sound for each target player - for target in targets { - let pos = position.unwrap_or(target.living_entity.entity.pos.load()); - - // Check if player can hear the sound based on volume and distance - let player_pos = target.living_entity.entity.pos.load(); - let distance = player_pos.squared_distance_to_vec(&pos); - let max_distance: f64 = (16.0 * volume).into(); // 16 blocks is base distance at volume 1.0 - - if distance <= max_distance || min_volume > 0.0 { - target.play_sound(sound as u16, source, &pos, volume, pitch, seed); - players_who_heard += 1; - } + for player in players { + if !Arc::ptr_eq(&player.world(), &world) { + continue; } - // Send appropriate message based on results - if players_who_heard == 0 { - Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_PLAYSOUND_FAILED, - translation::java::COMMANDS_PLAYSOUND_FAILED, - [], - ))) - } else { - let sound_name = sound.to_name(); - if players_who_heard == 1 { - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_PLAYSOUND_SUCCESS_SINGLE, - translation::bedrock::COMMANDS_PLAYSOUND_SUCCESS, - [ - TextComponent::text(sound_name), - targets[0].get_display_name(), - ], - )); - } else { - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_PLAYSOUND_SUCCESS_MULTIPLE, - translation::bedrock::COMMANDS_PLAYSOUND_SUCCESS, - [ - TextComponent::text(sound_name), - TextComponent::text(players_who_heard.to_string()), - ], - )); + let player_pos = player.position(); + let delta_x = position.x - player_pos.x; + let delta_y = position.y - player_pos.y; + let delta_z = position.z - player_pos.z; + let dist_sq = delta_x * delta_x + delta_y * delta_y + delta_z * delta_z; + + let (local_position, local_volume) = if dist_sq > max_dist_sq { + if min_volume <= 0.0 { + continue; } - Ok(players_who_heard) + let distance = dist_sq.sqrt(); + ( + Vector3::new( + player_pos.x + delta_x / distance * 2.0, + player_pos.y + delta_y / distance * 2.0, + player_pos.z + delta_z / distance * 2.0, + ), + min_volume, + ) + } else { + (position, volume) + }; + + let packet = CSoundEffect::new( + sound_event.clone(), + sound_source, + &local_position, + local_volume, + pitch, + seed, + ); + player.try_send_client_packet(&packet); + played_for.push(player.clone()); + } + + let count = played_for.len(); + if count == 0 { + return Err(ERROR_TOO_FAR.create_without_context()); + } + + let sound_str = sound.to_string(); + if count == 1 { + let player_name = played_for[0].gameprofile.name.clone(); + source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_PLAYSOUND_SUCCESS_SINGLE, + translation::java::COMMANDS_PLAYSOUND_SUCCESS_SINGLE, + [ + TextComponent::text(sound_str), + TextComponent::text(player_name), + ], + ), + true, + ); + } else { + source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_PLAYSOUND_SUCCESS_MULTIPLE, + translation::java::COMMANDS_PLAYSOUND_SUCCESS_MULTIPLE, + [ + TextComponent::text(sound_str), + TextComponent::text(count.to_string()), + ], + ), + true, + ); + } + + Ok(count as i32) +} + +#[derive(Clone, Copy)] +enum PlaySoundStep { + SoundOnly, + SourceOnly { source: SoundCategory }, + WithTargets { source: SoundCategory }, + WithPos { source: SoundCategory }, + WithVolume { source: SoundCategory }, + WithPitch { source: SoundCategory }, + WithMinVolume { source: SoundCategory }, +} + +struct PlaySoundExecutor { + step: PlaySoundStep, +} + +impl CommandExecutor for PlaySoundExecutor { + #[allow(clippy::too_many_lines)] + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let sound = IdentifierArgumentType::get(context, "sound")?; + let caller_players = context + .source + .output + .as_player() + .map_or_else(Vec::new, |p| vec![p]); + + match self.step { + PlaySoundStep::SoundOnly => play_sound( + &context.source, + &caller_players, + &sound, + SoundCategory::Master, + context.source.position, + 1.0, + 1.0, + 0.0, + ), + PlaySoundStep::SourceOnly { source } => play_sound( + &context.source, + &caller_players, + &sound, + source, + context.source.position, + 1.0, + 1.0, + 0.0, + ), + PlaySoundStep::WithTargets { source } => { + let targets = EntityArgumentType::get_players(context, "targets")?; + play_sound( + &context.source, + &targets, + &sound, + source, + context.source.position, + 1.0, + 1.0, + 0.0, + ) + } + PlaySoundStep::WithPos { source } => { + let targets = EntityArgumentType::get_players(context, "targets")?; + let pos = Vec3ArgumentType::get_vector3(context, "pos")?; + play_sound( + &context.source, + &targets, + &sound, + source, + pos, + 1.0, + 1.0, + 0.0, + ) + } + PlaySoundStep::WithVolume { source } => { + let targets = EntityArgumentType::get_players(context, "targets")?; + let pos = Vec3ArgumentType::get_vector3(context, "pos")?; + let volume = FloatArgumentType::get(context, "volume")?; + play_sound( + &context.source, + &targets, + &sound, + source, + pos, + volume, + 1.0, + 0.0, + ) + } + PlaySoundStep::WithPitch { source } => { + let targets = EntityArgumentType::get_players(context, "targets")?; + let pos = Vec3ArgumentType::get_vector3(context, "pos")?; + let volume = FloatArgumentType::get(context, "volume")?; + let pitch = FloatArgumentType::get(context, "pitch")?; + play_sound( + &context.source, + &targets, + &sound, + source, + pos, + volume, + pitch, + 0.0, + ) + } + PlaySoundStep::WithMinVolume { source } => { + let targets = EntityArgumentType::get_players(context, "targets")?; + let pos = Vec3ArgumentType::get_vector3(context, "pos")?; + let volume = FloatArgumentType::get(context, "volume")?; + let pitch = FloatArgumentType::get(context, "pitch")?; + let min_volume = FloatArgumentType::get(context, "minVolume")?; + play_sound( + &context.source, + &targets, + &sound, + source, + pos, + volume, + pitch, + min_volume, + ) + } } } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).then( - argument(ARG_SOUND, SoundArgumentConsumer) - .then( - argument(ARG_SOURCE, SoundCategoryArgumentConsumer) - .then( - argument(ARG_TARGETS, PlayersArgumentConsumer) - .then( - argument(ARG_POS, Position3DArgumentConsumer) - .then( - argument(ARG_VOLUME, volume_consumer()) - .then( - argument(ARG_PITCH, pitch_consumer()) - .then( - argument( - ARG_MIN_VOLUME, - min_volume_consumer(), - ) - .execute(Executor), - ) - .execute(Executor), - ) - .execute(Executor), - ) - .execute(Executor), - ) - .execute(Executor), - ) - .execute(Executor), - ) - .execute(Executor), - ) +fn build_source_branch(name: &'static str, source: SoundCategory) -> LiteralArgumentBuilder { + literal(name) + .executes(PlaySoundExecutor { + step: PlaySoundStep::SourceOnly { source }, + }) + .then( + argument("targets", EntityArgumentType::Players) + .executes(PlaySoundExecutor { + step: PlaySoundStep::WithTargets { source }, + }) + .then( + argument("pos", Vec3ArgumentType::Default) + .executes(PlaySoundExecutor { + step: PlaySoundStep::WithPos { source }, + }) + .then( + argument("volume", FloatArgumentType::with_min(0.0)) + .executes(PlaySoundExecutor { + step: PlaySoundStep::WithVolume { source }, + }) + .then( + argument("pitch", FloatArgumentType::new(0.0, 2.0)) + .executes(PlaySoundExecutor { + step: PlaySoundStep::WithPitch { source }, + }) + .then( + argument("minVolume", FloatArgumentType::new(0.0, 1.0)) + .executes(PlaySoundExecutor { + step: PlaySoundStep::WithMinVolume { source }, + }), + ), + ), + ), + ), + ) +} + +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + let mut sound_arg = argument("sound", IdentifierArgumentType).executes(PlaySoundExecutor { + step: PlaySoundStep::SoundOnly, + }); + + for (name, source) in SOUND_SOURCES { + sound_arg = sound_arg.then(build_source_branch(name, source)); + } + + dispatcher.register( + command("playsound", DESCRIPTION) + .requires(PERMISSION) + .then(sound_arg), + ); } diff --git a/crates/pumpkin/src/command/commands/plugin.rs b/crates/pumpkin/src/command/commands/plugin.rs index 3891a8994..41d17abb5 100644 --- a/crates/pumpkin/src/command/commands/plugin.rs +++ b/crates/pumpkin/src/command/commands/plugin.rs @@ -1,39 +1,25 @@ use std::path::Path; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; +use pumpkin_util::text::TextComponent; +use pumpkin_util::text::color::NamedColor; use pumpkin_util::text::hover::HoverEvent; -use pumpkin_util::text::{TextComponent, color::NamedColor}; -use crate::command::args::simple::SimpleArgConsumer; -use crate::command::args::{Arg, ConsumedArgs}; -use crate::command::dispatcher::CommandError::{self, InvalidConsumption}; -use crate::command::tree::CommandTree; -use crate::command::tree::builder::{argument, literal}; -use crate::command::{CommandExecutor, CommandResult, CommandSender}; - -const NAMES: [&str; 1] = ["plugin"]; +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::core::string::StringArgumentType; +use crate::command::context::command_context::CommandContext; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; const DESCRIPTION: &str = "Manage server plugins."; - -const PLUGIN_NAME: &str = "plugin"; +const PERMISSION: &str = "pumpkin:command.plugin"; struct ListExecutor; impl CommandExecutor for ListExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - _args: &ConsumedArgs, - ) -> CommandResult { - let Some(server_arc) = sender - .world_or_first(server) - .and_then(|w| w.server.upgrade()) - else { - return Err(CommandError::CommandFailed(TextComponent::text( - "Failed to get server instance", - ))); - }; - + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let server_arc = context.server().clone(); let plugins = server_arc.plugin_manager.active_plugins(); let loaded_plugins = server_arc.plugin_manager.loaded_plugins(); @@ -72,7 +58,7 @@ impl CommandExecutor for ListExecutor { message = message.add_child(plugin_component); } - sender.send_message(message); + context.source.send_feedback(message, false); Ok(1) } @@ -81,34 +67,19 @@ impl CommandExecutor for ListExecutor { struct LoadExecutor; impl CommandExecutor for LoadExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::Simple(plugin_name)) = args.get(PLUGIN_NAME) else { - return Err(InvalidConsumption(Some(PLUGIN_NAME.into()))); - }; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let plugin_name = StringArgumentType::get(context, "plugin")?.to_string(); + let server_arc = context.server().clone(); - let Some(server_arc) = sender - .world_or_first(server) - .and_then(|w| w.server.upgrade()) - else { - return Err(CommandError::CommandFailed(TextComponent::text( - "Failed to get server instance", - ))); - }; - - let plugin_name = plugin_name.to_string(); if server_arc.plugin_manager.is_plugin_active(&plugin_name) { - sender.send_message(TextComponent::text(format!( - "Plugin {plugin_name} is already loaded" - ))); + context.source.send_feedback( + TextComponent::text(format!("Plugin {plugin_name} is already loaded")), + false, + ); return Ok(1); } - let sender_clone = sender.clone(); + let source_clone = context.source.clone(); let plugin_name_clone = plugin_name; let server_clone = server_arc.clone(); server_arc.spawn_task(async move { @@ -119,17 +90,21 @@ impl CommandExecutor for LoadExecutor { match result { Ok(()) => { - sender_clone.send_message( + source_clone.send_feedback( TextComponent::text(format!( "Plugin {plugin_name_clone} loaded successfully" )) .color_named(NamedColor::Green), + true, ); } Err(e) => { - sender_clone.send_message(TextComponent::text(format!( - "Failed to load plugin {plugin_name_clone}: {e}" - ))); + source_clone.send_feedback( + TextComponent::text(format!( + "Failed to load plugin {plugin_name_clone}: {e}" + )), + false, + ); } } }); @@ -141,34 +116,19 @@ impl CommandExecutor for LoadExecutor { struct UnloadExecutor; impl CommandExecutor for UnloadExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::Simple(plugin_name)) = args.get(PLUGIN_NAME) else { - return Err(InvalidConsumption(Some(PLUGIN_NAME.into()))); - }; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let plugin_name = StringArgumentType::get(context, "plugin")?.to_string(); + let server_arc = context.server().clone(); - let Some(server_arc) = sender - .world_or_first(server) - .and_then(|w| w.server.upgrade()) - else { - return Err(CommandError::CommandFailed(TextComponent::text( - "Failed to get server instance", - ))); - }; - - let plugin_name = plugin_name.to_string(); if !server_arc.plugin_manager.is_plugin_active(&plugin_name) { - sender.send_message(TextComponent::text(format!( - "Plugin {plugin_name} is not loaded" - ))); + context.source.send_feedback( + TextComponent::text(format!("Plugin {plugin_name} is not loaded")), + false, + ); return Ok(1); } - let sender_clone = sender.clone(); + let source_clone = context.source.clone(); let plugin_name_clone = plugin_name; let server_clone = server_arc.clone(); server_arc.spawn_task(async move { @@ -179,17 +139,21 @@ impl CommandExecutor for UnloadExecutor { match result { Ok(()) => { - sender_clone.send_message( + source_clone.send_feedback( TextComponent::text(format!( "Plugin {plugin_name_clone} unloaded successfully" )) .color_named(NamedColor::Green), + true, ); } Err(e) => { - sender_clone.send_message(TextComponent::text(format!( - "Failed to unload plugin {plugin_name_clone}: {e}" - ))); + source_clone.send_feedback( + TextComponent::text(format!( + "Failed to unload plugin {plugin_name_clone}: {e}" + )), + false, + ); } } }); @@ -201,51 +165,42 @@ impl CommandExecutor for UnloadExecutor { struct HotReloadExecutor(bool); impl CommandExecutor for HotReloadExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - _args: &ConsumedArgs, - ) -> CommandResult { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { let enabled = self.0; - - let Some(server_arc) = sender - .world_or_first(server) - .and_then(|w| w.server.upgrade()) - else { - return Err(CommandError::CommandFailed(TextComponent::text( - "Failed to get server instance", - ))); - }; - - let sender_clone = sender.clone(); + let server_arc = context.server().clone(); + let source_clone = context.source.clone(); let server_clone = server_arc.clone(); + if enabled { server_arc.spawn_task(async move { if let Err(e) = server_clone.plugin_manager.start_watcher(&server_clone).await { - sender_clone.send_message(TextComponent::text(format!( - "Failed to start plugin watcher: {e}" - ))); + source_clone.send_feedback( + TextComponent::text(format!("Failed to start plugin watcher: {e}")), + false, + ); return; } - sender_clone.send_message( + source_clone.send_feedback( TextComponent::text("Hot reloading has been enabled.") .color_named(NamedColor::Green), + true, ); - sender_clone.send_message( + source_clone.send_feedback( TextComponent::text( "WARNING: Hot reloading can impact performance and should only be enabled during plugin development.", ) .color_named(NamedColor::Red), + false, ); }); } else { server_arc.spawn_task(async move { server_clone.plugin_manager.stop_watcher().await; - sender_clone.send_message( + source_clone.send_feedback( TextComponent::text("Hot reloading has been disabled.") .color_named(NamedColor::Yellow), + true, ); }); } @@ -254,17 +209,31 @@ impl CommandExecutor for HotReloadExecutor { } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION) - .then(literal("list").execute(ListExecutor)) - .then(literal("load").then(argument(PLUGIN_NAME, SimpleArgConsumer).execute(LoadExecutor))) - .then( - literal("unload") - .then(argument(PLUGIN_NAME, SimpleArgConsumer).execute(UnloadExecutor)), - ) - .then( - literal("hotreload") - .then(literal("enable").execute(HotReloadExecutor(true))) - .then(literal("disable").execute(HotReloadExecutor(false))), - ) +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Three), + )); + + dispatcher.register( + command("plugin", DESCRIPTION) + .requires(PERMISSION) + .then(literal("list").executes(ListExecutor)) + .then( + literal("load").then( + argument("plugin", StringArgumentType::SingleWord).executes(LoadExecutor), + ), + ) + .then( + literal("unload").then( + argument("plugin", StringArgumentType::SingleWord).executes(UnloadExecutor), + ), + ) + .then( + literal("hotreload") + .then(literal("enable").executes(HotReloadExecutor(true))) + .then(literal("disable").executes(HotReloadExecutor(false))), + ), + ); } diff --git a/crates/pumpkin/src/command/commands/plugins.rs b/crates/pumpkin/src/command/commands/plugins.rs index 8f431df8c..036e99acc 100644 --- a/crates/pumpkin/src/command/commands/plugins.rs +++ b/crates/pumpkin/src/command/commands/plugins.rs @@ -1,33 +1,22 @@ +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; +use pumpkin_util::text::TextComponent; +use pumpkin_util::text::color::NamedColor; use pumpkin_util::text::hover::HoverEvent; -use pumpkin_util::text::{TextComponent, color::NamedColor}; -use crate::command::args::ConsumedArgs; -use crate::command::dispatcher::CommandError; -use crate::command::tree::CommandTree; -use crate::command::{CommandExecutor, CommandResult, CommandSender}; - -const NAMES: [&str; 1] = ["plugins"]; +use crate::command::argument_builder::{ArgumentBuilder, command}; +use crate::command::context::command_context::CommandContext; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; const DESCRIPTION: &str = "Lists all plugins loaded on the server."; +const PERMISSION: &str = "pumpkin:command.plugins"; struct PluginsExecutor; impl CommandExecutor for PluginsExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - _args: &ConsumedArgs, - ) -> CommandResult { - let Some(server_arc) = sender - .world_or_first(server) - .and_then(|w| w.server.upgrade()) - else { - return Err(CommandError::CommandFailed(TextComponent::text( - "Failed to get server instance", - ))); - }; - + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let server_arc = context.server(); let plugins = server_arc.plugin_manager.active_plugins(); let message_text = if plugins.is_empty() { @@ -63,12 +52,22 @@ impl CommandExecutor for PluginsExecutor { base_message }; - sender.send_message(message_text); + context.source.send_feedback(message_text, false); Ok(1) } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).execute(PluginsExecutor) +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Three), + )); + + dispatcher.register( + command("plugins", DESCRIPTION) + .requires(PERMISSION) + .executes(PluginsExecutor), + ); } diff --git a/crates/pumpkin/src/command/commands/pumpkin.rs b/crates/pumpkin/src/command/commands/pumpkin.rs index a071d89ec..7b697f443 100644 --- a/crates/pumpkin/src/command/commands/pumpkin.rs +++ b/crates/pumpkin/src/command/commands/pumpkin.rs @@ -8,12 +8,17 @@ use std::borrow::Cow; use std::sync::{LazyLock, Mutex}; use std::time::{Duration, Instant}; -use crate::command::CommandResult; -use crate::command::{CommandExecutor, CommandSender, args::ConsumedArgs, tree::CommandTree}; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; + +use crate::command::argument_builder::{ArgumentBuilder, command}; +use crate::command::context::command_context::CommandContext; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; const NAMES: [&str; 3] = ["pumpkin", "version", "ver"]; - const DESCRIPTION: &str = "Display information about Pumpkin."; +const PERMISSION: &str = "pumpkin:command.pumpkin"; const CACHE_DURATION: Duration = Duration::from_hours(24); @@ -268,19 +273,14 @@ fn fetch_donators_hover_cached() -> TextComponent { #[expect(clippy::too_many_lines)] impl CommandExecutor for Executor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - _args: &ConsumedArgs, - ) -> CommandResult { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { let contributors = fetch_all_contributors_cached(); let contributor_names = contributors .iter() .map(|c| c.login.as_str()) .collect::>() .join(", "); - let locale = sender.get_locale(); + let locale = context.source.output.get_locale(); let profile = if cfg!(debug_assertions) { "debug" } else { @@ -405,7 +405,7 @@ impl CommandExecutor for Executor { .underlined(), ); - sender.send_message(msg); + context.source.send_message(msg); // It makes total sense to return the number of // contributors as the i32 result for this command. @@ -413,8 +413,20 @@ impl CommandExecutor for Executor { } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).execute(Executor) +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Zero), + )); + + for name in NAMES { + dispatcher.register( + command(name, DESCRIPTION) + .requires(PERMISSION) + .executes(Executor), + ); + } } #[cfg(test)] diff --git a/crates/pumpkin/src/command/commands/raid.rs b/crates/pumpkin/src/command/commands/raid.rs index 76d544ae1..a5485fd49 100644 --- a/crates/pumpkin/src/command/commands/raid.rs +++ b/crates/pumpkin/src/command/commands/raid.rs @@ -1,3 +1,4 @@ +use std::sync::Arc; use uuid::Uuid; use pumpkin_data::data_component_impl::EquipmentSlot; @@ -5,41 +6,36 @@ use pumpkin_data::effect::StatusEffect; use pumpkin_data::entity::EntityType; use pumpkin_data::potion::Effect; use pumpkin_data::sound::Sound; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; -use std::sync::Arc; - -use crate::command::args::bounded_num::BoundedNumArgumentConsumer; -use crate::command::args::{ConsumedArgs, FindArg}; -use crate::command::dispatcher::CommandError; -use crate::command::tree::CommandTree; -use crate::command::tree::builder::{argument, literal}; -use crate::command::{CommandExecutor, CommandResult, CommandSender}; +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::core::integer::IntegerArgumentType; +use crate::command::context::command_context::CommandContext; +use crate::command::errors::error_types::DISPATCHER_PARSE_EXCEPTION; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; use crate::entity::EntityBase; use crate::entity::mob::raider::create_ominous_banner; use crate::entity::player::Player; use crate::entity::r#type::from_type; -use crate::server::Server; use crate::world::raid::RaidStatus; -const NAMES: [&str; 1] = ["raid"]; const DESCRIPTION: &str = "Controls or queries village raids."; - -const ARG_OMEN_LVL: &str = "omenlvl"; -const ARG_LEVEL: &str = "level"; +const PERMISSION: &str = "minecraft:command.raid"; struct StartExecutor { has_omen_lvl: bool, } impl CommandExecutor for StartExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &Server, - args: &ConsumedArgs, - ) -> CommandResult { - let player = sender.as_player().ok_or(CommandError::InvalidRequirement)?; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let player = context.source.as_player().ok_or_else(|| { + DISPATCHER_PARSE_EXCEPTION.create_without_context(TextComponent::text( + "Only players can execute this command", + )) + })?; let entity = player.get_entity(); let pos = entity.block_pos.load(); let world = entity.world.load(); @@ -53,10 +49,7 @@ impl CommandExecutor for StartExecutor { (true, None) } else { let omen_lvl = if self.has_omen_lvl { - BoundedNumArgumentConsumer::::find_arg(args, ARG_OMEN_LVL) - .ok() - .and_then(Result::ok) - .unwrap_or(1) + IntegerArgumentType::get(context, "omenlvl")? } else { 1 }; @@ -70,16 +63,22 @@ impl CommandExecutor for StartExecutor { } }; if is_already_started { - sender.send_message(TextComponent::text("Raid already started close by")); + context + .source + .send_feedback(TextComponent::text("Raid already started close by"), false); return Ok(0); } if raid_created.is_some() { - sender.send_message(TextComponent::text("Created a raid in your local village")); + context.source.send_feedback( + TextComponent::text("Created a raid in your local village"), + true, + ); Ok(1) } else { - sender.send_message(TextComponent::text( - "Failed to create a raid in your local village", - )); + context.source.send_feedback( + TextComponent::text("Failed to create a raid in your local village"), + false, + ); Ok(0) } } @@ -88,13 +87,12 @@ impl CommandExecutor for StartExecutor { struct StopExecutor; impl CommandExecutor for StopExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &Server, - _args: &ConsumedArgs, - ) -> CommandResult { - let player = sender.as_player().ok_or(CommandError::InvalidRequirement)?; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let player = context.source.as_player().ok_or_else(|| { + DISPATCHER_PARSE_EXCEPTION.create_without_context(TextComponent::text( + "Only players can execute this command", + )) + })?; let entity = player.get_entity(); let pos = entity.block_pos.load(); let world = entity.world.load(); @@ -125,10 +123,14 @@ impl CommandExecutor for StopExecutor { for p in players { p.remove_bossbar(bossbar_uuid); } - sender.send_message(TextComponent::text("Stopped raid")); + context + .source + .send_feedback(TextComponent::text("Stopped raid"), true); Ok(1) } else { - sender.send_message(TextComponent::text("No raid here")); + context + .source + .send_feedback(TextComponent::text("No raid here"), false); Ok(0) } } @@ -137,13 +139,12 @@ impl CommandExecutor for StopExecutor { struct CheckExecutor; impl CommandExecutor for CheckExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &Server, - _args: &ConsumedArgs, - ) -> CommandResult { - let player = sender.as_player().ok_or(CommandError::InvalidRequirement)?; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let player = context.source.as_player().ok_or_else(|| { + DISPATCHER_PARSE_EXCEPTION.create_without_context(TextComponent::text( + "Only players can execute this command", + )) + })?; let entity = player.get_entity(); let pos = entity.block_pos.load(); let world = entity.world.load(); @@ -168,12 +169,18 @@ impl CommandExecutor for CheckExecutor { }; info.map_or_else( || { - sender.send_message(TextComponent::text("Found no started raids")); + context + .source + .send_feedback(TextComponent::text("Found no started raids"), false); Ok(0) }, |msg| { - sender.send_message(TextComponent::text("Found a started raid!")); - sender.send_message(TextComponent::text(msg)); + context + .source + .send_feedback(TextComponent::text("Found a started raid!"), false); + context + .source + .send_feedback(TextComponent::text(msg), false); Ok(1) }, ) @@ -183,13 +190,12 @@ impl CommandExecutor for CheckExecutor { struct SoundExecutor; impl CommandExecutor for SoundExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &Server, - _args: &ConsumedArgs, - ) -> CommandResult { - let player = sender.as_player().ok_or(CommandError::InvalidRequirement)?; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let player = context.source.as_player().ok_or_else(|| { + DISPATCHER_PARSE_EXCEPTION.create_without_context(TextComponent::text( + "Only players can execute this command", + )) + })?; let entity = player.get_entity(); let pos = entity.pos.load(); let world = entity.world.load(); @@ -207,13 +213,12 @@ impl CommandExecutor for SoundExecutor { struct SpawnLeaderExecutor; impl CommandExecutor for SpawnLeaderExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &Server, - _args: &ConsumedArgs, - ) -> CommandResult { - let player = sender.as_player().ok_or(CommandError::InvalidRequirement)?; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let player = context.source.as_player().ok_or_else(|| { + DISPATCHER_PARSE_EXCEPTION.create_without_context(TextComponent::text( + "Only players can execute this command", + )) + })?; let entity = player.get_entity(); let pos = entity.pos.load(); let world = entity.world.load(); @@ -237,7 +242,9 @@ impl CommandExecutor for SpawnLeaderExecutor { } world.spawn_entity(raider_entity); - sender.send_message(TextComponent::text("Spawned a raid captain")); + context + .source + .send_feedback(TextComponent::text("Spawned a raid captain"), true); Ok(1) } } @@ -245,21 +252,17 @@ impl CommandExecutor for SpawnLeaderExecutor { struct SetOmenExecutor; impl CommandExecutor for SetOmenExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &Server, - args: &ConsumedArgs, - ) -> CommandResult { - let player = sender.as_player().ok_or(CommandError::InvalidRequirement)?; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let player = context.source.as_player().ok_or_else(|| { + DISPATCHER_PARSE_EXCEPTION.create_without_context(TextComponent::text( + "Only players can execute this command", + )) + })?; let entity = player.get_entity(); let pos = entity.block_pos.load(); let world = entity.world.load(); - let level = BoundedNumArgumentConsumer::::find_arg(args, ARG_LEVEL) - .ok() - .and_then(Result::ok) - .unwrap_or(1); + let level = IntegerArgumentType::get(context, "level")?; let res = { let mut raids = world @@ -280,13 +283,18 @@ impl CommandExecutor for SetOmenExecutor { }; match res { Ok(before) => { - sender.send_message(TextComponent::text(format!( - "Changed village's raid omen level from {before} to {level}" - ))); + context.source.send_feedback( + TextComponent::text(format!( + "Changed village's raid omen level from {before} to {level}" + )), + true, + ); Ok(1) } Err(msg) => { - sender.send_message(TextComponent::text(msg)); + context + .source + .send_feedback(TextComponent::text(msg), false); Ok(0) } } @@ -296,13 +304,12 @@ impl CommandExecutor for SetOmenExecutor { struct GlowExecutor; impl CommandExecutor for GlowExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &Server, - _args: &ConsumedArgs, - ) -> CommandResult { - let player = sender.as_player().ok_or(CommandError::InvalidRequirement)?; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let player = context.source.as_player().ok_or_else(|| { + DISPATCHER_PARSE_EXCEPTION.create_without_context(TextComponent::text( + "Only players can execute this command", + )) + })?; let entity = player.get_entity(); let pos = entity.block_pos.load(); let world = entity.world.load(); @@ -318,7 +325,9 @@ impl CommandExecutor for GlowExecutor { }; raiders.map_or_else( || { - sender.send_message(TextComponent::text("No raid found here")); + context + .source + .send_feedback(TextComponent::text("No raid found here"), false); Ok(0) }, |raider_uuids| { @@ -344,30 +353,33 @@ impl CommandExecutor for GlowExecutor { } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION) - .then( - literal("start") - .execute(StartExecutor { - has_omen_lvl: false, - }) - .then( - argument( - ARG_OMEN_LVL, - BoundedNumArgumentConsumer::::new().min(0), - ) - .execute(StartExecutor { has_omen_lvl: true }), - ), - ) - .then(literal("stop").execute(StopExecutor)) - .then(literal("check").execute(CheckExecutor)) - .then(literal("sound").execute(SoundExecutor)) - .then(literal("spawnleader").execute(SpawnLeaderExecutor)) - .then( - literal("setomen").then( - argument(ARG_LEVEL, BoundedNumArgumentConsumer::::new().min(0)) - .execute(SetOmenExecutor), - ), - ) - .then(literal("glow").execute(GlowExecutor)) +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + dispatcher.register( + command("raid", DESCRIPTION) + .requires(PERMISSION) + .then( + literal("start") + .executes(StartExecutor { + has_omen_lvl: false, + }) + .then( + argument("omenlvl", IntegerArgumentType::with_min(0)) + .executes(StartExecutor { has_omen_lvl: true }), + ), + ) + .then(literal("stop").executes(StopExecutor)) + .then(literal("check").executes(CheckExecutor)) + .then(literal("sound").executes(SoundExecutor)) + .then(literal("spawnleader").executes(SpawnLeaderExecutor)) + .then(literal("setomen").then( + argument("level", IntegerArgumentType::with_min(0)).executes(SetOmenExecutor), + )) + .then(literal("glow").executes(GlowExecutor)), + ); } diff --git a/crates/pumpkin/src/command/commands/return.rs b/crates/pumpkin/src/command/commands/return.rs new file mode 100644 index 000000000..19a745c77 --- /dev/null +++ b/crates/pumpkin/src/command/commands/return.rs @@ -0,0 +1,44 @@ +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; + +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::core::integer::IntegerArgumentType; +use crate::command::context::command_context::CommandContext; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult, Redirection}; + +const DESCRIPTION: &str = "Controls execution flow in functions and sets return values."; +const PERMISSION: &str = "minecraft:command.return"; + +struct ReturnValueExecutor; + +impl CommandExecutor for ReturnValueExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let value = IntegerArgumentType::get(context, "value")?; + Ok(value) + } +} + +struct ReturnFailExecutor; + +impl CommandExecutor for ReturnFailExecutor { + fn execute(&self, _context: &CommandContext) -> CommandExecutorResult { + Ok(0) + } +} + +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + dispatcher.register( + command("return", DESCRIPTION) + .requires(PERMISSION) + .then(argument("value", IntegerArgumentType::any()).executes(ReturnValueExecutor)) + .then(literal("fail").executes(ReturnFailExecutor)) + .then(literal("run").redirect(Redirection::Root)), + ); +} diff --git a/crates/pumpkin/src/command/commands/rotate.rs b/crates/pumpkin/src/command/commands/rotate.rs index 822ca7997..f2c78289f 100644 --- a/crates/pumpkin/src/command/commands/rotate.rs +++ b/crates/pumpkin/src/command/commands/rotate.rs @@ -1,26 +1,24 @@ +use std::sync::Arc; + use pumpkin_data::translation; +use pumpkin_util::PermissionLvl; use pumpkin_util::math::vector3::Vector3; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; -use crate::command::args::entity::EntityArgumentConsumer; -use crate::command::args::entity_anchor::{EntityAnchor, EntityAnchorArgumentConsumer}; -use crate::command::args::position_3d::Position3DArgumentConsumer; -use crate::command::args::rotation::RotationArgumentConsumer; -use crate::command::args::{ConsumedArgs, FindArg}; -use crate::command::tree::CommandTree; -use crate::command::tree::builder::{argument, literal}; -use crate::command::{CommandExecutor, CommandResult, CommandSender}; +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::coordinates::rotation::RotationArgumentType; +use crate::command::argument_types::coordinates::vec3::Vec3ArgumentType; +use crate::command::argument_types::entity::EntityArgumentType; +use crate::command::argument_types::entity_anchor::{EntityAnchor, EntityAnchorArgumentType}; +use crate::command::context::command_context::CommandContext; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; +use crate::entity::EntityBase; -const NAMES: [&str; 1] = ["rotate"]; const DESCRIPTION: &str = "Changes the rotation of an entity."; +const PERMISSION: &str = "minecraft:command.rotate"; -const ARG_TARGET: &str = "target"; -const ARG_ROTATION: &str = "rotation"; -const ARG_FACING_LOCATION: &str = "facingLocation"; -const ARG_FACING_ENTITY: &str = "facingEntity"; -const ARG_FACING_ANCHOR: &str = "facingAnchor"; - -/// Calculates yaw and pitch to look from one position towards another. fn yaw_pitch_facing_position( looking_from: &Vector3, looking_towards: &Vector3, @@ -36,95 +34,49 @@ fn yaw_pitch_facing_position( (yaw_degrees as f32, pitch_degrees as f32) } -/// Gets the position to look at for an entity, considering the anchor point. -/// Matches vanilla's `EntityAnchorArgumentType.EntityAnchor.positionAt()`. -fn get_anchor_position(entity: &crate::entity::Entity, anchor: EntityAnchor) -> Vector3 { - let pos = entity.pos.load(); - match anchor { - EntityAnchor::Eyes => { - let eye_height = entity.get_eye_height(); - Vector3::new(pos.x, pos.y + eye_height, pos.z) - } - EntityAnchor::Feet => pos, - } -} - -/// Rotates an entity using vanilla-style rotation logic. -/// If relative flags are true, the values are added to current rotation. -/// If relative flags are false, the values are absolute. -fn rotate_entity( - target: &std::sync::Arc, - yaw: f32, - is_yaw_relative: bool, - pitch: f32, - is_pitch_relative: bool, -) { +fn rotate_entity(target: &Arc, yaw: f32, pitch: f32) { let entity = target.get_entity(); - - // Calculate final rotation values like vanilla's Entity.rotate() - // If relative, add to current rotation; if absolute, use directly - let final_yaw = if is_yaw_relative { - entity.yaw.load() + yaw - } else { - yaw - }; - - let final_pitch = if is_pitch_relative { - // Clamp pitch to valid range [-90, 90] - (entity.pitch.load() + pitch).clamp(-90.0, 90.0) - } else { - pitch.clamp(-90.0, 90.0) - }; - - // Use teleport with same position to update rotation let pos = entity.pos.load(); let world = entity.world.load_full(); - target.teleport(pos, Some(final_yaw), Some(final_pitch), world); + let clamped_pitch = pitch.clamp(-90.0, 90.0); + target.teleport(pos, Some(yaw), Some(clamped_pitch), world); } -/// Sends success message for the rotate command. -fn send_success_message(sender: &CommandSender, target: &dyn crate::entity::EntityBase) { +fn send_success_message(context: &CommandContext, target: &dyn EntityBase) { let target_name = target.get_display_name(); - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_ROTATE_SUCCESS, - translation::java::COMMANDS_ROTATE_SUCCESS, - [target_name], - )); + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_ROTATE_SUCCESS, + translation::java::COMMANDS_ROTATE_SUCCESS, + [target_name], + ), + true, + ); } -// /rotate struct RotateToRotationExecutor; impl CommandExecutor for RotateToRotationExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let target = EntityArgumentConsumer::find_arg(args, ARG_TARGET)?; - let (yaw, yaw_rel, pitch, pitch_rel) = - RotationArgumentConsumer::find_arg(args, ARG_ROTATION)?; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let target = EntityArgumentType::get_entity(context, "target")?; + let rot = RotationArgumentType::get(context, "rotation")?.rotation(&context.source); + let pitch = rot.x; + let yaw = rot.y; - rotate_entity(&target, yaw, yaw_rel, pitch, pitch_rel); - send_success_message(sender, target.as_ref()); + rotate_entity(&target, yaw, pitch); + send_success_message(context, target.as_ref()); Ok(1) } } -// /rotate facing struct RotateFacingLocationExecutor; impl CommandExecutor for RotateFacingLocationExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let target = EntityArgumentConsumer::find_arg(args, ARG_TARGET)?; - let facing_pos = Position3DArgumentConsumer::find_arg(args, ARG_FACING_LOCATION)?; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let target = EntityArgumentType::get_entity(context, "target")?; + let facing_pos = + Vec3ArgumentType::get_coordinates(context, "facingLocation")?.resolve(&context.source); let entity = target.get_entity(); let eye_height = entity.get_eye_height(); @@ -133,97 +85,73 @@ impl CommandExecutor for RotateFacingLocationExecutor { let (yaw, pitch) = yaw_pitch_facing_position(&looking_from, &facing_pos); - // Facing uses absolute rotation - rotate_entity(&target, yaw, false, pitch, false); - send_success_message(sender, target.as_ref()); + rotate_entity(&target, yaw, pitch); + send_success_message(context, target.as_ref()); Ok(1) } } -// /rotate facing entity [eyes|feet] -struct RotateFacingEntityExecutor; +struct RotateFacingEntityExecutor { + has_anchor: bool, +} impl CommandExecutor for RotateFacingEntityExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let target = EntityArgumentConsumer::find_arg(args, ARG_TARGET)?; - let facing_entity = EntityArgumentConsumer::find_arg(args, ARG_FACING_ENTITY)?; - let anchor = EntityAnchorArgumentConsumer::find_arg(args, ARG_FACING_ANCHOR)?; - let target_entity = target.get_entity(); - let eye_height = target_entity.get_eye_height(); - let pos = target_entity.pos.load(); - let looking_from = Vector3::new(pos.x, pos.y + eye_height, pos.z); - - let looking_towards = get_anchor_position(facing_entity.get_entity(), anchor); - - let (yaw, pitch) = yaw_pitch_facing_position(&looking_from, &looking_towards); - - // Facing uses absolute rotation - rotate_entity(&target, yaw, false, pitch, false); - send_success_message(sender, target.as_ref()); - - Ok(1) - } -} - -// /rotate facing entity (no anchor - defaults to feet) -struct RotateFacingEntityNoAnchorExecutor; - -impl CommandExecutor for RotateFacingEntityNoAnchorExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let target = EntityArgumentConsumer::find_arg(args, ARG_TARGET)?; - let facing_entity = EntityArgumentConsumer::find_arg(args, ARG_FACING_ENTITY)?; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let target = EntityArgumentType::get_entity(context, "target")?; + let facing_entity = EntityArgumentType::get_entity(context, "facingEntity")?; + let anchor = if self.has_anchor { + EntityAnchorArgumentType::get(context, "facingAnchor")? + } else { + EntityAnchor::Feet + }; let target_entity = target.get_entity(); let eye_height = target_entity.get_eye_height(); let pos = target_entity.pos.load(); let looking_from = Vector3::new(pos.x, pos.y + eye_height, pos.z); - // Default to feet (vanilla behavior) - let looking_towards = get_anchor_position(facing_entity.get_entity(), EntityAnchor::Feet); + let looking_towards = anchor.position_at_entity(facing_entity.get_entity()); let (yaw, pitch) = yaw_pitch_facing_position(&looking_from, &looking_towards); - // Facing uses absolute rotation - rotate_entity(&target, yaw, false, pitch, false); - send_success_message(sender, target.as_ref()); + rotate_entity(&target, yaw, pitch); + send_success_message(context, target.as_ref()); Ok(1) } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).then( - argument(ARG_TARGET, EntityArgumentConsumer) - .then( - argument(ARG_ROTATION, RotationArgumentConsumer).execute(RotateToRotationExecutor), - ) - .then( - literal("facing") - .then( - literal("entity").then( - argument(ARG_FACING_ENTITY, EntityArgumentConsumer) - .execute(RotateFacingEntityNoAnchorExecutor) - .then( - argument(ARG_FACING_ANCHOR, EntityAnchorArgumentConsumer) - .execute(RotateFacingEntityExecutor), - ), +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + dispatcher.register( + command("rotate", DESCRIPTION).requires(PERMISSION).then( + argument("target", EntityArgumentType::Entity) + .then(argument("rotation", RotationArgumentType).executes(RotateToRotationExecutor)) + .then( + literal("facing") + .then( + literal("entity").then( + argument("facingEntity", EntityArgumentType::Entity) + .executes(RotateFacingEntityExecutor { has_anchor: false }) + .then( + argument("facingAnchor", EntityAnchorArgumentType) + .executes(RotateFacingEntityExecutor { + has_anchor: true, + }), + ), + ), + ) + .then( + argument("facingLocation", Vec3ArgumentType::Default) + .executes(RotateFacingLocationExecutor), ), - ) - .then( - argument(ARG_FACING_LOCATION, Position3DArgumentConsumer) - .execute(RotateFacingLocationExecutor), - ), - ), - ) + ), + ), + ); } diff --git a/crates/pumpkin/src/command/commands/schedule.rs b/crates/pumpkin/src/command/commands/schedule.rs new file mode 100644 index 000000000..fc7c08051 --- /dev/null +++ b/crates/pumpkin/src/command/commands/schedule.rs @@ -0,0 +1,196 @@ +use std::sync::atomic::Ordering; + +use pumpkin_data::translation; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; +use pumpkin_util::text::TextComponent; + +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::core::string::StringArgumentType; +use crate::command::argument_types::time::TimeArgumentType; +use crate::command::context::command_context::CommandContext; +use crate::command::errors::command_syntax_error::CommandSyntaxError; +use crate::command::errors::error_types::CommandErrorType; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; +use crate::command::suggestion::provider::{SuggestionProvider, SuggestionProviderResult}; +use crate::command::suggestion::suggestions::SuggestionsBuilder; + +const DESCRIPTION: &str = "Schedules an action or function to execute after a given duration."; +const PERMISSION: &str = "minecraft:command.schedule"; + +pub const ERROR_SAME_TICK: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_SCHEDULE_SAME_TICK, + translation::java::COMMANDS_SCHEDULE_SAME_TICK, +); + +pub const ERROR_CANT_REMOVE: CommandErrorType<1> = CommandErrorType::new( + translation::java::COMMANDS_SCHEDULE_CLEARED_FAILURE, + translation::java::COMMANDS_SCHEDULE_CLEARED_FAILURE, +); + +#[expect(dead_code)] +pub const ERROR_MACRO: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_SCHEDULE_MACRO, + translation::java::COMMANDS_SCHEDULE_MACRO, +); + +struct FunctionSuggestionProvider; + +impl SuggestionProvider for FunctionSuggestionProvider { + fn suggest( + &self, + context: &CommandContext, + mut builder: SuggestionsBuilder, + ) -> SuggestionProviderResult { + let server = context.server(); + let function_names = server.datapack_manager.get_function_names(); + for name in function_names { + builder = builder.suggest(name); + } + builder.build() + } +} + +struct ScheduleSuggestionProvider; + +impl SuggestionProvider for ScheduleSuggestionProvider { + fn suggest( + &self, + context: &CommandContext, + mut builder: SuggestionsBuilder, + ) -> SuggestionProviderResult { + let server = context.server(); + for id in server.scheduled_functions.get_event_ids() { + builder = builder.suggest(id); + } + builder.build() + } +} + +fn execute_schedule(context: &CommandContext, replace: bool) -> Result { + let function_name = StringArgumentType::get(context, "function")?.to_string(); + let time = TimeArgumentType::get(context, "time")?; + + if time == 0 { + return Err(ERROR_SAME_TICK.create_without_context()); + } + + let server = context.server(); + let current_tick = server.tick_count.load(Ordering::Relaxed) as u64; + let tick_time = current_tick + (time as u64); + let is_tag = function_name.starts_with('#'); + + server.scheduled_functions.schedule( + function_name.clone(), + tick_time, + function_name.clone(), + is_tag, + replace, + ); + + if is_tag { + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_SCHEDULE_CREATED_TAG, + translation::java::COMMANDS_SCHEDULE_CREATED_TAG, + [ + TextComponent::text(function_name), + TextComponent::text(time.to_string()), + TextComponent::text(tick_time.to_string()), + ], + ), + true, + ); + } else { + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_SCHEDULE_CREATED_FUNCTION, + translation::java::COMMANDS_SCHEDULE_CREATED_FUNCTION, + [ + TextComponent::text(function_name), + TextComponent::text(time.to_string()), + TextComponent::text(tick_time.to_string()), + ], + ), + true, + ); + } + + Ok((tick_time % (i32::MAX as u64)) as i32) +} + +struct ScheduleExecutor { + replace: bool, +} + +impl CommandExecutor for ScheduleExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + execute_schedule(context, self.replace) + } +} + +struct ClearExecutor; + +impl CommandExecutor for ClearExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let function_name = StringArgumentType::get(context, "function")?; + let server = context.server(); + let count = server.scheduled_functions.remove(function_name); + + if count == 0 { + return Err(ERROR_CANT_REMOVE + .create_without_context(TextComponent::text(function_name.to_string()))); + } + + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_SCHEDULE_CLEARED_SUCCESS, + translation::java::COMMANDS_SCHEDULE_CLEARED_SUCCESS, + [ + TextComponent::text(count.to_string()), + TextComponent::text(function_name.to_string()), + ], + ), + true, + ); + + Ok(count as i32) + } +} + +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + dispatcher.register( + command("schedule", DESCRIPTION) + .requires(PERMISSION) + .then( + literal("function").then( + argument("function", StringArgumentType::SingleWord) + .suggests(FunctionSuggestionProvider) + .then( + argument("time", TimeArgumentType::any()) + .executes(ScheduleExecutor { replace: true }) + .then( + literal("append").executes(ScheduleExecutor { replace: false }), + ) + .then( + literal("replace").executes(ScheduleExecutor { replace: true }), + ), + ), + ), + ) + .then( + literal("clear").then( + argument("function", StringArgumentType::GreedyPhrase) + .suggests(ScheduleSuggestionProvider) + .executes(ClearExecutor), + ), + ), + ); +} diff --git a/crates/pumpkin/src/command/commands/setblock.rs b/crates/pumpkin/src/command/commands/setblock.rs index 2f6d25d90..d3fde74bf 100644 --- a/crates/pumpkin/src/command/commands/setblock.rs +++ b/crates/pumpkin/src/command/commands/setblock.rs @@ -1,19 +1,24 @@ +use pumpkin_data::translation; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; use pumpkin_world::world::BlockFlags; -use crate::command::args::block::BlockArgumentConsumer; -use crate::command::args::position_block::BlockPosArgumentConsumer; -use crate::command::args::{ConsumedArgs, FindArg}; -use crate::command::tree::CommandTree; -use crate::command::tree::builder::{argument, literal}; -use crate::command::{CommandError, CommandExecutor, CommandResult, CommandSender}; - -const NAMES: [&str; 1] = ["setblock"]; +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}; const DESCRIPTION: &str = "Place a block."; +const PERMISSION: &str = "minecraft:command.setblock"; -const ARG_BLOCK: &str = "block"; -const ARG_BLOCK_POS: &str = "pos"; +const ERROR_FAILED: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_SETBLOCK_FAILED, + translation::java::COMMANDS_SETBLOCK_FAILED, +); #[derive(Clone, Copy)] enum Mode { @@ -30,22 +35,15 @@ enum Mode { Strict, } -struct Executor(Mode); +struct SetBlockExecutor(Mode); -impl CommandExecutor for Executor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let block = BlockArgumentConsumer::find_arg(args, ARG_BLOCK)?; +impl CommandExecutor for SetBlockExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let block = BlockArgumentType::get(context, "block")?; let block_state_id = block.default_state.id; let mode = self.0; - let world = sender - .world_or_first(server) - .ok_or(CommandError::InvalidRequirement)?; - let pos = BlockPosArgumentConsumer::find_loaded_arg(args, ARG_BLOCK_POS, &world)?; + let world = context.source.world(); + let pos = BlockPosArgumentType::get_loaded_block_pos(context, "pos")?; let success = match mode { Mode::Destroy => { @@ -85,35 +83,42 @@ impl CommandExecutor for Executor { }; if success { - sender.send_message(TextComponent::translate_cross( - pumpkin_data::translation::java::COMMANDS_SETBLOCK_SUCCESS, - pumpkin_data::translation::bedrock::COMMANDS_SETBLOCK_SUCCESS, - [ - TextComponent::text(pos.0.x.to_string()), - TextComponent::text(pos.0.y.to_string()), - TextComponent::text(pos.0.z.to_string()), - ], - )); + context.source.send_feedback( + TextComponent::translate_cross( + pumpkin_data::translation::java::COMMANDS_SETBLOCK_SUCCESS, + pumpkin_data::translation::bedrock::COMMANDS_SETBLOCK_SUCCESS, + [ + TextComponent::text(pos.0.x.to_string()), + TextComponent::text(pos.0.y.to_string()), + TextComponent::text(pos.0.z.to_string()), + ], + ), + true, + ); Ok(1) } else { - Err(CommandError::CommandFailed(TextComponent::translate_cross( - pumpkin_data::translation::java::COMMANDS_SETBLOCK_FAILED, - pumpkin_data::translation::bedrock::COMMANDS_SETBLOCK_FAILED, - [], - ))) + Err(ERROR_FAILED.create_without_context()) } } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).then( - argument(ARG_BLOCK_POS, BlockPosArgumentConsumer).then( - argument(ARG_BLOCK, BlockArgumentConsumer) - .then(literal("replace").execute(Executor(Mode::Replace))) - .then(literal("destroy").execute(Executor(Mode::Destroy))) - .then(literal("keep").execute(Executor(Mode::Keep))) - .then(literal("strict").execute(Executor(Mode::Strict))) - .execute(Executor(Mode::Replace)), +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + dispatcher.register( + command("setblock", DESCRIPTION).requires(PERMISSION).then( + argument("pos", BlockPosArgumentType).then( + argument("block", BlockArgumentType) + .executes(SetBlockExecutor(Mode::Replace)) + .then(literal("destroy").executes(SetBlockExecutor(Mode::Destroy))) + .then(literal("keep").executes(SetBlockExecutor(Mode::Keep))) + .then(literal("replace").executes(SetBlockExecutor(Mode::Replace))) + .then(literal("strict").executes(SetBlockExecutor(Mode::Strict))), + ), ), - ) + ); } diff --git a/crates/pumpkin/src/command/commands/setworldspawn.rs b/crates/pumpkin/src/command/commands/setworldspawn.rs index 1e02d759e..01d09e356 100644 --- a/crates/pumpkin/src/command/commands/setworldspawn.rs +++ b/crates/pumpkin/src/command/commands/setworldspawn.rs @@ -1,175 +1,136 @@ use std::sync::Arc; -use crate::command::CommandResult; -use crate::command::dispatcher::CommandError::InvalidConsumption; -use crate::command::{ - CommandExecutor, CommandSender, - args::{ - Arg, ConsumedArgs, position_block::BlockPosArgumentConsumer, - rotation::RotationArgumentConsumer, - }, - dispatcher::CommandError, - tree::{CommandTree, builder::argument}, -}; -use crate::plugin::world::spawn_change::SpawnChangeEvent; -use crate::server::Server; use pumpkin_data::dimension::Dimension; use pumpkin_data::translation; -use pumpkin_util::{math::position::BlockPos, text::TextComponent}; +use pumpkin_util::PermissionLvl; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; +use pumpkin_util::text::TextComponent; -const NAMES: [&str; 1] = ["setworldspawn"]; +use crate::command::argument_builder::{ArgumentBuilder, argument, command}; +use crate::command::argument_types::coordinates::angle::AngleArgumentType; +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 crate::plugin::world::spawn_change::SpawnChangeEvent; const DESCRIPTION: &str = "Sets the world spawn point."; +const PERMISSION: &str = "minecraft:command.setworldspawn"; -const ARG_BLOCK_POS: &str = "position"; +const ERROR_NOT_OVERWORLD: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_SETWORLDSPAWN_FAILURE_NOT_OVERWORLD, + translation::java::COMMANDS_SETWORLDSPAWN_FAILURE_NOT_OVERWORLD, +); -const ARG_ANGLE: &str = "angle"; +enum SetWorldSpawnMode { + SelfPos, + PosOnly, + PosAndAngle, +} -struct NoArgsWorldSpawnExecutor; +struct SetWorldSpawnExecutor(SetWorldSpawnMode); -impl CommandExecutor for NoArgsWorldSpawnExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - _args: &ConsumedArgs, - ) -> CommandResult { - let Some(player) = sender.as_player() else { - if sender.is_console() { - return Err(CommandError::CommandFailed(TextComponent::text( - "You must specify a Position!", - ))); +impl CommandExecutor for SetWorldSpawnExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let (pos, angle) = match self.0 { + SetWorldSpawnMode::SelfPos => { + let block_pos = BlockPos::floored_v(context.source.position); + (block_pos, 0.0) + } + SetWorldSpawnMode::PosOnly => { + let block_pos = BlockPosArgumentType::get_block_pos(context, "pos")?; + (block_pos, 0.0) + } + SetWorldSpawnMode::PosAndAngle => { + let block_pos = BlockPosArgumentType::get_block_pos(context, "pos")?; + let angle = AngleArgumentType::get(context, "angle")?.get_angle(&context.source); + (block_pos, angle) } - return Err(CommandError::CommandFailed(TextComponent::text( - "Failed to get Sender as Player!", - ))); }; - let block_pos = player.position(); - setworldspawn(sender, server, block_pos.to_block_pos(), 0.0, 0.0) + + let world = context.source.world(); + if world.dimension != Dimension::OVERWORLD && world.dimension != Dimension::OVERWORLD_CAVES + { + return Err(ERROR_NOT_OVERWORLD.create_without_context()); + } + + let server = context.source.server(); + let current_info = server.level_info.load(); + let previous_position = BlockPos::new( + current_info.spawn_x, + current_info.spawn_y, + current_info.spawn_z, + ); + let new_position = pos; + let previous_yaw = current_info.spawn_yaw; + let previous_pitch = current_info.spawn_pitch; + let new_yaw = angle; + let new_pitch = 0.0; + let mut event = SpawnChangeEvent::new( + world.clone(), + previous_position, + previous_yaw, + previous_pitch, + new_position, + new_yaw, + new_pitch, + ); + if let Some(server_arc) = world.server.upgrade() { + server_arc + .plugin_manager + .fire_blocking(&server_arc, &mut event); + } + + let mut new_info = (**current_info).clone(); + + new_info.spawn_x = new_position.0.x; + new_info.spawn_y = new_position.0.y; + new_info.spawn_z = new_position.0.z; + new_info.spawn_yaw = new_yaw; + new_info.spawn_pitch = new_pitch; + + server.level_info.store(Arc::new(new_info)); + + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_SETWORLDSPAWN_SUCCESS, + translation::bedrock::COMMANDS_SETWORLDSPAWN_SUCCESS, + [ + TextComponent::text(new_position.0.x.to_string()), + TextComponent::text(new_position.0.y.to_string()), + TextComponent::text(new_position.0.z.to_string()), + TextComponent::text(new_yaw.to_string()), + TextComponent::text(new_pitch.to_string()), + TextComponent::text(world.dimension.minecraft_name), + ], + ), + true, + ); + + Ok(1) } } -struct DefaultWorldSpawnExecutor; - -impl CommandExecutor for DefaultWorldSpawnExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::BlockPos(block_pos)) = args.get(ARG_BLOCK_POS) else { - return Err(InvalidConsumption(Some(ARG_BLOCK_POS.into()))); - }; - - setworldspawn(sender, server, *block_pos, 0.0, 0.0) - } -} - -struct AngleWorldSpawnExecutor; - -impl CommandExecutor for AngleWorldSpawnExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::BlockPos(block_pos)) = args.get(ARG_BLOCK_POS) else { - return Err(InvalidConsumption(Some(ARG_BLOCK_POS.into()))); - }; - - // Note: Rotation argument is (yaw, is_yaw_relative, pitch, is_pitch_relative) - // For setworldspawn, we use absolute values only (ignore relative flags) - let Some(Arg::Rotation(yaw, _, pitch, _)) = args.get(ARG_ANGLE) else { - return Err(InvalidConsumption(Some(ARG_ANGLE.into()))); - }; - - setworldspawn(sender, server, *block_pos, *yaw, *pitch) - } -} - -fn setworldspawn( - sender: &CommandSender, - server: &Server, - block_pos: BlockPos, - yaw: f32, - pitch: f32, -) -> Result { - let Some(world) = sender.world_or_first(server) else { - return Err(CommandError::CommandFailed(TextComponent::text( - "Failed to get world.", - ))); - }; - if world.dimension != Dimension::OVERWORLD && world.dimension != Dimension::OVERWORLD_CAVES { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_SETWORLDSPAWN_FAILURE_NOT_OVERWORLD, - translation::java::COMMANDS_SETWORLDSPAWN_FAILURE_NOT_OVERWORLD, - [], - ))); - } - - let current_info = server.level_info.load(); - let previous_position = BlockPos::new( - current_info.spawn_x, - current_info.spawn_y, - current_info.spawn_z, - ); - let new_position = block_pos; - let previous_yaw = current_info.spawn_yaw; - let previous_pitch = current_info.spawn_pitch; - let new_yaw = yaw; - let new_pitch = pitch; - let mut event = SpawnChangeEvent::new( - world.clone(), - previous_position, - previous_yaw, - previous_pitch, - new_position, - new_yaw, - new_pitch, - ); - if let Some(server_arc) = world.server.upgrade() { - server_arc - .plugin_manager - .fire_blocking(&server_arc, &mut event); - } - - let mut new_info = (**current_info).clone(); - - new_info.spawn_x = new_position.0.x; - new_info.spawn_y = new_position.0.y; - new_info.spawn_z = new_position.0.z; - new_info.spawn_yaw = new_yaw; - new_info.spawn_pitch = new_pitch; - - server.level_info.store(Arc::new(new_info)); - - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_SETWORLDSPAWN_SUCCESS, - translation::bedrock::COMMANDS_SETWORLDSPAWN_SUCCESS, - [ - TextComponent::text(new_position.0.x.to_string()), - TextComponent::text(new_position.0.y.to_string()), - TextComponent::text(new_position.0.z.to_string()), - TextComponent::text(new_yaw.to_string()), - TextComponent::text(new_pitch.to_string()), - TextComponent::text(world.dimension.minecraft_name), - ], +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), )); - Ok(1) -} - -#[must_use] -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION) - .execute(NoArgsWorldSpawnExecutor) - .then( - argument(ARG_BLOCK_POS, BlockPosArgumentConsumer) - .execute(DefaultWorldSpawnExecutor) - .then( - argument(ARG_ANGLE, RotationArgumentConsumer).execute(AngleWorldSpawnExecutor), - ), - ) + dispatcher.register( + command("setworldspawn", DESCRIPTION) + .requires(PERMISSION) + .executes(SetWorldSpawnExecutor(SetWorldSpawnMode::SelfPos)) + .then( + argument("pos", BlockPosArgumentType) + .executes(SetWorldSpawnExecutor(SetWorldSpawnMode::PosOnly)) + .then( + argument("angle", AngleArgumentType) + .executes(SetWorldSpawnExecutor(SetWorldSpawnMode::PosAndAngle)), + ), + ), + ); } diff --git a/crates/pumpkin/src/command/commands/spawnpoint.rs b/crates/pumpkin/src/command/commands/spawnpoint.rs index b846d89e1..3a77e0755 100644 --- a/crates/pumpkin/src/command/commands/spawnpoint.rs +++ b/crates/pumpkin/src/command/commands/spawnpoint.rs @@ -1,151 +1,111 @@ -use std::sync::Arc; - use pumpkin_data::translation; -use pumpkin_util::math::position::BlockPos; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; -use crate::command::args::players::PlayersArgumentConsumer; -use crate::command::args::position_block::BlockPosArgumentConsumer; -use crate::command::args::rotation::RotationArgumentConsumer; -use crate::command::args::{Arg, ConsumedArgs, FindArgDefaultName}; -use crate::command::dispatcher::CommandError; -use crate::command::tree::CommandTree; -use crate::command::tree::builder::argument; -use crate::command::{CommandExecutor, CommandResult, CommandSender}; -use crate::entity::EntityBase; -use crate::entity::player::Player; - -const NAMES: [&str; 1] = ["spawnpoint"]; +use crate::command::argument_builder::{ArgumentBuilder, argument, command}; +use crate::command::argument_types::coordinates::angle::AngleArgumentType; +use crate::command::argument_types::coordinates::block_pos::BlockPosArgumentType; +use crate::command::argument_types::entity::EntityArgumentType; +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}; const DESCRIPTION: &str = "Sets the spawn point for a player."; +const PERMISSION: &str = "minecraft:command.spawnpoint"; -const ARG_TARGETS: &str = "targets"; -const ARG_POS: &str = "pos"; -const ARG_ANGLE: &str = "angle"; +const ERROR_NOT_PLAYER: CommandErrorType<0> = CommandErrorType::new( + translation::java::PERMISSIONS_REQUIRES_PLAYER, + translation::java::PERMISSIONS_REQUIRES_PLAYER, +); -/// `/spawnpoint` - set self at current position -struct SelfExecutor; - -impl CommandExecutor for SelfExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - _args: &ConsumedArgs, - ) -> CommandResult { - let Some(player) = sender.as_player() else { - return Err(CommandError::InvalidRequirement); - }; - let pos = player.position().to_block_pos(); - let yaw = player.get_entity().yaw.load(); - set_spawnpoint(sender, &player, pos, yaw); - - Ok(1) - } +enum SpawnpointMode { + SelfDefault, + TargetsDefault, + TargetsPos, + TargetsPosAngle, } -/// `/spawnpoint ` - set targets at their current positions -struct TargetsExecutor; +struct SpawnpointExecutor(SpawnpointMode); -impl CommandExecutor for TargetsExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let targets = PlayersArgumentConsumer.find_arg_default_name(args)?; - - for target in targets { - let pos = target.position().to_block_pos(); - let yaw = target.living_entity.entity.yaw.load(); - set_spawnpoint(sender, target, pos, yaw); - } - - Ok(targets.len() as i32) - } -} - -/// `/spawnpoint ` - set targets at specified position -struct TargetsPosExecutor; - -impl CommandExecutor for TargetsPosExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let targets = PlayersArgumentConsumer.find_arg_default_name(args)?; - let Some(Arg::BlockPos(pos)) = args.get(ARG_POS) else { - return Err(CommandError::InvalidConsumption(Some(ARG_POS.into()))); +impl CommandExecutor for SpawnpointExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let targets = match self.0 { + SpawnpointMode::SelfDefault => { + let player = context + .source + .output + .as_player() + .ok_or_else(|| ERROR_NOT_PLAYER.create_without_context())?; + vec![player] + } + _ => EntityArgumentType::get_players(context, "targets")?, }; - for target in targets { - let yaw = target.living_entity.entity.yaw.load(); - set_spawnpoint(sender, target, *pos, yaw); - } + for target in &targets { + let (pos, yaw) = match self.0 { + SpawnpointMode::SelfDefault | SpawnpointMode::TargetsDefault => { + let block_pos = target.position().to_block_pos(); + let yaw = target.living_entity.entity.yaw.load(); + (block_pos, yaw) + } + SpawnpointMode::TargetsPos => { + let block_pos = BlockPosArgumentType::get_block_pos(context, "pos")?; + let yaw = target.living_entity.entity.yaw.load(); + (block_pos, yaw) + } + SpawnpointMode::TargetsPosAngle => { + let block_pos = BlockPosArgumentType::get_block_pos(context, "pos")?; + let yaw = AngleArgumentType::get(context, "angle")?.get_angle(&context.source); + (block_pos, yaw) + } + }; - Ok(targets.len() as i32) - } -} + let dimension = target.world().dimension.clone(); + target.set_respawn_point(dimension, pos, yaw, 0.0, true); -/// `/spawnpoint ` - set targets at position with angle -struct TargetsPosAngleExecutor; - -impl CommandExecutor for TargetsPosAngleExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let targets = PlayersArgumentConsumer.find_arg_default_name(args)?; - let Some(Arg::BlockPos(pos)) = args.get(ARG_POS) else { - return Err(CommandError::InvalidConsumption(Some(ARG_POS.into()))); - }; - let Some(Arg::Rotation(yaw, _, _, _)) = args.get(ARG_ANGLE) else { - return Err(CommandError::InvalidConsumption(Some(ARG_ANGLE.into()))); - }; - - for target in targets { - set_spawnpoint(sender, target, *pos, *yaw); - } - - Ok(targets.len() as i32) - } -} - -fn set_spawnpoint(sender: &CommandSender, target: &Arc, pos: BlockPos, yaw: f32) { - let dimension = target.world().dimension.clone(); - target.set_respawn_point(dimension, pos, yaw, 0.0, true); - - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_SPAWNPOINT_SUCCESS_SINGLE, - translation::bedrock::COMMANDS_SPAWNPOINT_SUCCESS_SINGLE, - [ - TextComponent::text(target.gameprofile.name.clone()), - TextComponent::text(pos.0.x.to_string()), - TextComponent::text(pos.0.y.to_string()), - TextComponent::text(pos.0.z.to_string()), - ], - )); -} - -#[must_use] -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION) - .execute(SelfExecutor) - .then( - argument(ARG_TARGETS, PlayersArgumentConsumer) - .execute(TargetsExecutor) - .then( - argument(ARG_POS, BlockPosArgumentConsumer) - .execute(TargetsPosExecutor) - .then( - argument(ARG_ANGLE, RotationArgumentConsumer) - .execute(TargetsPosAngleExecutor), - ), + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_SPAWNPOINT_SUCCESS_SINGLE, + translation::bedrock::COMMANDS_SPAWNPOINT_SUCCESS_SINGLE, + [ + TextComponent::text(target.gameprofile.name.clone()), + TextComponent::text(pos.0.x.to_string()), + TextComponent::text(pos.0.y.to_string()), + TextComponent::text(pos.0.z.to_string()), + ], ), - ) + true, + ); + } + + Ok(targets.len() as i32) + } +} + +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + dispatcher.register( + command("spawnpoint", DESCRIPTION) + .requires(PERMISSION) + .executes(SpawnpointExecutor(SpawnpointMode::SelfDefault)) + .then( + argument("targets", EntityArgumentType::Players) + .executes(SpawnpointExecutor(SpawnpointMode::TargetsDefault)) + .then( + argument("pos", BlockPosArgumentType) + .executes(SpawnpointExecutor(SpawnpointMode::TargetsPos)) + .then( + argument("angle", AngleArgumentType) + .executes(SpawnpointExecutor(SpawnpointMode::TargetsPosAngle)), + ), + ), + ), + ); } diff --git a/crates/pumpkin/src/command/commands/spectate.rs b/crates/pumpkin/src/command/commands/spectate.rs index b8ad2a83d..aaf6486c3 100644 --- a/crates/pumpkin/src/command/commands/spectate.rs +++ b/crates/pumpkin/src/command/commands/spectate.rs @@ -1,172 +1,113 @@ use std::sync::Arc; -use crate::command::args::entity::EntityArgumentConsumer; -use crate::command::args::players::PlayersArgumentConsumer; -use crate::command::args::{Arg, ConsumedArgs, FindArg}; -use crate::command::dispatcher::CommandError::{self, InvalidConsumption, InvalidRequirement}; -use crate::command::tree::CommandTree; -use crate::command::tree::builder::{argument, require}; -use crate::command::{CommandExecutor, CommandResult, CommandSender}; -use crate::entity::EntityBase; use pumpkin_data::translation; use pumpkin_protocol::java::client::play::CSetCamera; -use pumpkin_util::GameMode; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; +use pumpkin_util::{GameMode, PermissionLvl}; -const NAMES: [&str; 1] = ["spectate"]; +use crate::command::argument_builder::{ArgumentBuilder, argument, command}; +use crate::command::argument_types::entity::EntityArgumentType; +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 crate::entity::EntityBase; const DESCRIPTION: &str = "Allows a player in spectator mode to spectate a given target entity."; +const PERMISSION: &str = "minecraft:command.spectate"; -const ARG_TARGET: &str = "target"; -const ARG_PLAYER: &str = "player"; +const ERROR_NOT_PLAYER: CommandErrorType<0> = CommandErrorType::new( + translation::java::PERMISSIONS_REQUIRES_PLAYER, + translation::java::PERMISSIONS_REQUIRES_PLAYER, +); + +const ERROR_NOT_SPECTATOR: CommandErrorType<1> = CommandErrorType::new( + translation::java::COMMANDS_SPECTATE_NOT_SPECTATOR, + translation::java::COMMANDS_SPECTATE_NOT_SPECTATOR, +); + +const ERROR_SELF: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_SPECTATE_SELF, + translation::java::COMMANDS_SPECTATE_SELF, +); + +const ERROR_CANNOT_SPECTATE: CommandErrorType<1> = CommandErrorType::new( + translation::java::COMMANDS_SPECTATE_CANNOT_SPECTATE, + translation::java::COMMANDS_SPECTATE_CANNOT_SPECTATE, +); struct StopSpectateExecutor; impl CommandExecutor for StopSpectateExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - _args: &ConsumedArgs, - ) -> CommandResult { - let Some(player) = sender.as_player() else { - return Err(InvalidRequirement); - }; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let player = context + .source + .output + .as_player() + .ok_or_else(|| ERROR_NOT_PLAYER.create_without_context())?; if player.gamemode.load() != GameMode::Spectator { let display_name = player.get_display_name(); - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_SPECTATE_NOT_SPECTATOR, - translation::java::COMMANDS_SPECTATE_NOT_SPECTATOR, - [display_name], - ))); + return Err(ERROR_NOT_SPECTATOR.create_without_context(display_name)); } player.camera_target_id.store(None); player.try_send_client_packet(&CSetCamera::new(player.entity_id().into())); - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_SPECTATE_SUCCESS_STOPPED, - translation::java::COMMANDS_SPECTATE_SUCCESS_STOPPED, - [], - )); - - Ok(1) - } -} - -struct SpectateTargetSelfExecutor; - -impl CommandExecutor for SpectateTargetSelfExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(player) = sender.as_player() else { - return Err(InvalidRequirement); - }; - - if player.gamemode.load() != GameMode::Spectator { - let display_name = player.get_display_name(); - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_SPECTATE_NOT_SPECTATOR, - translation::java::COMMANDS_SPECTATE_NOT_SPECTATOR, - [display_name], - ))); - } - - let target = EntityArgumentConsumer::find_arg(args, ARG_TARGET)?; - - let target_entity = target.get_entity(); - if target_entity.entity_id == player.entity_id() { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_SPECTATE_SELF, - translation::java::COMMANDS_SPECTATE_SELF, + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_SPECTATE_SUCCESS_STOPPED, + translation::java::COMMANDS_SPECTATE_SUCCESS_STOPPED, [], - ))); - } - - let target_world = target_entity.world.load_full(); - let player_world = player.world(); - if !Arc::ptr_eq(&target_world, &player_world) { - let target_name = target.get_display_name(); - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_SPECTATE_CANNOT_SPECTATE, - translation::java::COMMANDS_SPECTATE_CANNOT_SPECTATE, - [target_name], - ))); - } - - let target_id = target_entity.entity_id; - player.camera_target_id.store(Some(target_id)); - let pos = target_entity.pos.load(); - let yaw = target_entity.yaw.load(); - let pitch = target_entity.pitch.load(); - player.try_send_client_packet(&CSetCamera::new(target_id.into())); - player.teleport(pos, Some(yaw), Some(pitch), player_world); - - let target_name = target.get_display_name(); - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_SPECTATE_SUCCESS_STARTED, - translation::java::COMMANDS_SPECTATE_SUCCESS_STARTED, - [target_name], - )); + ), + false, + ); Ok(1) } } -struct SpectateTargetOtherExecutor; +struct SpectateTargetExecutor { + is_self: bool, +} -impl CommandExecutor for SpectateTargetOtherExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::Players(players)) = args.get(ARG_PLAYER) else { - return Err(InvalidConsumption(Some(ARG_PLAYER.into()))); - }; - - let target = EntityArgumentConsumer::find_arg(args, ARG_TARGET)?; +impl CommandExecutor for SpectateTargetExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let target = EntityArgumentType::get_entity(context, "target")?; let target_entity = target.get_entity(); let target_world = target_entity.world.load_full(); - // First validate all players - for player in players { + let players = if self.is_self { + let player = context + .source + .output + .as_player() + .ok_or_else(|| ERROR_NOT_PLAYER.create_without_context())?; + vec![player] + } else { + EntityArgumentType::get_players(context, "player")? + }; + + for player in &players { if player.gamemode.load() != GameMode::Spectator { let display_name = player.get_display_name(); - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_SPECTATE_NOT_SPECTATOR, - translation::java::COMMANDS_SPECTATE_NOT_SPECTATOR, - [display_name], - ))); + return Err(ERROR_NOT_SPECTATOR.create_without_context(display_name)); } if target_entity.entity_id == player.entity_id() { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_SPECTATE_SELF, - translation::java::COMMANDS_SPECTATE_SELF, - [], - ))); + return Err(ERROR_SELF.create_without_context()); } let player_world = player.world(); if !Arc::ptr_eq(&target_world, &player_world) { let target_name = target.get_display_name(); - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_SPECTATE_CANNOT_SPECTATE, - translation::java::COMMANDS_SPECTATE_CANNOT_SPECTATE, - [target_name], - ))); + return Err(ERROR_CANNOT_SPECTATE.create_without_context(target_name)); } } let mut succeeded = 0; - for player in players { + for player in &players { let target_id = target_entity.entity_id; player.camera_target_id.store(Some(target_id)); let pos = target_entity.pos.load(); @@ -179,26 +120,37 @@ impl CommandExecutor for SpectateTargetOtherExecutor { } let target_name = target.get_display_name(); - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_SPECTATE_SUCCESS_STARTED, - translation::java::COMMANDS_SPECTATE_SUCCESS_STARTED, - [target_name], - )); + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_SPECTATE_SUCCESS_STARTED, + translation::java::COMMANDS_SPECTATE_SUCCESS_STARTED, + [target_name], + ), + false, + ); Ok(succeeded) } } -#[expect(clippy::redundant_closure_for_method_calls)] -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION) - .then( - argument(ARG_TARGET, EntityArgumentConsumer) - .then(require(|sender| sender.is_player()).execute(SpectateTargetSelfExecutor)) - .then( - argument(ARG_PLAYER, PlayersArgumentConsumer) - .execute(SpectateTargetOtherExecutor), - ), - ) - .then(require(|sender| sender.is_player()).execute(StopSpectateExecutor)) +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + dispatcher.register( + command("spectate", DESCRIPTION) + .requires(PERMISSION) + .executes(StopSpectateExecutor) + .then( + argument("target", EntityArgumentType::Entity) + .executes(SpectateTargetExecutor { is_self: true }) + .then( + argument("player", EntityArgumentType::Players) + .executes(SpectateTargetExecutor { is_self: false }), + ), + ), + ); } diff --git a/crates/pumpkin/src/command/commands/spreadplayers.rs b/crates/pumpkin/src/command/commands/spreadplayers.rs index dfb192249..484d852d5 100644 --- a/crates/pumpkin/src/command/commands/spreadplayers.rs +++ b/crates/pumpkin/src/command/commands/spreadplayers.rs @@ -5,10 +5,11 @@ use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry use pumpkin_util::text::TextComponent; use pumpkin_world::chunk::ChunkHeightmapType; -use crate::command::argument_builder::{ArgumentBuilder, argument, command}; -use crate::command::argument_types::coordinates::column_pos::ColumnPosArgumentType; +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::coordinates::vec2::Vec2ArgumentType; use crate::command::argument_types::core::bool::BoolArgumentType; use crate::command::argument_types::core::float::FloatArgumentType; +use crate::command::argument_types::core::integer::IntegerArgumentType; use crate::command::argument_types::entity::EntityArgumentType; use crate::command::context::command_context::CommandContext; use crate::command::errors::error_types::CommandErrorType; @@ -23,6 +24,7 @@ const PERMISSION: &str = "minecraft:command.spreadplayers"; const ARG_CENTER: &str = "center"; const ARG_SPREAD_DISTANCE: &str = "spreadDistance"; const ARG_MAX_RANGE: &str = "maxRange"; +const ARG_MAX_HEIGHT: &str = "maxHeight"; const ARG_RESPECT_TEAMS: &str = "respectTeams"; const ARG_TARGETS: &str = "targets"; @@ -39,6 +41,11 @@ const FAILED_TEAMS_ERROR_TYPE: CommandErrorType<4> = CommandErrorType::new( translation::bedrock::COMMANDS_SPREADPLAYERS_FAILURE_TEAMS, ); +const INVALID_MAX_HEIGHT_ERROR_TYPE: CommandErrorType<2> = CommandErrorType::new( + translation::java::COMMANDS_SPREADPLAYERS_FAILED_INVALID_HEIGHT, + translation::java::COMMANDS_SPREADPLAYERS_FAILED_INVALID_HEIGHT, +); + /// A candidate spread location, equivalent to Vanilla's /// `SpreadPlayersCommand.Position`. #[derive(Clone, Copy, Default)] @@ -80,12 +87,16 @@ impl Pile { } /// Returns the y coordinate an entity should stand at for this pile, or - /// `None` if the location is unsafe (on top of a liquid). - fn surface_y(&self, world: &World) -> Option { + /// `None` if the location is unsafe (on top of a liquid or out of height bounds). + fn surface_y(&self, world: &World, max_height: i32) -> Option { let block_x = self.x.floor() as i32; let block_z = self.z.floor() as i32; let top = world.get_heightmap_height(ChunkHeightmapType::WorldSurface, block_x, block_z); + if top >= max_height { + return None; + } + let ground = pumpkin_util::math::position::BlockPos(Vector3::new(block_x, top, block_z)); let state = world.get_block_state(&ground); if state.is_liquid() { @@ -163,6 +174,7 @@ fn find_spread_positions( world: &World, pile_count: usize, spread_distance: f64, + max_height: i32, area: &SpreadArea, ) -> Option<(Vec, Vec)> { let mut piles = vec![Pile::default(); pile_count]; @@ -171,8 +183,6 @@ fn find_spread_positions( } let mut surface_ys = vec![0; pile_count]; - // A handful of surface retries is plenty; each retry re-runs the - // full relaxation, so this loop must stay small. for _ in 0..16 { if !spread_piles( &mut piles, @@ -187,7 +197,7 @@ fn find_spread_positions( let mut all_safe = true; for (pile, y) in piles.iter_mut().zip(&mut surface_ys) { - if let Some(surface) = pile.surface_y(world) { + if let Some(surface) = pile.surface_y(world, max_height) { *y = surface; } else { pile.randomize(area.min_x, area.min_z, area.max_x, area.max_z); @@ -221,19 +231,20 @@ fn average_min_distance(piles: &[Pile]) -> f64 { total / count } -struct SpreadPlayersExecutor; +struct SpreadPlayersExecutor { + has_max_height: bool, +} impl CommandExecutor for SpreadPlayersExecutor { fn execute(&self, context: &CommandContext) -> CommandExecutorResult { - let center = ColumnPosArgumentType::get_column_pos(context, ARG_CENTER)?; + let center = Vec2ArgumentType::get_vector2(context, ARG_CENTER)?; let spread_distance = f64::from(FloatArgumentType::get(context, ARG_SPREAD_DISTANCE)?); let max_range = f64::from(FloatArgumentType::get(context, ARG_MAX_RANGE)?); let respect_teams = BoolArgumentType::get(context, ARG_RESPECT_TEAMS)?; let targets = EntityArgumentType::get_entities(context, ARG_TARGETS)?; - // Vanilla center-corrects the vec2 argument to the middle of the block. - let center_x = f64::from(center.0.x) + 0.5; - let center_z = f64::from(center.0.y) + 0.5; + let center_x = center.x; + let center_z = center.y; let area = SpreadArea { min_x: center_x - max_range, min_z: center_z - max_range, @@ -241,15 +252,25 @@ impl CommandExecutor for SpreadPlayersExecutor { max_z: center_z + max_range, }; - // Teams are not implemented yet, so every target is teamless. With - // respectTeams=true that matches Vanilla's behavior of gathering all - // teamless entities onto a single position. + let world = context.source.world().clone(); + let min_y = world.min_y; + let max_height = if self.has_max_height { + let val = IntegerArgumentType::get(context, ARG_MAX_HEIGHT)?; + if val < min_y { + return Err(INVALID_MAX_HEIGHT_ERROR_TYPE.create_without_context( + TextComponent::text(val.to_string()), + TextComponent::text(min_y.to_string()), + )); + } + val + } else { + world.dimension.height + min_y + 1 + }; + let pile_count = if respect_teams { 1 } else { targets.len() }; - let world = context.source.world().clone(); - let Some((piles, surface_ys)) = - find_spread_positions(&world, pile_count, spread_distance, &area) + find_spread_positions(&world, pile_count, spread_distance, max_height, &area) else { let error_type = if respect_teams { &FAILED_TEAMS_ERROR_TYPE @@ -321,14 +342,28 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr command("spreadplayers", DESCRIPTION) .requires(PERMISSION) .then( - argument(ARG_CENTER, ColumnPosArgumentType).then( + argument(ARG_CENTER, Vec2ArgumentType::Default).then( argument(ARG_SPREAD_DISTANCE, FloatArgumentType::with_min(0.0)).then( - argument(ARG_MAX_RANGE, FloatArgumentType::with_min(1.0)).then( - argument(ARG_RESPECT_TEAMS, BoolArgumentType).then( - argument(ARG_TARGETS, EntityArgumentType::Entities) - .executes(SpreadPlayersExecutor), + argument(ARG_MAX_RANGE, FloatArgumentType::with_min(1.0)) + .then(argument(ARG_RESPECT_TEAMS, BoolArgumentType).then( + argument(ARG_TARGETS, EntityArgumentType::Entities).executes( + SpreadPlayersExecutor { + has_max_height: false, + }, + ), + )) + .then( + literal("under").then( + argument(ARG_MAX_HEIGHT, IntegerArgumentType::any()).then( + argument(ARG_RESPECT_TEAMS, BoolArgumentType).then( + argument(ARG_TARGETS, EntityArgumentType::Entities) + .executes(SpreadPlayersExecutor { + has_max_height: true, + }), + ), + ), + ), ), - ), ), ), ), diff --git a/crates/pumpkin/src/command/commands/stopsound.rs b/crates/pumpkin/src/command/commands/stopsound.rs index ae430868e..a45977a5d 100644 --- a/crates/pumpkin/src/command/commands/stopsound.rs +++ b/crates/pumpkin/src/command/commands/stopsound.rs @@ -1,85 +1,109 @@ -use crate::command::{ - CommandExecutor, CommandResult, CommandSender, - args::{ - ConsumedArgs, FindArg, players::PlayersArgumentConsumer, sound::SoundArgumentConsumer, - sound_category::SoundCategoryArgumentConsumer, - }, - tree::{CommandTree, builder::argument}, -}; use pumpkin_data::translation; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; -const NAMES: [&str; 1] = ["stopsound"]; +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::entity::EntityArgumentType; +use crate::command::argument_types::identifier::IdentifierArgumentType; +use crate::command::argument_types::sound_category::SoundCategoryArgumentType; +use crate::command::context::command_context::CommandContext; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; + const DESCRIPTION: &str = "Stops a currently playing sound."; +const PERMISSION: &str = "minecraft:command.stopsound"; -const ARG_TARGETS: &str = "targets"; -const ARG_SOURCE: &str = "source"; -const ARG_SOUND: &str = "sound"; +enum StopSoundMode { + All, + Category, + Sound, + CategoryAndSound, +} -pub struct Executor; +struct StopSoundExecutor(StopSoundMode); -impl CommandExecutor for Executor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let targets = PlayersArgumentConsumer::find_arg(args, ARG_TARGETS)?; +impl CommandExecutor for StopSoundExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let targets = EntityArgumentType::get_players(context, "targets")?; - let category = SoundCategoryArgumentConsumer::find_arg(args, ARG_SOURCE); - let sound = SoundArgumentConsumer::find_arg(args, ARG_SOUND); + let (category, sound) = match self.0 { + StopSoundMode::All => (None, None), + StopSoundMode::Category => { + let cat = SoundCategoryArgumentType::get(context, "source")?; + (Some(cat), None) + } + StopSoundMode::Sound => { + let snd = IdentifierArgumentType::get(context, "sound")?; + (None, Some(snd.to_string())) + } + StopSoundMode::CategoryAndSound => { + let cat = SoundCategoryArgumentType::get(context, "source")?; + let snd = IdentifierArgumentType::get(context, "sound")?; + (Some(cat), Some(snd.to_string())) + } + }; - let sound_name = sound - .as_ref() - .cloned() - .map(|s| format!("minecraft:{}", s.to_name())) - .ok(); - let cat = category.as_ref().map(|s| **s).ok(); - - for target in targets { - target.stop_sound(sound_name.clone(), cat); + for target in &targets { + target.stop_sound(sound.clone(), category); } - let text = match (category, sound) { - (Ok(c), Ok(s)) => TextComponent::translate_cross( + let text = match (category, &sound) { + (Some(c), Some(s)) => TextComponent::translate_cross( translation::java::COMMANDS_STOPSOUND_SUCCESS_SOURCE_SOUND, translation::java::COMMANDS_STOPSOUND_SUCCESS_SOURCE_SOUND, [ - TextComponent::text(s.to_name()), + TextComponent::text(s.clone()), TextComponent::text(c.to_name()), ], ), - (Ok(c), Err(_)) => TextComponent::translate_cross( + (Some(c), None) => TextComponent::translate_cross( translation::java::COMMANDS_STOPSOUND_SUCCESS_SOURCE_ANY, translation::java::COMMANDS_STOPSOUND_SUCCESS_SOURCE_ANY, [TextComponent::text(c.to_name())], ), - (Err(_), Ok(s)) => TextComponent::translate_cross( + (None, Some(s)) => TextComponent::translate_cross( translation::java::COMMANDS_STOPSOUND_SUCCESS_SOURCELESS_SOUND, translation::java::COMMANDS_STOPSOUND_SUCCESS_SOURCELESS_SOUND, - [TextComponent::text(s.to_name())], + [TextComponent::text(s.clone())], ), - (Err(_), Err(_)) => TextComponent::translate_cross( + (None, None) => TextComponent::translate_cross( translation::java::COMMANDS_STOPSOUND_SUCCESS_SOURCELESS_ANY, translation::java::COMMANDS_STOPSOUND_SUCCESS_SOURCELESS_ANY, [], ), }; - sender.send_message(text); + context.source.send_feedback(text, true); Ok(targets.len() as i32) } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).then( - argument(ARG_TARGETS, PlayersArgumentConsumer) - .execute(Executor) - .then( - argument(ARG_SOURCE, SoundCategoryArgumentConsumer) - .execute(Executor) - .then(argument(ARG_SOUND, SoundArgumentConsumer).execute(Executor)), - ), - ) +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + dispatcher.register( + command("stopsound", DESCRIPTION).requires(PERMISSION).then( + argument("targets", EntityArgumentType::Players) + .executes(StopSoundExecutor(StopSoundMode::All)) + .then( + literal("*").then( + argument("sound", IdentifierArgumentType) + .executes(StopSoundExecutor(StopSoundMode::Sound)), + ), + ) + .then( + argument("source", SoundCategoryArgumentType) + .executes(StopSoundExecutor(StopSoundMode::Category)) + .then( + argument("sound", IdentifierArgumentType) + .executes(StopSoundExecutor(StopSoundMode::CategoryAndSound)), + ), + ), + ), + ); } diff --git a/crates/pumpkin/src/command/commands/stopwatch.rs b/crates/pumpkin/src/command/commands/stopwatch.rs new file mode 100644 index 000000000..b1a0f6f7f --- /dev/null +++ b/crates/pumpkin/src/command/commands/stopwatch.rs @@ -0,0 +1,213 @@ +use pumpkin_data::translation; +use pumpkin_util::PermissionLvl; +use pumpkin_util::identifier::Identifier; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; +use pumpkin_util::text::TextComponent; + +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::core::double::DoubleArgumentType; +use crate::command::argument_types::identifier::IdentifierArgumentType; +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 crate::command::suggestion::provider::{SuggestionProvider, SuggestionProviderResult}; +use crate::command::suggestion::suggestions::SuggestionsBuilder; +use crate::world::stopwatches::{Stopwatch, Stopwatches}; + +const DESCRIPTION: &str = "Creates, queries, restarts, and removes global stopwatches."; +const PERMISSION: &str = "minecraft:command.stopwatch"; + +pub const ERROR_ALREADY_EXISTS: CommandErrorType<1> = CommandErrorType::new( + translation::java::COMMANDS_STOPWATCH_ALREADY_EXISTS, + translation::java::COMMANDS_STOPWATCH_ALREADY_EXISTS, +); + +pub const ERROR_DOES_NOT_EXIST: CommandErrorType<1> = CommandErrorType::new( + translation::java::COMMANDS_STOPWATCH_DOES_NOT_EXIST, + translation::java::COMMANDS_STOPWATCH_DOES_NOT_EXIST, +); + +struct StopwatchSuggestionProvider; + +impl SuggestionProvider for StopwatchSuggestionProvider { + fn suggest( + &self, + context: &CommandContext, + mut builder: SuggestionsBuilder, + ) -> SuggestionProviderResult { + let stopwatches = context + .server() + .stopwatches + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + for id in stopwatches.ids() { + builder = builder.suggest(id); + } + builder.build() + } +} + +struct CreateExecutor; + +impl CommandExecutor for CreateExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let id = context.get_argument::("id")?; + let id_str = id.to_string(); + let mut stopwatches = context + .server() + .stopwatches + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let now = Stopwatches::current_time(); + if !stopwatches.add(id_str.clone(), Stopwatch::new(now)) { + return Err(ERROR_ALREADY_EXISTS.create_without_context(TextComponent::text(id_str))); + } + + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_STOPWATCH_CREATE_SUCCESS, + translation::java::COMMANDS_STOPWATCH_CREATE_SUCCESS, + [TextComponent::text(id_str)], + ), + true, + ); + Ok(1) + } +} + +struct QueryExecutor { + has_scale: bool, +} + +impl CommandExecutor for QueryExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let id = context.get_argument::("id")?; + let id_str = id.to_string(); + let scale = if self.has_scale { + DoubleArgumentType::get(context, "scale")? + } else { + 1.0 + }; + + let stopwatches = context + .server() + .stopwatches + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let Some(stopwatch) = stopwatches.get(&id_str) else { + return Err(ERROR_DOES_NOT_EXIST.create_without_context(TextComponent::text(id_str))); + }; + + let now = Stopwatches::current_time(); + let elapsed_seconds = stopwatch.elapsed_seconds(now); + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_STOPWATCH_QUERY, + translation::java::COMMANDS_STOPWATCH_QUERY, + [ + TextComponent::text(id_str), + TextComponent::text(format!("{elapsed_seconds:.2}")), + ], + ), + true, + ); + Ok((elapsed_seconds * scale).floor() as i32) + } +} + +struct RestartExecutor; + +impl CommandExecutor for RestartExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let id = context.get_argument::("id")?; + let id_str = id.to_string(); + let mut stopwatches = context + .server() + .stopwatches + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let now = Stopwatches::current_time(); + if !stopwatches.update(&id_str, |_| Stopwatch::new(now)) { + return Err(ERROR_DOES_NOT_EXIST.create_without_context(TextComponent::text(id_str))); + } + + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_STOPWATCH_RESTART_SUCCESS, + translation::java::COMMANDS_STOPWATCH_RESTART_SUCCESS, + [TextComponent::text(id_str)], + ), + true, + ); + Ok(1) + } +} + +struct RemoveExecutor; + +impl CommandExecutor for RemoveExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let id = context.get_argument::("id")?; + let id_str = id.to_string(); + let mut stopwatches = context + .server() + .stopwatches + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + if !stopwatches.remove(&id_str) { + return Err(ERROR_DOES_NOT_EXIST.create_without_context(TextComponent::text(id_str))); + } + + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_STOPWATCH_REMOVE_SUCCESS, + translation::java::COMMANDS_STOPWATCH_REMOVE_SUCCESS, + [TextComponent::text(id_str)], + ), + true, + ); + Ok(1) + } +} + +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + let stopwatch_cmd = command("stopwatch", DESCRIPTION) + .requires(PERMISSION) + .then( + literal("create").then(argument("id", IdentifierArgumentType).executes(CreateExecutor)), + ) + .then( + literal("query").then( + argument("id", IdentifierArgumentType) + .suggests(StopwatchSuggestionProvider) + .executes(QueryExecutor { has_scale: false }) + .then( + argument("scale", DoubleArgumentType::any()) + .executes(QueryExecutor { has_scale: true }), + ), + ), + ) + .then( + literal("restart").then( + argument("id", IdentifierArgumentType) + .suggests(StopwatchSuggestionProvider) + .executes(RestartExecutor), + ), + ) + .then( + literal("remove").then( + argument("id", IdentifierArgumentType) + .suggests(StopwatchSuggestionProvider) + .executes(RemoveExecutor), + ), + ); + + dispatcher.register(stopwatch_cmd); +} diff --git a/crates/pumpkin/src/command/commands/summon.rs b/crates/pumpkin/src/command/commands/summon.rs index c97212c77..93eef25b6 100644 --- a/crates/pumpkin/src/command/commands/summon.rs +++ b/crates/pumpkin/src/command/commands/summon.rs @@ -1,87 +1,66 @@ -use crate::entity::EntityBase; -use crate::{ - command::{ - CommandError, CommandExecutor, CommandResult, CommandSender, - args::{ - ConsumedArgs, FindArg, position_3d::Position3DArgumentConsumer, - summonable_entities::SummonableEntitiesArgumentConsumer, - }, - tree::{CommandTree, builder::argument}, - }, - entity::r#type::from_type, -}; use pumpkin_data::translation; -use pumpkin_util::{math::vector3::Vector3, text::TextComponent}; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; +use pumpkin_util::text::TextComponent; use uuid::Uuid; -use crate::block::entities::BlockEntity; +use crate::command::argument_builder::{ArgumentBuilder, argument, command}; +use crate::command::argument_types::coordinates::vec3::Vec3ArgumentType; +use crate::command::argument_types::resource::{ENTITY_TYPE_ARGUMENT, ResourceArgument}; +use crate::command::context::command_context::CommandContext; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; +use crate::entity::r#type::from_type; -const NAMES: [&str; 1] = ["summon"]; +const DESCRIPTION: &str = "Spawns an entity at position."; +const PERMISSION: &str = "minecraft:command.summon"; -const DESCRIPTION: &str = "Spawns a Entity at position."; +struct SummonExecutor { + has_pos: bool, +} -const ARG_ENTITY: &str = "entity"; - -const ARG_POS: &str = "pos"; - -struct Executor; - -impl CommandExecutor for Executor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let entity_type = SummonableEntitiesArgumentConsumer::find_arg(args, ARG_ENTITY)?; - let pos = Position3DArgumentConsumer::find_arg(args, ARG_POS); - let (world, pos) = match sender { - CommandSender::Console | CommandSender::Rcon(_) | CommandSender::Dummy => { - let guard = server.worlds.load(); - let world = guard - .first() - .cloned() - .ok_or(CommandError::InvalidRequirement)?; - let pos = { - let info = &world.level_info.load(); - // default position for spawning a player, in this case for mob - pos.unwrap_or(Vector3::new( - f64::from(info.spawn_x) + 0.5, - f64::from(info.spawn_y) + 1.0, - f64::from(info.spawn_z) + 0.5, - )) - }; - - (world, pos) - } - CommandSender::Player(player) => { - let pos = pos.unwrap_or(player.get_entity().pos.load()); - - (player.world(), pos) - } - CommandSender::CommandBlock(c, w) => { - let pos = pos.unwrap_or(c.get_position().to_centered_f64()); - (w.clone(), pos) - } +impl CommandExecutor for SummonExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let entity_type = ResourceArgument::get_summonable_entity_type(context, "entity")?; + let pos = if self.has_pos { + Vec3ArgumentType::get_coordinates(context, "pos")?.resolve(&context.source) + } else { + context.source.position }; - let entity = from_type(entity_type, pos, &world, Uuid::new_v4()); + + let world = context.source.world(); + let entity = from_type(entity_type, pos, world, Uuid::new_v4()); let name = entity.get_display_name(); world.spawn_entity(entity); - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_SUMMON_SUCCESS, - translation::bedrock::COMMANDS_SUMMON_SUCCESS, - [name], - )); + + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_SUMMON_SUCCESS, + translation::bedrock::COMMANDS_SUMMON_SUCCESS, + [name], + ), + true, + ); Ok(1) } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).then( - argument(ARG_ENTITY, SummonableEntitiesArgumentConsumer) - .execute(Executor) - .then(argument(ARG_POS, Position3DArgumentConsumer).execute(Executor)), - // TODO: Add NBT - ) +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + dispatcher.register( + command("summon", DESCRIPTION).requires(PERMISSION).then( + argument("entity", ENTITY_TYPE_ARGUMENT.clone()) + .executes(SummonExecutor { has_pos: false }) + .then( + argument("pos", Vec3ArgumentType::Default) + .executes(SummonExecutor { has_pos: true }), + ), + ), + ); } diff --git a/crates/pumpkin/src/command/commands/teleport.rs b/crates/pumpkin/src/command/commands/teleport.rs index 22751e5e3..3f91c2147 100644 --- a/crates/pumpkin/src/command/commands/teleport.rs +++ b/crates/pumpkin/src/command/commands/teleport.rs @@ -1,44 +1,35 @@ use std::sync::Arc; use pumpkin_data::translation; +use pumpkin_util::PermissionLvl; use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::vector3::Vector3; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; -use crate::command::CommandError; -use crate::command::CommandResult; -use crate::command::args::ConsumedArgs; -use crate::command::args::FindArg; -use crate::command::args::entities::EntitiesArgumentConsumer; -use crate::command::args::entity::EntityArgumentConsumer; -use crate::command::args::position_3d::Position3DArgumentConsumer; -use crate::command::args::rotation::RotationArgumentConsumer; -use crate::command::tree::CommandTree; -use crate::command::tree::builder::{argument, literal}; -use crate::command::{CommandExecutor, CommandSender}; +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::coordinates::rotation::RotationArgumentType; +use crate::command::argument_types::coordinates::vec3::Vec3ArgumentType; +use crate::command::argument_types::entity::EntityArgumentType; +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 crate::entity::EntityBase; use crate::world::World; -const NAMES: [&str; 2] = ["teleport", "tp"]; -const DESCRIPTION: &str = "Teleports entities, including players."; // todo +const DESCRIPTION: &str = "Teleports entities, including players."; +const PERMISSION: &str = "minecraft:command.teleport"; -/// position -const ARG_LOCATION: &str = "location"; +const ERROR_NOT_PLAYER: CommandErrorType<0> = CommandErrorType::new( + translation::java::PERMISSIONS_REQUIRES_PLAYER, + translation::java::PERMISSIONS_REQUIRES_PLAYER, +); -/// single entity -const ARG_DESTINATION: &str = "destination"; - -/// multiple entities -const ARG_TARGETS: &str = "targets"; - -/// rotation: yaw/pitch -const ARG_ROTATION: &str = "rotation"; - -/// single entity -const ARG_FACING_ENTITY: &str = "facingEntity"; - -/// position -const ARG_FACING_LOCATION: &str = "facingLocation"; +const ERROR_INVALID_POSITION: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_TELEPORT_INVALIDPOSITION, + translation::java::COMMANDS_TELEPORT_INVALIDPOSITION, +); fn yaw_pitch_facing_position( looking_from: &Vector3, @@ -55,15 +46,6 @@ fn yaw_pitch_facing_position( (yaw_degrees as f32, pitch_degrees as f32) } -fn resolve_sender_world( - sender: &CommandSender, - server: &crate::server::Server, -) -> Result, CommandError> { - sender - .world_or_first(server) - .ok_or(CommandError::InvalidRequirement) -} - fn success_key_and_arg( targets: &[Arc], single_key: &'static str, @@ -76,186 +58,116 @@ fn success_key_and_arg( } } -struct EntitiesToEntityExecutor; +struct SelfToPosExecutor; -impl CommandExecutor for EntitiesToEntityExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let targets = EntitiesArgumentConsumer::find_arg(args, ARG_TARGETS)?; +impl CommandExecutor for SelfToPosExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let player = context + .source + .output + .as_player() + .ok_or_else(|| ERROR_NOT_PLAYER.create_without_context())?; - let destination = EntityArgumentConsumer::find_arg(args, ARG_DESTINATION)?; + let pos = Vec3ArgumentType::get_coordinates(context, "location")?.resolve(&context.source); + if !World::is_valid(BlockPos(pos.floor_to_i32())) { + return Err(ERROR_INVALID_POSITION.create_without_context()); + } + + let yaw = player.get_entity().yaw.load(); + let pitch = player.get_entity().pitch.load(); + let player_world = player.world(); + player.teleport(pos, Some(yaw), Some(pitch), player_world); + + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_TELEPORT_SUCCESS_LOCATION_SINGLE, + translation::bedrock::COMMANDS_TP_SUCCESS_COORDINATES, + [ + player.get_display_name(), + TextComponent::text(pos.x.to_string()), + TextComponent::text(pos.y.to_string()), + TextComponent::text(pos.z.to_string()), + ], + ), + true, + ); + + Ok(1) + } +} + +struct SelfToEntityExecutor; + +impl CommandExecutor for SelfToEntityExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let player = context + .source + .output + .as_player() + .ok_or_else(|| ERROR_NOT_PLAYER.create_without_context())?; + + let destination = EntityArgumentType::get_entity(context, "destination")?; let destination_entity = destination.get_entity(); let pos = destination_entity.pos.load(); let yaw = destination_entity.yaw.load(); let pitch = destination_entity.pitch.load(); let world = destination_entity.world.load_full(); + if !World::is_valid(BlockPos(pos.floor_to_i32())) { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_TELEPORT_INVALIDPOSITION, - translation::java::COMMANDS_TELEPORT_INVALIDPOSITION, - [], - ))); + return Err(ERROR_INVALID_POSITION.create_without_context()); } - for target in targets { + + player.teleport(pos, Some(yaw), Some(pitch), world); + + context.source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_TELEPORT_SUCCESS_ENTITY_SINGLE, + translation::bedrock::COMMANDS_TP_SUCCESSVICTIM, + [ + player.get_display_name(), + destination_entity.get_display_name(), + ], + ), + true, + ); + + Ok(1) + } +} + +struct EntitiesToEntityExecutor; + +impl CommandExecutor for EntitiesToEntityExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let targets = EntityArgumentType::get_entities(context, "targets")?; + let destination = EntityArgumentType::get_entity(context, "destination")?; + let destination_entity = destination.get_entity(); + let pos = destination_entity.pos.load(); + let yaw = destination_entity.yaw.load(); + let pitch = destination_entity.pitch.load(); + let world = destination_entity.world.load_full(); + + if !World::is_valid(BlockPos(pos.floor_to_i32())) { + return Err(ERROR_INVALID_POSITION.create_without_context()); + } + + for target in &targets { target.teleport(pos, Some(yaw), Some(pitch), world.clone()); } let (key, target_arg) = success_key_and_arg( - targets, + &targets, translation::java::COMMANDS_TELEPORT_SUCCESS_ENTITY_SINGLE, translation::java::COMMANDS_TELEPORT_SUCCESS_ENTITY_MULTIPLE, ); - sender.send_message(TextComponent::translate_cross( - key, - translation::bedrock::COMMANDS_TP_SUCCESSVICTIM, - [target_arg, destination_entity.get_display_name()], - )); - - Ok(targets.len() as i32) - } -} - -struct EntitiesToPosFacingPosExecutor; - -impl CommandExecutor for EntitiesToPosFacingPosExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let targets = EntitiesArgumentConsumer::find_arg(args, ARG_TARGETS)?; - - let pos = Position3DArgumentConsumer::find_arg(args, ARG_LOCATION)?; - if !World::is_valid(BlockPos(pos.floor_to_i32())) { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_TELEPORT_INVALIDPOSITION, - translation::java::COMMANDS_TELEPORT_INVALIDPOSITION, - [], - ))); - } - let facing_pos = Position3DArgumentConsumer::find_arg(args, ARG_FACING_LOCATION)?; - let (yaw, pitch) = yaw_pitch_facing_position(&pos, &facing_pos); - let world = resolve_sender_world(sender, server)?; - - for target in targets { - target.teleport(pos, Some(yaw), Some(pitch), world.clone()); - } - - let (key, target_arg) = success_key_and_arg( - targets, - translation::java::COMMANDS_TELEPORT_SUCCESS_LOCATION_SINGLE, - translation::java::COMMANDS_TELEPORT_SUCCESS_LOCATION_MULTIPLE, + context.source.send_feedback( + TextComponent::translate_cross( + key, + translation::bedrock::COMMANDS_TP_SUCCESSVICTIM, + [target_arg, destination_entity.get_display_name()], + ), + true, ); - sender.send_message(TextComponent::translate_cross( - key, - translation::bedrock::COMMANDS_TP_SUCCESS_COORDINATES, - [ - target_arg, - TextComponent::text(pos.x.to_string()), - TextComponent::text(pos.y.to_string()), - TextComponent::text(pos.z.to_string()), - ], - )); - - Ok(targets.len() as i32) - } -} - -struct EntitiesToPosFacingEntityExecutor; - -impl CommandExecutor for EntitiesToPosFacingEntityExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let targets = EntitiesArgumentConsumer::find_arg(args, ARG_TARGETS)?; - - let pos = Position3DArgumentConsumer::find_arg(args, ARG_LOCATION)?; - if !World::is_valid(BlockPos(pos.floor_to_i32())) { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_TELEPORT_INVALIDPOSITION, - translation::java::COMMANDS_TELEPORT_INVALIDPOSITION, - [], - ))); - } - let facing_entity = EntityArgumentConsumer::find_arg(args, ARG_FACING_ENTITY)?; - let (yaw, pitch) = yaw_pitch_facing_position(&pos, &facing_entity.get_entity().pos.load()); - let world = resolve_sender_world(sender, server)?; - - for target in targets { - target.teleport(pos, Some(yaw), Some(pitch), world.clone()); - } - - let (key, target_arg) = success_key_and_arg( - targets, - translation::java::COMMANDS_TELEPORT_SUCCESS_LOCATION_SINGLE, - translation::java::COMMANDS_TELEPORT_SUCCESS_LOCATION_MULTIPLE, - ); - sender.send_message(TextComponent::translate_cross( - key, - translation::bedrock::COMMANDS_TP_SUCCESS_COORDINATES, - [ - target_arg, - TextComponent::text(pos.x.to_string()), - TextComponent::text(pos.y.to_string()), - TextComponent::text(pos.z.to_string()), - ], - )); - - Ok(targets.len() as i32) - } -} - -struct EntitiesToPosWithRotationExecutor; - -impl CommandExecutor for EntitiesToPosWithRotationExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let targets = EntitiesArgumentConsumer::find_arg(args, ARG_TARGETS)?; - - let pos = Position3DArgumentConsumer::find_arg(args, ARG_LOCATION)?; - if !World::is_valid(BlockPos(pos.floor_to_i32())) { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_TELEPORT_INVALIDPOSITION, - translation::java::COMMANDS_TELEPORT_INVALIDPOSITION, - [], - ))); - } - // Note: Rotation returns (yaw, is_yaw_relative, pitch, is_pitch_relative) - // For teleport, we use absolute values only (ignore relative flags) - let (yaw, _, pitch, _) = RotationArgumentConsumer::find_arg(args, ARG_ROTATION)?; - let world = resolve_sender_world(sender, server)?; - - for target in targets { - target.teleport(pos, Some(yaw), Some(pitch), world.clone()); - } - - let (key, target_arg) = success_key_and_arg( - targets, - translation::java::COMMANDS_TELEPORT_SUCCESS_LOCATION_SINGLE, - translation::java::COMMANDS_TELEPORT_SUCCESS_LOCATION_MULTIPLE, - ); - sender.send_message(TextComponent::translate_cross( - key, - translation::bedrock::COMMANDS_TP_SUCCESS_COORDINATES, - [ - target_arg, - TextComponent::text(pos.x.to_string()), - TextComponent::text(pos.y.to_string()), - TextComponent::text(pos.z.to_string()), - ], - )); Ok(targets.len() as i32) } @@ -264,172 +176,211 @@ impl CommandExecutor for EntitiesToPosWithRotationExecutor { struct EntitiesToPosExecutor; impl CommandExecutor for EntitiesToPosExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let targets = EntitiesArgumentConsumer::find_arg(args, ARG_TARGETS)?; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let targets = EntityArgumentType::get_entities(context, "targets")?; + let pos = Vec3ArgumentType::get_coordinates(context, "location")?.resolve(&context.source); - let pos = Position3DArgumentConsumer::find_arg(args, ARG_LOCATION)?; if !World::is_valid(BlockPos(pos.floor_to_i32())) { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_TELEPORT_INVALIDPOSITION, - translation::java::COMMANDS_TELEPORT_INVALIDPOSITION, - [], - ))); + return Err(ERROR_INVALID_POSITION.create_without_context()); } - let world = resolve_sender_world(sender, server)?; - for target in targets { + + let world = context.source.world(); + for target in &targets { let yaw = target.get_entity().yaw.load(); let pitch = target.get_entity().pitch.load(); target.teleport(pos, Some(yaw), Some(pitch), world.clone()); } let (key, target_arg) = success_key_and_arg( - targets, + &targets, translation::java::COMMANDS_TELEPORT_SUCCESS_LOCATION_SINGLE, translation::java::COMMANDS_TELEPORT_SUCCESS_LOCATION_MULTIPLE, ); - sender.send_message(TextComponent::translate_cross( - key, - translation::bedrock::COMMANDS_TP_SUCCESS_COORDINATES, - [ - target_arg, - TextComponent::text(pos.x.to_string()), - TextComponent::text(pos.y.to_string()), - TextComponent::text(pos.z.to_string()), - ], - )); + context.source.send_feedback( + TextComponent::translate_cross( + key, + translation::bedrock::COMMANDS_TP_SUCCESS_COORDINATES, + [ + target_arg, + TextComponent::text(pos.x.to_string()), + TextComponent::text(pos.y.to_string()), + TextComponent::text(pos.z.to_string()), + ], + ), + true, + ); Ok(targets.len() as i32) } } -struct SelfToEntityExecutor; +struct EntitiesToPosWithRotationExecutor; -impl CommandExecutor for SelfToEntityExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let destination = EntityArgumentConsumer::find_arg(args, ARG_DESTINATION)?; - let destination_entity = destination.get_entity(); - let pos = destination_entity.pos.load(); - let yaw = destination_entity.yaw.load(); - let pitch = destination_entity.pitch.load(); - let world = destination_entity.world.load_full(); +impl CommandExecutor for EntitiesToPosWithRotationExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let targets = EntityArgumentType::get_entities(context, "targets")?; + let pos = Vec3ArgumentType::get_coordinates(context, "location")?.resolve(&context.source); - match sender { - CommandSender::Player(player) => { - if !World::is_valid(BlockPos(pos.floor_to_i32())) { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_TELEPORT_INVALIDPOSITION, - translation::java::COMMANDS_TELEPORT_INVALIDPOSITION, - [], - ))); - } - player.teleport(pos, Some(yaw), Some(pitch), world); - - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_TELEPORT_SUCCESS_ENTITY_SINGLE, - translation::bedrock::COMMANDS_TP_SUCCESSVICTIM, - [ - player.get_display_name(), - destination_entity.get_display_name(), - ], - )); - - Ok(1) - } - _ => Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::PERMISSIONS_REQUIRES_PLAYER, - translation::java::PERMISSIONS_REQUIRES_PLAYER, - [], - ))), + if !World::is_valid(BlockPos(pos.floor_to_i32())) { + return Err(ERROR_INVALID_POSITION.create_without_context()); } + + let rot = RotationArgumentType::get(context, "rotation")?.rotation(&context.source); + let yaw = rot.y; + let pitch = rot.x; + let world = context.source.world(); + + for target in &targets { + target.teleport(pos, Some(yaw), Some(pitch), world.clone()); + } + + let (key, target_arg) = success_key_and_arg( + &targets, + translation::java::COMMANDS_TELEPORT_SUCCESS_LOCATION_SINGLE, + translation::java::COMMANDS_TELEPORT_SUCCESS_LOCATION_MULTIPLE, + ); + context.source.send_feedback( + TextComponent::translate_cross( + key, + translation::bedrock::COMMANDS_TP_SUCCESS_COORDINATES, + [ + target_arg, + TextComponent::text(pos.x.to_string()), + TextComponent::text(pos.y.to_string()), + TextComponent::text(pos.z.to_string()), + ], + ), + true, + ); + + Ok(targets.len() as i32) } } -struct SelfToPosExecutor; +struct EntitiesToPosFacingPosExecutor; -impl CommandExecutor for SelfToPosExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - match sender { - CommandSender::Player(player) => { - let pos = Position3DArgumentConsumer::find_arg(args, ARG_LOCATION)?; - let yaw = player.get_entity().yaw.load(); - let pitch = player.get_entity().pitch.load(); - if !World::is_valid(BlockPos(pos.floor_to_i32())) { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::COMMANDS_TELEPORT_INVALIDPOSITION, - translation::java::COMMANDS_TELEPORT_INVALIDPOSITION, - [], - ))); - } - let player_world = player.world(); - player.teleport(pos, Some(yaw), Some(pitch), player_world); +impl CommandExecutor for EntitiesToPosFacingPosExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let targets = EntityArgumentType::get_entities(context, "targets")?; + let pos = Vec3ArgumentType::get_coordinates(context, "location")?.resolve(&context.source); - sender.send_message(TextComponent::translate_cross( - translation::java::COMMANDS_TELEPORT_SUCCESS_LOCATION_SINGLE, - translation::bedrock::COMMANDS_TP_SUCCESS_COORDINATES, - [ - player.get_display_name(), - TextComponent::text(pos.x.to_string()), - TextComponent::text(pos.y.to_string()), - TextComponent::text(pos.z.to_string()), - ], - )); - - Ok(1) - } - _ => Err(CommandError::CommandFailed(TextComponent::translate_cross( - translation::java::PERMISSIONS_REQUIRES_PLAYER, - translation::java::PERMISSIONS_REQUIRES_PLAYER, - [], - ))), + if !World::is_valid(BlockPos(pos.floor_to_i32())) { + return Err(ERROR_INVALID_POSITION.create_without_context()); } + + let facing_pos = + Vec3ArgumentType::get_coordinates(context, "facingLocation")?.resolve(&context.source); + let (yaw, pitch) = yaw_pitch_facing_position(&pos, &facing_pos); + let world = context.source.world(); + + for target in &targets { + target.teleport(pos, Some(yaw), Some(pitch), world.clone()); + } + + let (key, target_arg) = success_key_and_arg( + &targets, + translation::java::COMMANDS_TELEPORT_SUCCESS_LOCATION_SINGLE, + translation::java::COMMANDS_TELEPORT_SUCCESS_LOCATION_MULTIPLE, + ); + context.source.send_feedback( + TextComponent::translate_cross( + key, + translation::bedrock::COMMANDS_TP_SUCCESS_COORDINATES, + [ + target_arg, + TextComponent::text(pos.x.to_string()), + TextComponent::text(pos.y.to_string()), + TextComponent::text(pos.z.to_string()), + ], + ), + true, + ); + + Ok(targets.len() as i32) } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION) - .then(argument(ARG_LOCATION, Position3DArgumentConsumer).execute(SelfToPosExecutor)) - .then(argument(ARG_DESTINATION, EntityArgumentConsumer).execute(SelfToEntityExecutor)) +struct EntitiesToPosFacingEntityExecutor; + +impl CommandExecutor for EntitiesToPosFacingEntityExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let targets = EntityArgumentType::get_entities(context, "targets")?; + let pos = Vec3ArgumentType::get_coordinates(context, "location")?.resolve(&context.source); + + if !World::is_valid(BlockPos(pos.floor_to_i32())) { + return Err(ERROR_INVALID_POSITION.create_without_context()); + } + + let facing_entity = EntityArgumentType::get_entity(context, "facingEntity")?; + let (yaw, pitch) = yaw_pitch_facing_position(&pos, &facing_entity.get_entity().pos.load()); + let world = context.source.world(); + + for target in &targets { + target.teleport(pos, Some(yaw), Some(pitch), world.clone()); + } + + let (key, target_arg) = success_key_and_arg( + &targets, + translation::java::COMMANDS_TELEPORT_SUCCESS_LOCATION_SINGLE, + translation::java::COMMANDS_TELEPORT_SUCCESS_LOCATION_MULTIPLE, + ); + context.source.send_feedback( + TextComponent::translate_cross( + key, + translation::bedrock::COMMANDS_TP_SUCCESS_COORDINATES, + [ + target_arg, + TextComponent::text(pos.x.to_string()), + TextComponent::text(pos.y.to_string()), + TextComponent::text(pos.z.to_string()), + ], + ), + true, + ); + + Ok(targets.len() as i32) + } +} + +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + let cmd = command("teleport", DESCRIPTION) + .requires(PERMISSION) + .then(argument("location", Vec3ArgumentType::Default).executes(SelfToPosExecutor)) + .then(argument("destination", EntityArgumentType::Entity).executes(SelfToEntityExecutor)) .then( - argument(ARG_TARGETS, EntitiesArgumentConsumer) + argument("targets", EntityArgumentType::Entities) .then( - argument(ARG_LOCATION, Position3DArgumentConsumer) - .execute(EntitiesToPosExecutor) + argument("location", Vec3ArgumentType::Default) + .executes(EntitiesToPosExecutor) .then( - argument(ARG_ROTATION, RotationArgumentConsumer) - .execute(EntitiesToPosWithRotationExecutor), + argument("rotation", RotationArgumentType) + .executes(EntitiesToPosWithRotationExecutor), ) .then( literal("facing") .then( literal("entity").then( - argument(ARG_FACING_ENTITY, EntityArgumentConsumer) - .execute(EntitiesToPosFacingEntityExecutor), + argument("facingEntity", EntityArgumentType::Entity) + .executes(EntitiesToPosFacingEntityExecutor), ), ) .then( - argument(ARG_FACING_LOCATION, Position3DArgumentConsumer) - .execute(EntitiesToPosFacingPosExecutor), + argument("facingLocation", Vec3ArgumentType::Default) + .executes(EntitiesToPosFacingPosExecutor), ), ), ) .then( - argument(ARG_DESTINATION, EntityArgumentConsumer) - .execute(EntitiesToEntityExecutor), + argument("destination", EntityArgumentType::Entity) + .executes(EntitiesToEntityExecutor), ), - ) + ); + + dispatcher.register_with_aliases(cmd, &["tp"]); } diff --git a/crates/pumpkin/src/command/commands/tellraw.rs b/crates/pumpkin/src/command/commands/tellraw.rs index c59fcc81f..fb7c141ed 100644 --- a/crates/pumpkin/src/command/commands/tellraw.rs +++ b/crates/pumpkin/src/command/commands/tellraw.rs @@ -1,44 +1,42 @@ -use crate::command::{ - CommandError, CommandExecutor, CommandResult, CommandSender, - args::{ - Arg, ConsumedArgs, FindArg, players::PlayersArgumentConsumer, - textcomponent::TextComponentArgConsumer, - }, - tree::{CommandTree, builder::argument}, -}; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; -const NAMES: [&str; 1] = ["tellraw"]; +use crate::command::argument_builder::{ArgumentBuilder, argument, command}; +use crate::command::argument_types::component::ComponentArgumentType; +use crate::command::argument_types::entity::EntityArgumentType; +use crate::command::context::command_context::CommandContext; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; const DESCRIPTION: &str = "Send raw message to players."; - -const ARG_TARGETS: &str = "targets"; - -const ARG_MESSAGE: &str = "message"; +const PERMISSION: &str = "minecraft:command.tellraw"; struct TellRawExecutor; impl CommandExecutor for TellRawExecutor { - fn execute( - &self, - _sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::Players(targets)) = args.get(ARG_TARGETS) else { - return Err(CommandError::InvalidConsumption(Some(ARG_TARGETS.into()))); - }; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let targets = EntityArgumentType::get_players(context, "targets")?; + let text = ComponentArgumentType::get(context, "message")?; - let text = TextComponentArgConsumer::find_arg(args, ARG_MESSAGE)?; - for target in targets { + for target in &targets { target.send_system_message(&text); } + Ok(targets.len() as i32) } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).then( - argument(ARG_TARGETS, PlayersArgumentConsumer) - .then(argument(ARG_MESSAGE, TextComponentArgConsumer).execute(TellRawExecutor)), - ) +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + dispatcher.register( + command("tellraw", DESCRIPTION).requires(PERMISSION).then( + argument("targets", EntityArgumentType::Players) + .then(argument("message", ComponentArgumentType).executes(TellRawExecutor)), + ), + ); } diff --git a/crates/pumpkin/src/command/commands/time.rs b/crates/pumpkin/src/command/commands/time.rs index 980ce2711..21bb97a29 100644 --- a/crates/pumpkin/src/command/commands/time.rs +++ b/crates/pumpkin/src/command/commands/time.rs @@ -1,20 +1,18 @@ use pumpkin_data::translation; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; -use crate::command::args::{ - FindArg, bounded_num::BoundedNumArgumentConsumer, - resource_location::ResourceLocationArgumentConsumer, time::TimeArgumentConsumer, -}; -use crate::command::tree::builder::{argument, literal}; -use crate::command::{ - CommandError, CommandExecutor, CommandResult, CommandSender, ConsumedArgs, tree::CommandTree, -}; +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::core::float::FloatArgumentType; +use crate::command::argument_types::identifier::IdentifierArgumentType; +use crate::command::argument_types::time::TimeArgumentType; +use crate::command::context::command_context::CommandContext; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; -const NAMES: [&str; 1] = ["time"]; const DESCRIPTION: &str = "Query or modify the world time and clocks."; -const ARG_TIME: &str = "time"; -const ARG_RATE: &str = "rate"; -const ARG_CLOCK: &str = "clock"; +const PERMISSION: &str = "minecraft:command.time"; const DEFAULT_CLOCK: &str = "minecraft:overworld"; @@ -58,90 +56,104 @@ const fn wrap_time(ticks: i64) -> i32 { (ticks % 2_147_483_647) as i32 } -struct QueryExecutor(QueryMode); +struct QueryExecutor { + mode: QueryMode, + has_clock: bool, +} impl CommandExecutor for QueryExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let clock_name = ResourceLocationArgumentConsumer::find_arg(args, ARG_CLOCK) - .map_or_else(|_| DEFAULT_CLOCK.to_string(), ToString::to_string); - let mode = self.0; - let worlds = server.worlds.load(); - let world = worlds.first().ok_or(CommandError::InvalidRequirement)?; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let clock_name = if self.has_clock { + IdentifierArgumentType::get(context, "clock") + .map_or_else(|_| DEFAULT_CLOCK.to_string(), |id| id.to_string()) + } else { + DEFAULT_CLOCK.to_string() + }; + + let world = context.source.world(); let level_time = world .level_time .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone(); - match mode { + match self.mode { QueryMode::GameTime => { let game_time = level_time.query_gametime(); - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_TIME_QUERY_GAMETIME, - translation::bedrock::COMMANDS_TIME_QUERY_GAMETIME, - TextComponent::text(game_time.to_string()) - )); + context.source.send_feedback( + pumpkin_macros::translate_cross!( + translation::java::COMMANDS_TIME_QUERY_GAMETIME, + translation::bedrock::COMMANDS_TIME_QUERY_GAMETIME, + TextComponent::text(game_time.to_string()) + ), + false, + ); Ok(wrap_time(game_time)) } QueryMode::Time => { let total_ticks = level_time.time_of_day; - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_TIME_QUERY_ABSOLUTE, - translation::bedrock::COMMANDS_TIME_QUERY_DAYTIME, - TextComponent::text(clock_name), - TextComponent::text(total_ticks.to_string()) - )); + context.source.send_feedback( + pumpkin_macros::translate_cross!( + translation::java::COMMANDS_TIME_QUERY_ABSOLUTE, + translation::bedrock::COMMANDS_TIME_QUERY_DAYTIME, + TextComponent::text(clock_name), + TextComponent::text(total_ticks.to_string()) + ), + false, + ); Ok(wrap_time(total_ticks)) } QueryMode::DayTime => { let curr_time = level_time.query_daytime(); - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_TIME_QUERY, - translation::bedrock::COMMANDS_TIME_QUERY_DAYTIME, - TextComponent::text(curr_time.to_string()) - )); + context.source.send_feedback( + pumpkin_macros::translate_cross!( + translation::java::COMMANDS_TIME_QUERY, + translation::bedrock::COMMANDS_TIME_QUERY_DAYTIME, + TextComponent::text(curr_time.to_string()) + ), + false, + ); Ok(wrap_time(curr_time)) } QueryMode::Day => { let curr_time = level_time.query_day(); - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_TIME_QUERY, - translation::bedrock::COMMANDS_TIME_QUERY_DAY, - TextComponent::text(curr_time.to_string()) - )); + context.source.send_feedback( + pumpkin_macros::translate_cross!( + translation::java::COMMANDS_TIME_QUERY, + translation::bedrock::COMMANDS_TIME_QUERY_DAY, + TextComponent::text(curr_time.to_string()) + ), + false, + ); Ok(curr_time as i32) } } } } -struct ActionExecutor(Action); +struct ActionExecutor { + action: Action, + has_clock: bool, +} impl CommandExecutor for ActionExecutor { #[allow(clippy::too_many_lines)] - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let clock_name = ResourceLocationArgumentConsumer::find_arg(args, ARG_CLOCK) - .map_or_else(|_| DEFAULT_CLOCK.to_string(), ToString::to_string); - let action = self.0; - let worlds = server.worlds.load(); - let world = worlds.first().ok_or(CommandError::InvalidRequirement)?; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let clock_name = if self.has_clock { + IdentifierArgumentType::get(context, "clock") + .map_or_else(|_| DEFAULT_CLOCK.to_string(), |id| id.to_string()) + } else { + DEFAULT_CLOCK.to_string() + }; - match action { + let world = context.source.world(); + + match self.action { Action::Set(preset) => { let time_count = if let Some(p) = preset { p.to_ticks() } else { - TimeArgumentConsumer::find_arg(args, ARG_TIME)? + TimeArgumentType::get(context, "time")? }; let level_time = { let mut guard = world @@ -152,16 +164,19 @@ impl CommandExecutor for ActionExecutor { guard.clone() }; level_time.send_time(world); - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_TIME_SET_ABSOLUTE, - translation::bedrock::COMMANDS_TIME_SET, - TextComponent::text(clock_name), - TextComponent::text(time_count.to_string()) - )); + context.source.send_feedback( + pumpkin_macros::translate_cross!( + translation::java::COMMANDS_TIME_SET_ABSOLUTE, + translation::bedrock::COMMANDS_TIME_SET, + TextComponent::text(clock_name), + TextComponent::text(time_count.to_string()) + ), + true, + ); Ok(time_count) } Action::Add => { - let time_count = TimeArgumentConsumer::find_arg(args, ARG_TIME)?; + let time_count = TimeArgumentType::get(context, "time")?; let (level_time, total_ticks) = { let mut guard = world .level_time @@ -172,12 +187,15 @@ impl CommandExecutor for ActionExecutor { (guard.clone(), total_ticks) }; level_time.send_time(world); - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_TIME_SET_ABSOLUTE, - translation::bedrock::COMMANDS_TIME_ADDED, - TextComponent::text(clock_name), - TextComponent::text(total_ticks.to_string()) - )); + context.source.send_feedback( + pumpkin_macros::translate_cross!( + translation::java::COMMANDS_TIME_SET_ABSOLUTE, + translation::bedrock::COMMANDS_TIME_ADDED, + TextComponent::text(clock_name), + TextComponent::text(total_ticks.to_string()) + ), + true, + ); Ok(wrap_time(total_ticks)) } Action::Pause => { @@ -190,11 +208,14 @@ impl CommandExecutor for ActionExecutor { guard.clone() }; level_time.send_time(world); - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_TIME_PAUSE, - translation::bedrock::COMMANDS_TIME_STOP, - TextComponent::text(clock_name) - )); + context.source.send_feedback( + pumpkin_macros::translate_cross!( + translation::java::COMMANDS_TIME_PAUSE, + translation::bedrock::COMMANDS_TIME_STOP, + TextComponent::text(clock_name) + ), + true, + ); Ok(1) } Action::Resume => { @@ -207,16 +228,18 @@ impl CommandExecutor for ActionExecutor { guard.clone() }; level_time.send_time(world); - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_TIME_RESUME, - translation::bedrock::COMMANDS_TIME_SET, - TextComponent::text(clock_name) - )); + context.source.send_feedback( + pumpkin_macros::translate_cross!( + translation::java::COMMANDS_TIME_RESUME, + translation::bedrock::COMMANDS_TIME_SET, + TextComponent::text(clock_name) + ), + true, + ); Ok(1) } Action::Rate => { - let rate_res = BoundedNumArgumentConsumer::::find_arg(args, ARG_RATE)?; - let rate = rate_res?; + let rate = FloatArgumentType::get(context, "rate")?; let level_time = { let mut guard = world .level_time @@ -226,111 +249,112 @@ impl CommandExecutor for ActionExecutor { guard.clone() }; level_time.send_time(world); - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_TIME_RATE, - translation::bedrock::COMMANDS_TIME_SET, - TextComponent::text(clock_name), - TextComponent::text(rate.to_string()) - )); + context.source.send_feedback( + pumpkin_macros::translate_cross!( + translation::java::COMMANDS_TIME_RATE, + translation::bedrock::COMMANDS_TIME_SET, + TextComponent::text(clock_name), + TextComponent::text(rate.to_string()) + ), + true, + ); Ok(1) } } } } -pub fn init_command_tree() -> CommandTree { - let set_node = literal("set") - .then(literal("day").execute(ActionExecutor(Action::Set(Some(PresetTime::Day))))) - .then(literal("noon").execute(ActionExecutor(Action::Set(Some(PresetTime::Noon))))) - .then(literal("night").execute(ActionExecutor(Action::Set(Some(PresetTime::Night))))) - .then(literal("midnight").execute(ActionExecutor(Action::Set(Some(PresetTime::Midnight))))) - .then( - argument(ARG_TIME, TimeArgumentConsumer::new()) - .execute(ActionExecutor(Action::Set(None))), +macro_rules! build_branches { + ($builder:expr, $has_clock:expr) => {{ + let set_node = literal("set") + .then(literal("day").executes(ActionExecutor { + action: Action::Set(Some(PresetTime::Day)), + has_clock: $has_clock, + })) + .then(literal("noon").executes(ActionExecutor { + action: Action::Set(Some(PresetTime::Noon)), + has_clock: $has_clock, + })) + .then(literal("night").executes(ActionExecutor { + action: Action::Set(Some(PresetTime::Night)), + has_clock: $has_clock, + })) + .then(literal("midnight").executes(ActionExecutor { + action: Action::Set(Some(PresetTime::Midnight)), + has_clock: $has_clock, + })) + .then( + argument("time", TimeArgumentType::any()).executes(ActionExecutor { + action: Action::Set(None), + has_clock: $has_clock, + }), + ); + + let add_node = literal("add").then( + argument("time", TimeArgumentType::new(i32::MIN)).executes(ActionExecutor { + action: Action::Add, + has_clock: $has_clock, + }), ); - let add_node = literal("add").then( - argument(ARG_TIME, TimeArgumentConsumer::min(i32::MIN)) - .execute(ActionExecutor(Action::Add)), - ); + let pause_node = literal("pause").executes(ActionExecutor { + action: Action::Pause, + has_clock: $has_clock, + }); + let resume_node = literal("resume").executes(ActionExecutor { + action: Action::Resume, + has_clock: $has_clock, + }); - let pause_node = literal("pause").execute(ActionExecutor(Action::Pause)); - let resume_node = literal("resume").execute(ActionExecutor(Action::Resume)); + let rate_node = literal("rate").then( + argument("rate", FloatArgumentType::new(1.0e-5, 1000.0)).executes(ActionExecutor { + action: Action::Rate, + has_clock: $has_clock, + }), + ); - let rate_node = literal("rate").then( - argument( - ARG_RATE, - BoundedNumArgumentConsumer::::new() - .min(1.0e-5) - .max(1000.0), - ) - .execute(ActionExecutor(Action::Rate)), - ); + let query_node = literal("query") + .then(literal("time").executes(QueryExecutor { + mode: QueryMode::Time, + has_clock: $has_clock, + })) + .then(literal("gametime").executes(QueryExecutor { + mode: QueryMode::GameTime, + has_clock: $has_clock, + })) + .then(literal("daytime").executes(QueryExecutor { + mode: QueryMode::DayTime, + has_clock: $has_clock, + })) + .then(literal("day").executes(QueryExecutor { + mode: QueryMode::Day, + has_clock: $has_clock, + })); - let query_node = literal("query") - .then(literal("time").execute(QueryExecutor(QueryMode::Time))) - .then(literal("gametime").execute(QueryExecutor(QueryMode::GameTime))) - .then(literal("daytime").execute(QueryExecutor(QueryMode::DayTime))) - .then(literal("day").execute(QueryExecutor(QueryMode::Day))); - - let of_clock_node = literal("of").then( - argument(ARG_CLOCK, ResourceLocationArgumentConsumer) - .then( - literal("set") - .then( - literal("day").execute(ActionExecutor(Action::Set(Some(PresetTime::Day)))), - ) - .then( - literal("noon") - .execute(ActionExecutor(Action::Set(Some(PresetTime::Noon)))), - ) - .then( - literal("night") - .execute(ActionExecutor(Action::Set(Some(PresetTime::Night)))), - ) - .then( - literal("midnight") - .execute(ActionExecutor(Action::Set(Some(PresetTime::Midnight)))), - ) - .then( - argument(ARG_TIME, TimeArgumentConsumer::new()) - .execute(ActionExecutor(Action::Set(None))), - ), - ) - .then( - literal("add").then( - argument(ARG_TIME, TimeArgumentConsumer::min(i32::MIN)) - .execute(ActionExecutor(Action::Add)), - ), - ) - .then(literal("pause").execute(ActionExecutor(Action::Pause))) - .then(literal("resume").execute(ActionExecutor(Action::Resume))) - .then( - literal("rate").then( - argument( - ARG_RATE, - BoundedNumArgumentConsumer::::new() - .min(1.0e-5) - .max(1000.0), - ) - .execute(ActionExecutor(Action::Rate)), - ), - ) - .then( - literal("query") - .then(literal("time").execute(QueryExecutor(QueryMode::Time))) - .then(literal("gametime").execute(QueryExecutor(QueryMode::GameTime))) - .then(literal("daytime").execute(QueryExecutor(QueryMode::DayTime))) - .then(literal("day").execute(QueryExecutor(QueryMode::Day))), - ), - ); - - CommandTree::new(NAMES, DESCRIPTION) - .then(set_node) - .then(add_node) - .then(pause_node) - .then(resume_node) - .then(rate_node) - .then(query_node) - .then(of_clock_node) + $builder + .then(set_node) + .then(add_node) + .then(pause_node) + .then(resume_node) + .then(rate_node) + .then(query_node) + }}; +} + +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + let of_clock_node = literal("of").then(build_branches!( + argument("clock", IdentifierArgumentType), + true + )); + + let time_cmd = build_branches!(command("time", DESCRIPTION).requires(PERMISSION), false) + .then(of_clock_node); + + dispatcher.register(time_cmd); } diff --git a/crates/pumpkin/src/command/commands/title.rs b/crates/pumpkin/src/command/commands/title.rs index d8a6c88f2..113fc984d 100644 --- a/crates/pumpkin/src/command/commands/title.rs +++ b/crates/pumpkin/src/command/commands/title.rs @@ -1,57 +1,39 @@ use pumpkin_protocol::java::client::play::CClearTitle; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; -use crate::command::CommandResult; -use crate::command::args::time::TimeArgumentConsumer; +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::component::ComponentArgumentType; +use crate::command::argument_types::entity::EntityArgumentType; +use crate::command::argument_types::time::TimeArgumentType; +use crate::command::context::command_context::CommandContext; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; use crate::entity::EntityBase; -use crate::{ - command::{ - CommandError, CommandExecutor, CommandSender, - args::{ - Arg, ConsumedArgs, FindArg, players::PlayersArgumentConsumer, - textcomponent::TextComponentArgConsumer, - }, - tree::CommandTree, - tree::builder::{argument, literal}, - }, - entity::player::TitleMode, -}; - -const NAMES: [&str; 1] = ["title"]; +use crate::entity::player::TitleMode; const DESCRIPTION: &str = "Displays a title."; +const PERMISSION: &str = "minecraft:command.title"; -const ARG_TARGETS: &str = "targets"; - -const ARG_TITLE: &str = "title"; -const ARG_FADE_IN: &str = "fadeIn"; -const ARG_STAY: &str = "stay"; -const ARG_FADE_OUT: &str = "fadeOut"; -/// bool: Whether to reset or not struct ClearOrResetExecutor(bool); impl CommandExecutor for ClearOrResetExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::Players(targets)) = args.get(ARG_TARGETS) else { - return Err(CommandError::InvalidConsumption(Some(ARG_TARGETS.into()))); - }; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let targets = EntityArgumentType::get_players(context, "targets")?; let reset = self.0; - for target in targets { + for target in &targets { target.try_send_client_packet(&CClearTitle::new(reset)); } - sender.send_message(if targets.len() == 1 { + + let msg = if targets.len() == 1 { let text = if reset { "commands.title.reset.single" } else { "commands.title.cleared.single" }; - TextComponent::translate_cross(text, text, [targets[0].get_display_name()]) + TextComponent::translate_cross(text, text, [targets[0].as_ref().get_display_name()]) } else { let text = if reset { "commands.title.reset.multiple" @@ -63,7 +45,8 @@ impl CommandExecutor for ClearOrResetExecutor { text, [TextComponent::text(targets.len().to_string())], ) - }); + }; + context.source.send_feedback(msg, true); Ok(targets.len() as i32) } @@ -72,30 +55,21 @@ impl CommandExecutor for ClearOrResetExecutor { struct TitleExecutor(TitleMode); impl CommandExecutor for TitleExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::Players(targets)) = args.get(ARG_TARGETS) else { - return Err(CommandError::InvalidConsumption(Some(ARG_TARGETS.into()))); - }; - - let text = TextComponentArgConsumer::find_arg(args, ARG_TITLE)?; - + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let targets = EntityArgumentType::get_players(context, "targets")?; + let text = ComponentArgumentType::get(context, "title")?; let mode = self.0; - for target in targets { + for target in &targets { target.show_title(&text, &mode); } let mode_name = format!("{mode:?}").to_lowercase(); - sender.send_message(if targets.len() == 1 { + let msg = if targets.len() == 1 { TextComponent::translate_cross( format!("commands.title.show.{mode_name}.single"), format!("commands.title.show.{mode_name}.single"), - [targets[0].get_display_name()], + [targets[0].as_ref().get_display_name()], ) } else { TextComponent::translate_cross( @@ -103,7 +77,8 @@ impl CommandExecutor for TitleExecutor { format!("commands.title.show.{mode_name}.multiple"), [TextComponent::text(targets.len().to_string())], ) - }); + }; + context.source.send_feedback(msg, true); Ok(targets.len() as i32) } @@ -112,29 +87,21 @@ impl CommandExecutor for TitleExecutor { struct TimesTitleExecutor; impl CommandExecutor for TimesTitleExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::Players(targets)) = args.get(ARG_TARGETS) else { - return Err(CommandError::InvalidConsumption(Some(ARG_TARGETS.into()))); - }; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let targets = EntityArgumentType::get_players(context, "targets")?; + let fade_in = TimeArgumentType::get(context, "fadeIn")?; + let stay = TimeArgumentType::get(context, "stay")?; + let fade_out = TimeArgumentType::get(context, "fadeOut")?; - let fade_in = TimeArgumentConsumer::find_arg(args, ARG_FADE_IN)?; - let stay = TimeArgumentConsumer::find_arg(args, ARG_STAY)?; - let fade_out = TimeArgumentConsumer::find_arg(args, ARG_FADE_OUT)?; - - for target in targets { + for target in &targets { target.send_title_animation(fade_in, stay, fade_out); } - sender.send_message(if targets.len() == 1 { + let msg = if targets.len() == 1 { TextComponent::translate_cross( "commands.title.times.single", "commands.title.times.single", - [targets[0].get_display_name()], + [targets[0].as_ref().get_display_name()], ) } else { TextComponent::translate_cross( @@ -142,44 +109,53 @@ impl CommandExecutor for TimesTitleExecutor { "commands.title.times.multiple", [TextComponent::text(targets.len().to_string())], ) - }); + }; + context.source.send_feedback(msg, true); Ok(targets.len() as i32) } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).then( - argument(ARG_TARGETS, PlayersArgumentConsumer) - .then(literal("clear").execute(ClearOrResetExecutor(false))) - .then(literal("reset").execute(ClearOrResetExecutor(true))) - .then( - literal("title").then( - argument(ARG_TITLE, TextComponentArgConsumer) - .execute(TitleExecutor(TitleMode::Title)), - ), - ) - .then( - literal("subtitle").then( - argument(ARG_TITLE, TextComponentArgConsumer) - .execute(TitleExecutor(TitleMode::SubTitle)), - ), - ) - .then( - literal("actionbar").then( - argument(ARG_TITLE, TextComponentArgConsumer) - .execute(TitleExecutor(TitleMode::ActionBar)), - ), - ) - .then( - literal("times").then( - argument(ARG_FADE_IN, TimeArgumentConsumer::new()).then( - argument(ARG_STAY, TimeArgumentConsumer::new()).then( - argument(ARG_FADE_OUT, TimeArgumentConsumer::new()) - .execute(TimesTitleExecutor), +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + dispatcher.register( + command("title", DESCRIPTION).requires(PERMISSION).then( + argument("targets", EntityArgumentType::Players) + .then(literal("clear").executes(ClearOrResetExecutor(false))) + .then(literal("reset").executes(ClearOrResetExecutor(true))) + .then( + literal("title").then( + argument("title", ComponentArgumentType) + .executes(TitleExecutor(TitleMode::Title)), + ), + ) + .then( + literal("subtitle").then( + argument("title", ComponentArgumentType) + .executes(TitleExecutor(TitleMode::SubTitle)), + ), + ) + .then( + literal("actionbar").then( + argument("title", ComponentArgumentType) + .executes(TitleExecutor(TitleMode::ActionBar)), + ), + ) + .then( + literal("times").then( + argument("fadeIn", TimeArgumentType::any()).then( + argument("stay", TimeArgumentType::any()).then( + argument("fadeOut", TimeArgumentType::any()) + .executes(TimesTitleExecutor), + ), ), ), ), - ), - ) + ), + ); } diff --git a/crates/pumpkin/src/command/commands/tps.rs b/crates/pumpkin/src/command/commands/tps.rs index 40fa884b2..69480acd4 100644 --- a/crates/pumpkin/src/command/commands/tps.rs +++ b/crates/pumpkin/src/command/commands/tps.rs @@ -1,20 +1,20 @@ -use crate::command::CommandResult; -use crate::command::{CommandExecutor, CommandSender, args::ConsumedArgs, tree::CommandTree}; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::{TextComponent, color::NamedColor}; -const NAMES: [&str; 1] = ["tps"]; +use crate::command::argument_builder::{ArgumentBuilder, command}; +use crate::command::context::command_context::CommandContext; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; const DESCRIPTION: &str = "Displays the server TPS and MSPT."; +const PERMISSION: &str = "pumpkin:command.tps"; -struct Executor; +struct TpsExecutor; -impl CommandExecutor for Executor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - _args: &ConsumedArgs, - ) -> CommandResult { +impl CommandExecutor for TpsExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let server = context.source.server(); let tps = server.get_tps().min(server.basic_config.tps as f64); let mspt = server.get_mspt(); @@ -32,12 +32,22 @@ impl CommandExecutor for Executor { .add_child(TextComponent::text(" MSPT: ")) .add_child(TextComponent::text(format!("{mspt:.2}ms")).color_named(tps_color)); - sender.send_message(message); + context.source.send_message(message); Ok(tps as i32) } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).execute(Executor) +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + dispatcher.register( + command("tps", DESCRIPTION) + .requires(PERMISSION) + .executes(TpsExecutor), + ); } diff --git a/crates/pumpkin/src/command/commands/transfer.rs b/crates/pumpkin/src/command/commands/transfer.rs index ad343ea66..3ed381299 100644 --- a/crates/pumpkin/src/command/commands/transfer.rs +++ b/crates/pumpkin/src/command/commands/transfer.rs @@ -1,158 +1,148 @@ +use std::sync::Arc; + use pumpkin_protocol::bedrock::client::transfer::CTransfer as BedrockCTransfer; use pumpkin_protocol::codec::var_int::VarInt; use pumpkin_protocol::java::client::play::CTransfer as JavaCTransfer; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; use tracing::info; -use crate::command::CommandResult; -use crate::command::args::bounded_num::BoundedNumArgumentConsumer; -use crate::command::args::players::PlayersArgumentConsumer; -use crate::command::args::simple::SimpleArgConsumer; -use crate::command::args::{Arg, FindArgDefaultName}; -use crate::command::dispatcher::CommandError::{self, InvalidConsumption, InvalidRequirement}; -use crate::command::tree::builder::{argument, argument_default_name, require}; -use crate::command::{CommandExecutor, CommandSender, args::ConsumedArgs, tree::CommandTree}; +use crate::command::argument_builder::{ArgumentBuilder, argument, command}; +use crate::command::argument_types::core::integer::IntegerArgumentType; +use crate::command::argument_types::core::string::StringArgumentType; +use crate::command::argument_types::entity::EntityArgumentType; +use crate::command::context::command_context::CommandContext; +use crate::command::context::command_source::CommandSource; +use crate::command::errors::command_syntax_error::CommandSyntaxError; +use crate::command::errors::error_types::CommandErrorType; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; use crate::entity::EntityBase; - -const NAMES: [&str; 1] = ["transfer"]; +use crate::entity::player::Player; const DESCRIPTION: &str = "Triggers a transfer of a player to another server."; +const PERMISSION: &str = "minecraft:command.transfer"; -const ARG_HOSTNAME: &str = "hostname"; +const ERROR_NOT_PLAYER: CommandErrorType<0> = CommandErrorType::new( + pumpkin_data::translation::java::PERMISSIONS_REQUIRES_PLAYER, + pumpkin_data::translation::java::PERMISSIONS_REQUIRES_PLAYER, +); -const ARG_PLAYERS: &str = "players"; - -const fn port_consumer() -> BoundedNumArgumentConsumer { - BoundedNumArgumentConsumer::new() - .name("port") - .min(1) - .max(65535) -} - -struct TargetSelfExecutor; - -impl CommandExecutor for TargetSelfExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::Simple(hostname)) = args.get(ARG_HOSTNAME) else { - return Err(InvalidConsumption(Some(ARG_HOSTNAME.into()))); - }; - let hostname = (*hostname).to_string(); - - let port = match port_consumer().find_arg_default_name(args) { - Err(_) => 25565, - Ok(Ok(count)) => count, - Ok(Err(_)) => { - return Err(InvalidConsumption(Some( - "Port must be between 1 and 65535.".into(), - ))); - } - }; - - if let CommandSender::Player(player) = sender { - let name = &player.gameprofile.name; - info!("[{name}: Transferring {name} to {hostname}:{port}]"); - - let bedrock_packet = BedrockCTransfer::new(hostname.clone(), port as u16, false); - let java_packet = JavaCTransfer::new(&hostname, VarInt(port)); - player.try_enqueue_packet_editioned(&java_packet, &bedrock_packet); - - Ok(1) - } else { - Err(InvalidRequirement) - } +fn transfer_players( + source: &CommandSource, + hostname: &str, + port: i32, + players: &[Arc], +) -> Result { + if players.is_empty() { + return Err(ERROR_NOT_PLAYER.create_without_context()); } -} -struct TargetPlayerExecutor; + for p in players { + let bedrock_packet = BedrockCTransfer::new(hostname.to_string(), port as u16, false); + let java_packet = JavaCTransfer::new(hostname, VarInt(port)); + p.try_enqueue_packet_editioned(&java_packet, &bedrock_packet); -impl CommandExecutor for TargetPlayerExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::Simple(hostname)) = args.get(ARG_HOSTNAME) else { - return Err(InvalidConsumption(Some(ARG_HOSTNAME.into()))); - }; - let hostname = (*hostname).to_string(); + info!( + "[{}: Transferring {} to {hostname}:{port}]", + source.name, p.gameprofile.name + ); + } - let port = match port_consumer().find_arg_default_name(args) { - Err(_) => 25565, - Ok(Ok(count)) => count, - Ok(Err(_)) => { - return Err(InvalidConsumption(Some( - "Port must be between 1 and 65535.".into(), - ))); - } - }; - - let Some(Arg::Players(players)) = args.get(ARG_PLAYERS) else { - return Err(InvalidConsumption(Some(ARG_PLAYERS.into()))); - }; - - if players.is_empty() { - return Err(CommandError::CommandFailed(TextComponent::translate_cross( - "commands.transfer.error.no_players", - "commands.transfer.error.no_players", - [], - ))); - } - - for p in players { - let bedrock_packet = BedrockCTransfer::new(hostname.clone(), port as u16, false); - let java_packet = JavaCTransfer::new(&hostname, VarInt(port)); - p.try_enqueue_packet_editioned(&java_packet, &bedrock_packet); - - info!( - "[{sender}: Transferring {} to {hostname}:{port}]", - p.gameprofile.name - ); - } - - if players.len() == 1 { - sender.send_message(TextComponent::translate_cross( + if players.len() == 1 { + source.send_feedback( + TextComponent::translate_cross( "commands.transfer.success.single", "commands.transfer.success.single", [ - players[0].get_display_name(), - TextComponent::text(hostname), + players[0].as_ref().get_display_name(), + TextComponent::text(hostname.to_string()), TextComponent::text(port.to_string()), ], - )); - } else { - sender.send_message(TextComponent::translate_cross( + ), + true, + ); + } else { + source.send_feedback( + TextComponent::translate_cross( "commands.transfer.success.multiple", "commands.transfer.success.multiple", [ TextComponent::text(players.len().to_string()), - TextComponent::text(hostname), + TextComponent::text(hostname.to_string()), TextComponent::text(port.to_string()), ], - )); - } + ), + true, + ); + } - Ok(players.len() as i32) + Ok(players.len() as i32) +} + +#[derive(Clone, Copy)] +enum TransferStep { + HostOnly, + HostAndPort, + Full, +} + +struct TransferExecutor { + step: TransferStep, +} + +impl CommandExecutor for TransferExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let hostname = StringArgumentType::get(context, "hostname")?; + + let port = match self.step { + TransferStep::HostOnly => 25565, + TransferStep::HostAndPort | TransferStep::Full => { + IntegerArgumentType::get(context, "port")? + } + }; + + let players = match self.step { + TransferStep::HostOnly | TransferStep::HostAndPort => { + let player = context + .source + .output + .as_player() + .ok_or_else(|| ERROR_NOT_PLAYER.create_without_context())?; + vec![player] + } + TransferStep::Full => EntityArgumentType::get_players(context, "players")?, + }; + + transfer_players(&context.source, hostname, port, &players) } } -#[expect(clippy::redundant_closure_for_method_calls)] -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION).then( - argument(ARG_HOSTNAME, SimpleArgConsumer) - .then(require(|sender| sender.is_player()).execute(TargetSelfExecutor)) - .then( - argument_default_name(port_consumer()) - .then(require(|sender| sender.is_player()).execute(TargetSelfExecutor)) - .then( - argument(ARG_PLAYERS, PlayersArgumentConsumer) - .execute(TargetPlayerExecutor), - ), - ), - ) +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Three), + )); + + dispatcher.register( + command("transfer", DESCRIPTION).requires(PERMISSION).then( + argument("hostname", StringArgumentType::SingleWord) + .executes(TransferExecutor { + step: TransferStep::HostOnly, + }) + .then( + argument("port", IntegerArgumentType::new(1, 65535)) + .executes(TransferExecutor { + step: TransferStep::HostAndPort, + }) + .then(argument("players", EntityArgumentType::Players).executes( + TransferExecutor { + step: TransferStep::Full, + }, + )), + ), + ), + ); } diff --git a/crates/pumpkin/src/command/commands/waypoint.rs b/crates/pumpkin/src/command/commands/waypoint.rs index a5a33b8f0..2ae309fe3 100644 --- a/crates/pumpkin/src/command/commands/waypoint.rs +++ b/crates/pumpkin/src/command/commands/waypoint.rs @@ -1,41 +1,37 @@ use pumpkin_data::translation; use pumpkin_protocol::java::client::play::{CWaypoint, WaypointIcon}; +use pumpkin_util::PermissionLvl; use pumpkin_util::math::position::BlockPos; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; -use crate::command::args::{ - FindArg, entity::EntityArgumentConsumer, hex_color::HexColorArgumentConsumer, - resource_location::ResourceLocationArgumentConsumer, team_color::TeamColorArgumentConsumer, -}; -use crate::command::tree::builder::{argument, literal}; -use crate::command::{ - CommandError, CommandExecutor, CommandResult, CommandSender, ConsumedArgs, tree::CommandTree, -}; +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::entity::EntityArgumentType; +use crate::command::argument_types::hex_color::HexColorArgumentType; +use crate::command::argument_types::identifier::IdentifierArgumentType; +use crate::command::argument_types::team_color::TeamColorArgumentType; +use crate::command::context::command_context::CommandContext; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; -const NAMES: [&str; 1] = ["waypoint"]; const DESCRIPTION: &str = "List or modify waypoints."; -const ARG_WAYPOINT: &str = "waypoint"; -const ARG_COLOR: &str = "color"; -const ARG_STYLE: &str = "style"; +const PERMISSION: &str = "minecraft:command.waypoint"; struct ListExecutor; impl CommandExecutor for ListExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - _args: &ConsumedArgs, - ) -> CommandResult { - let worlds = server.worlds.load(); - let world = worlds.first().ok_or(CommandError::InvalidRequirement)?; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let world = context.source.world(); let dimension = world.dimension.minecraft_name.to_string(); - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WAYPOINT_LIST_EMPTY, - translation::java::COMMANDS_WAYPOINT_LIST_EMPTY, - TextComponent::text(dimension) - )); + context.source.send_feedback( + pumpkin_macros::translate_cross!( + translation::java::COMMANDS_WAYPOINT_LIST_EMPTY, + translation::java::COMMANDS_WAYPOINT_LIST_EMPTY, + TextComponent::text(dimension) + ), + false, + ); Ok(0) } } @@ -49,13 +45,8 @@ enum ColorAction { struct ColorExecutor(ColorAction); impl CommandExecutor for ColorExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let waypoint_entity = EntityArgumentConsumer::find_arg(args, ARG_WAYPOINT)?; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let waypoint_entity = EntityArgumentType::get_entity(context, "waypoint")?; let entity = waypoint_entity.get_entity(); let pos = entity.pos.load(); let block_pos = BlockPos::new( @@ -67,15 +58,18 @@ impl CommandExecutor for ColorExecutor { let color_val = match self.0 { ColorAction::Named => { - let color = TeamColorArgumentConsumer::find_arg(args, ARG_COLOR)?; + let color = TeamColorArgumentType::get(context, "color")?; let rgb = color.to_rgb(); i32::from_be_bytes([0, rgb.red, rgb.green, rgb.blue]) } - ColorAction::Hex => HexColorArgumentConsumer::find_arg(args, ARG_COLOR)? as i32, + ColorAction::Hex => { + let rgb = HexColorArgumentType::get(context, "color")?; + i32::from_be_bytes([0, rgb.red, rgb.green, rgb.blue]) + } ColorAction::Reset => 0xFFFFFF, }; - if let Some(player) = sender.as_player() { + if let Some(player) = context.source.as_player() { let packet = CWaypoint::update_position( uuid, Some(WaypointIcon { @@ -89,27 +83,36 @@ impl CommandExecutor for ColorExecutor { match self.0 { ColorAction::Named => { - let color = TeamColorArgumentConsumer::find_arg(args, ARG_COLOR)?; - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WAYPOINT_MODIFY_COLOR, - translation::java::COMMANDS_WAYPOINT_MODIFY_COLOR, - TextComponent::text(color.name()).color_named(color) - )); + let color = TeamColorArgumentType::get(context, "color")?; + context.source.send_feedback( + pumpkin_macros::translate_cross!( + translation::java::COMMANDS_WAYPOINT_MODIFY_COLOR, + translation::java::COMMANDS_WAYPOINT_MODIFY_COLOR, + TextComponent::text(color.name()).color_named(color) + ), + true, + ); } ColorAction::Hex => { - let color_val = HexColorArgumentConsumer::find_arg(args, ARG_COLOR)?; - let hex_str = format!("{:06X}", color_val & 0xFFFFFF); - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WAYPOINT_MODIFY_COLOR, - translation::java::COMMANDS_WAYPOINT_MODIFY_COLOR, - TextComponent::text(hex_str) - )); + let rgb = HexColorArgumentType::get(context, "color")?; + let hex_str = format!("{:02X}{:02X}{:02X}", rgb.red, rgb.green, rgb.blue); + context.source.send_feedback( + pumpkin_macros::translate_cross!( + translation::java::COMMANDS_WAYPOINT_MODIFY_COLOR, + translation::java::COMMANDS_WAYPOINT_MODIFY_COLOR, + TextComponent::text(hex_str) + ), + true, + ); } ColorAction::Reset => { - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WAYPOINT_MODIFY_COLOR_RESET, - translation::java::COMMANDS_WAYPOINT_MODIFY_COLOR_RESET - )); + context.source.send_feedback( + pumpkin_macros::translate_cross!( + translation::java::COMMANDS_WAYPOINT_MODIFY_COLOR_RESET, + translation::java::COMMANDS_WAYPOINT_MODIFY_COLOR_RESET + ), + true, + ); } } @@ -125,13 +128,8 @@ enum StyleAction { struct StyleExecutor(StyleAction); impl CommandExecutor for StyleExecutor { - fn execute( - &self, - sender: &CommandSender, - _server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let waypoint_entity = EntityArgumentConsumer::find_arg(args, ARG_WAYPOINT)?; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let waypoint_entity = EntityArgumentType::get_entity(context, "waypoint")?; let entity = waypoint_entity.get_entity(); let pos = entity.pos.load(); let block_pos = BlockPos::new( @@ -143,13 +141,13 @@ impl CommandExecutor for StyleExecutor { let style_owned = match self.0 { StyleAction::Set => { - let style = ResourceLocationArgumentConsumer::find_arg(args, ARG_STYLE)?; + let style = IdentifierArgumentType::get(context, "style")?; Some(style.to_string()) } StyleAction::Reset => None, }; - if let Some(player) = sender.as_player() { + if let Some(player) = context.source.as_player() { let packet = CWaypoint::update_position( uuid, Some(WaypointIcon { @@ -161,42 +159,48 @@ impl CommandExecutor for StyleExecutor { player.try_send_client_packet(&packet); } - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WAYPOINT_MODIFY_STYLE, - translation::java::COMMANDS_WAYPOINT_MODIFY_STYLE - )); + context.source.send_feedback( + pumpkin_macros::translate_cross!( + translation::java::COMMANDS_WAYPOINT_MODIFY_STYLE, + translation::java::COMMANDS_WAYPOINT_MODIFY_STYLE + ), + true, + ); Ok(0) } } -pub fn init_command_tree() -> CommandTree { +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + let color_node = literal("color") - .then( - argument(ARG_COLOR, TeamColorArgumentConsumer) - .execute(ColorExecutor(ColorAction::Named)), - ) + .then(argument("color", TeamColorArgumentType).executes(ColorExecutor(ColorAction::Named))) .then(literal("hex").then( - argument(ARG_COLOR, HexColorArgumentConsumer).execute(ColorExecutor(ColorAction::Hex)), + argument("color", HexColorArgumentType).executes(ColorExecutor(ColorAction::Hex)), )) - .then(literal("reset").execute(ColorExecutor(ColorAction::Reset))); + .then(literal("reset").executes(ColorExecutor(ColorAction::Reset))); let style_node = literal("style") - .then(literal("reset").execute(StyleExecutor(StyleAction::Reset))) - .then( - literal("set").then( - argument(ARG_STYLE, ResourceLocationArgumentConsumer) - .execute(StyleExecutor(StyleAction::Set)), - ), - ); + .then(literal("reset").executes(StyleExecutor(StyleAction::Reset))) + .then(literal("set").then( + argument("style", IdentifierArgumentType).executes(StyleExecutor(StyleAction::Set)), + )); let modify_node = literal("modify").then( - argument(ARG_WAYPOINT, EntityArgumentConsumer) + argument("waypoint", EntityArgumentType::Entity) .then(color_node) .then(style_node), ); - CommandTree::new(NAMES, DESCRIPTION) - .then(literal("list").execute(ListExecutor)) - .then(modify_node) + dispatcher.register( + command("waypoint", DESCRIPTION) + .requires(PERMISSION) + .then(literal("list").executes(ListExecutor)) + .then(modify_node), + ); } diff --git a/crates/pumpkin/src/command/commands/weather.rs b/crates/pumpkin/src/command/commands/weather.rs index 154ea626d..032d0710a 100644 --- a/crates/pumpkin/src/command/commands/weather.rs +++ b/crates/pumpkin/src/command/commands/weather.rs @@ -1,45 +1,38 @@ use pumpkin_data::translation; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; -use crate::command::{ - CommandError, CommandExecutor, CommandResult, CommandSender, - args::{ConsumedArgs, FindArg, time::TimeArgumentConsumer}, - tree::{ - CommandTree, - builder::{argument, literal}, - }, -}; +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::time::TimeArgumentType; +use crate::command::context::command_context::CommandContext; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; -const NAMES: [&str; 1] = ["weather"]; const DESCRIPTION: &str = "Changes the weather."; -const ARG_DURATION: &str = "duration"; - -struct Executor { - mode: WeatherMode, -} +const PERMISSION: &str = "minecraft:command.weather"; +#[derive(Clone, Copy)] enum WeatherMode { Clear, Rain, Thunder, } -impl CommandExecutor for Executor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let duration = TimeArgumentConsumer::find_arg(args, ARG_DURATION).ok(); - let world = { - let guard = server.worlds.load(); +struct WeatherExecutor { + mode: WeatherMode, + has_duration: bool, +} - guard - .first() - .cloned() - .ok_or(CommandError::InvalidRequirement)? +impl CommandExecutor for WeatherExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let duration = if self.has_duration { + Some(TimeArgumentType::get(context, "duration")?) + } else { + None }; + + let world = context.source.world(); let message = { let mut weather = world .weather @@ -51,7 +44,7 @@ impl CommandExecutor for Executor { let processed_duration = duration.unwrap_or_else(|| rand::random_range(12_000..=180_000)); - weather.set_weather_parameters(&world, processed_duration, 0, false, false); + weather.set_weather_parameters(world, processed_duration, 0, false, false); TextComponent::translate_cross( translation::java::COMMANDS_WEATHER_SET_CLEAR, translation::bedrock::COMMANDS_WEATHER_CLEAR, @@ -62,7 +55,7 @@ impl CommandExecutor for Executor { let processed_duration = duration.unwrap_or_else(|| rand::random_range(12_000..=24_000)); - weather.set_weather_parameters(&world, 0, processed_duration, true, false); + weather.set_weather_parameters(world, 0, processed_duration, true, false); TextComponent::translate_cross( translation::java::COMMANDS_WEATHER_SET_RAIN, translation::bedrock::COMMANDS_WEATHER_RAIN, @@ -71,9 +64,9 @@ impl CommandExecutor for Executor { } WeatherMode::Thunder => { let processed_duration = - duration.unwrap_or_else(|| rand::random_range(3_600..=15_600)); + duration.unwrap_or_else(|| rand::random_range(3600..=15_600)); - weather.set_weather_parameters(&world, 0, processed_duration, true, true); + weather.set_weather_parameters(world, 0, processed_duration, true, true); TextComponent::translate_cross( translation::java::COMMANDS_WEATHER_SET_THUNDER, translation::bedrock::COMMANDS_WEATHER_THUNDER, @@ -83,46 +76,60 @@ impl CommandExecutor for Executor { } }; - sender.send_message(message); + context.source.send_feedback(message, true); - // Vanilla returns -1 when duration is not specified - Ok(duration.unwrap_or(-1)) + Ok(1) } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION) - .then( - literal("clear") - .then( - argument(ARG_DURATION, TimeArgumentConsumer::new()).execute(Executor { +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + dispatcher.register( + command("weather", DESCRIPTION) + .requires(PERMISSION) + .then( + literal("clear") + .executes(WeatherExecutor { mode: WeatherMode::Clear, - }), - ) - .execute(Executor { - mode: WeatherMode::Clear, - }), - ) - .then( - literal("rain") - .then( - argument(ARG_DURATION, TimeArgumentConsumer::new()).execute(Executor { + has_duration: false, + }) + .then(argument("duration", TimeArgumentType::any()).executes( + WeatherExecutor { + mode: WeatherMode::Clear, + has_duration: true, + }, + )), + ) + .then( + literal("rain") + .executes(WeatherExecutor { mode: WeatherMode::Rain, - }), - ) - .execute(Executor { - mode: WeatherMode::Rain, - }), - ) - .then( - literal("thunder") - .then( - argument(ARG_DURATION, TimeArgumentConsumer::new()).execute(Executor { + has_duration: false, + }) + .then(argument("duration", TimeArgumentType::any()).executes( + WeatherExecutor { + mode: WeatherMode::Rain, + has_duration: true, + }, + )), + ) + .then( + literal("thunder") + .executes(WeatherExecutor { mode: WeatherMode::Thunder, - }), - ) - .execute(Executor { - mode: WeatherMode::Thunder, - }), - ) + has_duration: false, + }) + .then(argument("duration", TimeArgumentType::any()).executes( + WeatherExecutor { + mode: WeatherMode::Thunder, + has_duration: true, + }, + )), + ), + ); } diff --git a/crates/pumpkin/src/command/commands/whitelist.rs b/crates/pumpkin/src/command/commands/whitelist.rs index 3f378bcf6..ab2ab60d1 100644 --- a/crates/pumpkin/src/command/commands/whitelist.rs +++ b/crates/pumpkin/src/command/commands/whitelist.rs @@ -2,30 +2,42 @@ use std::sync::atomic::Ordering; use pumpkin_config::whitelist::WhitelistEntry; use pumpkin_data::translation; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; -use crate::command::CommandResult; -use crate::{ - command::{ - CommandExecutor, CommandSender, - args::{ - Arg, ConsumedArgs, - gameprofile::{GameProfileSuggestionMode, GameProfilesArgumentConsumer}, - }, - dispatcher::CommandError, - tree::{ - CommandTree, - builder::{argument, literal}, - }, - }, - data::{LoadJSONConfiguration, SaveJSONConfiguration, whitelist::WhitelistConfig}, - net::DisconnectReason, - server::Server, -}; +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::game_profile::GameProfileArgumentType; +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 crate::data::{LoadJSONConfiguration, SaveJSONConfiguration, whitelist::WhitelistConfig}; +use crate::net::DisconnectReason; +use crate::server::Server; -const NAMES: [&str; 1] = ["whitelist"]; const DESCRIPTION: &str = "Manage server whitelists."; -const ARG_TARGETS: &str = "targets"; +const PERMISSION: &str = "minecraft:command.whitelist"; + +const ERROR_ALREADY_ON: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_WHITELIST_ALREADYON, + translation::bedrock::COMMANDS_ALLOWLIST_ENABLED, +); + +const ERROR_ALREADY_OFF: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_WHITELIST_ALREADYOFF, + translation::bedrock::COMMANDS_ALLOWLIST_DISABLED, +); + +const ERROR_ADD_FAILED: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_WHITELIST_ADD_FAILED, + translation::bedrock::COMMANDS_ALLOWLIST_ADD_FAILED, +); + +const ERROR_REMOVE_FAILED: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_WHITELIST_REMOVE_FAILED, + translation::bedrock::COMMANDS_ALLOWLIST_REMOVE_FAILED, +); fn kick_non_whitelisted_players(server: &Server) { let whitelist = server.data.whitelist_config.read().unwrap(); @@ -48,26 +60,20 @@ fn kick_non_whitelisted_players(server: &Server) { struct OnExecutor; impl CommandExecutor for OnExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - _args: &ConsumedArgs, - ) -> CommandResult { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let server = context.source.server(); let previous = server.white_list.swap(true, Ordering::Relaxed); if previous { - Err(CommandError::CommandFailed( - pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WHITELIST_ALREADYON, - translation::bedrock::COMMANDS_ALLOWLIST_ENABLED - ), - )) + Err(ERROR_ALREADY_ON.create_without_context()) } else { kick_non_whitelisted_players(server); - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WHITELIST_ENABLED, - translation::bedrock::COMMANDS_ALLOWLIST_ENABLED - )); + context.source.send_feedback( + pumpkin_macros::translate_cross!( + translation::java::COMMANDS_WHITELIST_ENABLED, + translation::bedrock::COMMANDS_ALLOWLIST_ENABLED + ), + true, + ); Ok(1) } } @@ -76,26 +82,20 @@ impl CommandExecutor for OnExecutor { struct OffExecutor; impl CommandExecutor for OffExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - _args: &ConsumedArgs, - ) -> CommandResult { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let server = context.source.server(); let previous = server.white_list.swap(false, Ordering::Relaxed); if previous { - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WHITELIST_DISABLED, - translation::bedrock::COMMANDS_ALLOWLIST_DISABLED - )); - Ok(1) - } else { - Err(CommandError::CommandFailed( + context.source.send_feedback( pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WHITELIST_ALREADYOFF, + translation::java::COMMANDS_WHITELIST_DISABLED, translation::bedrock::COMMANDS_ALLOWLIST_DISABLED ), - )) + true, + ); + Ok(1) + } else { + Err(ERROR_ALREADY_OFF.create_without_context()) } } } @@ -103,19 +103,18 @@ impl CommandExecutor for OffExecutor { struct ListExecutor; impl CommandExecutor for ListExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - _args: &ConsumedArgs, - ) -> CommandResult { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let server = context.source.server(); let whitelist_guard = server.data.whitelist_config.read().unwrap(); let whitelist = &whitelist_guard.whitelist; if whitelist.is_empty() { - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WHITELIST_NONE, - translation::bedrock::COMMANDS_ALLOWLIST_LIST - )); + context.source.send_feedback( + pumpkin_macros::translate_cross!( + translation::java::COMMANDS_WHITELIST_NONE, + translation::bedrock::COMMANDS_ALLOWLIST_LIST + ), + false, + ); return Ok(0); } @@ -127,12 +126,15 @@ impl CommandExecutor for ListExecutor { let names_len = names.len() as i32; - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WHITELIST_LIST, - translation::bedrock::COMMANDS_ALLOWLIST_LIST, - TextComponent::text(whitelist.len().to_string()), - TextComponent::text(names) - )); + context.source.send_feedback( + pumpkin_macros::translate_cross!( + translation::java::COMMANDS_WHITELIST_LIST, + translation::bedrock::COMMANDS_ALLOWLIST_LIST, + TextComponent::text(whitelist.len().to_string()), + TextComponent::text(names) + ), + false, + ); Ok(names_len) } @@ -141,38 +143,30 @@ impl CommandExecutor for ListExecutor { struct ReloadExecutor; impl CommandExecutor for ReloadExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - _args: &ConsumedArgs, - ) -> CommandResult { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let server = context.source.server(); *server.data.whitelist_config.write().unwrap() = WhitelistConfig::load(); kick_non_whitelisted_players(server); - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WHITELIST_RELOADED, - translation::bedrock::COMMANDS_ALLOWLIST_RELOADED - )); + context.source.send_feedback( + pumpkin_macros::translate_cross!( + translation::java::COMMANDS_WHITELIST_RELOADED, + translation::bedrock::COMMANDS_ALLOWLIST_RELOADED + ), + true, + ); Ok(1) } } -pub struct AddExecutor; +struct AddExecutor; impl CommandExecutor for AddExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::GameProfiles(targets)) = args.get(ARG_TARGETS) else { - return Err(CommandError::InvalidConsumption(Some(ARG_TARGETS.into()))); - }; - + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let targets = GameProfileArgumentType::get(context, "targets")?; + let server = context.source.server(); let mut whitelist = server.data.whitelist_config.write().unwrap(); let mut successes: i32 = 0; - for profile in targets { + for profile in &targets { if let Some(existing_entry) = whitelist .whitelist .iter_mut() @@ -186,45 +180,36 @@ impl CommandExecutor for AddExecutor { whitelist .whitelist .push(WhitelistEntry::new(profile.id, profile.name.clone())); - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WHITELIST_ADD_SUCCESS, - translation::bedrock::COMMANDS_ALLOWLIST_ADD_SUCCESS, - TextComponent::text(profile.name.clone()) - )); + context.source.send_feedback( + pumpkin_macros::translate_cross!( + translation::java::COMMANDS_WHITELIST_ADD_SUCCESS, + translation::bedrock::COMMANDS_ALLOWLIST_ADD_SUCCESS, + TextComponent::text(profile.name.clone()) + ), + true, + ); successes += 1; } whitelist.save(); if successes == 0 { - Err(CommandError::CommandFailed( - pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WHITELIST_ADD_FAILED, - translation::bedrock::COMMANDS_ALLOWLIST_ADD_FAILED - ), - )) + Err(ERROR_ADD_FAILED.create_without_context()) } else { Ok(successes) } } } -pub struct RemoveExecutor; +struct RemoveExecutor; impl CommandExecutor for RemoveExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - let Some(Arg::GameProfiles(targets)) = args.get(ARG_TARGETS) else { - return Err(CommandError::InvalidConsumption(Some(ARG_TARGETS.into()))); - }; - + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let targets = GameProfileArgumentType::get(context, "targets")?; + let server = context.source.server(); let mut whitelist = server.data.whitelist_config.write().unwrap(); let mut successes: i32 = 0; - for player in targets { + for player in &targets { let i = whitelist .whitelist .iter() @@ -232,11 +217,14 @@ impl CommandExecutor for RemoveExecutor { if let Some(i) = i { whitelist.whitelist.remove(i); - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WHITELIST_REMOVE_SUCCESS, - translation::bedrock::COMMANDS_ALLOWLIST_REMOVE_SUCCESS, - TextComponent::text(player.name.clone()) - )); + context.source.send_feedback( + pumpkin_macros::translate_cross!( + translation::java::COMMANDS_WHITELIST_REMOVE_SUCCESS, + translation::bedrock::COMMANDS_ALLOWLIST_REMOVE_SUCCESS, + TextComponent::text(player.name.clone()) + ), + true, + ); successes += 1; } } @@ -247,46 +235,34 @@ impl CommandExecutor for RemoveExecutor { kick_non_whitelisted_players(server); if successes == 0 { - Err(CommandError::CommandFailed( - pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WHITELIST_REMOVE_FAILED, - translation::bedrock::COMMANDS_ALLOWLIST_REMOVE_FAILED - ), - )) + Err(ERROR_REMOVE_FAILED.create_without_context()) } else { Ok(successes) } } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION) - .then(literal("on").execute(OnExecutor)) - .then(literal("off").execute(OffExecutor)) - .then(literal("list").execute(ListExecutor)) - .then(literal("reload").execute(ReloadExecutor)) - .then( - literal("add").then( - argument( - ARG_TARGETS, - GameProfilesArgumentConsumer::new( - GameProfileSuggestionMode::NonWhitelistedOnlinePlayers, - false, - ), - ) - .execute(AddExecutor), +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Three), + )); + + dispatcher.register( + command("whitelist", DESCRIPTION) + .requires(PERMISSION) + .then(literal("on").executes(OnExecutor)) + .then(literal("off").executes(OffExecutor)) + .then(literal("list").executes(ListExecutor)) + .then(literal("reload").executes(ReloadExecutor)) + .then( + literal("add") + .then(argument("targets", GameProfileArgumentType).executes(AddExecutor)), + ) + .then( + literal("remove") + .then(argument("targets", GameProfileArgumentType).executes(RemoveExecutor)), ), - ) - .then( - literal("remove").then( - argument( - ARG_TARGETS, - GameProfilesArgumentConsumer::new( - GameProfileSuggestionMode::WhitelistedNames, - false, - ), - ) - .execute(RemoveExecutor), - ), - ) + ); } diff --git a/crates/pumpkin/src/command/commands/worldborder.rs b/crates/pumpkin/src/command/commands/worldborder.rs index 912519944..84abae4ad 100644 --- a/crates/pumpkin/src/command/commands/worldborder.rs +++ b/crates/pumpkin/src/command/commands/worldborder.rs @@ -1,547 +1,452 @@ use pumpkin_data::translation; -use pumpkin_util::{math::vector2::Vector2, text::TextComponent}; +use pumpkin_util::PermissionLvl; +use pumpkin_util::math::vector2::Vector2; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; +use pumpkin_util::text::TextComponent; -use crate::command::{ - CommandError, CommandExecutor, CommandResult, CommandSender, - args::{ - ConsumedArgs, FindArgDefaultName, bounded_num::BoundedNumArgumentConsumer, - position_2d::Position2DArgumentConsumer, - }, - tree::{ - CommandTree, - builder::{argument_default_name, literal}, - }, -}; +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::coordinates::vec2::Vec2ArgumentType; +use crate::command::argument_types::core::double::DoubleArgumentType; +use crate::command::argument_types::core::float::FloatArgumentType; +use crate::command::argument_types::core::integer::IntegerArgumentType; +use crate::command::argument_types::time::TimeArgumentType; +use crate::command::context::command_context::CommandContext; +use crate::command::context::command_source::CommandSource; +use crate::command::errors::command_syntax_error::CommandSyntaxError; +use crate::command::errors::error_types::CommandErrorType; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; -const NAMES: [&str; 1] = ["worldborder"]; +const DESCRIPTION: &str = "Manages the world border."; +const PERMISSION: &str = "minecraft:command.worldborder"; -const DESCRIPTION: &str = "Worldborder command."; +const ERROR_SAME_CENTER: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_WORLDBORDER_CENTER_FAILED, + translation::java::COMMANDS_WORLDBORDER_CENTER_FAILED, +); +const ERROR_SAME_SIZE: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_WORLDBORDER_SET_FAILED_NOCHANGE, + translation::java::COMMANDS_WORLDBORDER_SET_FAILED_NOCHANGE, +); +const ERROR_TOO_SMALL: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_WORLDBORDER_SET_FAILED_SMALL, + translation::java::COMMANDS_WORLDBORDER_SET_FAILED_SMALL, +); +const ERROR_TOO_BIG: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_WORLDBORDER_SET_FAILED_BIG, + translation::java::COMMANDS_WORLDBORDER_SET_FAILED_BIG, +); +const ERROR_TOO_FAR_OUT: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_WORLDBORDER_SET_FAILED_FAR, + translation::java::COMMANDS_WORLDBORDER_SET_FAILED_FAR, +); +const ERROR_SAME_WARNING_TIME: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_WORLDBORDER_WARNING_TIME_FAILED, + translation::java::COMMANDS_WORLDBORDER_WARNING_TIME_FAILED, +); +const ERROR_SAME_WARNING_DISTANCE: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_WORLDBORDER_WARNING_DISTANCE_FAILED, + translation::java::COMMANDS_WORLDBORDER_WARNING_DISTANCE_FAILED, +); +const ERROR_SAME_DAMAGE_BUFFER: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_WORLDBORDER_DAMAGE_BUFFER_FAILED, + translation::java::COMMANDS_WORLDBORDER_DAMAGE_BUFFER_FAILED, +); +const ERROR_SAME_DAMAGE_AMOUNT: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_WORLDBORDER_DAMAGE_AMOUNT_FAILED, + translation::java::COMMANDS_WORLDBORDER_DAMAGE_AMOUNT_FAILED, +); -const fn distance_consumer() -> BoundedNumArgumentConsumer { - BoundedNumArgumentConsumer::new().min(0.0).name("distance") -} - -const fn time_consumer() -> BoundedNumArgumentConsumer { - BoundedNumArgumentConsumer::new().min(0).name("time") -} - -const fn damage_per_block_consumer() -> BoundedNumArgumentConsumer { - BoundedNumArgumentConsumer::new() - .min(0.0) - .name("damage_per_block") -} - -const fn damage_buffer_consumer() -> BoundedNumArgumentConsumer { - BoundedNumArgumentConsumer::new().min(0.0).name("buffer") -} - -const fn warning_distance_consumer() -> BoundedNumArgumentConsumer { - BoundedNumArgumentConsumer::new().min(0).name("distance") -} - -struct GetExecutor; - -impl CommandExecutor for GetExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - _args: &ConsumedArgs, - ) -> CommandResult { - // TODO: Maybe ask player for world, or get the current world - let worlds = server.worlds.load(); - let world = worlds.first().ok_or(CommandError::InvalidRequirement)?; - let diameter = world +fn set_size( + source: &CommandSource, + distance: f64, + time_in_ticks: i64, +) -> Result { + let world = source.world().clone(); + let (current, diff) = { + let mut border = world .worldborder .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .new_diameter - .round() as i32; + .unwrap_or_else(std::sync::PoisonError::into_inner); + let current = border.new_diameter; - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WORLDBORDER_GET, - translation::bedrock::COMMANDS_WORLDBORDER_GET_SUCCESS, - TextComponent::text(diameter.to_string()) - )); - - Ok(diameter) - } -} - -struct SetExecutor; - -impl CommandExecutor for SetExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - // TODO: Maybe ask player for world, or get the current world - let worlds = server.worlds.load(); - let world = worlds.first().ok_or(CommandError::InvalidRequirement)?; - let distance = distance_consumer().find_arg_default_name(args)??; - - let (d, diff) = { - let mut border = world - .worldborder - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - - if (distance - border.new_diameter).abs() < f64::EPSILON { - return Err(CommandError::CommandFailed( - pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WORLDBORDER_SET_FAILED_NOCHANGE, - translation::bedrock::COMMANDS_WORLDBORDER_SET_SUCCESS - ), - )); - } - - let d = border.new_diameter; - border.set_diameter(world, distance, None); - (d, (distance - d) as i32) - }; - - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WORLDBORDER_SET_IMMEDIATE, - translation::bedrock::COMMANDS_WORLDBORDER_SET_SUCCESS, - TextComponent::text(format!("{distance:.1}")), - TextComponent::text(format!("{d:.1}")) - )); - - Ok(diff) - } -} - -struct SetTimeExecutor; - -impl CommandExecutor for SetTimeExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - // TODO: Maybe ask player for world, or get the current world - let worlds = server.worlds.load(); - let world = worlds.first().ok_or(CommandError::InvalidRequirement)?; - let distance = distance_consumer().find_arg_default_name(args)??; - let time = time_consumer().find_arg_default_name(args)??; - - let (old_dist, ordering, diff) = { - let mut border = world - .worldborder - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - - let old_dist = format!("{:.1}", border.new_diameter); - let ordering = distance.total_cmp(&border.new_diameter); - if ordering == std::cmp::Ordering::Equal { - return Err(CommandError::CommandFailed( - pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WORLDBORDER_SET_FAILED_NOCHANGE, - translation::bedrock::COMMANDS_WORLDBORDER_SET_SUCCESS - ), - )); - } - let d = border.new_diameter; - border.set_diameter(world, distance, Some(i64::from(time) * 1000)); - (old_dist, ordering, (distance - d) as i32) - }; - - let dist = format!("{distance:.1}"); - match ordering { - std::cmp::Ordering::Less => { - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WORLDBORDER_SET_SHRINK, - translation::bedrock::COMMANDS_WORLDBORDER_SETSLOWLY_SHRINK_SUCCESS, - TextComponent::text(dist), - TextComponent::text(old_dist), - TextComponent::text(time.to_string()) - )); - } - std::cmp::Ordering::Greater => { - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WORLDBORDER_SET_GROW, - translation::bedrock::COMMANDS_WORLDBORDER_SETSLOWLY_GROW_SUCCESS, - TextComponent::text(dist), - TextComponent::text(old_dist), - TextComponent::text(time.to_string()) - )); - } - std::cmp::Ordering::Equal => {} + if (current - distance).abs() < f64::EPSILON { + return Err(ERROR_SAME_SIZE.create_without_context()); + } + if distance < 1.0 { + return Err(ERROR_TOO_SMALL.create_without_context()); + } + if distance > 5.999_997e7 { + return Err(ERROR_TOO_BIG.create_without_context()); } - Ok(diff) - } -} + let speed = (time_in_ticks > 0).then(|| time_in_ticks * 50); // ticks to milliseconds -struct AddExecutor; + border.set_diameter(&world, distance, speed); + (current, (distance - current) as i32) + }; -impl CommandExecutor for AddExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - // TODO: Maybe ask player for world, or get the current world - let worlds = server.worlds.load(); - let world = worlds.first().ok_or(CommandError::InvalidRequirement)?; - let distance_add = distance_consumer().find_arg_default_name(args)??; - - if distance_add == 0.0 { - return Err(CommandError::CommandFailed( - pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WORLDBORDER_SET_FAILED_NOCHANGE, - translation::bedrock::COMMANDS_WORLDBORDER_SET_SUCCESS + let formatted_distance = format!("{distance:.1}"); + if time_in_ticks > 0 { + let seconds_str = format!("{:.2}", time_in_ticks as f64 / 20.0); + if distance > current { + source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_WORLDBORDER_SET_GROW, + translation::java::COMMANDS_WORLDBORDER_SET_GROW, + [ + TextComponent::text(formatted_distance), + TextComponent::text(seconds_str), + ], ), - )); + true, + ); + } else { + source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_WORLDBORDER_SET_SHRINK, + translation::java::COMMANDS_WORLDBORDER_SET_SHRINK, + [ + TextComponent::text(formatted_distance), + TextComponent::text(seconds_str), + ], + ), + true, + ); } - - let (dist, old_dist) = { - let mut border = world - .worldborder - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - - let distance = border.new_diameter + distance_add; - let dist = format!("{distance:.1}"); - let old_dist = format!("{:.1}", border.new_diameter); - border.set_diameter(world, distance, None); - (dist, old_dist) - }; - - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WORLDBORDER_SET_IMMEDIATE, - translation::bedrock::COMMANDS_WORLDBORDER_SET_SUCCESS, - TextComponent::text(dist), - TextComponent::text(old_dist) - )); - Ok(distance_add as i32) + } else { + source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_WORLDBORDER_SET_IMMEDIATE, + translation::java::COMMANDS_WORLDBORDER_SET_IMMEDIATE, + [TextComponent::text(formatted_distance)], + ), + true, + ); } + + Ok(diff) } -struct AddTimeExecutor; +fn set_center(source: &CommandSource, center: Vector2) -> Result { + let world = source.world().clone(); + let mut border = world + .worldborder + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); -impl CommandExecutor for AddTimeExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - // TODO: Maybe ask player for world, or get the current world - let worlds = server.worlds.load(); - let world = worlds.first().ok_or(CommandError::InvalidRequirement)?; - let distance_add = distance_consumer().find_arg_default_name(args)??; - let time = time_consumer().find_arg_default_name(args)??; + if (border.center_x - center.x).abs() < f64::EPSILON + && (border.center_z - center.y).abs() < f64::EPSILON + { + return Err(ERROR_SAME_CENTER.create_without_context()); + } - let (distance, old_dist, ordering) = { - let mut border = world - .worldborder - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); + if center.x.abs() > 2.999_998_4e7 || center.y.abs() > 2.999_998_4e7 { + return Err(ERROR_TOO_FAR_OUT.create_without_context()); + } - let distance = distance_add + border.new_diameter; - let old_dist = format!("{:.1}", border.new_diameter); - let ordering = distance.total_cmp(&border.new_diameter); - if ordering == std::cmp::Ordering::Equal { - return Err(CommandError::CommandFailed( - pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WORLDBORDER_SET_FAILED_NOCHANGE, - translation::bedrock::COMMANDS_WORLDBORDER_SET_SUCCESS - ), - )); - } - border.set_diameter(world, distance, Some(i64::from(time) * 1000)); - (distance, old_dist, ordering) + border.set_center(&world, center.x, center.y); + + source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_WORLDBORDER_CENTER_SUCCESS, + translation::java::COMMANDS_WORLDBORDER_CENTER_SUCCESS, + [ + TextComponent::text(format!("{:.2}", center.x)), + TextComponent::text(format!("{:.2}", center.y)), + ], + ), + true, + ); + + Ok(0) +} + +#[allow(clippy::unnecessary_wraps)] +fn get_size(source: &CommandSource) -> Result { + let world = source.world().clone(); + let size = world + .worldborder + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .new_diameter; + + source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_WORLDBORDER_GET, + translation::java::COMMANDS_WORLDBORDER_GET, + [TextComponent::text(format!("{size:.0}"))], + ), + false, + ); + + Ok((size + 0.5).floor() as i32) +} + +fn set_damage_amount( + source: &CommandSource, + damage_per_block: f32, +) -> Result { + let world = source.world().clone(); + let mut border = world + .worldborder + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + + if (border.damage_per_block - damage_per_block).abs() < f32::EPSILON { + return Err(ERROR_SAME_DAMAGE_AMOUNT.create_without_context()); + } + + border.set_damage_per_block(damage_per_block); + + source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_WORLDBORDER_DAMAGE_AMOUNT_SUCCESS, + translation::java::COMMANDS_WORLDBORDER_DAMAGE_AMOUNT_SUCCESS, + [TextComponent::text(format!("{damage_per_block:.2}"))], + ), + true, + ); + + Ok(damage_per_block as i32) +} + +fn set_damage_buffer(source: &CommandSource, distance: f32) -> Result { + let world = source.world().clone(); + let mut border = world + .worldborder + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + + if (border.buffer - distance).abs() < f32::EPSILON { + return Err(ERROR_SAME_DAMAGE_BUFFER.create_without_context()); + } + + border.set_damage_buffer(distance); + + source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_WORLDBORDER_DAMAGE_BUFFER_SUCCESS, + translation::java::COMMANDS_WORLDBORDER_DAMAGE_BUFFER_SUCCESS, + [TextComponent::text(format!("{distance:.2}"))], + ), + true, + ); + + Ok(distance as i32) +} + +fn set_warning_distance(source: &CommandSource, distance: i32) -> Result { + let world = source.world().clone(); + let mut border = world + .worldborder + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + + if border.warning_blocks == distance { + return Err(ERROR_SAME_WARNING_DISTANCE.create_without_context()); + } + + border.set_warning_distance(&world, distance); + + source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_WORLDBORDER_WARNING_DISTANCE_SUCCESS, + translation::java::COMMANDS_WORLDBORDER_WARNING_DISTANCE_SUCCESS, + [TextComponent::text(distance.to_string())], + ), + true, + ); + + Ok(distance) +} + +fn set_warning_time(source: &CommandSource, ticks: i32) -> Result { + let world = source.world().clone(); + let mut border = world + .worldborder + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + + if border.warning_time == ticks { + return Err(ERROR_SAME_WARNING_TIME.create_without_context()); + } + + border.set_warning_delay(&world, ticks); + + source.send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_WORLDBORDER_WARNING_TIME_SUCCESS, + translation::java::COMMANDS_WORLDBORDER_WARNING_TIME_SUCCESS, + [TextComponent::text(format!("{:.2}", ticks as f64 / 20.0))], + ), + true, + ); + + Ok(ticks) +} + +struct SetSizeExecutor { + is_add: bool, + has_time: bool, +} + +impl CommandExecutor for SetSizeExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let distance = DoubleArgumentType::get(context, "distance")?; + let time = if self.has_time { + i64::from(TimeArgumentType::get(context, "time")?) + } else { + 0 }; - let dist = format!("{distance:.1}"); - match ordering { - std::cmp::Ordering::Less => { - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WORLDBORDER_SET_SHRINK, - translation::bedrock::COMMANDS_WORLDBORDER_SETSLOWLY_SHRINK_SUCCESS, - TextComponent::text(dist), - TextComponent::text(old_dist), - TextComponent::text(time.to_string()) - )); - } - std::cmp::Ordering::Greater => { - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WORLDBORDER_SET_GROW, - translation::bedrock::COMMANDS_WORLDBORDER_SETSLOWLY_GROW_SUCCESS, - TextComponent::text(dist), - TextComponent::text(old_dist), - TextComponent::text(time.to_string()) - )); - } - std::cmp::Ordering::Equal => {} - } + let target_distance = if self.is_add { + let current = context + .source + .world() + .worldborder + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .new_diameter; + current + distance + } else { + distance + }; - Ok(distance_add as i32) + set_size(&context.source, target_distance, time) } } struct CenterExecutor; impl CommandExecutor for CenterExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - // TODO: Maybe ask player for world, or get the current world - let worlds = server.worlds.load(); - let world = worlds.first().ok_or(CommandError::InvalidRequirement)?; - let Vector2 { x, y } = Position2DArgumentConsumer.find_arg_default_name(args)?; + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let center = Vec2ArgumentType::get_vector2(context, "pos")?; + set_center(&context.source, center) + } +} - world - .worldborder - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .set_center(world, x, y); +struct GetSizeExecutor; - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WORLDBORDER_CENTER_SUCCESS, - translation::bedrock::COMMANDS_WORLDBORDER_CENTER_SUCCESS, - TextComponent::text(format!("{x:.2}")), - TextComponent::text(format!("{y:.2}")) - )); - - Ok(0) +impl CommandExecutor for GetSizeExecutor { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + get_size(&context.source) } } struct DamageAmountExecutor; impl CommandExecutor for DamageAmountExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - // TODO: Maybe ask player for world, or get the current world - let worlds = server.worlds.load(); - let world = worlds.first().ok_or(CommandError::InvalidRequirement)?; - let damage_per_block = damage_per_block_consumer().find_arg_default_name(args)??; - - let old_damage = { - let mut border = world - .worldborder - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - - if (damage_per_block - border.damage_per_block).abs() < f32::EPSILON { - return Err(CommandError::CommandFailed( - pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WORLDBORDER_DAMAGE_AMOUNT_FAILED, - translation::bedrock::COMMANDS_WORLDBORDER_DAMAGE_AMOUNT_SUCCESS - ), - )); - } - - let old_damage = format!("{:.2}", border.damage_per_block); - border.damage_per_block = damage_per_block; - old_damage - }; - - let damage = format!("{damage_per_block:.2}"); - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WORLDBORDER_DAMAGE_AMOUNT_SUCCESS, - translation::bedrock::COMMANDS_WORLDBORDER_DAMAGE_AMOUNT_SUCCESS, - TextComponent::text(damage), - TextComponent::text(old_damage) - )); - - Ok(damage_per_block as i32) + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let damage = FloatArgumentType::get(context, "damagePerBlock")?; + set_damage_amount(&context.source, damage) } } struct DamageBufferExecutor; impl CommandExecutor for DamageBufferExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - // TODO: Maybe ask player for world, or get the current world - let worlds = server.worlds.load(); - let world = worlds.first().ok_or(CommandError::InvalidRequirement)?; - let buffer = damage_buffer_consumer().find_arg_default_name(args)??; - - let old_buf = { - let mut border = world - .worldborder - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - - if (buffer - border.buffer).abs() < f32::EPSILON { - return Err(CommandError::CommandFailed( - pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WORLDBORDER_DAMAGE_BUFFER_FAILED, - translation::bedrock::COMMANDS_WORLDBORDER_DAMAGE_BUFFER_SUCCESS - ), - )); - } - - let old_buf = format!("{:.2}", border.buffer); - border.buffer = buffer; - old_buf - }; - - let buf = format!("{buffer:.2}"); - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WORLDBORDER_DAMAGE_BUFFER_SUCCESS, - translation::bedrock::COMMANDS_WORLDBORDER_DAMAGE_BUFFER_SUCCESS, - TextComponent::text(buf), - TextComponent::text(old_buf) - )); - - Ok(buffer as i32) + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let distance = FloatArgumentType::get(context, "distance")?; + set_damage_buffer(&context.source, distance) } } struct WarningDistanceExecutor; impl CommandExecutor for WarningDistanceExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - // TODO: Maybe ask player for world, or get the current world - let worlds = server.worlds.load(); - let world = worlds.first().ok_or(CommandError::InvalidRequirement)?; - let distance = warning_distance_consumer().find_arg_default_name(args)??; - - let old_warning = { - let mut border = world - .worldborder - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - - if distance == border.warning_blocks { - return Err(CommandError::CommandFailed( - pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WORLDBORDER_WARNING_DISTANCE_FAILED, - translation::bedrock::COMMANDS_WORLDBORDER_WARNING_DISTANCE_SUCCESS - ), - )); - } - - let old_warning = border.warning_blocks; - border.set_warning_distance(world, distance); - old_warning - }; - - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WORLDBORDER_WARNING_DISTANCE_SUCCESS, - translation::bedrock::COMMANDS_WORLDBORDER_WARNING_DISTANCE_SUCCESS, - TextComponent::text(distance.to_string()), - TextComponent::text(old_warning.to_string()) - )); - - Ok(distance) + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let distance = IntegerArgumentType::get(context, "distance")?; + set_warning_distance(&context.source, distance) } } struct WarningTimeExecutor; impl CommandExecutor for WarningTimeExecutor { - fn execute( - &self, - sender: &CommandSender, - server: &crate::server::Server, - args: &ConsumedArgs, - ) -> CommandResult { - // TODO: Maybe ask player for world, or get the current world - let worlds = server.worlds.load(); - let world = worlds.first().ok_or(CommandError::InvalidRequirement)?; - let time = time_consumer().find_arg_default_name(args)??; - - let old_time = { - let mut border = world - .worldborder - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - - if time == border.warning_time { - return Err(CommandError::CommandFailed( - pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WORLDBORDER_WARNING_TIME_FAILED, - translation::bedrock::COMMANDS_WORLDBORDER_WARNING_TIME_SUCCESS - ), - )); - } - - let old_time = border.warning_time; - border.set_warning_delay(world, time); - old_time - }; - - sender.send_message(pumpkin_macros::translate_cross!( - translation::java::COMMANDS_WORLDBORDER_WARNING_TIME_SUCCESS, - translation::bedrock::COMMANDS_WORLDBORDER_WARNING_TIME_SUCCESS, - TextComponent::text(time.to_string()), - TextComponent::text(old_time.to_string()) - )); - - Ok(time) + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { + let time = TimeArgumentType::get(context, "time")?; + set_warning_time(&context.source, time) } } -pub fn init_command_tree() -> CommandTree { - CommandTree::new(NAMES, DESCRIPTION) - .then( - literal("add").then( - argument_default_name(distance_consumer()) - .execute(AddExecutor) - .then(argument_default_name(time_consumer()).execute(AddTimeExecutor)), - ), - ) - .then( - literal("center") - .then(argument_default_name(Position2DArgumentConsumer).execute(CenterExecutor)), - ) - .then( - literal("damage") - .then( - literal("amount").then( - argument_default_name(damage_per_block_consumer()) - .execute(DamageAmountExecutor), +pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + dispatcher.register( + command("worldborder", DESCRIPTION) + .requires(PERMISSION) + .then( + literal("add").then( + argument( + "distance", + DoubleArgumentType::new(-5.999_997e7, 5.999_997e7), + ) + .executes(SetSizeExecutor { + is_add: true, + has_time: false, + }) + .then( + argument("time", TimeArgumentType::new(0)).executes(SetSizeExecutor { + is_add: true, + has_time: true, + }), ), - ) - .then(literal("buffer").then( - argument_default_name(damage_buffer_consumer()).execute(DamageBufferExecutor), - )), - ) - .then(literal("get").execute(GetExecutor)) - .then( - literal("set").then( - argument_default_name(distance_consumer()) - .execute(SetExecutor) - .then(argument_default_name(time_consumer()).execute(SetTimeExecutor)), - ), - ) - .then( - literal("warning") - .then( - literal("distance").then( - argument_default_name(warning_distance_consumer()) - .execute(WarningDistanceExecutor), - ), - ) - .then( - literal("time") - .then(argument_default_name(time_consumer()).execute(WarningTimeExecutor)), ), - ) + ) + .then( + literal("set").then( + argument( + "distance", + DoubleArgumentType::new(-5.999_997e7, 5.999_997e7), + ) + .executes(SetSizeExecutor { + is_add: false, + has_time: false, + }) + .then( + argument("time", TimeArgumentType::new(0)).executes(SetSizeExecutor { + is_add: false, + has_time: true, + }), + ), + ), + ) + .then( + literal("center") + .then(argument("pos", Vec2ArgumentType::Default).executes(CenterExecutor)), + ) + .then( + literal("damage") + .then( + literal("amount").then( + argument("damagePerBlock", FloatArgumentType::with_min(0.0)) + .executes(DamageAmountExecutor), + ), + ) + .then( + literal("buffer").then( + argument("distance", FloatArgumentType::with_min(0.0)) + .executes(DamageBufferExecutor), + ), + ), + ) + .then(literal("get").executes(GetSizeExecutor)) + .then( + literal("warning") + .then( + literal("distance").then( + argument("distance", IntegerArgumentType::with_min(0)) + .executes(WarningDistanceExecutor), + ), + ) + .then(literal("time").then( + argument("time", TimeArgumentType::new(0)).executes(WarningTimeExecutor), + )), + ), + ); } diff --git a/crates/pumpkin/src/command/context/command_source.rs b/crates/pumpkin/src/command/context/command_source.rs index 8cd36c6e1..fa4e6bac6 100644 --- a/crates/pumpkin/src/command/context/command_source.rs +++ b/crates/pumpkin/src/command/context/command_source.rs @@ -383,6 +383,12 @@ impl CommandSource { self.entity.as_ref().and_then(|entity| entity.get_player()) } + /// Gets the player as an `Arc` from the underlying output sender. + #[must_use] + pub fn as_player(&self) -> Option> { + self.output.as_player() + } + /// Gets the player as a result: /// /// - If this source actually contains a player, it returns that wrapped in an [`Ok`]. diff --git a/crates/pumpkin/src/command/dispatcher.rs b/crates/pumpkin/src/command/dispatcher.rs index 32b493732..a3aa35c40 100644 --- a/crates/pumpkin/src/command/dispatcher.rs +++ b/crates/pumpkin/src/command/dispatcher.rs @@ -1,968 +1 @@ -use pumpkin_data::translation; -use pumpkin_util::text::TextComponent; -use pumpkin_util::text::click::ClickEvent; -use pumpkin_util::text::color::NamedColor; -use rustc_hash::FxHashMap; -use tracing::{debug, error, warn}; - -use super::args::ConsumedArgs; -use super::errors::command_syntax_error::{CommandSyntaxError, CommandSyntaxErrorContext}; -use super::errors::error_types; - -use crate::command::CommandSender; -use crate::command::dispatcher::CommandError::{ - CommandFailed, InvalidConsumption, InvalidRequirement, PermissionDenied, SyntaxError, -}; -use crate::command::tree::{Command, CommandTree, NodeType, RawArg, RawArgs}; -use crate::command::{ - context::string_range::StringRange, - suggestion::{Suggestion, suggestions::Suggestions}, -}; -use crate::server::Server; -use std::collections::HashMap; - -#[derive(Debug)] -pub enum CommandError { - /// This error means that there was an error while parsing a previously consumed argument. - /// That only happens when consumption is wrongly implemented, as it should ensure parsing may - /// never fail. - InvalidConsumption(Option), - /// Return this if a condition that a `Node::Require` should ensure is met is not met. - InvalidRequirement, - /// The command could not be executed due to insufficient permissions. - /// The user attempting to run the command lacks the necessary authorization. - PermissionDenied, - /// A general error occurred during command execution that doesn't fit into - /// more specific `CommandError` variants. - CommandFailed(TextComponent), - SyntaxError(CommandSyntaxError), -} - -impl CommandError { - #[must_use] - pub fn into_messages(self, cmd: &str) -> Vec { - match self { - InvalidConsumption(s) => { - error!( - "Error while parsing command \"{cmd}\": {s:?} was consumed, but couldn't be parsed" - ); - vec![TextComponent::text("Internal error (See logs for details)")] - } - InvalidRequirement => { - error!( - "Error while parsing command \"{cmd}\": a requirement that was expected was not met." - ); - vec![TextComponent::text("Internal error (See logs for details)")] - } - PermissionDenied => { - warn!("Permission denied for command \"{cmd}\""); - vec![TextComponent::text( - "I'm sorry, but you do not have permission to perform this command. Please contact the server administrator if you believe this is an error.", - )] - } - CommandFailed(s) => vec![s], - SyntaxError(s) => render_syntax_error_messages(s), - } - } -} - -#[derive(Debug)] -struct PathParsingFailure { - cursor: usize, - consumed_tokens: usize, - matched_any_node: bool, - syntax_error: Option, -} - -enum PathResult { - Matched, - Failed(PathParsingFailure), -} - -fn render_syntax_error_messages(syntax_error: CommandSyntaxError) -> Vec { - let Some(context) = syntax_error.context else { - return vec![syntax_error.message]; - }; - - let input = context.input; - let cursor = clamp_cursor_to_char_boundary(&input, context.cursor); - let context_start = last_n_char_start(&input, cursor, 10); - let before_cursor = &input[context_start..cursor]; - let remaining_input = &input[cursor..]; - - let mut context_message = TextComponent::text("") - .color_named(NamedColor::Gray) - .click_event(ClickEvent::SuggestCommand { - command: format!("/{input}").into(), - }); - if context_start > 0 { - context_message = context_message.add_child(TextComponent::text("...")); - } - context_message = context_message.add_child(TextComponent::text(before_cursor.to_string())); - - if !remaining_input.is_empty() { - context_message = context_message.add_child( - TextComponent::text(remaining_input.to_string()) - .color_named(NamedColor::Red) - .underlined(), - ); - } - - context_message = context_message.add_child( - TextComponent::translate_cross( - translation::java::COMMAND_CONTEXT_HERE, - translation::java::COMMAND_CONTEXT_HERE, - [], - ) - .color_named(NamedColor::Red) - .italic(), - ); - - vec![syntax_error.message, context_message] -} - -fn clamp_cursor_to_char_boundary(input: &str, cursor: usize) -> usize { - let mut clamped = cursor.min(input.len()); - while clamped > 0 && !input.is_char_boundary(clamped) { - clamped -= 1; - } - clamped -} - -fn last_n_char_start(input: &str, cursor: usize, char_count: usize) -> usize { - let mut start = cursor; - let mut seen = 0usize; - for (index, _) in input[..cursor].char_indices().rev() { - start = index; - seen += 1; - if seen == char_count { - break; - } - } - if seen < char_count { 0 } else { start } -} - -fn unknown_command_syntax_error(input: &str, cursor: usize) -> CommandSyntaxError { - let context = CommandSyntaxErrorContext { - input: input.to_string(), - cursor: clamp_cursor_to_char_boundary(input, cursor), - }; - error_types::DISPATCHER_UNKNOWN_COMMAND.create(&context) -} - -fn unknown_argument_syntax_error(input: &str, cursor: usize) -> CommandSyntaxError { - let context = CommandSyntaxErrorContext { - input: input.to_string(), - cursor: clamp_cursor_to_char_boundary(input, cursor), - }; - error_types::DISPATCHER_UNKNOWN_ARGUMENT.create(&context) -} - -fn select_parse_error( - input: &str, - failures: &[PathParsingFailure], - known_command: bool, -) -> CommandSyntaxError { - if failures.is_empty() { - return if known_command { - unknown_argument_syntax_error(input, input.len()) - } else { - unknown_command_syntax_error(input, input.len()) - }; - } - - let farthest_cursor = failures - .iter() - .map(|failure| failure.cursor) - .max() - .unwrap_or(input.len()); - - let best_progress = failures - .iter() - .filter(|failure| failure.cursor == farthest_cursor) - .map(|failure| failure.consumed_tokens) - .max() - .unwrap_or(0); - - let finalists = failures - .iter() - .filter(|failure| { - failure.cursor == farthest_cursor && failure.consumed_tokens == best_progress - }) - .collect::>(); - - let syntax_errors = finalists - .iter() - .filter_map(|failure| failure.syntax_error.clone()) - .collect::>(); - if syntax_errors.len() == 1 { - return syntax_errors[0].clone(); - } - - let matched_any_node = finalists.iter().any(|failure| failure.matched_any_node); - if matched_any_node || known_command { - unknown_argument_syntax_error(input, farthest_cursor) - } else { - unknown_command_syntax_error(input, farthest_cursor) - } -} - -fn next_unread_cursor(raw_args: &RawArgs<'_>, input_len: usize) -> usize { - raw_args.last().map_or(input_len, |arg| arg.start) -} - -fn path_failure( - raw_args: &RawArgs<'_>, - total_args: usize, - input_len: usize, - matched_any_node: bool, - syntax_error: Option, -) -> PathParsingFailure { - let syntax_error_cursor = syntax_error - .as_ref() - .and_then(|error| error.context.as_ref().map(|context| context.cursor)) - .unwrap_or_else(|| next_unread_cursor(raw_args, input_len)); - - PathParsingFailure { - cursor: syntax_error_cursor, - consumed_tokens: total_args.saturating_sub(raw_args.len()), - matched_any_node, - syntax_error, - } -} - -#[derive(Default, Clone)] -pub struct CommandDispatcher { - pub commands: FxHashMap, - pub permissions: FxHashMap, -} - -/// Stores registered [`CommandTree`]s and dispatches commands to them. -impl CommandDispatcher { - pub fn handle_command(&self, sender: &CommandSender, server: &Server, cmd: &str) { - let result = self.dispatch(sender, server, cmd); - sender.set_success_count(u32::from(result.is_ok())); - - if let Err(e) = result { - for text in e.into_messages(cmd) { - sender.send_message( - TextComponent::text("") - .add_child(text) - .color_named(pumpkin_util::text::color::NamedColor::Red), - ); - } - } - } - - /// server side suggestions (client side suggestions work independently) - /// - /// # todo - /// - make this less ugly - /// - do not query suggestions for the same consumer multiple times just because they are on different paths through the tree - pub(crate) fn find_suggestions<'a>( - &'a self, - src: &'a CommandSender, - server: &'a Server, - cmd: &'a str, - ) -> Suggestions { - let mut parts = cmd.split_whitespace(); - let Some(key) = parts.next() else { - return Suggestions::empty(); - }; - let mut raw_args: RawArgs<'a> = parts - .rev() - .map(|value| RawArg { - value, - start: 0, - end: 0, - input: cmd, - }) - .collect(); - - let Ok(tree) = self.get_tree(key) else { - return Suggestions::empty(); - }; - - // Gate suggestions on command permissions, mirroring `dispatch`. Many - // legacy commands store their permission only in `self.permissions` and - // have no in-tree `Require` node, so without this check their argument - // consumers would leak privileged data (e.g. banned/op/whitelisted - // player names) to unprivileged players via tab-complete. - let Some(permission) = self.permissions.get(key) else { - return Suggestions::empty(); - }; - - if !src.has_permission(server, permission.as_str()) { - return Suggestions::empty(); - } - - let mut suggestions = Vec::new(); - - // try paths and collect the nodes that fail - // todo: make this more fine-grained - for path in tree.iter_paths() { - match Self::try_find_suggestions_on_path(src, server, &path, tree, &mut raw_args, cmd) { - Err(InvalidConsumption(s)) => { - debug!( - "Error while parsing command \"{cmd}\": {s:?} was consumed, but couldn't be parsed" - ); - return Suggestions::empty(); - } - Err(InvalidRequirement) => { - debug!( - "Error while parsing command \"{cmd}\": a requirement that was expected was not met." - ); - return Suggestions::empty(); - } - Err(PermissionDenied) => { - debug!("Permission denied for command \"{cmd}\""); - return Suggestions::empty(); - } - Err(CommandFailed(_)) => { - debug!("Command failed"); - return Suggestions::empty(); - } - Err(SyntaxError(_)) => { - return Suggestions::empty(); - } - Ok(Some(new_suggestions)) => { - suggestions.push(new_suggestions); - } - Ok(None) => { - debug!("Command none"); - } - } - } - - Suggestions::merge(cmd, suggestions) - } - - #[allow(clippy::too_many_lines)] - pub(crate) fn split_parts(cmd: &str) -> Result<(&str, RawArgs<'_>), CommandError> { - if cmd.is_empty() { - return Err(CommandFailed(TextComponent::text("Empty Command"))); - } - let mut args = Vec::new(); - let mut current_arg_start = 0usize; - let mut in_single_quotes = false; - let mut in_double_quotes = false; - let mut in_braces = 0u32; - let mut in_brackets = 0u32; - let mut is_escaping = false; - for (i, c) in cmd.char_indices() { - if c == '\\' { - is_escaping = !is_escaping; - continue; - } - if is_escaping { - is_escaping = false; - continue; - } - match c { - '{' if !in_single_quotes && !in_double_quotes => { - in_braces += 1; - } - '}' if !in_single_quotes && !in_double_quotes => { - if in_braces == 0 { - return Err(CommandFailed(TextComponent::text("Unmatched braces"))); - } - in_braces -= 1; - } - '[' if !in_single_quotes && !in_double_quotes => { - in_brackets += 1; - } - ']' if !in_single_quotes && !in_double_quotes => { - if in_brackets == 0 { - return Err(CommandFailed(TextComponent::text("Unmatched brackets"))); - } - in_brackets -= 1; - } - '\'' if !in_double_quotes => { - in_single_quotes = !in_single_quotes; - } - '"' if !in_single_quotes => { - in_double_quotes = !in_double_quotes; - } - ' ' if !in_single_quotes - && !in_double_quotes - && in_braces == 0 - && in_brackets == 0 => - { - if current_arg_start != i { - args.push(RawArg { - value: &cmd[current_arg_start..i], - start: current_arg_start, - end: i, - input: cmd, - }); - } - current_arg_start = i + 1; - } - _ => {} - } - } - if current_arg_start != cmd.len() { - args.push(RawArg { - value: &cmd[current_arg_start..], - start: current_arg_start, - end: cmd.len(), - input: cmd, - }); - } - if in_single_quotes || in_double_quotes { - return Err(CommandFailed(TextComponent::text( - "Unmatched quotes at the end", - ))); - } - if in_braces != 0 { - return Err(CommandFailed(TextComponent::text( - "Unmatched braces at the end", - ))); - } - if in_brackets != 0 { - return Err(CommandFailed(TextComponent::text( - "Unmatched brackets at the end", - ))); - } - if args.is_empty() { - return Err(CommandFailed(TextComponent::text("Empty Command"))); - } - let key = args.remove(0).value; - args.reverse(); - Ok((key, args)) - } - - /// Execute a command using its corresponding [`CommandTree`]. - pub(crate) fn dispatch<'a>( - &'a self, - src: &CommandSender, - server: &'a Server, - cmd: &'a str, - ) -> Result<(), CommandError> { - let (key, raw_args) = Self::split_parts(cmd)?; - - let tree = self - .get_tree(key) - .map_err(|_| SyntaxError(unknown_command_syntax_error(cmd, 0)))?; - - let Some(permission) = self.permissions.get(key) else { - return Err(CommandFailed(TextComponent::text( - "Permission for Command not found".to_string(), - ))); - }; - - if !src.has_permission(server, permission.as_str()) { - return Err(PermissionDenied); - } - - let mut path_failures = Vec::new(); - - // try paths until fitting path is found - for path in tree.iter_paths() { - match Self::try_is_fitting_path(src, server, &path, tree, &mut raw_args.clone(), cmd) { - Ok(PathResult::Matched) => return Ok(()), - Ok(PathResult::Failed(failure)) => path_failures.push(failure), - Err(error) => return Err(error), - } - } - - Err(SyntaxError(select_parse_error(cmd, &path_failures, true))) - } - - pub fn get_tree<'a>(&'a self, key: &str) -> Result<&'a CommandTree, CommandError> { - let command = self - .commands - .get(key) - .ok_or(CommandFailed(TextComponent::text("Command not found")))?; - - match command { - Command::Tree(tree) => Ok(tree), - Command::Alias(target) => { - let Some(Command::Tree(tree)) = self.commands.get(target) else { - error!( - "Error while parsing command alias \"{key}\": pointing to \"{target}\" which is not a valid tree" - ); - return Err(CommandFailed(TextComponent::text( - "Internal Error (See logs for details)", - ))); - }; - Ok(tree) - } - } - } - - #[allow(clippy::too_many_lines)] - fn try_is_fitting_path<'a>( - src: &'a CommandSender, - server: &'a Server, - path: &[usize], - tree: &'a CommandTree, - raw_args: &mut RawArgs<'a>, - input: &str, - ) -> Result { - let mut parsed_args: ConsumedArgs = HashMap::new(); - let total_args = raw_args.len(); - let mut matched_any_node = false; - let input_len = input.len(); - - for node in path.iter().map(|&i| &tree.nodes[i]) { - match &node.node_type { - NodeType::ExecuteLeaf { executor } => { - return if raw_args.is_empty() { - executor.execute(src, server, &parsed_args)?; - Ok(PathResult::Matched) - } else { - debug!( - "Error while parsing command: {raw_args:?} was not consumed, but should have been" - ); - Ok(PathResult::Failed(path_failure( - raw_args, - total_args, - input_len, - matched_any_node, - None, - ))) - }; - } - NodeType::Literal { string, .. } => { - let Some(raw_arg) = raw_args.last() else { - return Ok(PathResult::Failed(path_failure( - raw_args, - total_args, - input_len, - matched_any_node, - None, - ))); - }; - if raw_arg.value != string.as_str() { - debug!("Error while parsing command: {raw_args:?}: expected {string}"); - return Ok(PathResult::Failed(path_failure( - raw_args, - total_args, - input_len, - matched_any_node, - None, - ))); - } - raw_args.pop(); - matched_any_node = true; - } - NodeType::Argument { consumer, name, .. } => { - match consumer.consume_with_syntax(src, server, raw_args) { - Ok(Some(consumed)) => { - parsed_args.insert(name, consumed); - matched_any_node = true; - } - Ok(None) => { - debug!( - "Error while parsing command: {raw_args:?}: cannot parse argument {name}" - ); - return Ok(PathResult::Failed(path_failure( - raw_args, - total_args, - input_len, - matched_any_node, - None, - ))); - } - Err(error) => { - return Ok(PathResult::Failed(path_failure( - raw_args, - total_args, - input_len, - matched_any_node, - Some(error), - ))); - } - } - } - NodeType::Require { predicate, .. } => { - if !predicate(src) { - debug!( - "Error while parsing command: {raw_args:?} does not meet the requirement" - ); - return Ok(PathResult::Failed(path_failure( - raw_args, - total_args, - input_len, - matched_any_node, - None, - ))); - } - matched_any_node = true; - } - } - } - - debug!("Error while parsing command: {raw_args:?} was not consumed, but should have been"); - Ok(PathResult::Failed(path_failure( - raw_args, - total_args, - input_len, - matched_any_node, - None, - ))) - } - - fn try_find_suggestions_on_path<'a>( - src: &'a CommandSender, - server: &'a Server, - path: &[usize], - tree: &'a CommandTree, - raw_args: &mut RawArgs<'a>, - input: &'a str, - ) -> Result, CommandError> { - //let mut parsed_args: ConsumedArgs = HashMap::new(); - - for node in path.iter().map(|&i| &tree.nodes[i]) { - match &node.node_type { - NodeType::ExecuteLeaf { .. } => { - return Ok(None); - } - NodeType::Literal { string, .. } => { - if raw_args.pop().map(|arg| arg.value) != Some(string.as_str()) { - return Ok(None); - } - } - NodeType::Argument { - consumer, - suggestion_provider, - name: _, - } => { - match consumer.consume_with_syntax(src, server, raw_args) { - Ok(Some(_consumed)) => { - //parsed_args.insert(name, consumed); - } - Ok(None) => { - return if raw_args.is_empty() { - let start = input - .char_indices() - .rfind(|(_, c)| c.is_whitespace()) - .map_or(0, |(index, c)| index + c.len_utf8()); - if let Some(provider) = suggestion_provider { - Ok(Some(provider.suggest( - src, - server, - input, - start, - input.len(), - ))) - } else { - let suggestions = consumer.suggest(src, server, input)?; - let range = StringRange::between(start, input.len()); - Ok(suggestions.map(|suggestions| { - Suggestions::new( - range, - suggestions - .into_iter() - .map(|suggestion| match suggestion.tooltip { - Some(tooltip) => Suggestion::with_tooltip( - range, - suggestion.suggestion, - tooltip, - ), - None => Suggestion::without_tooltip( - range, - suggestion.suggestion, - ), - }) - .collect(), - ) - })) - } - } else { - Ok(None) - }; - } - Err(_) => return Ok(None), - } - } - NodeType::Require { predicate, .. } => { - if !predicate(src) { - return Ok(None); - } - } - } - } - - Ok(None) - } - - /// Register a command with the dispatcher. - pub fn register>(&mut self, tree: CommandTree, permission: P) { - let names = tree.names.clone(); - let mut names_iter = names.iter(); - let permission = permission.into(); - - let Some(primary_name) = names_iter.next() else { - tracing::warn!("Command registration skipped: command tree has no names"); - return; - }; - - for name in names_iter { - self.commands - .insert(name.clone(), Command::Alias(primary_name.clone())); - self.permissions.insert(name.clone(), permission.clone()); - - // For double-slash or slash-prefixed commands (like WorldEdit's //set or /set), - // automatically register the alternate slash variant as an alias. - if let Some(stripped) = name.strip_prefix("//") { - let single_slash = format!("/{stripped}"); - if !self.commands.contains_key(&single_slash) && &single_slash != primary_name { - self.commands - .insert(single_slash.clone(), Command::Alias(primary_name.clone())); - self.permissions.insert(single_slash, permission.clone()); - } - } else if let Some(stripped) = name.strip_prefix('/') { - let double_slash = format!("//{stripped}"); - if !self.commands.contains_key(&double_slash) && &double_slash != primary_name { - self.commands - .insert(double_slash.clone(), Command::Alias(primary_name.clone())); - self.permissions.insert(double_slash, permission.clone()); - } - } - } - - self.permissions - .insert(primary_name.clone(), permission.clone()); - self.commands - .insert(primary_name.clone(), Command::Tree(tree)); - - // For double-slash or slash-prefixed primary commands (like //set or /set), - // automatically register the alternate slash variant as an alias. - if let Some(stripped) = primary_name.strip_prefix("//") { - let single_slash = format!("/{stripped}"); - if !self.commands.contains_key(&single_slash) { - self.commands - .insert(single_slash.clone(), Command::Alias(primary_name.clone())); - self.permissions.insert(single_slash, permission); - } - } else if let Some(stripped) = primary_name.strip_prefix('/') { - let double_slash = format!("//{stripped}"); - if !self.commands.contains_key(&double_slash) { - self.commands - .insert(double_slash.clone(), Command::Alias(primary_name.clone())); - self.permissions.insert(double_slash, permission); - } - } - } - - /// Remove a command from the dispatcher by its primary name. - pub fn unregister(&mut self, name: &str) { - let mut to_remove = Vec::new(); - for (key, value) in &self.commands { - if key == name { - to_remove.push(key.clone()); - } else if let Command::Alias(target) = value - && target == name - { - to_remove.push(key.clone()); - } - } - - if let Some(stripped) = name.strip_prefix("//") { - let single_slash = format!("/{stripped}"); - for (key, value) in &self.commands { - if key == &single_slash { - to_remove.push(key.clone()); - } else if let Command::Alias(target) = value - && target == &single_slash - { - to_remove.push(key.clone()); - } - } - } else if let Some(stripped) = name.strip_prefix('/') { - let double_slash = format!("//{stripped}"); - for (key, value) in &self.commands { - if key == &double_slash { - to_remove.push(key.clone()); - } else if let Command::Alias(target) = value - && target == &double_slash - { - to_remove.push(key.clone()); - } - } - } - - for key in to_remove { - self.commands.remove(&key); - self.permissions.remove(&key); - } - } -} - -#[cfg(test)] -mod test { - use pumpkin_data::translation; - use pumpkin_util::permission::PermissionManager; - use pumpkin_util::text::TextContent; - use pumpkin_util::text::click::ClickEvent; - use pumpkin_util::text::color::{Color, NamedColor}; - - use super::{ - PathParsingFailure, render_syntax_error_messages, select_parse_error, - unknown_argument_syntax_error, - }; - use crate::command::errors::error_types; - use crate::command::{commands::default_dispatcher, tree::CommandTree}; - - fn component_plain_text(component: &pumpkin_util::text::TextComponentBase) -> Option<&str> { - if let TextContent::Text { text } = component.content.as_ref() { - Some(text.as_ref()) - } else { - None - } - } - - #[test] - fn dynamic_command() { - let commands_config = pumpkin_config::CommandsConfig::default(); - let manager = PermissionManager::new(); - let mut dispatcher = default_dispatcher(&manager, &commands_config).fallback_dispatcher; - let tree = CommandTree::new(["test"], "test_desc"); - dispatcher.register(tree, "minecraft:test"); - } - - #[test] - fn pumpkin_command_aliases() { - let commands_config = pumpkin_config::CommandsConfig::default(); - let manager = PermissionManager::new(); - let dispatcher = default_dispatcher(&manager, &commands_config).fallback_dispatcher; - - let pumpkin_tree = dispatcher.get_tree("pumpkin").unwrap(); - let version_tree = dispatcher.get_tree("version").unwrap(); - let ver_tree = dispatcher.get_tree("ver").unwrap(); - - assert_eq!(pumpkin_tree.description, version_tree.description); - assert_eq!(pumpkin_tree.description, ver_tree.description); - } - - #[test] - fn syntax_renderer_outputs_two_messages_with_context_styling() { - let input = "0123456789abcdefghij"; - let error = unknown_argument_syntax_error(input, 15); - let messages = render_syntax_error_messages(error); - - assert_eq!(messages.len(), 2); - - let context = &messages[1].0; - assert_eq!(context.style.color, Some(Color::Named(NamedColor::Gray))); - assert_eq!( - context.style.click_event, - Some(ClickEvent::SuggestCommand { - command: format!("/{input}").into() - }) - ); - assert_eq!(context.extra.len(), 4); - - assert_eq!(component_plain_text(&context.extra[0]), Some("...")); - assert_eq!(component_plain_text(&context.extra[1]), Some("56789abcde")); - assert_eq!(component_plain_text(&context.extra[2]), Some("fghij")); - assert_eq!( - context.extra[2].style.color, - Some(Color::Named(NamedColor::Red)) - ); - assert_eq!(context.extra[2].style.underlined, Some(true)); - - let here_component = &context.extra[3]; - if let TextContent::Translate { translate, .. } = here_component.content.as_ref() { - assert_eq!(translate, translation::java::COMMAND_CONTEXT_HERE); - } else { - panic!("expected translate component for command.context.here"); - } - assert_eq!( - here_component.style.color, - Some(Color::Named(NamedColor::Red)) - ); - assert_eq!(here_component.style.italic, Some(true)); - } - - #[test] - fn parse_error_selection_prefers_farthest_cursor_then_progress() { - let fallback_error = unknown_argument_syntax_error("test one two", 5); - let preferred_error = unknown_argument_syntax_error("test one two", 9); - - let selected = select_parse_error( - "test one two", - &[ - PathParsingFailure { - cursor: 5, - consumed_tokens: 2, - matched_any_node: true, - syntax_error: Some(fallback_error), - }, - PathParsingFailure { - cursor: 9, - consumed_tokens: 1, - matched_any_node: true, - syntax_error: None, - }, - PathParsingFailure { - cursor: 9, - consumed_tokens: 2, - matched_any_node: true, - syntax_error: Some(preferred_error.clone()), - }, - ], - true, - ); - - assert_eq!(selected.context, preferred_error.context); - } - - #[test] - fn parse_error_selection_synthesizes_unknown_argument_for_tied_syntax_errors() { - let selected = select_parse_error( - "alpha beta gamma", - &[ - PathParsingFailure { - cursor: 12, - consumed_tokens: 2, - matched_any_node: true, - syntax_error: Some(unknown_argument_syntax_error("alpha beta gamma", 12)), - }, - PathParsingFailure { - cursor: 12, - consumed_tokens: 2, - matched_any_node: true, - syntax_error: Some(unknown_argument_syntax_error("alpha beta gamma", 12)), - }, - ], - true, - ); - - assert!(selected.is(&error_types::DISPATCHER_UNKNOWN_ARGUMENT)); - } - - #[test] - fn parse_error_selection_can_synthesize_unknown_command_when_not_matched() { - let selected = select_parse_error( - "abc", - &[PathParsingFailure { - cursor: 0, - consumed_tokens: 0, - matched_any_node: false, - syntax_error: None, - }], - false, - ); - - assert!(selected.is(&error_types::DISPATCHER_UNKNOWN_COMMAND)); - } - - #[test] - fn double_slash_command_registration_and_lookup() { - let mut dispatcher = super::CommandDispatcher::default(); - let tree = CommandTree::new(["//set", "/set"], "WorldEdit set block"); - dispatcher.register(tree, "worldedit.set"); - - assert!(dispatcher.commands.contains_key("//set")); - assert!(dispatcher.commands.contains_key("/set")); - assert!(dispatcher.get_tree("//set").is_ok()); - assert!(dispatcher.get_tree("/set").is_ok()); - - dispatcher.unregister("//set"); - assert!(!dispatcher.commands.contains_key("//set")); - assert!(!dispatcher.commands.contains_key("/set")); - } -} +pub use crate::command::node::dispatcher::*; diff --git a/crates/pumpkin/src/command/mod.rs b/crates/pumpkin/src/command/mod.rs index 0754011c6..90a69d8df 100644 --- a/crates/pumpkin/src/command/mod.rs +++ b/crates/pumpkin/src/command/mod.rs @@ -5,16 +5,13 @@ use std::str::FromStr; use std::sync::Arc; use std::sync::atomic::AtomicBool; -use crate::entity::player::Player; -use crate::server::Server; -use crate::world::World; -use args::ConsumedArgs; - use crate::block::entities::BlockEntity; use crate::block::entities::command_block::CommandBlockEntity; use crate::command::context::command_source::CommandSource; use crate::entity::EntityBase; -use dispatcher::CommandError; +use crate::entity::player::Player; +use crate::server::Server; +use crate::world::World; use pumpkin_data::{ Block, block_properties::{BlockProperties, CommandBlockLikeProperties, Facing}, @@ -26,7 +23,6 @@ use pumpkin_util::permission::{PermissionDefault, PermissionLvl}; use pumpkin_util::text::TextComponent; use pumpkin_util::translation::Locale; -pub mod args; pub mod argument_builder; pub mod argument_types; pub mod client_suggestions; @@ -39,7 +35,6 @@ pub mod parser; pub mod snbt; pub mod string_reader; pub mod suggestion; -pub mod tree; /// Whether console and RCON command output is broadcast to online operators. /// @@ -403,27 +398,5 @@ const fn command_block_y_rot(facing: Facing) -> f32 { } } -/// Represents the result of running a command after completion. -/// -/// If the command **ran successfully**, an [`Ok`] is returned containing an [`i32`]. -/// This represents the 'output value' of the command, which is *homologous* to the -/// `int` that command executors in vanilla return **upon success**. -/// -/// **You should choose the successful result as `1` if**: -/// - you don't know what value to use for a success for your -/// own commands, or -/// - you don't understand what this value means, or -/// - you just simply don't care about this value at all -/// -/// If the command **fails**, an [`Err`] is returned, containing the [`CommandError`] -/// that led to this result. -pub type CommandResult = Result; - -pub trait CommandExecutor: Sync + Send { - fn execute( - &self, - sender: &CommandSender, - server: &Server, - args: &ConsumedArgs, - ) -> CommandResult; -} +pub use node::dispatcher::CommandDispatcher; +pub use node::{Command, CommandExecutor, CommandExecutorResult}; diff --git a/crates/pumpkin/src/command/node/detached.rs b/crates/pumpkin/src/command/node/detached.rs index 8bcea4113..8f882883f 100644 --- a/crates/pumpkin/src/command/node/detached.rs +++ b/crates/pumpkin/src/command/node/detached.rs @@ -43,6 +43,7 @@ impl Default for GlobalNodeId { /// If you want to start a command with this node, use [`CommandDetachedNode`] instead. /// /// To be of any utility, this must be attached to a tree later. +#[derive(Clone)] pub struct LiteralDetachedNode { pub owned: OwnedNodeData, pub children: FxHashMap, @@ -86,6 +87,7 @@ impl LiteralDetachedNode { /// If you don't want to start a command with this node, use [`LiteralDetachedNode`] instead. /// /// To be of any utility, this must be attached to a tree later. +#[derive(Clone)] pub struct CommandDetachedNode { pub owned: OwnedNodeData, pub children: FxHashMap, @@ -128,6 +130,7 @@ impl CommandDetachedNode { /// Represents a node that accepts a specific type of argument. /// /// To be of any utility, this must be attached to a tree later. +#[derive(Clone)] pub struct ArgumentDetachedNode { pub owned: OwnedNodeData, pub children: FxHashMap, @@ -169,6 +172,7 @@ impl ArgumentDetachedNode { } /// Represents a node not attached to a `Tree` yet. +#[derive(Clone)] pub enum DetachedNode { Literal(LiteralDetachedNode), Command(CommandDetachedNode), diff --git a/crates/pumpkin/src/command/node/dispatcher.rs b/crates/pumpkin/src/command/node/dispatcher.rs index 1884986ce..7d4b0ff4c 100644 --- a/crates/pumpkin/src/command/node/dispatcher.rs +++ b/crates/pumpkin/src/command/node/dispatcher.rs @@ -14,7 +14,6 @@ use crate::command::node::detached::CommandDetachedNode; use crate::command::node::tree::{NodeIdClassification, ROOT_NODE_ID, Tree}; use crate::command::string_reader::StringReader; use crate::command::suggestion::suggestions::{Suggestions, SuggestionsBuilder}; -use crate::command::tree::Command; use pumpkin_data::translation::java::COMMAND_CONTEXT_HERE; use pumpkin_protocol::java::client::play::CommandSuggestion; use pumpkin_util::text::TextComponent; @@ -23,7 +22,6 @@ use pumpkin_util::text::color::{Color, NamedColor}; use rustc_hash::{FxHashMap, FxHashSet}; use std::collections::BTreeMap; use std::sync::{Arc, LazyLock}; -use tracing::warn; pub const ARG_SEPARATOR: &str = " "; pub const ARG_SEPARATOR_CHAR: char = ' '; @@ -82,11 +80,6 @@ pub struct CommandDispatcher { pub tree: Tree, pub consumer: Arc, - // Temporary setup: - // We add this because we have a lot of commands - // still dependent on this dispatcher. - pub fallback_dispatcher: crate::command::dispatcher::CommandDispatcher, - /// Primary names of commands that have been turned off through the server /// configuration. A disabled command behaves as if it does not exist: it /// cannot be executed and is left out of listings and suggestions. @@ -111,7 +104,6 @@ impl CommandDispatcher { Self { tree, consumer: RESULT_DEFERRER.clone(), - fallback_dispatcher: crate::command::dispatcher::CommandDispatcher::default(), disabled: FxHashSet::default(), } } @@ -144,10 +136,10 @@ impl CommandDispatcher { } /// Returns `true` if a command (or alias) with the given name is registered - /// on either the node-based tree or the legacy dispatcher. + /// on the node-based tree. #[must_use] pub fn has_command(&self, name: &str) -> bool { - self.tree.get(name).is_some() || self.fallback_dispatcher.commands.contains_key(name) + self.tree.get(name).is_some() } /// Collects the names of every root-level alias that redirects to the command @@ -179,6 +171,21 @@ impl CommandDispatcher { names } + /// Resolves the primary name of a command from either an alias or a primary name. + #[must_use] + pub fn primary_command_name(&self, name: &str) -> String { + if let Some(node_id) = self.tree.get(name) { + let node = &self.tree[NodeId::from(node_id)]; + if let Some(redirect) = node.redirect() + && let Some(resolved) = self.tree.resolve(redirect) + { + return self.tree[resolved].name().to_ascii_lowercase(); + } + return node.name().to_ascii_lowercase(); + } + name.to_string() + } + /// Extracts the command name (the first whitespace-separated token) from a /// raw input string. fn command_name(input: &str) -> &str { @@ -511,21 +518,7 @@ impl CommandDispatcher { let output = self.execute_input(input, source); if let Err(error) = output { - // We check if the error came because a command could not be found. - // Note: 'Permission denied' also falls under this error as - // no executable node could be found. - if error.is(&DISPATCHER_UNKNOWN_COMMAND) { - // Run the fallback dispatcher instead. - // It might have the command we're looking for. - self.fallback_dispatcher.handle_command( - &source.output, - source.server().as_ref(), - input, - ); - } else { - // Print the error to the output. - Self::send_error_to_source(source, error, input); - } + Self::send_error_to_source(source, error, input); } } @@ -679,12 +672,7 @@ impl CommandDispatcher { } let parsed = self.parse_input(input, source); - let s1 = self.get_completion_suggestions_at_end(parsed); - let s2 = self - .fallback_dispatcher - .find_suggestions(&source.output, source.server(), input); - - Suggestions::merge(input, vec![s1, s2]) + self.get_completion_suggestions_at_end(parsed) } /// Gets all the commands usable in this dispatcher, sorted. @@ -702,17 +690,6 @@ impl CommandDispatcher { commands.insert(&meta.literal_lowercase, &meta.description); } - for fallback_command in self.fallback_dispatcher.commands.values() { - if let Command::Tree(command_tree) = fallback_command { - if self.is_disabled(&command_tree.names[0]) { - continue; - } - for name in &command_tree.names { - commands.insert(name, &command_tree.description); - } - } - } - commands } @@ -734,29 +711,6 @@ impl CommandDispatcher { } } - for fallback_command in self.fallback_dispatcher.commands.values() { - if let Command::Tree(command_tree) = fallback_command { - if self.is_disabled(&command_tree.names[0]) { - continue; - } - if let Some(permission) = self - .fallback_dispatcher - .permissions - .get(&command_tree.names[0]) - && source.has_permission(permission) - { - for name in &command_tree.names { - commands.insert(name, &command_tree.description); - } - } else { - warn!( - "Command /{} does not have a permission set up", - &command_tree.names[0] - ); - } - } - } - commands } @@ -781,28 +735,6 @@ impl CommandDispatcher { commands.insert(command_name, (command_description, usage.into_boxed_str())); } - for fallback_command in self.fallback_dispatcher.commands.values() { - if let Command::Tree(command_tree) = fallback_command - && !self.is_disabled(&command_tree.names[0]) - && let Some(permission) = self - .fallback_dispatcher - .permissions - .get(&command_tree.names[0]) - && source.has_permission(permission) - { - let usage = command_tree.to_string(); - for name in &command_tree.names { - commands.insert( - name, - ( - command_tree.description.as_ref(), - usage.clone().into_boxed_str(), - ), - ); - } - } - } - commands } @@ -816,26 +748,17 @@ impl CommandDispatcher { ) -> BTreeMap<&str, (&str, Box)> { let mut commands: BTreeMap<&str, (&str, Box)> = BTreeMap::new(); - for fallback_command in self.fallback_dispatcher.commands.values() { - if let Command::Tree(command_tree) = fallback_command - && let Some(source_name) = &command_tree.source - && source_name == plugin_name - && let Some(permission) = self - .fallback_dispatcher - .permissions - .get(&command_tree.names[0]) - && source.has_permission(permission) + for (command_node_id, usage) in self.get_usage_of_commands(source) { + let meta = &self.tree[command_node_id].meta; + if self.is_disabled(&meta.literal_lowercase) { + continue; + } + if let Some(src) = &meta.source + && src == plugin_name { - let usage = command_tree.to_string(); - for name in &command_tree.names { - commands.insert( - name, - ( - command_tree.description.as_ref(), - usage.clone().into_boxed_str(), - ), - ); - } + let command_name = meta.literal.as_ref(); + let command_description = meta.description.as_ref(); + commands.insert(command_name, (command_description, usage.into_boxed_str())); } } @@ -852,25 +775,6 @@ impl CommandDispatcher { &self, source: &CommandSource, command: &str, - ) -> Option<(&str, Box)> { - if let Some(output) = self.get_permitted_command_usage_non_fallback(source, command) { - Some(output) - } else { - let tree = self.fallback_dispatcher.get_tree(command).ok()?; - if let Some(permission) = self.fallback_dispatcher.permissions.get(&tree.names[0]) - && source.has_permission(permission) - { - Some((tree.description.as_ref(), tree.to_string().into_boxed_str())) - } else { - None - } - } - } - - fn get_permitted_command_usage_non_fallback( - &self, - source: &CommandSource, - command: &str, ) -> Option<(&str, Box)> { let command_node_id = self.tree.get(command)?; diff --git a/crates/pumpkin/src/command/node/mod.rs b/crates/pumpkin/src/command/node/mod.rs index c582fc96a..54dd676a0 100644 --- a/crates/pumpkin/src/command/node/mod.rs +++ b/crates/pumpkin/src/command/node/mod.rs @@ -182,6 +182,7 @@ pub struct CommandNodeMetadata { pub literal: Cow<'static, str>, pub literal_lowercase: String, pub description: Cow<'static, str>, + pub source: Option, } impl CommandNodeMetadata { @@ -194,6 +195,7 @@ impl CommandNodeMetadata { literal: literal.clone(), literal_lowercase: literal.to_lowercase(), description: description.into(), + source: None, } } } diff --git a/crates/pumpkin/src/command/node/tree.rs b/crates/pumpkin/src/command/node/tree.rs index b8e2c1811..5f3e67cd4 100644 --- a/crates/pumpkin/src/command/node/tree.rs +++ b/crates/pumpkin/src/command/node/tree.rs @@ -111,6 +111,18 @@ impl Tree { } } + /// Returns the number of nodes in this tree. + #[must_use] + pub const fn len(&self) -> usize { + self.nodes.len() + } + + /// Returns `true` if the tree has no nodes. + #[must_use] + pub const fn is_empty(&self) -> bool { + self.nodes.is_empty() + } + /// Allocates a new [`NodeId`] by creating a new unique one. const fn alloc(&self) -> NodeId { NodeId(NonZero::new(self.nodes.len() + 1).expect("expected a non-zero id")) diff --git a/crates/pumpkin/src/command/tree/builder.rs b/crates/pumpkin/src/command/tree/builder.rs deleted file mode 100644 index 6997e40fd..000000000 --- a/crates/pumpkin/src/command/tree/builder.rs +++ /dev/null @@ -1,206 +0,0 @@ -use std::borrow::Cow; -use std::sync::Arc; - -use super::CommandExecutor; -use crate::command::CommandSender; -use crate::command::args::{ArgumentConsumer, DefaultNameArgConsumer}; -use crate::command::tree::{CommandSuggestionProvider, CommandTree, Node, NodeType}; - -impl CommandTree { - /// Add a child [Node] to the root of this [`CommandTree`]. - #[must_use] - pub fn then(mut self, child: impl NodeBuilder) -> Self { - let node = child.build(&mut self); - self.children.push(self.nodes.len()); - self.nodes.push(node); - self - } - - /// provide at least one name - #[must_use] - pub fn new( - names: impl IntoIterator>, - description: impl Into>, - ) -> Self { - let names_vec = names.into_iter().map(Into::into).collect(); - - Self { - nodes: Vec::new(), - children: Vec::new(), - names: names_vec, - description: description.into(), - source: None, - } - } - - /// Executes if a command terminates at this [Node], i.e. without any arguments. - /// - /// `ConsumedArgs` maps the names of all - /// arguments to the result of their consumption, i.e. a string that can be parsed to the - /// desired type. - /// - /// Also see [`NonLeafNodeBuilder::execute`]. - #[must_use] - pub fn execute(mut self, executor: impl CommandExecutor + 'static) -> Self { - let node = Node { - node_type: NodeType::ExecuteLeaf { - executor: Arc::new(executor), - }, - children: Vec::new(), - }; - - self.children.push(self.nodes.len()); - self.nodes.push(node); - - self - } -} - -pub trait NodeBuilder { - fn build(self, tree: &mut CommandTree) -> Node; -} - -struct LeafNodeBuilder { - node_type: NodeType, -} - -impl NodeBuilder for LeafNodeBuilder { - fn build(self, _tree: &mut CommandTree) -> Node { - Node { - children: Vec::new(), - node_type: self.node_type, - } - } -} - -pub struct NonLeafNodeBuilder { - node_type: NodeType, - child_nodes: Vec, - leaf_nodes: Vec, -} - -impl NodeBuilder for NonLeafNodeBuilder { - fn build(self, tree: &mut CommandTree) -> Node { - let mut child_indices = Vec::new(); - - for node_builder in self.child_nodes { - let node = node_builder.build(tree); - child_indices.push(tree.nodes.len()); - tree.nodes.push(node); - } - - for node_builder in self.leaf_nodes { - let node = node_builder.build(tree); - child_indices.push(tree.nodes.len()); - tree.nodes.push(node); - } - - Node { - children: child_indices, - node_type: self.node_type, - } - } -} - -impl NonLeafNodeBuilder { - /// Add a child [Node] to this one. - #[must_use] - pub fn then(mut self, child: Self) -> Self { - self.child_nodes.push(child); - self - } - - /// Executes if a command terminates at this [Node]. - /// - /// `ConsumedArgs` maps the names of all - /// arguments to the result of their consumption, i.e. a string that can be parsed to the - /// desired type. - /// - /// Also see [`CommandTree::execute`]. - #[must_use] - pub fn execute(mut self, executor: impl CommandExecutor + 'static) -> Self { - self.leaf_nodes.push(LeafNodeBuilder { - node_type: NodeType::ExecuteLeaf { - executor: Arc::new(executor), - }, - }); - - self - } - - #[must_use] - pub fn suggests(mut self, provider: impl CommandSuggestionProvider + 'static) -> Self { - if let NodeType::Argument { - suggestion_provider, - .. - } = &mut self.node_type - { - *suggestion_provider = Some(Arc::new(provider)); - } - self - } -} - -/// Matches a string literal. -#[must_use] -pub fn literal(string: impl Into) -> NonLeafNodeBuilder { - NonLeafNodeBuilder { - node_type: NodeType::Literal { - string: string.into(), - }, - child_nodes: Vec::new(), - leaf_nodes: Vec::new(), - } -} - -/// `name` identifies this argument in `ConsumedArgs`. -/// -/// `consumer: ArgumentConsumer` has the purpose of validating arguments. Conversion may start -/// here, as long as the result remains a [String] (e.g. convert offset to absolute position actual -/// coordinates), because the result of this function will be passed to following -/// [`NonLeafNodeBuilder::execute`] nodes in a `ConsumedArgs` instance. It must remove consumed arg(s) -/// from `RawArgs` and return them. It must return None if `RawArgs` are invalid. `RawArgs` is -/// reversed, so [`Vec::pop`] can be used to obtain args in ltr order. -pub fn argument( - name: impl Into, - consumer: impl ArgumentConsumer + 'static, -) -> NonLeafNodeBuilder { - NonLeafNodeBuilder { - node_type: NodeType::Argument { - name: name.into(), - consumer: Arc::new(consumer), - suggestion_provider: None, - }, - child_nodes: Vec::new(), - leaf_nodes: Vec::new(), - } -} - -/// same as [`crate::command::tree::builder::argument`], but uses default arg name of consumer -pub fn argument_default_name( - consumer: impl DefaultNameArgConsumer + 'static + Send, -) -> NonLeafNodeBuilder { - NonLeafNodeBuilder { - node_type: NodeType::Argument { - name: consumer.default_name().to_string(), - consumer: Arc::new(consumer), - suggestion_provider: None, - }, - child_nodes: Vec::new(), - leaf_nodes: Vec::new(), - } -} - -/// ```predicate``` should return ```false``` if requirement for reaching following [Node]s is not -/// met. -pub fn require( - predicate: impl Fn(&CommandSender) -> bool + Send + Sync + 'static, -) -> NonLeafNodeBuilder { - NonLeafNodeBuilder { - node_type: NodeType::Require { - predicate: Arc::new(predicate), - }, - child_nodes: Vec::new(), - leaf_nodes: Vec::new(), - } -} diff --git a/crates/pumpkin/src/command/tree/format.rs b/crates/pumpkin/src/command/tree/format.rs deleted file mode 100644 index 89ff3a801..000000000 --- a/crates/pumpkin/src/command/tree/format.rs +++ /dev/null @@ -1,122 +0,0 @@ -use crate::command::tree::{CommandTree, Node, NodeType}; -use std::fmt::{Display, Formatter, Write}; - -trait IsVisible { - /// whether node should be printed in help command/usage hint - fn is_visible(&self) -> bool; -} - -impl IsVisible for Node { - fn is_visible(&self) -> bool { - matches!( - self.node_type, - NodeType::Literal { .. } | NodeType::Argument { .. } - ) - } -} - -impl Display for Node { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match &self.node_type { - NodeType::Literal { string } => { - f.write_str(string)?; - } - NodeType::Argument { name, .. } => { - f.write_char('<')?; - f.write_str(name)?; - f.write_char('>')?; - } - _ => {} - } - - Ok(()) - } -} - -fn flatten_require_nodes(nodes: &[Node], children: &[usize]) -> Vec { - let mut new_children = Vec::with_capacity(children.len()); - - for &i in children { - let node = &nodes[i]; - match &node.node_type { - NodeType::Require { .. } => { - new_children.extend(flatten_require_nodes(nodes, &node.children)); - } - _ => new_children.push(i), - } - } - - new_children -} - -fn write_tree(f: &mut Formatter<'_>, nodes: &[Node], children: &[usize]) -> std::fmt::Result { - // Map node indices to actual nodes - // NOTE: We assume that Require nodes have already been "flattened" out - let nodes_iter = children.iter().map(|&idx| &nodes[idx]); - - // Check if there is a sibling of type ExecuteLeaf - // If there is, arguments on the current level are optional and will be printed surrounded by square brackets - let argument_is_optional = nodes_iter - .clone() - .any(|node| matches!(node.node_type, NodeType::ExecuteLeaf { .. })); - - let visible_nodes: Vec<&Node> = nodes_iter - .clone() - .filter(|node| node.is_visible()) - .collect(); - - if visible_nodes.is_empty() { - return Ok(()); - } - - let multiple_visible_nodes = visible_nodes.len() > 1; - - write!(f, " ")?; - - if argument_is_optional { - write!(f, "[")?; - } - - if multiple_visible_nodes { - write!(f, "(")?; - } - - for (idx, &node) in visible_nodes.iter().enumerate() { - // Print usage of this node - write!(f, "{node}")?; - - // Recursively print usage of it's children - write_tree(f, nodes, &node.children)?; - - if multiple_visible_nodes && idx != visible_nodes.len() - 1 { - write!(f, " | ")?; - } - } - - if multiple_visible_nodes { - write!(f, ")")?; - } - - if argument_is_optional { - write!(f, "]")?; - } - - Ok(()) -} - -impl Display for CommandTree { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "/{}", self.names[0])?; - // TODO: Help of commands like /bossbar becomes really long - // A possible solution would be to add a check if the first level consists of literals - // only and if so to run the printing as if it were separate commands - - // TODO: Consider adding a max depth to print command usage only up to a certain depth. - // Vanilla seems to do this too (as can be seen with /help effect) - - // Clean up graph by 'flattening' require nodes to their children - let flattened = flatten_require_nodes(&self.nodes, &self.children); - // Recursively iterate over tree to print help usage of a command - write_tree(f, &self.nodes, &flattened) - } -} diff --git a/crates/pumpkin/src/command/tree/mod.rs b/crates/pumpkin/src/command/tree/mod.rs deleted file mode 100644 index e6bb96308..000000000 --- a/crates/pumpkin/src/command/tree/mod.rs +++ /dev/null @@ -1,150 +0,0 @@ -use super::{CommandExecutor, args::ArgumentConsumer}; -use crate::command::CommandSender; -use crate::command::suggestion::suggestions::Suggestions; -use crate::server::Server; -use std::{borrow::Cow, collections::VecDeque, fmt::Debug, sync::Arc}; - -pub mod builder; -pub mod format; - -#[derive(Clone, Copy)] -pub struct RawArg<'a> { - pub value: &'a str, - pub start: usize, - pub end: usize, - pub input: &'a str, -} - -impl Debug for RawArg<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("RawArg") - .field("value", &self.value) - .field("start", &self.start) - .field("end", &self.end) - .finish() - } -} - -/// see [`crate::command::tree::builder::argument`] -pub type RawArgs<'a> = Vec>; - -pub type CommandSuggestionResult = Suggestions; - -pub trait CommandSuggestionProvider: Send + Sync { - fn suggest( - &self, - src: &CommandSender, - server: &Server, - input: &str, - start: usize, - end: usize, - ) -> CommandSuggestionResult; -} - -#[derive(Debug, Clone)] -pub struct Node { - pub children: Vec, - pub node_type: NodeType, -} - -#[derive(Clone)] -pub enum NodeType { - ExecuteLeaf { - executor: Arc, - }, - Literal { - string: String, - }, - Argument { - name: String, - consumer: Arc, - suggestion_provider: Option>, - }, - Require { - predicate: Arc bool + Send + Sync>, - }, -} - -impl Debug for NodeType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::ExecuteLeaf { .. } => f - .debug_struct("ExecuteLeaf") - .field("executor", &"..") - .finish(), - Self::Literal { string } => f.debug_struct("Literal").field("string", string).finish(), - Self::Argument { name, .. } => f - .debug_struct("Argument") - .field("name", name) - .field("consumer", &"..") - .finish(), - Self::Require { .. } => f.debug_struct("Require").field("predicate", &"..").finish(), - } - } -} - -#[derive(Clone, Debug)] -pub enum Command { - Tree(CommandTree), - Alias(String), -} - -#[derive(Debug, Clone)] -pub struct CommandTree { - pub nodes: Vec, - pub children: Vec, - pub names: Vec, - pub description: Cow<'static, str>, - pub source: Option, -} - -impl CommandTree { - /// iterate over all possible paths that end in a [`NodeType::ExecuteLeaf`] - pub(crate) fn iter_paths(&self) -> impl Iterator> + use<'_> { - let mut todo = VecDeque::<(usize, usize)>::new(); - - // add root's children - todo.extend(self.children.iter().map(|&i| (0, i))); - - TraverseAllPathsIter { - tree: self, - path: Vec::::new(), - todo, - } - } -} - -struct TraverseAllPathsIter<'a> { - tree: &'a CommandTree, - path: Vec, - /// (depth, i) - todo: VecDeque<(usize, usize)>, -} - -impl Iterator for TraverseAllPathsIter<'_> { - type Item = Vec; - - fn next(&mut self) -> Option { - loop { - let (depth, i) = self.todo.pop_front()?; - let node = &self.tree.nodes[i]; - - // add new children to front - self.todo.reserve(node.children.len()); - node.children - .iter() - .rev() - .for_each(|&c| self.todo.push_front((depth + 1, c))); - - // update path - while self.path.len() > depth { - self.path.pop(); - } - self.path.push(i); - - if let NodeType::ExecuteLeaf { .. } = node.node_type { - return Some(self.path.clone()); - } - } - } -} diff --git a/crates/pumpkin/src/entity/combat.rs b/crates/pumpkin/src/entity/combat.rs index e717c6dc9..ffeb8cf18 100644 --- a/crates/pumpkin/src/entity/combat.rs +++ b/crates/pumpkin/src/entity/combat.rs @@ -1,6 +1,7 @@ use std::sync::atomic::Ordering; use crate::entity::EntityBase; +use pumpkin_data::tag::Taggable; use pumpkin_data::{ attributes::Attributes, particle::Particle, @@ -129,9 +130,500 @@ pub fn player_attack_sound(pos: &Vector3, world: &World, attack_type: Attac } } +/// Combat calculation rules mirroring vanilla `net.minecraft.world.damagesource.CombatRules`. +pub struct CombatRules; + +impl CombatRules { + pub const MAX_ARMOR: f32 = 20.0; + pub const ARMOR_PROTECTION_DIVIDER: f32 = 25.0; + pub const BASE_ARMOR_TOUGHNESS: f32 = 2.0; + pub const MIN_ARMOR_RATIO: f32 = 0.2; + + /// Calculates damage after armor reduction, mirroring vanilla `CombatRules.getDamageAfterAbsorb`. + #[must_use] + pub fn get_damage_after_absorb( + damage: f32, + total_armor: f32, + armor_toughness: f32, + breach_level: u32, + ) -> f32 { + let toughness = Self::BASE_ARMOR_TOUGHNESS + armor_toughness / 4.0; + let real_armor = (total_armor - damage / toughness) + .clamp(total_armor * Self::MIN_ARMOR_RATIO, Self::MAX_ARMOR); + let mut armor_fraction = real_armor / Self::ARMOR_PROTECTION_DIVIDER; + + if breach_level > 0 { + let reduction = (breach_level as f32 * 0.15).min(1.0); + armor_fraction = (armor_fraction * (1.0 - reduction)).clamp(0.0, 1.0); + } + + let damage_multiplier = 1.0 - armor_fraction; + damage * damage_multiplier + } + + /// Calculates damage after magic/enchantment armor reduction, mirroring vanilla `CombatRules.getDamageAfterMagicAbsorb`. + #[must_use] + pub fn get_damage_after_magic_absorb(damage: f32, total_magic_armor: f32) -> f32 { + let real_armor = total_magic_armor.clamp(0.0, Self::MAX_ARMOR); + damage * (1.0 - real_armor / Self::ARMOR_PROTECTION_DIVIDER) + } +} + +pub const RESET_DAMAGE_STATUS_TIME: i64 = 100; +pub const RESET_COMBAT_STATUS_TIME: i64 = 300; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FallLocation { + Generic, + Ladder, + Vines, + WeepingVines, + TwistingVines, + Scaffolding, + OtherClimbable, + Water, +} + +impl FallLocation { + #[must_use] + pub const fn language_key(self) -> &'static str { + match self { + Self::Generic => "death.fell.accident.generic", + Self::Ladder => "death.fell.accident.ladder", + Self::Vines => "death.fell.accident.vines", + Self::WeepingVines => "death.fell.accident.weeping_vines", + Self::TwistingVines => "death.fell.accident.twisting_vines", + Self::Scaffolding => "death.fell.accident.scaffolding", + Self::OtherClimbable => "death.fell.accident.other_climbable", + Self::Water => "death.fell.accident.water", + } + } + + pub fn get_current_fall_location( + living: &crate::entity::living::LivingEntity, + world: &World, + ) -> Self { + let pos = living + .climbing_pos + .load() + .unwrap_or_else(|| living.entity.block_pos.load()); + let block = world.get_block(&pos); + if block == &pumpkin_data::Block::LADDER + || block.has_tag(&pumpkin_data::tag::Block::MINECRAFT_TRAPDOORS) + { + Self::Ladder + } else if block == &pumpkin_data::Block::VINE { + Self::Vines + } else if block == &pumpkin_data::Block::WEEPING_VINES + || block == &pumpkin_data::Block::WEEPING_VINES_PLANT + { + Self::WeepingVines + } else if block == &pumpkin_data::Block::TWISTING_VINES + || block == &pumpkin_data::Block::TWISTING_VINES_PLANT + { + Self::TwistingVines + } else if block == &pumpkin_data::Block::SCAFFOLDING { + Self::Scaffolding + } else if living.entity.is_in_water() { + Self::Water + } else if block.has_tag(&pumpkin_data::tag::Block::MINECRAFT_CLIMBABLE) { + Self::OtherClimbable + } else { + Self::Generic + } + } +} + +#[derive(Clone)] +pub struct CombatEntry { + pub damage_type: pumpkin_data::damage::DamageType, + pub damage: f32, + pub fall_location: Option, + pub fall_distance: f32, + pub timestamp: i64, + pub source_id: Option, + pub attacker_id: Option, + pub attacker_name: Option, + pub attacker_item_name: Option, + pub attacker_is_living: bool, + pub attacker_is_player: bool, +} + +#[derive(Default)] +pub struct CombatTracker { + entries: Vec, + last_damage_time: i64, + combat_start_time: i64, + combat_end_time: i64, + in_combat: bool, + taking_damage: bool, +} + +impl CombatTracker { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + #[allow(clippy::too_many_arguments)] + pub fn record_damage( + &mut self, + current_tick: i64, + is_alive: bool, + fall_distance: f32, + fall_location: FallLocation, + damage_type: pumpkin_data::damage::DamageType, + damage: f32, + source: Option<&dyn EntityBase>, + cause: Option<&dyn EntityBase>, + ) { + self.recheck_status(current_tick, is_alive); + + let attacker = cause.or(source); + let attacker_id = attacker.map(|e| e.get_entity().entity_id); + let attacker_name = attacker.map(super::EntityBase::get_display_name); + let attacker_is_living = attacker.is_some_and(|e| e.get_living_entity().is_some()); + let attacker_is_player = attacker.is_some_and(|e| { + e.get_entity().entity_type == &pumpkin_data::entity::EntityType::PLAYER + }); + + // Check for custom named item in attacker's main hand + let attacker_item_name = + attacker.and_then(|e| { + if let Some(player) = e.get_player() { + let hand_stack = player + .inventory() + .get_stack_in_hand(pumpkin_util::Hand::Right); + if !hand_stack.is_empty() + && let Some(custom_name) = hand_stack + .get_data_component::() + { + return Some(custom_name.name.clone()); + } + } else if let Some(living) = e.get_living_entity() { + let equip_guard = living + .entity_equipment + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + if let Some(stack) = equip_guard + .equipment + .get(&pumpkin_data::data_component_impl::EquipmentSlot::MAIN_HAND) + && !stack.is_empty() + && let Some(custom_name) = stack + .get_data_component::() + { + return Some(custom_name.name.clone()); + } + } + None + }); + + let entry = CombatEntry { + damage_type, + damage, + fall_location: Some(fall_location), + fall_distance, + timestamp: current_tick, + source_id: source.map(|e| e.get_entity().entity_id), + attacker_id, + attacker_name, + attacker_item_name, + attacker_is_living, + attacker_is_player, + }; + + self.entries.push(entry); + self.last_damage_time = current_tick; + self.taking_damage = true; + + if !self.in_combat && is_alive && attacker_is_living { + self.in_combat = true; + self.combat_start_time = current_tick; + self.combat_end_time = self.combat_start_time; + } + } + + pub fn recheck_status(&mut self, current_tick: i64, is_alive: bool) { + let reset = if self.in_combat { + RESET_COMBAT_STATUS_TIME + } else { + RESET_DAMAGE_STATUS_TIME + }; + if self.taking_damage && (!is_alive || current_tick - self.last_damage_time > reset) { + self.taking_damage = false; + self.in_combat = false; + self.combat_end_time = current_tick; + self.entries.clear(); + } + } + + #[must_use] + pub const fn get_combat_duration(&self, current_tick: i64) -> i64 { + if self.in_combat { + current_tick - self.combat_start_time + } else { + self.combat_end_time - self.combat_start_time + } + } + + #[must_use] + pub const fn is_in_combat(&self) -> bool { + self.in_combat + } + + #[must_use] + pub fn has_player_attacker(&self) -> bool { + self.entries.iter().any(|e| e.attacker_is_player) + } + + #[must_use] + pub fn get_killer_entry(&self) -> Option<&CombatEntry> { + let mut best_living: Option<&CombatEntry> = None; + let mut best_player: Option<&CombatEntry> = None; + let mut max_living_dmg = 0.0f32; + let mut max_player_dmg = 0.0f32; + + for entry in &self.entries { + if entry.attacker_is_player && (best_player.is_none() || entry.damage > max_player_dmg) + { + max_player_dmg = entry.damage; + best_player = Some(entry); + } + if entry.attacker_is_living && (best_living.is_none() || entry.damage > max_living_dmg) + { + max_living_dmg = entry.damage; + best_living = Some(entry); + } + } + + if best_player.is_some() && max_player_dmg >= max_living_dmg / 3.0 { + best_player + } else { + best_living + } + } + + #[must_use] + pub fn get_death_message( + &self, + victim_name: pumpkin_util::text::TextComponent, + ) -> pumpkin_util::text::TextComponent { + if self.entries.is_empty() { + return pumpkin_util::text::TextComponent::translate_cross( + "death.attack.generic", + "death.attack.generic", + [victim_name], + ); + } + + let killing_blow = &self.entries[self.entries.len() - 1]; + let killing_damage_type = killing_blow.damage_type; + let knock_off_entry = self.get_most_significant_fall(); + + match killing_damage_type.death_message_type { + pumpkin_data::damage::DeathMessageType::FallVariants => { + if let Some(knock_off) = knock_off_entry { + Self::get_fall_message(victim_name, knock_off, killing_blow) + } else { + let loc = killing_blow.fall_location.unwrap_or(FallLocation::Generic); + pumpkin_util::text::TextComponent::translate_cross( + loc.language_key(), + loc.language_key(), + [victim_name], + ) + } + } + pumpkin_data::damage::DeathMessageType::IntentionalGameDesign => { + let death_msg = format!("death.attack.{}", killing_damage_type.message_id); + let link = pumpkin_util::text::TextComponent::text("[") + .add_child(pumpkin_util::text::TextComponent::translate_cross( + format!("{death_msg}.link"), + format!("{death_msg}.link"), + [], + )) + .add_child(pumpkin_util::text::TextComponent::text("]")) + .click_event(pumpkin_util::text::click::ClickEvent::OpenUrl { + url: "https://bugs.mojang.com/browse/MCPE-28723".into(), + }) + .hover_event(pumpkin_util::text::hover::HoverEvent::show_text( + pumpkin_util::text::TextComponent::text("MCPE-28723"), + )); + + pumpkin_util::text::TextComponent::translate_cross( + format!("{death_msg}.message"), + format!("{death_msg}.message"), + [victim_name, link], + ) + } + pumpkin_data::damage::DeathMessageType::Default => { + self.get_default_death_message(victim_name, killing_blow) + } + } + } + + fn get_most_significant_fall(&self) -> Option<&CombatEntry> { + let mut result = None; + let mut alternative = None; + let mut alt_damage = 0.0f32; + let mut best_fall = 0.0f32; + + for i in 0..self.entries.len() { + let entry = &self.entries[i]; + let previous = (i > 0).then(|| &self.entries[i - 1]); + let damage_type = entry.damage_type; + let is_fake_fall = damage_type + .has_tag(&pumpkin_data::tag::DamageType::MINECRAFT_ALWAYS_MOST_SIGNIFICANT_FALL); + let fall_distance = if is_fake_fall { + f32::MAX + } else { + entry.fall_distance + }; + + if (damage_type.has_tag(&pumpkin_data::tag::DamageType::MINECRAFT_IS_FALL) + || is_fake_fall) + && fall_distance > 0.0 + && (result.is_none() || fall_distance > best_fall) + { + if i > 0 { + result = previous; + } else { + result = Some(entry); + } + best_fall = fall_distance; + } + + if entry.fall_location.is_some() && (alternative.is_none() || entry.damage > alt_damage) + { + alternative = Some(entry); + alt_damage = entry.damage; + } + } + + if best_fall > 5.0 && result.is_some() { + result + } else if alt_damage > 5.0 && alternative.is_some() { + alternative + } else { + None + } + } + + fn get_fall_message( + victim_name: pumpkin_util::text::TextComponent, + knock_off_entry: &CombatEntry, + killing_blow: &CombatEntry, + ) -> pumpkin_util::text::TextComponent { + let knock_off_type = knock_off_entry.damage_type; + if !knock_off_type.has_tag(&pumpkin_data::tag::DamageType::MINECRAFT_IS_FALL) + && !knock_off_type + .has_tag(&pumpkin_data::tag::DamageType::MINECRAFT_ALWAYS_MOST_SIGNIFICANT_FALL) + { + let killer_name = killing_blow.attacker_name.as_ref(); + let attacker_name = knock_off_entry.attacker_name.as_ref(); + let attacker_item = knock_off_entry.attacker_item_name.clone(); + + if let Some(attacker_name) = attacker_name + && (killer_name.is_none() || killer_name != Some(attacker_name)) + { + Self::get_message_for_assisted_fall( + victim_name, + attacker_name.clone(), + attacker_item, + "death.fell.assist.item", + "death.fell.assist", + ) + } else if let Some(killer_name) = killer_name { + Self::get_message_for_assisted_fall( + victim_name, + killer_name.clone(), + killing_blow.attacker_item_name.clone(), + "death.fell.finish.item", + "death.fell.finish", + ) + } else { + pumpkin_util::text::TextComponent::translate_cross( + "death.fell.killer", + "death.fell.killer", + [victim_name], + ) + } + } else { + let loc = knock_off_entry + .fall_location + .unwrap_or(FallLocation::Generic); + pumpkin_util::text::TextComponent::translate_cross( + loc.language_key(), + loc.language_key(), + [victim_name], + ) + } + } + + fn get_message_for_assisted_fall( + victim_name: pumpkin_util::text::TextComponent, + attacker_name: pumpkin_util::text::TextComponent, + attacker_item: Option, + message_with_item: &'static str, + message_without_item: &'static str, + ) -> pumpkin_util::text::TextComponent { + if let Some(item_name) = attacker_item { + pumpkin_util::text::TextComponent::translate_cross( + message_with_item, + message_with_item, + [victim_name, attacker_name, item_name], + ) + } else { + pumpkin_util::text::TextComponent::translate_cross( + message_without_item, + message_without_item, + [victim_name, attacker_name], + ) + } + } + + fn get_default_death_message( + &self, + victim_name: pumpkin_util::text::TextComponent, + killing_blow: &CombatEntry, + ) -> pumpkin_util::text::TextComponent { + let damage_type = killing_blow.damage_type; + let msg_id = damage_type.message_id; + + if let Some(attacker_name) = &killing_blow.attacker_name { + if let Some(item_name) = &killing_blow.attacker_item_name { + pumpkin_util::text::TextComponent::translate_cross( + format!("death.attack.{msg_id}.item"), + format!("death.attack.{msg_id}.item"), + [victim_name, attacker_name.clone(), item_name.clone()], + ) + } else { + pumpkin_util::text::TextComponent::translate_cross( + format!("death.attack.{msg_id}.player"), + format!("death.attack.{msg_id}.player"), + [victim_name, attacker_name.clone()], + ) + } + } else if let Some(killer) = self.get_killer_entry() + && let Some(killer_name) = &killer.attacker_name + { + pumpkin_util::text::TextComponent::translate_cross( + format!("death.attack.{msg_id}.player"), + format!("death.attack.{msg_id}.player"), + [victim_name, killer_name.clone()], + ) + } else { + pumpkin_util::text::TextComponent::translate_cross( + format!("death.attack.{msg_id}"), + format!("death.attack.{msg_id}"), + [victim_name], + ) + } + } +} + #[cfg(test)] mod tests { - use super::knockback_after_resistance; + use super::*; #[test] fn zero_resistance_keeps_full_strength() { diff --git a/crates/pumpkin/src/entity/living.rs b/crates/pumpkin/src/entity/living.rs index 3ff2bb58c..270b5a97e 100644 --- a/crates/pumpkin/src/entity/living.rs +++ b/crates/pumpkin/src/entity/living.rs @@ -29,7 +29,7 @@ use crate::entity::NBTStorage; use crate::entity::attributes::AttributeInstance; use crate::entity::attributes::Modifier; use crate::entity::attributes::ModifierOperation; -use crate::entity::combat::knockback_after_resistance; +use crate::entity::combat::{CombatRules, CombatTracker, FallLocation, knockback_after_resistance}; use crate::entity::mob::equipment::DEFAULT_EQUIPMENT_DROP_CHANCE; use crate::entity::mob::slime::SlimeEntity; use crate::entity::player::statistics::{CustomStatistic, StatisticCategory}; @@ -37,7 +37,6 @@ use crate::server::Server; use crate::world::loot::{LootContextParameters, LootTableExt}; use crossbeam::atomic::AtomicCell; use pumpkin_data::attributes::Attributes; -use pumpkin_data::damage::DeathMessageType; use pumpkin_data::data_component_impl::Operation; use pumpkin_data::data_component_impl::food::{ConsumableImpl, ConsumeEffect}; use pumpkin_data::data_component_impl::{ @@ -49,7 +48,7 @@ use pumpkin_data::entity::{EntityPose, EntityStatus, EntityType}; use pumpkin_data::fluid::Fluid; use pumpkin_data::item_stack::{DamageResult, ItemStack}; use pumpkin_data::sound::SoundCategory; -use pumpkin_data::{Block, Enchantment, translation}; +use pumpkin_data::{Block, Enchantment}; use pumpkin_data::{damage::DamageType, sound::Sound}; use pumpkin_inventory::entity_equipment::EntityEquipment; use pumpkin_nbt::compound::NbtCompound; @@ -116,6 +115,9 @@ pub struct LivingEntity { /// The tick at which this entity last attacked something (entity age). pub last_attack_time: AtomicI32, + /// Tracks combat entries, assisted falls, kill credit, and death messages. + pub combat_tracker: std::sync::Mutex, + water_movement_speed_multiplier: f32, livings_flags: AtomicU8, @@ -221,6 +223,7 @@ impl LivingEntity { last_attacked_time: AtomicI32::new(0), last_attacking_id: AtomicI32::new(0), last_attack_time: AtomicI32::new(0), + combat_tracker: std::sync::Mutex::new(CombatTracker::new()), movement_input: AtomicCell::new(Vector3::default()), water_movement_speed_multiplier, } @@ -1483,39 +1486,28 @@ impl LivingEntity { source: Option<&dyn EntityBase>, cause: Option<&dyn EntityBase>, ) -> TextComponent { - match damage_type.death_message_type { - DeathMessageType::Default => { - if let Some(cause) = cause - && source.is_some() - { - TextComponent::translate_cross( - format!("death.attack.{}.player", damage_type.message_id), - format!("death.attack.{}.player", damage_type.message_id), - [dyn_self.get_display_name(), cause.get_display_name()], - ) - } else { - TextComponent::translate_cross( - format!("death.attack.{}", damage_type.message_id), - format!("death.attack.{}", damage_type.message_id), - [dyn_self.get_display_name()], - ) - } - } - DeathMessageType::FallVariants => { - //TODO - TextComponent::translate_cross( - translation::java::DEATH_FELL_ACCIDENT_GENERIC, - translation::bedrock::DEATH_FELL_ACCIDENT_GENERIC, - [dyn_self.get_display_name()], - ) - } - DeathMessageType::IntentionalGameDesign => TextComponent::text("[") - .add_child(TextComponent::translate_cross( - format!("death.attack.{}.message", damage_type.message_id), - format!("death.attack.{}.message", damage_type.message_id), - [dyn_self.get_display_name()], - )) - .add_child(TextComponent::text("]")), + if let Some(living) = dyn_self.get_living_entity() { + let tracker = living + .combat_tracker + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + return tracker.get_death_message(dyn_self.get_display_name()); + } + + if let Some(cause) = cause + && source.is_some() + { + TextComponent::translate_cross( + format!("death.attack.{}.player", damage_type.message_id), + format!("death.attack.{}.player", damage_type.message_id), + [dyn_self.get_display_name(), cause.get_display_name()], + ) + } else { + TextComponent::translate_cross( + format!("death.attack.{}", damage_type.message_id), + format!("death.attack.{}", damage_type.message_id), + [dyn_self.get_display_name()], + ) } } @@ -1567,8 +1559,17 @@ impl LivingEntity { let is_raining = world.is_raining(); let is_thundering = world.is_thundering(); + let has_player_kill = + cause.is_some_and(|c| c.get_entity().entity_type == &EntityType::PLAYER) || { + let tracker = self + .combat_tracker + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + tracker.has_player_attacker() + }; + let params = LootContextParameters { - killed_by_player: cause.map(|c| c.get_entity().entity_type == &EntityType::PLAYER), + killed_by_player: Some(has_player_kill), this_entity: Some(self.entity.entity_type), killer_entity: cause.map(|c| c.get_entity().entity_type), direct_killer_entity: source.map(|s| s.get_entity().entity_type), @@ -2182,6 +2183,165 @@ impl LivingEntity { // todo more... } + /// Calculates damage after armor reduction, mirroring vanilla `LivingEntity.getDamageAfterArmorAbsorb`. + pub fn get_damage_after_armor_absorb( + &self, + damage: f32, + damage_type: &DamageType, + attacker: Option<&dyn EntityBase>, + ) -> f32 { + if damage_type.has_tag(&tag::DamageType::MINECRAFT_BYPASSES_ARMOR) { + return damage; + } + + let mut armor = 0.0f32; + let mut toughness = 0.0f32; + { + let equipment_lock = self + .entity_equipment + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + for slot in [ + EquipmentSlot::HEAD, + EquipmentSlot::CHEST, + EquipmentSlot::LEGS, + EquipmentSlot::FEET, + ] { + if let Some(stack) = equipment_lock.equipment.get(&slot) + && !stack.is_empty() + && let Some(modifiers) = stack.get_data_component::() + { + for modifier in modifiers.attribute_modifiers.iter() { + if modifier.r#type == &Attributes::ARMOR { + armor += modifier.amount as f32; + } else if modifier.r#type == &Attributes::ARMOR_TOUGHNESS { + toughness += modifier.amount as f32; + } + } + } + } + } + + let breach_level = attacker + .and_then(|att| { + let player = att.get_player()?; + let hand_stack = player + .inventory() + .get_stack_in_hand(pumpkin_util::Hand::Right); + let level = hand_stack.get_enchantment_level(&Enchantment::BREACH); + (level > 0).then_some(level as u32) + }) + .unwrap_or(0); + + CombatRules::get_damage_after_absorb(damage, armor, toughness, breach_level) + } + + /// Calculates damage after magic/resistance/enchantment reduction, mirroring vanilla `LivingEntity.getDamageAfterMagicAbsorb`. + pub fn get_damage_after_magic_absorb( + &self, + mut damage: f32, + damage_type: &DamageType, + caller: &dyn EntityBase, + cause: Option<&dyn EntityBase>, + ) -> f32 { + if damage_type.has_tag(&tag::DamageType::MINECRAFT_BYPASSES_EFFECTS) { + return damage; + } + + // 1. Resistance Effect (evaluated before enchantments) + if !damage_type.has_tag(&tag::DamageType::MINECRAFT_BYPASSES_RESISTANCE) + && let Some(effect) = self.get_effect(&StatusEffect::RESISTANCE) + { + let absorb_value = (effect.amplifier + 1) * 5; + let absorb = 25 - absorb_value; + let v = damage * absorb as f32; + let old_damage = damage; + damage = (v / 25.0).max(0.0); + let damage_resisted = old_damage - damage; + if damage_resisted > 0.0 { + if let Some(victim_player) = caller.get_player() { + victim_player.increment_stat( + StatisticCategory::Custom, + CustomStatistic::DamageResisted as i32, + (damage_resisted * 10.0).round() as i32, + ); + } else if let Some(attacker_player) = cause.and_then(|c| c.get_player()) { + attacker_player.increment_stat( + StatisticCategory::Custom, + CustomStatistic::DamageDealtResisted as i32, + (damage_resisted * 10.0).round() as i32, + ); + } + } + } + + if damage <= 0.0 { + return 0.0; + } + + // 2. Enchantment Protection + if damage_type.has_tag(&tag::DamageType::MINECRAFT_BYPASSES_ENCHANTMENTS) { + return damage; + } + + let is_fire_damage = damage_type.has_tag(&tag::DamageType::MINECRAFT_IS_FIRE); + let mut epf = 0.0f32; + { + let equipment_lock = self + .entity_equipment + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + for slot in [ + EquipmentSlot::HEAD, + EquipmentSlot::CHEST, + EquipmentSlot::LEGS, + EquipmentSlot::FEET, + ] { + if let Some(stack) = equipment_lock.equipment.get(&slot) + && !stack.is_empty() + && let Some(enchantments) = stack.get_data_component::() + { + for (enchantment, level) in enchantments.enchantment.iter() { + let enc = *enchantment; + let lvl = *level as f32; + if enc == &Enchantment::PROTECTION { + if !damage_type + .has_tag(&tag::DamageType::MINECRAFT_BYPASSES_INVULNERABILITY) + && damage_type != &DamageType::STARVE + && damage_type != &DamageType::GENERIC_KILL + && damage_type != &DamageType::OUT_OF_WORLD + { + epf += lvl; + } + } else if enc == &Enchantment::FIRE_PROTECTION { + if is_fire_damage { + epf += lvl * 2.0; + } + } else if enc == &Enchantment::BLAST_PROTECTION { + if damage_type.has_tag(&tag::DamageType::MINECRAFT_IS_EXPLOSION) { + epf += lvl * 2.0; + } + } else if enc == &Enchantment::PROJECTILE_PROTECTION { + if damage_type.has_tag(&tag::DamageType::MINECRAFT_IS_PROJECTILE) { + epf += lvl * 2.0; + } + } else if enc == &Enchantment::FEATHER_FALLING + && damage_type.has_tag(&tag::DamageType::MINECRAFT_IS_FALL) + { + epf += lvl * 3.0; + } + } + } + } + } + + if epf > 0.0 { + damage = CombatRules::get_damage_after_magic_absorb(damage, epf); + } + + damage + } + #[allow(clippy::too_many_lines)] pub fn damage_with_context( &self, @@ -2224,10 +2384,7 @@ impl LivingEntity { amount = damage_event.damage; let world = self.entity.world.load(); - let is_fire_damage = damage_type == DamageType::IN_FIRE - || damage_type == DamageType::ON_FIRE - || damage_type == DamageType::LAVA - || damage_type == DamageType::HOT_FLOOR; + let is_fire_damage = damage_type.has_tag(&tag::DamageType::MINECRAFT_IS_FIRE); // Fire damage can be prevented by either game rules or fire resistance if is_fire_damage { @@ -2239,7 +2396,9 @@ impl LivingEntity { } // Check for fire resistance effect - if self.has_effect(&StatusEffect::FIRE_RESISTANCE) { + if self.has_effect(&StatusEffect::FIRE_RESISTANCE) + && !damage_type.has_tag(&tag::DamageType::MINECRAFT_BYPASSES_EFFECTS) + { return false; } } @@ -2254,139 +2413,7 @@ impl LivingEntity { amount *= 5.0; } - // These damage types bypass the hurt cooldown and death protection - let bypasses_cooldown_protection = - damage_type == DamageType::GENERIC_KILL || damage_type == DamageType::OUT_OF_WORLD; - - let mut damage_after_armor = amount; - if !bypasses_armor_durability(&damage_type) { - let mut armor = 0.0f32; - let mut toughness = 0.0f32; - { - let equipment_lock = self - .entity_equipment - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - for slot in [ - EquipmentSlot::HEAD, - EquipmentSlot::CHEST, - EquipmentSlot::LEGS, - EquipmentSlot::FEET, - ] { - if let Some(stack) = equipment_lock.equipment.get(&slot) - && !stack.is_empty() - && let Some(modifiers) = - stack.get_data_component::() - { - for modifier in modifiers.attribute_modifiers.iter() { - if modifier.r#type == &Attributes::ARMOR { - armor += modifier.amount as f32; - } else if modifier.r#type == &Attributes::ARMOR_TOUGHNESS { - toughness += modifier.amount as f32; - } - } - } - } - } - let value = 2.0f32 + toughness / 4.0; - let clamped_armor = (armor - damage_after_armor / value) - .max(armor / 5.0) - .min(20.0); - damage_after_armor *= 1.0 - clamped_armor / 25.0; - } - - let mut damage_after_enchantments = damage_after_armor; - if damage_type != DamageType::OUT_OF_WORLD { - let mut epf = 0i32; - { - let equipment_lock = self - .entity_equipment - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - for slot in [ - EquipmentSlot::HEAD, - EquipmentSlot::CHEST, - EquipmentSlot::LEGS, - EquipmentSlot::FEET, - ] { - if let Some(stack) = equipment_lock.equipment.get(&slot) - && !stack.is_empty() - && let Some(enchantments) = stack.get_data_component::() - { - for (enchantment, level) in enchantments.enchantment.iter() { - let mut factor = 0; - let enc = *enchantment; - if enc == &Enchantment::PROTECTION { - if damage_type != DamageType::DROWN - && damage_type != DamageType::STARVE - && damage_type != DamageType::GENERIC_KILL - { - factor = *level; - } - } else if enc == &Enchantment::FIRE_PROTECTION { - if is_fire_damage { - factor = *level * 2; - } - } else if enc == &Enchantment::BLAST_PROTECTION { - if damage_type == DamageType::EXPLOSION - || damage_type == DamageType::PLAYER_EXPLOSION - { - factor = *level * 2; - } - } else if enc == &Enchantment::PROJECTILE_PROTECTION { - if damage_type == DamageType::ARROW - || damage_type == DamageType::MOB_PROJECTILE - || damage_type == DamageType::THROWN - { - factor = (*level) * 2; - } - } else if enc == &Enchantment::FEATHER_FALLING - && damage_type == DamageType::FALL - { - factor = (*level) * 4; - } - epf += factor; - } - } - } - } - epf = epf.min(20); - if epf > 0 { - damage_after_enchantments *= 1.0 - (epf as f32 * 0.04); - } - } - - // Apply Resistance effect reduction (20% per level) - let resistance_reduction = - if !bypasses_cooldown_protection && damage_type != DamageType::STARVE { - self.get_effect(&StatusEffect::RESISTANCE) - .map_or(0.0, |e| 0.2 * (e.amplifier + 1) as f32) - } else { - 0.0 - }; - - // Total damage after reductions - let effective_amount = damage_after_enchantments * (1.0 - resistance_reduction); - - if resistance_reduction > 0.0 { - let resisted = damage_after_enchantments * resistance_reduction; - if let Some(player) = caller.get_player() { - player.increment_stat( - StatisticCategory::Custom, - CustomStatistic::DamageResisted as i32, - (resisted * 10.0) as i32, - ); - } - if let Some(attacker_player) = cause.and_then(|c| c.get_player()) { - attacker_player.increment_stat( - StatisticCategory::Custom, - CustomStatistic::DamageDealtResisted as i32, - (resisted * 10.0) as i32, - ); - } - } - - // Check for shield blocking + // Check for shield blocking before armor/magic/cooldown if self.is_blocking() && !damage_type.has_tag(&tag::DamageType::MINECRAFT_BYPASSES_SHIELD) && let Some(pos) = position @@ -2403,7 +2430,7 @@ impl LivingEntity { player.increment_stat( StatisticCategory::Custom, CustomStatistic::DamageBlockedByShield as i32, - (effective_amount * 10.0) as i32, + (amount * 10.0).round() as i32, ); } @@ -2451,6 +2478,18 @@ impl LivingEntity { } } + // Vanilla parity: 1. Armor absorb + let damage_after_armor = + self.get_damage_after_armor_absorb(amount, &damage_type, cause.or(source)); + + // Vanilla parity: 2. Magic absorb (Resistance effect first, then Enchantments) + let effective_amount = + self.get_damage_after_magic_absorb(damage_after_armor, &damage_type, caller, cause); + + // These damage types bypass the hurt cooldown and death protection + let bypasses_cooldown_protection = + damage_type == DamageType::GENERIC_KILL || damage_type == DamageType::OUT_OF_WORLD; + // Apply hurt cooldown logic let last_damage = self.last_damage_taken.load(); let (damage_amount, play_sound) = @@ -2521,16 +2560,21 @@ impl LivingEntity { } } - // Consume absorption first, then apply remaining damage to health - let mut remaining = damage_amount; + // Vanilla parity: actuallyHurt + let original_damage = damage_amount; let current_abs = self.absorption.load(); - if current_abs > 0.0 { - let absorbed = current_abs.min(remaining); + let dmg_to_health = (original_damage - current_abs).max(0.0); + let absorbed_damage = original_damage - dmg_to_health; + + if absorbed_damage > 0.0 { + let new_abs = (current_abs - absorbed_damage).max(0.0); + self.set_absorption(new_abs); + if let Some(player) = caller.get_player() { player.increment_stat( StatisticCategory::Custom, CustomStatistic::DamageAbsorbed as i32, - (absorbed * 10.0) as i32, + (absorbed_damage * 10.0).round() as i32, ); } @@ -2538,19 +2582,10 @@ impl LivingEntity { attacker_player.increment_stat( StatisticCategory::Custom, CustomStatistic::DamageDealtAbsorbed as i32, - (absorbed * 10.0) as i32, + (absorbed_damage * 10.0).round() as i32, ); } - if current_abs >= remaining { - let new_abs = current_abs - remaining; - self.set_absorption(new_abs); - remaining = 0.0; - } else { - remaining -= current_abs; - self.set_absorption(0.0); - } - if let Some(attacker) = cause.or(source) { self.last_attacker_id .store(attacker.get_entity().entity_id, Relaxed); @@ -2560,24 +2595,27 @@ impl LivingEntity { } let max_h = self.get_max_health(); - let new_health = self.health.load() - remaining; - let clamped_health = new_health.max(0.0).min(max_h); - if remaining > 0.0 { - self.set_health(clamped_health); + let new_health = (self.health.load() - dmg_to_health).clamp(0.0, max_h); + if dmg_to_health > 0.0 { if let Some(player) = caller.get_player() { + if damage_type.exhaustion > 0.0 { + player.add_exhaustion(damage_type.exhaustion); + } player.increment_stat( StatisticCategory::Custom, CustomStatistic::DamageTaken as i32, - (remaining * 10.0) as i32, + (dmg_to_health * 10.0).round() as i32, ); } + self.set_health(new_health); + if let Some(attacker_player) = cause.and_then(|c| c.get_player()) { attacker_player.increment_stat( StatisticCategory::Custom, CustomStatistic::DamageDealt as i32, - (remaining * 10.0) as i32, + (dmg_to_health * 10.0).round() as i32, ); } @@ -2589,7 +2627,30 @@ impl LivingEntity { } } - if clamped_health <= 0.0 { + if dmg_to_health > 0.0 || absorbed_damage > 0.0 { + let current_tick = world.level_info.load().day_time; + let fall_location = FallLocation::get_current_fall_location(self, &world); + let fall_distance = self.fall_distance.load(); + + { + let mut tracker = self + .combat_tracker + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + tracker.record_damage( + current_tick, + self.health.load() > 0.0 && !self.dead.load(Relaxed), + fall_distance, + fall_location, + damage_type, + effective_amount, + source, + cause, + ); + } + } + + if new_health <= 0.0 { let mut death_event = crate::plugin::api::events::entity::entity_death::EntityDeathEvent::new( self.entity.entity_id, @@ -2600,11 +2661,7 @@ impl LivingEntity { .plugin_manager .fire_blocking(&server, &mut death_event); } - world.send_entity_status( - &self.entity, - pumpkin_data::entity::EntityStatus::Death, - None, - ); + self.on_death(damage_type, source, cause); } true diff --git a/crates/pumpkin/src/entity/player.rs b/crates/pumpkin/src/entity/player.rs index 1d347190f..6be533988 100644 --- a/crates/pumpkin/src/entity/player.rs +++ b/crates/pumpkin/src/entity/player.rs @@ -3745,6 +3745,11 @@ impl Player { }) < d * d } + #[must_use] + pub fn may_build(&self) -> bool { + self.abilities.lock().is_ok_and(|a| a.allow_modify_world) + } + pub fn kick(&self, reason: DisconnectReason, message: &TextComponent) { if let Some(server) = self.world().server.upgrade() && let Some(player_arc) = self.world().get_player_by_uuid(self.gameprofile.id) @@ -4596,6 +4601,22 @@ impl Player { // todo this.player.stopUsingItem(); } + #[must_use] + pub fn is_text_filtering_enabled(&self) -> bool { + self.config.load().text_filtering + } + + pub fn send_chat_message( + self: &Arc, + tracked: &crate::net::chat::OutgoingChatMessage, + filtered: bool, + chat_type: pumpkin_protocol::codec::var_int::VarInt, + sender_name: &TextComponent, + target_name: Option<&TextComponent>, + ) { + tracked.send_to_player(self, filtered, chat_type, sender_name, target_name); + } + pub fn send_system_message(&self, text: &TextComponent) { self.send_system_message_raw(text, false); } @@ -6649,18 +6670,28 @@ impl Abilities { self.allow_flying = true; self.creative = true; self.invulnerable = true; + self.allow_modify_world = true; } GameMode::Spectator => { self.flying = true; self.allow_flying = true; self.creative = false; self.invulnerable = true; + self.allow_modify_world = false; } - _ => { + GameMode::Adventure => { self.flying = false; self.allow_flying = false; self.creative = false; self.invulnerable = false; + self.allow_modify_world = false; + } + GameMode::Survival => { + self.flying = false; + self.allow_flying = false; + self.creative = false; + self.invulnerable = false; + self.allow_modify_world = true; } } } diff --git a/crates/pumpkin/src/item/items/dye.rs b/crates/pumpkin/src/item/items/dye.rs index 339d92f25..bd4f6e8ad 100644 --- a/crates/pumpkin/src/item/items/dye.rs +++ b/crates/pumpkin/src/item/items/dye.rs @@ -40,6 +40,9 @@ impl DyeItem { color_name: &str, ) -> BlockActionResult { let dye_color = DyeColor::by_name(color_name).unwrap_or_default(); + if text.get_color() == dye_color { + return BlockActionResult::PassToDefaultBlockAction; + } text.set_color(dye_color); diff --git a/crates/pumpkin/src/item/items/honeycomb.rs b/crates/pumpkin/src/item/items/honeycomb.rs index 91464f293..966848949 100644 --- a/crates/pumpkin/src/item/items/honeycomb.rs +++ b/crates/pumpkin/src/item/items/honeycomb.rs @@ -1,9 +1,7 @@ use std::sync::Arc; -use std::sync::atomic::Ordering; use crate::block::UseWithItemArgs; use crate::block::entities::BlockEntity; -use crate::block::entities::sign::SignBlockEntity; use crate::block::registry::BlockActionResult; use crate::entity::player::Player; use crate::item::{ItemBehaviour, ItemMetadata}; @@ -83,9 +81,12 @@ impl HoneyCombItem { &self, args: &UseWithItemArgs<'_>, block_entity: &Arc, - sign_entity: &SignBlockEntity, + sign_entity: &crate::block::entities::sign::SignEntityRef<'_>, ) -> BlockActionResult { - sign_entity.is_waxed.store(true, Ordering::Relaxed); + if sign_entity.is_waxed() { + return BlockActionResult::PassToDefaultBlockAction; + } + sign_entity.set_waxed(true); args.world.update_block_entity(block_entity); args.world diff --git a/crates/pumpkin/src/logging.rs b/crates/pumpkin/src/logging.rs index 81a48d05b..85b51a45a 100644 --- a/crates/pumpkin/src/logging.rs +++ b/crates/pumpkin/src/logging.rs @@ -9,7 +9,6 @@ use rustyline::history::FileHistory; use rustyline::validate::Validator; use rustyline::{Editor, Helper}; use std::borrow::Cow; -use std::collections::VecDeque; use std::fs::File; use std::io::{self, BufWriter, Write}; use std::path::PathBuf; @@ -21,7 +20,6 @@ use tracing_subscriber::filter::LevelFilter; use crate::command::CommandSender; use crate::command::string_reader::StringReader; -use crate::command::tree::NodeType; use crate::server::Server; #[macro_export] @@ -447,7 +445,6 @@ impl Validator for PumpkinCommandCompleter {} impl Completer for PumpkinCommandCompleter { type Candidate = String; - #[expect(clippy::too_many_lines)] fn complete( &self, line: &str, @@ -469,9 +466,6 @@ impl Completer for PumpkinCommandCompleter { return Ok((0, Vec::new())); }; - let parts: Vec<&str> = cmd.split_whitespace().collect(); - let ends_with_space = cmd.ends_with(' '); - let dispatcher = server.command_dispatcher.load(); let source = CommandSender::Console.into_source(server); @@ -510,109 +504,6 @@ impl Completer for PumpkinCommandCompleter { } } - let dispatcher = &dispatcher.fallback_dispatcher; - let src = CommandSender::Console; - - if parts.is_empty() || (parts.len() == 1 && !ends_with_space) { - let typing = parts.first().unwrap_or(&""); - let candidates = dispatcher - .commands - .keys() - .filter(|k| k.starts_with(typing)) - .cloned() - .collect(); - return Ok((usize::from(has_slash), candidates)); - } - - let Some(tree) = dispatcher.get_tree(parts[0]).ok() else { - return Ok((0, Vec::new())); - }; - - let mut current_indices = tree.children.clone(); - let mut word_index = 1; - let walk_limit = if ends_with_space { - parts.len() - } else { - parts.len() - 1 - }; - - while word_index < walk_limit { - let token = parts[word_index]; - let mut next_indices = Vec::new(); - - let mut worklist: VecDeque = current_indices.iter().copied().collect(); - - while let Some(idx) = worklist.pop_front() { - let node = &tree.nodes[idx]; - - match &node.node_type { - NodeType::Require { predicate } => { - if predicate(&src) { - worklist.extend(node.children.iter().copied()); - } - } - NodeType::Literal { string } => { - if string.eq_ignore_ascii_case(token) { - next_indices.extend(node.children.iter().copied()); - } - } - NodeType::Argument { .. } => { - next_indices.extend(node.children.iter().copied()); - } - NodeType::ExecuteLeaf { .. } => {} - } - } - - if next_indices.is_empty() { - return Ok((0, Vec::new())); - } - - current_indices = next_indices; - word_index += 1; - } - - let typing = if ends_with_space { - "" - } else { - parts.last().unwrap_or(&"") - }; - let mut candidates = Vec::new(); - - let mut suggestion_worklist: VecDeque = current_indices.into_iter().collect(); - - while let Some(idx) = suggestion_worklist.pop_front() { - let node = &tree.nodes[idx]; - match &node.node_type { - NodeType::Require { predicate } => { - if predicate(&src) { - suggestion_worklist.extend(node.children.iter().copied()); - } - } - NodeType::Literal { string } => { - if string.starts_with(typing) { - candidates.push(string.clone()); - } - } - NodeType::Argument { name, consumer, .. } => { - if let Ok(Some(suggestions)) = consumer.suggest(&src, server, typing) { - for s in suggestions { - let s = s.suggestion; - if s.starts_with(typing) { - candidates.push(s); - } - } - } else { - let placeholder = format!("<{name}>"); - if placeholder.starts_with(typing) || typing.is_empty() { - candidates.push(placeholder); - } - } - } - NodeType::ExecuteLeaf { executor: _ } => {} - } - } - - let last_space = cmd.rfind(' ').map_or(0, |i| i + 1); - Ok((last_space + usize::from(has_slash), candidates)) + Ok((0, Vec::new())) } } diff --git a/crates/pumpkin/src/net/chat/mod.rs b/crates/pumpkin/src/net/chat/mod.rs new file mode 100644 index 000000000..267078553 --- /dev/null +++ b/crates/pumpkin/src/net/chat/mod.rs @@ -0,0 +1,432 @@ +use std::sync::Arc; +use std::time::Duration; + +use pumpkin_protocol::bedrock::server::text::SText; +use pumpkin_protocol::codec::bit_set::BitSet; +use pumpkin_protocol::codec::var_int::VarInt; +use pumpkin_protocol::java::client::play::{CDisguisedChatMessage, CPlayerChatMessage, FilterType}; +use pumpkin_util::text::TextComponent; +use uuid::Uuid; + +use crate::entity::player::Player; + +/// Mask specifying how a message should be filtered for a receiving client. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum FilterMask { + PassThrough, + FullyFiltered, + PartiallyFiltered(BitSet), +} + +impl FilterMask { + pub const PASS_THROUGH: Self = Self::PassThrough; + pub const FULLY_FILTERED: Self = Self::FullyFiltered; + + #[must_use] + pub const fn is_pass_through(&self) -> bool { + matches!(self, Self::PassThrough) + } + + #[must_use] + pub const fn is_fully_filtered(&self) -> bool { + matches!(self, Self::FullyFiltered) + } + + #[must_use] + pub const fn is_empty(&self) -> bool { + self.is_pass_through() + } + + #[must_use] + pub fn to_filter_type(&self) -> FilterType { + match self { + Self::PassThrough => FilterType::PassThrough, + Self::FullyFiltered => FilterType::FullyFiltered, + Self::PartiallyFiltered(bits) => FilterType::PartiallyFiltered(bits.clone()), + } + } + + #[must_use] + pub fn from_filter_type(filter_type: FilterType) -> Self { + match filter_type { + FilterType::PassThrough => Self::PassThrough, + FilterType::FullyFiltered => Self::FullyFiltered, + FilterType::PartiallyFiltered(bits) => Self::PartiallyFiltered(bits), + } + } + + #[must_use] + pub fn apply(&self, text: &str) -> Option { + match self { + Self::PassThrough => Some(text.to_string()), + Self::FullyFiltered => None, + Self::PartiallyFiltered(bits) => { + let mut chars = Vec::with_capacity(text.len()); + for (i, c) in text.chars().enumerate() { + if bits.get_bit(i) { + chars.push('#'); + } else { + chars.push(c); + } + } + Some(chars.into_iter().collect()) + } + } + } + + #[must_use] + pub fn filter_component(&self, component: &TextComponent) -> Option { + match self { + Self::PassThrough => Some(component.clone()), + Self::FullyFiltered => None, + Self::PartiallyFiltered(_) => { + let text = component.clone().get_text(); + self.apply(&text).map(|filtered_str| { + let mut comp = component.clone(); + comp.0.content = Box::new(pumpkin_util::text::TextContent::Text { + text: filtered_str.into(), + }); + comp + }) + } + } + } +} + +/// Identifies the sender, session, and index of a chat message in the secure chat chain. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SignedMessageLink { + pub index: i32, + pub sender: Uuid, + pub session_id: Uuid, +} + +impl SignedMessageLink { + #[must_use] + pub const fn unsigned(sender: Uuid) -> Self { + Self { + index: 0, + sender, + session_id: Uuid::nil(), + } + } + + #[must_use] + pub const fn new(index: i32, sender: Uuid, session_id: Uuid) -> Self { + Self { + index, + sender, + session_id, + } + } +} + +/// Signed body of a chat message. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SignedMessageBody { + pub content: String, + pub time_stamp: i64, + pub salt: i64, + pub last_seen: Vec>, +} + +impl SignedMessageBody { + #[must_use] + pub const fn unsigned(content: String) -> Self { + Self { + content, + time_stamp: 0, + salt: 0, + last_seen: Vec::new(), + } + } + + #[must_use] + pub const fn new( + content: String, + time_stamp: i64, + salt: i64, + last_seen: Vec>, + ) -> Self { + Self { + content, + time_stamp, + salt, + last_seen, + } + } +} + +/// Represents a chat message sent by a player or the system, with optional digital signature and filter mask. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PlayerChatMessage { + pub link: SignedMessageLink, + pub signature: Option>, + pub signed_body: SignedMessageBody, + pub unsigned_content: Option, + pub filter_mask: FilterMask, +} + +impl PlayerChatMessage { + pub const SYSTEM_SENDER: Uuid = Uuid::nil(); + pub const MESSAGE_EXPIRES_AFTER_SERVER: Duration = Duration::from_secs(300); // 5 minutes + pub const MESSAGE_EXPIRES_AFTER_CLIENT: Duration = Duration::from_secs(420); // 7 minutes + + #[must_use] + pub const fn system(content: String) -> Self { + Self::unsigned(Self::SYSTEM_SENDER, content) + } + + #[must_use] + pub const fn unsigned(sender: Uuid, content: String) -> Self { + let body = SignedMessageBody::unsigned(content); + let link = SignedMessageLink::unsigned(sender); + Self { + link, + signature: None, + signed_body: body, + unsigned_content: None, + filter_mask: FilterMask::PassThrough, + } + } + + #[must_use] + pub const fn new( + link: SignedMessageLink, + signature: Option>, + signed_body: SignedMessageBody, + unsigned_content: Option, + filter_mask: FilterMask, + ) -> Self { + Self { + link, + signature, + signed_body, + unsigned_content, + filter_mask, + } + } + + #[must_use] + pub fn with_unsigned_content(mut self, content: TextComponent) -> Self { + let is_same = match &*content.0.content { + pumpkin_util::text::TextContent::Text { text } => text == &self.signed_body.content, + _ => false, + }; + self.unsigned_content = if is_same { None } else { Some(content) }; + self + } + + #[must_use] + pub fn remove_unsigned_content(mut self) -> Self { + self.unsigned_content = None; + self + } + + #[must_use] + pub fn filter(&self, filter_mask: FilterMask) -> Self { + if self.filter_mask == filter_mask { + self.clone() + } else { + Self { + link: self.link.clone(), + signature: self.signature.clone(), + signed_body: self.signed_body.clone(), + unsigned_content: self.unsigned_content.clone(), + filter_mask, + } + } + } + + #[must_use] + pub fn filter_by_bool(&self, filtered: bool) -> Self { + self.filter(if filtered { + self.filter_mask.clone() + } else { + FilterMask::PassThrough + }) + } + + #[must_use] + pub fn remove_signature(&self) -> Self { + let body = SignedMessageBody::unsigned(self.signed_content().to_string()); + let link = SignedMessageLink::unsigned(self.sender()); + Self { + link, + signature: None, + signed_body: body, + unsigned_content: self.unsigned_content.clone(), + filter_mask: self.filter_mask.clone(), + } + } + + #[must_use] + pub fn signed_content(&self) -> &str { + &self.signed_body.content + } + + #[must_use] + pub fn decorated_content(&self) -> TextComponent { + self.unsigned_content + .clone() + .unwrap_or_else(|| TextComponent::text(self.signed_content().to_string())) + } + + #[must_use] + pub const fn timestamp(&self) -> i64 { + self.signed_body.time_stamp + } + + #[must_use] + pub const fn salt(&self) -> i64 { + self.signed_body.salt + } + + #[must_use] + pub const fn sender(&self) -> Uuid { + self.link.sender + } + + #[must_use] + pub fn is_system(&self) -> bool { + self.sender() == Self::SYSTEM_SENDER + } + + #[must_use] + pub const fn has_signature(&self) -> bool { + self.signature.is_some() + } + + #[must_use] + pub fn has_signature_from(&self, profile_id: &Uuid) -> bool { + self.has_signature() && self.sender() == *profile_id + } + + #[must_use] + pub const fn is_fully_filtered(&self) -> bool { + self.filter_mask.is_fully_filtered() + } +} + +/// Outgoing chat message abstraction sending either disguised system chat or player-signed chat. +#[derive(Clone, Debug)] +pub enum OutgoingChatMessage { + Disguised { content: TextComponent }, + Player { message: PlayerChatMessage }, +} + +impl OutgoingChatMessage { + #[must_use] + pub fn create(message: PlayerChatMessage) -> Self { + if message.is_system() { + Self::Disguised { + content: message.decorated_content(), + } + } else { + Self::Player { message } + } + } + + #[must_use] + pub fn content(&self) -> TextComponent { + match self { + Self::Disguised { content } => content.clone(), + Self::Player { message } => message.decorated_content(), + } + } + + pub fn send_to_player( + &self, + player: &Arc, + filtered: bool, + chat_type: VarInt, + sender_name: &TextComponent, + target_name: Option<&TextComponent>, + ) { + match self { + Self::Disguised { content } => { + let je_packet = + CDisguisedChatMessage::new(content, chat_type, sender_name, target_name); + let be_packet = + SText::new(content.clone().get_text(), sender_name.clone().get_text()); + player.try_enqueue_packet_editioned(&je_packet, &be_packet); + } + Self::Player { message } => { + let filtered_message = message.filter_by_bool(filtered); + if !filtered_message.is_fully_filtered() { + let messages_sent = VarInt(filtered_message.link.index); + let messages_received: i32 = player + .chat_session + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .messages_received; + + let sender_last_seen = { + let cache = player + .signature_cache + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + cache.last_seen.indexed_for(player) + }; + + let je_packet = CPlayerChatMessage::new( + VarInt(messages_received), + filtered_message.sender(), + messages_sent, + filtered_message.signature.clone(), + filtered_message.signed_content().into(), + filtered_message.timestamp(), + filtered_message.salt(), + sender_last_seen, + filtered_message.unsigned_content.clone(), + filtered_message.filter_mask.to_filter_type(), + chat_type, + sender_name.clone(), + target_name.cloned(), + ); + let be_packet = SText::new( + filtered_message.decorated_content().get_text(), + sender_name.clone().get_text(), + ); + + player.try_enqueue_packet_editioned(&je_packet, &be_packet); + + if let Some(signature) = &filtered_message.signature { + let mut cache = player + .signature_cache + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + cache.add_seen_signature(signature); + cache.last_seen_validator.add_pending(signature); + let tracked_count = cache.last_seen_validator.tracked_messages_count(); + drop(cache); + + if tracked_count > 4096 { + player.kick( + crate::net::DisconnectReason::Kicked, + &TextComponent::translate_cross( + pumpkin_data::translation::java::MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, + pumpkin_data::translation::java::MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, + [], + ), + ); + } + } + + if player.gameprofile.id != filtered_message.sender() { + player + .signature_cache + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .cache_signatures(&filtered_message.signed_body.last_seen); + } + player + .chat_session + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .messages_received += 1; + } + } + } + } +} diff --git a/crates/pumpkin/src/net/java/play/chat_message.rs b/crates/pumpkin/src/net/java/play/chat_message.rs index f9562898f..589277311 100644 --- a/crates/pumpkin/src/net/java/play/chat_message.rs +++ b/crates/pumpkin/src/net/java/play/chat_message.rs @@ -1,5 +1,6 @@ #[allow(clippy::wildcard_imports)] use super::*; +use pumpkin_data::world::RAW; impl JavaClient { pub async fn handle_chat_message( @@ -67,17 +68,17 @@ impl JavaClient { let entity = &player.get_entity(); let world = entity.world.load_full(); if server.basic_config.allow_chat_reports { - world.broadcast_secure_player_chat(player, &chat_message, &decorated_message).await; + world.broadcast_secure_player_chat(player, &chat_message, &decorated_message); } else { - let je_packet = CSystemChatMessage::new( - &decorated_message, - false, + let outgoing = crate::net::chat::PlayerChatMessage::system(message).with_unsigned_content(decorated_message); + world.broadcast_chat_message( + &outgoing, + Player::is_text_filtering_enabled, + Some(player), + (RAW + 1).into(), + &TextComponent::empty(), + None, ); - let be_packet = SText::new( - message, player.gameprofile.name.clone() - ); - - world.broadcast_editioned(&je_packet, &be_packet); } } }} diff --git a/crates/pumpkin/src/net/java/play/mod.rs b/crates/pumpkin/src/net/java/play/mod.rs index bbf9bb68a..194534f6f 100644 --- a/crates/pumpkin/src/net/java/play/mod.rs +++ b/crates/pumpkin/src/net/java/play/mod.rs @@ -1,4 +1,3 @@ -use pumpkin_protocol::bedrock::server::text::SText; use pumpkin_util::{Hand, PermissionLvl}; use rsa::pkcs1v15::{Signature as RsaPkcs1v15Signature, VerifyingKey}; use rsa::signature::Verifier; @@ -35,7 +34,6 @@ use crate::plugin::player::player_toggle_sneak_event::PlayerToggleSneakEvent; use crate::block::entities::command_block::CommandBlockEntity; use crate::block::entities::jigsaw_block::JigsawBlockEntity; -use crate::block::entities::sign::SignBlockEntity; use crate::plugin::player::player_toggle_sprint_event::PlayerToggleSprintEvent; use crate::server::{Server, seasonal_events}; use crate::world::{BlockBreakingProgress, World, chunker}; @@ -58,9 +56,8 @@ use pumpkin_protocol::codec::var_int::VarInt; use pumpkin_protocol::codec::var_ulong::VarULong; use pumpkin_protocol::java::client::play::{ CBlockUpdate, CCommandSuggestions, CEntityPositionSync, CHeadRot, CPingResponse, - CPlayerInfoUpdate, CPlayerPosition, CSetCamera, CSetSelectedSlot, CSystemChatMessage, - CUpdateEntityPos, CUpdateEntityPosRot, CUpdateEntityRot, InitChat, PlayerAction, - PlayerInfoFlags, + CPlayerInfoUpdate, CPlayerPosition, CSetCamera, CSetSelectedSlot, CUpdateEntityPos, + CUpdateEntityPosRot, CUpdateEntityRot, InitChat, PlayerAction, PlayerInfoFlags, }; use pumpkin_protocol::java::server::play::{ Action, ActionType, CommandBlockMode, FLAG_ON_GROUND, SAttack, SBundleItemSelected, diff --git a/crates/pumpkin/src/net/java/play/update_sign.rs b/crates/pumpkin/src/net/java/play/update_sign.rs index a14ae75ae..7997ab019 100644 --- a/crates/pumpkin/src/net/java/play/update_sign.rs +++ b/crates/pumpkin/src/net/java/play/update_sign.rs @@ -7,10 +7,22 @@ impl JavaClient { let Some(block_entity) = world.get_block_entity(&sign_data.location) else { return; }; - let Some(sign_entity) = block_entity.as_any().downcast_ref::() else { + let Some(sign_entity) = + crate::block::entities::sign::SignEntityRef::from_block_entity(&*block_entity) + else { return; }; - if sign_entity.is_waxed.load(Ordering::Relaxed) { + if sign_entity.is_waxed() { + return; + } + + let currently_editing = *sign_entity + .currently_editing_player() + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + if let Some(editor_id) = currently_editing + && editor_id != player.gameprofile.id + { return; } @@ -35,11 +47,7 @@ impl JavaClient { } } - let text = if sign_data.is_front_text { - &sign_entity.front_text - } else { - &sign_entity.back_text - }; + let text = sign_entity.get_text(sign_data.is_front_text); *text .messages @@ -51,7 +59,7 @@ impl JavaClient { sign_data.line_4.into(), ]; *sign_entity - .currently_editing_player + .currently_editing_player() .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) = None; world.update_block_entity(&block_entity); diff --git a/crates/pumpkin/src/net/mod.rs b/crates/pumpkin/src/net/mod.rs index f6e56eb45..05f351c1f 100644 --- a/crates/pumpkin/src/net/mod.rs +++ b/crates/pumpkin/src/net/mod.rs @@ -28,6 +28,7 @@ use thiserror::Error; use uuid::Uuid; pub mod authentication; pub mod bedrock; +pub mod chat; pub mod chunk_sender; pub use chunk_sender::ChunkSender; pub mod java; diff --git a/crates/pumpkin/src/plugin/api/context.rs b/crates/pumpkin/src/plugin/api/context.rs index 887255461..18e2d911f 100644 --- a/crates/pumpkin/src/plugin/api/context.rs +++ b/crates/pumpkin/src/plugin/api/context.rs @@ -157,28 +157,66 @@ impl Context { /// Registers a new command to the server with a specified permission level. /// /// # Arguments - /// - `tree`: The command tree to register. + /// - `node`: The command node to register. /// - `permission`: The permission level required to execute the command. pub fn register_command>( &self, - mut tree: crate::command::tree::CommandTree, + node: impl Into, permission: P, ) { let permission = permission.into(); - tree.source = Some(self.metadata.name.clone()); - let full_permission_node = if permission.contains(':') { permission } else { format!("{}:{permission}", self.metadata.name) }; + let mut node = node.into(); + node.meta.source = Some(self.metadata.name.clone()); + node.owned + .requirements + .0 + .push(crate::command::node::Requirement(Arc::new(move |source| { + source.has_permission(&full_permission_node) + }))); + self.server.command_dispatcher.rcu(|dispatcher| { let mut new_dispatcher = (**dispatcher).clone(); - new_dispatcher - .fallback_dispatcher - .register(tree.clone(), full_permission_node.clone()); + new_dispatcher.register(node.clone()); + Arc::new(new_dispatcher) + }); + + self.reload_commands_for_everyone(); + } + + /// Registers a new command with aliases to the server with a specified permission level. + pub fn register_command_with_aliases>( + &self, + node: impl Into, + aliases: &[String], + permission: P, + ) { + let permission = permission.into(); + + let full_permission_node = if permission.contains(':') { + permission + } else { + format!("{}:{permission}", self.metadata.name) + }; + + let mut node = node.into(); + node.meta.source = Some(self.metadata.name.clone()); + node.owned + .requirements + .0 + .push(crate::command::node::Requirement(Arc::new(move |source| { + source.has_permission(&full_permission_node) + }))); + + self.server.command_dispatcher.rcu(|dispatcher| { + let mut new_dispatcher = (**dispatcher).clone(); + new_dispatcher.register_with_aliases(node.clone(), aliases); Arc::new(new_dispatcher) }); @@ -192,7 +230,7 @@ impl Context { pub fn unregister_command(&self, name: &str) { self.server.command_dispatcher.rcu(|dispatcher| { let mut new_dispatcher = (**dispatcher).clone(); - new_dispatcher.fallback_dispatcher.unregister(name); + new_dispatcher.disable_command(name.to_string()); Arc::new(new_dispatcher) }); diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/args.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/args.rs index e3445da8a..126a5baf2 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/args.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/args.rs @@ -2,7 +2,26 @@ use std::{collections::HashMap, sync::Arc}; use pumpkin_util::text::TextComponent; -use crate::{command::tree::CommandTree, entity::player::Player}; +use crate::{ + command::{ + argument_types::entity_anchor::EntityAnchor, context::command_context::CommandContext, + }, + entity::player::Player, +}; + +#[derive(Clone, Copy, Debug)] +pub enum Number { + F64(f64), + F32(f32), + I32(i32), + I64(i64), +} + +#[derive(Clone, Copy, Debug)] +pub enum NotInBounds { + LowerBound(Number, Number), + UpperBound(Number, Number), +} #[derive(Clone)] pub enum OwnedArg { @@ -16,7 +35,6 @@ pub enum OwnedArg { Rotation(f32, bool, f32, bool), GameMode(pumpkin_util::GameMode), Difficulty(pumpkin_util::Difficulty), - CommandTree(CommandTree), Item(String), ItemPredicate(String), ResourceLocation(String), @@ -28,64 +46,59 @@ pub enum OwnedArg { Msg(String), TextComponent(TextComponent), Time(i32), - Num( - Result< - crate::command::args::bounded_num::Number, - crate::command::args::bounded_num::NotInBounds, - >, - ), + Num(Result), Bool(bool), Simple(String), SoundCategory(pumpkin_data::sound::SoundCategory), DamageType(pumpkin_data::damage::DamageType), Effect(&'static pumpkin_data::effect::StatusEffect), Enchantment(&'static pumpkin_data::Enchantment), - EntityAnchor(crate::command::args::EntityAnchor), + EntityAnchor(EntityAnchor), Advancement(&'static pumpkin_data::Advancement), } -impl OwnedArg { - #[must_use] - #[expect(clippy::unreachable)] - pub fn from_arg(arg: &crate::command::args::Arg<'_>) -> Self { - use crate::command::args::Arg; - match arg { - Arg::Entities(v) => Self::Entities(v.clone()), - Arg::Entity(e) => Self::Entity(e.clone()), - Arg::Players(v) => Self::Players(v.clone()), - Arg::GameProfiles(v) => Self::GameProfiles(v.clone()), - Arg::BlockPos(p) => Self::BlockPos(*p), - Arg::Pos3D(v) => Self::Pos3D(*v), - Arg::Pos2D(v) => Self::Pos2D(*v), - Arg::Rotation(a, b, c, d) => Self::Rotation(*a, *b, *c, *d), - Arg::GameMode(g) => Self::GameMode(*g), - Arg::Difficulty(d) => Self::Difficulty(*d), - Arg::CommandTree(t) => Self::CommandTree(t.clone()), - Arg::Item(s) => Self::Item(s.to_string()), - Arg::ItemPredicate(s) => Self::ItemPredicate(s.to_string()), - Arg::ResourceLocation(s) => Self::ResourceLocation(s.to_string()), - Arg::Block(s) => Self::Block(s.to_string()), - Arg::BlockPredicate(s) => Self::BlockPredicate(s.to_string()), - Arg::BossbarColor(c) => Self::BossbarColor(*c), - Arg::BossbarStyle(s) => Self::BossbarStyle(*s), - Arg::Particle(p) => Self::Particle(*p), - Arg::Msg(m) => Self::Msg(m.clone()), - Arg::TextComponent(t) => Self::TextComponent(t.clone()), - Arg::Time(t) => Self::Time(*t), - Arg::Num(n) => Self::Num(*n), - Arg::Bool(b) => Self::Bool(*b), - Arg::Simple(s) => Self::Simple(s.to_string()), - Arg::SoundCategory(s) => Self::SoundCategory(*s), - Arg::DamageType(d) => Self::DamageType(*d), - Arg::Effect(e) => Self::Effect(e), - Arg::Enchantment(e) => Self::Enchantment(e), - Arg::EntityAnchor(a) => Self::EntityAnchor(*a), - Arg::Advancement(a) => Self::Advancement(a), - Arg::Slot(_, _) | Arg::Slots(_, _) | Arg::TeamColor(_) | Arg::HexColor(_) => { - unreachable!() +#[must_use] +pub fn build_consumed_args_from_context(context: &CommandContext) -> HashMap { + let mut map = HashMap::new(); + for (name, parsed) in &context.arguments { + let res = &parsed.result; + if let Some(&b) = res.downcast_ref::() { + map.insert(name.clone(), OwnedArg::Bool(b)); + } else if let Some(&i) = res.downcast_ref::() { + map.insert(name.clone(), OwnedArg::Num(Ok(Number::I32(i)))); + } else if let Some(&i) = res.downcast_ref::() { + map.insert(name.clone(), OwnedArg::Num(Ok(Number::I64(i)))); + } else if let Some(&f) = res.downcast_ref::() { + map.insert(name.clone(), OwnedArg::Num(Ok(Number::F32(f)))); + } else if let Some(&f) = res.downcast_ref::() { + map.insert(name.clone(), OwnedArg::Num(Ok(Number::F64(f)))); + } else if let Some(s) = res.downcast_ref::() { + map.insert(name.clone(), OwnedArg::Simple(s.clone())); + } else if let Some(&pos) = res.downcast_ref::() { + map.insert(name.clone(), OwnedArg::BlockPos(pos)); + } else if let Some(&v) = res.downcast_ref::>() { + map.insert(name.clone(), OwnedArg::Pos3D(v)); + } else if let Some(&v) = res.downcast_ref::>() { + map.insert(name.clone(), OwnedArg::Pos2D(v)); + } else if let Some(&mode) = res.downcast_ref::() { + map.insert(name.clone(), OwnedArg::GameMode(mode)); + } else if let Some(&diff) = res.downcast_ref::() { + map.insert(name.clone(), OwnedArg::Difficulty(diff)); + } else if let Some(t) = res.downcast_ref::() { + map.insert(name.clone(), OwnedArg::TextComponent(t.clone())); + } else if let Some(&anchor) = res.downcast_ref::() { + map.insert(name.clone(), OwnedArg::EntityAnchor(anchor)); + } else if let Some(selector) = + res.downcast_ref::() + { + if let Ok(players) = selector.find_players(&context.source) { + map.insert(name.clone(), OwnedArg::Players(players)); + } else if let Ok(entities) = selector.find_entities(&context.source) { + map.insert(name.clone(), OwnedArg::Entities(entities)); } } } + map } pub struct ConsumedArgsResource { diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs index 03499868c..8ecc16849 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs @@ -12,11 +12,7 @@ use wasmtime_wasi_http::{ }; use crate::{ - command::{ - CommandSender, - args::ConsumedArgs, - tree::{CommandTree, builder::NonLeafNodeBuilder}, - }, + command::CommandSender, entity::EntityBase, entity::player::Player, plugin::{ @@ -28,6 +24,82 @@ use crate::{ world::World, }; +pub struct WasmCommand { + pub names: Vec, + pub builder: crate::command::argument_builder::CommandArgumentBuilder, +} + +impl WasmCommand { + #[must_use] + pub fn new(names: Vec, description: String) -> Self { + let primary = names.first().cloned().unwrap_or_default(); + let builder = crate::command::argument_builder::command(primary, description); + Self { names, builder } + } + + #[must_use] + pub fn then(mut self, child: WasmCommandNode) -> Self { + use crate::command::argument_builder::ArgumentBuilder; + self.builder = self.builder.then(child.into_detached_node()); + self + } + + #[must_use] + pub fn executes( + mut self, + executor: impl crate::command::node::CommandExecutor + 'static, + ) -> Self { + use crate::command::argument_builder::ArgumentBuilder; + self.builder = self.builder.executes(executor); + self + } +} + +pub enum WasmCommandNode { + Literal(crate::command::argument_builder::LiteralArgumentBuilder), + Argument(crate::command::argument_builder::RequiredArgumentBuilder), +} + +impl WasmCommandNode { + #[must_use] + pub fn then(self, child: Self) -> Self { + use crate::command::argument_builder::ArgumentBuilder; + match self { + Self::Literal(b) => Self::Literal(b.then(child.into_detached_node())), + Self::Argument(b) => Self::Argument(b.then(child.into_detached_node())), + } + } + + #[must_use] + pub fn executes(self, executor: impl crate::command::node::CommandExecutor + 'static) -> Self { + use crate::command::argument_builder::ArgumentBuilder; + match self { + Self::Literal(b) => Self::Literal(b.executes(executor)), + Self::Argument(b) => Self::Argument(b.executes(executor)), + } + } + + #[must_use] + pub fn suggests( + self, + provider: impl crate::command::suggestion::provider::SuggestionProvider + 'static, + ) -> Self { + match self { + Self::Literal(b) => Self::Literal(b), + Self::Argument(b) => Self::Argument(b.suggests(provider)), + } + } + + #[must_use] + pub fn into_detached_node(self) -> crate::command::node::detached::DetachedNode { + use crate::command::argument_builder::ArgumentBuilder; + match self { + Self::Literal(b) => crate::command::node::detached::DetachedNode::Literal(b.build()), + Self::Argument(b) => crate::command::node::detached::DetachedNode::Argument(b.build()), + } + } +} + pub struct WasmResource { pub provider: T, } @@ -55,10 +127,10 @@ pub type BossBarResource = WasmResource< Arc>, >; pub type TextComponentResource = WasmResource; -pub type CommandResource = WasmResource; +pub type CommandResource = WasmResource; pub type CommandSenderResource = WasmResource; -pub type ConsumedArgsResource = WasmResource; -pub type CommandNodeResource = WasmResource; +pub type ConsumedArgsResource = WasmResource>; +pub type CommandNodeResource = WasmResource; pub type ItemStackResource = WasmResource>>; pub type RecipeManagerResource = WasmResource>; pub type EnchantmentManagerResource = @@ -298,7 +370,7 @@ impl PluginHostState { pub fn add_command( &mut self, - provider: CommandTree, + provider: WasmCommand, ) -> wasmtime::Result> { let resource = self.resource_table.push(CommandResource { provider })?; Ok(wasmtime::component::Resource::new_own(resource.rep())) @@ -316,21 +388,17 @@ impl PluginHostState { pub fn add_consumed_args( &mut self, - provider: &ConsumedArgs<'_>, + provider: HashMap, ) -> wasmtime::Result> { - let owned: HashMap = provider - .iter() - .map(|(k, v)| (k.to_string(), OwnedArg::from_arg(v))) - .collect(); let resource = self .resource_table - .push(ConsumedArgsResource { provider: owned })?; + .push(ConsumedArgsResource { provider })?; Ok(wasmtime::component::Resource::new_own(resource.rep())) } pub fn add_command_node( &mut self, - provider: NonLeafNodeBuilder, + provider: WasmCommandNode, ) -> wasmtime::Result> { let resource = self.resource_table.push(CommandNodeResource { provider })?; Ok(wasmtime::component::Resource::new_own(resource.rep())) diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/commands/executor.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/commands/executor.rs index 281f74056..ee9ee9a88 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/commands/executor.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/commands/executor.rs @@ -7,14 +7,17 @@ use pumpkin_util::text::{ use crate::{ command::{ - CommandExecutor, - context::string_range::StringRange, - dispatcher::CommandError, - suggestion::{Suggestion, suggestions::Suggestions}, - tree::{CommandSuggestionProvider, CommandSuggestionResult}, + context::command_context::CommandContext, + errors::error_types::DISPATCHER_PARSE_EXCEPTION, + node::{CommandExecutor, CommandExecutorResult}, + suggestion::{ + provider::SuggestionProvider, + suggestions::{Suggestions, SuggestionsBuilder}, + }, }, plugin::loader::wasm::wasm_host::{ DowncastResourceExt, PluginInstance, WasmPlugin, + args::build_consumed_args_from_context, wit::v0_1::pumpkin::plugin::command::{CommandError as CommandErrorWit, SuggestionRequest}, }, server::Server, @@ -27,28 +30,36 @@ pub struct WasmCommandExecutor { } impl CommandExecutor for WasmCommandExecutor { - fn execute( - &self, - sender: &crate::command::CommandSender, - _server: &crate::server::Server, - args: &crate::command::args::ConsumedArgs, - ) -> crate::command::CommandResult { + fn execute(&self, context: &CommandContext) -> CommandExecutorResult { tokio::task::block_in_place(|| { tokio::runtime::Handle::current().block_on(async { let mut store = self.plugin.store.lock().await; let sender_resource = store .data_mut() - .add_command_sender(sender.clone()) - .expect("valid command sender"); + .add_command_sender(context.source.output.clone()) + .map_err(|e| { + DISPATCHER_PARSE_EXCEPTION.create_without_context(TextComponent::text( + format!("Failed to create sender: {e}"), + )) + })?; let server_resource = store .data_mut() .add_server(self.server.clone()) - .expect("valid server"); + .map_err(|e| { + DISPATCHER_PARSE_EXCEPTION.create_without_context(TextComponent::text( + format!("Failed to create server: {e}"), + )) + })?; + let consumed_args = build_consumed_args_from_context(context); let args_resource = store .data_mut() - .add_consumed_args(args) - .expect("valid consumed args"); + .add_consumed_args(consumed_args) + .map_err(|e| { + DISPATCHER_PARSE_EXCEPTION.create_without_context(TextComponent::text( + format!("Failed to create args: {e}"), + )) + })?; let sender_rep = sender_resource.rep(); let server_rep = server_resource.rep(); @@ -76,8 +87,8 @@ impl CommandExecutor for WasmCommandExecutor { .data_mut() .resource_table .delete::( - wasmtime::component::Resource::new_own(server_rep), - ); + wasmtime::component::Resource::new_own(server_rep), + ); let _ = store .data_mut() .resource_table @@ -86,7 +97,7 @@ impl CommandExecutor for WasmCommandExecutor { ); let result = result.map_err(|e| { - CommandError::CommandFailed( + DISPATCHER_PARSE_EXCEPTION.create_without_context( TextComponent::text(format!( "Wasm command failed with following error: {e}" )) @@ -98,16 +109,22 @@ impl CommandExecutor for WasmCommandExecutor { Ok(value) => Ok(value), Err(err) => match err { CommandErrorWit::InvalidConsumption(value) => { - Err(CommandError::InvalidConsumption(value)) + Err(DISPATCHER_PARSE_EXCEPTION.create_without_context( + TextComponent::text(format!( + "Invalid consumption: {value:?}" + )), + )) } CommandErrorWit::InvalidRequirement => { - Err(CommandError::InvalidRequirement) + Err(DISPATCHER_PARSE_EXCEPTION + .create_without_context(TextComponent::text("Invalid requirement"))) } CommandErrorWit::PermissionDenied => { - Err(CommandError::PermissionDenied) + Err(DISPATCHER_PARSE_EXCEPTION + .create_without_context(TextComponent::text("Permission denied"))) } CommandErrorWit::CommandFailed(resource) => { - Err(CommandError::CommandFailed( + Err(DISPATCHER_PARSE_EXCEPTION.create_without_context( resource.consume(store.data_mut()).provider, )) } @@ -126,26 +143,22 @@ pub struct WasmCommandSuggestionProvider { pub server: Arc, } -impl CommandSuggestionProvider for WasmCommandSuggestionProvider { - fn suggest( - &self, - src: &crate::command::CommandSender, - _server: &Server, - input: &str, - start: usize, - end: usize, - ) -> CommandSuggestionResult { +impl SuggestionProvider for WasmCommandSuggestionProvider { + fn suggest(&self, context: &CommandContext, builder: SuggestionsBuilder) -> Suggestions { tokio::task::block_in_place(|| { tokio::runtime::Handle::current().block_on(async { let mut store = self.plugin.store.lock().await; - let sender_resource = match store.data_mut().add_command_sender(src.clone()) { + let sender_resource = match store + .data_mut() + .add_command_sender(context.source.output.clone()) + { Ok(resource) => resource, Err(error) => { tracing::error!( "Failed to create command sender resource for suggestions: {error}" ); - return Suggestions::empty(); + return builder.build(); } }; let server_resource = match store.data_mut().add_server(self.server.clone()) { @@ -154,15 +167,16 @@ impl CommandSuggestionProvider for WasmCommandSuggestionProvider { tracing::error!( "Failed to create server resource for suggestions: {error}" ); - return Suggestions::empty(); + return builder.build(); } }; + let input = &context.input; let request = SuggestionRequest { - input: input.to_string(), + input: input.clone(), cursor: input.len().try_into().unwrap_or(u32::MAX), - start: start.try_into().unwrap_or(u32::MAX), - remaining: input[start.min(input.len())..end.min(input.len())].to_string(), + start: builder.start.try_into().unwrap_or(u32::MAX), + remaining: builder.remaining().to_string(), }; let response = match self.plugin.plugin_instance { @@ -183,30 +197,22 @@ impl CommandSuggestionProvider for WasmCommandSuggestionProvider { Ok(response) => response, Err(error) => { tracing::error!("Wasm command suggestion failed: {error}"); - return Suggestions::empty(); + return builder.build(); } }; - let start = response.start as usize; - let end = start.saturating_add(response.length as usize); - let range = StringRange::between(start, end.min(input.len())); - let suggestions = response - .values - .into_iter() - .map(|suggestion| { - if let Some(tooltip) = suggestion.tooltip { - Suggestion::with_tooltip( - range, - suggestion.value, - tooltip.consume(store.data_mut()).provider, - ) - } else { - Suggestion::without_tooltip(range, suggestion.value) - } - }) - .collect(); + let mut result_builder = builder; + for suggestion in response.values { + if let Some(tooltip) = suggestion.tooltip { + let text = tooltip.consume(store.data_mut()).provider; + result_builder = + result_builder.suggest_with_tooltip(suggestion.value, text); + } else { + result_builder = result_builder.suggest(suggestion.value); + } + } - Suggestions::new(range, suggestions) + result_builder.build() }) }) } diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/commands/mod.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/commands/mod.rs index c894d8626..363a69e65 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/commands/mod.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/commands/mod.rs @@ -2,37 +2,33 @@ use wasmtime::component::Resource; use crate::{ command::{ - args::{ - GetClientSideArgParser, - block::{BlockArgumentConsumer, BlockPredicateArgumentConsumer}, - bool::BoolArgConsumer, - bounded_num::{BoundedNumArgumentConsumer, ToFromNumber}, - difficulty::DifficultyArgumentConsumer, - entities::EntitiesArgumentConsumer, - entity::EntityArgumentConsumer, - entity_anchor::EntityAnchorArgumentConsumer, - gamemode::GamemodeArgumentConsumer, - message::MsgArgConsumer, - players::PlayersArgumentConsumer, - position_2d::Position2DArgumentConsumer, - position_3d::Position3DArgumentConsumer, - position_block::BlockPosArgumentConsumer, - resource::item::{ItemArgumentConsumer, ItemPredicateArgumentConsumer}, - resource_location::ResourceLocationArgumentConsumer, - rotation::RotationArgumentConsumer, - simple::SimpleArgConsumer, - textcomponent::TextComponentArgConsumer, - time::TimeArgumentConsumer, - }, - tree::{ - CommandTree, - builder::{NonLeafNodeBuilder, argument, literal}, + argument_builder::{argument, literal}, + argument_types::{ + block::BlockArgumentType, + block_predicate::BlockPredicateArgumentType, + component::ComponentArgumentType, + coordinates::{ + block_pos::BlockPosArgumentType, rotation::RotationArgumentType, + vec2::Vec2ArgumentType, vec3::Vec3ArgumentType, + }, + core::{ + bool::BoolArgumentType, double::DoubleArgumentType, float::FloatArgumentType, + integer::IntegerArgumentType, long::LongArgumentType, string::StringArgumentType, + }, + entity::EntityArgumentType, + entity_anchor::EntityAnchorArgumentType, + game_profile::GameProfileArgumentType, + gamemode::GameModeArgumentType, + identifier::IdentifierArgumentType, + item::ItemStackArgumentType, + item_predicate::ItemPredicateArgumentType, + time::TimeArgumentType, }, }, plugin::loader::wasm::wasm_host::{ state::{ CommandNodeResource, CommandResource, CommandSenderResource, ConsumedArgsResource, - PluginHostState, ServerResource, TextComponentResource, + PluginHostState, ServerResource, TextComponentResource, WasmCommand, WasmCommandNode, }, wit::v0_1::{ commands::executor::{WasmCommandExecutor, WasmCommandSuggestionProvider}, @@ -127,7 +123,7 @@ impl pumpkin::plugin::command::HostConsumedArgs for PluginHostState { OwnedArg::BlockPredicate(s) => Arg::BlockPredicate(s), OwnedArg::Time(t) => Arg::Time(t), OwnedArg::Num(n) => { - use crate::command::args::bounded_num::{NotInBounds, Number}; + use crate::plugin::loader::wasm::wasm_host::args::{NotInBounds, Number}; let convert_num = |n: Number| match n { Number::F64(v) => pumpkin::plugin::command::Number::Float64(v), Number::F32(v) => pumpkin::plugin::command::Number::Float32(v), @@ -264,18 +260,17 @@ impl pumpkin::plugin::command::HostConsumedArgs for PluginHostState { OwnedArg::Enchantment(e) => Arg::Enchantment(e.name.to_string()), OwnedArg::Advancement(a) => Arg::Advancement(a.to_string()), OwnedArg::EntityAnchor(a) => Arg::EntityAnchor(match a { - crate::command::args::EntityAnchor::Eyes => { + crate::command::argument_types::entity_anchor::EntityAnchor::Eyes => { pumpkin::plugin::command::EntityAnchor::Eyes } - crate::command::args::EntityAnchor::Feet => { + crate::command::argument_types::entity_anchor::EntityAnchor::Feet => { pumpkin::plugin::command::EntityAnchor::Feet } }), // These types don't have direct WIT resource mappings yet - OwnedArg::Entities(_) - | OwnedArg::Entity(_) - | OwnedArg::GameProfiles(_) - | OwnedArg::CommandTree(_) => Arg::Simple(String::new()), + OwnedArg::Entities(_) | OwnedArg::Entity(_) | OwnedArg::GameProfiles(_) => { + Arg::Simple(String::new()) + } }) } @@ -293,7 +288,7 @@ impl pumpkin::plugin::command::HostCommand for PluginHostState { names: Vec, description: String, ) -> wasmtime::Result> { - self.add_command(CommandTree::new(names, description)) + self.add_command(WasmCommand::new(names, description)) .map_err(|_| wasmtime::Error::msg("Failed to add command resource")) } @@ -304,7 +299,11 @@ impl pumpkin::plugin::command::HostCommand for PluginHostState { ) -> wasmtime::Result<()> { let node_data = self.take_node(&node)?; let command_res = self.get_command_mut(&command)?; - command_res.provider = command_res.provider.clone().then(node_data.provider); + let cmd = std::mem::replace( + &mut command_res.provider, + WasmCommand::new(Vec::new(), String::new()), + ); + command_res.provider = cmd.then(node_data.provider); Ok(()) } @@ -329,7 +328,11 @@ impl pumpkin::plugin::command::HostCommand for PluginHostState { server, }; let command_res = self.get_command_mut(&command)?; - command_res.provider = command_res.provider.clone().execute(executor); + let cmd = std::mem::replace( + &mut command_res.provider, + WasmCommand::new(Vec::new(), String::new()), + ); + command_res.provider = cmd.executes(executor); Ok(()) } @@ -556,7 +559,7 @@ impl pumpkin::plugin::command::HostCommandSender for PluginHostState { impl pumpkin::plugin::command::HostCommandNode for PluginHostState { async fn literal(&mut self, name: String) -> wasmtime::Result> { - self.add_command_node(literal(name)) + self.add_command_node(WasmCommandNode::Literal(literal(name))) .map_err(|_| wasmtime::Error::msg("Failed to add literal node")) } @@ -566,36 +569,86 @@ impl pumpkin::plugin::command::HostCommandNode for PluginHostState { arg_type: ArgumentType, ) -> wasmtime::Result> { let node = match arg_type { - ArgumentType::Bool => argument(name, BoolArgConsumer), - ArgumentType::Float((min, max)) => build_bounded_node::(name, min, max), - ArgumentType::Double((min, max)) => build_bounded_node::(name, min, max), - ArgumentType::Integer((min, max)) => build_bounded_node::(name, min, max), - ArgumentType::Long((min, max)) => build_bounded_node::(name, min, max), + ArgumentType::Bool => WasmCommandNode::Argument(argument(name, BoolArgumentType)), + ArgumentType::Float((min, max)) => WasmCommandNode::Argument(argument( + name, + FloatArgumentType::new(min.unwrap_or(f32::MIN), max.unwrap_or(f32::MAX)), + )), + ArgumentType::Double((min, max)) => WasmCommandNode::Argument(argument( + name, + DoubleArgumentType::new(min.unwrap_or(f64::MIN), max.unwrap_or(f64::MAX)), + )), + ArgumentType::Integer((min, max)) => WasmCommandNode::Argument(argument( + name, + IntegerArgumentType::new(min.unwrap_or(i32::MIN), max.unwrap_or(i32::MAX)), + )), + ArgumentType::Long((min, max)) => WasmCommandNode::Argument(argument( + name, + LongArgumentType::new(min.unwrap_or(i64::MIN), max.unwrap_or(i64::MAX)), + )), ArgumentType::String(st) => match st { - StringType::SingleWord | StringType::Quotable => argument(name, SimpleArgConsumer), - StringType::Greedy => argument(name, MsgArgConsumer), + StringType::SingleWord => { + WasmCommandNode::Argument(argument(name, StringArgumentType::SingleWord)) + } + StringType::Quotable => { + WasmCommandNode::Argument(argument(name, StringArgumentType::QuotablePhrase)) + } + StringType::Greedy => { + WasmCommandNode::Argument(argument(name, StringArgumentType::GreedyPhrase)) + } }, - ArgumentType::Entities => argument(name, EntitiesArgumentConsumer), - ArgumentType::Entity => argument(name, EntityArgumentConsumer), - ArgumentType::Players | ArgumentType::GameProfile => { - argument(name, PlayersArgumentConsumer) + ArgumentType::Entities => { + WasmCommandNode::Argument(argument(name, EntityArgumentType::Entities)) + } + ArgumentType::Entity => { + WasmCommandNode::Argument(argument(name, EntityArgumentType::Entity)) + } + ArgumentType::Players => { + WasmCommandNode::Argument(argument(name, EntityArgumentType::Players)) + } + ArgumentType::GameProfile => { + WasmCommandNode::Argument(argument(name, GameProfileArgumentType)) + } + ArgumentType::BlockPos => { + WasmCommandNode::Argument(argument(name, BlockPosArgumentType)) + } + ArgumentType::Position3d => { + WasmCommandNode::Argument(argument(name, Vec3ArgumentType::Default)) + } + ArgumentType::Position2d => { + WasmCommandNode::Argument(argument(name, Vec2ArgumentType::Default)) + } + ArgumentType::BlockState => { + WasmCommandNode::Argument(argument(name, BlockArgumentType)) + } + ArgumentType::BlockPredicate => { + WasmCommandNode::Argument(argument(name, BlockPredicateArgumentType)) + } + ArgumentType::Item => WasmCommandNode::Argument(argument(name, ItemStackArgumentType)), + ArgumentType::ItemPredicate => { + WasmCommandNode::Argument(argument(name, ItemPredicateArgumentType)) + } + ArgumentType::Component => { + WasmCommandNode::Argument(argument(name, ComponentArgumentType)) + } + ArgumentType::Rotation => { + WasmCommandNode::Argument(argument(name, RotationArgumentType)) } - ArgumentType::BlockPos => argument(name, BlockPosArgumentConsumer), - ArgumentType::Position3d => argument(name, Position3DArgumentConsumer), - ArgumentType::Position2d => argument(name, Position2DArgumentConsumer), - ArgumentType::BlockState => argument(name, BlockArgumentConsumer), - ArgumentType::BlockPredicate => argument(name, BlockPredicateArgumentConsumer), - ArgumentType::Item => argument(name, ItemArgumentConsumer), - ArgumentType::ItemPredicate => argument(name, ItemPredicateArgumentConsumer), - ArgumentType::Component => argument(name, TextComponentArgConsumer), - ArgumentType::Rotation => argument(name, RotationArgumentConsumer), ArgumentType::ResourceLocation | ArgumentType::Resource(_) => { - argument(name, ResourceLocationArgumentConsumer) + WasmCommandNode::Argument(argument(name, IdentifierArgumentType)) + } + ArgumentType::EntityAnchor => { + WasmCommandNode::Argument(argument(name, EntityAnchorArgumentType)) + } + ArgumentType::Gamemode => { + WasmCommandNode::Argument(argument(name, GameModeArgumentType)) + } + ArgumentType::Difficulty => { + WasmCommandNode::Argument(argument(name, StringArgumentType::SingleWord)) + } + ArgumentType::Time(min) => { + WasmCommandNode::Argument(argument(name, TimeArgumentType::new(min.unwrap_or(0)))) } - ArgumentType::EntityAnchor => argument(name, EntityAnchorArgumentConsumer), - ArgumentType::Gamemode => argument(name, GamemodeArgumentConsumer), - ArgumentType::Difficulty => argument(name, DifficultyArgumentConsumer), - ArgumentType::Time(min) => argument(name, TimeArgumentConsumer::min(min.unwrap_or(0))), _ => { return Err(wasmtime::Error::msg(format!( "Unimplemented argument type: {arg_type:?}" @@ -613,7 +666,8 @@ impl pumpkin::plugin::command::HostCommandNode for PluginHostState { ) -> wasmtime::Result<()> { let child = self.take_node(&node)?; let parent = self.get_node_mut(&self_node)?; - let builder = std::mem::replace(&mut parent.provider, literal("")); + let builder = + std::mem::replace(&mut parent.provider, WasmCommandNode::Literal(literal(""))); parent.provider = builder.then(child.provider); Ok(()) } @@ -639,8 +693,11 @@ impl pumpkin::plugin::command::HostCommandNode for PluginHostState { server, }; let resource = self.get_node_mut(&node)?; - let builder = std::mem::replace(&mut resource.provider, literal("")); - resource.provider = builder.execute(executor); + let builder = std::mem::replace( + &mut resource.provider, + WasmCommandNode::Literal(literal("")), + ); + resource.provider = builder.executes(executor); Ok(()) } @@ -665,7 +722,10 @@ impl pumpkin::plugin::command::HostCommandNode for PluginHostState { server, }; let resource = self.get_node_mut(&node)?; - let builder = std::mem::replace(&mut resource.provider, literal("")); + let builder = std::mem::replace( + &mut resource.provider, + WasmCommandNode::Literal(literal("")), + ); resource.provider = builder.suggests(provider); Ok(()) } @@ -688,25 +748,6 @@ impl pumpkin::plugin::command::HostCommandNode for PluginHostState { } } -fn build_bounded_node( - name: String, - min: Option, - max: Option, -) -> NonLeafNodeBuilder -where - BoundedNumArgumentConsumer: GetClientSideArgParser, -{ - let mut consumer = BoundedNumArgumentConsumer::::new(); - if let Some(m) = min { - consumer = consumer.min(m); - } - if let Some(m) = max { - consumer = consumer.max(m); - } - - argument(name, consumer) -} - #[expect(clippy::too_many_lines)] const fn map_util_locale_to_wit(locale: pumpkin_util::translation::Locale) -> Locale { match locale { diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/context.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/context.rs index e7795c87a..ac355b6af 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/context.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/context.rs @@ -1764,14 +1764,22 @@ impl pumpkin::plugin::context::HostContext for PluginHostState { command: Resource, permission: String, ) -> wasmtime::Result<()> { - // Updated return type - // Use your helpers to safely take/get resources + use crate::command::argument_builder::ArgumentBuilder; + let command_res = self.take_command(&command)?; let context_res = self.get_context(&context)?; + let wasm_command = command_res.provider; + let aliases = if wasm_command.names.len() > 1 { + wasm_command.names[1..].to_vec() + } else { + Vec::new() + }; + + let node = wasm_command.builder.build(); context_res .provider - .register_command(command_res.provider, permission); + .register_command_with_aliases(node, &aliases, permission); Ok(()) } diff --git a/crates/pumpkin/src/server/mod.rs b/crates/pumpkin/src/server/mod.rs index 27ea05312..d032f674e 100644 --- a/crates/pumpkin/src/server/mod.rs +++ b/crates/pumpkin/src/server/mod.rs @@ -3,7 +3,7 @@ use crate::command::commands::default_dispatcher; use crate::command::commands::defaultgamemode::DefaultGamemode; use crate::data::VanillaData; use crate::data::player_server::ServerPlayerData; -use crate::entity::{EntityBase, NBTStorage}; +use crate::entity::NBTStorage; use crate::item::registry::ItemRegistry; use crate::net::authentication::fetch_mojang_public_keys; use crate::net::{ClientPlatform, DisconnectReason, EncryptionError, GameProfile, PlayerConfig}; @@ -22,7 +22,6 @@ use connection_cache::{CachedBranding, CachedStatus}; use key_store::KeyStore; use pumpkin_config::{AdvancedConfiguration, BasicConfiguration}; use pumpkin_data::dimension::Dimension; -use pumpkin_data::entity::EntityType; use pumpkin_util::permission::PermissionManager; use pumpkin_util::text::color::NamedColor; use pumpkin_world::dimension::into_level; @@ -30,7 +29,6 @@ use pumpkin_world::generation::generator::GeneratorInit; use pumpkin_world::world::WorldPortalExt; use tracing::{debug, error, info, warn}; -use crate::command::CommandSender; use pumpkin_protocol::java::client::login::CEncryptionRequest; use pumpkin_protocol::java::client::play::{CChangeDifficulty, CTabList}; use pumpkin_protocol::{ClientPacket, java::client::config::CPluginMessage}; @@ -40,10 +38,9 @@ use pumpkin_world::world_info::anvil::{ AnvilLevelInfo, LEVEL_DAT_BACKUP_FILE_NAME, LEVEL_DAT_FILE_NAME, }; use pumpkin_world::world_info::{LevelData, WorldInfoError, WorldInfoReader, WorldInfoWriter}; -use rand::seq::{IndexedRandom, SliceRandom}; +use rand::seq::IndexedRandom; use rayon::prelude::*; use rsa::RsaPublicKey; -use rustc_hash::FxHashSet; use std::fs; use std::net::IpAddr; use std::sync::Arc; @@ -65,9 +62,6 @@ pub mod ticker; pub use recipe::RecipeManager; -use crate::command::args::entities::{ - EntityFilter, EntityFilterSort, EntitySelectorType, TargetSelector, ValueCondition, -}; use crate::data::advancement_data::AdvancementManager; use crate::server::scheduler::TaskScheduler; @@ -122,6 +116,11 @@ pub struct Server { pub defaultgamemode: std::sync::Mutex, /// Manages player data storage pub player_data_storage: ServerPlayerData, + /// Command storage for `/data` command + pub command_storage: + std::sync::Mutex>, + /// Global server stopwatches + pub stopwatches: std::sync::Mutex, // Manages player advancement pub advancement_manager: Arc, // Whether the server whitelist is on or off @@ -142,6 +141,8 @@ pub struct Server { pub player_idle_timeout: AtomicI32, /// Manages scheduled tasks (e.g. from plugins) pub task_scheduler: Arc, + /// Manages scheduled datapack functions (`/schedule`) + pub scheduled_functions: Arc, tasks: TaskTracker, pub runtime: tokio::runtime::Handle, @@ -296,6 +297,8 @@ impl Server { map_manager: MapManager::new(), defaultgamemode, player_data_storage, + command_storage: std::sync::Mutex::new(std::collections::HashMap::new()), + stopwatches: std::sync::Mutex::new(crate::world::stopwatches::Stopwatches::new()), advancement_manager, white_list, tick_rate_manager, @@ -306,6 +309,7 @@ impl Server { tasks: TaskTracker::new(), runtime: tokio::runtime::Handle::current(), task_scheduler: Arc::new(TaskScheduler::new()), + scheduled_functions: Arc::new(crate::server::scheduler::ScheduledFunctionQueue::new()), server_guid: rand::random(), player_idle_timeout: AtomicI32::new(0), mojang_public_keys: ArcSwap::from_pointee(Vec::new()), @@ -787,6 +791,27 @@ impl Server { } } + pub fn broadcast_chat_message( + self: &Arc, + message: &crate::net::chat::PlayerChatMessage, + is_filtered: impl Fn(&crate::entity::player::Player) -> bool, + sender_player: Option<&Arc>, + chat_type: pumpkin_protocol::codec::var_int::VarInt, + sender_name: &TextComponent, + target_name: Option<&TextComponent>, + ) { + for world in self.worlds.load().iter() { + world.broadcast_chat_message( + message, + &is_filtered, + sender_player, + chat_type, + sender_name, + target_name, + ); + } + } + /// Gets the current difficulty of the server. pub fn get_difficulty(&self) -> Difficulty { self.level_info.load().difficulty @@ -1066,6 +1091,10 @@ impl Server { /// Ticks the game logic for all worlds. This is the part that is affected by `/tick freeze`. pub fn tick_worlds(self: &Arc) { self.task_scheduler.tick(self); + self.scheduled_functions.tick( + self, + self.tick_count.load(std::sync::atomic::Ordering::Relaxed) as u64, + ); let worlds = self.worlds.load(); let handle = self.runtime.clone(); @@ -1155,207 +1184,6 @@ impl Server { .unwrap_or_else(std::sync::PoisonError::into_inner) } - #[allow(clippy::too_many_lines, clippy::option_if_let_else)] - pub fn select_players( - &self, - target_selector: &TargetSelector, - source: Option<&CommandSender>, - ) -> Vec> { - let mut players = match &target_selector.selector_type { - EntitySelectorType::Source => source - .and_then(CommandSender::as_player) - .map_or_else(Vec::new, |player| vec![player]), - EntitySelectorType::NearestPlayer - | EntitySelectorType::NearestEntity - | EntitySelectorType::RandomPlayer - | EntitySelectorType::AllPlayers - | EntitySelectorType::AllEntities => self.get_all_players(), - EntitySelectorType::NamedPlayer(name) => self - .get_player_by_name(name) - .map_or_else(Vec::new, |player| vec![player]), - EntitySelectorType::Uuid(uuid) => self - .get_player_by_uuid(*uuid) - .map_or_else(Vec::new, |player| vec![player]), - }; - - let player_type = &EntityType::PLAYER; - let type_included = target_selector - .conditions - .iter() - .filter_map(|f| { - if let EntityFilter::Type(ValueCondition::Equals(entity_type)) = f { - Some(*entity_type) - } else { - None - } - }) - .collect::>(); - let type_excluded = target_selector - .conditions - .iter() - .filter_map(|f| { - if let EntityFilter::Type(ValueCondition::NotEquals(entity_type)) = f { - Some(*entity_type) - } else { - None - } - }) - .collect::>(); - - players.retain(|_| { - (type_excluded.is_empty() || !type_excluded.contains(player_type)) - && (type_included.is_empty() || type_included.contains(player_type)) - }); - - let limit = target_selector.get_limit(); - if limit == 0 { - return Vec::new(); - } - - match target_selector - .get_sort() - .unwrap_or(EntityFilterSort::Arbitrary) - { - EntityFilterSort::Arbitrary => players.into_iter().take(limit).collect(), - EntityFilterSort::Random => { - players.shuffle(&mut rand::rng()); - players.into_iter().take(limit).collect() - } - EntityFilterSort::Nearest | EntityFilterSort::Furthest => { - let center = source.and_then(CommandSender::position).unwrap_or_default(); - let nearest_first = target_selector - .get_sort() - .is_none_or(|sort| sort == EntityFilterSort::Nearest); - players.sort_by(|a, b| { - let a_distance = a.get_entity().pos.load().squared_distance_to_vec(¢er); - let b_distance = b.get_entity().pos.load().squared_distance_to_vec(¢er); - if nearest_first { - a_distance - .partial_cmp(&b_distance) - .unwrap_or(core::cmp::Ordering::Equal) - } else { - b_distance - .partial_cmp(&a_distance) - .unwrap_or(core::cmp::Ordering::Equal) - } - }); - players.into_iter().take(limit).collect() - } - } - } - - #[allow(clippy::too_many_lines, clippy::option_if_let_else)] - pub fn select_entities( - &self, - target_selector: &TargetSelector, - source: Option<&CommandSender>, - ) -> Vec> { - let all_entities_and_players = || { - let mut entities = Vec::new(); - for world in self.worlds.load().iter() { - entities.extend(world.entities.load().iter().cloned()); - entities.extend( - world - .players - .load() - .iter() - .cloned() - .map(|player| player as Arc), - ); - } - entities - }; - let all_players_as_entities = || { - self.get_all_players() - .into_iter() - .map(|player| player as Arc) - .collect::>() - }; - - let mut entities = match &target_selector.selector_type { - EntitySelectorType::Source => source - .and_then(CommandSender::as_player) - .map_or_else(Vec::new, |player| vec![player as Arc]), - EntitySelectorType::NearestPlayer - | EntitySelectorType::RandomPlayer - | EntitySelectorType::AllPlayers => all_players_as_entities(), - EntitySelectorType::NearestEntity | EntitySelectorType::AllEntities => { - all_entities_and_players() - } - EntitySelectorType::NamedPlayer(name) => self - .get_player_by_name(name) - .map_or_else(Vec::new, |player| vec![player as Arc]), - EntitySelectorType::Uuid(uuid) => self - .get_player_by_uuid(*uuid) - .map_or_else(Vec::new, |player| vec![player as Arc]), - }; - - let type_included = target_selector - .conditions - .iter() - .filter_map(|f| { - if let EntityFilter::Type(ValueCondition::Equals(entity_type)) = f { - Some(*entity_type) - } else { - None - } - }) - .collect::>(); - let type_excluded = target_selector - .conditions - .iter() - .filter_map(|f| { - if let EntityFilter::Type(ValueCondition::NotEquals(entity_type)) = f { - Some(*entity_type) - } else { - None - } - }) - .collect::>(); - entities.retain(|entity| { - // Filter by entity type - (type_excluded.is_empty() || !type_excluded.contains(&entity.get_entity().entity_type)) - && (type_included.is_empty() - || type_included.contains(&entity.get_entity().entity_type)) - }); - - let limit = target_selector.get_limit(); - if limit == 0 { - return vec![]; - } - - match target_selector - .get_sort() - .unwrap_or(EntityFilterSort::Arbitrary) - { - EntityFilterSort::Arbitrary => entities.into_iter().take(limit).collect(), - EntityFilterSort::Random => { - entities.shuffle(&mut rand::rng()); - entities.into_iter().take(limit).collect() - } - EntityFilterSort::Nearest | EntityFilterSort::Furthest => { - let center = source.and_then(CommandSender::position).unwrap_or_default(); - let nearest_first = target_selector - .get_sort() - .is_none_or(|sort| sort == EntityFilterSort::Nearest); - entities.sort_by(|a, b| { - let a_distance = a.get_entity().pos.load().squared_distance_to_vec(¢er); - let b_distance = b.get_entity().pos.load().squared_distance_to_vec(¢er); - if nearest_first { - a_distance - .partial_cmp(&b_distance) - .unwrap_or(core::cmp::Ordering::Equal) - } else { - b_distance - .partial_cmp(&a_distance) - .unwrap_or(core::cmp::Ordering::Equal) - } - }); - entities.into_iter().take(limit).collect() - } - } - } - pub async fn execute_remote_command(self: &Arc, command: String) { let mut remote_event = crate::plugin::api::events::server::remote_server_command::RemoteServerCommandEvent::new( command, diff --git a/crates/pumpkin/src/server/scheduler.rs b/crates/pumpkin/src/server/scheduler.rs index 9063a3fff..8603bf034 100644 --- a/crates/pumpkin/src/server/scheduler.rs +++ b/crates/pumpkin/src/server/scheduler.rs @@ -193,3 +193,124 @@ impl TaskScheduler { } } } + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ScheduledFunctionEvent { + pub id: String, + pub trigger_tick: u64, + pub function_name: String, + pub is_tag: bool, +} + +impl PartialOrd for ScheduledFunctionEvent { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ScheduledFunctionEvent { + fn cmp(&self, other: &Self) -> Ordering { + other.trigger_tick.cmp(&self.trigger_tick) + } +} + +#[derive(Default)] +pub struct ScheduledFunctionQueue { + queue: Mutex>, +} + +impl ScheduledFunctionQueue { + #[must_use] + pub const fn new() -> Self { + Self { + queue: Mutex::new(BinaryHeap::new()), + } + } + + pub fn schedule( + &self, + id: String, + trigger_tick: u64, + function_name: String, + is_tag: bool, + replace: bool, + ) { + let mut queue = self + .queue + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + if replace { + let mut retained = Vec::new(); + while let Some(event) = queue.pop() { + if event.id != id { + retained.push(event); + } + } + for event in retained { + queue.push(event); + } + } + queue.push(ScheduledFunctionEvent { + id, + trigger_tick, + function_name, + is_tag, + }); + } + + pub fn remove(&self, id: &str) -> usize { + let mut queue = self + .queue + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let mut count = 0; + let mut retained = Vec::new(); + while let Some(event) = queue.pop() { + if event.id == id { + count += 1; + } else { + retained.push(event); + } + } + for event in retained { + queue.push(event); + } + count + } + + #[must_use] + pub fn get_event_ids(&self) -> Vec { + let queue = self + .queue + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let mut ids: Vec = queue.iter().map(|e| e.id.clone()).collect(); + ids.sort(); + ids.dedup(); + ids + } + + pub fn tick(&self, server: &Arc, current_tick: u64) { + let mut to_run = Vec::new(); + { + let mut queue = self + .queue + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + while let Some(event) = queue.peek() { + if event.trigger_tick > current_tick { + break; + } + if let Some(event) = queue.pop() { + to_run.push(event); + } + } + } + for event in to_run { + let _ = crate::data::datapack::DatapackManager::execute_function_from_console( + server, + &event.function_name, + ); + } + } +} diff --git a/crates/pumpkin/src/world/custom_bossbar.rs b/crates/pumpkin/src/world/custom_bossbar.rs index 594a787d9..a64487f0d 100644 --- a/crates/pumpkin/src/world/custom_bossbar.rs +++ b/crates/pumpkin/src/world/custom_bossbar.rs @@ -1,4 +1,3 @@ -use crate::command::args::GetCloned; use crate::entity::player::Player; use crate::server::Server; use crate::world::bossbar::{Bossbar, BossbarColor, BossbarDivisions}; @@ -113,7 +112,7 @@ impl CustomBossbars { server: &Server, resource_location: String, ) -> Result<(), BossbarUpdateError> { - let bossbar = self.custom_bossbars.get_cloned(&resource_location); + let bossbar = self.custom_bossbars.get(&resource_location).cloned(); if let Some(bossbar) = bossbar { self.custom_bossbars.remove(&resource_location); diff --git a/crates/pumpkin/src/world/mod.rs b/crates/pumpkin/src/world/mod.rs index f554efd5d..3d834016a 100644 --- a/crates/pumpkin/src/world/mod.rs +++ b/crates/pumpkin/src/world/mod.rs @@ -23,6 +23,7 @@ pub mod loot; pub mod map; pub mod portal; pub mod raid; +pub mod stopwatches; pub mod time; pub mod villager_poi; @@ -117,9 +118,8 @@ use pumpkin_protocol::{ self, client::play::{ CBlockEntityData, CDamageEvent, CEntityStatus, CGameEvent, CLogin, CMultiBlockUpdate, - CPlayerChatMessage, CPlayerInfoUpdate, CRemoveEntities, CRemovePlayerInfo, - CSetSelectedSlot, CSoundEffect, CSpawnEntity, FilterType, GameEvent, InitChat, - PlayerAction, PlayerInfoFlags, + CPlayerInfoUpdate, CRemoveEntities, CRemovePlayerInfo, CSetSelectedSlot, CSoundEffect, + CSpawnEntity, GameEvent, InitChat, PlayerAction, PlayerInfoFlags, }, server::play::SChatMessage, }, @@ -968,7 +968,35 @@ impl World { ); } - pub async fn broadcast_secure_player_chat( + pub fn broadcast_chat_message( + &self, + message: &crate::net::chat::PlayerChatMessage, + is_filtered: impl Fn(&Player) -> bool, + sender_player: Option<&Arc>, + chat_type: VarInt, + sender_name: &TextComponent, + target_name: Option<&TextComponent>, + ) { + let tracked = crate::net::chat::OutgoingChatMessage::create(message.clone()); + let mut was_fully_filtered = false; + + let players = self.players.load(); + for player in players.iter() { + let filtered = is_filtered(player); + tracked.send_to_player(player, filtered, chat_type, sender_name, target_name); + was_fully_filtered |= filtered && message.is_fully_filtered(); + } + + if was_fully_filtered && let Some(sender) = sender_player { + let filter_notice = + TextComponent::translate(pumpkin_data::translation::java::CHAT_FILTERED_FULL, []) + .color_named(pumpkin_util::text::color::NamedColor::Red) + .italic(); + sender.send_system_message(&filter_notice); + } + } + + pub fn broadcast_secure_player_chat( &self, sender: &Arc, chat_message: &SChatMessage<'_>, @@ -984,79 +1012,36 @@ impl World { .signature_cache .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); - cache.last_seen.clone() + cache.last_seen.as_ref().to_vec() }; - for recipient in self.players.load().iter() { - let messages_received: i32 = recipient - .chat_session - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .messages_received; - let packet = &CPlayerChatMessage::new( - VarInt(messages_received), - sender.gameprofile.id, - VarInt(messages_sent), - chat_message.signature.map(std::convert::Into::into), - chat_message.message.into(), - chat_message.timestamp, - chat_message.salt, - sender_last_seen.indexed_for(recipient), - Some(decorated_message.clone()), - FilterType::PassThrough, - (RAW + 1).into(), // Custom registry chat_type with no sender name - TextComponent::empty(), // Not needed since we're injecting the name in the message for custom formatting - None, - ); - let packet_data = recipient.client.java().map_or_else( - || { - JavaClient::serialize_packet_for_version( - packet, - recipient.client.java_version(), - ) - }, - |j| j.serialize_packet(packet), - ); - if let Ok(data) = packet_data { - recipient.client.enqueue_packet(data).await; - } + let link = crate::net::chat::SignedMessageLink::new( + messages_sent, + sender.gameprofile.id, + Uuid::nil(), + ); + let signed_body = crate::net::chat::SignedMessageBody::new( + chat_message.message.to_string(), + chat_message.timestamp, + chat_message.salt, + sender_last_seen, + ); + let player_chat_msg = crate::net::chat::PlayerChatMessage::new( + link, + chat_message.signature.map(std::convert::Into::into), + signed_body, + Some(decorated_message.clone()), + crate::net::chat::FilterMask::PassThrough, + ); - if let Some(signature) = chat_message.signature { - let mut cache = recipient - .signature_cache - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - cache.add_seen_signature(signature); - cache.last_seen_validator.add_pending(signature); - let tracked_count = cache.last_seen_validator.tracked_messages_count(); - drop(cache); - - if tracked_count > 4096 { - recipient.kick( - crate::net::DisconnectReason::Kicked, - &TextComponent::translate_cross( - pumpkin_data::translation::java::MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, - pumpkin_data::translation::java::MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, - [], - ), - ); - } - } - - if recipient.gameprofile.id != sender.gameprofile.id { - // Sender may update recipient on signatures recipient hasn't seen - recipient - .signature_cache - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .cache_signatures(sender_last_seen.as_ref()); - } - recipient - .chat_session - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .messages_received += 1; - } + self.broadcast_chat_message( + &player_chat_msg, + Player::is_text_filtering_enabled, + Some(sender), + (RAW + 1).into(), + &TextComponent::empty(), + None, + ); sender .chat_session diff --git a/crates/pumpkin/src/world/stopwatches.rs b/crates/pumpkin/src/world/stopwatches.rs new file mode 100644 index 000000000..9b50cce01 --- /dev/null +++ b/crates/pumpkin/src/world/stopwatches.rs @@ -0,0 +1,168 @@ +use std::collections::HashMap; +use std::time::{SystemTime, UNIX_EPOCH}; + +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_nbt::tag::NbtTag; + +/// A single named stopwatch tracking elapsed time. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Stopwatch { + pub start_time: u64, + pub accumulated_elapsed_time: u64, +} + +impl Stopwatch { + #[must_use] + pub const fn new(current_time: u64) -> Self { + Self { + start_time: current_time, + accumulated_elapsed_time: 0, + } + } + + #[must_use] + pub const fn new_with_accumulated(current_time: u64, accumulated_elapsed_time: u64) -> Self { + Self { + start_time: current_time, + accumulated_elapsed_time, + } + } + + #[must_use] + pub const fn elapsed_milliseconds(&self, current_time: u64) -> u64 { + current_time + .saturating_sub(self.start_time) + .saturating_add(self.accumulated_elapsed_time) + } + + #[must_use] + pub fn elapsed_seconds(&self, current_time: u64) -> f64 { + self.elapsed_milliseconds(current_time) as f64 / 1000.0 + } +} + +/// Persistent manager for all server stopwatches. +#[derive(Debug, Clone, Default)] +pub struct Stopwatches { + stopwatches: HashMap, + dirty: bool, +} + +impl Stopwatches { + #[must_use] + pub fn new() -> Self { + Self { + stopwatches: HashMap::new(), + dirty: false, + } + } + + #[must_use] + pub fn current_time() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as u64 + } + + #[must_use] + pub fn get(&self, id: &str) -> Option<&Stopwatch> { + self.stopwatches.get(id) + } + + pub fn add(&mut self, id: String, stopwatch: Stopwatch) -> bool { + if let std::collections::hash_map::Entry::Vacant(e) = self.stopwatches.entry(id) { + e.insert(stopwatch); + self.dirty = true; + true + } else { + false + } + } + + pub fn update(&mut self, id: &str, update: impl FnOnce(&Stopwatch) -> Stopwatch) -> bool { + if let Some(stopwatch) = self.stopwatches.get_mut(id) { + *stopwatch = update(stopwatch); + self.dirty = true; + true + } else { + false + } + } + + pub fn remove(&mut self, id: &str) -> bool { + let removed = self.stopwatches.remove(id).is_some(); + if removed { + self.dirty = true; + } + removed + } + + #[must_use] + pub fn ids(&self) -> Vec { + let mut ids: Vec = self.stopwatches.keys().cloned().collect(); + ids.sort(); + ids + } + + #[must_use] + pub fn is_dirty(&self) -> bool { + self.dirty || !self.stopwatches.is_empty() + } + + pub const fn set_dirty(&mut self) { + self.dirty = true; + } + + #[must_use] + pub fn unpack(map: &HashMap) -> Self { + let mut result = Self::new(); + let now = Self::current_time(); + for (id, accumulated) in map { + result.stopwatches.insert( + id.clone(), + Stopwatch::new_with_accumulated(now, *accumulated), + ); + } + result + } + + #[must_use] + pub fn pack(&self) -> HashMap { + let now = Self::current_time(); + let mut result = HashMap::new(); + for (id, stopwatch) in &self.stopwatches { + result.insert(id.clone(), stopwatch.elapsed_milliseconds(now)); + } + result + } + + #[must_use] + pub fn to_nbt(&self) -> NbtCompound { + let mut stopwatches_tag = NbtCompound::new(); + let now = Self::current_time(); + for (id, stopwatch) in &self.stopwatches { + stopwatches_tag.put_long(id, stopwatch.elapsed_milliseconds(now) as i64); + } + let mut data = NbtCompound::new(); + data.put_compound("stopwatches", stopwatches_tag); + data + } + + #[must_use] + pub fn from_nbt(nbt: &NbtCompound) -> Self { + let mut result = Self::new(); + let now = Self::current_time(); + if let Some(NbtTag::Compound(stopwatches_tag)) = nbt.get("stopwatches") { + for (id, tag) in &stopwatches_tag.child_tags { + if let NbtTag::Long(accumulated) = tag { + result.stopwatches.insert( + id.to_string(), + Stopwatch::new_with_accumulated(now, *accumulated as u64), + ); + } + } + } + result + } +}