Adds support for Block properties (#485)

* Something

* Double slabs working

* Revert changes to blocks.json

* Fixed merge and clippy issues

* Fixed Bad merge for play.rs

* fix slabs and adds stairs

* Fix issue with fmt

---------

Co-authored-by: Bafran <aachinoy@gmail.com>
Co-authored-by: Alexander Medvedev <lilalexmed@proton.me>
This commit is contained in:
Kyle Davis
2025-01-28 05:15:55 -05:00
committed by GitHub
parent 136254729f
commit 6c4eb88dd3
11 changed files with 713 additions and 16 deletions

View File

@@ -21,6 +21,8 @@ serde.workspace = true
serde_json.workspace = true
log.workspace = true
num-derive = "0.4.2"
dashmap = "6.1.0"
num-traits = "0.2"

View File

@@ -126,11 +126,10 @@ pub struct Block {
pub default_state_id: u16,
pub states: Vec<State>,
}
#[expect(dead_code)]
#[derive(Deserialize, Clone, Debug)]
pub struct Property {
name: String,
values: Vec<String>,
pub name: String,
pub values: Vec<String>,
}
#[derive(Deserialize, Clone, Debug)]
pub struct State {

View File

@@ -1,10 +1,12 @@
pub mod block_registry;
pub mod block_state;
use num_derive::FromPrimitive;
use pumpkin_util::math::vector3::Vector3;
pub use block_state::BlockState;
#[derive(FromPrimitive, PartialEq, Clone, Copy)]
pub enum BlockFace {
Bottom = 0,
Top,

View File

@@ -1 +1,9 @@
pub mod entity_registry;
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum FacingDirection {
North,
South,
East,
West,
}

View File

@@ -0,0 +1,135 @@
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::{
block_registry::{Block, BLOCKS},
BlockFace,
},
entity::FacingDirection,
};
use crate::world::World;
use super::properties::{slab::SlabBehavior, stair::StairBehavior};
#[async_trait]
pub trait BlockBehavior: Send + Sync {
async fn map_state_id(
&self,
world: &World,
block: &Block,
face: &BlockFace,
block_pos: &BlockPos,
use_item_on: &SUseItemOn,
player_direction: &FacingDirection,
) -> u16;
async fn is_updateable(
&self,
world: &World,
block: &Block,
face: &BlockFace,
block_pos: &BlockPos,
) -> bool;
}
#[derive(Clone, Debug)]
pub enum BlockProperty {
Waterlogged(bool),
Facing(Direction),
SlabType(SlabPosition),
StairShape(StairShape),
Half(BlockHalf), // Add other properties as needed
}
#[derive(Clone, Debug)]
pub enum BlockHalf {
Top,
Bottom,
}
#[derive(Clone, Debug)]
pub enum SlabPosition {
Top,
Bottom,
Double,
}
#[derive(Clone, Debug)]
pub enum StairShape {
Straight,
InnerLeft,
InnerRight,
OuterLeft,
OuterRight,
}
#[derive(Clone, Debug)]
pub enum Direction {
North,
South,
East,
West,
}
#[must_use]
pub fn get_property_key(property_name: &str) -> Option<BlockProperty> {
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)),
_ => None,
}
}
#[derive(Default)]
pub struct BlockPropertiesManager {
properties_registry: HashMap<u16, Arc<dyn BlockBehavior>>,
}
impl BlockPropertiesManager {
pub fn build_properties_registry(&mut self) {
for block in &BLOCKS.blocks {
let behaviour: Arc<dyn BlockBehavior> = match block.name.as_str() {
name if name.ends_with("_slab") => SlabBehavior::get_or_init(&block.properties),
name if name.ends_with("_stairs") => StairBehavior::get_or_init(&block.properties),
_ => continue,
};
self.properties_registry.insert(block.id, behaviour);
}
}
pub async fn get_state_id(
&self,
world: &World,
block: &Block,
face: &BlockFace,
block_pos: &BlockPos,
use_item_on: &SUseItemOn,
player_direction: &FacingDirection,
) -> u16 {
if let Some(behaviour) = self.properties_registry.get(&block.id) {
return behaviour
.map_state_id(world, block, face, block_pos, use_item_on, player_direction)
.await;
}
block.default_state_id
}
pub async fn is_updateable(
&self,
world: &World,
block: &Block,
face: &BlockFace,
block_pos: &BlockPos,
) -> bool {
if let Some(behaviour) = self.properties_registry.get(&block.id) {
return behaviour.is_updateable(world, block, face, block_pos).await;
}
false
}
}

View File

@@ -1,3 +1,4 @@
use block_properties_manager::BlockPropertiesManager;
use blocks::chest::ChestBlock;
use blocks::furnace::FurnaceBlock;
@@ -7,7 +8,9 @@ use crate::block::blocks::jukebox::JukeboxBlock;
use std::sync::Arc;
pub mod block_manager;
pub mod block_properties_manager;
mod blocks;
mod properties;
pub mod pumpkin_block;
#[must_use]
@@ -21,3 +24,12 @@ pub fn default_block_manager() -> Arc<BlockManager> {
Arc::new(manager)
}
#[must_use]
pub fn default_block_properties_manager() -> Arc<BlockPropertiesManager> {
let mut manager = BlockPropertiesManager::default();
manager.build_properties_registry();
Arc::new(manager)
}

View File

@@ -0,0 +1,2 @@
pub(crate) mod slab;
pub(crate) mod stair;

View File

@@ -0,0 +1,167 @@
use std::{
collections::HashMap,
sync::{Arc, OnceLock},
};
use pumpkin_protocol::server::play::SUseItemOn;
use pumpkin_util::math::position::BlockPos;
use pumpkin_world::block::{block_registry::Property, BlockFace};
use pumpkin_world::{block::block_registry::Block, entity::FacingDirection};
use crate::{
block::block_properties_manager::{get_property_key, BlockBehavior, BlockProperty},
world::World,
};
pub static SLAB_BEHAVIOR: OnceLock<Arc<SlabBehavior>> = OnceLock::new();
// Example of a behavior with shared static data
pub struct SlabBehavior {
// Shared static data for all slabs
state_mappings: HashMap<Vec<String>, u16>,
property_mappings: HashMap<u16, Vec<String>>,
}
impl SlabBehavior {
pub fn get_or_init(properties: &[Property]) -> Arc<Self> {
SLAB_BEHAVIOR
.get_or_init(|| Arc::new(Self::new(properties)))
.clone()
}
pub fn get() -> Arc<Self> {
SLAB_BEHAVIOR.get().expect("Slab Uninitialized").clone()
}
pub fn new(properties: &[Property]) -> Self {
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<String> = combination
.iter()
.enumerate()
.map(|(prop_idx, &state_idx)| {
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 {
state_mappings: forward_map,
property_mappings: reverse_map,
}
}
pub fn evalute_property_type(
block: &Block,
clicked_block: &Block,
face: BlockFace,
use_item_on: &SUseItemOn,
) -> String {
if block.id == clicked_block.id && face == BlockFace::Top {
return format!("{}{}", "type", "double");
}
if face == BlockFace::Top {
return format!("{}{}", "type", "bottom");
}
if face == BlockFace::North
|| face == BlockFace::South
|| face == BlockFace::West
|| face == BlockFace::East
{
let y_pos = use_item_on.cursor_pos.y;
if y_pos > 0.5 {
return format!("{}{}", "type", "top");
}
return format!("{}{}", "type", "bottom");
}
format!("{}{}", "type", "bottom")
}
pub fn evalute_property_waterlogged(block: &Block) -> String {
if block.name == "water" {
return format!("{}{}", "waterlogged", "true");
}
format!("{}{}", "waterlogged", "false")
}
}
#[async_trait::async_trait]
impl BlockBehavior for SlabBehavior {
async fn map_state_id(
&self,
world: &World,
block: &Block,
face: &BlockFace,
block_pos: &BlockPos,
use_item_on: &SUseItemOn,
_player_direction: &FacingDirection,
) -> u16 {
let clicked_block = world.get_block(block_pos).await.unwrap();
let mut hmap_key: Vec<String> = Vec::with_capacity(block.properties.len());
let slab_behaviour = Self::get();
for property in &block.properties {
let state = match get_property_key(property.name.as_str()).expect("Property not found")
{
BlockProperty::SlabType(_) => {
Self::evalute_property_type(block, clicked_block, *face, use_item_on)
}
BlockProperty::Waterlogged(false) => Self::evalute_property_waterlogged(block),
_ => panic!("Property not found"),
};
hmap_key.push(state.to_string());
}
// Base state id plus offset
block.states[0].id + slab_behaviour.state_mappings[&hmap_key]
}
async fn is_updateable(
&self,
world: &World,
block: &Block,
_face: &BlockFace,
block_pos: &BlockPos,
) -> bool {
let clicked_block = world.get_block(block_pos).await.unwrap();
if block.id != clicked_block.id {
return false; // Ensure the block being interacted with matches the target block.
}
let clicked_block_state_id = world.get_block_state_id(block_pos).await.unwrap();
let key = clicked_block_state_id - clicked_block.states[0].id;
if let Some(properties) = Self::get().property_mappings.get(&key) {
log::debug!("Properties: {:?}", properties);
if properties.contains(&"typebottom".to_string()) {
return true;
}
}
false
}
}

View File

@@ -0,0 +1,335 @@
use std::{
collections::HashMap,
sync::{Arc, OnceLock},
};
use pumpkin_protocol::server::play::SUseItemOn;
use pumpkin_util::math::{position::BlockPos, vector3::Vector3};
use pumpkin_world::block::block_registry::Block;
use pumpkin_world::{
block::{block_registry::Property, BlockFace},
entity::FacingDirection,
};
use crate::{
block::block_properties_manager::{get_property_key, BlockBehavior, BlockProperty},
world::World,
};
/// Global static for `StairBehavior`
pub static STAIRS_BEHAVIOR: OnceLock<Arc<StairBehavior>> = OnceLock::new();
/// Behavior for Stairs
pub struct StairBehavior {
// Mappings from property state strings -> offset
state_mappings: HashMap<Vec<String>, u16>,
// Mappings from offset -> property state strings
property_mappings: HashMap<u16, Vec<String>>,
}
impl StairBehavior {
/// Initialize or return the existing `StairBehavior`
pub fn get_or_init(properties: &[Property]) -> Arc<Self> {
STAIRS_BEHAVIOR
.get_or_init(|| Arc::new(Self::new(properties)))
.clone()
}
/// Returns the global `StairBehavior` (must have been init once)
pub fn get() -> Arc<Self> {
STAIRS_BEHAVIOR
.get()
.expect("StairsBehavior not initialized")
.clone()
}
/// Build up our forward/reverse property state maps
pub fn new(properties: &[Property]) -> Self {
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<String> = 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 {
state_mappings: forward_map,
property_mappings: reverse_map,
}
}
fn calculate_positions(
player_direction: FacingDirection,
block_pos: BlockPos,
) -> (BlockPos, BlockPos) {
match player_direction {
FacingDirection::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,
)),
),
FacingDirection::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,
)),
),
FacingDirection::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,
)),
),
FacingDirection::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,
)),
),
}
}
pub async fn evaluate_property_shape(
world: &World,
block_pos: &BlockPos,
face: &BlockFace,
use_item_on: &SUseItemOn,
player_direction: &FacingDirection,
) -> String {
let block_half = Self::evaluate_property_half(*face, use_item_on);
let (front_block_pos, back_block_pos) =
Self::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) = Self::get().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 == FacingDirection::West)
|| (is_facing_west && *player_direction == FacingDirection::South)
|| (is_facing_south && *player_direction == FacingDirection::East)
|| (is_facing_east && *player_direction == FacingDirection::North)
{
return "shapeouter_right".to_owned();
}
if (is_facing_north && *player_direction == FacingDirection::East)
|| (is_facing_west && *player_direction == FacingDirection::North)
|| (is_facing_south && *player_direction == FacingDirection::West)
|| (is_facing_east && *player_direction == FacingDirection::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) = Self::get().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 == FacingDirection::West)
|| (is_facing_west && *player_direction == FacingDirection::South)
|| (is_facing_south && *player_direction == FacingDirection::East)
|| (is_facing_east && *player_direction == FacingDirection::North)
{
return "shapeinner_right".to_owned();
}
if (is_facing_north && *player_direction == FacingDirection::East)
|| (is_facing_west && *player_direction == FacingDirection::North)
|| (is_facing_south && *player_direction == FacingDirection::West)
|| (is_facing_east && *player_direction == FacingDirection::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")
}
pub fn evaluate_property_waterlogged(block: &Block) -> String {
if block.name == "water" {
return format!("{}{}", "waterlogged", "true");
}
format!("{}{}", "waterlogged", "false")
}
pub fn evaluate_property_facing(face: BlockFace, player_direction: FacingDirection) -> String {
let facing = match face {
BlockFace::North => "south",
BlockFace::South => "north",
BlockFace::East => "west",
BlockFace::West => "east",
BlockFace::Top | BlockFace::Bottom => match player_direction {
FacingDirection::North => "north",
FacingDirection::South => "south",
FacingDirection::East => "east",
FacingDirection::West => "west",
},
};
format!("facing{facing}")
}
pub fn evaluate_property_half(face: BlockFace, use_item_on: &SUseItemOn) -> String {
match face {
BlockFace::Top => format!("{}{}", "half", "bottom"),
BlockFace::Bottom => format!("{}{}", "half", "top"),
_ => {
if use_item_on.cursor_pos.y > 0.5 {
format!("{}{}", "half", "top")
} else {
format!("{}{}", "half", "bottom")
}
}
}
}
}
#[async_trait::async_trait]
impl BlockBehavior for StairBehavior {
/// Given the block and environment, compute the correct state ID.
async fn map_state_id(
&self,
world: &World,
block: &Block,
face: &BlockFace,
block_pos: &BlockPos,
use_item_on: &SUseItemOn,
player_direction: &FacingDirection,
) -> u16 {
let mut hmap_key: Vec<String> = Vec::with_capacity(block.properties.len());
let stair_behaviour = Self::get();
for property in &block.properties {
let state = match get_property_key(property.name.as_str()).expect("Property not found")
{
BlockProperty::Facing(_) => {
Self::evaluate_property_facing(*face, *player_direction)
}
BlockProperty::Half(_) => Self::evaluate_property_half(*face, use_item_on),
BlockProperty::StairShape(_) => {
Self::evaluate_property_shape(
world,
block_pos,
face,
use_item_on,
player_direction,
)
.await
}
BlockProperty::Waterlogged(_) => Self::evaluate_property_waterlogged(block),
BlockProperty::SlabType(_) => panic!("SlabType BlockProperty invalid for Stairs"),
};
hmap_key.push(state);
}
block.states[0].id + stair_behaviour.state_mappings[&hmap_key]
}
async fn is_updateable(
&self,
_world: &World,
_block: &Block,
_face: &BlockFace,
_block_pos: &BlockPos,
) -> bool {
false
}
}

View File

@@ -41,7 +41,9 @@ use pumpkin_util::{
text::TextComponent,
GameMode,
};
use pumpkin_world::block::block_registry::{get_block_collision_shapes, Block};
use pumpkin_world::block::block_registry::get_block_collision_shapes;
use pumpkin_world::block::block_registry::Block;
use pumpkin_world::entity::FacingDirection;
use pumpkin_world::item::item_registry::get_item_by_id;
use pumpkin_world::item::ItemStack;
use pumpkin_world::{
@@ -49,6 +51,7 @@ use pumpkin_world::{
entity::entity_registry::get_entity_id,
item::item_registry::get_spawn_egg,
};
use pumpkin_world::{WORLD_LOWEST_Y, WORLD_MAX_Y};
use thiserror::Error;
@@ -1142,6 +1145,18 @@ impl Player {
Ok(true)
}
fn get_player_direction(&self) -> FacingDirection {
let adjusted_yaw = (self.living_entity.entity.yaw.load() % 360.0 + 360.0) % 360.0; // Normalize yaw to [0, 360)
match adjusted_yaw {
0.0..=45.0 | 315.0..=360.0 => FacingDirection::South,
45.0..=135.0 => FacingDirection::West,
135.0..=225.0 => FacingDirection::North,
225.0..=315.0 => FacingDirection::East,
_ => FacingDirection::South, // Default case, should not occur
}
}
async fn run_is_block_place(
&self,
block: Block,
@@ -1153,6 +1168,9 @@ impl Player {
let entity = &self.living_entity.entity;
let world = &entity.world;
let clicked_block_pos = BlockPos(location.0);
let clicked_block_state = world.get_block_state(&clicked_block_pos).await?;
// check block under the world
if location.0.y + face.to_offset().y < WORLD_LOWEST_Y.into() {
self.client
@@ -1188,20 +1206,22 @@ impl Player {
_ => {}
}
let clicked_world_pos = BlockPos(location.0);
let clicked_block_state = world.get_block_state(&clicked_world_pos).await?;
let clicked_block_updated_able = server
.block_properties_manager
.is_updateable(world, &block, face, &clicked_block_pos)
.await;
let world_pos = if clicked_block_state.replaceable {
clicked_world_pos
let final_block_pos = if clicked_block_state.replaceable || clicked_block_updated_able {
clicked_block_pos
} else {
let world_pos = BlockPos(location.0 + face.to_offset());
let previous_block_state = world.get_block_state(&world_pos).await?;
let block_pos = BlockPos(location.0 + face.to_offset());
let previous_block_state = world.get_block_state(&block_pos).await?;
if !previous_block_state.replaceable {
return Ok(true);
}
world_pos
block_pos
};
// To this point we must have the new block state
@@ -1210,17 +1230,28 @@ impl Player {
let mut intersects = false;
for player in world.get_nearby_players(entity.pos.load(), 20.0).await {
let bounding_box = player.1.living_entity.entity.bounding_box.load();
if bounding_box.intersects_block(&world_pos, &block_bounding_box) {
if bounding_box.intersects_block(&final_block_pos, &block_bounding_box) {
intersects = true;
}
}
if !intersects {
world
.set_block_state(&world_pos, block.default_state_id)
let mapped_block_id = server
.block_properties_manager
.get_state_id(
world,
&block,
face,
&final_block_pos,
&use_item_on,
&self.get_player_direction(),
)
.await;
let _replaced_id = world
.set_block_state(&final_block_pos, mapped_block_id)
.await;
server
.block_manager
.on_placed(&block, self, world_pos, server)
.on_placed(&block, self, final_block_pos, server)
.await;
}
self.client

View File

@@ -32,7 +32,8 @@ use tokio::sync::{Mutex, RwLock};
use uuid::Uuid;
use crate::block::block_manager::BlockManager;
use crate::block::default_block_manager;
use crate::block::block_properties_manager::BlockPropertiesManager;
use crate::block::{default_block_manager, default_block_properties_manager};
use crate::entity::ai::path::Navigator;
use crate::entity::living::LivingEntity;
use crate::entity::mob::MobEntity;
@@ -64,6 +65,8 @@ pub struct Server {
pub command_dispatcher: RwLock<CommandDispatcher>,
/// Saves and calls blocks blocks
pub block_manager: Arc<BlockManager>,
/// Creates and stores block property registry and managed behaviours.
pub block_properties_manager: Arc<BlockPropertiesManager>,
/// Manages multiple worlds within the server.
pub worlds: RwLock<Vec<Arc<World>>>,
// All the dimensions that exists on the server,
@@ -134,6 +137,7 @@ impl Server {
],
command_dispatcher,
block_manager: default_block_manager(),
block_properties_manager: default_block_properties_manager(),
auth_client,
key_store: KeyStore::new(),
server_listing: Mutex::new(CachedStatus::new()),