diff --git a/pumpkin-macros/Cargo.toml b/pumpkin-macros/Cargo.toml index 28d6c06f5..b98ba8f1c 100644 --- a/pumpkin-macros/Cargo.toml +++ b/pumpkin-macros/Cargo.toml @@ -12,6 +12,7 @@ pumpkin-data = { path = "../pumpkin-data" } serde.workspace = true serde_json.workspace = true +heck = "0.5" proc-macro2 = "1.0" quote = "1.0" syn = "2.0" diff --git a/pumpkin-macros/src/lib.rs b/pumpkin-macros/src/lib.rs index 906ced9fe..57adb0b66 100644 --- a/pumpkin-macros/src/lib.rs +++ b/pumpkin-macros/src/lib.rs @@ -1,3 +1,4 @@ +use heck::{ToPascalCase, ToSnakeCase}; use proc_macro::TokenStream; use pumpkin_data::item::Item; use quote::quote; @@ -238,6 +239,142 @@ pub fn pumpkin_item(input: TokenStream, item: TokenStream) -> TokenStream { gen.into() } +#[proc_macro_attribute] +pub fn block_property(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 input_parts: Vec<&str> = input_string.split("[").collect(); + let property_name = input_parts[0].trim_ascii().trim_matches(&['"', ','][..]); + let mut property_values: Vec<&str> = Vec::new(); + if input_parts.len() > 1 { + property_values = input_parts[1] + .trim_matches(']') + .split(", ") + .map(|p| p.trim_ascii().trim_matches(&['"', ','][..])) + .collect::>(); + } + + let item: proc_macro2::TokenStream = item.into(); + + let (variants, is_enum): (Vec, bool) = match ast.data { + syn::Data::Enum(enum_item) => ( + enum_item.variants.into_iter().map(|v| v.ident).collect(), + true, + ), + syn::Data::Struct(struct_type) => { + let fields = match struct_type.fields { + Fields::Named(_) => panic!("Block properties can't have named fields"), + Fields::Unnamed(fields) => fields.unnamed, + Fields::Unit => panic!("Block properties must have fields"), + }; + if fields.len() != 1 { + panic!("Block properties structs must have exactly one field"); + } + let struct_type = match fields.first().unwrap().ty { + syn::Type::Path(ref type_path) => { + type_path.path.segments.first().unwrap().ident.to_string() + } + _ => panic!("Block properties can only have primitive types"), + }; + match struct_type.as_str() { + "bool" => ( + vec![ + proc_macro2::Ident::new("true", proc_macro2::Span::call_site()), + proc_macro2::Ident::new("false", proc_macro2::Span::call_site()), + ], + false, + ), + _ => panic!("This type is not supported (Why not implement it yourself?)"), + } + } + _ => panic!("Block properties can only be enums or structs"), + }; + + let values = variants.iter().enumerate().map(|(i, v)| match is_enum { + true => { + let mut value = v.to_string().to_snake_case(); + if !property_values.is_empty() && i < property_values.len() { + value = property_values[i].to_string(); + } + quote! { + Self::#v => #value.to_string(), + } + } + false => { + let value = v.to_string(); + quote! { + Self(#v) => #value.to_string(), + } + } + }); + + let from_values = variants.iter().enumerate().map(|(i, v)| match is_enum { + true => { + let mut value = v.to_string().to_snake_case(); + if !property_values.is_empty() && i < property_values.len() { + value = property_values[i].to_string(); + } + quote! { + #value => Self::#v, + } + } + false => { + let value = v.to_string(); + quote! { + #value => Self(#v), + } + } + }); + + let extra_fns = variants.iter().map(|v| { + let title = proc_macro2::Ident::new( + &v.to_string().to_pascal_case(), + proc_macro2::Span::call_site(), + ); + quote! { + pub fn #title() -> Self { + Self(#v) + } + } + }); + + let extra = if is_enum { + quote! {} + } else { + quote! { + impl #name { + #(#extra_fns)* + } + } + }; + + let gen = quote! { + #item + impl #impl_generics crate::block::properties::BlockPropertyMetadata for #name #ty_generics { + fn name(&self) -> &'static str { + #property_name + } + fn value(&self) -> String { + match self { + #(#values)* + } + } + fn from_value(value: String) -> Self { + match value.as_str() { + #(#from_values)* + _ => panic!("Invalid value for block property"), + } + } + } + #extra + }; + + gen.into() +} + mod block_state; #[proc_macro] pub fn block_state(item: TokenStream) -> TokenStream { diff --git a/pumpkin-util/src/math/vector3.rs b/pumpkin-util/src/math/vector3.rs index 1a3d81ba2..8e0615153 100644 --- a/pumpkin-util/src/math/vector3.rs +++ b/pumpkin-util/src/math/vector3.rs @@ -158,6 +158,22 @@ where } } +impl Vector3 +where + T: Into, +{ + pub fn to_i32(&self) -> Vector3 { + let x: f64 = self.x.into(); + let y: f64 = self.y.into(); + let z: f64 = self.z.into(); + Vector3 { + x: x.round() as i32, + y: y.round() as i32, + z: z.round() as i32, + } + } +} + pub trait Math: Mul //+ Neg diff --git a/pumpkin/src/block/blocks/lever.rs b/pumpkin/src/block/blocks/lever.rs new file mode 100644 index 000000000..429ec3e06 --- /dev/null +++ b/pumpkin/src/block/blocks/lever.rs @@ -0,0 +1,60 @@ +use crate::entity::player::Player; +use async_trait::async_trait; +use pumpkin_data::item::Item; +use pumpkin_macros::pumpkin_block; +use pumpkin_protocol::server::play::SUseItemOn; +use pumpkin_util::math::position::BlockPos; +use pumpkin_world::block::{registry::Block, BlockDirection}; + +use crate::{ + block::{properties::Direction, pumpkin_block::PumpkinBlock, registry::BlockActionResult}, + server::Server, + world::World, +}; + +#[pumpkin_block("minecraft:lever")] +pub struct LeverBlock; + +#[async_trait] +impl PumpkinBlock for LeverBlock { + async fn on_place( + &self, + server: &Server, + world: &World, + block: &Block, + face: &BlockDirection, + block_pos: &BlockPos, + use_item_on: &SUseItemOn, + player_direction: &Direction, + other: bool, + ) -> u16 { + let face = match face { + BlockDirection::Bottom | BlockDirection::Top => *face, + _ => face.opposite(), + }; + + server + .block_properties_manager + .on_place_state( + world, + block, + &face, + block_pos, + use_item_on, + player_direction, + other, + ) + .await + } + + async fn use_with_item( + &self, + _block: &Block, + _player: &Player, + _location: BlockPos, + _item: &Item, + _server: &Server, + ) -> BlockActionResult { + BlockActionResult::Consume + } +} diff --git a/pumpkin/src/block/blocks/mod.rs b/pumpkin/src/block/blocks/mod.rs index 9922bc9fe..9a47083c5 100644 --- a/pumpkin/src/block/blocks/mod.rs +++ b/pumpkin/src/block/blocks/mod.rs @@ -9,6 +9,7 @@ pub(crate) mod chest; pub(crate) mod crafting_table; pub(crate) mod furnace; pub(crate) mod jukebox; +pub(crate) mod lever; /// The standard destroy with container removes the player forcibly from the container, /// drops items to the floor, and back to the player's inventory if the item stack is in movement. diff --git a/pumpkin/src/block/mod.rs b/pumpkin/src/block/mod.rs index 4d30959fa..295d7221c 100644 --- a/pumpkin/src/block/mod.rs +++ b/pumpkin/src/block/mod.rs @@ -1,6 +1,21 @@ -use blocks::chest::ChestBlock; -use blocks::furnace::FurnaceBlock; -use properties::BlockPropertiesManager; +use blocks::{chest::ChestBlock, furnace::FurnaceBlock, lever::LeverBlock}; +use properties::{ + age::Age, + attachment::Attachment, + axis::Axis, + cardinal::{Down, East, North, South, Up, West}, + face::Face, + facing::Facing, + half::Half, + layers::Layers, + open::Open, + powered::Powered, + signal_fire::SignalFire, + slab_type::SlabType, + stair_shape::StairShape, + waterlog::Waterlogged, + BlockPropertiesManager, +}; use pumpkin_data::entity::EntityType; use pumpkin_data::item::Item; use pumpkin_util::math::position::BlockPos; @@ -30,6 +45,7 @@ pub fn default_registry() -> Arc { manager.register(CraftingTableBlock); manager.register(FurnaceBlock); manager.register(ChestBlock); + manager.register(LeverBlock); Arc::new(manager) } @@ -56,6 +72,27 @@ pub async fn drop_loot(server: &Server, world: &Arc, block: &Block, pos: pub fn default_block_properties_manager() -> Arc { let mut manager = BlockPropertiesManager::default(); + // This is the default state of the blocks + manager.register(Age::Age0); + manager.register(Attachment::Floor); + manager.register(Axis::Y); + manager.register(Down::False); + manager.register(East::False); + manager.register(Face::Floor); + manager.register(Facing::North); + manager.register(Half::Bottom); + manager.register(Layers::Lay1); + manager.register(North::False); + manager.register(Open::False()); + manager.register(Powered::False()); + manager.register(SignalFire::False()); + manager.register(SlabType::Bottom); + manager.register(South::False); + manager.register(StairShape::Straight); + manager.register(Up::False); + manager.register(Waterlogged::False()); + manager.register(West::False); + manager.build_properties_registry(); Arc::new(manager) diff --git a/pumpkin/src/block/properties.rs b/pumpkin/src/block/properties.rs deleted file mode 100644 index d93e0a598..000000000 --- a/pumpkin/src/block/properties.rs +++ /dev/null @@ -1,516 +0,0 @@ -use std::collections::HashMap; - -use pumpkin_protocol::server::play::SUseItemOn; -use pumpkin_util::math::{position::BlockPos, vector3::Vector3}; -use pumpkin_world::block::{ - registry::{Block, State, BLOCKS}, - BlockDirection, -}; - -use crate::world::World; - -#[derive(Clone, Debug)] -pub enum BlockProperty { - Waterlogged(bool), - Facing(Direction), - Face(BlockFace), - Powered(bool), - SlabType(SlabPosition), - StairShape(StairShape), - Layers(u8), - Half(BlockHalf), // Add other properties as needed -} - -#[derive(Clone, Debug)] -pub enum BlockFace { - Floor, - Wall, - Ceiling, -} - -#[derive(Clone, Debug)] -pub enum BlockHalf { - Top, - Bottom, -} - -#[derive(Clone, Debug, PartialEq)] -pub enum SlabPosition { - Top, - Bottom, - Double, -} - -#[derive(Clone, Debug)] -pub enum StairShape { - Straight, - InnerLeft, - InnerRight, - OuterLeft, - OuterRight, -} - -#[derive(Clone, Debug, PartialEq)] -pub enum Direction { - North, - South, - East, - West, -} - -// TODO: We can automatically parse them ig -#[must_use] -pub fn get_property_key(property_name: &str) -> Option { - match property_name { - "waterlogged" => Some(BlockProperty::Waterlogged(false)), - "facing" => Some(BlockProperty::Facing(Direction::North)), - "type" => Some(BlockProperty::SlabType(SlabPosition::Top)), - "shape" => Some(BlockProperty::StairShape(StairShape::Straight)), - "half" => Some(BlockProperty::Half(BlockHalf::Bottom)), - "powered" => Some(BlockProperty::Powered(false)), - "layers" => Some(BlockProperty::Layers(1)), - "face" => Some(BlockProperty::Face(BlockFace::Wall)), - _ => None, - } -} - -#[must_use] -pub fn evaluate_layers( - block: &Block, - clicked_block: &Block, - clicked_block_state: &State, - face: BlockDirection, - use_item_on: &SUseItemOn, - properties: &BlockProperties, - _other: bool, -) -> (String, bool) { - let state = - &properties.property_mappings[&(clicked_block_state.id - clicked_block.states[0].id)]; - for property in state { - // Max - if property == "layers8" { - return (property.clone(), false); - } - - if block.id == clicked_block.id { - // bro its is so hacky :crying: - let mut layer: u8 = property.replace("layers", "").parse().unwrap(); - // lets add a new layer - layer += 1; - return (format!("{}{}", "layers", layer), true); - } - } - - let y_pos = use_item_on.cursor_pos.y; - if (y_pos > 0.5 && face != BlockDirection::Bottom) || face == BlockDirection::Top { - return (format!("{}{}", "layers", "1"), false); - } - - (format!("{}{}", "layers", "1"), false) -} - -#[must_use] -pub fn evaluate_property_type( - block: &Block, - clicked_block: &Block, - clicked_block_state: &State, - face: BlockDirection, - use_item_on: &SUseItemOn, - properties: &BlockProperties, - other: bool, -) -> (String, bool) { - let state = - &properties.property_mappings[&(clicked_block_state.id - clicked_block.states[0].id)]; - for property in state { - if property == "typedouble" { - return (property.clone(), false); - } - - if block.id == clicked_block.id { - if property == "typebottom" && face == BlockDirection::Top { - return (format!("{}{}", "type", "double"), true); - } - if property == "typetop" && face == BlockDirection::Bottom { - return (format!("{}{}", "type", "double"), true); - } - if !other { - return (format!("{}{}", "type", "double"), true); - } - } - } - - let y_pos = use_item_on.cursor_pos.y; - if (y_pos > 0.5 && face != BlockDirection::Bottom) || face == BlockDirection::Top { - return (format!("{}{}", "type", "top"), false); - } - - (format!("{}{}", "type", "bottom"), false) -} - -#[must_use] -pub fn evaluate_property_waterlogged(block: &Block) -> String { - if block.name == "water" { - return format!("{}{}", "waterlogged", "true"); - } - format!("{}{}", "waterlogged", "false") -} - -fn calculate_positions(player_direction: &Direction, block_pos: &BlockPos) -> (BlockPos, BlockPos) { - match player_direction { - Direction::North => ( - BlockPos(Vector3::new( - block_pos.0.x, - block_pos.0.y, - block_pos.0.z - 1, - )), - BlockPos(Vector3::new( - block_pos.0.x, - block_pos.0.y, - block_pos.0.z + 1, - )), - ), - Direction::South => ( - BlockPos(Vector3::new( - block_pos.0.x, - block_pos.0.y, - block_pos.0.z + 1, - )), - BlockPos(Vector3::new( - block_pos.0.x, - block_pos.0.y, - block_pos.0.z - 1, - )), - ), - Direction::East => ( - BlockPos(Vector3::new( - block_pos.0.x + 1, - block_pos.0.y, - block_pos.0.z, - )), - BlockPos(Vector3::new( - block_pos.0.x - 1, - block_pos.0.y, - block_pos.0.z, - )), - ), - Direction::West => ( - BlockPos(Vector3::new( - block_pos.0.x - 1, - block_pos.0.y, - block_pos.0.z, - )), - BlockPos(Vector3::new( - block_pos.0.x + 1, - block_pos.0.y, - block_pos.0.z, - )), - ), - } -} - -#[expect(clippy::implicit_hasher)] -pub async fn evaluate_property_shape( - world: &World, - block_pos: &BlockPos, - face: &BlockDirection, - use_item_on: &SUseItemOn, - player_direction: &Direction, - property_mappings: &HashMap>, -) -> String { - let block_half = evaluate_property_half(*face, use_item_on); - let (front_block_pos, back_block_pos) = calculate_positions(player_direction, block_pos); - - let front_block_and_state = world.get_block_and_block_state(&front_block_pos).await; - let back_block_and_state = world.get_block_and_block_state(&back_block_pos).await; - - match front_block_and_state { - Ok((block, state)) => { - if block.name.ends_with("stairs") { - log::debug!("Block in front is a stair block"); - - let key = state.id - block.states[0].id; - if let Some(properties) = property_mappings.get(&key) { - if properties.contains(&"shapestraight".to_owned()) - && properties.contains(&block_half) - { - let is_facing_north = properties.contains(&"facingnorth".to_owned()); - let is_facing_west = properties.contains(&"facingwest".to_owned()); - let is_facing_south = properties.contains(&"facingsouth".to_owned()); - let is_facing_east = properties.contains(&"facingeast".to_owned()); - - if (is_facing_north && *player_direction == Direction::West) - || (is_facing_west && *player_direction == Direction::South) - || (is_facing_south && *player_direction == Direction::East) - || (is_facing_east && *player_direction == Direction::North) - { - return "shapeouter_right".to_owned(); - } - - if (is_facing_north && *player_direction == Direction::East) - || (is_facing_west && *player_direction == Direction::North) - || (is_facing_south && *player_direction == Direction::West) - || (is_facing_east && *player_direction == Direction::South) - { - return "shapeouter_left".to_owned(); - } - } - } - } else { - log::debug!("Block to the left is not a stair block"); - } - } - Err(_) => { - log::debug!("There is no block to the left"); - } - } - - match back_block_and_state { - Ok((block, state)) => { - if block.name.ends_with("stairs") { - log::debug!("Block in back is a stair block"); - - let key = state.id - block.states[0].id; - if let Some(properties) = property_mappings.get(&key) { - if properties.contains(&"shapestraight".to_owned()) - && properties.contains(&block_half) - { - let is_facing_north = properties.contains(&"facingnorth".to_owned()); - let is_facing_west = properties.contains(&"facingwest".to_owned()); - let is_facing_south = properties.contains(&"facingsouth".to_owned()); - let is_facing_east = properties.contains(&"facingeast".to_owned()); - - if (is_facing_north && *player_direction == Direction::West) - || (is_facing_west && *player_direction == Direction::South) - || (is_facing_south && *player_direction == Direction::East) - || (is_facing_east && *player_direction == Direction::North) - { - return "shapeinner_right".to_owned(); - } - - if (is_facing_north && *player_direction == Direction::East) - || (is_facing_west && *player_direction == Direction::North) - || (is_facing_south && *player_direction == Direction::West) - || (is_facing_east && *player_direction == Direction::South) - { - return "shapeinner_left".to_owned(); - } - } - } - } else { - log::debug!("Block to the right is not a stair block"); - } - } - Err(_) => { - log::debug!("There is no block to the right"); - } - } - - // TODO: We currently don't notify adjacent stair blocks to update their shape after placement. - // We should implement a block update mechanism (e.g., tracking state changes and triggering - // a server-wide or chunk-level update) so that neighbors properly recalculate their shape. - - format!("{}{}", "shape", "straight") -} - -#[must_use] -pub fn evaluate_property_facing(face: BlockDirection, player_direction: &Direction) -> String { - let facing = match face { - BlockDirection::North => "south", - BlockDirection::South => "north", - BlockDirection::East => "west", - BlockDirection::West => "east", - BlockDirection::Top | BlockDirection::Bottom => match player_direction { - Direction::North => "north", - Direction::South => "south", - Direction::East => "east", - Direction::West => "west", - }, - }; - - format!("facing{facing}") -} - -#[must_use] -pub fn evaluate_property_block_face(dir: BlockDirection) -> String { - let block_face = if dir == BlockDirection::Bottom || dir == BlockDirection::Top { - if dir == BlockDirection::Top { - BlockFace::Ceiling - } else { - BlockFace::Floor - } - } else { - BlockFace::Wall - }; - - let facing = match block_face { - BlockFace::Floor => "floor", - BlockFace::Wall => "wall", - BlockFace::Ceiling => "ceiling", - }; - - format!("face{facing}") -} - -#[must_use] -pub fn evaluate_property_half(face: BlockDirection, use_item_on: &SUseItemOn) -> String { - match face { - BlockDirection::Bottom => format!("{}{}", "half", "bottom"), - BlockDirection::Top => format!("{}{}", "half", "top"), - _ => { - if use_item_on.cursor_pos.y > 0.5 { - format!("{}{}", "half", "top") - } else { - format!("{}{}", "half", "bottom") - } - } - } -} - -#[derive(Default)] -pub struct BlockPropertiesManager { - properties_registry: HashMap, -} - -pub struct BlockProperties { - // Mappings from property state strings -> offset - state_mappings: HashMap, u16>, - // Mappings from offset -> property state strings - property_mappings: HashMap>, -} - -impl BlockPropertiesManager { - pub fn build_properties_registry(&mut self) { - for block in &BLOCKS.blocks { - let properties = &block.properties; - if properties.is_empty() { - continue; - } - let total_combinations: usize = properties.iter().map(|p| p.values.len()).product(); - - let mut forward_map = HashMap::with_capacity(total_combinations); - let mut reverse_map = HashMap::with_capacity(total_combinations); - - for i in 0..total_combinations { - let mut current = i; - let mut combination = Vec::with_capacity(properties.len()); - - for property in properties.iter().rev() { - let property_size = property.values.len(); - combination.push(current % property_size); - current /= property_size; - } - - combination.reverse(); - - let key: Vec = combination - .iter() - .enumerate() - .map(|(prop_idx, &state_idx)| { - // Build "namevalue" strings, e.g. "facingnorth", "halfbottom", etc. - format!( - "{}{}", - properties[prop_idx].name, properties[prop_idx].values[state_idx] - ) - }) - .collect(); - - forward_map.insert(key.clone(), i as u16); - reverse_map.insert(i as u16, key); - } - self.properties_registry.insert( - block.id, - BlockProperties { - state_mappings: forward_map, - property_mappings: reverse_map, - }, - ); - } - } - - #[allow(clippy::too_many_arguments)] - pub async fn get_state_data( - &self, - world: &World, - block: &Block, - face: &BlockDirection, - block_pos: &BlockPos, - use_item_on: &SUseItemOn, - player_direction: &Direction, - other: bool, - ) -> (u16, bool) { - if let Some(properties) = self.properties_registry.get(&block.id) { - let mut hmap_key: Vec = Vec::with_capacity(block.properties.len()); - let mut updateable = false; - - for raw_property in &block.properties { - let property = get_property_key(raw_property.name.as_str()); - if let Some(property) = property { - let state = match property { - BlockProperty::SlabType(_) => { - let clicked_block = world.get_block(block_pos).await.unwrap(); - let clicked_block_state = - world.get_block_state(block_pos).await.unwrap(); - let (state, can_update) = evaluate_property_type( - block, - clicked_block, - clicked_block_state, - *face, - use_item_on, - properties, - other, - ); - updateable = can_update; - state - } - BlockProperty::Waterlogged(_) => evaluate_property_waterlogged(block), - BlockProperty::Facing(_) => { - evaluate_property_facing(*face, player_direction) - } - BlockProperty::Half(_) => evaluate_property_half(*face, use_item_on), - BlockProperty::StairShape(_) => { - evaluate_property_shape( - world, - block_pos, - face, - use_item_on, - player_direction, - &properties.property_mappings, - ) - .await - } - BlockProperty::Powered(_) => "poweredfalse".to_string(), // todo - BlockProperty::Face(_) => evaluate_property_block_face(*face), - BlockProperty::Layers(_) => { - let clicked_block = world.get_block(block_pos).await.unwrap(); - let clicked_block_state = - world.get_block_state(block_pos).await.unwrap(); - let (state, can_update) = evaluate_layers( - block, - clicked_block, - clicked_block_state, - *face, - use_item_on, - properties, - other, - ); - updateable = can_update; - state - } - }; - hmap_key.push(state.to_string()); - } else { - log::warn!("Unknown Block Property: {}", &raw_property.name); - // if one property is not found everything will not work - return (block.default_state_id, false); - } - } - // Base state id plus offset - let mapping = properties.state_mappings.get(&hmap_key); - if let Some(mapping) = mapping { - return (block.states[0].id + mapping, updateable); - } - log::error!("Failed to get Block Properties mapping for {}", block.name); - } - (block.default_state_id, false) - } -} diff --git a/pumpkin/src/block/properties/age.rs b/pumpkin/src/block/properties/age.rs new file mode 100644 index 000000000..3e74a6fed --- /dev/null +++ b/pumpkin/src/block/properties/age.rs @@ -0,0 +1,28 @@ +use async_trait::async_trait; +use pumpkin_macros::block_property; + +use super::BlockProperty; + +// Those which requires custom names to values can be defined like this +#[block_property("age", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])] +pub enum Age { + Age0, + Age1, + Age2, + Age3, + Age4, + Age5, + Age6, + Age7, + Age8, + Age9, + Age10, + Age11, + Age12, + Age13, + Age14, + Age15, +} + +#[async_trait] +impl BlockProperty for Age {} diff --git a/pumpkin/src/block/properties/attachment.rs b/pumpkin/src/block/properties/attachment.rs new file mode 100644 index 000000000..39d12f418 --- /dev/null +++ b/pumpkin/src/block/properties/attachment.rs @@ -0,0 +1,44 @@ +use crate::world::World; +use async_trait::async_trait; +use pumpkin_macros::block_property; +use pumpkin_protocol::server::play::SUseItemOn; +use pumpkin_util::math::position::BlockPos; +use pumpkin_world::block::{registry::Block, BlockDirection}; + +use super::{BlockProperties, BlockProperty, BlockPropertyMetadata, Direction}; + +#[block_property("attachment")] +pub enum Attachment { + Floor, + Ceiling, + SingleWall, + DoubleWall, +} + +#[async_trait] +impl BlockProperty for Attachment { + async fn on_place( + &self, + world: &World, + _block: &Block, + face: &BlockDirection, + block_pos: &BlockPos, + _use_item_on: &SUseItemOn, + _player_direction: &Direction, + _properties: &BlockProperties, + _other: bool, + ) -> String { + match face { + BlockDirection::Top => Self::Ceiling.value(), + BlockDirection::Bottom => Self::Floor.value(), + _ => { + let other_side_block = BlockPos(block_pos.0.sub(&face.to_offset())); + let block = world.get_block(&other_side_block).await.unwrap(); + if block.id != 0 { + return Self::DoubleWall.value(); + } + Self::SingleWall.value() + } + } + } +} diff --git a/pumpkin/src/block/properties/axis.rs b/pumpkin/src/block/properties/axis.rs new file mode 100644 index 000000000..615254d4e --- /dev/null +++ b/pumpkin/src/block/properties/axis.rs @@ -0,0 +1,36 @@ +use crate::world::World; +use async_trait::async_trait; +use pumpkin_macros::block_property; +use pumpkin_protocol::server::play::SUseItemOn; +use pumpkin_util::math::position::BlockPos; +use pumpkin_world::block::{registry::Block, BlockDirection}; + +use super::{BlockProperties, BlockProperty, BlockPropertyMetadata, Direction}; + +#[block_property("axis")] +pub enum Axis { + X, + Y, + Z, +} + +#[async_trait] +impl BlockProperty for Axis { + async fn on_place( + &self, + _world: &World, + _block: &Block, + face: &BlockDirection, + _block_pos: &BlockPos, + _use_item_on: &SUseItemOn, + _player_direction: &Direction, + _properties: &BlockProperties, + _other: bool, + ) -> String { + match face { + BlockDirection::North | BlockDirection::South => Self::Z.value(), + BlockDirection::East | BlockDirection::West => Self::X.value(), + BlockDirection::Top | BlockDirection::Bottom => Self::Y.value(), + } + } +} diff --git a/pumpkin/src/block/properties/cardinal.rs b/pumpkin/src/block/properties/cardinal.rs new file mode 100644 index 000000000..49fac6a2c --- /dev/null +++ b/pumpkin/src/block/properties/cardinal.rs @@ -0,0 +1,210 @@ +use crate::world::World; +use async_trait::async_trait; +use pumpkin_macros::block_property; +use pumpkin_protocol::server::play::SUseItemOn; +use pumpkin_util::math::{position::BlockPos, vector3::Vector3}; +use pumpkin_world::block::{registry::Block, BlockDirection}; + +use super::{BlockProperties, BlockProperty, BlockPropertyMetadata, Direction}; + +#[block_property("down")] +pub enum Down { + True, + False, + Tall, + Low, + None, +} +#[block_property("east")] +pub enum East { + True, + False, + Tall, + Low, + None, +} +#[block_property("north")] +pub enum North { + True, + False, + Tall, + Low, + None, +} +#[block_property("south")] +pub enum South { + True, + False, + Tall, + Low, + None, +} +#[block_property("up")] +pub enum Up { + True, + False, + Tall, + Low, + None, +} +#[block_property("west")] +pub enum West { + True, + False, + Tall, + Low, + None, +} + +pub async fn evaluate_fence_direction( + world: &World, + placed_block: &Block, + face: &BlockDirection, + block_pos: &BlockPos, +) -> String { + let other_side_block = BlockPos(block_pos.0.add(&face.to_offset())); + let block = world.get_block(&other_side_block).await.unwrap(); + if placed_block.name.ends_with("_wall") { + if face == &BlockDirection::Top { + if block.id != 0 { + return North::True.value(); + } + + let mut connections = 0u8; + let mut x = 0u8; + let mut z = 0u8; + for side in &[ + BlockDirection::North, + BlockDirection::East, + BlockDirection::South, + BlockDirection::West, + ] { + let other_side_block = BlockPos(block_pos.0.add(&side.to_offset())); + let block = world.get_block(&other_side_block).await.unwrap(); + if block.id != 0 { + connections += 1; + if *side == BlockDirection::North || *side == BlockDirection::South { + x += 1; + } else { + z += 1; + } + } + } + if connections > 2 || x == 2 || z == 2 { + return North::False.value(); + } + + return North::True.value(); + } + if block.id != 0 { + let other_side_block = BlockPos(other_side_block.0.add(&Vector3::new(0, 1, 0))); + let block = world.get_block(&other_side_block).await.unwrap(); + if block.id != 0 && block.name.ends_with("_wall") { + return North::Tall.value(); + } + return North::Low.value(); + } + return North::None.value(); + } + if block.id != 0 { + return North::True.value(); + } + North::False.value() +} + +#[async_trait] +impl BlockProperty for Down { + async fn on_place( + &self, + world: &World, + block: &Block, + face: &BlockDirection, + block_pos: &BlockPos, + _use_item_on: &SUseItemOn, + _player_direction: &Direction, + _properties: &BlockProperties, + _other: bool, + ) -> String { + evaluate_fence_direction(world, block, face, block_pos).await + } +} +#[async_trait] +impl BlockProperty for East { + async fn on_place( + &self, + world: &World, + block: &Block, + face: &BlockDirection, + block_pos: &BlockPos, + _use_item_on: &SUseItemOn, + _player_direction: &Direction, + _properties: &BlockProperties, + _other: bool, + ) -> String { + evaluate_fence_direction(world, block, face, block_pos).await + } +} +#[async_trait] +impl BlockProperty for North { + async fn on_place( + &self, + world: &World, + block: &Block, + face: &BlockDirection, + block_pos: &BlockPos, + _use_item_on: &SUseItemOn, + _player_direction: &Direction, + _properties: &BlockProperties, + _other: bool, + ) -> String { + evaluate_fence_direction(world, block, face, block_pos).await + } +} +#[async_trait] +impl BlockProperty for South { + async fn on_place( + &self, + world: &World, + block: &Block, + face: &BlockDirection, + block_pos: &BlockPos, + _use_item_on: &SUseItemOn, + _player_direction: &Direction, + _properties: &BlockProperties, + _other: bool, + ) -> String { + evaluate_fence_direction(world, block, face, block_pos).await + } +} +#[async_trait] +impl BlockProperty for Up { + async fn on_place( + &self, + world: &World, + block: &Block, + face: &BlockDirection, + block_pos: &BlockPos, + _use_item_on: &SUseItemOn, + _player_direction: &Direction, + _properties: &BlockProperties, + _other: bool, + ) -> String { + evaluate_fence_direction(world, block, face, block_pos).await + } +} +#[async_trait] +impl BlockProperty for West { + async fn on_place( + &self, + world: &World, + block: &Block, + face: &BlockDirection, + block_pos: &BlockPos, + _use_item_on: &SUseItemOn, + _player_direction: &Direction, + _properties: &BlockProperties, + _other: bool, + ) -> String { + evaluate_fence_direction(world, block, face, block_pos).await + } +} diff --git a/pumpkin/src/block/properties/face.rs b/pumpkin/src/block/properties/face.rs new file mode 100644 index 000000000..3b7735ce4 --- /dev/null +++ b/pumpkin/src/block/properties/face.rs @@ -0,0 +1,37 @@ +use crate::world::World; +use async_trait::async_trait; +use pumpkin_macros::block_property; +use pumpkin_protocol::server::play::SUseItemOn; +use pumpkin_util::math::position::BlockPos; +use pumpkin_world::block::{registry::Block, BlockDirection}; + +use super::{BlockProperties, BlockProperty, BlockPropertyMetadata, Direction}; + +#[block_property("face")] +pub enum Face { + Ceiling, + Floor, + Wall, +} + +#[async_trait] +impl BlockProperty for Face { + async fn on_place( + &self, + _world: &World, + _block: &Block, + face: &BlockDirection, + _block_pos: &BlockPos, + _use_item_on: &SUseItemOn, + _player_direction: &Direction, + _properties: &BlockProperties, + _other: bool, + ) -> String { + let face = match face { + BlockDirection::Top => Self::Ceiling, + BlockDirection::Bottom => Self::Floor, + _ => Self::Wall, + }; + face.value() + } +} diff --git a/pumpkin/src/block/properties/facing.rs b/pumpkin/src/block/properties/facing.rs new file mode 100644 index 000000000..558233925 --- /dev/null +++ b/pumpkin/src/block/properties/facing.rs @@ -0,0 +1,46 @@ +use crate::world::World; +use async_trait::async_trait; +use pumpkin_macros::block_property; +use pumpkin_protocol::server::play::SUseItemOn; +use pumpkin_util::math::position::BlockPos; +use pumpkin_world::block::{registry::Block, BlockDirection}; + +use super::{BlockProperties, BlockProperty, BlockPropertyMetadata, Direction}; + +#[block_property("facing")] +pub enum Facing { + North, + South, + East, + West, +} + +#[async_trait] +impl BlockProperty for Facing { + async fn on_place( + &self, + _world: &World, + _block: &Block, + face: &BlockDirection, + _block_pos: &BlockPos, + _use_item_on: &SUseItemOn, + player_direction: &Direction, + _properties: &BlockProperties, + _other: bool, + ) -> String { + // Some blocks have also facing with top and bottom + let facing = match face { + BlockDirection::North => Self::North, + BlockDirection::South => Self::South, + BlockDirection::East => Self::East, + BlockDirection::West => Self::West, + BlockDirection::Top | BlockDirection::Bottom => match player_direction { + Direction::North => Self::North, + Direction::South => Self::South, + Direction::East => Self::East, + Direction::West => Self::West, + }, + }; + facing.value() + } +} diff --git a/pumpkin/src/block/properties/half.rs b/pumpkin/src/block/properties/half.rs new file mode 100644 index 000000000..01964ba83 --- /dev/null +++ b/pumpkin/src/block/properties/half.rs @@ -0,0 +1,45 @@ +use crate::world::World; +use async_trait::async_trait; +use pumpkin_macros::block_property; +use pumpkin_protocol::server::play::SUseItemOn; +use pumpkin_util::math::position::BlockPos; +use pumpkin_world::block::{registry::Block, BlockDirection}; + +use super::{BlockProperties, BlockProperty, BlockPropertyMetadata, Direction}; + +#[block_property("half")] +pub enum Half { + Top, + Bottom, +} + +#[async_trait] +impl BlockProperty for Half { + async fn on_place( + &self, + _world: &World, + _block: &Block, + face: &BlockDirection, + _block_pos: &BlockPos, + use_item_on: &SUseItemOn, + _player_direction: &Direction, + _properties: &BlockProperties, + _other: bool, + ) -> String { + evaluate_half(*face, use_item_on) + } +} + +pub fn evaluate_half(face: BlockDirection, use_item_on: &SUseItemOn) -> String { + match face { + BlockDirection::Bottom => Half::Bottom.value(), + BlockDirection::Top => Half::Top.value(), + _ => { + if use_item_on.cursor_pos.y > 0.5 { + Half::Top.value() + } else { + Half::Bottom.value() + } + } + } +} diff --git a/pumpkin/src/block/properties/layers.rs b/pumpkin/src/block/properties/layers.rs new file mode 100644 index 000000000..bce83b332 --- /dev/null +++ b/pumpkin/src/block/properties/layers.rs @@ -0,0 +1,79 @@ +use crate::world::World; +use async_trait::async_trait; +use pumpkin_macros::block_property; +use pumpkin_protocol::server::play::SUseItemOn; +use pumpkin_util::math::position::BlockPos; +use pumpkin_world::block::{ + registry::{Block, State}, + BlockDirection, +}; + +use super::{BlockProperties, BlockProperty, BlockPropertyMetadata, Direction}; + +// Those which requires custom names to values can be defined like this +#[block_property("layers", [1, 2, 3, 4, 5, 6, 7, 8])] +pub enum Layers { + Lay1, + Lay2, + Lay3, + Lay4, + Lay5, + Lay6, + Lay7, + Lay8, +} + +#[async_trait] +impl BlockProperty for Layers { + async fn on_place( + &self, + world: &World, + block: &Block, + _face: &BlockDirection, + block_pos: &BlockPos, + _use_item_on: &SUseItemOn, + _player_direction: &Direction, + properties: &BlockProperties, + _other: bool, + ) -> String { + let clicked_block = world.get_block(block_pos).await.unwrap(); + let clicked_block_state = world.get_block_state(block_pos).await.unwrap(); + let state = + &properties.property_mappings[&(clicked_block_state.id - clicked_block.states[0].id)]; + for property in state { + if block.id == clicked_block.id { + // bro its is so hacky :crying: + let mut layer: u8 = property.parse().unwrap(); + // lets add a new layer + layer += 1; + return Self::from_value(layer.to_string()).value(); + } + } + + Self::Lay1.value() + } + + async fn can_update( + &self, + value: String, + block: &Block, + _block_state: &State, + face: &BlockDirection, + _use_item_on: &SUseItemOn, + other: bool, + ) -> bool { + if value == Self::Lay8.value() { + return false; + } + if value == Self::Lay1.value() { + return true; + } + if !other { + match face { + BlockDirection::Top => return block.name == "snow", + _ => return false, + } + } + true + } +} diff --git a/pumpkin/src/block/properties/mod.rs b/pumpkin/src/block/properties/mod.rs new file mode 100644 index 000000000..71494b9c5 --- /dev/null +++ b/pumpkin/src/block/properties/mod.rs @@ -0,0 +1,251 @@ +use std::{collections::HashMap, sync::Arc}; + +use async_trait::async_trait; +use pumpkin_protocol::server::play::SUseItemOn; +use pumpkin_util::math::position::BlockPos; +use pumpkin_world::{ + block::{ + registry::{Block, State, BLOCKS}, + BlockDirection, + }, + item::ItemStack, +}; + +pub(crate) mod age; +pub(crate) mod attachment; +pub(crate) mod axis; +pub(crate) mod cardinal; +pub(crate) mod face; +pub(crate) mod facing; +pub(crate) mod half; +pub(crate) mod layers; +pub(crate) mod open; +pub(crate) mod powered; +pub(crate) mod signal_fire; +pub(crate) mod slab_type; +pub(crate) mod stair_shape; +pub(crate) mod waterlog; + +use crate::world::World; + +#[derive(Clone, Debug, PartialEq)] +pub enum Direction { + North, + South, + East, + West, +} + +pub trait BlockPropertyMetadata: Sync + Send { + fn name(&self) -> &'static str; + fn value(&self) -> String; + fn from_value(value: String) -> Self + where + Self: Sized; +} + +#[async_trait] +#[allow(clippy::too_many_arguments)] +pub trait BlockProperty: Sync + Send + BlockPropertyMetadata { + async fn on_place( + &self, + _world: &World, + _block: &Block, + _face: &BlockDirection, + _block_pos: &BlockPos, + _use_item_on: &SUseItemOn, + _player_direction: &Direction, + _properties: &BlockProperties, + _other: bool, + ) -> String { + self.value() + } + + async fn can_update( + &self, + _value: String, + _block: &Block, + block_state: &State, + _face: &BlockDirection, + _use_item_on: &SUseItemOn, + _other: bool, + ) -> bool { + block_state.replaceable + } + + async fn on_interact(&self, value: String, _block: &Block, _item: &ItemStack) -> String { + value + } +} + +#[derive(Default)] +pub struct BlockPropertiesManager { + properties_registry: HashMap, + // Properties that are implemented + registered_properties: HashMap>, +} + +pub struct BlockProperties { + // Mappings from property state strings -> offset + state_mappings: HashMap, u16>, + // Mappings from offset -> property state strings + property_mappings: HashMap>, +} + +impl BlockPropertiesManager { + pub fn build_properties_registry(&mut self) { + for block in &BLOCKS.blocks { + let properties = &block.properties; + if properties.is_empty() { + continue; + } + let total_combinations: usize = properties.iter().map(|p| p.values.len()).product(); + + let mut forward_map = HashMap::with_capacity(total_combinations); + let mut reverse_map = HashMap::with_capacity(total_combinations); + + for i in 0..total_combinations { + let mut current = i; + let mut combination = Vec::with_capacity(properties.len()); + + for property in properties.iter().rev() { + let property_size = property.values.len(); + combination.push(current % property_size); + current /= property_size; + } + + combination.reverse(); + + let key: Vec = combination + .iter() + .enumerate() + .map(|(prop_idx, &state_idx)| properties[prop_idx].values[state_idx].clone()) + .collect(); + + forward_map.insert(key.clone(), i as u16); + reverse_map.insert(i as u16, key); + } + self.properties_registry.insert( + block.id, + BlockProperties { + state_mappings: forward_map, + property_mappings: reverse_map, + }, + ); + } + } + + pub fn register(&mut self, property: T) { + self.registered_properties + .insert(property.name().to_string(), Arc::new(property)); + } + + pub async fn can_update( + &self, + block: &Block, + block_state: &State, + face: &BlockDirection, + use_item_on: &SUseItemOn, + other: bool, + ) -> bool { + if let Some(properties) = self.properties_registry.get(&block.id) { + let key = block_state.id - block.states[0].id; + let property_states = properties.property_mappings.get(&key).unwrap(); + for (i, property_value) in property_states.iter().enumerate() { + let property_name = block.properties[i].name.clone(); + if let Some(property) = self.registered_properties.get(&property_name) { + if property + .can_update( + property_value.clone(), + block, + block_state, + face, + use_item_on, + other, + ) + .await + { + return true; + } + } + } + } + false + } + + #[allow(clippy::too_many_arguments)] + pub async fn on_place_state( + &self, + world: &World, + block: &Block, + face: &BlockDirection, + block_pos: &BlockPos, + use_item_on: &SUseItemOn, + player_direction: &Direction, + other: bool, + ) -> u16 { + if let Some(properties) = self.properties_registry.get(&block.id) { + let mut hmap_key: Vec = Vec::with_capacity(block.properties.len()); + + for raw_property in &block.properties { + let property = self.registered_properties.get(&raw_property.name); + if let Some(property) = property { + let state = property + .on_place( + world, + block, + face, + block_pos, + use_item_on, + player_direction, + properties, + other, + ) + .await; + hmap_key.push(state); + } else { + log::warn!("Unknown Block Property: {}", &raw_property.name); + // if one property is not found everything will not work + return block.default_state_id; + } + } + // Base state id plus offset + let mapping = properties.state_mappings.get(&hmap_key); + if let Some(mapping) = mapping { + return block.states[0].id + mapping; + } + log::error!("Failed to get Block Properties mapping for {}", block.name); + } + block.default_state_id + } + + pub async fn on_interact(&self, block: &Block, block_state: &State, item: &ItemStack) -> u16 { + if let Some(properties) = self.properties_registry.get(&block.id) { + if let Some(states) = properties + .property_mappings + .get(&(block_state.id - block.states[0].id)) + { + let mut hmap_key: Vec = Vec::with_capacity(block.properties.len()); + + for (i, raw_property) in block.properties.iter().enumerate() { + let property = self.registered_properties.get(&raw_property.name); + if let Some(property) = property { + let state = property.on_interact(states[i].clone(), block, item).await; + hmap_key.push(state); + } else { + log::warn!("Unknown Block Property: {}", &raw_property.name); + // if one property is not found everything will not work + return block.default_state_id; + } + } + // Base state id plus offset + let mapping = properties.state_mappings.get(&hmap_key); + if let Some(mapping) = mapping { + return block.states[0].id + mapping; + } + log::error!("Failed to get Block Properties mapping for {}", block.name); + } + } + block_state.id + } +} diff --git a/pumpkin/src/block/properties/open.rs b/pumpkin/src/block/properties/open.rs new file mode 100644 index 000000000..620ff3977 --- /dev/null +++ b/pumpkin/src/block/properties/open.rs @@ -0,0 +1,9 @@ +use super::BlockProperty; +use async_trait::async_trait; +use pumpkin_macros::block_property; + +#[block_property("open")] +pub struct Open(bool); + +#[async_trait] +impl BlockProperty for Open {} diff --git a/pumpkin/src/block/properties/powered.rs b/pumpkin/src/block/properties/powered.rs new file mode 100644 index 000000000..7de8836d9 --- /dev/null +++ b/pumpkin/src/block/properties/powered.rs @@ -0,0 +1,20 @@ +use async_trait::async_trait; +use pumpkin_macros::block_property; +use pumpkin_world::block::registry::Block; +use pumpkin_world::item::ItemStack; + +use super::{BlockProperty, BlockPropertyMetadata}; + +#[block_property("powered")] +pub struct Powered(bool); + +#[async_trait] +impl BlockProperty for Powered { + async fn on_interact(&self, value: String, _block: &Block, _item: &ItemStack) -> String { + if value == Self::True().value() { + Self::False().value() + } else { + Self::True().value() + } + } +} diff --git a/pumpkin/src/block/properties/signal_fire.rs b/pumpkin/src/block/properties/signal_fire.rs new file mode 100644 index 000000000..a739d571e --- /dev/null +++ b/pumpkin/src/block/properties/signal_fire.rs @@ -0,0 +1,33 @@ +use crate::world::World; +use async_trait::async_trait; +use pumpkin_macros::block_property; +use pumpkin_protocol::server::play::SUseItemOn; +use pumpkin_util::math::{position::BlockPos, vector3::Vector3}; +use pumpkin_world::block::{registry::Block, BlockDirection}; + +use super::{BlockProperties, BlockProperty, BlockPropertyMetadata, Direction}; + +#[block_property("signal_fire")] +pub struct SignalFire(bool); + +#[async_trait] +impl BlockProperty for SignalFire { + async fn on_place( + &self, + world: &World, + _block: &Block, + _face: &BlockDirection, + block_pos: &BlockPos, + _use_item_on: &SUseItemOn, + _player_direction: &Direction, + _properties: &BlockProperties, + _other: bool, + ) -> String { + let other_side_block = BlockPos(block_pos.0.sub(&Vector3::new(0, 1, 0))); + let block = world.get_block(&other_side_block).await.unwrap(); + if block.name == "hay_block" { + return Self::True().value(); + } + Self::False().value() + } +} diff --git a/pumpkin/src/block/properties/slab_type.rs b/pumpkin/src/block/properties/slab_type.rs new file mode 100644 index 000000000..355354b9b --- /dev/null +++ b/pumpkin/src/block/properties/slab_type.rs @@ -0,0 +1,78 @@ +use crate::world::World; +use async_trait::async_trait; +use pumpkin_macros::block_property; +use pumpkin_protocol::server::play::SUseItemOn; +use pumpkin_util::math::position::BlockPos; +use pumpkin_world::block::{ + registry::{Block, State}, + BlockDirection, +}; + +use super::{BlockProperties, BlockProperty, BlockPropertyMetadata, Direction}; + +#[block_property("type")] +pub enum SlabType { + Top, + Bottom, + Double, +} + +#[async_trait] +impl BlockProperty for SlabType { + async fn on_place( + &self, + world: &World, + block: &Block, + face: &BlockDirection, + block_pos: &BlockPos, + use_item_on: &SUseItemOn, + _player_direction: &Direction, + _properties: &BlockProperties, + other: bool, + ) -> String { + let clicked_block = world.get_block(block_pos).await.unwrap(); + + if block.id == clicked_block.id && !other { + return Self::Double.value(); + } + + let y_pos = use_item_on.cursor_pos.y; + if (y_pos > 0.5 && face != &BlockDirection::Bottom) || face == &BlockDirection::Top { + return Self::Top.value(); + } + + Self::Bottom.value() + } + + async fn can_update( + &self, + value: String, + _block: &Block, + _block_state: &State, + face: &BlockDirection, + use_item_on: &SUseItemOn, + other: bool, + ) -> bool { + if other { + let y = use_item_on.cursor_pos.y; + match face { + BlockDirection::Top => return value == Self::Bottom.value(), + BlockDirection::Bottom => return value == Self::Top.value(), + _ => { + if y < 0.5 { + return value == Self::Top.value(); + } + return value == Self::Bottom.value(); + } + } + } + if value == Self::Double.value() { + return false; + } + match face { + BlockDirection::Top => value == Self::Bottom.value(), + BlockDirection::Bottom => value == Self::Top.value(), + _ => false, + } + } +} diff --git a/pumpkin/src/block/properties/stair_shape.rs b/pumpkin/src/block/properties/stair_shape.rs new file mode 100644 index 000000000..be2a3f418 --- /dev/null +++ b/pumpkin/src/block/properties/stair_shape.rs @@ -0,0 +1,182 @@ +use crate::{ + block::properties::{facing::Facing, half::evaluate_half, BlockPropertyMetadata}, + world::World, +}; +use async_trait::async_trait; +use pumpkin_macros::block_property; +use pumpkin_protocol::server::play::SUseItemOn; +use pumpkin_util::math::{position::BlockPos, vector3::Vector3}; +use pumpkin_world::block::{registry::Block, BlockDirection}; + +use super::{BlockProperties, BlockProperty, Direction}; + +#[block_property("shape")] +pub enum StairShape { + Straight, + InnerLeft, + InnerRight, + OuterLeft, + OuterRight, +} + +#[async_trait] +impl BlockProperty for StairShape { + async fn on_place( + &self, + world: &World, + _block: &Block, + face: &BlockDirection, + block_pos: &BlockPos, + use_item_on: &SUseItemOn, + player_direction: &Direction, + properties: &BlockProperties, + _other: bool, + ) -> String { + let block_half = evaluate_half(*face, use_item_on); + let (front_block_pos, back_block_pos) = calculate_positions(player_direction, block_pos); + + let front_block_and_state = world.get_block_and_block_state(&front_block_pos).await; + let back_block_and_state = world.get_block_and_block_state(&back_block_pos).await; + + match front_block_and_state { + Ok((block, state)) => { + if block.name.ends_with("stairs") { + log::debug!("Block in front is a stair block"); + + let key = state.id - block.states[0].id; + if let Some(properties) = properties.property_mappings.get(&key) { + if properties.contains(&Self::Straight.value()) + && properties.contains(&block_half) + { + let is_facing_north = properties.contains(&Facing::North.value()); + let is_facing_west = properties.contains(&Facing::West.value()); + let is_facing_south = properties.contains(&Facing::South.value()); + let is_facing_east = properties.contains(&Facing::East.value()); + + if (is_facing_north && *player_direction == Direction::West) + || (is_facing_west && *player_direction == Direction::South) + || (is_facing_south && *player_direction == Direction::East) + || (is_facing_east && *player_direction == Direction::North) + { + return Self::OuterRight.value(); + } + + if (is_facing_north && *player_direction == Direction::East) + || (is_facing_west && *player_direction == Direction::North) + || (is_facing_south && *player_direction == Direction::West) + || (is_facing_east && *player_direction == Direction::South) + { + return Self::OuterLeft.value(); + } + } + } + } else { + log::debug!("Block to the left is not a stair block"); + } + } + Err(_) => { + log::debug!("There is no block to the left"); + } + } + + match back_block_and_state { + Ok((block, state)) => { + if block.name.ends_with("stairs") { + log::debug!("Block in back is a stair block"); + + let key = state.id - block.states[0].id; + if let Some(properties) = properties.property_mappings.get(&key) { + if properties.contains(&Self::Straight.value()) + && properties.contains(&block_half) + { + let is_facing_north = properties.contains(&Facing::North.value()); + let is_facing_west = properties.contains(&Facing::West.value()); + let is_facing_south = properties.contains(&Facing::South.value()); + let is_facing_east = properties.contains(&Facing::East.value()); + + if (is_facing_north && *player_direction == Direction::West) + || (is_facing_west && *player_direction == Direction::South) + || (is_facing_south && *player_direction == Direction::East) + || (is_facing_east && *player_direction == Direction::North) + { + return Self::InnerRight.value(); + } + + if (is_facing_north && *player_direction == Direction::East) + || (is_facing_west && *player_direction == Direction::North) + || (is_facing_south && *player_direction == Direction::West) + || (is_facing_east && *player_direction == Direction::South) + { + return Self::InnerLeft.value(); + } + } + } + } else { + log::debug!("Block to the right is not a stair block"); + } + } + Err(_) => { + log::debug!("There is no block to the right"); + } + } + + // TODO: We currently don't notify adjacent stair blocks to update their shape after placement. + // We should implement a block update mechanism (e.g., tracking state changes and triggering + // a server-wide or chunk-level update) so that neighbors properly recalculate their shape. + + Self::Straight.value() + } +} + +fn calculate_positions(player_direction: &Direction, block_pos: &BlockPos) -> (BlockPos, BlockPos) { + match player_direction { + Direction::North => ( + BlockPos(Vector3::new( + block_pos.0.x, + block_pos.0.y, + block_pos.0.z - 1, + )), + BlockPos(Vector3::new( + block_pos.0.x, + block_pos.0.y, + block_pos.0.z + 1, + )), + ), + Direction::South => ( + BlockPos(Vector3::new( + block_pos.0.x, + block_pos.0.y, + block_pos.0.z + 1, + )), + BlockPos(Vector3::new( + block_pos.0.x, + block_pos.0.y, + block_pos.0.z - 1, + )), + ), + Direction::East => ( + BlockPos(Vector3::new( + block_pos.0.x + 1, + block_pos.0.y, + block_pos.0.z, + )), + BlockPos(Vector3::new( + block_pos.0.x - 1, + block_pos.0.y, + block_pos.0.z, + )), + ), + Direction::West => ( + BlockPos(Vector3::new( + block_pos.0.x - 1, + block_pos.0.y, + block_pos.0.z, + )), + BlockPos(Vector3::new( + block_pos.0.x + 1, + block_pos.0.y, + block_pos.0.z, + )), + ), + } +} diff --git a/pumpkin/src/block/properties/waterlog.rs b/pumpkin/src/block/properties/waterlog.rs new file mode 100644 index 000000000..4cf5d8ac4 --- /dev/null +++ b/pumpkin/src/block/properties/waterlog.rs @@ -0,0 +1,31 @@ +use crate::world::World; +use async_trait::async_trait; +use pumpkin_macros::block_property; +use pumpkin_protocol::server::play::SUseItemOn; +use pumpkin_util::math::position::BlockPos; +use pumpkin_world::block::{registry::Block, BlockDirection}; + +use super::{BlockProperties, BlockProperty, BlockPropertyMetadata, Direction}; + +#[block_property("waterlogged")] +pub struct Waterlogged(bool); + +#[async_trait] +impl BlockProperty for Waterlogged { + async fn on_place( + &self, + _world: &World, + block: &Block, + _face: &BlockDirection, + _block_pos: &BlockPos, + _use_item_on: &SUseItemOn, + _player_direction: &Direction, + _properties: &BlockProperties, + _other: bool, + ) -> String { + if block.name == "water" { + return Self::True().value(); + } + Self::False().value() + } +} diff --git a/pumpkin/src/block/pumpkin_block.rs b/pumpkin/src/block/pumpkin_block.rs index 647bc53d4..5a88ceaf8 100644 --- a/pumpkin/src/block/pumpkin_block.rs +++ b/pumpkin/src/block/pumpkin_block.rs @@ -1,11 +1,16 @@ use crate::block::registry::BlockActionResult; use crate::entity::player::Player; use crate::server::Server; +use crate::world::World; use async_trait::async_trait; use pumpkin_data::item::Item; use pumpkin_inventory::OpenContainer; +use pumpkin_protocol::server::play::SUseItemOn; use pumpkin_util::math::position::BlockPos; use pumpkin_world::block::registry::Block; +use pumpkin_world::block::BlockDirection; + +use super::properties::Direction; pub trait BlockMetadata { const NAMESPACE: &'static str; @@ -36,6 +41,32 @@ pub trait PumpkinBlock: Send + Sync { BlockActionResult::Continue } + #[allow(clippy::too_many_arguments)] + async fn on_place( + &self, + server: &Server, + world: &World, + block: &Block, + face: &BlockDirection, + block_pos: &BlockPos, + use_item_on: &SUseItemOn, + player_direction: &Direction, + other: bool, + ) -> u16 { + server + .block_properties_manager + .on_place_state( + world, + block, + face, + block_pos, + use_item_on, + player_direction, + other, + ) + .await + } + async fn placed( &self, _block: &Block, diff --git a/pumpkin/src/block/registry.rs b/pumpkin/src/block/registry.rs index a33aca6ad..cb8743d5d 100644 --- a/pumpkin/src/block/registry.rs +++ b/pumpkin/src/block/registry.rs @@ -1,13 +1,18 @@ use crate::block::pumpkin_block::{BlockMetadata, PumpkinBlock}; use crate::entity::player::Player; use crate::server::Server; +use crate::world::World; use pumpkin_data::item::Item; use pumpkin_inventory::OpenContainer; +use pumpkin_protocol::server::play::SUseItemOn; use pumpkin_util::math::position::BlockPos; use pumpkin_world::block::registry::Block; +use pumpkin_world::block::BlockDirection; use std::collections::HashMap; use std::sync::Arc; +use super::properties::Direction; + pub enum BlockActionResult { /// Allow other actions to be executed Continue, @@ -57,6 +62,47 @@ impl BlockRegistry { BlockActionResult::Continue } + #[allow(clippy::too_many_arguments)] + pub async fn on_place( + &self, + server: &Server, + world: &World, + block: &Block, + face: &BlockDirection, + block_pos: &BlockPos, + use_item_on: &SUseItemOn, + player_direction: &Direction, + other: bool, + ) -> u16 { + let pumpkin_block = self.get_pumpkin_block(block); + if let Some(pumpkin_block) = pumpkin_block { + return pumpkin_block + .on_place( + server, + world, + block, + face, + block_pos, + use_item_on, + player_direction, + other, + ) + .await; + } + server + .block_properties_manager + .on_place_state( + world, + block, + face, + block_pos, + use_item_on, + player_direction, + other, + ) + .await + } + pub async fn on_placed( &self, block: &Block, diff --git a/pumpkin/src/net/packet/play.rs b/pumpkin/src/net/packet/play.rs index 5d5efb9c3..865f21d4c 100644 --- a/pumpkin/src/net/packet/play.rs +++ b/pumpkin/src/net/packet/play.rs @@ -969,6 +969,15 @@ impl Player { .block_registry .on_use(block, self, location, server) .await; + let block_state = world.get_block_state(&location).await?; + let new_state = server + .block_properties_manager + .on_interact(block, block_state, &ItemStack::new(0, Item::AIR)) + .await; + world.set_block_state(&location, new_state).await; + self.client + .send_packet(&CAcknowledgeBlockChange::new(use_item_on.sequence)) + .await; } return Ok(()); }; @@ -982,6 +991,15 @@ impl Player { .block_registry .use_with_item(block, self, location, &stack.item, server) .await; + let block_state = world.get_block_state(&location).await?; + let new_state = server + .block_properties_manager + .on_interact(block, block_state, &ItemStack::new(0, Item::AIR)) + .await; + world.set_block_state(&location, new_state).await; + self.client + .send_packet(&CAcknowledgeBlockChange::new(use_item_on.sequence.clone())) + .await; match action_result { BlockActionResult::Continue => {} BlockActionResult::Consume => { @@ -1206,6 +1224,7 @@ impl Player { } } + #[allow(clippy::too_many_lines)] async fn run_is_block_place( &self, block: Block, @@ -1219,6 +1238,7 @@ impl Player { let clicked_block_pos = BlockPos(location.0); let clicked_block_state = world.get_block_state(&clicked_block_pos).await?; + let clicked_block = world.get_block(&clicked_block_pos).await?; // check block under the world if location.0.y + face.to_offset().y < WORLD_LOWEST_Y.into() { @@ -1255,34 +1275,31 @@ impl Player { _ => {} } - let (mut new_state, mut updateable) = server + let mut updateable = server .block_properties_manager - .get_state_data( - world, - &block, + .can_update( + clicked_block, + clicked_block_state, face, - &clicked_block_pos, &use_item_on, - &self.get_player_direction(), - true, + false, ) .await; - let final_block_pos = if clicked_block_state.replaceable || updateable { - clicked_block_pos + let (final_block_pos, final_face) = if updateable { + (clicked_block_pos, face) } else { let block_pos = BlockPos(location.0 + face.to_offset()); + let previous_block = world.get_block(&block_pos).await?; let previous_block_state = world.get_block_state(&block_pos).await?; - (new_state, updateable) = server + updateable = server .block_properties_manager - .get_state_data( - world, - &block, + .can_update( + previous_block, + previous_block_state, &face.opposite(), - &block_pos, &use_item_on, - &self.get_player_direction(), - false, + true, ) .await; @@ -1290,9 +1307,23 @@ impl Player { return Ok(true); } - block_pos + (block_pos, &face.opposite()) }; + let new_state = server + .block_registry + .on_place( + server, + world, + &block, + final_face, + &final_block_pos, + &use_item_on, + &self.get_player_direction(), + !(clicked_block_state.replaceable || updateable), + ) + .await; + // To this point we must have the new block state let shapes = get_block_collision_shapes(new_state).unwrap_or_default(); let mut intersects = false;