Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion steel-core/build/blocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ pub fn build(blocks: &[BlockClass]) -> String {
let mut wall_torch_blocks = Vec::new();
let mut redstone_torch_blocks = Vec::new();
let mut redstone_wall_torch_blocks = Vec::new();
let mut stair_blocks = Vec::new();
let mut slab_blocks = Vec::new();

for block in blocks {
let const_ident = to_const_ident(&block.name);
Expand Down Expand Up @@ -104,6 +106,8 @@ pub fn build(blocks: &[BlockClass]) -> String {
"WallTorchBlock" => wall_torch_blocks.push(const_ident),
"RedstoneTorchBlock" => redstone_torch_blocks.push(const_ident),
"RedstoneWallTorchBlock" => redstone_wall_torch_blocks.push(const_ident),
"StairBlock" => stair_blocks.push(const_ident),
"SlabBlock" => slab_blocks.push(const_ident),
_ => {}
}
}
Expand All @@ -124,6 +128,8 @@ pub fn build(blocks: &[BlockClass]) -> String {
let wall_torch_type = Ident::new("WallTorchBlock", Span::call_site());
let redstone_torch_type = Ident::new("RedstoneTorchBlock", Span::call_site());
let redstone_wall_torch_type = Ident::new("RedstoneWallTorchBlock", Span::call_site());
let stair_type = Ident::new("StairBlock", Span::call_site());
let slab_type = Ident::new("SlabBlock", Span::call_site());

let barrel_registrations = generate_registrations(barrel_blocks.iter(), &barrel_type);
let button_registrations = {
Expand Down Expand Up @@ -181,6 +187,8 @@ pub fn build(blocks: &[BlockClass]) -> String {
generate_registrations(redstone_torch_blocks.iter(), &redstone_torch_type);
let redstone_wall_torch_registrations =
generate_registrations(redstone_wall_torch_blocks.iter(), &redstone_wall_torch_type);
let stair_registrations = generate_registrations(stair_blocks.iter(), &stair_type);
let slab_registrations = generate_registrations(slab_blocks.iter(), &slab_type);

let output = quote! {
//! Generated block behavior assignments.
Expand All @@ -191,7 +199,7 @@ pub fn build(blocks: &[BlockClass]) -> String {
BarrelBlock, ButtonBlock, CandleBlock, CraftingTableBlock, CropBlock, EndPortalFrameBlock,
FarmlandBlock, FenceBlock, LiquidBlock, RotatedPillarBlock, StandingSignBlock, WallSignBlock,
CeilingHangingSignBlock, WallHangingSignBlock, TorchBlock, WallTorchBlock,
RedstoneTorchBlock, RedstoneWallTorchBlock,
RedstoneTorchBlock, RedstoneWallTorchBlock, StairBlock, SlabBlock,
};

pub fn register_block_behaviors(registry: &mut BlockBehaviorRegistry) {
Expand All @@ -213,6 +221,8 @@ pub fn build(blocks: &[BlockClass]) -> String {
#wall_torch_registrations
#redstone_torch_registrations
#redstone_wall_torch_registrations
#stair_registrations
#slab_registrations
}
};

Expand Down
30 changes: 29 additions & 1 deletion steel-core/src/behavior/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ use steel_registry::item_stack::ItemStack;
use steel_utils::types::InteractionHand;
use steel_utils::{BlockPos, BlockStateId};

use crate::behavior::context::{BlockHitResult, BlockPlaceContext, InteractionResult};
use crate::behavior::context::{
BlockHitResult, BlockPlaceContext, InteractionResult, UseOnContext,
};
use crate::block_entity::SharedBlockEntity;
use crate::player::Player;
use crate::world::World;
Expand Down Expand Up @@ -40,6 +42,32 @@ pub trait BlockBehaviour: Send + Sync {
/// Returns the block state to use when placing this block.
fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId>;

/// Returns whether this block state can be replaced by another block placement.
///
/// This is used for blocks like slabs that can be merged into double slabs,
/// or other blocks with special replacement logic matches the vanilla logik in java the canBeReplaced thingy
///
/// The default implementation returns `false`, meaning the block cannot be replaced
/// unless it has the `replaceable` config flag set.
///
/// # args
/// * `state` - The current block state
/// * `context` - The use context containing placement information
/// * `placing_block` - The block that would replace this one
/// * `placement_pos` - The position where the block would be placed
/// * `replace_clicked` - Whether we're replacing the clicked block or an adjacent block
#[allow(unused_variables)]
fn can_be_replaced(
&self,
state: BlockStateId,
context: &UseOnContext<'_>,
placing_block: BlockRef,
placement_pos: BlockPos,
replace_clicked: bool,
) -> bool {
false
}

/// Called when this block is placed in the world.
///
/// # Arguments
Expand Down
4 changes: 4 additions & 0 deletions steel-core/src/behavior/blocks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ mod liquid_block;
mod redstone_torch_block;
mod rotated_pillar_block;
mod sign_block;
mod slab_block;
mod stair_block;
mod torch_block;

pub use barrel_block::BarrelBlock;
Expand All @@ -31,4 +33,6 @@ pub use rotated_pillar_block::RotatedPillarBlock;
pub use sign_block::{
CeilingHangingSignBlock, StandingSignBlock, WallHangingSignBlock, WallSignBlock,
};
pub use slab_block::SlabBlock;
pub use stair_block::StairBlock;
pub use torch_block::{TorchBlock, WallTorchBlock};
167 changes: 167 additions & 0 deletions steel-core/src/behavior/blocks/slab_block.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
//! Slab block behavior implementation.
//!
//! Slabs are half-height blocks that can be placed on the top or bottom
//! half of a block space, or combined into a double slab.

use std::ptr;

use steel_registry::blocks::BlockRef;
use steel_registry::blocks::block_state_ext::BlockStateExt;
use steel_registry::blocks::properties::{BlockStateProperties, Direction, SlabType};
use steel_registry::fluid::FluidState;
use steel_registry::items::ItemRef;
use steel_registry::{REGISTRY, vanilla_blocks, vanilla_fluids};
use steel_utils::BlockStateId;

use crate::behavior::block::BlockBehaviour;
use crate::behavior::context::{BlockPlaceContext, UseOnContext};
use crate::world::World;

/// Behavior for slab blocks.
///
/// Slabs have 2 properties:
/// - `type`: The type of slab (`top`, `bottom`, or `double`)
/// - `waterlogged`: Whether the slab is waterlogged (only for non-double slabs)
///
/// When placing a slab on an existing slab of the same type, they merge into
/// a double slab.
pub struct SlabBlock {
block: BlockRef,
}

impl SlabBlock {
/// Create a new slab block
#[must_use]
pub const fn new(block: BlockRef) -> Self {
Self { block }
}

/// Checks if it is the same slab block.
fn is_same_slab(&self, state: BlockStateId) -> bool {
ptr::eq(state.get_block(), self.block)
}

/// Gets the item that corresponds to this slab block.
fn get_slab_item(&self) -> Option<ItemRef> {
REGISTRY.items.by_key(&self.block.key)
}
}

impl BlockBehaviour for SlabBlock {
fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
let pos = &context.relative_pos;
let existing_state = context.world.get_block_state(pos);

if self.is_same_slab(existing_state) {
let existing_type: SlabType =
existing_state.get_value(&BlockStateProperties::SLAB_TYPE);
if existing_type != SlabType::Double {
return Some(
existing_state
.set_value(&BlockStateProperties::SLAB_TYPE, SlabType::Double)
.set_value(&BlockStateProperties::WATERLOGGED, false),
);
}
}

let waterlogged = ptr::eq(existing_state.get_block(), vanilla_blocks::WATER);

// Determine slab type based on click position
let clicked_face = context.clicked_face;
let click_y = context.click_location.y;
let pos_y = f64::from(pos.y());

let slab_type = if clicked_face != Direction::Down
&& (clicked_face == Direction::Up || (click_y - pos_y) <= 0.5)
{
SlabType::Bottom
} else {
SlabType::Top
};

Some(
self.block
.default_state()
.set_value(&BlockStateProperties::SLAB_TYPE, slab_type)
.set_value(&BlockStateProperties::WATERLOGGED, waterlogged),
)
}

fn get_fluid_state(&self, state: BlockStateId) -> FluidState {
let slab_type: SlabType = state.get_value(&BlockStateProperties::SLAB_TYPE);
if slab_type != SlabType::Double && state.get_value(&BlockStateProperties::WATERLOGGED) {
FluidState::source(&vanilla_fluids::WATER)
} else {
FluidState::EMPTY
}
}

fn update_shape(
&self,
state: BlockStateId,
_world: &World,
_pos: steel_utils::BlockPos,
_direction: Direction,
_neighbor_pos: steel_utils::BlockPos,
_neighbor_state: BlockStateId,
) -> BlockStateId {
// Slabs don't change shape based on neighbors
// TODO: Schedule water tick if waterlogged (when tick system is implemented)
state
}

fn can_be_replaced(
&self,
state: BlockStateId,
context: &UseOnContext<'_>,
placing_block: BlockRef,
_placement_pos: steel_utils::BlockPos,
replace_clicked: bool,
) -> bool {
let slab_type: SlabType = state.get_value(&BlockStateProperties::SLAB_TYPE);

if slab_type == SlabType::Double {
return false;
}

let Some(slab_item) = self.get_slab_item() else {
return false;
};

if !ptr::eq(context.item_stack.item, slab_item) || !ptr::eq(placing_block, self.block) {
return false;
}

let hit = &context.hit_result;
let clicked_face = hit.direction;
let click_y = hit.location.y;
let pos_y = f64::from(hit.block_pos.y());
let clicked_upper_half = (click_y - pos_y) > 0.5;

if replace_clicked {
// directly clicking on the slab to merg it
match slab_type {
SlabType::Bottom => {
clicked_face == Direction::Up
|| (clicked_upper_half && clicked_face.is_horizontal())
}
SlabType::Top => {
clicked_face == Direction::Down
|| (!clicked_upper_half && clicked_face.is_horizontal())
}
SlabType::Double => false,
}
} else {
// placing adjacent to a block into an existing slab position
match clicked_face {
Direction::Up => slab_type == SlabType::Top,
Direction::Down => slab_type == SlabType::Bottom,
_ => match slab_type {
SlabType::Bottom => clicked_upper_half,
SlabType::Top => !clicked_upper_half,
SlabType::Double => false,
},
}
}
}
}
Loading