Add /give Command (#234)

* add prototype for /give command

* fix /give namespace

* fix accidentally replaced text

* remove dbg! statements

* replace block_registry with item_registry

* sync added items with client

* warn!(..) => log::warn!(..)

---------

Co-authored-by: user622628252416 <nnew8715@gmail.com>
This commit is contained in:
user622628252416
2024-11-07 20:34:56 +01:00
committed by GitHub
parent 41e71a857d
commit 16dabf1826
16 changed files with 470 additions and 74 deletions

View File

@@ -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<Option<ItemStack>> {
self.items.iter_mut()
}
}
impl Container for PlayerInventory {

View File

@@ -168,3 +168,10 @@ impl From<Option<&ItemStack>> for Slot {
item.map(Slot::from).unwrap_or(Slot::empty())
}
}
impl From<&Option<ItemStack>> for Slot {
fn from(item: &Option<ItemStack>) -> Self {
item.map(|stack| Self::from(&stack))
.unwrap_or(Slot::empty())
}
}

View File

@@ -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<HashMap<String, Item>> = LazyLock::new(|| {
pub static ITEMS: LazyLock<Vec<Item>> = 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<FoodComponent>,

View File

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

View File

@@ -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<T: ArgNum> {
pub(crate) struct BoundedNumArgumentConsumer<T: ToFromNumber> {
min_inclusive: Option<T>,
max_inclusive: Option<T>,
name: Option<&'static str>,
}
#[async_trait]
impl<T: ArgNum> ArgumentConsumer for BoundedNumArgumentConsumer<T> {
impl<T: ToFromNumber> ArgumentConsumer for BoundedNumArgumentConsumer<T> {
async fn consume<'a>(
&self,
_src: &CommandSender<'a>,
@@ -30,39 +30,59 @@ impl<T: ArgNum> ArgumentConsumer for BoundedNumArgumentConsumer<T> {
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<T> {
type Data = T;
impl<'a, T: ToFromNumber> FindArg<'a> for BoundedNumArgumentConsumer<T> {
type Data = Result<T, NotInBounds>;
fn find_arg(args: &super::ConsumedArgs, name: &str) -> Result<Self::Data, InvalidTreeError> {
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<T: ArgNum> BoundedNumArgumentConsumer<T> {
pub(crate) type NotInBounds = ();
#[derive(Clone, Copy)]
pub(crate) enum Number {
F64(f64),
F32(f32),
I32(i32),
#[allow(unused)]
U32(u32),
}
impl<T: ToFromNumber> BoundedNumArgumentConsumer<T> {
pub(crate) const fn new() -> Self {
Self {
min_inclusive: None,
@@ -88,64 +108,64 @@ impl<T: ArgNum> BoundedNumArgumentConsumer<T> {
}
}
pub(crate) trait ArgNum: PartialOrd + Copy + Send + Sync + FromStr {
fn to_arg<'a>(self) -> Arg<'a>;
fn from_arg(arg: &Arg<'_>) -> Option<Self>;
pub(crate) trait ToFromNumber: PartialOrd + Copy + Send + Sync + FromStr {
fn to_number(self) -> Number;
fn from_number(arg: &Number) -> Option<Self>;
}
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<Self> {
fn from_number(arg: &Number) -> Option<Self> {
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<Self> {
fn from_number(arg: &Number) -> Option<Self> {
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<Self> {
fn from_number(arg: &Number) -> Option<Self> {
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<Self> {
fn from_number(arg: &Number) -> Option<Self> {
match arg {
Arg::U32(x) => Some(*x),
Number::U32(x) => Some(*x),
_ => None,
}
}
}
impl<T: ArgNum> DefaultNameArgConsumer for BoundedNumArgumentConsumer<T> {
impl<T: ToFromNumber> DefaultNameArgConsumer for BoundedNumArgumentConsumer<T> {
fn default_name(&self) -> &'static str {
// setting a single default name for all BoundedNumArgumentConsumer variants is probably a bad idea since it would lead to confusion
self.name.expect("Only use *_default variants of methods with a BoundedNumArgumentConsumer that has a name.")

View File

@@ -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`])

View File

@@ -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<Arg<'a>> {
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<Self::Data, InvalidTreeError> {
match args.get(name) {
Some(Arg::Item(name)) => Ok(name),
_ => Err(InvalidTreeError::InvalidConsumptionError(Some(
name.to_string(),
))),
}
}
}

View File

@@ -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<Number, NotInBounds>),
#[allow(unused)]
Simple(String),
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<CommandDispatcher<'a>> {
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)

View File

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