Add Pick block (Middle Mouse Click) (#418)

* Pick Item Changes

* Changed some logging

* Pick Item working for hotbar slots

* Basic functionality, changes needed to adhere to expected minecraft behaviour

* Clean up code

* Added todo for pick from entity

* Added item swapping

* Swapping and cleanup

* Fix for rebase

* Remove unused function

* Bugfix

* remove todo

---------

Co-authored-by: Alexander Medvedev <lilalexmed@proton.me>
This commit is contained in:
Adel C
2024-12-29 10:04:57 -08:00
committed by GitHub
parent b40c274445
commit 02cec5873b
5 changed files with 159 additions and 6 deletions

View File

@@ -116,6 +116,35 @@ impl PlayerInventory {
&mut self.items[self.selected + 36 - 9]
}
pub fn get_slot_with_item(&self, item_id: u16) -> Option<usize> {
for slot in 9..=44 {
match &self.items[slot - 9] {
Some(item) if item.item_id == item_id => return Some(slot),
_ => continue,
}
}
None
}
pub fn get_pick_item_hotbar_slot(&self) -> usize {
if self.items[self.selected + 36 - 9].is_none() {
return self.selected;
}
for slot in 0..9 {
if self.items[slot + 36 - 9].is_none() {
return slot;
}
}
self.selected
}
pub fn get_empty_slot(&self) -> Option<usize> {
(9..=44).find(|&slot| self.items[slot - 9].is_none())
}
pub fn slots(&self) -> Vec<Option<&ItemStack>> {
let mut slots = vec![self.crafting_output.as_ref()];
slots.extend(self.crafting.iter().map(|c| c.as_ref()));

View File

@@ -10,6 +10,7 @@ mod s_confirm_teleport;
mod s_cookie_response;
mod s_interact;
mod s_keep_alive;
mod s_pick_item;
mod s_ping_request;
mod s_player_abilities;
mod s_player_action;
@@ -37,6 +38,7 @@ pub use s_confirm_teleport::*;
pub use s_cookie_response::*;
pub use s_interact::*;
pub use s_keep_alive::*;
pub use s_pick_item::*;
pub use s_ping_request::*;
pub use s_player_abilities::*;
pub use s_player_action::*;

View File

@@ -0,0 +1,17 @@
use pumpkin_core::math::position::WorldPosition;
use pumpkin_macros::server_packet;
use serde::Deserialize;
#[derive(Deserialize)]
#[server_packet("play:pick_item_from_block")]
pub struct SPickItemFromBlock {
pub pos: WorldPosition,
pub include_data: bool,
}
#[derive(Deserialize)]
#[server_packet("play:pick_item_from_entity")]
pub struct SPickItemFromEntity {
pub id: i32,
pub include_data: bool,
}

View File

@@ -37,9 +37,10 @@ use pumpkin_protocol::{
},
server::play::{
SChatCommand, SChatMessage, SClientCommand, SClientInformationPlay, SClientTickEnd,
SCommandSuggestion, SConfirmTeleport, SInteract, SPlayerAbilities, SPlayerAction,
SPlayerCommand, SPlayerInput, SPlayerPosition, SPlayerPositionRotation, SPlayerRotation,
SSetCreativeSlot, SSetHeldItem, SSetPlayerGround, SSwingArm, SUseItem, SUseItemOn,
SCommandSuggestion, SConfirmTeleport, SInteract, SPickItemFromBlock, SPlayerAbilities,
SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerPosition, SPlayerPositionRotation,
SPlayerRotation, SSetCreativeSlot, SSetHeldItem, SSetPlayerGround, SSwingArm, SUseItem,
SUseItemOn,
},
RawPacket, ServerPacket, SoundCategory,
};
@@ -675,6 +676,7 @@ impl Player {
}
}
#[allow(clippy::too_many_lines)]
pub async fn handle_play_packet(
self: &Arc<Self>,
server: &Arc<Server>,
@@ -726,6 +728,10 @@ impl Player {
SSetPlayerGround::PACKET_ID => {
self.handle_player_ground(&SSetPlayerGround::read(bytebuf)?);
}
SPickItemFromBlock::PACKET_ID => {
self.handle_pick_item_from_block(SPickItemFromBlock::read(bytebuf)?)
.await;
}
SPlayerAbilities::PACKET_ID => {
self.handle_player_abilities(SPlayerAbilities::read(bytebuf)?)
.await;

View File

@@ -18,7 +18,10 @@ use pumpkin_core::{
text::TextComponent,
GameMode,
};
use pumpkin_inventory::player::PlayerInventory;
use pumpkin_inventory::InventoryError;
use pumpkin_protocol::client::play::{CSetContainerSlot, CSetHeldItem};
use pumpkin_protocol::codec::slot::Slot;
use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::server::play::SCookieResponse as SPCookieResponse;
use pumpkin_protocol::{
@@ -32,13 +35,14 @@ use pumpkin_protocol::{
},
server::play::{
Action, ActionType, SChatCommand, SChatMessage, SClientCommand, SClientInformationPlay,
SConfirmTeleport, SInteract, SPlayPingRequest, SPlayerAbilities, SPlayerAction,
SPlayerCommand, SPlayerPosition, SPlayerPositionRotation, SPlayerRotation,
SSetCreativeSlot, SSetHeldItem, SSwingArm, SUseItemOn, Status,
SConfirmTeleport, SInteract, SPickItemFromBlock, SPickItemFromEntity, SPlayPingRequest,
SPlayerAbilities, SPlayerAction, SPlayerCommand, SPlayerPosition, SPlayerPositionRotation,
SPlayerRotation, SSetCreativeSlot, SSetHeldItem, SSwingArm, SUseItemOn, Status,
},
};
use pumpkin_world::block::{block_registry::get_block_by_item, BlockFace};
use pumpkin_world::item::item_registry::get_item_by_id;
use pumpkin_world::item::ItemStack;
use thiserror::Error;
fn modulus(a: f32, b: f32) -> f32 {
@@ -310,6 +314,101 @@ impl Player {
.store(ground.on_ground, std::sync::atomic::Ordering::Relaxed);
}
async fn update_single_slot(
&self,
inventory: &mut tokio::sync::MutexGuard<'_, PlayerInventory>,
slot: usize,
slot_data: Slot,
) {
inventory.state_id += 1;
let dest_packet = CSetContainerSlot::new(0, inventory.state_id as i32, slot, &slot_data);
self.client.send_packet(&dest_packet).await;
if inventory
.set_slot(slot, slot_data.to_item(), false)
.is_err()
{
log::error!("Pick item set slot error!");
}
}
pub async fn handle_pick_item_from_block(&self, pick_item: SPickItemFromBlock) {
if !self.can_interact_with_block_at(&pick_item.pos, 1.0) {
return;
}
let Ok(block) = self.world().get_block(pick_item.pos).await else {
return;
};
if block.item_id == 0 {
// Invalid block id (blocks such as tall seagrass)
return;
}
let mut inventory = self.inventory().lock().await;
let source_slot = inventory.get_slot_with_item(block.item_id);
let mut dest_slot = inventory.get_pick_item_hotbar_slot();
let dest_slot_data = match inventory.get_slot(dest_slot + 36) {
Ok(Some(stack)) => Slot::from(&*stack),
_ => Slot::from(None),
};
// Early return if no source slot and not in creative mode
if source_slot.is_none() && self.gamemode.load() != GameMode::Creative {
return;
}
match source_slot {
Some(slot_index) if (36..=44).contains(&slot_index) => {
// Case where item is in hotbar
dest_slot = slot_index - 36;
}
Some(slot_index) => {
// Case where item is in inventory
// Update destination slot
let source_slot_data = match inventory.get_slot(slot_index) {
Ok(Some(stack)) => Slot::from(&*stack),
_ => return,
};
self.update_single_slot(&mut inventory, dest_slot + 36, source_slot_data)
.await;
// Update source slot
self.update_single_slot(&mut inventory, slot_index, dest_slot_data)
.await;
}
None if self.gamemode.load() == GameMode::Creative => {
// Case where item is not present, if in creative mode create the item
let item_stack = ItemStack::new(1, block.item_id);
let slot_data = Slot::from(&item_stack);
self.update_single_slot(&mut inventory, dest_slot + 36, slot_data)
.await;
// Check if there is any empty slot in the player inventory
if let Some(slot_index) = inventory.get_empty_slot() {
inventory.state_id += 1;
self.update_single_slot(&mut inventory, slot_index, dest_slot_data)
.await;
}
}
_ => return,
}
// Update held item
inventory.set_selected(dest_slot);
self.client
.send_packet(&CSetHeldItem::new(dest_slot as i8))
.await;
}
pub fn handle_pick_item_from_entity(&self, _pick_item: SPickItemFromEntity) {
// TODO: Implement and merge any redundant code with pick_item_from_block
}
pub async fn handle_player_command(&self, command: SPlayerCommand) {
if command.entity_id != self.entity_id().into() {
return;