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 <lilalexmed@proton.me>
This commit is contained in:
4lve
2025-04-13 16:22:12 +02:00
committed by GitHub
parent 929388fc9d
commit 0dd176e8e2
28 changed files with 778 additions and 139 deletions

View File

@@ -89,14 +89,14 @@ impl OpenContainer {
}
}
#[derive(Default)]
pub struct Chest([Option<ItemStack>; 27]);
pub struct ChestContainer([Option<ItemStack>; 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
}

View File

@@ -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()
}

View File

@@ -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)
}

View File

@@ -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<NbtCompound> for NbtCompound {
self
}
}
impl Serialize for NbtCompound {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<D>(deserializer: D) -> Result<Self, D::Error>
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<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut compound = NbtCompound::new();
while let Some((key, value)) = map.next_entry::<String, NbtTag>()? {
compound.put(&key, value);
}
Ok(compound)
}
}
deserializer.deserialize_map(CompoundVisitor)
}
}
impl From<NbtCompound> for NbtTag {
fn from(value: NbtCompound) -> Self {
NbtTag::Compound(value)
}
}

View File

@@ -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<bool> for NbtTag {
NbtTag::Byte(value as i8)
}
}
impl Serialize for NbtTag {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, v: bool) -> Result<Self::Value, E> {
Ok(NbtTag::Byte(v as i8))
}
fn visit_i8<E>(self, v: i8) -> Result<Self::Value, E> {
Ok(NbtTag::Byte(v))
}
fn visit_i16<E>(self, v: i16) -> Result<Self::Value, E> {
Ok(NbtTag::Short(v))
}
fn visit_i32<E>(self, v: i32) -> Result<Self::Value, E> {
Ok(NbtTag::Int(v))
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E> {
Ok(NbtTag::Long(v))
}
fn visit_f32<E>(self, v: f32) -> Result<Self::Value, E> {
Ok(NbtTag::Float(v))
}
fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E> {
Ok(NbtTag::Double(v))
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(NbtTag::String(v.to_string()))
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
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<A>(self, map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
Ok(NbtTag::Compound(NbtCompound::deserialize(
serde::de::value::MapAccessDeserializer::new(map),
)?))
}
}
deserializer.deserialize_any(NbtTagVisitor)
}
}

View File

@@ -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.

View File

@@ -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<W: Write> NetworkWriteExt for W {
@@ -408,6 +411,40 @@ impl<W: Write> NetworkWriteExt for W {
fn write_bitset(&mut self, data: &BitSet) -> Result<(), WritingError> {
data.encode(self)
}
fn write_option<G>(
&mut self,
data: &Option<G>,
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<G>(
&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)]

View File

@@ -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 }
}
}

View File

@@ -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<NbtCompound> {
None
}
}
pub fn block_entity_from_generic<T: BlockEntity>(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<Arc<dyn BlockEntity>> {
let id = nbt.get_string("id").unwrap();
match id.as_str() {
ChestBlockEntity::ID => Some(Arc::new(block_entity_from_generic::<ChestBlockEntity>(nbt))),
SignBlockEntity::ID => Some(Arc::new(block_entity_from_generic::<SignBlockEntity>(nbt))),
_ => None,
}
}

View File

@@ -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<DyeColor> 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<String> 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<DyeColor> 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<Text> 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<NbtTag> 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<String> = 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<NbtCompound> {
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(),
}
}
}

View File

@@ -1 +0,0 @@
pub mod sign;

View File

@@ -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<Text>,
#[serde(default, skip_serializing_if = "Option::is_none")]
back_text: Option<Text>,
#[serde(default, skip_serializing_if = "Option::is_none")]
is_waxed: Option<u8>,
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<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
color: Option<String>,
messages: Vec<String>,
// 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
},
}
}
}

View File

@@ -1,4 +1,4 @@
pub mod interactive;
pub mod entities;
pub mod state;
use num_derive::FromPrimitive;

View File

@@ -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<Vec<u8>, 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();

View File

@@ -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<SerializedScheduledTick>,
#[serde(rename = "fluid_ticks")]
fluid_ticks: Vec<SerializedScheduledTick>,
#[serde(rename = "block_entities")]
block_entities: Vec<NbtCompound>,
}

View File

@@ -365,7 +365,7 @@ where
//TODO: we need to handle the errors and return the result
// files to save
let _: Vec<Result<(), ChunkWritingError>> = join_all(tasks).await;
let _test: Vec<Result<(), ChunkWritingError>> = join_all(tasks).await;
Ok(())
}

View File

@@ -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<i32>,
pub block_ticks: Vec<ScheduledTick>,
pub fluid_ticks: Vec<ScheduledTick>,
pub block_entities: HashMap<BlockPos, Arc<dyn BlockEntity>>,
pub dirty: bool,
}

View File

@@ -86,6 +86,7 @@ impl WorldGenerator for VanillaGenerator {
dirty: true,
block_ticks: Default::default(),
fluid_ticks: Default::default(),
block_entities: Default::default(),
}
}
}

View File

@@ -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);

View File

@@ -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<DashMap<Vector2<i32>, SyncChunk>>,
loaded_chunks: Arc<DashMap<Vector2<i32>, SyncChunk>>,
chunk_watchers: Arc<DashMap<Vector2<i32>, usize>>,
chunk_saver: Arc<dyn ChunkIO<Data = SyncChunk>>,

View File

@@ -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<World>,
_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<World>,
_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::<Chest>(
super::standard_open_container::<ChestContainer>(
block,
player,
location,

View File

@@ -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;

View File

@@ -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<World>,
_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<World>,
_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<World>,
_block: &Block,
location: BlockPos,
_old_state_id: u16,
_moved: bool,
) {
world.remove_block_entity(&location).await;
}
}
manager.register(SignBlock { id: block });
}
}

View File

@@ -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<BlockRegistry> {
register_button_blocks(&mut manager);
register_torch_blocks(&mut manager);
register_redstone_torch_blocks(&mut manager);
register_sign_blocks(&mut manager);
Arc::new(manager)
}

View File

@@ -81,6 +81,17 @@ pub trait PumpkinBlock: Send + Sync {
) {
}
async fn player_placed(
&self,
_world: &Arc<World>,
_block: &Block,
_state_id: u16,
_pos: &BlockPos,
_face: &BlockDirection,
_player: &Player,
) {
}
async fn broken(
&self,
_block: &Block,

View File

@@ -126,6 +126,23 @@ impl BlockRegistry {
block.default_state_id
}
pub async fn player_placed(
&self,
world: &Arc<World>,
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 {

View File

@@ -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;
}
}

View File

@@ -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<Arc<dyn BlockEntity>> {
let chunk = self.get_chunk(block_pos).await;
let chunk: tokio::sync::RwLockReadGuard<ChunkData> = chunk.read().await;
chunk.block_entities.get(block_pos).cloned()
}
pub async fn add_block_entity(&self, block_entity: Arc<dyn BlockEntity>) {
let block_pos = block_entity.get_position();
let chunk = self.get_chunk(&block_pos).await;
let mut chunk: tokio::sync::RwLockWriteGuard<ChunkData> = 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<ChunkData> = chunk.write().await;
chunk.block_entities.remove(block_pos);
chunk.dirty = true;
}
}