From 0dd176e8e23ffbbab8bd0f320adce3225d8cc959 Mon Sep 17 00:00:00 2001 From: 4lve <72332750+4lve@users.noreply.github.com> Date: Sun, 13 Apr 2025 16:22:12 +0200 Subject: [PATCH] add Block Entities (#737) * Start implemetnign block entities * fix * Move block entites * bruh wtf * fix * Add seralizers for compund * don't panic on unknown block enity types * Move block entites to pumpkin-world * begin implementation * Chunk packet works * fix signs * fix clippy * merge * Fix signs * merge * fix * merge * fix * remove println * move folder --------- Co-authored-by: Alexander Medvedev --- pumpkin-inventory/src/open_container.rs | 6 +- pumpkin-macros/src/block.rs | 16 -- pumpkin-macros/src/lib.rs | 5 - pumpkin-nbt/src/compound.rs | 52 +++++ pumpkin-nbt/src/tag.rs | 126 +++++++++++ .../src/client/play/chunk_data.rs | 19 +- pumpkin-protocol/src/ser/mod.rs | 37 ++++ pumpkin-world/src/block/entities/chest.rs | 34 +++ pumpkin-world/src/block/entities/mod.rs | 53 +++++ pumpkin-world/src/block/entities/sign.rs | 207 ++++++++++++++++++ pumpkin-world/src/block/interactive/mod.rs | 1 - pumpkin-world/src/block/interactive/sign.rs | 69 ------ pumpkin-world/src/block/mod.rs | 2 +- pumpkin-world/src/chunk/format/anvil.rs | 11 +- pumpkin-world/src/chunk/format/mod.rs | 16 +- .../src/chunk/io/chunk_file_manager.rs | 2 +- pumpkin-world/src/chunk/mod.rs | 4 + .../src/generation/implementation/mod.rs | 1 + pumpkin-world/src/generation/seed.rs | 6 +- pumpkin-world/src/level.rs | 2 +- pumpkin/src/block/blocks/chest.rs | 29 ++- pumpkin/src/block/blocks/mod.rs | 1 + pumpkin/src/block/blocks/signs.rs | 98 +++++++++ pumpkin/src/block/mod.rs | 2 + pumpkin/src/block/pumpkin_block.rs | 11 + pumpkin/src/block/registry.rs | 17 ++ pumpkin/src/net/packet/play.rs | 47 ++-- pumpkin/src/world/mod.rs | 43 +++- 28 files changed, 778 insertions(+), 139 deletions(-) delete mode 100644 pumpkin-macros/src/block.rs create mode 100644 pumpkin-world/src/block/entities/chest.rs create mode 100644 pumpkin-world/src/block/entities/mod.rs create mode 100644 pumpkin-world/src/block/entities/sign.rs delete mode 100644 pumpkin-world/src/block/interactive/mod.rs delete mode 100644 pumpkin-world/src/block/interactive/sign.rs create mode 100644 pumpkin/src/block/blocks/signs.rs diff --git a/pumpkin-inventory/src/open_container.rs b/pumpkin-inventory/src/open_container.rs index cab815697..14ab60863 100644 --- a/pumpkin-inventory/src/open_container.rs +++ b/pumpkin-inventory/src/open_container.rs @@ -89,14 +89,14 @@ impl OpenContainer { } } #[derive(Default)] -pub struct Chest([Option; 27]); +pub struct ChestContainer([Option; 27]); -impl Chest { +impl ChestContainer { pub fn new() -> Self { Self([const { None }; 27]) } } -impl Container for Chest { +impl Container for ChestContainer { fn window_type(&self) -> &'static WindowType { &WindowType::Generic9x3 } diff --git a/pumpkin-macros/src/block.rs b/pumpkin-macros/src/block.rs deleted file mode 100644 index ca58fd29e..000000000 --- a/pumpkin-macros/src/block.rs +++ /dev/null @@ -1,16 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; - -pub(crate) fn block_entity_impl(item: TokenStream) -> TokenStream { - let input_string = item.to_string(); - let block_entity_name = input_string.trim_matches('"'); - - quote! { - pumpkin_data::block::BLOCK_ENTITY_TYPES - .iter() - .position(|block_type| *block_type == #block_entity_name) - .unwrap() as u32 - - } - .into() -} diff --git a/pumpkin-macros/src/lib.rs b/pumpkin-macros/src/lib.rs index cd84be68c..900637d93 100644 --- a/pumpkin-macros/src/lib.rs +++ b/pumpkin-macros/src/lib.rs @@ -343,8 +343,3 @@ mod block_state; pub fn default_block_state(item: TokenStream) -> TokenStream { block_state::default_block_state_impl(item) } -mod block; -#[proc_macro] -pub fn block_entity(item: TokenStream) -> TokenStream { - block::block_entity_impl(item) -} diff --git a/pumpkin-nbt/src/compound.rs b/pumpkin-nbt/src/compound.rs index 329f8f945..0bd356202 100644 --- a/pumpkin-nbt/src/compound.rs +++ b/pumpkin-nbt/src/compound.rs @@ -1,3 +1,5 @@ +use serde::{Deserialize, Serialize}; + use crate::deserializer::NbtReadHelper; use crate::serializer::WriteAdaptor; use crate::tag::NbtTag; @@ -241,3 +243,53 @@ impl AsRef for NbtCompound { self } } + +impl Serialize for NbtCompound { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeMap; + let mut map = serializer.serialize_map(Some(self.child_tags.len()))?; + for (key, value) in &self.child_tags { + map.serialize_entry(key, &value)?; + } + map.end() + } +} + +impl<'de> Deserialize<'de> for NbtCompound { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct CompoundVisitor; + + impl<'de> serde::de::Visitor<'de> for CompoundVisitor { + type Value = NbtCompound; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("an NBT compound") + } + + fn visit_map(self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + let mut compound = NbtCompound::new(); + while let Some((key, value)) = map.next_entry::()? { + compound.put(&key, value); + } + Ok(compound) + } + } + + deserializer.deserialize_map(CompoundVisitor) + } +} + +impl From for NbtTag { + fn from(value: NbtCompound) -> Self { + NbtTag::Compound(value) + } +} diff --git a/pumpkin-nbt/src/tag.rs b/pumpkin-nbt/src/tag.rs index e2e60083d..e25a80649 100644 --- a/pumpkin-nbt/src/tag.rs +++ b/pumpkin-nbt/src/tag.rs @@ -1,6 +1,7 @@ use compound::NbtCompound; use deserializer::NbtReadHelper; use io::Read; +use serde::{Deserialize, Serialize}; use serializer::WriteAdaptor; use crate::*; @@ -390,3 +391,128 @@ impl From for NbtTag { NbtTag::Byte(value as i8) } } + +impl Serialize for NbtTag { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + NbtTag::End => serializer.serialize_unit(), + NbtTag::Byte(v) => serializer.serialize_i8(*v), + NbtTag::Short(v) => serializer.serialize_i16(*v), + NbtTag::Int(v) => serializer.serialize_i32(*v), + NbtTag::Long(v) => serializer.serialize_i64(*v), + NbtTag::Float(v) => serializer.serialize_f32(*v), + NbtTag::Double(v) => serializer.serialize_f64(*v), + NbtTag::ByteArray(v) => { + use serde::ser::SerializeSeq; + let mut seq = serializer.serialize_seq(Some(v.len()))?; + for byte in v.iter() { + seq.serialize_element(byte)?; + } + seq.end() + } + NbtTag::String(v) => serializer.serialize_str(v), + NbtTag::List(v) => { + use serde::ser::SerializeSeq; + let mut seq = serializer.serialize_seq(Some(v.len()))?; + for item in v.iter() { + seq.serialize_element(item)?; + } + seq.end() + } + NbtTag::Compound(v) => v.serialize(serializer), + NbtTag::IntArray(v) => { + use serde::ser::SerializeSeq; + let mut seq = serializer.serialize_seq(Some(v.len()))?; + for int in v.iter() { + seq.serialize_element(int)?; + } + seq.end() + } + NbtTag::LongArray(v) => { + use serde::ser::SerializeSeq; + let mut seq = serializer.serialize_seq(Some(v.len()))?; + for long in v.iter() { + seq.serialize_element(long)?; + } + seq.end() + } + } + } +} + +impl<'de> Deserialize<'de> for NbtTag { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct NbtTagVisitor; + + impl<'de> serde::de::Visitor<'de> for NbtTagVisitor { + type Value = NbtTag; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("an NBT tag") + } + + fn visit_bool(self, v: bool) -> Result { + Ok(NbtTag::Byte(v as i8)) + } + + fn visit_i8(self, v: i8) -> Result { + Ok(NbtTag::Byte(v)) + } + + fn visit_i16(self, v: i16) -> Result { + Ok(NbtTag::Short(v)) + } + + fn visit_i32(self, v: i32) -> Result { + Ok(NbtTag::Int(v)) + } + + fn visit_i64(self, v: i64) -> Result { + Ok(NbtTag::Long(v)) + } + + fn visit_f32(self, v: f32) -> Result { + Ok(NbtTag::Float(v)) + } + + fn visit_f64(self, v: f64) -> Result { + Ok(NbtTag::Double(v)) + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Ok(NbtTag::String(v.to_string())) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut vec = Vec::new(); + while let Some(value) = seq.next_element()? { + vec.push(value); + } + Ok(NbtTag::List(vec.into_boxed_slice())) + } + + fn visit_map(self, map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + Ok(NbtTag::Compound(NbtCompound::deserialize( + serde::de::value::MapAccessDeserializer::new(map), + )?)) + } + } + + deserializer.deserialize_any(NbtTagVisitor) + } +} diff --git a/pumpkin-protocol/src/client/play/chunk_data.rs b/pumpkin-protocol/src/client/play/chunk_data.rs index efa8b27f5..38a8761f0 100644 --- a/pumpkin-protocol/src/client/play/chunk_data.rs +++ b/pumpkin-protocol/src/client/play/chunk_data.rs @@ -8,6 +8,8 @@ use crate::{ use pumpkin_data::packet::clientbound::PLAY_LEVEL_CHUNK_WITH_LIGHT; use pumpkin_macros::packet; +use pumpkin_nbt::END_ID; +use pumpkin_util::math::position::get_local_cord; use pumpkin_world::chunk::{ChunkData, palette::NetworkPalette}; #[packet(PLAY_LEVEL_CHUNK_WITH_LIGHT)] @@ -147,8 +149,23 @@ impl ClientPacket for CChunkData<'_> { write.write_slice(&blocks_and_biomes_buf)?; // TODO: block entities - write.write_var_int(&VarInt(0))?; + write.write_var_int(&VarInt(self.0.block_entities.len() as i32))?; + for block_entity in self.0.block_entities.values() { + let chunk_data_nbt = block_entity.chunk_data_nbt(); + let pos = block_entity.get_position(); + let block_entity_id = block_entity.get_id(); + let local_xz = (get_local_cord(pos.0.x) << 4) | get_local_cord(pos.0.z); + write.write_u8_be(local_xz as u8)?; + write.write_i16_be(pos.0.y as i16)?; + write.write_var_int(&VarInt(block_entity_id as i32))?; + if let Some(chunk_data_nbt) = chunk_data_nbt { + write.write_nbt(&chunk_data_nbt.into())?; + } else { + write.write_u8_be(END_ID)?; + } + } + // Sky Light Mask // All of the chunks, this is not optimal and uses way more data than needed but will be // overhauled with a full lighting system. diff --git a/pumpkin-protocol/src/ser/mod.rs b/pumpkin-protocol/src/ser/mod.rs index d635d3398..de22514fc 100644 --- a/pumpkin-protocol/src/ser/mod.rs +++ b/pumpkin-protocol/src/ser/mod.rs @@ -7,6 +7,7 @@ use crate::{ }; pub mod deserializer; +use pumpkin_nbt::{serializer::WriteAdaptor, tag::NbtTag}; use thiserror::Error; pub mod packet; pub mod serializer; @@ -322,6 +323,8 @@ pub trait NetworkWriteExt { Ok(()) } + + fn write_nbt(&mut self, data: &NbtTag) -> Result<(), WritingError>; } impl NetworkWriteExt for W { @@ -408,6 +411,40 @@ impl NetworkWriteExt for W { fn write_bitset(&mut self, data: &BitSet) -> Result<(), WritingError> { data.encode(self) } + + fn write_option( + &mut self, + data: &Option, + writer: impl FnOnce(&mut Self, &G) -> Result<(), WritingError>, + ) -> Result<(), WritingError> { + if let Some(data) = data { + self.write_bool(true)?; + writer(self, data) + } else { + self.write_bool(false) + } + } + + fn write_list( + &mut self, + list: &[G], + writer: impl Fn(&mut Self, &G) -> Result<(), WritingError>, + ) -> Result<(), WritingError> { + self.write_var_int(&(list.len() as i32).into())?; + for data in list { + writer(self, data)?; + } + + Ok(()) + } + + fn write_nbt(&mut self, data: &NbtTag) -> Result<(), WritingError> { + let mut write_adaptor = WriteAdaptor::new(self); + data.serialize(&mut write_adaptor) + .map_err(|e| WritingError::Message(e.to_string()))?; + + Ok(()) + } } #[cfg(test)] diff --git a/pumpkin-world/src/block/entities/chest.rs b/pumpkin-world/src/block/entities/chest.rs new file mode 100644 index 000000000..b4dc99885 --- /dev/null +++ b/pumpkin-world/src/block/entities/chest.rs @@ -0,0 +1,34 @@ +use pumpkin_util::math::position::BlockPos; + +use super::BlockEntity; + +pub struct ChestBlockEntity { + pub position: BlockPos, + //pub items: [Item; 27], +} + +impl BlockEntity for ChestBlockEntity { + fn identifier(&self) -> &'static str { + Self::ID + } + + fn get_position(&self) -> BlockPos { + self.position + } + + fn from_nbt(_nbt: &pumpkin_nbt::compound::NbtCompound, position: BlockPos) -> Self + where + Self: Sized, + { + Self { position } + } + + fn write_nbt(&self, _nbt: &mut pumpkin_nbt::compound::NbtCompound) {} +} + +impl ChestBlockEntity { + pub const ID: &'static str = "minecraft:chest"; + pub fn new(position: BlockPos) -> Self { + Self { position } + } +} diff --git a/pumpkin-world/src/block/entities/mod.rs b/pumpkin-world/src/block/entities/mod.rs new file mode 100644 index 000000000..4b2983192 --- /dev/null +++ b/pumpkin-world/src/block/entities/mod.rs @@ -0,0 +1,53 @@ +use std::sync::Arc; + +use chest::ChestBlockEntity; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_util::math::position::BlockPos; +use sign::SignBlockEntity; + +pub mod chest; +pub mod sign; + +pub trait BlockEntity: Send + Sync { + fn write_nbt(&self, nbt: &mut NbtCompound); + fn from_nbt(nbt: &NbtCompound, position: BlockPos) -> Self + where + Self: Sized; + fn identifier(&self) -> &'static str; + fn get_position(&self) -> BlockPos; + fn write_internal(&self, nbt: &mut NbtCompound) { + nbt.put_string("id", self.identifier().to_string()); + let position = self.get_position(); + nbt.put_int("x", position.0.x); + nbt.put_int("y", position.0.y); + nbt.put_int("z", position.0.z); + self.write_nbt(nbt); + } + fn get_id(&self) -> u32 { + pumpkin_data::block::BLOCK_ENTITY_TYPES + .iter() + .position(|block_entity_name| { + *block_entity_name == self.identifier().split(":").last().unwrap() + }) + .unwrap() as u32 + } + fn chunk_data_nbt(&self) -> Option { + None + } +} + +pub fn block_entity_from_generic(nbt: &NbtCompound) -> T { + let x = nbt.get_int("x").unwrap(); + let y = nbt.get_int("y").unwrap(); + let z = nbt.get_int("z").unwrap(); + T::from_nbt(nbt, BlockPos::new(x, y, z)) +} + +pub fn block_entity_from_nbt(nbt: &NbtCompound) -> Option> { + let id = nbt.get_string("id").unwrap(); + match id.as_str() { + ChestBlockEntity::ID => Some(Arc::new(block_entity_from_generic::(nbt))), + SignBlockEntity::ID => Some(Arc::new(block_entity_from_generic::(nbt))), + _ => None, + } +} diff --git a/pumpkin-world/src/block/entities/sign.rs b/pumpkin-world/src/block/entities/sign.rs new file mode 100644 index 000000000..af6a880b1 --- /dev/null +++ b/pumpkin-world/src/block/entities/sign.rs @@ -0,0 +1,207 @@ +use super::BlockEntity; +use num_derive::FromPrimitive; +use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag}; +use pumpkin_util::math::position::BlockPos; + +#[derive(Clone, Default, FromPrimitive)] +#[repr(i8)] +pub enum DyeColor { + White = 0, + Orange = 1, + Magenta = 2, + LightBlue = 3, + Yellow = 4, + Lime = 5, + Pink = 6, + Gray = 7, + LightGray = 8, + Cyan = 9, + Purple = 10, + Blue = 11, + Brown = 12, + Green = 13, + Red = 14, + #[default] + Black = 15, +} + +impl From for String { + fn from(value: DyeColor) -> Self { + match value { + DyeColor::White => "white".to_string(), + DyeColor::Orange => "orange".to_string(), + DyeColor::Magenta => "magenta".to_string(), + DyeColor::LightBlue => "light_blue".to_string(), + DyeColor::Yellow => "yellow".to_string(), + DyeColor::Lime => "lime".to_string(), + DyeColor::Pink => "pink".to_string(), + DyeColor::Gray => "gray".to_string(), + DyeColor::LightGray => "light_gray".to_string(), + DyeColor::Cyan => "cyan".to_string(), + DyeColor::Purple => "purple".to_string(), + DyeColor::Blue => "blue".to_string(), + DyeColor::Brown => "brown".to_string(), + DyeColor::Green => "green".to_string(), + DyeColor::Red => "red".to_string(), + DyeColor::Black => "black".to_string(), + } + } +} + +impl From for DyeColor { + fn from(s: String) -> Self { + match s.as_str() { + "white" => DyeColor::White, + "orange" => DyeColor::Orange, + "magenta" => DyeColor::Magenta, + "light_blue" => DyeColor::LightBlue, + "yellow" => DyeColor::Yellow, + "lime" => DyeColor::Lime, + "pink" => DyeColor::Pink, + "gray" => DyeColor::Gray, + "light_gray" => DyeColor::LightGray, + "cyan" => DyeColor::Cyan, + "purple" => DyeColor::Purple, + "blue" => DyeColor::Blue, + "brown" => DyeColor::Brown, + "green" => DyeColor::Green, + "red" => DyeColor::Red, + "black" => DyeColor::Black, + _ => DyeColor::Black, + } + } +} + +impl From for NbtTag { + fn from(value: DyeColor) -> Self { + NbtTag::Byte(value as i8) + } +} + +// NBT data structure +pub struct SignBlockEntity { + front_text: Text, + back_text: Text, + is_waxed: bool, + position: BlockPos, +} + +#[derive(Clone, Default)] +struct Text { + has_glowing_text: bool, + color: DyeColor, + messages: [String; 4], +} + +impl From for NbtTag { + fn from(value: Text) -> Self { + let mut nbt = NbtCompound::new(); + nbt.put_bool("has_glowing_text", value.has_glowing_text); + nbt.put_string("color", value.color.into()); + nbt.put_list( + "messages", + value.messages.into_iter().map(NbtTag::String).collect(), + ); + NbtTag::Compound(nbt) + } +} + +impl From for Text { + fn from(tag: NbtTag) -> Self { + let nbt = tag.extract_compound().unwrap(); + let has_glowing_text = nbt.get_bool("has_glowing_text").unwrap_or(false); + let color = nbt.get_string("color").unwrap(); + let messages: Vec = nbt + .get_list("messages") + .unwrap() + .iter() + .filter_map(|tag| tag.extract_string().cloned()) + .collect(); + Self { + has_glowing_text, + color: DyeColor::from(color.clone()), + messages: [ + // its important that we use unwrap_or since otherwise we may crash on older versions + messages.first().unwrap_or(&"".to_string()).clone(), + messages.get(1).unwrap_or(&"".to_string()).clone(), + messages.get(2).unwrap_or(&"".to_string()).clone(), + messages.get(3).unwrap_or(&"".to_string()).clone(), + ], + } + } +} + +impl Text { + fn new(messages: [String; 4]) -> Self { + Self { + has_glowing_text: false, + color: DyeColor::Black, + messages, + } + } +} + +impl BlockEntity for SignBlockEntity { + fn identifier(&self) -> &'static str { + Self::ID + } + + fn get_position(&self) -> BlockPos { + self.position + } + + fn from_nbt(nbt: &pumpkin_nbt::compound::NbtCompound, position: BlockPos) -> Self + where + Self: Sized, + { + let front_text = Text::from(nbt.get("front_text").unwrap().clone()); + let back_text = Text::from(nbt.get("back_text").unwrap().clone()); + let is_waxed = nbt.get_bool("is_waxed").unwrap_or(false); + Self { + position, + front_text, + back_text, + is_waxed, + } + } + + fn write_nbt(&self, nbt: &mut pumpkin_nbt::compound::NbtCompound) { + nbt.put("front_text", self.front_text.clone()); + nbt.put("back_text", self.back_text.clone()); + nbt.put_bool("is_waxed", self.is_waxed); + } + + fn chunk_data_nbt(&self) -> Option { + let mut nbt = NbtCompound::new(); + self.write_nbt(&mut nbt); + Some(nbt) + } +} + +impl SignBlockEntity { + pub const ID: &'static str = "minecraft:sign"; + pub fn new(position: BlockPos, is_front: bool, messages: [String; 4]) -> Self { + Self { + position, + is_waxed: false, + front_text: if is_front { + Text::new(messages.clone()) + } else { + Text::default() + }, + back_text: if !is_front { + Text::new(messages.clone()) + } else { + Text::default() + }, + } + } + pub fn empty(position: BlockPos) -> Self { + Self { + position, + is_waxed: false, + front_text: Text::default(), + back_text: Text::default(), + } + } +} diff --git a/pumpkin-world/src/block/interactive/mod.rs b/pumpkin-world/src/block/interactive/mod.rs deleted file mode 100644 index 66ccbc9eb..000000000 --- a/pumpkin-world/src/block/interactive/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod sign; diff --git a/pumpkin-world/src/block/interactive/sign.rs b/pumpkin-world/src/block/interactive/sign.rs deleted file mode 100644 index 3bb475d78..000000000 --- a/pumpkin-world/src/block/interactive/sign.rs +++ /dev/null @@ -1,69 +0,0 @@ -use pumpkin_util::math::position::BlockPos; -use serde::{Deserialize, Serialize}; - -// NBT data structure -#[derive(Serialize, Deserialize)] -pub struct Sign { - #[serde(default, skip_serializing_if = "Option::is_none")] - front_text: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - back_text: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - is_waxed: Option, - x: i32, - y: i32, - z: i32, - id: String, -} - -#[derive(Serialize, Deserialize)] -struct Text { - #[serde(default, skip_serializing_if = "Option::is_none")] - has_glowing_text: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - color: Option, - messages: Vec, - // TODO: uncomment when pumpkin-nbt supports arrays - // messages: [String; 4], -} - -impl Text { - fn new(messages: [String; 4]) -> Self { - Self { - has_glowing_text: None, - color: None, - messages: messages.to_vec(), - // TODO: uncomment when pumpkin-nbt supports arrays - // messages, - } - } -} - -impl Sign { - pub fn new(location: BlockPos, is_front: bool, messages: [String; 4]) -> Self { - let formatted_messages = [ - format!("\"{}\"", messages[0]), - format!("\"{}\"", messages[1]), - format!("\"{}\"", messages[2]), - format!("\"{}\"", messages[3]), - ]; - - Self { - id: "minecraft:sign".to_string(), - is_waxed: None, - x: location.0.x, - y: location.0.y, - z: location.0.z, - front_text: if is_front { - Some(Text::new(formatted_messages.clone())) - } else { - None - }, - back_text: if !is_front { - Some(Text::new(formatted_messages.clone())) - } else { - None - }, - } - } -} diff --git a/pumpkin-world/src/block/mod.rs b/pumpkin-world/src/block/mod.rs index 983e6c9bd..7aa2c1c84 100644 --- a/pumpkin-world/src/block/mod.rs +++ b/pumpkin-world/src/block/mod.rs @@ -1,4 +1,4 @@ -pub mod interactive; +pub mod entities; pub mod state; use num_derive::FromPrimitive; diff --git a/pumpkin-world/src/chunk/format/anvil.rs b/pumpkin-world/src/chunk/format/anvil.rs index 83341d328..092aee16e 100644 --- a/pumpkin-world/src/chunk/format/anvil.rs +++ b/pumpkin-world/src/chunk/format/anvil.rs @@ -4,7 +4,7 @@ use flate2::read::{GzDecoder, GzEncoder, ZlibDecoder, ZlibEncoder}; use itertools::Itertools; use pumpkin_config::advanced_config; use pumpkin_data::{block::Block, chunk::ChunkStatus}; -use pumpkin_nbt::serializer::to_bytes; +use pumpkin_nbt::{compound::NbtCompound, serializer::to_bytes}; use pumpkin_util::math::vector2::Vector2; use std::{ collections::HashSet, @@ -874,6 +874,15 @@ pub fn chunk_to_bytes(chunk_data: &ChunkData) -> Result, ChunkSerializin }) .collect() }, + block_entities: chunk_data + .block_entities + .values() + .map(|block_entity| { + let mut nbt = NbtCompound::new(); + block_entity.write_internal(&mut nbt); + nbt + }) + .collect(), }; let mut result = Vec::new(); diff --git a/pumpkin-world/src/chunk/format/mod.rs b/pumpkin-world/src/chunk/format/mod.rs index 509ee1c9b..f0908d85b 100644 --- a/pumpkin-world/src/chunk/format/mod.rs +++ b/pumpkin-world/src/chunk/format/mod.rs @@ -1,12 +1,12 @@ use std::collections::HashMap; use pumpkin_data::{block::Block, chunk::ChunkStatus}; -use pumpkin_nbt::{from_bytes, nbt_long_array}; +use pumpkin_nbt::{compound::NbtCompound, from_bytes, nbt_long_array}; use pumpkin_util::math::{position::BlockPos, vector2::Vector2}; use serde::{Deserialize, Serialize}; -use crate::generation::section_coords; +use crate::{block::entities::block_entity_from_nbt, generation::section_coords}; use super::{ ChunkData, ChunkHeightmaps, ChunkParsingError, ChunkSections, ScheduledTick, SubChunk, @@ -95,6 +95,16 @@ impl ChunkData { .id, }) .collect(), + block_entities: { + let mut block_entities = HashMap::new(); + for nbt in chunk_data.block_entities { + let block_entity = block_entity_from_nbt(&nbt); + if let Some(block_entity) = block_entity { + block_entities.insert(block_entity.get_position(), block_entity); + } + } + block_entities + }, }) } } @@ -183,4 +193,6 @@ struct ChunkNbt { block_ticks: Vec, #[serde(rename = "fluid_ticks")] fluid_ticks: Vec, + #[serde(rename = "block_entities")] + block_entities: Vec, } diff --git a/pumpkin-world/src/chunk/io/chunk_file_manager.rs b/pumpkin-world/src/chunk/io/chunk_file_manager.rs index 5927e7d88..1b6b697f3 100644 --- a/pumpkin-world/src/chunk/io/chunk_file_manager.rs +++ b/pumpkin-world/src/chunk/io/chunk_file_manager.rs @@ -365,7 +365,7 @@ where //TODO: we need to handle the errors and return the result // files to save - let _: Vec> = join_all(tasks).await; + let _test: Vec> = join_all(tasks).await; Ok(()) } diff --git a/pumpkin-world/src/chunk/mod.rs b/pumpkin-world/src/chunk/mod.rs index 140264868..5cb53684c 100644 --- a/pumpkin-world/src/chunk/mod.rs +++ b/pumpkin-world/src/chunk/mod.rs @@ -2,8 +2,11 @@ use palette::{BiomePalette, BlockPalette}; use pumpkin_nbt::nbt_long_array; use pumpkin_util::math::{position::BlockPos, vector2::Vector2}; use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, sync::Arc}; use thiserror::Error; +use crate::block::entities::BlockEntity; + use crate::BlockStateId; pub mod format; @@ -112,6 +115,7 @@ pub struct ChunkData { pub position: Vector2, pub block_ticks: Vec, pub fluid_ticks: Vec, + pub block_entities: HashMap>, pub dirty: bool, } diff --git a/pumpkin-world/src/generation/implementation/mod.rs b/pumpkin-world/src/generation/implementation/mod.rs index 067a1bb2b..ce948cc65 100644 --- a/pumpkin-world/src/generation/implementation/mod.rs +++ b/pumpkin-world/src/generation/implementation/mod.rs @@ -86,6 +86,7 @@ impl WorldGenerator for VanillaGenerator { dirty: true, block_ticks: Default::default(), fluid_ticks: Default::default(), + block_entities: Default::default(), } } } diff --git a/pumpkin-world/src/generation/seed.rs b/pumpkin-world/src/generation/seed.rs index f916d3731..ef9eb242a 100644 --- a/pumpkin-world/src/generation/seed.rs +++ b/pumpkin-world/src/generation/seed.rs @@ -1,5 +1,7 @@ -use pumpkin_util::math::java_string_hash; -use pumpkin_util::random::{RandomImpl, get_seed, legacy_rand::LegacyRand}; +use pumpkin_util::{ + math::java_string_hash, + random::{RandomImpl, get_seed, legacy_rand::LegacyRand}, +}; #[derive(Clone, Copy)] pub struct Seed(pub u64); diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index b20eecb2b..51629e9ae 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -54,7 +54,7 @@ pub struct Level { // Chunks that are paired with chunk watchers. When a chunk is no longer watched, it is removed // from the loaded chunks map and sent to the underlying ChunkIO - pub loaded_chunks: Arc, SyncChunk>>, + loaded_chunks: Arc, SyncChunk>>, chunk_watchers: Arc, usize>>, chunk_saver: Arc>, diff --git a/pumpkin/src/block/blocks/chest.rs b/pumpkin/src/block/blocks/chest.rs index dacdc34eb..1ee40a855 100644 --- a/pumpkin/src/block/blocks/chest.rs +++ b/pumpkin/src/block/blocks/chest.rs @@ -7,10 +7,11 @@ use pumpkin_data::{ screen::WindowType, sound::{Sound, SoundCategory}, }; -use pumpkin_inventory::{Chest, OpenContainer}; +use pumpkin_inventory::{ChestContainer, OpenContainer}; use pumpkin_macros::pumpkin_block; use pumpkin_protocol::{client::play::CBlockAction, codec::var_int::VarInt}; use pumpkin_util::math::position::BlockPos; +use pumpkin_world::block::entities::chest::ChestBlockEntity; use crate::world::World; use crate::{ @@ -42,6 +43,30 @@ impl PumpkinBlock for ChestBlock { .await; } + async fn placed( + &self, + world: &Arc, + _block: &Block, + _state_id: u16, + pos: &BlockPos, + _old_state_id: u16, + _notify: bool, + ) { + let chest = ChestBlockEntity::new(*pos); + world.add_block_entity(Arc::new(chest)).await; + } + + async fn on_state_replaced( + &self, + world: &Arc, + _block: &Block, + location: BlockPos, + _old_state_id: u16, + _moved: bool, + ) { + world.remove_block_entity(&location).await; + } + async fn use_with_item( &self, block: &Block, @@ -91,7 +116,7 @@ impl ChestBlock { server: &Server, ) { // TODO: shouldn't Chest and window type be constrained together to avoid errors? - super::standard_open_container::( + super::standard_open_container::( block, player, location, diff --git a/pumpkin/src/block/blocks/mod.rs b/pumpkin/src/block/blocks/mod.rs index 237a37794..7e248c5aa 100644 --- a/pumpkin/src/block/blocks/mod.rs +++ b/pumpkin/src/block/blocks/mod.rs @@ -18,6 +18,7 @@ pub(crate) mod furnace; pub(crate) mod jukebox; pub(crate) mod logs; pub(crate) mod redstone; +pub(crate) mod signs; pub(crate) mod sugar_cane; pub(crate) mod tnt; pub(crate) mod torches; diff --git a/pumpkin/src/block/blocks/signs.rs b/pumpkin/src/block/blocks/signs.rs new file mode 100644 index 000000000..97ea9a777 --- /dev/null +++ b/pumpkin/src/block/blocks/signs.rs @@ -0,0 +1,98 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use pumpkin_data::block::Block; +use pumpkin_data::block::BlockProperties; +use pumpkin_data::block::HorizontalFacing; +use pumpkin_data::tag::RegistryKey; +use pumpkin_data::tag::get_tag_values; +use pumpkin_protocol::server::play::SUseItemOn; +use pumpkin_util::math::position::BlockPos; +use pumpkin_world::block::BlockDirection; +use pumpkin_world::block::entities::sign::SignBlockEntity; + +use crate::block::pumpkin_block::{BlockMetadata, PumpkinBlock}; +use crate::block::registry::BlockRegistry; +use crate::entity::player::Player; +use crate::server::Server; +use crate::world::World; + +type SignProperties = pumpkin_data::block::OakSignLikeProperties; + +pub fn register_sign_blocks(manager: &mut BlockRegistry) { + let tag_values: &'static [&'static str] = + get_tag_values(RegistryKey::Block, "minecraft:signs").unwrap(); + + for block in tag_values { + pub struct SignBlock { + id: &'static str, + } + impl BlockMetadata for SignBlock { + fn namespace(&self) -> &'static str { + "minecraft" + } + + fn id(&self) -> &'static str { + self.id + } + } + + #[async_trait] + impl PumpkinBlock for SignBlock { + async fn on_place( + &self, + _server: &Server, + _world: &World, + block: &Block, + _face: &BlockDirection, + _block_pos: &BlockPos, + _use_item_on: &SUseItemOn, + _player_direction: &HorizontalFacing, + _other: bool, + ) -> u16 { + let sign_props = SignProperties::default(block); + + sign_props.to_state_id(block) + } + + async fn placed( + &self, + world: &Arc, + _block: &Block, + _state_id: u16, + pos: &BlockPos, + _old_state_id: u16, + _notify: bool, + ) { + world + .add_block_entity(Arc::new(SignBlockEntity::empty(*pos))) + .await; + } + + async fn player_placed( + &self, + _world: &Arc, + _block: &Block, + _state_id: u16, + pos: &BlockPos, + _face: &BlockDirection, + player: &Player, + ) { + player.send_sign_packet(*pos).await; + } + + async fn on_state_replaced( + &self, + world: &Arc, + _block: &Block, + location: BlockPos, + _old_state_id: u16, + _moved: bool, + ) { + world.remove_block_entity(&location).await; + } + } + + manager.register(SignBlock { id: block }); + } +} diff --git a/pumpkin/src/block/mod.rs b/pumpkin/src/block/mod.rs index fadffe2fd..8f717ef99 100644 --- a/pumpkin/src/block/mod.rs +++ b/pumpkin/src/block/mod.rs @@ -15,6 +15,7 @@ use blocks::redstone::redstone_torch::register_redstone_torch_blocks; use blocks::redstone::redstone_wire::RedstoneWireBlock; use blocks::redstone::repeater::RepeaterBlock; use blocks::redstone::target_block::TargetBlock; +use blocks::signs::register_sign_blocks; use blocks::sugar_cane::SugarCaneBlock; use blocks::torches::register_torch_blocks; use blocks::{ @@ -75,6 +76,7 @@ pub fn default_registry() -> Arc { register_button_blocks(&mut manager); register_torch_blocks(&mut manager); register_redstone_torch_blocks(&mut manager); + register_sign_blocks(&mut manager); Arc::new(manager) } diff --git a/pumpkin/src/block/pumpkin_block.rs b/pumpkin/src/block/pumpkin_block.rs index adc1690ef..0f826b5fd 100644 --- a/pumpkin/src/block/pumpkin_block.rs +++ b/pumpkin/src/block/pumpkin_block.rs @@ -81,6 +81,17 @@ pub trait PumpkinBlock: Send + Sync { ) { } + async fn player_placed( + &self, + _world: &Arc, + _block: &Block, + _state_id: u16, + _pos: &BlockPos, + _face: &BlockDirection, + _player: &Player, + ) { + } + async fn broken( &self, _block: &Block, diff --git a/pumpkin/src/block/registry.rs b/pumpkin/src/block/registry.rs index 71f19951c..2ff55fa60 100644 --- a/pumpkin/src/block/registry.rs +++ b/pumpkin/src/block/registry.rs @@ -126,6 +126,23 @@ impl BlockRegistry { block.default_state_id } + pub async fn player_placed( + &self, + world: &Arc, + block: &Block, + state_id: u16, + pos: &BlockPos, + face: &BlockDirection, + player: &Player, + ) { + let pumpkin_block = self.get_pumpkin_block(block); + if let Some(pumpkin_block) = pumpkin_block { + pumpkin_block + .player_placed(world, block, state_id, pos, face, player) + .await; + } + } + pub async fn can_place_at(&self, world: &World, block: &Block, block_pos: &BlockPos) -> bool { let pumpkin_block = self.get_pumpkin_block(block); if let Some(pumpkin_block) = pumpkin_block { diff --git a/pumpkin/src/net/packet/play.rs b/pumpkin/src/net/packet/play.rs index b84a5b36d..9fd454f45 100644 --- a/pumpkin/src/net/packet/play.rs +++ b/pumpkin/src/net/packet/play.rs @@ -33,10 +33,10 @@ use pumpkin_inventory::InventoryError; use pumpkin_inventory::player::{ PlayerInventory, SLOT_HOTBAR_END, SLOT_HOTBAR_START, SLOT_OFFHAND, }; -use pumpkin_macros::{block_entity, send_cancellable}; +use pumpkin_macros::send_cancellable; use pumpkin_protocol::client::play::{ - CBlockEntityData, CBlockUpdate, COpenSignEditor, CPlayerInfoUpdate, CPlayerPosition, - CSetContainerSlot, CSetHeldItem, CSystemChatMessage, EquipmentSlot, InitChat, PlayerAction, + CBlockUpdate, COpenSignEditor, CPlayerInfoUpdate, CPlayerPosition, CSetContainerSlot, + CSetHeldItem, CSystemChatMessage, EquipmentSlot, InitChat, PlayerAction, }; use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer; use pumpkin_protocol::codec::var_int::VarInt; @@ -66,7 +66,7 @@ use pumpkin_util::{ text::TextComponent, }; use pumpkin_world::block::BlockDirection; -use pumpkin_world::block::interactive::sign::Sign; +use pumpkin_world::block::entities::sign::SignBlockEntity; use pumpkin_world::item::ItemStack; use thiserror::Error; @@ -1456,7 +1456,7 @@ impl Player { pub async fn handle_sign_update(&self, sign_data: SUpdateSign) { let world = &self.living_entity.entity.world.read().await; - let updated_sign = Sign::new( + let updated_sign = SignBlockEntity::new( sign_data.location, sign_data.is_front_text, [ @@ -1467,15 +1467,7 @@ impl Player { ], ); - let mut sign_buf = Vec::new(); - pumpkin_nbt::serializer::to_bytes_unnamed(&updated_sign, &mut sign_buf).unwrap(); - world - .broadcast_packet_all(&CBlockEntityData::new( - sign_data.location, - VarInt(block_entity!("sign") as i32), - sign_buf.into_boxed_slice(), - )) - .await; + world.add_block_entity(Arc::new(updated_sign)).await; } pub async fn handle_use_item(&self, _use_item: &SUseItem, server: &Server) { @@ -1750,7 +1742,11 @@ impl Player { .set_block_state(&final_block_pos, new_state, BlockFlags::NOTIFY_ALL) .await; - self.send_sign_packet(block, final_block_pos, face).await; + server + .block_registry + .player_placed(world, &block, new_state, &final_block_pos, face, self) + .await; + // The block was placed successfully, so decrement their inventory return Ok(true); } @@ -1759,22 +1755,9 @@ impl Player { } /// Checks if the block placed was a sign, then opens a dialog. - async fn send_sign_packet( - &self, - block: Block, - block_position: BlockPos, - selected_face: &BlockDirection, - ) { - if block.states.iter().any(|state| { - state.get_state().block_entity_type == Some(block_entity!("sign")) - || state.get_state().block_entity_type == Some(block_entity!("hanging_sign")) - }) { - self.client - .enqueue_packet(&COpenSignEditor::new( - block_position, - selected_face.to_offset().z == 1, - )) - .await; - } + pub async fn send_sign_packet(&self, block_position: BlockPos) { + self.client + .enqueue_packet(&COpenSignEditor::new(block_position, true)) + .await; } } diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index 0d6fcb292..82bbe779e 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -34,10 +34,11 @@ use pumpkin_data::{ world::{RAW, WorldEvent}, }; use pumpkin_macros::send_cancellable; +use pumpkin_nbt::to_bytes_unnamed; use pumpkin_protocol::{ ClientPacket, IdOr, SoundEvent, client::play::{ - CEntityStatus, CGameEvent, CLogin, CMultiBlockUpdate, CPlayerChatMessage, + CBlockEntityData, CEntityStatus, CGameEvent, CLogin, CMultiBlockUpdate, CPlayerChatMessage, CPlayerInfoUpdate, CRemoveEntities, CRemovePlayerInfo, CSoundEffect, CSpawnEntity, FilterType, GameEvent, InitChat, PlayerAction, PlayerInfoFlags, }, @@ -55,7 +56,10 @@ use pumpkin_registry::DimensionType; use pumpkin_util::math::{position::BlockPos, vector3::Vector3}; use pumpkin_util::math::{position::chunk_section_from_pos, vector2::Vector2}; use pumpkin_util::text::{TextComponent, color::NamedColor}; -use pumpkin_world::{BlockStateId, GENERATION_SETTINGS, GeneratorSetting, biome, level::SyncChunk}; +use pumpkin_world::{ + BlockStateId, GENERATION_SETTINGS, GeneratorSetting, biome, block::entities::BlockEntity, + level::SyncChunk, +}; use pumpkin_world::{block::BlockDirection, chunk::ChunkData}; use pumpkin_world::{chunk::TickPriority, level::Level}; use rand::{Rng, thread_rng}; @@ -1646,4 +1650,39 @@ impl World { self.set_block_state(block_pos, new_state_id, flags).await; } } + + pub async fn get_block_entity(&self, block_pos: &BlockPos) -> Option> { + let chunk = self.get_chunk(block_pos).await; + let chunk: tokio::sync::RwLockReadGuard = chunk.read().await; + + chunk.block_entities.get(block_pos).cloned() + } + + pub async fn add_block_entity(&self, block_entity: Arc) { + let block_pos = block_entity.get_position(); + let chunk = self.get_chunk(&block_pos).await; + let mut chunk: tokio::sync::RwLockWriteGuard = chunk.write().await; + let block_entity_nbt = block_entity.chunk_data_nbt(); + + if let Some(nbt) = block_entity_nbt { + let mut bytes = Vec::new(); + to_bytes_unnamed(&nbt, &mut bytes).unwrap(); + self.broadcast_packet_all(&CBlockEntityData::new( + block_entity.get_position(), + VarInt(block_entity.get_id() as i32), + bytes.into_boxed_slice(), + )) + .await; + } + + chunk.block_entities.insert(block_pos, block_entity); + chunk.dirty = true; + } + + pub async fn remove_block_entity(&self, block_pos: &BlockPos) { + let chunk = self.get_chunk(block_pos).await; + let mut chunk: tokio::sync::RwLockWriteGuard = chunk.write().await; + chunk.block_entities.remove(block_pos); + chunk.dirty = true; + } }