chore: port to new command system

This commit is contained in:
Alexander Medvedev 2026-08-30 23:10:34 +02:00
parent 544bfc21d5
commit e393751b8d
142 changed files with 12194 additions and 13278 deletions

View File

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

View File

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

View File

@ -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::<SignBlockEntity>() 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::<SignBlockEntity>() 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::<HoneyCombItem>()
.map_or_else(
|| {
pumpkin_item
.as_any()
.downcast_ref::<GlowingInkSacItem>()
.map_or_else(
|| {
if let Some(ink_sac_item) =
pumpkin_item.as_any().downcast_ref::<InkSacItem>()
{
ink_sac_item.apply_to_sign(&args, &block_entity, text)
} else if let Some(dye) =
pumpkin_item.as_any().downcast_ref::<DyeItem>()
{
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::<HoneyCombItem>()
|| pumpkin_item.as_any().is::<GlowingInkSacItem>()
|| pumpkin_item.as_any().is::<InkSacItem>()
|| pumpkin_item.as_any().is::<DyeItem>();
if result == BlockActionResult::Success {
if pumpkin_item
.as_any()
.downcast_ref::<crate::item::items::glowing_ink_sac::GlowingInkSacItem>()
.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::<HoneyCombItem>()
{
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::<GlowingInkSacItem>()
{
g_ink_sac_item.apply_to_sign(&args, &block_entity, text)
} else if let Some(ink_sac_item) = pumpkin_item.as_any().downcast_ref::<InkSacItem>() {
ink_sac_item.apply_to_sign(&args, &block_entity, text)
} else if let Some(dye) = pumpkin_item.as_any().downcast_ref::<DyeItem>() {
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::<GlowingInkSacItem>() {
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<Mutex<Option<Uuid>>>,
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<Mutex<Option<Uuid>>>,
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::<TextComponent>(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<World>,
player: &Arc<Player>,
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::<TextComponent>(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<BlockDirection> {
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: &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
}

View File

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

View File

@ -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<Mutex<Option<uuid::Uuid>>>,
}
pub struct Text {
pub type Text = SignText;
pub struct SignText {
pub has_glowing_text: AtomicBool,
color: AtomicI8,
pub messages: Arc<std::sync::Mutex<[Box<str>; 4]>>,
pub messages: Arc<Mutex<[Box<str>; 4]>>,
pub filtered_messages: Arc<Mutex<[Box<str>; 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<Text> for NbtTag {
fn from(value: Text) -> Self {
impl From<SignText> 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<NbtTag> for Text {
impl From<NbtTag> for SignText {
fn from(tag: NbtTag) -> Self {
let Some(nbt) = tag.extract_compound() else {
return Self::default();
@ -82,41 +102,155 @@ impl From<NbtTag> for Text {
let get_message =
|i: usize| -> Box<str> { 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<Vec<Box<str>>> =
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<str> {
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<str>; 4]) -> Self {
pub fn empty_messages() -> [Box<str>; 4] {
[Box::from(""), Box::from(""), Box::from(""), Box::from("")]
}
#[must_use]
pub fn new(
messages: [Box<str>; 4],
filtered_messages: Option<[Box<str>; 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<str>; 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<str> {
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<str>, filtered_message: Option<Box<str>>) {
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<str>; 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::<TextComponent>(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<str>; 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<Self> {
entity
.as_any()
.downcast_ref::<SignBlockEntity>()
.map(Self::Sign)
.or_else(|| {
entity
.as_any()
.downcast_ref::<super::hanging_sign::HangingSignBlockEntity>()
.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<Mutex<Option<uuid::Uuid>>> {
match self {
Self::Sign(s) => &s.currently_editing_player,
Self::Hanging(s) => &s.currently_editing_player,
}
}
}

View File

@ -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<SuggestionProviders> {
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<Self::Data, CommandError> {
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<u16>),
Block(BlockId),
}
impl GetClientSideArgParser for BlockPredicateArgumentConsumer {
fn get_client_side_parser(&self) -> ArgumentType {
ArgumentType::BlockPredicate
}
fn get_client_side_suggestion_type_override(&self) -> Option<SuggestionProviders> {
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<BlockPredicate>;
fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result<Self::Data, CommandError> {
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),
}
}
}

View File

@ -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<SuggestionProviders> {
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<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::Bool(data)) => Ok(*data),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}

View File

@ -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<SuggestionProviders> {
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<CommandSuggestion> = 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<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::BossbarColor(data)) => Ok(data),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}

View File

@ -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<SuggestionProviders> {
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<CommandSuggestion> = 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<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::BossbarStyle(data)) => Ok(data),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}

View File

@ -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<T: ToFromNumber> {
min_inclusive: Option<T>,
max_inclusive: Option<T>,
name: Option<&'static str>,
}
impl<T: ToFromNumber> ArgumentConsumer for BoundedNumArgumentConsumer<T>
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::<T>().ok()?
.and_then(|s| s.parse::<T>().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<T> {
type Data = Result<T, NotInBounds>;
fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result<Self::Data, CommandError> {
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<NotInBounds> 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<T: ToFromNumber> BoundedNumArgumentConsumer<T> {
#[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<Self>;
}
impl<T: ToFromNumber> Default for BoundedNumArgumentConsumer<T> {
fn default() -> Self {
Self::new()
}
}
impl ToFromNumber for f64 {
fn to_number(self) -> Number {
Number::F64(self)
}
fn from_number(arg: &Number) -> Option<Self> {
match arg {
Number::F64(x) => Some(*x),
_ => None,
}
}
}
impl GetClientSideArgParser for BoundedNumArgumentConsumer<f64> {
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<pumpkin_protocol::java::client::play::SuggestionProviders> {
None
}
}
impl ToFromNumber for f32 {
fn to_number(self) -> Number {
Number::F32(self)
}
fn from_number(arg: &Number) -> Option<Self> {
match arg {
Number::F32(x) => Some(*x),
_ => None,
}
}
}
impl GetClientSideArgParser for BoundedNumArgumentConsumer<f32> {
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<pumpkin_protocol::java::client::play::SuggestionProviders> {
None
}
}
impl ToFromNumber for i32 {
fn to_number(self) -> Number {
Number::I32(self)
}
fn from_number(arg: &Number) -> Option<Self> {
match arg {
Number::I32(x) => Some(*x),
_ => None,
}
}
}
impl GetClientSideArgParser for BoundedNumArgumentConsumer<i32> {
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<pumpkin_protocol::java::client::play::SuggestionProviders> {
None
}
}
impl ToFromNumber for i64 {
fn to_number(self) -> Number {
Number::I64(self)
}
fn from_number(arg: &Number) -> Option<Self> {
match arg {
Number::I64(x) => Some(*x),
_ => None,
}
}
}
impl GetClientSideArgParser for BoundedNumArgumentConsumer<i64> {
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<pumpkin_protocol::java::client::play::SuggestionProviders> {
None
}
}
impl<T: ToFromNumber> DefaultNameArgConsumer for BoundedNumArgumentConsumer<T>
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.")
}
}

View File

@ -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<SuggestionProviders> {
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<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::CommandTree(tree)) => Ok(tree),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}

View File

@ -1,75 +0,0 @@
use std::str::FromStr;
pub enum MaybeRelativeCoordinate<const IS_Y: bool> {
Absolute(f64),
Relative(f64),
}
impl<const IS_Y: bool> TryFrom<&str> for MaybeRelativeCoordinate<IS_Y> {
type Error = <f64 as FromStr>::Err;
fn try_from(s: &str) -> Result<Self, Self::Error> {
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<const IS_Y: bool> MaybeRelativeCoordinate<IS_Y> {
pub fn into_absolute(self, origin: Option<f64>) -> Option<f64> {
match self {
Self::Absolute(v) => Some(v),
Self::Relative(offset) => Some(origin? + offset),
}
}
}
#[derive(Debug)]
pub enum MaybeRelativeBlockCoordinate<const IS_Y: bool> {
Absolute(i32),
Relative(f64),
}
#[derive(Debug)]
pub enum ParseMaybeRelativeBlockCoordinateError {
InvalidAbsolute,
InvalidRelative,
}
impl<const IS_Y: bool> TryFrom<&str> for MaybeRelativeBlockCoordinate<IS_Y> {
type Error = ParseMaybeRelativeBlockCoordinateError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
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<const IS_Y: bool> MaybeRelativeBlockCoordinate<IS_Y> {
pub fn into_absolute(self, origin: Option<f64>) -> Option<i32> {
Some(match self {
Self::Absolute(v) => v,
Self::Relative(offset) => (origin? + offset).floor() as i32,
})
}
}

View File

@ -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<SuggestionProviders> {
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<CommandSuggestion> = 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<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::Difficulty(data)) => Ok(*data),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}

View File

@ -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<T> {
Equals(T),
NotEquals(T),
}
pub enum ComparableValueCondition<T> {
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<f64>),
Y(ComparableValueCondition<f64>),
Z(ComparableValueCondition<f64>),
Distance(ComparableValueCondition<f64>),
Dx(ComparableValueCondition<f64>),
Dy(ComparableValueCondition<f64>),
Dz(ComparableValueCondition<f64>),
XRotation(ComparableValueCondition<f64>),
YRotation(ComparableValueCondition<f64>),
Score(ComparableValueCondition<i32>),
Tag(ValueCondition<String>),
Team(ValueCondition<String>),
Name(ValueCondition<String>),
Type(ValueCondition<&'static EntityType>),
Nbt(NbtCompound),
Gamemode(ValueCondition<GameMode>),
Limit(usize),
Sort(EntityFilterSort),
}
impl FromStr for EntityFilter {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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::<usize>()
.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}")),
}
}
}
/// <https://minecraft.wiki/w/Target_selectors>
pub struct TargetSelector {
pub selector_type: EntitySelectorType,
pub conditions: Vec<EntityFilter>,
}
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<EntityFilterSort> {
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<Self, Self::Err> {
parse_target_selector(arg).map_err(|error| error.message)
}
}
#[derive(Debug)]
struct TargetSelectorParseError {
message: String,
cursor: usize,
}
fn parse_target_selector(arg: &str) -> Result<TargetSelector, TargetSelectorParseError> {
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<SuggestionProviders> {
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::<TargetSelector>() {
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<dyn EntityBase>];
fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result<Self::Data, CommandError> {
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<TargetSelector, CommandSyntaxError> {
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::<TargetSelector>()
.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::<TargetSelector>()
.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::<TargetSelector>()
.expect("should parse name selectors");
assert_eq!(s.conditions.len(), 2);
// Test tag selector
let s = "@e[tag=admin,tag=!vip]"
.parse::<TargetSelector>()
.expect("should parse tag selectors");
assert_eq!(s.conditions.len(), 2);
// Test team selector
let s = "@e[team=red,team=!blue]"
.parse::<TargetSelector>()
.expect("should parse team selectors");
assert_eq!(s.conditions.len(), 2);
// Test scores selector
let s = "@e[scores={kills=1..,deaths=..5}]"
.parse::<TargetSelector>()
.expect("should parse scores selectors");
assert_eq!(s.conditions.len(), 1);
// Test advancements selector
let s = "@e[advancements={story/mine_stone=true}]"
.parse::<TargetSelector>()
.expect("should parse advancements selectors");
assert_eq!(s.conditions.len(), 1);
// Test NBT selector
let s = "@e[nbt={OnGround:1b}]"
.parse::<TargetSelector>()
.expect("should parse NBT selectors");
assert_eq!(s.conditions.len(), 1);
}
}

View File

@ -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<SuggestionProviders> {
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::<TargetSelector>() {
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<dyn EntityBase>;
fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::Entity(data)) => Ok(data.clone()),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}

View File

@ -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<SuggestionProviders> {
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<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::EntityAnchor(anchor)) => Ok(*anchor),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}

View File

@ -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<SuggestionProviders> {
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::<i8>()
&& let Ok(gamemode) = GameMode::try_from(id)
{
return Some(Arg::GameMode(gamemode));
}
GameMode::from_str(s).map(Arg::GameMode).ok() // Convert Result to Option<T>
})
}
}
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<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::GameMode(data)) => Ok(*data),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}

View File

@ -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<SuggestionProviders> {
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<Self::Data, CommandError> {
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<Vec<GameProfile>, 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<GameProfile> {
{
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<GameProfile> {
{
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<String>, name: String) {
if names
.iter()
.any(|known_name| known_name.eq_ignore_ascii_case(&name))
{
return;
}
names.push(name);
}
fn selector_suggestions() -> Vec<CommandSuggestion> {
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);
}
}

View File

@ -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<u32> {
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<SuggestionProviders> {
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<Self::Data, CommandError> {
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);
}
}

View File

@ -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<SuggestionProviders> {
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<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::Msg(data)) => Ok(data),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}

View File

@ -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<Arg<'a>>;
pub type ConsumeResultWithSyntax<'a> = Result<Option<Arg<'a>>, CommandSyntaxError>;
pub type SuggestResult = Result<Option<Vec<CommandSuggestion>>, 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<SuggestionProviders>;
}
pub trait DefaultNameArgConsumer: ArgumentConsumer {
fn default_name(&self) -> &str;
}
#[derive(Clone)]
pub enum Arg<'a> {
Entities(Vec<Arc<dyn EntityBase>>),
Entity(Arc<dyn EntityBase>),
Players(Vec<Arc<Player>>),
GameProfiles(Vec<crate::net::GameProfile>),
BlockPos(BlockPos),
Pos3D(Vector3<f64>),
Pos2D(Vector2<f64>),
/// (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<Number, NotInBounds>),
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<K, V: Clone, S: ::std::hash::BuildHasher> {
fn get_cloned(&self, key: &K) -> Option<V>;
}
impl<K: Eq + Hash, V: Clone, S: ::std::hash::BuildHasher> GetCloned<K, V, S> for HashMap<K, V, S> {
fn get_cloned(&self, key: &K) -> Option<V> {
self.get(key).cloned()
}
}
pub trait FindArg<'a> {
type Data;
fn find_arg(args: &'a ConsumedArgs, name: &str) -> Result<Self::Data, CommandError>;
}
pub trait FindArgDefaultName<'a, T> {
fn find_arg_default_name(&self, args: &'a ConsumedArgs) -> Result<T, CommandError>;
}
impl<'a, T, C: FindArg<'a, Data = T> + DefaultNameArgConsumer> FindArgDefaultName<'a, T> for C {
fn find_arg_default_name(&self, args: &'a ConsumedArgs) -> Result<T, CommandError> {
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<Item = &'a str>;
}
impl<'a> SplitSingleWhitespaceIncludingEmptyParts<'a> for &'a str {
fn split_single_whitespace_including_empty_parts(self) -> impl Iterator<Item = &'a str> {
SplitSingleWhitespaceIncludingEmptyPartsIter {
s: self,
pos: 0,
chars_iter: self.char_indices(),
is_complete: false,
}
}
}
struct SplitSingleWhitespaceIncludingEmptyPartsIter<'a, T: Iterator<Item = (usize, char)>> {
s: &'a str,
pos: usize,
chars_iter: T,
is_complete: bool,
}
impl<'a, T: DoubleEndedIterator<Item = (usize, char)>> Iterator
for SplitSingleWhitespaceIncludingEmptyPartsIter<'a, T>
{
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
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]);
}
}
}
}
}

View File

@ -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<SuggestionProviders> {
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<Vec<Arc<Player>>> = 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<Player>];
fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::Players(data)) => Ok(data),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}

View File

@ -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<SuggestionProviders> {
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<false>,
MaybeRelativeCoordinate<false>,
);
impl MaybeRelativePosition2D {
fn try_new(x: &str, z: &str) -> Option<Self> {
Some(Self(x.try_into().ok()?, z.try_into().ok()?))
}
fn try_to_absolute(self, origin: Option<Vector3<f64>>) -> Option<Vector2<f64>> {
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<f64>;
fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::Pos2D(data)) => Ok(*data),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}

View File

@ -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<SuggestionProviders> {
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<false>,
MaybeRelativeCoordinate<true>,
MaybeRelativeCoordinate<false>,
);
impl MaybeRelativePosition3D {
fn try_new(x: &str, y: &str, z: &str) -> Option<Self> {
Some(Self(
x.try_into().ok()?,
y.try_into().ok()?,
z.try_into().ok()?,
))
}
fn try_to_absolute(self, origin: Option<Vector3<f64>>) -> Option<Vector3<f64>> {
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<f64>;
fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::Pos3D(data)) => Ok(*data),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}

View File

@ -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<SuggestionProviders> {
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<false>,
MaybeRelativeBlockCoordinate<true>,
MaybeRelativeBlockCoordinate<false>,
),
Local(f64, f64, f64),
}
impl MaybeRelativeBlockPos {
fn try_new(x: &str, y: &str, z: &str) -> Option<Self> {
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<BlockPos> {
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<f64> {
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<f64>) -> Vector3<f64> {
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<Self::Data, CommandError> {
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<BlockPos, CommandError> {
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<BlockPos, CommandError> {
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)
}
}

View File

@ -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<SuggestionProviders> {
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<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::DamageType(data)) => Ok(data),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}

View File

@ -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<SuggestionProviders> {
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<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::Effect(data)) => Ok(data),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}

View File

@ -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<SuggestionProviders> {
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<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::Enchantment(data)) => Ok(data),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}

View File

@ -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<SuggestionProviders> {
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<Self::Data, CommandError> {
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<u16>),
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<SuggestionProviders> {
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<Self::Data, CommandError> {
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());
}
}

View File

@ -1,5 +0,0 @@
pub mod damage_type;
pub mod effect;
pub mod enchantment;
pub mod item;
pub mod particle;

View File

@ -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<SuggestionProviders> {
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<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::Particle(data)) => Ok(data),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}

View File

@ -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<SuggestionProviders> {
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<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::ResourceLocation(data)) => Ok(data),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}

View File

@ -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::<f32>().ok()?
};
Some((value, true))
} else {
// Absolute rotation
let mut value = s.parse::<f32>().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<SuggestionProviders> {
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<Self::Data, CommandError> {
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()))),
}
}
}

View File

@ -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<SuggestionProviders> {
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<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::Simple(data)) => Ok(data),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}

View File

@ -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<SuggestionProviders> {
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<Self::Data, CommandError> {
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<SuggestionProviders> {
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<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::Slots(slots, name)) => Ok((*slots, name.clone())),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}

View File

@ -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<SuggestionProviders> {
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<Self::Data, CommandError> {
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()))),
}
}
}

View File

@ -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<SuggestionProviders> {
// 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<CommandSuggestion> = 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<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::SoundCategory(data)) => Ok(data),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}

View File

@ -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<SuggestionProviders> {
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<Self::Data, CommandError> {
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()))),
}
}
}

View File

@ -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<SuggestionProviders> {
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<CommandSuggestion> = 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<Self::Data, CommandError> {
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(()));
}
}

View File

@ -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<SuggestionProviders> {
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<Self::Data, CommandError> {
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<TextComponent> {
serde_json::from_str(input)
.map_err(|e| debug!("Failed to parse text component: {e}"))
.ok()
}

View File

@ -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<SuggestionProviders> {
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<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::Time(ticks)) => Ok(*ticks),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}

View File

@ -113,6 +113,7 @@ pub struct CommandArgumentBuilder {
common: CommonArgumentBuilder,
literal: Cow<'static, str>,
description: Cow<'static, str>,
source: Option<String>,
}
/// 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<String>) -> Self {
self.source = Some(source.into());
self
}
}
impl RequiredArgumentBuilder {
@ -408,6 +417,7 @@ impl ArgumentBuilder<CommandDetachedNode> for CommandArgumentBuilder {
self.common.modifier,
self.common.forks,
);
node.meta.source = self.source;
node.children = self.common.arguments;
node
}

View File

@ -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<String, String>,
nbt: Option<NbtCompound>,
},
Tag {
tag_name: String,
block_ids: Vec<u16>,
properties: HashMap<String, String>,
nbt: Option<NbtCompound>,
},
}
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<HashMap<String, String>, 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<Option<NbtCompound>, 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<Self::Item, CommandSyntaxError> {
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<String> {
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<BlockPredicate, CommandSyntaxError> {
Ok(context.get_argument::<BlockPredicate>(name)?.clone())
}
}

View File

@ -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<Self::Item, CommandSyntaxError> {
let remaining = reader.remaining_part();
if let Ok(comp) = serde_json::from_str::<TextComponent>(remaining) {
reader.set_cursor(reader.string().len());
return Ok(comp);
}
let s = reader.read_string()?;
if let Ok(comp) = serde_json::from_str::<TextComponent>(&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<TextComponent, CommandSyntaxError> {
context.get_argument::<TextComponent>(name).cloned()
}
}

View File

@ -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<Self::Item, CommandSyntaxError> {
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<String> {
examples!("0", "~", "~-5")
}
}
impl AngleArgumentType {
pub fn get(context: &CommandContext, name: &str) -> Result<Angle, CommandSyntaxError> {
context.get_argument::<Angle>(name).copied()
}
}

View File

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

View File

@ -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<f64>) -> Vector3<f64> {
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<Self::Item, CommandSyntaxError> {
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<String> {
vec!["xyz".to_string(), "x".to_string()]
}
}
impl SwizzleArgumentType {
pub fn get(context: &CommandContext, name: &str) -> Result<Swizzle, CommandSyntaxError> {
Ok(*context.get_argument::<Swizzle>(name)?)
}
}

View File

@ -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<Self::Item, CommandSyntaxError> {
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<String> {
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<Coordinates, CommandSyntaxError> {
Ok(*context.get_argument(name)?)
}
pub fn get_vector2(
context: &CommandContext,
name: &str,
) -> Result<Vector2<f64>, CommandSyntaxError> {
let resolved = Self::get_coordinates(context, name)?.resolve(context.source.as_ref());
Ok(Vector2::new(resolved.x, resolved.z))
}
}

View File

@ -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<String> {
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);
}
}

View File

@ -161,11 +161,10 @@ impl EntitySelector {
source: &CommandSource,
) -> Result<Arc<Player>, 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()),
}
}

View File

@ -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<Identifier, CommandSyntaxError> {
Ok(context.get_argument::<Identifier>(name)?.clone())
}
}

View File

@ -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<Self::Item, CommandSyntaxError> {
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<ItemStack, CommandSyntaxError> {
context.get_argument::<ItemStack>(name).cloned()
}
}

View File

@ -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<u16>),
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<Self::Item, CommandSyntaxError> {
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<String> {
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<ItemPredicate, CommandSyntaxError> {
Ok(context.get_argument::<ItemPredicate>(name)?.clone())
}
}

View File

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

View File

@ -35,9 +35,7 @@ impl ArgumentType for NbtTagArgumentType {
}
fn examples(&self) -> Vec<String> {
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};

View File

@ -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<NbtTag>) {
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<NbtPathNode>,
node_to_original_position: Vec<usize>,
}
impl NbtPath {
#[must_use]
pub fn new(
original: String,
nodes: Vec<NbtPathNode>,
node_to_original_position: Vec<usize>,
) -> 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<Vec<NbtTag>, CommandSyntaxError> {
let mut current = vec![tag.clone()];
for (i, node) in self.nodes.iter().enumerate() {
let mut next = Vec::new();
for parent in &current {
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 &current {
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<Vec<*mut NbtTag>, 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 &current {
// 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<i32, CommandSyntaxError> {
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<i32, CommandSyntaxError> {
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<Self::Item, CommandSyntaxError> {
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<String> {
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<NbtPathNode, CommandSyntaxError> {
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<NbtPathNode, CommandSyntaxError> {
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<String, CommandSyntaxError> {
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<NbtCompound, CommandSyntaxError> {
match SnbtParser::parse_for_commands(reader)? {
NbtTag::Compound(compound) => Ok(compound),
_ => Err(ERROR_INVALID_NODE.create(reader)),
}
}

View File

@ -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<Self::Item, CommandSyntaxError> {
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<String> {
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<Particle, CommandSyntaxError> {
Ok(*context.get_argument::<Particle>(name)?)
}
}

View File

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

View File

@ -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<Self::Item, CommandSyntaxError> {
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<SoundCategory, CommandSyntaxError> {
context.get_argument::<SoundCategory>(name).copied()
}
}

View File

@ -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<Player>,
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<ProtoNode> = 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<Self>,
node_type: ProtoNodeType<'a>,
}
impl<'a> ProtoNodeBuilder<'a> {
fn build(self, buffer: &mut Vec<ProtoNode<'a>>) -> 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<ProtoNodeBuilder<'a>>) {
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<String>,
enums: &'a mut Vec<EnumData>,
}
#[expect(clippy::too_many_lines)]
pub fn send_bedrock_commands_packet(
player: &Arc<Player>,
server: &Server,
_server: &Server,
dispatcher: &CommandDispatcher,
) {
let cmd_src = super::CommandSender::Player(player.clone());
let mut enum_values: Vec<String> = Vec::new();
let mut enums: Vec<EnumData> = Vec::new();
let mut commands: Vec<CommandData> = 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<NodeId> = 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<OverloadData> {
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<ParamData>,
overloads: &mut Vec<OverloadData>,
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],

View File

@ -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<i32, CommandError> {
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<String>,
) -> bool {
fn ban_profile(context: &CommandContext, profile: &GameProfile, reason: Option<String>) -> 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 }),
),
),
);
}

View File

@ -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<IpAddr> {
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<String>,
) -> Result<i32, CommandError> {
fn ban_ip(context: &CommandContext, target: &str, reason: Option<String>) -> 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::<Vec<_>>()
.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"]);
}

View File

@ -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<Self::Item, CommandSyntaxError> {
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<String, CommandSyntaxError> {
context.get_argument::<String>(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<Self::Item, CommandSyntaxError> {
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<BossbarColor, CommandSyntaxError> {
context.get_argument::<BossbarColor>(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<Self::Item, CommandSyntaxError> {
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<BossbarDivisions, CommandSyntaxError> {
context.get_argument::<BossbarDivisions>(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<Uuid> =
targets.iter().map(|player| player.gameprofile.id).collect();
let count = players.len();
@ -420,42 +570,42 @@ impl CommandExecutor for SetExecutor {
.collect::<Vec<_>>()
.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<i32> {
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<i32> {
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),
);
}

View File

@ -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<Player>],
predicate: &ItemPredicate,
max_count: i32,
) -> Result<i32, CommandSyntaxError> {
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<Player>],
) -> Result<i32, CommandError> {
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<Player>],
) -> Result<TextComponent, TextComponent> {
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<i32> {
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,
},
),
),
),
),
);
}

View File

@ -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<f32> {
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<i32, CommandError> {
) -> 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::<f32>::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::<f32>::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)),
),
),
),
),
),
),
),
),
)
);
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -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<i32> {
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<dyn EntityBase>,
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::<EnchantmentsImpl>()
&& !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 }),
),
),
),
);
}

File diff suppressed because it is too large Load Diff

View File

@ -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<i32> {
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<Player>, 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::<i32>::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"]);
}

View File

@ -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<i32, CommandSyntaxError> {
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<BlockPredicate>,
world: Arc<World>,
placed_blocks: i32,
to_update: Vec<BlockPos>,
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)),
),
);
}

View File

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

View File

@ -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::<i64>::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::<i64>::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);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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 `<namespace>:command.<name>` convention,
/// so we fall back to probing the registry for those.
/// Follows the `<namespace>:command.<name>` convention by probing the registry.
fn resolve_permission_node(
dispatcher: &CommandDispatcher,
_dispatcher: &CommandDispatcher,
registry: &PermissionRegistry,
name: &str,
) -> Option<String> {
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]

View File

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

View File

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

View File

@ -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"]);
}

View File

@ -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::<f32>::find_arg(args, ARG_SPEED);
let count = BoundedNumArgumentConsumer::<i32>::find_arg(args, ARG_COUNT);
#[allow(clippy::too_many_arguments)]
fn send_particles(
source: &CommandSource,
particle: Particle,
pos: Vector3<f64>,
delta: Vector3<f64>,
speed: f32,
count: i32,
force: bool,
viewers: Option<Vec<Arc<Player>>>,
) -> Result<i32, CommandSyntaxError> {
let world = source.world().clone();
let players: Vec<Arc<Player>> = 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<f32> = 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::<f32>::new().min(0.0),
)
.execute(Executor)
.then(
argument(
ARG_COUNT,
BoundedNumArgumentConsumer::<i32>::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
)
),
),
),
);
}

View File

@ -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 <sound> [<source>] [<targets>] [<pos>] [<volume>] [<pitch>] [<minVolume>]
///
/// 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<f32> {
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<f32> {
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<f32> {
BoundedNumArgumentConsumer::new()
.name(ARG_MIN_VOLUME)
.min(0.0)
.max(1.0)
}
#[allow(clippy::too_many_arguments)]
fn play_sound(
source: &CommandSource,
players: &[Arc<Player>],
sound: &Identifier,
sound_source: SoundCategory,
position: Vector3<f64>,
volume: f32,
pitch: f32,
min_volume: f32,
) -> Result<i32, CommandSyntaxError> {
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::<f64>();
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<Player>] = 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::<f32>::find_arg(args, ARG_VOLUME) {
Ok(Ok(v)) => v,
_ => 1.0, // Default volume
};
// Get optional pitch parameter
let pitch = match BoundedNumArgumentConsumer::<f32>::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::<f32>::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::<f64>();
// 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),
);
}

View File

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

View File

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

View File

@ -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::<Vec<_>>()
.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)]

View File

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

View File

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

View File

@ -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<f64>,
looking_towards: &Vector3<f64>,
@ -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<f64> {
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<dyn crate::entity::EntityBase>,
yaw: f32,
is_yaw_relative: bool,
pitch: f32,
is_pitch_relative: bool,
) {
fn rotate_entity(target: &Arc<dyn EntityBase>, 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 <target> <rotation>
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 <target> facing <x> <y> <z>
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 <target> facing entity <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 <target> facing entity <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),
),
),
)
),
),
);
}

View File

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

View File

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

View File

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

View File

@ -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 <targets>` - 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 <targets> <pos>` - 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 <targets> <pos> <angle>` - 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<Player>, 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)),
),
),
),
);
}

View File

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

View File

@ -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<i32> {
/// `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<i32> {
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<Pile>, Vec<i32>)> {
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,
}),
),
),
),
),
),
),
),
),

View File

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

View File

@ -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::<Identifier>("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::<Identifier>("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::<Identifier>("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::<Identifier>("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);
}

View File

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

Some files were not shown because too many files have changed in this diff Show More