diff --git a/assets/entities.json b/assets/entities.json new file mode 100644 index 000000000..de6239b26 --- /dev/null +++ b/assets/entities.json @@ -0,0 +1 @@ +["acacia_boat","acacia_chest_boat","allay","area_effect_cloud","armadillo","armor_stand","arrow","axolotl","bamboo_chest_raft","bamboo_raft","bat","bee","birch_boat","birch_chest_boat","blaze","block_display","bogged","breeze","breeze_wind_charge","camel","cat","cave_spider","cherry_boat","cherry_chest_boat","chest_minecart","chicken","cod","command_block_minecart","cow","creaking","creaking_transient","creeper","dark_oak_boat","dark_oak_chest_boat","dolphin","donkey","dragon_fireball","drowned","egg","elder_guardian","enderman","endermite","ender_dragon","ender_pearl","end_crystal","evoker","evoker_fangs","experience_bottle","experience_orb","eye_of_ender","falling_block","fireball","firework_rocket","fox","frog","furnace_minecart","ghast","giant","glow_item_frame","glow_squid","goat","guardian","hoglin","hopper_minecart","horse","husk","illusioner","interaction","iron_golem","item","item_display","item_frame","jungle_boat","jungle_chest_boat","leash_knot","lightning_bolt","llama","llama_spit","magma_cube","mangrove_boat","mangrove_chest_boat","marker","minecart","mooshroom","mule","oak_boat","oak_chest_boat","ocelot","ominous_item_spawner","painting","pale_oak_boat","pale_oak_chest_boat","panda","parrot","phantom","pig","piglin","piglin_brute","pillager","polar_bear","potion","pufferfish","rabbit","ravager","salmon","sheep","shulker","shulker_bullet","silverfish","skeleton","skeleton_horse","slime","small_fireball","sniffer","snowball","snow_golem","spawner_minecart","spectral_arrow","spider","spruce_boat","spruce_chest_boat","squid","stray","strider","tadpole","text_display","tnt","tnt_minecart","trader_llama","trident","tropical_fish","turtle","vex","villager","vindicator","wandering_trader","warden","wind_charge","witch","wither","wither_skeleton","wither_skull","wolf","zoglin","zombie","zombie_horse","zombie_villager","zombified_piglin","player","fishing_bobber"] \ No newline at end of file diff --git a/extractor/src/main/kotlin/de/snowii/extractor/Extractor.kt b/extractor/src/main/kotlin/de/snowii/extractor/Extractor.kt index d8e67013b..ca361a185 100644 --- a/extractor/src/main/kotlin/de/snowii/extractor/Extractor.kt +++ b/extractor/src/main/kotlin/de/snowii/extractor/Extractor.kt @@ -32,6 +32,7 @@ class Extractor : ModInitializer { Tags(), Items(), Blocks(), + Entities(), ) val outputDirectory: Path diff --git a/extractor/src/main/kotlin/de/snowii/extractor/extractors/Entities.kt b/extractor/src/main/kotlin/de/snowii/extractor/extractors/Entities.kt new file mode 100644 index 000000000..03a6b091e --- /dev/null +++ b/extractor/src/main/kotlin/de/snowii/extractor/extractors/Entities.kt @@ -0,0 +1,25 @@ +package de.snowii.extractor.extractors + +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import de.snowii.extractor.Extractor +import net.minecraft.registry.Registries +import net.minecraft.server.MinecraftServer + + +class Entities : Extractor.Extractor { + override fun fileName(): String { + return "entities.json" + } + + override fun extract(server: MinecraftServer): JsonElement { + val entitiesJson = JsonArray() + for (entity in Registries.ENTITY_TYPE) { + entitiesJson.add( + Registries.ENTITY_TYPE.getId(entity)!!.path, + ) + } + + return entitiesJson + } +} diff --git a/pumpkin-world/src/entity/entity_registry.rs b/pumpkin-world/src/entity/entity_registry.rs new file mode 100644 index 000000000..9cd477f0d --- /dev/null +++ b/pumpkin-world/src/entity/entity_registry.rs @@ -0,0 +1,19 @@ +use std::{collections::HashMap, sync::LazyLock}; + +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 (i, entity_name) in ENTITIES.iter().enumerate() { + map.insert(entity_name.clone(), i as u16); + } + map +}); + +pub fn get_entity_id(name: &str) -> Option<&u16> { + ENTITIES_BY_ID.get(&name.replace("minecraft:", "")) +} diff --git a/pumpkin-world/src/entity/mod.rs b/pumpkin-world/src/entity/mod.rs new file mode 100644 index 000000000..30282ee57 --- /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 bc68280ca..6ad61ae69 100644 --- a/pumpkin-world/src/item/item_registry.rs +++ b/pumpkin-world/src/item/item_registry.rs @@ -8,10 +8,33 @@ pub static ITEMS: LazyLock> = LazyLock::new(|| { serde_json::from_str(ITEMS_JSON).expect("Could not parse items.json registry.") }); +pub static ITEMS_REGISTRY_ID_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>(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_ID_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 +} + #[derive(Deserialize, Clone, Debug)] pub struct Item { pub id: u16, diff --git a/pumpkin-world/src/lib.rs b/pumpkin-world/src/lib.rs index 6e30ee9ab..0ae6d743b 100644 --- a/pumpkin-world/src/lib.rs +++ b/pumpkin-world/src/lib.rs @@ -4,6 +4,7 @@ pub mod chunk; pub mod coordinates; pub mod cylindrical_chunk_iterator; pub mod dimension; +pub mod entity; pub mod item; pub mod level; mod world_gen; diff --git a/pumpkin/src/client/player_packet.rs b/pumpkin/src/client/player_packet.rs index b327563c8..5b451e990 100644 --- a/pumpkin/src/client/player_packet.rs +++ b/pumpkin/src/client/player_packet.rs @@ -15,13 +15,16 @@ use pumpkin_core::{ text::TextComponent, GameMode, }; +use pumpkin_entity::entity_type::EntityType; use pumpkin_inventory::{InventoryError, WindowType}; -use pumpkin_protocol::server::play::SCookieResponse as SPCookieResponse; use pumpkin_protocol::{ client::play::CCommandSuggestions, server::play::{SCloseContainer, SCommandSuggestion, SKeepAlive, SSetPlayerGround, SUseItem}, VarInt, }; +use pumpkin_protocol::{ + client::play::CSpawnEntity, server::play::SCookieResponse as SPCookieResponse, +}; use pumpkin_protocol::{ client::play::{ Animation, CAcknowledgeBlockChange, CEntityAnimation, CHeadRot, CPingResponse, @@ -34,7 +37,11 @@ use pumpkin_protocol::{ SSetCreativeSlot, SSetHeldItem, SSwingArm, SUseItemOn, Status, }, }; -use pumpkin_world::block::{block_registry::get_block_by_item, BlockFace}; +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; use super::PlayerConfig; @@ -608,9 +615,11 @@ impl Player { pub async fn handle_use_item_on( &self, + server: &Arc, use_item_on: SUseItemOn, ) -> Result<(), Box> { let location = use_item_on.location; + let mut should_try_decrement: bool = false; if !self.can_interact_with_block_at(&location, 1.0) { // TODO: maybe log? @@ -621,12 +630,20 @@ impl Player { let mut inventory = self.inventory.lock().await; let item_slot = inventory.held_item_mut(); if let Some(item) = item_slot { - let block = get_block_by_item(item.item_id); // check if item is a block, Because Not every item can be placed :D - if let Some(block) = block { - let entity = &self.living_entity.entity; - let world = &entity.world; + if let Some(block) = get_block_by_item(item.item_id) { + should_try_decrement = self + .run_is_block_place(block.default_state_id, use_item_on, location, &face) + .await?; + } + // check if item is a spawn egg + if let Some(item_t) = get_spawn_egg(item.item_id) { + should_try_decrement = self + .run_is_spawn_egg(item_t, server, location, &face) + .await?; + }; + if should_try_decrement { // TODO: Config // Decrease Block count if self.gamemode.load() != GameMode::Creative { @@ -635,49 +652,107 @@ impl Player { *item_slot = None; } } + } + } + Ok(()) + } else { + Err(BlockPlacingError::InvalidBlockFace.into()) + } + } + + async fn run_is_spawn_egg( + &self, + item_t: String, + server: &Server, + location: WorldPosition, + face: &BlockFace, + ) -> Result> { + // check if spawn egg has corresponding entity name + if let Some(spawn_item_name) = get_entity_id(&item_t) { + // TODO: should be facing player + let yaw = 10.0; + let pitch = 10.0; + // TODO: should be at precise spot not just the block location + let world_pos = WorldPosition(location.0 + face.to_offset()); + + let (mob, _world, uuid) = server.add_mob(EntityType::Chicken); + + self.client + .send_packet(&CSpawnEntity::new( + VarInt(mob.entity.entity_id), + uuid, + // VarInt(server.new_entity_id()), + // uuid::Uuid::new_v4(), + VarInt((*spawn_item_name).into()), + // (EntityType::Sheep as i32).into(), + world_pos.0.x.into(), + world_pos.0.y.into(), + world_pos.0.z.into(), + pitch, + yaw, + yaw, + 0.into(), + 0.0, + 0.0, + 0.0, + )) + .await; - let clicked_world_pos = WorldPosition(location.0); - let clicked_block_state = world.get_block_state(clicked_world_pos).await?; + // TODO: send update inventory command? - 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?; + // TODO: send/configure additional commands/data based on type of entity (horse, slime, etc) + } else { + // TODO: fix + return Err(BlockPlacingError::BlockOutOfWorld.into()); + }; - if !previous_block_state.replaceable { - return Ok(()); - } + Ok(true) + } - world_pos - }; + async fn run_is_block_place( + &self, + block: u16, + use_item_on: SUseItemOn, + location: WorldPosition, + face: &BlockFace, + ) -> Result> { + let entity = &self.living_entity.entity; + let world = &entity.world; - //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 clicked_world_pos = WorldPosition(location.0); + let clicked_block_state = world.get_block_state(clicked_world_pos).await?; - let block_bounding_box = BoundingBox::from_block(&world_pos); - let bounding_box = entity.bounding_box.load(); - //TODO: Make this check for every entity in that posistion - if !bounding_box.intersects(&block_bounding_box) { - world - .set_block_state(world_pos, block.default_state_id) - .await; - } - } - self.client - .send_packet(&CAcknowledgeBlockChange::new(use_item_on.sequence)) - .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); } - Ok(()) - } else { - Err(BlockPlacingError::InvalidBlockFace.into()) + 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 bounding_box = entity.bounding_box.load(); + //TODO: Make this check for every entity in that posistion + if !bounding_box.intersects(&block_bounding_box) { + world.set_block_state(world_pos, block).await; } + self.client + .send_packet(&CAcknowledgeBlockChange::new(use_item_on.sequence)) + .await; + Ok(true) } pub fn handle_use_item(&self, _use_item: &SUseItem) { diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index bced6f559..2765c1ecd 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -783,7 +783,8 @@ impl Player { self.handle_swing_arm(SSwingArm::read(bytebuf)?).await; } SUseItemOn::PACKET_ID => { - self.handle_use_item_on(SUseItemOn::read(bytebuf)?).await?; + self.handle_use_item_on(server, SUseItemOn::read(bytebuf)?) + .await?; } SUseItem::PACKET_ID => self.handle_use_item(&SUseItem::read(bytebuf)?), SCommandSuggestion::PACKET_ID => { diff --git a/pumpkin/src/server/mod.rs b/pumpkin/src/server/mod.rs index 4f7ab1eb7..33a84e8e5 100644 --- a/pumpkin/src/server/mod.rs +++ b/pumpkin/src/server/mod.rs @@ -1,7 +1,10 @@ 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::GameMode; +use pumpkin_entity::entity_type::EntityType; use pumpkin_entity::EntityId; use pumpkin_inventory::drag_handler::DragHandler; use pumpkin_inventory::{Container, OpenContainer}; @@ -19,8 +22,11 @@ use std::{ time::Duration, }; use tokio::sync::{Mutex, RwLock}; +use uuid::Uuid; use crate::client::EncryptionError; +use crate::entity::living::LivingEntity; +use crate::entity::Entity; use crate::world::custom_bossbar::CustomBossbars; use crate::{ client::Client, @@ -166,6 +172,31 @@ impl Server { self.server_listing.lock().await.remove_player(); } + // TODO: move to world + pub fn add_mob(&self, entity_type: EntityType) -> (Arc, Arc, Uuid) { + let entity_id = self.new_entity_id(); + // Basically the default world + // TODO: select default from config + let world = &self.worlds[0]; + + // TODO: set per each mob + let bounding_box_size = BoundingBoxSize { + width: 0.6, + height: 1.8, + }; + + let mob = Arc::new(LivingEntity::new(Entity::new( + entity_id, + world.clone(), + entity_type, + 1.62, + AtomicCell::new(BoundingBox::new_default(&bounding_box_size)), + AtomicCell::new(bounding_box_size), + ))); + + (mob, world.clone(), uuid::Uuid::new_v4()) + } + pub async fn try_get_container( &self, player_id: EntityId,