fix: 26.1 attack packet

This commit is contained in:
Alexander Medvedev 2026-04-02 20:55:03 +02:00
parent 7f95d43982
commit 8a6698cd21
18 changed files with 164 additions and 89 deletions

View File

@ -110,6 +110,13 @@ impl ItemStack {
patch: Vec::new(),
};
pub fn split_off(&mut self, amount: u8) -> Self {
let count = amount.min(self.item_count);
let result = self.copy_with_count(count);
self.decrement(count);
result
}
#[must_use]
pub fn get_max_stack_size(&self) -> u8 {
self.get_data_component::<MaxStackSizeImpl>()

View File

@ -1,7 +1,7 @@
use std::{any::Any, pin::Pin, sync::Arc};
use pumpkin_data::screen::WindowType;
use pumpkin_data::tag::Taggable;
use pumpkin_data::{screen::WindowType, tag};
use pumpkin_world::{block::entities::PropertyDelegate, inventory::Inventory};
use crate::{
@ -122,10 +122,7 @@ impl ScreenHandler for BrewingScreenHandler {
.is_some();
// Check if item is brewing fuel (for slot 4)
let is_fuel = stack
.get_item()
.is_tagged_with("minecraft:brewing_fuel")
.is_some_and(|b| b);
let is_fuel = stack.get_item().has_tag(&tag::Item::MINECRAFT_BREWING_FUEL);
if has_potion_contents {
// Try to insert into potion slots (0-2)

View File

@ -13,9 +13,9 @@ use serde::{
};
#[derive(Clone, Copy)]
pub struct Velocity(pub Vector3<f64>);
pub struct LpVector3d(pub Vector3<f64>);
impl Velocity {
impl LpVector3d {
pub fn write<W: std::io::Write>(&self, writer: &mut W) -> Result<(), WritingError> {
let velocity = self.0;
@ -153,7 +153,7 @@ fn from_long(quantized: i64, scale: f64) -> f64 {
(normalized / 0.5) * scale
}
impl Serialize for Velocity {
impl Serialize for LpVector3d {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut buf = Vec::new();
self.write(&mut buf).unwrap();
@ -161,7 +161,7 @@ impl Serialize for Velocity {
}
}
impl<'de> de::Deserialize<'de> for Velocity {
impl<'de> de::Deserialize<'de> for LpVector3d {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
@ -169,7 +169,7 @@ impl<'de> de::Deserialize<'de> for Velocity {
struct VelocityVisitor;
impl Visitor<'_> for VelocityVisitor {
type Value = Velocity;
type Value = LpVector3d;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("a byte array representing bit-packed velocity")
@ -180,7 +180,7 @@ impl<'de> de::Deserialize<'de> for Velocity {
E: de::Error,
{
let mut cursor = std::io::Cursor::new(v);
Velocity::read(&mut cursor).map_err(de::Error::custom)
LpVector3d::read(&mut cursor).map_err(de::Error::custom)
}
}
@ -190,7 +190,7 @@ impl<'de> de::Deserialize<'de> for Velocity {
#[cfg(test)]
mod tests {
use super::{Velocity, encode_legacy_velocity_component};
use super::{LpVector3d, encode_legacy_velocity_component};
use pumpkin_util::math::vector3::Vector3;
#[test]
@ -203,7 +203,7 @@ mod tests {
#[test]
fn write_legacy_writes_three_i16_be_components() {
let velocity = Velocity(Vector3::new(0.5, -0.5, 0.0));
let velocity = LpVector3d(Vector3::new(0.5, -0.5, 0.0));
let mut buf = Vec::new();
velocity.write_legacy(&mut buf).unwrap();
assert_eq!(buf, vec![0x0F, 0xA0, 0xF0, 0x60, 0x00, 0x00]);

View File

@ -4,6 +4,7 @@ pub mod bitset;
pub mod data_component;
pub mod item_stack_seralizer;
pub mod little_endian;
pub mod lp_vector_3d;
pub mod optional_int;
mod u24_type;
pub mod uuid;
@ -11,6 +12,5 @@ pub mod var_int;
pub mod var_long;
pub mod var_uint;
pub mod var_ulong;
pub mod velocity;
pub use u24_type::u24;

View File

@ -4,7 +4,9 @@ use pumpkin_data::packet::clientbound::PLAY_SET_ENTITY_MOTION;
use pumpkin_macros::java_packet;
use pumpkin_util::{math::vector3::Vector3, version::MinecraftVersion};
use crate::{ClientPacket, VarInt, WritingError, codec::velocity::Velocity, ser::NetworkWriteExt};
use crate::{
ClientPacket, VarInt, WritingError, codec::lp_vector_3d::LpVector3d, ser::NetworkWriteExt,
};
/// Updates the velocity of an entity.
///
@ -15,7 +17,7 @@ pub struct CEntityVelocity {
/// The Entity ID of the entity whose velocity is being set
pub entity_id: VarInt,
/// The velocity vector
pub velocity: Velocity,
pub velocity: LpVector3d,
}
impl CEntityVelocity {
@ -23,7 +25,7 @@ impl CEntityVelocity {
pub const fn new(entity_id: VarInt, velocity: Vector3<f64>) -> Self {
Self {
entity_id,
velocity: Velocity(velocity),
velocity: LpVector3d(velocity),
}
}
}
@ -56,7 +58,7 @@ mod tests {
use super::CEntityVelocity;
use crate::{
ClientPacket, VarInt,
codec::velocity::{Velocity, encode_legacy_velocity_component},
codec::lp_vector_3d::{LpVector3d, encode_legacy_velocity_component},
};
fn encode_packet(version: MinecraftVersion) -> Vec<u8> {
@ -95,7 +97,7 @@ mod tests {
// Ensure the packed bytes can still be decoded back to a velocity.
let mut cursor = std::io::Cursor::new(&encoded[1..]);
let decoded = Velocity::read(&mut cursor).unwrap();
let decoded = LpVector3d::read(&mut cursor).unwrap();
assert!(decoded.0.x.is_finite());
assert!(decoded.0.y.is_finite());
assert!(decoded.0.z.is_finite());

View File

@ -9,7 +9,7 @@ use pumpkin_util::{math::vector3::Vector3, version::MinecraftVersion};
use crate::{
ClientPacket, VarInt,
codec::velocity::Velocity,
codec::lp_vector_3d::LpVector3d,
ser::{NetworkWriteExt, WritingError},
};
@ -19,7 +19,7 @@ pub struct CSpawnEntity {
pub entity_uuid: uuid::Uuid,
pub r#type: VarInt,
pub position: Vector3<f64>,
pub velocity: Velocity,
pub velocity: LpVector3d,
pub pitch: u8, // angle
pub yaw: u8, // angle
pub head_yaw: u8, // angle
@ -49,7 +49,7 @@ impl CSpawnEntity {
yaw: (yaw.rem_euclid(360.0) * 256.0 / 360.0).floor() as u8,
head_yaw: (head_yaw.rem_euclid(360.0) * 256.0 / 360.0).floor() as u8,
data,
velocity: Velocity(velocity),
velocity: LpVector3d(velocity),
}
}
}
@ -100,7 +100,7 @@ impl ClientPacket for CSpawnEntity {
#[cfg(test)]
mod tests {
use super::CSpawnEntity;
use crate::{ClientPacket, VarInt, codec::velocity::encode_legacy_velocity_component};
use crate::{ClientPacket, VarInt, codec::lp_vector_3d::encode_legacy_velocity_component};
use pumpkin_util::version::MinecraftVersion;
fn legacy_tail(velocity: pumpkin_util::math::vector3::Vector3<f64>) -> [u8; 6] {

View File

@ -0,0 +1,11 @@
use pumpkin_data::packet::serverbound::PLAY_ATTACK;
use pumpkin_macros::java_packet;
use serde::Deserialize;
use crate::codec::var_int::VarInt;
#[derive(Deserialize)]
#[java_packet(PLAY_ATTACK)]
pub struct SAttack {
pub entity_id: VarInt,
}

View File

@ -6,7 +6,7 @@ use pumpkin_util::{math::vector3::Vector3, version::MinecraftVersion};
use crate::{
ServerPacket,
codec::var_int::VarInt,
codec::{lp_vector_3d::LpVector3d, var_int::VarInt},
ser::{NetworkReadExt, ReadingError},
};
@ -14,26 +14,42 @@ use crate::{
pub struct SInteract {
pub entity_id: VarInt,
pub r#type: VarInt,
pub target_position: Option<Vector3<f32>>,
pub target_position: Option<Vector3<f64>>,
pub hand: Option<VarInt>,
pub sneaking: bool,
}
// Great job Mojang ;D
impl ServerPacket for SInteract {
fn read(mut read: impl Read, _version: &MinecraftVersion) -> Result<Self, ReadingError> {
fn read(mut read: impl Read, version: &MinecraftVersion) -> Result<Self, ReadingError> {
// 26.1+ removes the 'type' field and uses doubles for location
if version >= &MinecraftVersion::V_26_1 {
let entity_id = read.get_var_int()?;
let hand = Some(read.get_var_int()?);
let target_position = Some(LpVector3d::read(&mut read)?.0);
let sneaking = read.get_bool()?;
return Ok(Self {
entity_id,
r#type: VarInt(2), // InteractAt for compatibility
target_position,
hand,
sneaking,
});
}
let entity_id = read.get_var_int()?;
let r#type = read.get_var_int()?;
let action = ActionType::try_from(r#type.0)
.map_err(|_| ReadingError::Message("invalid action type".to_string()))?;
let target_position: Option<Vector3<f32>> = match action {
let target_position: Option<Vector3<f64>> = match action {
ActionType::Interact | ActionType::Attack => None,
ActionType::InteractAt => Some(Vector3::new(
read.get_f32_be()?,
read.get_f32_be()?,
read.get_f32_be()?,
)),
ActionType::InteractAt => Some(
Vector3::new(read.get_f32_be()?, read.get_f32_be()?, read.get_f32_be()?).to_f64(),
),
};
let hand = match action {
ActionType::Interact | ActionType::InteractAt => Some(read.get_var_int()?),
ActionType::Attack => None,

View File

@ -1,3 +1,4 @@
mod attack;
mod change_game_mode;
mod chat_command;
mod chat_message;
@ -35,6 +36,7 @@ mod update_sign;
mod use_item;
mod use_item_on;
pub use attack::*;
pub use change_game_mode::*;
pub use chat_command::*;
pub use chat_message::*;

View File

@ -15,7 +15,7 @@ use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::potion_brewing::{ITEM_RECIPES, POTION_RECIPES};
use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_data::tag::Taggable;
use pumpkin_data::tag::{self, Taggable};
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3;
@ -294,20 +294,14 @@ impl crate::inventory::Inventory for BrewingStandBlockEntity {
// Slot 3 - ingredient (must be tagged as brewable)
3 => {
// Check if item is a valid brewing ingredient
if let Some(is_brewing_fuel) =
stack.get_item().is_tagged_with("minecraft:brewing_fuel")
&& is_brewing_fuel
{
if stack.get_item().has_tag(&tag::Item::MINECRAFT_BREWING_FUEL) {
return false; // Fuel should not go in ingredient slot
}
// Allow any item that's not fuel (ingredient validation happens during brewing)
true
}
// Slot 4 - fuel
4 => stack
.get_item()
.is_tagged_with("minecraft:brewing_fuel")
.is_some_and(|b| b),
4 => stack.get_item().has_tag(&tag::Item::MINECRAFT_BREWING_FUEL),
_ => false,
}
}
@ -422,8 +416,7 @@ impl crate::block::entities::BlockEntity for BrewingStandBlockEntity {
if !fuel_stack.is_empty()
&& fuel_stack
.get_item()
.is_tagged_with("minecraft:brewing_fuel")
.is_some_and(|b| b)
.has_tag(&tag::Item::MINECRAFT_BREWING_FUEL)
{
self.fuel.store(20, Ordering::Relaxed);
fuel_stack.decrement(1);

View File

@ -14,7 +14,6 @@ pub mod positions;
pub mod proto_chunk;
pub mod proto_chunk_test;
pub mod rule;
mod rule_test;
pub mod structure;
mod surface;

View File

@ -1,41 +0,0 @@
use pumpkin_data::{Block, tag::Taggable};
use serde::Deserialize;
/// Rule tests are used in structure or features generation to check if a block state matches some condition.
#[derive(Deserialize)]
pub enum RuleTest {
TagMatch(TagMatchTest),
BlockMatch(BlockMatchRuleTest),
// TODO: add more
}
pub struct AlwaysTrueRuleTest;
impl AlwaysTrueRuleTest {
pub const fn test() -> bool {
true
}
}
#[derive(Deserialize)]
pub struct BlockMatchRuleTest {
block: String,
}
impl BlockMatchRuleTest {
pub fn test(&self, block: &'static Block) -> bool {
let test_block = Block::from_registry_key(&self.block).expect("Failed to find block");
test_block == block
}
}
#[derive(Deserialize)]
pub struct TagMatchTest {
tag: String,
}
impl TagMatchTest {
pub fn test(&self, block: &'static Block) -> bool {
block.is_tagged_with(&self.tag).unwrap()
}
}

View File

@ -0,0 +1,26 @@
use std::sync::Arc;
use crate::entity::{
Entity, NBTStorage,
mob::{Mob, MobEntity},
};
pub struct EnderDragonEntity {
pub mob_entity: MobEntity,
}
impl EnderDragonEntity {
pub fn new(entity: Entity) -> Arc<Self> {
let mob_entity = MobEntity::new(entity);
let dragon = Self { mob_entity };
Arc::new(dragon)
}
}
impl NBTStorage for EnderDragonEntity {}
impl Mob for EnderDragonEntity {
fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity
}
}

View File

@ -1 +1,2 @@
pub mod ender_dragon;
pub mod wither;

View File

@ -4,6 +4,7 @@ use pumpkin_data::entity::EntityType;
use pumpkin_util::math::vector3::Vector3;
use uuid::Uuid;
use crate::entity::boss::ender_dragon::EnderDragonEntity;
use crate::entity::mob::zombie::zombie_villager::ZombieVillagerEntity;
use crate::{
entity::{
@ -66,7 +67,10 @@ pub async fn from_type(
id if id == EntityType::IRON_GOLEM.id => IronGolemEntity::new(entity).await,
id if id == EntityType::SHEEP.id => SheepEntity::new(entity).await,
id if id == EntityType::WOLF.id => WolfEntity::new(entity).await,
id if id == EntityType::WITHER.id => WitherEntity::new(entity).await,
id if id == EntityType::ENDER_DRAGON.id => EnderDragonEntity::new(entity),
id if id == EntityType::AREA_EFFECT_CLOUD.id => {
crate::entity::area_effect_cloud::AreaEffectCloudEntity::new(entity)
}

View File

@ -6,7 +6,7 @@ use crossbeam::atomic::AtomicCell;
use pumpkin_config::networking::compression::CompressionInfo;
use pumpkin_data::packet::CURRENT_MC_VERSION;
use pumpkin_protocol::java::server::play::{
SChangeGameMode, SChatCommand, SChatMessage, SChunkBatch, SClickSlot, SClientCommand,
SAttack, SChangeGameMode, SChatCommand, SChatMessage, SChunkBatch, SClickSlot, SClientCommand,
SClientInformationPlay, SClientTickEnd, SCloseContainer, SCommandSuggestion, SConfirmTeleport,
SCookieResponse as SPCookieResponse, SCustomPayload, SInteract, SKeepAlive, SMoveVehicle,
SPaddleBoat, SPickItemFromBlock, SPlayPingRequest, SPlayerAbilities, SPlayerAction,
@ -727,6 +727,10 @@ impl JavaClient {
self.handle_interact(player, SInteract::read(payload, &version)?, server)
.await;
}
id if id == SAttack::to_id(version) => {
self.handle_attack(player, SAttack::read(payload, &version)?, server)
.await;
}
id if id == SKeepAlive::to_id(version) => {
self.handle_keep_alive(player, SKeepAlive::read(payload, &version)?)
.await;

View File

@ -52,7 +52,7 @@ use pumpkin_protocol::java::client::play::{
CUpdateEntityPos, CUpdateEntityPosRot, CUpdateEntityRot, InitChat, PlayerAction,
};
use pumpkin_protocol::java::server::play::{
Action, ActionType, CommandBlockMode, FLAG_ON_GROUND, SChangeGameMode, SChatCommand,
Action, ActionType, CommandBlockMode, FLAG_ON_GROUND, SAttack, SChangeGameMode, SChatCommand,
SChatMessage, SChunkBatch, SClientCommand, SClientInformationPlay, SCloseContainer,
SCommandSuggestion, SConfirmTeleport, SCookieResponse as SPCookieResponse, SInteract,
SKeepAlive, SMoveVehicle, SPaddleBoat, SPickItemFromBlock, SPlayPingRequest, SPlayerAbilities,
@ -1249,6 +1249,60 @@ impl JavaClient {
}
}
pub async fn handle_attack(&self, player: &Arc<Player>, attack: SAttack, server: &Arc<Server>) {
if !player.has_client_loaded() {
return;
}
player.update_last_action_time();
let entity_id = attack.entity_id;
let player_entity = &player.living_entity.entity;
let world = player_entity.world.load_full();
let config = &server.advanced_config.pvp;
if !config.enabled {
return;
}
if entity_id.0 == player.entity_id() {
self.kick(TextComponent::translate(
translation::MULTIPLAYER_DISCONNECT_INVALID_ENTITY_ATTACKED,
[],
))
.await;
return;
}
let player_target = world.get_player_by_id(entity_id.0);
let target: Option<Arc<dyn EntityBase>> = player_target
.as_ref()
.map(|p| Arc::clone(p) as Arc<dyn EntityBase>)
.or_else(|| world.get_entity_by_id(entity_id.0));
let Some(target) = target else {
self.kick(TextComponent::translate(
translation::MULTIPLAYER_DISCONNECT_INVALID_ENTITY_ATTACKED,
[],
))
.await;
return;
};
if let Some(player_victim) = &player_target {
if player_victim.living_entity.health.load() <= 0.0 {
return;
}
if config.protect_creative && player_victim.gamemode.load() == GameMode::Creative {
world
.play_sound(
Sound::EntityPlayerAttackNodamage,
SoundCategory::Players,
&player_victim.position(),
)
.await;
return;
}
}
player.attack(target).await;
}
pub async fn handle_interact(
&self,
player: &Arc<Player>,

View File

@ -26,7 +26,7 @@ pub struct PlayerInteractEntityEvent {
pub action: ActionType,
/// The position on the entity that was clicked (only for `InteractAt`).
pub target_position: Option<Vector3<f32>>,
pub target_position: Option<Vector3<f64>>,
/// Whether the player was sneaking during the interaction.
pub sneaking: bool,
@ -37,7 +37,7 @@ impl PlayerInteractEntityEvent {
player: &Arc<Player>,
target: Arc<dyn EntityBase>,
action: ActionType,
target_position: Option<Vector3<f32>>,
target_position: Option<Vector3<f64>>,
sneaking: bool,
) -> Self {
Self {