diff --git a/pumpkin-macros/src/lib.rs b/pumpkin-macros/src/lib.rs index 82dfad011..43546eddf 100644 --- a/pumpkin-macros/src/lib.rs +++ b/pumpkin-macros/src/lib.rs @@ -42,6 +42,32 @@ pub fn server_packet(input: TokenStream, item: TokenStream) -> TokenStream { gen.into() } +#[proc_macro_attribute] +pub fn pumpkin_block(input: TokenStream, item: TokenStream) -> TokenStream { + let ast: syn::DeriveInput = syn::parse(item.clone()).unwrap(); + let name = &ast.ident; + let (impl_generics, ty_generics, _) = ast.generics.split_for_impl(); + + let input_string = input.to_string(); + let packet_name = input_string.trim_matches('"'); + let packet_name_split: Vec<&str> = packet_name.split(":").collect(); + + let namespace = packet_name_split[0]; + let id = packet_name_split[1]; + + let item: proc_macro2::TokenStream = item.into(); + + let gen = quote! { + #item + impl #impl_generics crate::block::pumpkin_block::BlockMetadata for #name #ty_generics { + const NAMESPACE: &'static str = #namespace; + const ID: &'static str = #id; + } + }; + + gen.into() +} + mod screen; #[proc_macro] pub fn screen(item: TokenStream) -> TokenStream { diff --git a/pumpkin-protocol/src/client/play/c_level_event.rs b/pumpkin-protocol/src/client/play/c_level_event.rs new file mode 100644 index 000000000..df1cde149 --- /dev/null +++ b/pumpkin-protocol/src/client/play/c_level_event.rs @@ -0,0 +1,28 @@ +use pumpkin_core::math::position::WorldPosition; +use pumpkin_macros::client_packet; +use serde::Serialize; + +#[derive(Serialize)] +#[client_packet("play:level_event")] +pub struct CLevelEvent { + event: i32, + location: WorldPosition, + data: i32, + disable_relative_volume: bool, +} + +impl CLevelEvent { + pub fn new( + event: i32, + location: WorldPosition, + data: i32, + disable_relative_volume: bool, + ) -> Self { + Self { + event, + location, + data, + disable_relative_volume, + } + } +} diff --git a/pumpkin-protocol/src/client/play/mod.rs b/pumpkin-protocol/src/client/play/mod.rs index 97af45c83..6d8289fc5 100644 --- a/pumpkin-protocol/src/client/play/mod.rs +++ b/pumpkin-protocol/src/client/play/mod.rs @@ -25,6 +25,7 @@ mod c_head_rot; mod c_hurt_animation; mod c_initialize_world_border; mod c_keep_alive; +mod c_level_event; mod c_login; mod c_open_screen; mod c_particle; @@ -94,6 +95,7 @@ pub use c_head_rot::*; pub use c_hurt_animation::*; pub use c_initialize_world_border::*; pub use c_keep_alive::*; +pub use c_level_event::*; pub use c_login::*; pub use c_open_screen::*; pub use c_particle::*; diff --git a/pumpkin-registry/src/jukebox_song.rs b/pumpkin-registry/src/jukebox_song.rs index 44b01bc88..adcb0e440 100644 --- a/pumpkin-registry/src/jukebox_song.rs +++ b/pumpkin-registry/src/jukebox_song.rs @@ -3,7 +3,12 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JukeboxSong { sound_event: String, - // description: TextComponent<'static>, + description: Description, length_in_seconds: f32, comparator_output: u32, } + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Description { + translate: String, +} diff --git a/pumpkin-registry/src/lib.rs b/pumpkin-registry/src/lib.rs index d78e680be..8be13d6f5 100644 --- a/pumpkin-registry/src/lib.rs +++ b/pumpkin-registry/src/lib.rs @@ -58,7 +58,7 @@ pub struct SyncedRegistry { damage_type: IndexMap, banner_pattern: IndexMap, enchantment: IndexMap, - jukebox_song: IndexMap, + pub jukebox_song: IndexMap, instrument: IndexMap, } @@ -225,18 +225,18 @@ impl Registry { // registry_entries, // }; - // let registry_entries = SYNCED_REGISTRIES - // .jukebox_song - // .iter() - // .map(|s| RegistryEntry { - // entry_id: s.0, - // data: pumpkin_nbt::serializer::to_bytes_unnamed(&s.1).unwrap(), - // }) - // .collect(); - // let jukebox_song = Registry { - // registry_id: "minecraft:jukebox_song".to_string(), - // registry_entries, - // }; + let registry_entries = SYNCED_REGISTRIES + .jukebox_song + .iter() + .map(|s| RegistryEntry { + entry_id: s.0, + data: pumpkin_nbt::serializer::to_bytes_unnamed(&s.1).unwrap(), + }) + .collect(); + let jukebox_song = Registry { + registry_id: "minecraft:jukebox_song".to_string(), + registry_entries, + }; // let registry_entries = SYNCED_REGISTRIES // .instrument @@ -262,7 +262,7 @@ impl Registry { damage_type, banner_pattern, // enchantment, - // jukebox_song, + jukebox_song, // instrument, ] } diff --git a/pumpkin-world/src/item/item_registry.rs b/pumpkin-world/src/item/item_registry.rs index bc68280ca..ccc7c61cd 100644 --- a/pumpkin-world/src/item/item_registry.rs +++ b/pumpkin-world/src/item/item_registry.rs @@ -1,4 +1,5 @@ -use std::{collections::HashMap, sync::LazyLock}; +use std::collections::HashMap; +use std::sync::LazyLock; use serde::Deserialize; @@ -12,6 +13,14 @@ 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); + } + None +} + #[derive(Deserialize, Clone, Debug)] pub struct Item { pub id: u16, @@ -22,4 +31,11 @@ pub struct Item { pub struct ItemComponents { #[serde(rename = "minecraft:max_stack_size")] pub max_stack_size: u8, + #[serde(rename = "minecraft:jukebox_playable")] + pub jukebox_playable: Option, +} + +#[derive(Deserialize, Clone, Debug)] +pub struct JukeboxPlayable { + pub song: String, } diff --git a/pumpkin/src/block/block_manager.rs b/pumpkin/src/block/block_manager.rs new file mode 100644 index 000000000..50ab938e8 --- /dev/null +++ b/pumpkin/src/block/block_manager.rs @@ -0,0 +1,89 @@ +use crate::block::pumpkin_block::{BlockMetadata, PumpkinBlock}; +use crate::entity::player::Player; +use crate::server::Server; +use pumpkin_core::math::position::WorldPosition; +use pumpkin_world::block::block_registry::Block; +use pumpkin_world::item::item_registry::Item; +use std::collections::HashMap; +use std::sync::Arc; + +pub enum BlockActionResult { + /// Allow other actions to be executed + Continue, + /// Block other actions + Consume, +} + +#[derive(Default)] +pub struct BlockManager { + blocks: HashMap>, +} + +impl BlockManager { + pub fn register(&mut self, block: T) { + self.blocks + .insert(block.name().to_string(), Arc::new(block)); + } + + pub async fn on_use( + &self, + block: &Block, + player: &Player, + location: WorldPosition, + server: &Server, + ) { + let pumpkin_block = self.get_pumpkin_block(block); + if let Some(pumpkin_block) = pumpkin_block { + pumpkin_block.on_use(player, location, server).await; + } + } + + pub async fn on_use_with_item( + &self, + block: &Block, + player: &Player, + location: WorldPosition, + item: &Item, + server: &Server, + ) -> BlockActionResult { + let pumpkin_block = self.get_pumpkin_block(block); + if let Some(pumpkin_block) = pumpkin_block { + return pumpkin_block + .on_use_with_item(player, location, item, server) + .await; + } + BlockActionResult::Continue + } + + pub async fn on_placed( + &self, + block: &Block, + player: &Player, + location: WorldPosition, + server: &Server, + ) { + let pumpkin_block = self.get_pumpkin_block(block); + if let Some(pumpkin_block) = pumpkin_block { + pumpkin_block.on_placed(player, location, server).await; + } + } + + pub async fn on_broken( + &self, + block: &Block, + player: &Player, + location: WorldPosition, + server: &Server, + ) { + let pumpkin_block = self.get_pumpkin_block(block); + if let Some(pumpkin_block) = pumpkin_block { + pumpkin_block.on_broken(player, location, server).await; + } + } + + #[must_use] + pub fn get_pumpkin_block(&self, block: &Block) -> Option<&Arc> { + self.blocks + .get(format!("minecraft:{}", block.name).as_str()) + } +} diff --git a/pumpkin/src/block/blocks/crafting_table.rs b/pumpkin/src/block/blocks/crafting_table.rs new file mode 100644 index 000000000..22e224fca --- /dev/null +++ b/pumpkin/src/block/blocks/crafting_table.rs @@ -0,0 +1,50 @@ +use crate::block::block_manager::BlockActionResult; +use crate::block::pumpkin_block::PumpkinBlock; +use crate::entity::player::Player; +use crate::server::Server; +use async_trait::async_trait; +use pumpkin_core::math::position::WorldPosition; +use pumpkin_inventory::{CraftingTable, OpenContainer, WindowType}; +use pumpkin_macros::pumpkin_block; +use pumpkin_world::item::item_registry::Item; + +#[pumpkin_block("minecraft:crafting_table")] +pub struct CraftingTableBlock; + +#[async_trait] +impl PumpkinBlock for CraftingTableBlock { + async fn on_use<'a>(&self, player: &Player, _location: WorldPosition, server: &Server) { + self.open_crafting_screen(player, server).await; + } + + async fn on_use_with_item<'a>( + &self, + player: &Player, + _location: WorldPosition, + _item: &Item, + server: &Server, + ) -> BlockActionResult { + self.open_crafting_screen(player, server).await; + BlockActionResult::Consume + } +} + +impl CraftingTableBlock { + pub async fn open_crafting_screen(&self, player: &Player, server: &Server) { + //TODO: Adjust /craft command to real crafting table + let entity_id = player.entity_id(); + player.open_container.store(Some(1)); + { + let mut open_containers = server.open_containers.write().await; + if let Some(ender_chest) = open_containers.get_mut(&1) { + ender_chest.add_player(entity_id); + } else { + let open_container = OpenContainer::new_empty_container::(entity_id); + open_containers.insert(1, open_container); + } + } + player + .open_container(server, WindowType::CraftingTable) + .await; + } +} diff --git a/pumpkin/src/block/blocks/jukebox.rs b/pumpkin/src/block/blocks/jukebox.rs new file mode 100644 index 000000000..3c52395ba --- /dev/null +++ b/pumpkin/src/block/blocks/jukebox.rs @@ -0,0 +1,58 @@ +use crate::block::block_manager::BlockActionResult; +use crate::block::pumpkin_block::PumpkinBlock; +use crate::entity::player::Player; +use crate::server::Server; +use async_trait::async_trait; +use pumpkin_core::math::position::WorldPosition; +use pumpkin_macros::pumpkin_block; +use pumpkin_registry::SYNCED_REGISTRIES; +use pumpkin_world::item::item_registry::Item; + +#[pumpkin_block("minecraft:jukebox")] +pub struct JukeboxBlock; + +#[async_trait] +impl PumpkinBlock for JukeboxBlock { + async fn on_use<'a>(&self, player: &Player, location: WorldPosition, _server: &Server) { + // For now just stop the music at this position + let world = &player.living_entity.entity.world; + + world.stop_record(location).await; + } + + async fn on_use_with_item<'a>( + &self, + player: &Player, + location: WorldPosition, + item: &Item, + _server: &Server, + ) -> BlockActionResult { + let world = &player.living_entity.entity.world; + + let Some(jukebox_playable) = &item.components.jukebox_playable else { + return BlockActionResult::Continue; + }; + + let Some(song) = jukebox_playable.song.split(':').nth(1) else { + return BlockActionResult::Continue; + }; + + let Some(jukebox_song) = SYNCED_REGISTRIES.jukebox_song.get_index_of(song) else { + log::error!("Jukebox playable song not registered!"); + return BlockActionResult::Continue; + }; + + //TODO: Update block state and block nbt + + world.play_record(jukebox_song as i32, location).await; + + BlockActionResult::Consume + } + + async fn on_broken<'a>(&self, player: &Player, location: WorldPosition, _server: &Server) { + // For now just stop the music at this position + let world = &player.living_entity.entity.world; + + world.stop_record(location).await; + } +} diff --git a/pumpkin/src/block/blocks/mod.rs b/pumpkin/src/block/blocks/mod.rs new file mode 100644 index 000000000..8e7fde733 --- /dev/null +++ b/pumpkin/src/block/blocks/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod crafting_table; +pub(crate) mod jukebox; diff --git a/pumpkin/src/block/mod.rs b/pumpkin/src/block/mod.rs new file mode 100644 index 000000000..10d1bc3ff --- /dev/null +++ b/pumpkin/src/block/mod.rs @@ -0,0 +1,18 @@ +use crate::block::block_manager::BlockManager; +use crate::block::blocks::crafting_table::CraftingTableBlock; +use crate::block::blocks::jukebox::JukeboxBlock; +use std::sync::Arc; + +pub mod block_manager; +mod blocks; +pub mod pumpkin_block; + +#[must_use] +pub fn default_block_manager() -> Arc { + let mut manager = BlockManager::default(); + + manager.register(JukeboxBlock); + manager.register(CraftingTableBlock); + + Arc::new(manager) +} diff --git a/pumpkin/src/block/pumpkin_block.rs b/pumpkin/src/block/pumpkin_block.rs new file mode 100644 index 000000000..78a3bd057 --- /dev/null +++ b/pumpkin/src/block/pumpkin_block.rs @@ -0,0 +1,32 @@ +use crate::block::block_manager::BlockActionResult; +use crate::entity::player::Player; +use crate::server::Server; +use async_trait::async_trait; +use pumpkin_core::math::position::WorldPosition; +use pumpkin_world::item::item_registry::Item; + +pub trait BlockMetadata { + const NAMESPACE: &'static str; + const ID: &'static str; + fn name(&self) -> String { + format!("{}:{}", Self::NAMESPACE, Self::ID) + } +} + +#[async_trait] +pub trait PumpkinBlock: Send + Sync { + async fn on_use<'a>(&self, _player: &Player, _location: WorldPosition, _server: &Server) {} + async fn on_use_with_item<'a>( + &self, + _player: &Player, + _location: WorldPosition, + _item: &Item, + _server: &Server, + ) -> BlockActionResult { + BlockActionResult::Continue + } + + async fn on_placed<'a>(&self, _player: &Player, _location: WorldPosition, _server: &Server) {} + + async fn on_broken<'a>(&self, _player: &Player, _location: WorldPosition, _server: &Server) {} +} diff --git a/pumpkin/src/client/player_packet.rs b/pumpkin/src/client/player_packet.rs index 376c63aa7..89bcbda23 100644 --- a/pumpkin/src/client/player_packet.rs +++ b/pumpkin/src/client/player_packet.rs @@ -1,5 +1,7 @@ use std::sync::Arc; +use super::PlayerConfig; +use crate::block::block_manager::BlockActionResult; use crate::{ command::CommandSender, entity::player::{ChatMode, Hand, Player}, @@ -35,10 +37,9 @@ use pumpkin_protocol::{ }, }; use pumpkin_world::block::{block_registry::get_block_by_item, BlockFace}; +use pumpkin_world::item::item_registry::get_item_by_id; use thiserror::Error; -use super::PlayerConfig; - fn modulus(a: f32, b: f32) -> f32 { ((a % b) + b) % b } @@ -48,6 +49,7 @@ pub enum BlockPlacingError { BlockOutOfReach, InvalidBlockFace, BlockOutOfWorld, + InventoryInvalid, } impl std::fmt::Display for BlockPlacingError { @@ -60,7 +62,7 @@ impl PumpkinError for BlockPlacingError { fn is_kick(&self) -> bool { match self { Self::BlockOutOfReach | Self::BlockOutOfWorld => false, - Self::InvalidBlockFace => true, + Self::InvalidBlockFace | Self::InventoryInvalid => true, } } @@ -69,6 +71,7 @@ impl PumpkinError for BlockPlacingError { Self::BlockOutOfReach | Self::BlockOutOfWorld | Self::InvalidBlockFace => { log::Level::Warn } + Self::InventoryInvalid => log::Level::Error, } } @@ -76,6 +79,7 @@ impl PumpkinError for BlockPlacingError { match self { Self::BlockOutOfReach | Self::BlockOutOfWorld => None, Self::InvalidBlockFace => Some("Invalid block face".into()), + Self::InventoryInvalid => Some("Held item invalid".into()), } } } @@ -523,7 +527,7 @@ impl Player { } } - pub async fn handle_player_action(&self, player_action: SPlayerAction) { + pub async fn handle_player_action(&self, player_action: SPlayerAction, server: &Server) { match Status::from_i32(player_action.status.0) { Some(status) => match status { Status::StartedDigging => { @@ -542,7 +546,16 @@ impl Player { // Block break & block break sound let entity = &self.living_entity.entity; let world = &entity.world; + let block = world.get_block(location).await; + world.break_block(location, Some(self)).await; + + if let Ok(block) = block { + server + .block_manager + .on_broken(block, self, location, server) + .await; + } } } Status::CancelledDigging => { @@ -571,7 +584,16 @@ impl Player { // Block break & block break sound let entity = &self.living_entity.entity; let world = &entity.world; + let block = world.get_block(location).await; + world.break_block(location, Some(self)).await; + + if let Ok(block) = block { + server + .block_manager + .on_broken(block, self, location, server) + .await; + } // TODO: Send this every tick self.client .send_packet(&CAcknowledgeBlockChange::new(player_action.sequence)) @@ -624,6 +646,7 @@ impl Player { pub async fn handle_use_item_on( &self, use_item_on: SUseItemOn, + server: &Server, ) -> Result<(), Box> { let location = use_item_on.location; @@ -633,22 +656,46 @@ impl Player { } if let Some(face) = BlockFace::from_i32(use_item_on.face.0) { - 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; + let inventory = self.inventory().lock().await; + let entity = &self.living_entity.entity; + let world = &entity.world; + let item_slot = inventory.held_item(); + + if let Some(item_stack) = item_slot { + let item_stack = *item_stack; + drop(inventory); + + if let Some(item) = get_item_by_id(item_stack.item_id) { + if let Ok(block) = world.get_block(location).await { + let result = server + .block_manager + .on_use_with_item(block, self, location, item, server) + .await; + match result { + BlockActionResult::Continue => {} + BlockActionResult::Consume => { + return Ok(()); + } + } + } + } + // 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) { // TODO: Config // Decrease Block count if self.gamemode.load() != GameMode::Creative { - item.item_count -= 1; - if item.item_count == 0 { + 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()); + }; + item_stack.item_count -= 1; + if item_stack.item_count == 0 { *item_slot = None; } + drop(inventory); } let clicked_world_pos = WorldPosition(location.0); @@ -682,11 +729,25 @@ impl Player { 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; + } + } 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; } - self.client - .send_packet(&CAcknowledgeBlockChange::new(use_item_on.sequence)) - .await; } Ok(()) diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index b5c42a863..ea5b8dc95 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -657,7 +657,7 @@ impl Player { .await; } SPlayerAction::PACKET_ID => { - self.handle_player_action(SPlayerAction::read(bytebuf)?) + self.handle_player_action(SPlayerAction::read(bytebuf)?, server) .await; } SPlayerCommand::PACKET_ID => { @@ -684,7 +684,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(SUseItemOn::read(bytebuf)?, server) + .await?; } SUseItem::PACKET_ID => self.handle_use_item(&SUseItem::read(bytebuf)?), SCommandSuggestion::PACKET_ID => { diff --git a/pumpkin/src/main.rs b/pumpkin/src/main.rs index 8e463aa49..9358d5378 100644 --- a/pumpkin/src/main.rs +++ b/pumpkin/src/main.rs @@ -38,6 +38,7 @@ use rcon::RCONServer; use std::time::Instant; // Setup some tokens to allow us to identify which event is for which socket. +pub mod block; pub mod client; pub mod command; pub mod entity; diff --git a/pumpkin/src/server/mod.rs b/pumpkin/src/server/mod.rs index 92fd32cca..b01d7d0c8 100644 --- a/pumpkin/src/server/mod.rs +++ b/pumpkin/src/server/mod.rs @@ -21,6 +21,8 @@ use std::{ }; use tokio::sync::{Mutex, RwLock}; +use crate::block::block_manager::BlockManager; +use crate::block::default_block_manager; use crate::client::EncryptionError; use crate::world::custom_bossbar::CustomBossbars; use crate::{ @@ -46,6 +48,8 @@ pub struct Server { server_branding: CachedBranding, /// Saves and Dispatches commands to appropriate handlers. pub command_dispatcher: Arc>, + /// Saves and calls blocks blocks + pub block_manager: Arc, /// Manages multiple worlds within the server. pub worlds: Vec>, // All the dimensions that exists on the server, @@ -112,6 +116,7 @@ impl Server { DimensionType::TheEnd, ], command_dispatcher, + block_manager: default_block_manager(), auth_client, key_store: KeyStore::new(), server_listing: Mutex::new(CachedStatus::new()), diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index bc73dc592..3b0db3de6 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -16,6 +16,7 @@ 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_protocol::client::play::CLevelEvent; use pumpkin_protocol::{ client::play::{CBlockUpdate, CRespawn, CSoundEffect, CWorldEvent}, SoundCategory, @@ -166,6 +167,16 @@ impl World { .await; } + pub async fn play_record(&self, record_id: i32, position: WorldPosition) { + self.broadcast_packet_all(&CLevelEvent::new(1010, position, record_id, false)) + .await; + } + + pub async fn stop_record(&self, position: WorldPosition) { + self.broadcast_packet_all(&CLevelEvent::new(1011, position, 0, false)) + .await; + } + pub async fn tick(&self) { // world ticks let mut level_time = self.level_time.lock().await;