feat: Implement hand swing animation for beds (#1025)

* feat: Swing hand like vanilla

* feat: Make it more vanilla

* fix: Remove comment

* fix: Stop action if the block it too be stopped

* fix: rename to match vanilla

* fix: Add missing comment

* fix: clippy

* fix: Use names from yarn mappings

* fix: Make clippy happy
This commit is contained in:
Rafael
2025-07-27 10:35:39 +02:00
committed by GitHub
parent 40296b6f86
commit 3d72b72639
19 changed files with 157 additions and 115 deletions

View File

@@ -38,10 +38,9 @@ use pumpkin_inventory::screen_handler::ScreenHandler;
use pumpkin_macros::send_cancellable;
use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::java::client::play::{
Animation, CBlockUpdate, CCommandSuggestions, CEntityAnimation, CEntityPositionSync, CHeadRot,
COpenSignEditor, CPingResponse, CPlayerInfoUpdate, CPlayerPosition, CSetSelectedSlot,
CSystemChatMessage, CUpdateEntityPos, CUpdateEntityPosRot, CUpdateEntityRot, InitChat,
PlayerAction,
CBlockUpdate, CCommandSuggestions, CEntityPositionSync, CHeadRot, COpenSignEditor,
CPingResponse, CPlayerInfoUpdate, CPlayerPosition, CSetSelectedSlot, CSystemChatMessage,
CUpdateEntityPos, CUpdateEntityPosRot, CUpdateEntityRot, InitChat, PlayerAction,
};
use pumpkin_protocol::java::server::play::{
Action, ActionType, CommandBlockMode, FLAG_ON_GROUND, SChangeGameMode, SChatCommand,
@@ -60,6 +59,7 @@ use pumpkin_world::block::entities::command_block::CommandBlockEntity;
use pumpkin_world::block::entities::sign::SignBlockEntity;
use pumpkin_world::item::ItemStack;
use pumpkin_world::world::BlockFlags;
use tokio::sync::Mutex;
use uuid::Uuid;
/// In secure chat mode, Player will be kicked if they send a chat message with a timestamp that is older than this (in ms)
@@ -69,6 +69,7 @@ const CHAT_MESSAGE_MAX_AGE: i64 = 1000 * 60 * 2;
#[derive(Debug, Error)]
pub enum BlockPlacingError {
BlockOutOfReach,
InvalidHand,
InvalidBlockFace,
BlockOutOfWorld,
InvalidGamemode,
@@ -84,14 +85,14 @@ impl PumpkinError for BlockPlacingError {
fn is_kick(&self) -> bool {
match self {
Self::BlockOutOfReach | Self::BlockOutOfWorld | Self::InvalidGamemode => false,
Self::InvalidBlockFace => true,
Self::InvalidBlockFace | Self::InvalidHand => true,
}
}
fn severity(&self) -> log::Level {
match self {
Self::BlockOutOfWorld | Self::InvalidGamemode => log::Level::Trace,
Self::BlockOutOfReach | Self::InvalidBlockFace => log::Level::Warn,
Self::BlockOutOfReach | Self::InvalidBlockFace | Self::InvalidHand => log::Level::Warn,
}
}
@@ -99,6 +100,7 @@ impl PumpkinError for BlockPlacingError {
match self {
Self::BlockOutOfReach | Self::BlockOutOfWorld | Self::InvalidGamemode => None,
Self::InvalidBlockFace => Some("Invalid block face".into()),
Self::InvalidHand => Some("Invalid hand".into()),
}
}
}
@@ -693,26 +695,10 @@ impl JavaClient {
}
pub async fn handle_swing_arm(&self, player: &Arc<Player>, swing_arm: SSwingArm) {
let animation = match swing_arm.hand.0 {
0 => Animation::SwingMainArm,
1 => Animation::SwingOffhand,
_ => {
self.kick(TextComponent::text("Invalid hand")).await;
return;
}
let Ok(hand) = Hand::try_from(swing_arm.hand.0) else {
self.kick(TextComponent::text("Invalid hand")).await;
return;
};
// Invert hand if player is left handed
let animation = match player.config.read().await.main_hand {
Hand::Left => match animation {
Animation::SwingMainArm => Animation::SwingOffhand,
Animation::SwingOffhand => Animation::SwingMainArm,
_ => unreachable!(),
},
Hand::Right => animation,
};
let id = player.entity_id();
let world = player.world().await;
let inventory = player.inventory();
let item = inventory.held_item();
@@ -754,12 +740,7 @@ impl JavaClient {
send_cancellable! {{
event;
'after: {
world
.broadcast_packet_except(
&[player.gameprofile.id],
&CEntityAnimation::new(id.into(), animation),
)
.await;
player.swing_hand(hand, false).await;
}
}}
}
@@ -1398,70 +1379,53 @@ impl JavaClient {
let Ok(face) = BlockDirection::try_from(use_item_on.face.0) else {
return Err(BlockPlacingError::InvalidBlockFace);
};
let Ok(hand) = Hand::try_from(use_item_on.hand.0) else {
return Err(BlockPlacingError::InvalidHand);
};
//TODO this.player.resetLastActionTime();
//TODO this.gameModeForPlayer == GameType.SPECTATOR
let inventory = player.inventory();
let held_item = inventory.held_item();
let off_hand_item = inventory.off_hand_item().await;
let held_item_empty = held_item.lock().await.is_empty();
let off_hand_item_empty = off_hand_item.lock().await.is_empty();
let item = if use_item_on.hand.0 == 0 {
let item = if matches!(hand, Hand::Left) {
held_item
} else {
off_hand_item
};
let entity = &player.living_entity.entity;
let world = &entity.world.read().await;
let world = entity.world.read().await;
let block = world.get_block(&position).await;
let sneaking = player.living_entity.entity.sneaking.load(Ordering::Relaxed);
// Code based on the java class ServerPlayerInteractionManager
if !(sneaking && (!held_item_empty || !off_hand_item_empty)) {
match match server
.block_registry
.use_with_item(
block,
let result = self
.call_use_item_on(
player,
&position,
&BlockHitResult {
side: &face,
cursor_pos: &cursor_pos,
},
&cursor_pos,
&face,
&item,
&world,
block,
server,
world,
)
.await
{
BlockActionResult::PassToDefault => {
server
.block_registry
.on_use(
block,
player,
&position,
&BlockHitResult {
side: &face,
cursor_pos: &cursor_pos,
},
server,
world,
)
.await
.await;
if result.consumes_action() {
// TODO: Trigger ANY_BLOCK_USE Criteria
if matches!(result, BlockActionResult::SuccessServer) {
drop(world); // IMPORTANT: We need to drop this to prevent a deadlock
player.swing_hand(hand, true).await;
}
BlockActionResult::Fail => BlockActionResult::Fail,
BlockActionResult::Consume => BlockActionResult::Consume,
BlockActionResult::Continue => BlockActionResult::Continue,
BlockActionResult::Success => BlockActionResult::Success,
} {
BlockActionResult::Fail => return Ok(()),
BlockActionResult::Success | BlockActionResult::Consume => {
/* TODO: Swing hand */
return Ok(());
}
BlockActionResult::Continue | BlockActionResult::PassToDefault => {} // Do nothing,
return Ok(());
}
}
@@ -1509,6 +1473,58 @@ impl JavaClient {
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn call_use_item_on(
&self,
player: &Player,
position: &BlockPos,
cursor_pos: &Vector3<f32>,
face: &BlockDirection,
held_item: &Arc<Mutex<ItemStack>>,
world: &Arc<World>,
block: &Block,
server: &Arc<Server>,
) -> BlockActionResult {
let result = server
.block_registry
.use_with_item(
block,
player,
position,
&BlockHitResult { face, cursor_pos },
held_item,
server,
world,
)
.await;
if result.consumes_action() {
// TODO: Trigger ITEM_USED_ON_BLOCK Criteria
return result;
}
if matches!(result, BlockActionResult::PassToDefaultBlockAction) {
let result = server
.block_registry
.on_use(
block,
player,
position,
&BlockHitResult { face, cursor_pos },
server,
world,
)
.await;
if result.consumes_action() {
// TODO: Trigger DEFAULT_BLOCK_USE Criteria
return result;
}
}
BlockActionResult::Pass
}
pub async fn handle_sign_update(&self, player: &Player, sign_data: SUpdateSign) {
let world = &player.living_entity.entity.world.read().await;
let updated_sign = SignBlockEntity::new(