diff --git a/pumpkin-inventory/src/player.rs b/pumpkin-inventory/src/player.rs index dd868249..8bd91e20 100644 --- a/pumpkin-inventory/src/player.rs +++ b/pumpkin-inventory/src/player.rs @@ -1,3 +1,4 @@ +use std::slice::IterMut; use std::sync::atomic::AtomicU32; use crate::container_click::MouseClick; @@ -25,6 +26,8 @@ impl Default for PlayerInventory { } impl PlayerInventory { + pub const CONTAINER_ID: i8 = 0; + pub fn new() -> Self { Self { crafting: [None; 4], @@ -134,6 +137,10 @@ impl PlayerInventory { slots.push(&mut self.offhand); slots } + + pub fn iter_items_mut(&mut self) -> IterMut> { + self.items.iter_mut() + } } impl Container for PlayerInventory { diff --git a/pumpkin-protocol/src/slot.rs b/pumpkin-protocol/src/slot.rs index 221066ce..f8dee118 100644 --- a/pumpkin-protocol/src/slot.rs +++ b/pumpkin-protocol/src/slot.rs @@ -168,3 +168,10 @@ impl From> for Slot { item.map(Slot::from).unwrap_or(Slot::empty()) } } + +impl From<&Option> for Slot { + fn from(item: &Option) -> Self { + item.map(|stack| Self::from(&stack)) + .unwrap_or(Slot::empty()) + } +} diff --git a/pumpkin-world/src/item/item_registry.rs b/pumpkin-world/src/item/item_registry.rs index eb9f9637..a3446aee 100644 --- a/pumpkin-world/src/item/item_registry.rs +++ b/pumpkin-world/src/item/item_registry.rs @@ -1,20 +1,24 @@ -use std::{collections::HashMap, sync::LazyLock}; +use std::sync::LazyLock; use serde::Deserialize; const ITEMS_JSON: &str = include_str!("../../../assets/items.json"); -pub static ITEMS: LazyLock> = LazyLock::new(|| { +pub static ITEMS: LazyLock> = LazyLock::new(|| { serde_json::from_str(ITEMS_JSON).expect("Could not parse items.json registry.") }); +pub fn get_item<'a>(name: &str) -> Option<&'a Item> { + ITEMS.iter().find(|item| item.name == name) +} + #[expect(dead_code)] #[derive(Deserialize, Clone, Debug)] pub struct Item { - id: u16, - name: String, + pub id: u16, + pub name: String, translation_key: String, - max_stack: i8, + pub max_stack: i8, max_durability: u16, break_sound: String, food: Option, diff --git a/pumpkin-world/src/item/mod.rs b/pumpkin-world/src/item/mod.rs index 43de8dfe..a9eb8885 100644 --- a/pumpkin-world/src/item/mod.rs +++ b/pumpkin-world/src/item/mod.rs @@ -1,5 +1,5 @@ mod item_categories; -mod item_registry; +pub mod item_registry; pub use item_registry::ITEMS; #[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "lowercase")] @@ -24,3 +24,12 @@ impl PartialEq for ItemStack { self.item_id == other.item_id } } + +impl ItemStack { + pub fn new(item_count: u8, item_id: u16) -> Self { + Self { + item_count, + item_id, + } + } +} diff --git a/pumpkin/src/command/args/arg_bounded_num.rs b/pumpkin/src/command/args/arg_bounded_num.rs index 97c23ccb..c799e10a 100644 --- a/pumpkin/src/command/args/arg_bounded_num.rs +++ b/pumpkin/src/command/args/arg_bounded_num.rs @@ -12,14 +12,14 @@ use super::super::args::ArgumentConsumer; use super::{Arg, DefaultNameArgConsumer, FindArg}; /// Consumes a single generic num, but only if it's in bounds. -pub(crate) struct BoundedNumArgumentConsumer { +pub(crate) struct BoundedNumArgumentConsumer { min_inclusive: Option, max_inclusive: Option, name: Option<&'static str>, } #[async_trait] -impl ArgumentConsumer for BoundedNumArgumentConsumer { +impl ArgumentConsumer for BoundedNumArgumentConsumer { async fn consume<'a>( &self, _src: &CommandSender<'a>, @@ -30,39 +30,59 @@ impl ArgumentConsumer for BoundedNumArgumentConsumer { if let Some(max) = self.max_inclusive { if x > max { - return None; + return Some(Arg::Num(Err(()))); } } if let Some(min) = self.min_inclusive { if x < min { - return None; + return Some(Arg::Num(Err(()))); } } - Some(x.to_arg()) + Some(Arg::Num(Ok(x.to_number()))) } } -impl<'a, T: ArgNum> FindArg<'a> for BoundedNumArgumentConsumer { - type Data = T; +impl<'a, T: ToFromNumber> FindArg<'a> for BoundedNumArgumentConsumer { + type Data = Result; fn find_arg(args: &super::ConsumedArgs, name: &str) -> Result { - match args.get(name) { - Some(arg) => match T::from_arg(arg) { - Some(x) => Ok(x), - _ => Err(InvalidTreeError::InvalidConsumptionError(Some( - name.to_string(), - ))), - }, - _ => Err(InvalidTreeError::InvalidConsumptionError(Some( + let Some(Arg::Num(result)) = args.get(name) else { + return Err(InvalidTreeError::InvalidConsumptionError(Some( name.to_string(), - ))), - } + ))); + }; + + let data: Self::Data = match result { + Ok(num) => { + if let Some(x) = T::from_number(num) { + Ok(x) + } else { + return Err(InvalidTreeError::InvalidConsumptionError(Some( + name.to_string(), + ))); + } + } + Err(()) => Err(()), + }; + + Ok(data) } } -impl BoundedNumArgumentConsumer { +pub(crate) type NotInBounds = (); + +#[derive(Clone, Copy)] +pub(crate) enum Number { + F64(f64), + F32(f32), + I32(i32), + #[allow(unused)] + U32(u32), +} + +impl BoundedNumArgumentConsumer { pub(crate) const fn new() -> Self { Self { min_inclusive: None, @@ -88,64 +108,64 @@ impl BoundedNumArgumentConsumer { } } -pub(crate) trait ArgNum: PartialOrd + Copy + Send + Sync + FromStr { - fn to_arg<'a>(self) -> Arg<'a>; - fn from_arg(arg: &Arg<'_>) -> Option; +pub(crate) trait ToFromNumber: PartialOrd + Copy + Send + Sync + FromStr { + fn to_number(self) -> Number; + fn from_number(arg: &Number) -> Option; } -impl ArgNum for f64 { - fn to_arg<'a>(self) -> Arg<'a> { - Arg::F64(self) +impl ToFromNumber for f64 { + fn to_number(self) -> Number { + Number::F64(self) } - fn from_arg(arg: &Arg<'_>) -> Option { + fn from_number(arg: &Number) -> Option { match arg { - Arg::F64(x) => Some(*x), + Number::F64(x) => Some(*x), _ => None, } } } -impl ArgNum for f32 { - fn to_arg<'a>(self) -> Arg<'a> { - Arg::F32(self) +impl ToFromNumber for f32 { + fn to_number(self) -> Number { + Number::F32(self) } - fn from_arg(arg: &Arg<'_>) -> Option { + fn from_number(arg: &Number) -> Option { match arg { - Arg::F32(x) => Some(*x), + Number::F32(x) => Some(*x), _ => None, } } } -impl ArgNum for i32 { - fn to_arg<'a>(self) -> Arg<'a> { - Arg::I32(self) +impl ToFromNumber for i32 { + fn to_number(self) -> Number { + Number::I32(self) } - fn from_arg(arg: &Arg<'_>) -> Option { + fn from_number(arg: &Number) -> Option { match arg { - Arg::I32(x) => Some(*x), + Number::I32(x) => Some(*x), _ => None, } } } -impl ArgNum for u32 { - fn to_arg<'a>(self) -> Arg<'a> { - Arg::U32(self) +impl ToFromNumber for u32 { + fn to_number(self) -> Number { + Number::U32(self) } - fn from_arg(arg: &Arg<'_>) -> Option { + fn from_number(arg: &Number) -> Option { match arg { - Arg::U32(x) => Some(*x), + Number::U32(x) => Some(*x), _ => None, } } } -impl DefaultNameArgConsumer for BoundedNumArgumentConsumer { +impl DefaultNameArgConsumer for BoundedNumArgumentConsumer { fn default_name(&self) -> &'static str { // setting a single default name for all BoundedNumArgumentConsumer variants is probably a bad idea since it would lead to confusion self.name.expect("Only use *_default variants of methods with a BoundedNumArgumentConsumer that has a name.") diff --git a/pumpkin/src/command/args/arg_entities.rs b/pumpkin/src/command/args/arg_entities.rs index 1483eaca..2f35bba3 100644 --- a/pumpkin/src/command/args/arg_entities.rs +++ b/pumpkin/src/command/args/arg_entities.rs @@ -9,7 +9,7 @@ use crate::entity::player::Player; use crate::server::Server; use super::super::args::ArgumentConsumer; -use super::arg_player::PlayersArgumentConsumer; +use super::arg_players::PlayersArgumentConsumer; use super::{Arg, DefaultNameArgConsumer, FindArg}; /// todo: implement (currently just calls [`super::arg_player::PlayerArgumentConsumer`]) diff --git a/pumpkin/src/command/args/arg_item.rs b/pumpkin/src/command/args/arg_item.rs new file mode 100644 index 00000000..7eaf6390 --- /dev/null +++ b/pumpkin/src/command/args/arg_item.rs @@ -0,0 +1,60 @@ +use async_trait::async_trait; + +use crate::{command::dispatcher::InvalidTreeError, server::Server}; + +use super::{ + super::{ + args::{ArgumentConsumer, RawArgs}, + CommandSender, + }, + Arg, DefaultNameArgConsumer, FindArg, +}; + +pub(crate) struct ItemArgumentConsumer; + +#[async_trait] +impl ArgumentConsumer for ItemArgumentConsumer { + async fn consume<'a>( + &self, + _sender: &CommandSender<'a>, + _server: &'a Server, + args: &mut RawArgs<'a>, + ) -> Option> { + let s = args.pop()?; + + let name = if s.contains(':') { + s.to_string() + } else { + format!("minecraft:{s}") + }; + + // todo: get an actual item + Some(Arg::Item(name)) + } +} + +impl DefaultNameArgConsumer for ItemArgumentConsumer { + fn default_name(&self) -> &'static str { + "item" + } + + fn get_argument_consumer(&self) -> &dyn ArgumentConsumer { + self + } +} + +impl<'a> FindArg<'a> for ItemArgumentConsumer { + type Data = &'a str; + + fn find_arg( + args: &'a super::ConsumedArgs, + name: &'a str, + ) -> Result { + match args.get(name) { + Some(Arg::Item(name)) => Ok(name), + _ => Err(InvalidTreeError::InvalidConsumptionError(Some( + name.to_string(), + ))), + } + } +} diff --git a/pumpkin/src/command/args/arg_player.rs b/pumpkin/src/command/args/arg_players.rs similarity index 100% rename from pumpkin/src/command/args/arg_player.rs rename to pumpkin/src/command/args/arg_players.rs diff --git a/pumpkin/src/command/args/mod.rs b/pumpkin/src/command/args/mod.rs index 877dc8bc..7a3d72ff 100644 --- a/pumpkin/src/command/args/mod.rs +++ b/pumpkin/src/command/args/mod.rs @@ -1,5 +1,6 @@ use std::{collections::HashMap, hash::Hash, sync::Arc}; +use arg_bounded_num::{NotInBounds, Number}; use async_trait::async_trait; use pumpkin_core::{ math::{vector2::Vector2, vector3::Vector3}, @@ -19,8 +20,9 @@ pub(crate) mod arg_command; pub(crate) mod arg_entities; pub(crate) mod arg_entity; pub(crate) mod arg_gamemode; +pub(crate) mod arg_item; pub(crate) mod arg_message; -pub(crate) mod arg_player; +pub(crate) mod arg_players; pub(crate) mod arg_position_2d; pub(crate) mod arg_position_3d; pub(crate) mod arg_rotation; @@ -55,12 +57,9 @@ pub(crate) enum Arg<'a> { Rotation(f32, f32), GameMode(GameMode), CommandTree(&'a CommandTree<'a>), + Item(String), Msg(String), - F64(f64), - F32(f32), - I32(i32), - #[allow(unused)] - U32(u32), + Num(Result), #[allow(unused)] Simple(String), } diff --git a/pumpkin/src/command/commands/cmd_gamemode.rs b/pumpkin/src/command/commands/cmd_gamemode.rs index af19ac11..aefd00b4 100644 --- a/pumpkin/src/command/commands/cmd_gamemode.rs +++ b/pumpkin/src/command/commands/cmd_gamemode.rs @@ -5,7 +5,7 @@ use crate::command::args::GetCloned; use crate::TextComponent; -use crate::command::args::arg_player::PlayersArgumentConsumer; +use crate::command::args::arg_players::PlayersArgumentConsumer; use crate::command::args::{Arg, ConsumedArgs}; use crate::command::dispatcher::InvalidTreeError; diff --git a/pumpkin/src/command/commands/cmd_give.rs b/pumpkin/src/command/commands/cmd_give.rs new file mode 100644 index 00000000..d1631502 --- /dev/null +++ b/pumpkin/src/command/commands/cmd_give.rs @@ -0,0 +1,89 @@ +use async_trait::async_trait; +use pumpkin_core::text::color::{Color, NamedColor}; +use pumpkin_core::text::TextComponent; +use pumpkin_world::item::item_registry; + +use crate::command::args::arg_bounded_num::BoundedNumArgumentConsumer; +use crate::command::args::arg_item::ItemArgumentConsumer; +use crate::command::args::arg_players::PlayersArgumentConsumer; +use crate::command::args::{ConsumedArgs, FindArg, FindArgDefaultName}; +use crate::command::tree::CommandTree; +use crate::command::tree_builder::{argument, argument_default_name, require}; +use crate::command::{CommandExecutor, CommandSender, InvalidTreeError}; + +const NAMES: [&str; 1] = ["give"]; + +const DESCRIPTION: &str = "Give items to player(s)."; + +const ARG_ITEM: &str = "item"; + +static ITEM_COUNT_CONSUMER: BoundedNumArgumentConsumer = + BoundedNumArgumentConsumer::new().name("count").max(6400); + +struct GiveExecutor; + +#[async_trait] +impl CommandExecutor for GiveExecutor { + async fn execute<'a>( + &self, + sender: &mut CommandSender<'a>, + _server: &crate::server::Server, + args: &ConsumedArgs<'a>, + ) -> Result<(), InvalidTreeError> { + let targets = PlayersArgumentConsumer.find_arg_default_name(args)?; + + let item_name = ItemArgumentConsumer::find_arg(args, ARG_ITEM)?; + + let Some(item) = item_registry::get_item(item_name) else { + sender + .send_message( + TextComponent::text_string(format!("Item {item_name} does not exist.")) + .color(Color::Named(NamedColor::Red)), + ) + .await; + return Ok(()); + }; + + let item_count = match ITEM_COUNT_CONSUMER.find_arg_default_name(args) { + Err(_) => 1, + Ok(Ok(count)) => count, + Ok(Err(())) => { + sender + .send_message( + TextComponent::text("Item count is too large or too small.") + .color(Color::Named(NamedColor::Red)), + ) + .await; + return Ok(()); + } + }; + + for target in targets { + target.give_items(item, item_count).await; + } + + sender + .send_message(TextComponent::text_string(match targets { + [target] => format!( + "Gave {item_count} {item_name} to {}", + target.gameprofile.name + ), + _ => format!("Gave {item_count} {item_name} to {} players", targets.len()), + })) + .await; + + Ok(()) + } +} + +pub fn init_command_tree<'a>() -> CommandTree<'a> { + CommandTree::new(NAMES, DESCRIPTION).with_child( + require(&|sender| sender.permission_lvl() >= 2).with_child( + argument_default_name(&PlayersArgumentConsumer).with_child( + argument(ARG_ITEM, &ItemArgumentConsumer) + .execute(&GiveExecutor) + .with_child(argument_default_name(&ITEM_COUNT_CONSUMER).execute(&GiveExecutor)), + ), + ), + ) +} diff --git a/pumpkin/src/command/commands/cmd_kick.rs b/pumpkin/src/command/commands/cmd_kick.rs index 33cb696a..e5967ede 100644 --- a/pumpkin/src/command/commands/cmd_kick.rs +++ b/pumpkin/src/command/commands/cmd_kick.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; use pumpkin_core::text::color::NamedColor; use pumpkin_core::text::TextComponent; -use crate::command::args::arg_player::PlayersArgumentConsumer; +use crate::command::args::arg_players::PlayersArgumentConsumer; use crate::command::args::{Arg, ConsumedArgs}; use crate::command::tree::CommandTree; use crate::command::tree_builder::argument; diff --git a/pumpkin/src/command/commands/cmd_worldborder.rs b/pumpkin/src/command/commands/cmd_worldborder.rs index f3a1fd31..72eaefd2 100644 --- a/pumpkin/src/command/commands/cmd_worldborder.rs +++ b/pumpkin/src/command/commands/cmd_worldborder.rs @@ -11,7 +11,8 @@ use crate::{ command::{ args::{ arg_bounded_num::BoundedNumArgumentConsumer, - arg_position_2d::Position2DArgumentConsumer, ConsumedArgs, FindArgDefaultName, + arg_position_2d::Position2DArgumentConsumer, ConsumedArgs, DefaultNameArgConsumer, + FindArgDefaultName, }, tree::CommandTree, tree_builder::{argument_default_name, literal}, @@ -66,7 +67,18 @@ impl CommandExecutor for WorldborderSetExecutor { .expect("There should always be atleast one world"); let mut border = world.worldborder.lock().await; - let distance = DISTANCE_CONSUMER.find_arg_default_name(args)?; + let Ok(distance) = DISTANCE_CONSUMER.find_arg_default_name(args)? else { + sender + .send_message( + TextComponent::text_string(format!( + "{} is out of bounds.", + DISTANCE_CONSUMER.default_name() + )) + .color(Color::Named(NamedColor::Red)), + ) + .await; + return Ok(()); + }; if (distance - border.new_diameter).abs() < f64::EPSILON { sender @@ -104,8 +116,30 @@ impl CommandExecutor for WorldborderSetTimeExecutor { .expect("There should always be atleast one world"); let mut border = world.worldborder.lock().await; - let distance = DISTANCE_CONSUMER.find_arg_default_name(args)?; - let time = TIME_CONSUMER.find_arg_default_name(args)?; + let Ok(distance) = DISTANCE_CONSUMER.find_arg_default_name(args)? else { + sender + .send_message( + TextComponent::text_string(format!( + "{} is out of bounds.", + DISTANCE_CONSUMER.default_name() + )) + .color(Color::Named(NamedColor::Red)), + ) + .await; + return Ok(()); + }; + let Ok(time) = TIME_CONSUMER.find_arg_default_name(args)? else { + sender + .send_message( + TextComponent::text_string(format!( + "{} is out of bounds.", + TIME_CONSUMER.default_name() + )) + .color(Color::Named(NamedColor::Red)), + ) + .await; + return Ok(()); + }; match distance.total_cmp(&border.new_diameter) { std::cmp::Ordering::Equal => { @@ -154,7 +188,18 @@ impl CommandExecutor for WorldborderAddExecutor { .expect("There should always be atleast one world"); let mut border = world.worldborder.lock().await; - let distance = DISTANCE_CONSUMER.find_arg_default_name(args)?; + let Ok(distance) = DISTANCE_CONSUMER.find_arg_default_name(args)? else { + sender + .send_message( + TextComponent::text_string(format!( + "{} is out of bounds.", + DISTANCE_CONSUMER.default_name() + )) + .color(Color::Named(NamedColor::Red)), + ) + .await; + return Ok(()); + }; if distance == 0.0 { sender @@ -194,8 +239,30 @@ impl CommandExecutor for WorldborderAddTimeExecutor { .expect("There should always be atleast one world"); let mut border = world.worldborder.lock().await; - let distance = DISTANCE_CONSUMER.find_arg_default_name(args)?; - let time = TIME_CONSUMER.find_arg_default_name(args)?; + let Ok(distance) = DISTANCE_CONSUMER.find_arg_default_name(args)? else { + sender + .send_message( + TextComponent::text_string(format!( + "{} is out of bounds.", + DISTANCE_CONSUMER.default_name() + )) + .color(Color::Named(NamedColor::Red)), + ) + .await; + return Ok(()); + }; + let Ok(time) = TIME_CONSUMER.find_arg_default_name(args)? else { + sender + .send_message( + TextComponent::text_string(format!( + "{} is out of bounds.", + TIME_CONSUMER.default_name() + )) + .color(Color::Named(NamedColor::Red)), + ) + .await; + return Ok(()); + }; let distance = distance + border.new_diameter; @@ -274,7 +341,18 @@ impl CommandExecutor for WorldborderDamageAmountExecutor { .expect("There should always be atleast one world"); let mut border = world.worldborder.lock().await; - let damage_per_block = DAMAGE_PER_BLOCK_CONSUMER.find_arg_default_name(args)?; + let Ok(damage_per_block) = DAMAGE_PER_BLOCK_CONSUMER.find_arg_default_name(args)? else { + sender + .send_message( + TextComponent::text_string(format!( + "{} is out of bounds.", + DAMAGE_PER_BLOCK_CONSUMER.default_name() + )) + .color(Color::Named(NamedColor::Red)), + ) + .await; + return Ok(()); + }; if (damage_per_block - border.damage_per_block).abs() < f32::EPSILON { sender @@ -314,7 +392,18 @@ impl CommandExecutor for WorldborderDamageBufferExecutor { .expect("There should always be atleast one world"); let mut border = world.worldborder.lock().await; - let buffer = DAMAGE_BUFFER_CONSUMER.find_arg_default_name(args)?; + let Ok(buffer) = DAMAGE_BUFFER_CONSUMER.find_arg_default_name(args)? else { + sender + .send_message( + TextComponent::text_string(format!( + "{} is out of bounds.", + DAMAGE_BUFFER_CONSUMER.default_name() + )) + .color(Color::Named(NamedColor::Red)), + ) + .await; + return Ok(()); + }; if (buffer - border.buffer).abs() < f32::EPSILON { sender @@ -354,7 +443,18 @@ impl CommandExecutor for WorldborderWarningDistanceExecutor { .expect("There should always be atleast one world"); let mut border = world.worldborder.lock().await; - let distance = WARNING_DISTANCE_CONSUMER.find_arg_default_name(args)?; + let Ok(distance) = WARNING_DISTANCE_CONSUMER.find_arg_default_name(args)? else { + sender + .send_message( + TextComponent::text_string(format!( + "{} is out of bounds.", + WARNING_DISTANCE_CONSUMER.default_name() + )) + .color(Color::Named(NamedColor::Red)), + ) + .await; + return Ok(()); + }; if distance == border.warning_blocks { sender @@ -394,7 +494,18 @@ impl CommandExecutor for WorldborderWarningTimeExecutor { .expect("There should always be atleast one world"); let mut border = world.worldborder.lock().await; - let time = TIME_CONSUMER.find_arg_default_name(args)?; + let Ok(time) = TIME_CONSUMER.find_arg_default_name(args)? else { + sender + .send_message( + TextComponent::text_string(format!( + "{} is out of bounds.", + TIME_CONSUMER.default_name() + )) + .color(Color::Named(NamedColor::Red)), + ) + .await; + return Ok(()); + }; if time == border.warning_time { sender diff --git a/pumpkin/src/command/commands/mod.rs b/pumpkin/src/command/commands/mod.rs index a8fcdf92..67ea72e3 100644 --- a/pumpkin/src/command/commands/mod.rs +++ b/pumpkin/src/command/commands/mod.rs @@ -1,5 +1,6 @@ pub mod cmd_echest; pub mod cmd_gamemode; +pub mod cmd_give; pub mod cmd_help; pub mod cmd_kick; pub mod cmd_kill; diff --git a/pumpkin/src/command/mod.rs b/pumpkin/src/command/mod.rs index 04744c75..b062b693 100644 --- a/pumpkin/src/command/mod.rs +++ b/pumpkin/src/command/mod.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use args::ConsumedArgs; use async_trait::async_trait; use commands::{ - cmd_echest, cmd_gamemode, cmd_help, cmd_kick, cmd_kill, cmd_list, cmd_pumpkin, cmd_say, - cmd_stop, cmd_teleport, cmd_worldborder, + cmd_echest, cmd_gamemode, cmd_give, cmd_help, cmd_kick, cmd_kill, cmd_list, cmd_pumpkin, + cmd_say, cmd_stop, cmd_teleport, cmd_worldborder, }; use dispatcher::InvalidTreeError; use pumpkin_core::math::vector3::Vector3; @@ -82,6 +82,7 @@ pub fn default_dispatcher<'a>() -> Arc> { dispatcher.register(cmd_kick::init_command_tree()); dispatcher.register(cmd_worldborder::init_command_tree()); dispatcher.register(cmd_teleport::init_command_tree()); + dispatcher.register(cmd_give::init_command_tree()); dispatcher.register(cmd_list::init_command_tree()); Arc::new(dispatcher) diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index aff07d01..04af647a 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -1,4 +1,5 @@ use std::{ + cmp::min, collections::{HashMap, VecDeque}, sync::{ atomic::{AtomicBool, AtomicI32, AtomicI64, AtomicU32, AtomicU8}, @@ -17,14 +18,14 @@ use pumpkin_core::{ GameMode, }; use pumpkin_entity::{entity_type::EntityType, EntityId}; -use pumpkin_inventory::player::PlayerInventory; +use pumpkin_inventory::{player::PlayerInventory, InventoryError}; use pumpkin_macros::sound; use pumpkin_protocol::{ bytebuf::packet_id::Packet, client::play::{ CCombatDeath, CGameEvent, CHurtAnimation, CKeepAlive, CPlayDisconnect, CPlayerAbilities, - CPlayerInfoUpdate, CRespawn, CSetHealth, CSpawnEntity, CSyncPlayerPosition, - CSystemChatMessage, GameEvent, PlayerAction, + CPlayerInfoUpdate, CRespawn, CSetContainerSlot, CSetHealth, CSpawnEntity, + CSyncPlayerPosition, CSystemChatMessage, GameEvent, PlayerAction, }, server::play::{ SChatCommand, SChatMessage, SClientCommand, SClientInformationPlay, SClientTickEnd, @@ -38,7 +39,10 @@ use tokio::sync::{Mutex, Notify}; use tokio::task::JoinHandle; use pumpkin_protocol::server::play::SKeepAlive; -use pumpkin_world::{cylindrical_chunk_iterator::Cylindrical, item::ItemStack}; +use pumpkin_world::{ + cylindrical_chunk_iterator::Cylindrical, + item::{item_registry::Item, ItemStack}, +}; use super::Entity; use crate::{ @@ -841,6 +845,90 @@ impl Player { }; Ok(()) } + + /// Syncs inventory slot with client. + pub async fn send_inventory_slot_update( + &self, + inventory: &mut PlayerInventory, + slot_index: usize, + ) -> Result<(), InventoryError> { + let slot = (&*inventory.get_slot(slot_index)?).into(); + + // Returns previous value + let i = inventory + .state_id + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let packet = CSetContainerSlot::new( + PlayerInventory::CONTAINER_ID, + (i + 1) as i32, + slot_index, + &slot, + ); + self.client.send_packet(&packet).await; + + Ok(()) + } + + /// Add items to inventory if there's space, else drop them to the ground. + /// + /// This method automatically syncs changes with the client. + pub async fn give_items(&self, item: &Item, count: u32) { + let mut remaining_items: u32 = count; + let max_stack_size = item.max_stack as u8; + + let mut inventory = self.inventory.lock().await; + + // try filling existing ItemStacks first + for slot_index in (36..45).chain(9..36) { + let Some(stack) = inventory.get_slot(slot_index).unwrap() else { + continue; + }; + + if stack.item_id != item.id { + continue; + } + + if stack.item_count < max_stack_size { + let space = max_stack_size - stack.item_count; + let deposit_amount = min(remaining_items, space.into()); + + stack.item_count += deposit_amount as u8; + remaining_items -= deposit_amount; + + self.send_inventory_slot_update(&mut inventory, slot_index) + .await + .unwrap(); + + if remaining_items == 0 { + return; + } + } + } + + // then try filling empty slots + for slot_index in (36..45).chain(9..36) { + let slot = inventory.get_slot(slot_index).unwrap(); + + if slot.is_some() { + continue; + } + + let deposit_amount = min(remaining_items, max_stack_size.into()); + + *slot = Some(ItemStack::new(deposit_amount as u8, item.id)); + remaining_items -= deposit_amount; + + self.send_inventory_slot_update(&mut inventory, slot_index) + .await + .unwrap(); + + if remaining_items == 0 { + return; + } + } + + log::warn!("{remaining_items} items ({}) were discarded because dropping them to the ground is not implemented", item.name); + } } /// Represents a player's abilities and special powers.