Fixed slabs properties (#515)

* Fixed slabs properties

Changed a little bit the properties system to make the slabs work properly and the collision calculation when placing them.

* Added Layers

Used for Snow

* Prevent some crashes

Fixes https://github.com/Pumpkin-MC/Pumpkin/issues/514

* fix clippy

* Improve bounding box

* Added margin on intersection

Added a margin on intersections calculation to prevent the player to pass through blocks.

* Rolling back intersection margin

---------

Co-authored-by: Alexander Medvedev <lilalexmed@proton.me>
This commit is contained in:
MrMelther
2025-01-31 10:32:58 +01:00
committed by GitHub
parent 0e8f8ecf6e
commit 7ef2770ee0
6 changed files with 216 additions and 113 deletions

View File

@@ -2,12 +2,8 @@ use super::{position::BlockPos, vector3::Vector3};
#[derive(Clone, Copy)]
pub struct BoundingBox {
pub min_x: f64,
pub min_y: f64,
pub min_z: f64,
pub max_x: f64,
pub max_y: f64,
pub max_z: f64,
pub min: Vector3<f64>,
pub max: Vector3<f64>,
}
impl BoundingBox {
@@ -18,68 +14,62 @@ impl BoundingBox {
pub fn new_from_pos(x: f64, y: f64, z: f64, size: &BoundingBoxSize) -> Self {
let f = size.width / 2.;
Self {
min_x: x - f,
min_y: y,
min_z: z - f,
max_x: x + f,
max_y: y + size.height,
max_z: z + f,
min: Vector3::new(x - f, y, z - f),
max: Vector3::new(x + f, y + size.height, z + f),
}
}
pub fn offset(&self, other: Self) -> Self {
Self {
min: self.min.add(&other.min),
max: self.max.add(&other.max),
}
}
pub fn new(min: Vector3<f64>, max: Vector3<f64>) -> Self {
Self { min, max }
}
pub fn new_array(min: [f64; 3], max: [f64; 3]) -> Self {
Self {
min_x: min.x,
min_y: min.y,
min_z: min.z,
max_x: max.x,
max_y: max.y,
max_z: max.z,
min: Vector3::new(min[0], min[1], min[2]),
max: Vector3::new(max[0], max[1], max[2]),
}
}
pub fn from_block(position: &BlockPos) -> Self {
let position = position.0;
Self {
min_x: position.x as f64,
min_y: position.y as f64,
min_z: position.z as f64,
max_x: (position.x as f64) + 1.0,
max_y: (position.y as f64) + 1.0,
max_z: (position.z as f64) + 1.0,
min: Vector3::new(position.x as f64, position.y as f64, position.z as f64),
max: Vector3::new(
position.x as f64 + 1.0,
position.y as f64 + 1.0,
position.z as f64 + 1.0,
),
}
}
pub fn from_block_raw(position: &BlockPos) -> Self {
let position = position.0;
Self {
min: Vector3::new(position.x as f64, position.y as f64, position.z as f64),
max: Vector3::new(position.x as f64, position.y as f64, position.z as f64),
}
}
pub fn intersects(&self, other: &BoundingBox) -> bool {
self.min_x < other.max_x
&& self.max_x > other.min_x
&& self.min_y < other.max_y
&& self.max_y > other.min_y
&& self.min_z < other.max_z
&& self.max_z > other.min_z
}
pub fn intersects_block(&self, position: &BlockPos, bounding_box: &[f32]) -> bool {
for i in 0..bounding_box.len() / 6 {
let other = BoundingBox {
min_x: position.0.x as f64 + bounding_box[i * 6] as f64,
min_y: position.0.y as f64 + bounding_box[i * 6 + 1] as f64,
min_z: position.0.z as f64 + bounding_box[i * 6 + 2] as f64,
max_x: position.0.x as f64 + bounding_box[i * 6 + 3] as f64,
max_y: position.0.y as f64 + bounding_box[i * 6 + 4] as f64,
max_z: position.0.z as f64 + bounding_box[i * 6 + 5] as f64,
};
if self.intersects(&other) {
return true;
}
}
false
self.min.x < other.max.x
&& self.max.x > other.min.x
&& self.min.y < other.max.y
&& self.max.y > other.min.y
&& self.min.z < other.max.z
&& self.max.z > other.min.z
}
pub fn squared_magnitude(&self, pos: Vector3<f64>) -> f64 {
let d = f64::max(f64::max(self.min_x - pos.x, pos.x - self.max_x), 0.0);
let e = f64::max(f64::max(self.min_y - pos.y, pos.y - self.max_y), 0.0);
let f = f64::max(f64::max(self.min_z - pos.z, pos.z - self.max_z), 0.0);
let d = f64::max(f64::max(self.min.x - pos.x, pos.x - self.max.x), 0.0);
let e = f64::max(f64::max(self.min.y - pos.y, pos.y - self.max.y), 0.0);
let f = f64::max(f64::max(self.min.z - pos.z, pos.z - self.max.z), 0.0);
super::squared_magnitude(d, e, f)
}
}

View File

@@ -128,6 +128,19 @@ impl<T> From<Vector3<T>> for (T, T, T) {
}
}
impl<T: Math + Copy> Vector3<T>
where
T: Into<f64>,
{
pub fn to_f64(&self) -> Vector3<f64> {
Vector3 {
x: self.x.into(),
y: self.y.into(),
z: self.z.into(),
}
}
}
pub trait Math:
Mul<Output = Self>
//+ Neg<Output = Self>

View File

@@ -91,18 +91,13 @@ pub fn get_block_by_item<'a>(item_id: u16) -> Option<&'a Block> {
BLOCKS_BY_ID.get(block_id)
}
pub fn get_block_collision_shapes(block_id: u16) -> Option<Vec<f32>> {
pub fn get_block_collision_shapes(block_id: u16) -> Option<Vec<Shape>> {
let block = BLOCKS_BY_ID.get(&BLOCK_ID_BY_STATE_ID[&block_id])?;
let state = &block.states[STATE_INDEX_BY_STATE_ID[&block_id] as usize];
let mut shapes: Vec<f32> = vec![];
let mut shapes: Vec<Shape> = vec![];
for i in 0..state.collision_shapes.len() {
let shape = &BLOCKS.shapes[state.collision_shapes[i] as usize];
shapes.push(shape.min[0]);
shapes.push(shape.min[1]);
shapes.push(shape.min[2]);
shapes.push(shape.max[0]);
shapes.push(shape.max[1]);
shapes.push(shape.max[2]);
shapes.push(shape.clone());
}
Some(shapes)
}
@@ -143,7 +138,7 @@ pub struct State {
pub block_entity_type: Option<u32>,
}
#[derive(Deserialize, Clone, Debug)]
struct Shape {
min: [f32; 3],
max: [f32; 3],
pub struct Shape {
pub min: [f64; 3],
pub max: [f64; 3],
}

View File

@@ -46,4 +46,14 @@ impl BlockDirection {
}
.into()
}
pub fn opposite(&self) -> BlockDirection {
match self {
BlockDirection::Bottom => BlockDirection::Top,
BlockDirection::Top => BlockDirection::Bottom,
BlockDirection::North => BlockDirection::South,
BlockDirection::South => BlockDirection::North,
BlockDirection::West => BlockDirection::East,
BlockDirection::East => BlockDirection::West,
}
}
}

View File

@@ -3,7 +3,7 @@ use std::collections::HashMap;
use pumpkin_protocol::server::play::SUseItemOn;
use pumpkin_util::math::{position::BlockPos, vector3::Vector3};
use pumpkin_world::block::{
block_registry::{Block, BLOCKS},
block_registry::{Block, State, BLOCKS},
BlockDirection,
};
@@ -17,6 +17,7 @@ pub enum BlockProperty {
Powered(bool),
SlabType(SlabPosition),
StairShape(StairShape),
Layers(u8),
Half(BlockHalf), // Add other properties as needed
}
@@ -33,7 +34,7 @@ pub enum BlockHalf {
Bottom,
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub enum SlabPosition {
Top,
Bottom,
@@ -67,40 +68,84 @@ pub fn get_property_key(property_name: &str) -> Option<BlockProperty> {
"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 {
dbg!(property);
// 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,
) -> String {
if block.id == clicked_block.id && face == BlockDirection::Top {
return format!("{}{}", "type", "double");
}
if face == BlockDirection::Top {
return format!("{}{}", "type", "bottom");
}
if face == BlockDirection::North
|| face == BlockDirection::South
|| face == BlockDirection::West
|| face == BlockDirection::East
{
let y_pos = use_item_on.cursor_pos.y;
if y_pos > 0.5 {
return format!("{}{}", "type", "top");
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);
}
return format!("{}{}", "type", "bottom");
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);
}
}
}
format!("{}{}", "type", "bottom")
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]
@@ -310,8 +355,8 @@ pub fn evaluate_property_block_face(dir: BlockDirection) -> String {
#[must_use]
pub fn evaluate_property_half(face: BlockDirection, use_item_on: &SUseItemOn) -> String {
match face {
BlockDirection::Top => format!("{}{}", "half", "bottom"),
BlockDirection::Bottom => format!("{}{}", "half", "top"),
BlockDirection::Bottom => format!("{}{}", "half", "bottom"),
BlockDirection::Top => format!("{}{}", "half", "top"),
_ => {
if use_item_on.cursor_pos.y > 0.5 {
format!("{}{}", "half", "top")
@@ -383,7 +428,8 @@ impl BlockPropertiesManager {
}
}
pub async fn get_state_id(
#[allow(clippy::too_many_arguments)]
pub async fn get_state_data(
&self,
world: &World,
block: &Block,
@@ -391,9 +437,11 @@ impl BlockPropertiesManager {
block_pos: &BlockPos,
use_item_on: &SUseItemOn,
player_direction: &Direction,
) -> u16 {
other: bool,
) -> (u16, bool) {
if let Some(properties) = self.properties_registry.get(&block.id) {
let mut hmap_key: Vec<String> = 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());
@@ -401,7 +449,19 @@ impl BlockPropertiesManager {
let state = match property {
BlockProperty::SlabType(_) => {
let clicked_block = world.get_block(block_pos).await.unwrap();
evaluate_property_type(block, clicked_block, *face, use_item_on)
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(_) => {
@@ -421,17 +481,37 @@ impl BlockPropertiesManager {
}
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;
return (block.default_state_id, false);
}
}
// Base state id plus offset
return block.states[0].id + properties.state_mappings[&hmap_key];
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
(block.default_state_id, false)
}
}

View File

@@ -35,6 +35,7 @@ use pumpkin_protocol::{
SSetPlayerGround, SSwingArm, SUseItem, SUseItemOn, Status,
},
};
use pumpkin_util::math::boundingbox::BoundingBox;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::text::color::NamedColor;
use pumpkin_util::{
@@ -1206,15 +1207,38 @@ impl Player {
_ => {}
}
let clicked_block_updated_able = false;
let (mut new_state, mut updateable) = server
.block_properties_manager
.get_state_data(
world,
&block,
face,
&clicked_block_pos,
&use_item_on,
&self.get_player_direction(),
true,
)
.await;
let final_block_pos = if clicked_block_state.replaceable || clicked_block_updated_able {
let final_block_pos = if clicked_block_state.replaceable || updateable {
clicked_block_pos
} else {
let block_pos = BlockPos(location.0 + face.to_offset());
let previous_block_state = world.get_block_state(&block_pos).await?;
(new_state, updateable) = server
.block_properties_manager
.get_state_data(
world,
&block,
&face.opposite(),
&block_pos,
&use_item_on,
&self.get_player_direction(),
false,
)
.await;
if !previous_block_state.replaceable {
if !previous_block_state.replaceable && !updateable {
return Ok(true);
}
@@ -1222,30 +1246,21 @@ impl Player {
};
// To this point we must have the new block state
let block_bounding_box =
get_block_collision_shapes(block.default_state_id).unwrap_or_default();
let shapes = get_block_collision_shapes(new_state).unwrap_or_default();
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(&final_block_pos, &block_bounding_box) {
intersects = true;
for player in world.get_nearby_players(location.0.to_f64(), 3.0).await {
let player_box = player.1.living_entity.entity.bounding_box.load();
for shape in &shapes {
let block_box = BoundingBox::from_block_raw(&final_block_pos)
.offset(BoundingBox::new_array(shape.min, shape.max));
if player_box.intersects(&block_box) {
intersects = true;
break;
}
}
}
if !intersects {
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;
let _replaced_id = world.set_block_state(&final_block_pos, new_state).await;
server
.block_manager
.on_placed(&block, self, final_block_pos, server)