diff --git a/pumpkin-data/src/item_stack/mod.rs b/pumpkin-data/src/item_stack/mod.rs index 6f2fc1954..fe01815b0 100644 --- a/pumpkin-data/src/item_stack/mod.rs +++ b/pumpkin-data/src/item_stack/mod.rs @@ -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::() diff --git a/pumpkin-inventory/src/brewing/brewing_screen_handler.rs b/pumpkin-inventory/src/brewing/brewing_screen_handler.rs index 1c7cca1bb..5a011a9fd 100644 --- a/pumpkin-inventory/src/brewing/brewing_screen_handler.rs +++ b/pumpkin-inventory/src/brewing/brewing_screen_handler.rs @@ -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) diff --git a/pumpkin-protocol/src/codec/velocity.rs b/pumpkin-protocol/src/codec/lp_vector_3d.rs similarity index 94% rename from pumpkin-protocol/src/codec/velocity.rs rename to pumpkin-protocol/src/codec/lp_vector_3d.rs index 1726d5767..d9ce9dc79 100644 --- a/pumpkin-protocol/src/codec/velocity.rs +++ b/pumpkin-protocol/src/codec/lp_vector_3d.rs @@ -13,9 +13,9 @@ use serde::{ }; #[derive(Clone, Copy)] -pub struct Velocity(pub Vector3); +pub struct LpVector3d(pub Vector3); -impl Velocity { +impl LpVector3d { pub fn 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(&self, serializer: S) -> Result { 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(deserializer: D) -> Result 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]); diff --git a/pumpkin-protocol/src/codec/mod.rs b/pumpkin-protocol/src/codec/mod.rs index b59836df1..bbc5e60bc 100644 --- a/pumpkin-protocol/src/codec/mod.rs +++ b/pumpkin-protocol/src/codec/mod.rs @@ -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; diff --git a/pumpkin-protocol/src/java/client/play/entity_velocity.rs b/pumpkin-protocol/src/java/client/play/entity_velocity.rs index 1d4e6cda7..fd3d95e4d 100644 --- a/pumpkin-protocol/src/java/client/play/entity_velocity.rs +++ b/pumpkin-protocol/src/java/client/play/entity_velocity.rs @@ -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) -> 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 { @@ -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()); diff --git a/pumpkin-protocol/src/java/client/play/spawn_entity.rs b/pumpkin-protocol/src/java/client/play/spawn_entity.rs index 7f83cdda9..c40acbd19 100644 --- a/pumpkin-protocol/src/java/client/play/spawn_entity.rs +++ b/pumpkin-protocol/src/java/client/play/spawn_entity.rs @@ -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, - 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) -> [u8; 6] { diff --git a/pumpkin-protocol/src/java/server/play/attack.rs b/pumpkin-protocol/src/java/server/play/attack.rs new file mode 100644 index 000000000..a9385a092 --- /dev/null +++ b/pumpkin-protocol/src/java/server/play/attack.rs @@ -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, +} diff --git a/pumpkin-protocol/src/java/server/play/interact.rs b/pumpkin-protocol/src/java/server/play/interact.rs index 613e8dfcf..ff7e641de 100644 --- a/pumpkin-protocol/src/java/server/play/interact.rs +++ b/pumpkin-protocol/src/java/server/play/interact.rs @@ -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>, + pub target_position: Option>, pub hand: Option, pub sneaking: bool, } // Great job Mojang ;D impl ServerPacket for SInteract { - fn read(mut read: impl Read, _version: &MinecraftVersion) -> Result { + fn read(mut read: impl Read, version: &MinecraftVersion) -> Result { + // 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> = match action { + + let target_position: Option> = 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, diff --git a/pumpkin-protocol/src/java/server/play/mod.rs b/pumpkin-protocol/src/java/server/play/mod.rs index 9faae6a1c..7f7d9c179 100644 --- a/pumpkin-protocol/src/java/server/play/mod.rs +++ b/pumpkin-protocol/src/java/server/play/mod.rs @@ -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::*; diff --git a/pumpkin-world/src/block/entities/brewing_stand.rs b/pumpkin-world/src/block/entities/brewing_stand.rs index 163f6ae87..d1946b310 100644 --- a/pumpkin-world/src/block/entities/brewing_stand.rs +++ b/pumpkin-world/src/block/entities/brewing_stand.rs @@ -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); diff --git a/pumpkin-world/src/generation/mod.rs b/pumpkin-world/src/generation/mod.rs index 6508d6b40..f6433fc89 100644 --- a/pumpkin-world/src/generation/mod.rs +++ b/pumpkin-world/src/generation/mod.rs @@ -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; diff --git a/pumpkin-world/src/generation/rule_test.rs b/pumpkin-world/src/generation/rule_test.rs deleted file mode 100644 index 3769a2676..000000000 --- a/pumpkin-world/src/generation/rule_test.rs +++ /dev/null @@ -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() - } -} diff --git a/pumpkin/src/entity/boss/ender_dragon.rs b/pumpkin/src/entity/boss/ender_dragon.rs new file mode 100644 index 000000000..1ebc046de --- /dev/null +++ b/pumpkin/src/entity/boss/ender_dragon.rs @@ -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 { + 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 + } +} diff --git a/pumpkin/src/entity/boss/mod.rs b/pumpkin/src/entity/boss/mod.rs index 4ad811449..3adebbbe3 100644 --- a/pumpkin/src/entity/boss/mod.rs +++ b/pumpkin/src/entity/boss/mod.rs @@ -1 +1,2 @@ +pub mod ender_dragon; pub mod wither; diff --git a/pumpkin/src/entity/type.rs b/pumpkin/src/entity/type.rs index 95af4c986..8bdd55ae2 100644 --- a/pumpkin/src/entity/type.rs +++ b/pumpkin/src/entity/type.rs @@ -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) } diff --git a/pumpkin/src/net/java/mod.rs b/pumpkin/src/net/java/mod.rs index d832b854e..560a7063f 100644 --- a/pumpkin/src/net/java/mod.rs +++ b/pumpkin/src/net/java/mod.rs @@ -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; diff --git a/pumpkin/src/net/java/play.rs b/pumpkin/src/net/java/play.rs index 9d6a8af6a..60a633a68 100644 --- a/pumpkin/src/net/java/play.rs +++ b/pumpkin/src/net/java/play.rs @@ -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, attack: SAttack, server: &Arc) { + 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> = player_target + .as_ref() + .map(|p| Arc::clone(p) as Arc) + .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, diff --git a/pumpkin/src/plugin/api/events/player/player_interact_entity_event.rs b/pumpkin/src/plugin/api/events/player/player_interact_entity_event.rs index ccda3acf3..c5fc7483d 100644 --- a/pumpkin/src/plugin/api/events/player/player_interact_entity_event.rs +++ b/pumpkin/src/plugin/api/events/player/player_interact_entity_event.rs @@ -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>, + pub target_position: Option>, /// Whether the player was sneaking during the interaction. pub sneaking: bool, @@ -37,7 +37,7 @@ impl PlayerInteractEntityEvent { player: &Arc, target: Arc, action: ActionType, - target_position: Option>, + target_position: Option>, sneaking: bool, ) -> Self { Self {