chore: port to new command system
This commit is contained in:
parent
544bfc21d5
commit
e393751b8d
@ -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))?;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)),
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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.")
|
||||
}
|
||||
}
|
||||
@ -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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
pub mod damage_type;
|
||||
pub mod effect;
|
||||
pub mod enchantment;
|
||||
pub mod item;
|
||||
pub mod particle;
|
||||
@ -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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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(()));
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
@ -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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
228
crates/pumpkin/src/command/argument_types/block_predicate.rs
Normal file
228
crates/pumpkin/src/command/argument_types/block_predicate.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
54
crates/pumpkin/src/command/argument_types/component.rs
Normal file
54
crates/pumpkin/src/command/argument_types/component.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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(
|
||||
|
||||
@ -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)?)
|
||||
}
|
||||
}
|
||||
111
crates/pumpkin/src/command/argument_types/coordinates/vec2.rs
Normal file
111
crates/pumpkin/src/command/argument_types/coordinates/vec2.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
114
crates/pumpkin/src/command/argument_types/item.rs
Normal file
114
crates/pumpkin/src/command/argument_types/item.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
170
crates/pumpkin/src/command/argument_types/item_predicate.rs
Normal file
170
crates/pumpkin/src/command/argument_types/item_predicate.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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};
|
||||
|
||||
802
crates/pumpkin/src/command/argument_types/nbt_path.rs
Normal file
802
crates/pumpkin/src/command/argument_types/nbt_path.rs
Normal 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 ¤t {
|
||||
node.get_tag(parent, &mut next);
|
||||
}
|
||||
if next.is_empty() {
|
||||
return Err(self.create_not_found_exception(i));
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
Ok(current)
|
||||
}
|
||||
|
||||
/// Counts how many elements match the path.
|
||||
pub fn count_matching(&self, tag: &NbtTag) -> usize {
|
||||
let mut current = vec![tag.clone()];
|
||||
for node in &self.nodes {
|
||||
let mut next = Vec::new();
|
||||
for parent in ¤t {
|
||||
node.get_tag(parent, &mut next);
|
||||
}
|
||||
if next.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
current.len()
|
||||
}
|
||||
|
||||
fn get_or_create_parents(
|
||||
&self,
|
||||
tag: &mut NbtTag,
|
||||
) -> Result<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 ¤t {
|
||||
// SAFETY: We traverse disjoint paths in the NBT tree.
|
||||
let parent = unsafe { &mut *parent_ptr };
|
||||
match node {
|
||||
NbtPathNode::CompoundChild(name) => {
|
||||
if let NbtTag::Compound(compound) = parent {
|
||||
if !compound.child_tags.contains_key(name.as_str()) {
|
||||
compound.child_tags.insert(
|
||||
name.clone().into(),
|
||||
next_node.create_preferred_parent_tag(),
|
||||
);
|
||||
}
|
||||
if let Some(child) = compound.child_tags.get_mut(name.as_str()) {
|
||||
next.push(child as *mut NbtTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
NbtPathNode::AllElements => {
|
||||
if let NbtTag::List(list) = parent {
|
||||
if list.is_empty() {
|
||||
list.push(next_node.create_preferred_parent_tag());
|
||||
}
|
||||
for elem in list.iter_mut() {
|
||||
next.push(elem as *mut NbtTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
NbtPathNode::IndexedElement(index) => {
|
||||
if let NbtTag::List(list) = parent {
|
||||
let size = list.len() as i32;
|
||||
let actual_index = if *index < 0 { size + index } else { *index };
|
||||
if actual_index >= 0 && (actual_index as usize) < list.len() {
|
||||
next.push(&mut list[actual_index as usize] as *mut NbtTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
NbtPathNode::MatchElement(pattern) => {
|
||||
if let NbtTag::List(list) = parent {
|
||||
let pattern_tag = NbtTag::Compound(pattern.clone());
|
||||
let mut found = false;
|
||||
for elem in list.iter_mut() {
|
||||
if compare_nbt(&pattern_tag, elem) {
|
||||
next.push(elem as *mut NbtTag);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
list.push(pattern_tag.clone());
|
||||
if let Some(last) = list.last_mut() {
|
||||
next.push(last as *mut NbtTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
NbtPathNode::MatchObject(name, pattern) => {
|
||||
if let NbtTag::Compound(compound) = parent {
|
||||
let pattern_tag = NbtTag::Compound(pattern.clone());
|
||||
if !compound.child_tags.contains_key(name.as_str()) {
|
||||
compound
|
||||
.child_tags
|
||||
.insert(name.clone().into(), pattern_tag.clone());
|
||||
}
|
||||
if let Some(child) = compound.child_tags.get_mut(name.as_str()) {
|
||||
if compare_nbt(&pattern_tag, child) {
|
||||
next.push(child as *mut NbtTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
NbtPathNode::MatchRootObject(pattern) => {
|
||||
let pattern_tag = NbtTag::Compound(pattern.clone());
|
||||
if compare_nbt(&pattern_tag, parent) {
|
||||
next.push(parent as *mut NbtTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if next.is_empty() {
|
||||
return Err(self.create_not_found_exception(i));
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
Ok(current)
|
||||
}
|
||||
|
||||
/// Sets the value at this path on the given root tag.
|
||||
pub fn set(&self, tag: &mut NbtTag, to_add: NbtTag) -> Result<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)),
|
||||
}
|
||||
}
|
||||
61
crates/pumpkin/src/command/argument_types/particle.rs
Normal file
61
crates/pumpkin/src/command/argument_types/particle.rs
Normal 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)?)
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
68
crates/pumpkin/src/command/argument_types/sound_category.rs
Normal file
68
crates/pumpkin/src/command/argument_types/sound_category.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
@ -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],
|
||||
|
||||
@ -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 }),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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"]);
|
||||
}
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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
@ -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)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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
@ -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"]);
|
||||
}
|
||||
|
||||
@ -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)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 }),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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 }),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -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)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 }),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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"]);
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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))),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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)),
|
||||
);
|
||||
}
|
||||
|
||||
44
crates/pumpkin/src/command/commands/return.rs
Normal file
44
crates/pumpkin/src/command/commands/return.rs
Normal 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)),
|
||||
);
|
||||
}
|
||||
@ -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),
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
196
crates/pumpkin/src/command/commands/schedule.rs
Normal file
196
crates/pumpkin/src/command/commands/schedule.rs
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -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))),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -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)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 }),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -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)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
213
crates/pumpkin/src/command/commands/stopwatch.rs
Normal file
213
crates/pumpkin/src/command/commands/stopwatch.rs
Normal 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);
|
||||
}
|
||||
@ -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
Loading…
Reference in New Issue
Block a user