From a60a3ba2438f2c4526fe5777bc87750fb5abff4c Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Sat, 1 Mar 2025 16:58:02 +0100 Subject: [PATCH] Add Experience Orb and drop it when breaking Blocks --- .../src/client/play/set_experience.rs | 2 +- pumpkin-util/Cargo.toml | 2 + pumpkin-util/src/math/int_provider.rs | 63 ++++++++++++ pumpkin-util/src/math/mod.rs | 1 + pumpkin-world/src/block/registry.rs | 6 ++ pumpkin/src/block/mod.rs | 20 +++- pumpkin/src/entity/experience_orb.rs | 98 +++++++++++++++++++ pumpkin/src/entity/item.rs | 19 ++-- pumpkin/src/entity/living.rs | 17 +++- pumpkin/src/entity/mod.rs | 1 + pumpkin/src/entity/player.rs | 12 ++- pumpkin/src/world/explosion.rs | 2 +- pumpkin/src/world/mod.rs | 12 +-- 13 files changed, 225 insertions(+), 30 deletions(-) create mode 100644 pumpkin-util/src/math/int_provider.rs create mode 100644 pumpkin/src/entity/experience_orb.rs diff --git a/pumpkin-protocol/src/client/play/set_experience.rs b/pumpkin-protocol/src/client/play/set_experience.rs index 692249100..fb8aa35dc 100644 --- a/pumpkin-protocol/src/client/play/set_experience.rs +++ b/pumpkin-protocol/src/client/play/set_experience.rs @@ -8,8 +8,8 @@ use crate::VarInt; #[packet(PLAY_SET_EXPERIENCE)] pub struct CSetExperience { progress: f32, - level: VarInt, total_experience: VarInt, + level: VarInt, } impl CSetExperience { diff --git a/pumpkin-util/Cargo.toml b/pumpkin-util/Cargo.toml index 294e20eef..1617dc26f 100644 --- a/pumpkin-util/Cargo.toml +++ b/pumpkin-util/Cargo.toml @@ -8,6 +8,8 @@ pumpkin-nbt = { path = "../pumpkin-nbt" } serde.workspace = true serde_json.workspace = true bytes.workspace = true +rand = "0.9" + num-traits = "0.2" diff --git a/pumpkin-util/src/math/int_provider.rs b/pumpkin-util/src/math/int_provider.rs new file mode 100644 index 000000000..b7f447d1f --- /dev/null +++ b/pumpkin-util/src/math/int_provider.rs @@ -0,0 +1,63 @@ +use serde::Deserialize; + +#[derive(Deserialize, Clone)] +#[serde(tag = "type")] +pub enum NormalInvProvider { + #[serde(rename = "minecraft:uniform")] + Uniform(UniformIntProvider), + // TODO: Add more... +} + +#[derive(Deserialize, Clone)] +#[serde(untagged)] +pub enum InvProvider { + Object(NormalInvProvider), + Constant(i32), +} + +impl InvProvider { + pub fn get_min(&self) -> i32 { + match self { + InvProvider::Object(inv_provider) => match inv_provider { + NormalInvProvider::Uniform(uniform) => uniform.get_min(), + }, + InvProvider::Constant(i) => *i, + } + } + + pub fn get(&self) -> i32 { + match self { + InvProvider::Object(inv_provider) => match inv_provider { + NormalInvProvider::Uniform(uniform) => uniform.get(), + }, + InvProvider::Constant(i) => *i, + } + } + + pub fn get_max(&self) -> i32 { + match self { + InvProvider::Object(inv_provider) => match inv_provider { + NormalInvProvider::Uniform(uniform) => uniform.get_max(), + }, + InvProvider::Constant(i) => *i, + } + } +} + +#[derive(Deserialize, Clone)] +pub struct UniformIntProvider { + min_inclusive: i32, + max_inclusive: i32, +} + +impl UniformIntProvider { + pub fn get_min(&self) -> i32 { + self.min_inclusive + } + pub fn get(&self) -> i32 { + rand::random_range(self.min_inclusive..self.max_inclusive) + } + pub fn get_max(&self) -> i32 { + self.max_inclusive + } +} diff --git a/pumpkin-util/src/math/mod.rs b/pumpkin-util/src/math/mod.rs index 17b8523bb..114927e1e 100644 --- a/pumpkin-util/src/math/mod.rs +++ b/pumpkin-util/src/math/mod.rs @@ -2,6 +2,7 @@ use num_traits::{One, PrimInt, Zero}; pub mod boundingbox; pub mod experience; +pub mod int_provider; pub mod position; pub mod vector2; pub mod vector3; diff --git a/pumpkin-world/src/block/registry.rs b/pumpkin-world/src/block/registry.rs index c44bfb755..06d11e99b 100644 --- a/pumpkin-world/src/block/registry.rs +++ b/pumpkin-world/src/block/registry.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::sync::LazyLock; +use pumpkin_util::math::int_provider::InvProvider; use serde::Deserialize; use crate::loot::LootTable; @@ -118,6 +119,7 @@ pub struct Block { pub id: u16, pub item_id: u16, pub hardness: f32, + pub experience: Option, pub blast_resistance: f32, pub wall_variant_id: Option, pub translation_key: String, @@ -127,6 +129,10 @@ pub struct Block { pub default_state_id: u16, pub states: Vec, } +#[derive(Deserialize, Clone)] +pub struct Experience { + pub experience: InvProvider, +} #[derive(Deserialize, Clone, Debug)] pub struct Property { pub name: String, diff --git a/pumpkin/src/block/mod.rs b/pumpkin/src/block/mod.rs index 5985659ac..fe9af9cc5 100644 --- a/pumpkin/src/block/mod.rs +++ b/pumpkin/src/block/mod.rs @@ -25,12 +25,12 @@ use pumpkin_world::block::registry::{Block, State}; use pumpkin_world::item::ItemStack; use rand::Rng; -use crate::block::blocks::jukebox::JukeboxBlock; use crate::block::registry::BlockRegistry; use crate::entity::item::ItemEntity; use crate::server::Server; use crate::world::World; use crate::{block::blocks::crafting_table::CraftingTableBlock, entity::player::Player}; +use crate::{block::blocks::jukebox::JukeboxBlock, entity::experience_orb::ExperienceOrbEntity}; use std::sync::Arc; mod blocks; @@ -52,7 +52,13 @@ pub fn default_registry() -> Arc { Arc::new(manager) } -pub async fn drop_loot(server: &Server, world: &Arc, block: &Block, pos: &BlockPos) { +pub async fn drop_loot( + server: &Server, + world: &Arc, + block: &Block, + pos: &BlockPos, + experience: bool, +) { // TODO: Currently only the item block is dropped, We should drop the loop table let height = EntityType::ITEM.dimension[1] / 2.0; let pos = Vector3::new( @@ -68,6 +74,16 @@ pub async fn drop_loot(server: &Server, world: &Arc, block: &Block, pos: )); world.spawn_entity(item_entity.clone()).await; item_entity.send_meta_packet().await; + + if experience { + if let Some(experience) = &block.experience { + let amount = experience.experience.get(); + // TODO: Silk touch gives no exp + if amount > 0 { + ExperienceOrbEntity::spawn(world, server, pos, amount as u32).await; + } + } + } } pub async fn calc_block_breaking(player: &Player, state: &State, block_name: &str) -> f32 { diff --git a/pumpkin/src/entity/experience_orb.rs b/pumpkin/src/entity/experience_orb.rs new file mode 100644 index 000000000..f8af8c677 --- /dev/null +++ b/pumpkin/src/entity/experience_orb.rs @@ -0,0 +1,98 @@ +use std::sync::{Arc, atomic::AtomicU32}; + +use async_trait::async_trait; +use pumpkin_data::{damage::DamageType, entity::EntityType}; +use pumpkin_util::math::vector3::Vector3; + +use crate::{server::Server, world::World}; + +use super::{Entity, EntityBase, living::LivingEntity, player::Player}; + +pub struct ExperienceOrbEntity { + entity: Entity, + amount: u32, + orb_age: AtomicU32, +} + +impl ExperienceOrbEntity { + pub fn new(entity: Entity, amount: u32) -> Self { + entity.yaw.store(rand::random::() * 360.0); + Self { + entity, + amount, + orb_age: AtomicU32::new(0), + } + } + + pub async fn spawn(world: &Arc, server: &Server, position: Vector3, amount: u32) { + let mut amount = amount; + while amount > 0 { + let i = Self::round_to_orb_size(amount); + amount -= i; + let entity = server.add_entity(position, EntityType::EXPERIENCE_ORB, world); + let orb = Arc::new(Self::new(entity, i)); + world.spawn_entity(orb).await; + } + } + + fn round_to_orb_size(value: u32) -> u32 { + if value >= 2477 { + 2477 + } else if value >= 1237 { + 1237 + } else if value >= 617 { + 617 + } else if value >= 307 { + 307 + } else if value >= 149 { + 149 + } else if value >= 73 { + 73 + } else if value >= 37 { + 37 + } else if value >= 17 { + 17 + } else if value >= 7 { + 7 + } else if value >= 3 { + 3 + } else { + 1 + } + } +} + +#[async_trait] +impl EntityBase for ExperienceOrbEntity { + async fn tick(&self, _: &Server) { + let age = self + .orb_age + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + if age >= 6000 { + self.entity.remove().await; + } + } + + fn get_entity(&self) -> &Entity { + &self.entity + } + + async fn on_player_collision(&self, player: Arc) { + let mut delay = player.experience_pick_up_delay.lock().await; + if *delay == 0 { + *delay = 2; + player.living_entity.pickup(&self.entity, 1).await; + player.add_experience_points(self.amount as i32).await; + // TODO: pickingCount for merging + self.entity.remove().await; + } + } + + async fn damage(&self, _amount: f32, _damage_type: DamageType) -> bool { + false + } + + fn get_living_entity(&self) -> Option<&LivingEntity> { + None + } +} diff --git a/pumpkin/src/entity/item.rs b/pumpkin/src/entity/item.rs index 0f84482ea..49d9e920b 100644 --- a/pumpkin/src/entity/item.rs +++ b/pumpkin/src/entity/item.rs @@ -2,7 +2,7 @@ use crate::server::Server; use async_trait::async_trait; use pumpkin_data::damage::DamageType; use pumpkin_protocol::{ - client::play::{CTakeItemEntity, MetaDataType, Metadata}, + client::play::{MetaDataType, Metadata}, codec::slot::Slot, }; use pumpkin_world::item::ItemStack; @@ -23,6 +23,7 @@ pub struct ItemEntity { impl ItemEntity { pub fn new(entity: Entity, stack: ItemStack) -> Self { + entity.yaw.store(rand::random::() * 360.0); Self { entity, item: stack, @@ -85,12 +86,8 @@ impl EntityBase for ItemEntity { item.item_count = stack.item_count; player - .client - .send_packet(&CTakeItemEntity::new( - self.entity.entity_id.into(), - player.entity_id().into(), - item.item_count.into(), - )) + .living_entity + .pickup(&self.entity, u32::from(item.item_count)) .await; self.entity.remove().await; } @@ -99,12 +96,8 @@ impl EntityBase for ItemEntity { item.item_count = self.count.load(std::sync::atomic::Ordering::Relaxed); player - .client - .send_packet(&CTakeItemEntity::new( - self.entity.entity_id.into(), - player.entity_id().into(), - item.item_count.into(), - )) + .living_entity + .pickup(&self.entity, u32::from(item.item_count)) .await; self.entity.remove().await; } diff --git a/pumpkin/src/entity/living.rs b/pumpkin/src/entity/living.rs index bb5ba54e2..da9847337 100644 --- a/pumpkin/src/entity/living.rs +++ b/pumpkin/src/entity/living.rs @@ -8,7 +8,7 @@ use pumpkin_config::ADVANCED_CONFIG; use pumpkin_data::entity::{EffectType, EntityStatus}; use pumpkin_data::{damage::DamageType, sound::Sound}; use pumpkin_nbt::tag::NbtTag; -use pumpkin_protocol::client::play::CHurtAnimation; +use pumpkin_protocol::client::play::{CHurtAnimation, CTakeItemEntity}; use pumpkin_protocol::codec::var_int::VarInt; use pumpkin_protocol::{ client::play::{CDamageEvent, CSetEquipment, EquipmentSlot, MetaDataType, Metadata}, @@ -70,6 +70,21 @@ impl LivingEntity { .await; } + /// Picks up and Item entity or XP Orb + pub async fn pickup(&self, item: &Entity, stack_amount: u32) { + // TODO: Only nearby + self.entity + .world + .read() + .await + .broadcast_packet_all(&CTakeItemEntity::new( + item.entity_id.into(), + self.entity.entity_id.into(), + stack_amount.into(), + )) + .await; + } + pub fn set_pos(&self, position: Vector3) { self.last_pos.store(self.entity.pos.load()); self.entity.set_pos(position); diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index 9f33fb1b4..828faf6b8 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -35,6 +35,7 @@ use crate::world::World; pub mod ai; pub mod effect; +pub mod experience_orb; pub mod hunger; pub mod item; pub mod living; diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 90c1c1356..4edaa4b0b 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -147,6 +147,7 @@ pub struct Player { pub experience_progress: AtomicCell, /// The player's total experience points pub experience_points: AtomicI32, + pub experience_pick_up_delay: Mutex, } impl Player { @@ -201,6 +202,7 @@ impl Player { packet_sequence: AtomicI32::new(-1), start_mining_time: AtomicI32::new(0), carried_item: AtomicCell::new(None), + experience_pick_up_delay: Mutex::new(0), teleport_id_count: AtomicI32::new(0), mining: AtomicBool::new(false), mining_pos: Mutex::new(BlockPos(Vector3::new(0, 0, 0))), @@ -449,6 +451,12 @@ impl Player { )) .await; } + { + let mut xp = self.experience_pick_up_delay.lock().await; + if *xp > 0 { + *xp -= 1; + } + } self.tick_counter.fetch_add(1, Ordering::Relaxed); @@ -1050,8 +1058,8 @@ impl Player { self.client .send_packet(&CSetExperience::new( progress.clamp(0.0, 1.0), - level.into(), points.into(), + level.into(), )) .await; } @@ -1140,7 +1148,7 @@ impl Player { let total_exp = experience::points_to_level(current_level) + current_points; let new_total_exp = total_exp + added_points; let (new_level, new_points) = experience::total_to_level_and_points(new_total_exp); - let progress = experience::progress_in_level(new_level, new_points); + let progress = experience::progress_in_level(new_points, new_level); self.set_experience(new_level, progress, new_points).await; } } diff --git a/pumpkin/src/world/explosion.rs b/pumpkin/src/world/explosion.rs index c61794c07..f385c0220 100644 --- a/pumpkin/src/world/explosion.rs +++ b/pumpkin/src/world/explosion.rs @@ -82,7 +82,7 @@ impl Explosion { let block = world.get_block(&pos).await.unwrap(); let pumpkin_block = server.block_registry.get_pumpkin_block(block); if pumpkin_block.is_none_or(|s| s.should_drop_items_on_explosion()) { - drop_loot(server, world, block, &pos).await; + drop_loot(server, world, block, &pos, false).await; } if let Some(pumpkin_block) = pumpkin_block { pumpkin_block.explode(block, world, pos, server).await; diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index 9ecf31b29..1e93cac92 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -1003,15 +1003,7 @@ impl World { } } - /// Adds a living entity to the world. - /// - /// This function takes a living entity's UUID and an `Arc` reference. - /// It inserts the living entity into the world's `current_living_entities` map using the UUID as the key. - /// - /// # Arguments - /// - /// * `uuid`: The unique UUID of the living entity to add. - /// * `living_entity`: A `Arc` reference to the living entity object. + /// Adds a entity to the world. pub async fn spawn_entity(&self, entity: Arc) { let base_entity = entity.get_entity(); self.broadcast_packet_all(&base_entity.create_spawn_packet()) @@ -1120,7 +1112,7 @@ impl World { ); if drop { - block::drop_loot(server, self, block, position).await; + block::drop_loot(server, self, block, position, true).await; } match cause {