diff --git a/pumpkin-inventory/src/player.rs b/pumpkin-inventory/src/player.rs index f406ba5a..1305ea45 100644 --- a/pumpkin-inventory/src/player.rs +++ b/pumpkin-inventory/src/player.rs @@ -106,6 +106,10 @@ impl PlayerInventory { self.selected = slot; } + pub fn get_selected(&self) -> usize { + self.selected + 36 + } + pub fn held_item(&self) -> Option<&ItemStack> { debug_assert!((0..9).contains(&self.selected)); self.items[self.selected + 36 - 9].as_ref() diff --git a/pumpkin-world/src/entity/entity_registry.rs b/pumpkin-world/src/entity/entity_registry.rs new file mode 100644 index 00000000..f393b08c --- /dev/null +++ b/pumpkin-world/src/entity/entity_registry.rs @@ -0,0 +1,35 @@ +use std::{collections::HashMap, sync::LazyLock}; + +use serde::Deserialize; + +const ENTITIES_JSON: &str = include_str!("../../../assets/entities.json"); + +pub static ENTITIES: LazyLock> = LazyLock::new(|| { + serde_json::from_str(ENTITIES_JSON).expect("Could not parse entity.json registry.") +}); + +pub static ENTITIES_BY_ID: LazyLock> = LazyLock::new(|| { + let mut map = HashMap::new(); + for (entity_name, entity) in ENTITIES.iter() { + map.insert(entity_name.clone(), entity.id); + } + map +}); + +pub fn get_entity_id(name: &str) -> Option<&u16> { + ENTITIES_BY_ID.get(&name.replace("minecraft:", "")) +} + +pub fn get_entity_by_id<'a>(entity_id: u16) -> Option<&'a Entity> { + ENTITIES.values().find(|&entity| entity.id == entity_id) +} + +#[derive(Deserialize, Clone, Debug)] +pub struct Entity { + pub id: u16, + pub max_health: Option, + pub attackable: bool, + pub summonable: bool, + pub fire_immune: bool, + pub dimension: [f32; 2], +} diff --git a/pumpkin-world/src/entity/mod.rs b/pumpkin-world/src/entity/mod.rs new file mode 100644 index 00000000..30282ee5 --- /dev/null +++ b/pumpkin-world/src/entity/mod.rs @@ -0,0 +1 @@ +pub mod entity_registry; diff --git a/pumpkin-world/src/item/item_registry.rs b/pumpkin-world/src/item/item_registry.rs index e393acc1..704840c3 100644 --- a/pumpkin-world/src/item/item_registry.rs +++ b/pumpkin-world/src/item/item_registry.rs @@ -9,15 +9,30 @@ pub static ITEMS: LazyLock> = LazyLock::new(|| { serde_json::from_str(ITEMS_JSON).expect("Could not parse items.json registry.") }); +pub static ITEMS_REGISTRY_NAME_BY_ID: LazyLock> = LazyLock::new(|| { + let mut map = HashMap::new(); + for item in ITEMS.clone() { + map.insert(item.1.id, item.0.clone()); + } + map +}); + pub fn get_item(name: &str) -> Option<&Item> { ITEMS.get(&name.replace("minecraft:", "")) } -pub fn get_item_by_id<'a>(id: u16) -> Option<&'a Item> { - let item = ITEMS.iter().find(|item| item.1.id == id); - if let Some(item) = item { - return Some(item.1); - } +pub fn get_item_by_id<'a>(item_id: u16) -> Option<&'a Item> { + ITEMS.values().find(|&item| item.id == item_id) +} + +pub fn get_spawn_egg(item_id: u16) -> Option { + if let Some(item_name) = ITEMS_REGISTRY_NAME_BY_ID.get(&item_id) { + if item_name.ends_with("_spawn_egg") { + if let Some(res) = item_name.strip_suffix("_spawn_egg") { + return Some(res.to_owned()); + } + } + }; None } diff --git a/pumpkin-world/src/lib.rs b/pumpkin-world/src/lib.rs index ec23b151..c80fa97a 100644 --- a/pumpkin-world/src/lib.rs +++ b/pumpkin-world/src/lib.rs @@ -13,6 +13,7 @@ pub mod chunk; pub mod coordinates; pub mod cylindrical_chunk_iterator; pub mod dimension; +pub mod entity; mod generation; pub mod item; pub mod level; diff --git a/pumpkin/src/entity/living.rs b/pumpkin/src/entity/living.rs index e116ef43..19a1bcb6 100644 --- a/pumpkin/src/entity/living.rs +++ b/pumpkin/src/entity/living.rs @@ -2,6 +2,7 @@ use std::sync::atomic::AtomicI32; use crossbeam::atomic::AtomicCell; use pumpkin_core::math::vector3::Vector3; +use pumpkin_entity::EntityId; use pumpkin_inventory::{Container, EmptyContainer}; use pumpkin_protocol::client::play::{CDamageEvent, CEntityStatus, CSetEntityMetadata, Metadata}; use tokio::sync::Mutex; @@ -85,6 +86,10 @@ impl LivingEntity { .await; } + pub const fn entity_id(&self) -> EntityId { + self.entity.entity_id + } + // TODO add damage_type enum pub async fn damage(&self, amount: f32, damage_type: u8) { self.entity diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index 649144a7..bb4bea4c 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -24,6 +24,8 @@ pub mod player; pub struct Entity { /// A unique identifier for the entity pub entity_id: EntityId, + /// A persistant, unique identifier for the entity + pub entity_uuid: uuid::Uuid, /// The type of entity (e.g., player, zombie, item) pub entity_type: EntityType, /// The world in which the entity exists. @@ -63,6 +65,7 @@ pub struct Entity { impl Entity { pub fn new( entity_id: EntityId, + entity_uuid: uuid::Uuid, world: Arc, entity_type: EntityType, standing_eye_height: f32, @@ -71,6 +74,7 @@ impl Entity { ) -> Self { Self { entity_id, + entity_uuid, entity_type, on_ground: AtomicBool::new(false), pos: AtomicCell::new(Vector3::new(0.0, 0.0, 0.0)), diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 5d58a7d7..5e9c88e2 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -133,11 +133,12 @@ impl Player { entity_id: EntityId, gamemode: GameMode, ) -> Self { + let player_uuid = uuid::Uuid::new_v4(); let gameprofile = client.gameprofile.lock().await.clone().map_or_else( || { log::error!("Client {} has no game profile!", client.id); GameProfile { - id: uuid::Uuid::new_v4(), + id: player_uuid, name: String::new(), properties: vec![], profile_actions: None, @@ -157,6 +158,7 @@ impl Player { living_entity: LivingEntity::new_with_container( Entity::new( entity_id, + player_uuid, world, EntityType::Player, 1.62, diff --git a/pumpkin/src/net/container.rs b/pumpkin/src/net/container.rs index c157b5e2..9bb08803 100644 --- a/pumpkin/src/net/container.rs +++ b/pumpkin/src/net/container.rs @@ -200,6 +200,22 @@ impl Player { Ok(()) } + pub async fn handle_decrease_item( + &self, + _server: &Server, + slot_index: usize, + item_stack: Option<&ItemStack>, + state_id: &mut u32, + ) -> Result<(), InventoryError> { + // TODO: this will not update hotbar when server admin is peeking + // TODO: check and iterate over all players in player inventory + let slot = Slot::from(item_stack); + *state_id += 1; + let packet = CSetContainerSlot::new(0, *state_id as i32, slot_index, &slot); + self.client.send_packet(&packet).await; + Ok(()) + } + async fn match_click_behaviour( &self, opened_container: Option<&mut Box>, diff --git a/pumpkin/src/net/packet/play.rs b/pumpkin/src/net/packet/play.rs index e1be4968..87130e53 100644 --- a/pumpkin/src/net/packet/play.rs +++ b/pumpkin/src/net/packet/play.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use crate::block::block_manager::BlockActionResult; use crate::net::PlayerConfig; +use crate::world::World; use crate::{ command::CommandSender, entity::player::{ChatMode, Hand, Player}, @@ -18,9 +19,10 @@ use pumpkin_core::{ text::TextComponent, GameMode, }; +use pumpkin_entity::entity_type::EntityType; use pumpkin_inventory::player::PlayerInventory; use pumpkin_inventory::InventoryError; -use pumpkin_protocol::client::play::{CSetContainerSlot, CSetHeldItem}; +use pumpkin_protocol::client::play::{CSetContainerSlot, CSetHeldItem, CSpawnEntity}; use pumpkin_protocol::codec::slot::Slot; use pumpkin_protocol::codec::var_int::VarInt; use pumpkin_protocol::server::play::SCookieResponse as SPCookieResponse; @@ -40,9 +42,14 @@ use pumpkin_protocol::{ SPlayerRotation, SSetCreativeSlot, SSetHeldItem, SSwingArm, SUseItemOn, Status, }, }; -use pumpkin_world::block::{block_registry::get_block_by_item, BlockFace}; +use pumpkin_world::block::block_registry::Block; use pumpkin_world::item::item_registry::get_item_by_id; use pumpkin_world::item::ItemStack; +use pumpkin_world::{ + block::{block_registry::get_block_by_item, BlockFace}, + entity::entity_registry::get_entity_id, + item::item_registry::get_spawn_egg, +}; use thiserror::Error; fn modulus(a: f32, b: f32) -> f32 { @@ -648,24 +655,38 @@ impl Player { } let world = &entity.world; - let victim = world.get_player_by_entityid(entity_id.0).await; - let Some(victim) = victim else { + let player_victim = world.get_player_by_entityid(entity_id.0).await; + let entity_victim = world.get_living_entity_by_entityid(entity_id.0).await; + if let Some(player_victim) = player_victim { + if player_victim.living_entity.health.load() <= 0.0 { + // you can trigger this from a non-modded / innocent client client, + // so we shouldn't kick the player + return; + } + self.attack(&player_victim).await; + } else if let Some(entity_victim) = entity_victim { + if entity_victim.health.load() <= 0.0 { + return; + } + entity_victim.kill().await; + World::remove_living_entity(entity_victim, world.clone()).await; + // TODO: block entities should be checked here (signs) + } else { + log::error!( + "Player id {} interacted with entity id {} which was not found.", + self.entity_id(), + entity_id.0 + ); self.kick(TextComponent::text("Interacted with invalid entity id")) .await; return; }; - if victim.living_entity.health.load() <= 0.0 { - // you can trigger this from a non-modded / innocent client, - // so we shouldn't kick the player - return; - } - if victim.entity_id() == self.entity_id() { + + if entity_id.0 == self.entity_id() { // this, however, can't be triggered from a non-modded client. self.kick(TextComponent::text("You can't attack yourself")) .await; - return; } - self.attack(&victim).await; } ActionType::Interact | ActionType::InteractAt => { log::debug!("todo"); @@ -792,9 +813,10 @@ impl Player { pub async fn handle_use_item_on( &self, use_item_on: SUseItemOn, - server: &Server, + server: &Arc, ) -> Result<(), Box> { let location = use_item_on.location; + let mut should_try_decrement = false; if !self.can_interact_with_block_at(&location, 1.0) { // TODO: maybe log? @@ -802,15 +824,16 @@ impl Player { } if let Some(face) = BlockFace::from_i32(use_item_on.face.0) { - let inventory = self.inventory().lock().await; + let mut inventory = self.inventory().lock().await; let entity = &self.living_entity.entity; let world = &entity.world; - let item_slot = inventory.held_item(); + let slot_id = inventory.get_selected(); + let cursor_pos = use_item_on.cursor_pos; + let mut state_id = inventory.state_id; + let item_slot = inventory.held_item_mut(); if let Some(item_stack) = item_slot { - let item_stack = *item_stack; - drop(inventory); - + // check if block is interactive if let Some(item) = get_item_by_id(item_stack.item_id) { if let Ok(block) = world.get_block(location).await { let result = server @@ -828,11 +851,21 @@ impl Player { // check if item is a block, Because Not every item can be placed :D if let Some(block) = get_block_by_item(item_stack.item_id) { + should_try_decrement = self + .run_is_block_place(block.clone(), server, use_item_on, location, &face) + .await?; + } + // check if item is a spawn egg + if let Some(item_t) = get_spawn_egg(item_stack.item_id) { + should_try_decrement = self + .run_is_spawn_egg(item_t, server, location, cursor_pos, &face) + .await?; + }; + + if should_try_decrement { // TODO: Config // Decrease Block count if self.gamemode.load() != GameMode::Creative { - let mut inventory = self.inventory().lock().await; - let item_slot = inventory.held_item_mut(); // This should never be possible let Some(item_stack) = item_slot else { return Err(BlockPlacingError::InventoryInvalid.into()); @@ -841,63 +874,17 @@ impl Player { if item_stack.item_count == 0 { *item_slot = None; } - drop(inventory); - } - - let clicked_world_pos = WorldPosition(location.0); - let clicked_block_state = world.get_block_state(clicked_world_pos).await?; - - let world_pos = if clicked_block_state.replaceable { - clicked_world_pos - } else { - let world_pos = WorldPosition(location.0 + face.to_offset()); - let previous_block_state = world.get_block_state(world_pos).await?; - - if !previous_block_state.replaceable { - return Ok(()); - } - - world_pos - }; - //check max world build height - if world_pos.0.y > 319 { - self.client - .send_packet(&CAcknowledgeBlockChange::new(use_item_on.sequence)) - .await; - return Err(BlockPlacingError::BlockOutOfWorld.into()); - } - - let block_bounding_box = BoundingBox::from_block(&world_pos); - let mut intersects = false; - for player in world.get_nearby_players(entity.pos.load(), 20).await { - let bounding_box = player.1.living_entity.entity.bounding_box.load(); - if bounding_box.intersects(&block_bounding_box) { - intersects = true; - } - } - if !intersects { - world - .set_block_state(world_pos, block.default_state_id) - .await; - server - .block_manager - .on_placed(block, self, world_pos, server) + // TODO: this should be by use item on not currently selected as they might be different + let _ = self + .handle_decrease_item( + server, + slot_id, + item_slot.as_ref(), + &mut state_id, + ) .await; } - - self.client - .send_packet(&CAcknowledgeBlockChange::new(use_item_on.sequence)) - .await; - } - } else { - drop(inventory); - let block = world.get_block(location).await; - if let Ok(block) = block { - server - .block_manager - .on_use(block, self, location, server) - .await; } } @@ -1011,4 +998,106 @@ impl Player { packet.payload_length.unwrap_or(VarInt::from(0)).0 ); } + + async fn run_is_spawn_egg( + &self, + item_t: String, + server: &Server, + location: WorldPosition, + cursor_pos: Vector3, + face: &BlockFace, + ) -> Result> { + // checks if spawn egg has a corresponding entity name + if let Some(spawn_item_name) = get_entity_id(&item_t) { + let head_yaw = 10.0; + let world_pos = WorldPosition(location.0 + face.to_offset()); + + // TODO: this should not be hardcoded + let (mob, _world, uuid) = server.add_living_entity(EntityType::Chicken).await; + + let opposite_yaw = self.living_entity.entity.yaw.load() + 180.0; + server + .broadcast_packet_all(&CSpawnEntity::new( + VarInt(mob.entity.entity_id), + uuid, + VarInt((*spawn_item_name).into()), + f64::from(world_pos.0.x) + f64::from(cursor_pos.x), + f64::from(world_pos.0.y), + f64::from(world_pos.0.z) + f64::from(cursor_pos.z), + 10.0, + head_yaw, + opposite_yaw, + 0.into(), + 0.0, + 0.0, + 0.0, + )) + .await; + + // TODO: send/configure additional commands/data based on type of entity (horse, slime, etc) + } else { + // TODO: maybe include additional error types + return Ok(false); + }; + + Ok(true) + } + + async fn run_is_block_place( + &self, + block: Block, + server: &Server, + use_item_on: SUseItemOn, + location: WorldPosition, + face: &BlockFace, + ) -> Result> { + let entity = &self.living_entity.entity; + let world = &entity.world; + + let clicked_world_pos = WorldPosition(location.0); + let clicked_block_state = world.get_block_state(clicked_world_pos).await?; + + let world_pos = if clicked_block_state.replaceable { + clicked_world_pos + } else { + let world_pos = WorldPosition(location.0 + face.to_offset()); + let previous_block_state = world.get_block_state(world_pos).await?; + + if !previous_block_state.replaceable { + return Ok(true); + } + + world_pos + }; + + //check max world build height + if world_pos.0.y > 319 { + self.client + .send_packet(&CAcknowledgeBlockChange::new(use_item_on.sequence)) + .await; + return Err(BlockPlacingError::BlockOutOfWorld.into()); + } + + let block_bounding_box = BoundingBox::from_block(&world_pos); + let mut intersects = false; + for player in world.get_nearby_players(entity.pos.load(), 20).await { + let bounding_box = player.1.living_entity.entity.bounding_box.load(); + if bounding_box.intersects(&block_bounding_box) { + intersects = true; + } + } + if !intersects { + world + .set_block_state(world_pos, block.default_state_id) + .await; + server + .block_manager + .on_placed(&block, self, world_pos, server) + .await; + } + self.client + .send_packet(&CAcknowledgeBlockChange::new(use_item_on.sequence)) + .await; + Ok(true) + } } diff --git a/pumpkin/src/server/mod.rs b/pumpkin/src/server/mod.rs index 96090ae8..18db57e0 100644 --- a/pumpkin/src/server/mod.rs +++ b/pumpkin/src/server/mod.rs @@ -1,9 +1,12 @@ use connection_cache::{CachedBranding, CachedStatus}; +use crossbeam::atomic::AtomicCell; use key_store::KeyStore; use pumpkin_config::BASIC_CONFIG; +use pumpkin_core::math::boundingbox::{BoundingBox, BoundingBoxSize}; use pumpkin_core::math::position::WorldPosition; use pumpkin_core::math::vector2::Vector2; use pumpkin_core::GameMode; +use pumpkin_entity::entity_type::EntityType; use pumpkin_entity::EntityId; use pumpkin_inventory::drag_handler::DragHandler; use pumpkin_inventory::{Container, OpenContainer}; @@ -12,6 +15,7 @@ use pumpkin_protocol::{client::config::CPluginMessage, ClientPacket}; use pumpkin_registry::{DimensionType, Registry}; use pumpkin_world::block::block_registry::Block; use pumpkin_world::dimension::Dimension; +use pumpkin_world::entity::entity_registry::get_entity_by_id; use rand::prelude::SliceRandom; use std::collections::HashMap; use std::sync::atomic::AtomicU32; @@ -23,9 +27,12 @@ use std::{ time::Duration, }; use tokio::sync::{Mutex, RwLock}; +use uuid::Uuid; use crate::block::block_manager::BlockManager; use crate::block::default_block_manager; +use crate::entity::living::LivingEntity; +use crate::entity::Entity; use crate::net::EncryptionError; use crate::world::custom_bossbar::CustomBossbars; use crate::{ @@ -187,6 +194,54 @@ impl Server { } } + /// Adds a new living entity to the server. + /// + /// # Returns + /// + /// A tuple containing: + /// + /// - `Arc`: A reference to the newly created living entity. + /// - `Arc`: A reference to the world that the living entity was added to. + /// - `Uuid`: The uuid of the newly created living entity to be used to send to the client. + pub async fn add_living_entity( + &self, + entity_type: EntityType, + ) -> (Arc, Arc, Uuid) { + let entity_id = self.new_entity_id(); + // TODO: select current + let world = &self.worlds[0]; + + // TODO: this should be resolved to a integer using a macro when calling this function + let bounding_box_size: BoundingBoxSize; + if let Some(entity) = get_entity_by_id(entity_type.clone() as u16) { + bounding_box_size = BoundingBoxSize { + width: f64::from(entity.dimension[0]), + height: f64::from(entity.dimension[1]), + }; + } else { + bounding_box_size = BoundingBoxSize { + width: 0.6, + height: 1.8, + }; + } + + // TODO: standing eye height should be per mob + let new_uuid = uuid::Uuid::new_v4(); + let mob = Arc::new(LivingEntity::new(Entity::new( + entity_id, + new_uuid, + world.clone(), + entity_type, + 1.62, + AtomicCell::new(BoundingBox::new_default(&bounding_box_size)), + AtomicCell::new(bounding_box_size), + ))); + + world.add_living_entity(new_uuid, mob.clone()).await; + + (mob, world.clone(), new_uuid) + } + pub async fn try_get_container( &self, player_id: EntityId, diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index 92239719..8aafd465 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -5,7 +5,7 @@ pub mod player_chunker; use crate::{ command::client_cmd_suggestions, - entity::{player::Player, Entity}, + entity::{living::LivingEntity, player::Player, Entity}, error::PumpkinError, server::Server, }; @@ -14,7 +14,7 @@ use pumpkin_config::BasicConfiguration; use pumpkin_core::math::vector2::Vector2; use pumpkin_core::math::{position::WorldPosition, vector3::Vector3}; use pumpkin_core::text::{color::NamedColor, TextComponent}; -use pumpkin_entity::{entity_type::EntityType, EntityId}; +use pumpkin_entity::{entity_type::EntityType, pose::EntityPose, EntityId}; use pumpkin_protocol::{ client::play::CLevelEvent, codec::{identifier::Identifier, var_int::VarInt}, @@ -102,6 +102,8 @@ pub struct World { pub level_time: Mutex, /// The type of dimension the world is in pub dimension_type: DimensionType, + /// A map of active entities within the world, keyed by their unique UUID. + pub current_living_entities: Arc>>>, // TODO: entities } @@ -115,6 +117,7 @@ impl World { worldborder: Mutex::new(Worldborder::new(0.0, 0.0, 29_999_984.0, 0, 0, 0)), level_time: Mutex::new(LevelTime::new()), dimension_type, + current_living_entities: Arc::new(Mutex::new(HashMap::new())), } } @@ -604,6 +607,16 @@ impl World { None } + /// Gets a Living Entity by entity id + pub async fn get_living_entity_by_entityid(&self, id: EntityId) -> Option> { + for living_entity in self.current_living_entities.lock().await.values() { + if living_entity.entity_id() == id { + return Some(living_entity.clone()); + } + } + None + } + /// Gets a Player by username pub async fn get_player_by_name(&self, name: &str) -> Option> { for player in self.current_players.lock().await.values() { @@ -759,6 +772,31 @@ impl World { log::info!("{}", disconn_msg_cmp.to_pretty_console()); } + /// 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. + pub async fn add_living_entity(&self, uuid: uuid::Uuid, living_entity: Arc) { + let mut current_living_entities = self.current_living_entities.lock().await; + current_living_entities.insert(uuid, living_entity); + } + + pub async fn remove_living_entity(living_entity: Arc, world: Arc) { + let mut current_living_entities = world.current_living_entities.lock().await.clone(); + // TODO: does this work with collisions? + living_entity.entity.set_pose(EntityPose::Dying).await; + tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_millis(2000)).await; + world.remove_entity(&living_entity.entity).await; + current_living_entities.remove(&living_entity.entity.entity_uuid); + }); + } + pub async fn remove_entity(&self, entity: &Entity) { self.broadcast_packet_all(&CRemoveEntities::new(&[entity.entity_id.into()])) .await;