From 9194f93d86574ebe57434320d6e8934029ecf5cf Mon Sep 17 00:00:00 2001 From: Joniii11 <88550325+Joniii11@users.noreply.github.com> Date: Fri, 6 Feb 2026 16:46:05 +0100 Subject: [PATCH 1/3] Slabs and stairs --- steel-core/build/blocks.rs | 12 +- steel-core/src/behavior/block.rs | 30 ++- steel-core/src/behavior/blocks/mod.rs | 4 + steel-core/src/behavior/blocks/slab_block.rs | 167 ++++++++++++++++ steel-core/src/behavior/blocks/stair_block.rs | 182 ++++++++++++++++++ steel-core/src/behavior/items/block_item.rs | 36 +++- 6 files changed, 427 insertions(+), 4 deletions(-) create mode 100644 steel-core/src/behavior/blocks/slab_block.rs create mode 100644 steel-core/src/behavior/blocks/stair_block.rs diff --git a/steel-core/build/blocks.rs b/steel-core/build/blocks.rs index f74beaa52c9..8a11aa22b6d 100644 --- a/steel-core/build/blocks.rs +++ b/steel-core/build/blocks.rs @@ -52,6 +52,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); @@ -77,6 +79,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), _ => {} } } @@ -97,6 +101,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 candle_registrations = generate_registrations(candle_blocks.iter(), &candle_type); @@ -135,6 +141,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. @@ -145,7 +153,7 @@ pub fn build(blocks: &[BlockClass]) -> String { BarrelBlock, 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) { @@ -166,6 +174,8 @@ pub fn build(blocks: &[BlockClass]) -> String { #wall_torch_registrations #redstone_torch_registrations #redstone_wall_torch_registrations + #stair_registrations + #slab_registrations } }; diff --git a/steel-core/src/behavior/block.rs b/steel-core/src/behavior/block.rs index ffc7a22d829..dedff1a4369 100644 --- a/steel-core/src/behavior/block.rs +++ b/steel-core/src/behavior/block.rs @@ -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; @@ -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; + /// 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 vanillla 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 diff --git a/steel-core/src/behavior/blocks/mod.rs b/steel-core/src/behavior/blocks/mod.rs index 0b4c8a19859..80278ec835d 100644 --- a/steel-core/src/behavior/blocks/mod.rs +++ b/steel-core/src/behavior/blocks/mod.rs @@ -14,6 +14,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; @@ -29,4 +31,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}; diff --git a/steel-core/src/behavior/blocks/slab_block.rs b/steel-core/src/behavior/blocks/slab_block.rs new file mode 100644 index 00000000000..2f0fd32df82 --- /dev/null +++ b/steel-core/src/behavior/blocks/slab_block.rs @@ -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 { + REGISTRY.items.by_key(&self.block.key) + } +} + +impl BlockBehaviour for SlabBlock { + fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option { + 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 exisitng 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, + }, + } + } + } +} diff --git a/steel-core/src/behavior/blocks/stair_block.rs b/steel-core/src/behavior/blocks/stair_block.rs new file mode 100644 index 00000000000..81fcf78400d --- /dev/null +++ b/steel-core/src/behavior/blocks/stair_block.rs @@ -0,0 +1,182 @@ +//! Stair block behavior implementation. +//! +//! Stairs connect to adjacent stairs to form corners (inner and outer). + +use std::ptr; + +use steel_registry::blocks::BlockRef; +use steel_registry::blocks::block_state_ext::BlockStateExt; +use steel_registry::blocks::properties::{BlockStateProperties, Direction, Half, StairsShape}; +use steel_registry::fluid::FluidState; +use steel_registry::{vanilla_blocks, vanilla_fluids}; +use steel_utils::{BlockPos, BlockStateId}; + +use crate::behavior::block::BlockBehaviour; +use crate::behavior::context::BlockPlaceContext; +use crate::world::World; + +/// Behavior for stair blocks. +/// +/// Stairs have 4 properties: +/// - `facing`: The horizontal direction the stairs face (north, south, east, west) +/// - `half`: Whether the stairs are on the top or bottom half of the block +/// - `shape`: The shape of the stairs (`straight`, `inner_left`, `inner_right`, `outer_left`, `outer_right`) +/// - `waterlogged`: Whether the stairs are waterlogged +/// +/// The shape is determined by adjacent stair blocks to form corners. +pub struct StairBlock { + block: BlockRef, +} + +impl StairBlock { + /// Creates a new stair block behavior for the given block. + #[must_use] + pub const fn new(block: BlockRef) -> Self { + Self { block } + } + + /// Checks if a block state is a stair block. + /// + /// A block is considered a stair if it has the `STAIRS_SHAPE` property. + fn is_stairs(state: BlockStateId) -> bool { + state + .try_get_value(&BlockStateProperties::STAIRS_SHAPE) + .is_some() + } + + /// Determines the shape of the stairs based on adjacent blocks. + fn get_stairs_shape(state: BlockStateId, world: &World, pos: &BlockPos) -> StairsShape { + let facing: Direction = state.get_value(&BlockStateProperties::HORIZONTAL_FACING); + let half: Half = state.get_value(&BlockStateProperties::HALF); + + // Check the block behind (in the facing direction) and do some other stuff + let behind_pos = facing.relative(pos); + let behind_state = world.get_block_state(&behind_pos); + + if Self::is_stairs(behind_state) { + let behind_half: Half = behind_state.get_value(&BlockStateProperties::HALF); + if half == behind_half { + let behind_facing: Direction = + behind_state.get_value(&BlockStateProperties::HORIZONTAL_FACING); + + if behind_facing.axis() != facing.axis() + && Self::can_take_shape(state, world, pos, behind_facing.opposite()) + { + if behind_facing == facing.rotate_y_counter_clockwise() { + return StairsShape::OuterLeft; + } + return StairsShape::OuterRight; + } + } + } + + let front_pos = facing.opposite().relative(pos); + let front_state = world.get_block_state(&front_pos); + + if Self::is_stairs(front_state) { + let front_half: Half = front_state.get_value(&BlockStateProperties::HALF); + if half == front_half { + let front_facing: Direction = + front_state.get_value(&BlockStateProperties::HORIZONTAL_FACING); + + if front_facing.axis() != facing.axis() + && Self::can_take_shape(state, world, pos, front_facing) + { + if front_facing == facing.rotate_y_counter_clockwise() { + return StairsShape::InnerLeft; + } + return StairsShape::InnerRight; + } + } + } + + StairsShape::Straight + } + + /// Checks if this stair can take a corner shape by verifying the neighbor + /// in the given direction doesn't block it. + fn can_take_shape( + state: BlockStateId, + world: &World, + pos: &BlockPos, + neighbor_direction: Direction, + ) -> bool { + let neighbor_pos = neighbor_direction.relative(pos); + let neighbor_state = world.get_block_state(&neighbor_pos); + + if !Self::is_stairs(neighbor_state) { + return true; + } + + let our_facing: Direction = state.get_value(&BlockStateProperties::HORIZONTAL_FACING); + let our_half: Half = state.get_value(&BlockStateProperties::HALF); + let neighbor_facing: Direction = + neighbor_state.get_value(&BlockStateProperties::HORIZONTAL_FACING); + let neighbor_half: Half = neighbor_state.get_value(&BlockStateProperties::HALF); + + // Can take shape if the neighbor stair has a different facing or half + neighbor_facing != our_facing || neighbor_half != our_half + } +} + +impl BlockBehaviour for StairBlock { + fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option { + let clicked_face = context.clicked_face; + let click_y = context.click_location.y; + let pos_y = f64::from(context.relative_pos.y()); + + // Determine if stairs should be on top or bottom half + let half = if clicked_face != Direction::Down + && (clicked_face == Direction::Up || (click_y - pos_y) <= 0.5) + { + Half::Bottom + } else { + Half::Top + }; + + // Check if position is waterlogged (replacing water block) + let existing_state = context.world.get_block_state(&context.relative_pos); + let waterlogged = ptr::eq(existing_state.get_block(), vanilla_blocks::WATER); + + // Build the initial state + let state = self + .block + .default_state() + .set_value( + &BlockStateProperties::HORIZONTAL_FACING, + context.horizontal_direction, + ) + .set_value(&BlockStateProperties::HALF, half) + .set_value(&BlockStateProperties::WATERLOGGED, waterlogged); + + // Calculate the shape based on neighbors + let shape = Self::get_stairs_shape(state, context.world, &context.relative_pos); + Some(state.set_value(&BlockStateProperties::STAIRS_SHAPE, shape)) + } + + fn update_shape( + &self, + state: BlockStateId, + world: &World, + pos: BlockPos, + direction: Direction, + _neighbor_pos: BlockPos, + _neighbor_state: BlockStateId, + ) -> BlockStateId { + // Only update shape for horizontal neighbor changes + if direction.is_horizontal() { + let shape = Self::get_stairs_shape(state, world, &pos); + state.set_value(&BlockStateProperties::STAIRS_SHAPE, shape) + } else { + state + } + } + + fn get_fluid_state(&self, state: BlockStateId) -> FluidState { + if state.get_value(&BlockStateProperties::WATERLOGGED) { + FluidState::source(&vanilla_fluids::WATER) + } else { + FluidState::EMPTY + } + } +} diff --git a/steel-core/src/behavior/items/block_item.rs b/steel-core/src/behavior/items/block_item.rs index d0b97c19cc2..8d315c2f7dc 100644 --- a/steel-core/src/behavior/items/block_item.rs +++ b/steel-core/src/behavior/items/block_item.rs @@ -32,9 +32,22 @@ impl ItemBehavior for BlockItemBehavior { let clicked_block = REGISTRY.blocks.by_state_id(clicked_state); let clicked_replaceable = clicked_block.is_some_and(|b| b.config.replaceable); + // Check if the clicked block can be dynamically replaced (e.g., slabs merging) + let clicked_can_be_replaced = if !clicked_replaceable { + let block_behaviors = &*BLOCK_BEHAVIORS; + if let Some(clicked) = clicked_block { + let behavior = block_behaviors.get_behavior(clicked); + behavior.can_be_replaced(clicked_state, context, self.block, clicked_pos, true) + } else { + false + } + } else { + true + }; + // Determine placement position: replace clicked block if replaceable, // otherwise place adjacent to the clicked face - let (place_pos, replace_clicked) = if clicked_replaceable { + let (place_pos, replace_clicked) = if clicked_can_be_replaced { (clicked_pos, true) } else { (context.hit_result.direction.relative(&clicked_pos), false) @@ -50,7 +63,26 @@ impl ItemBehavior for BlockItemBehavior { let existing_block = REGISTRY.blocks.by_state_id(existing_state); let existing_replaceable = existing_block.is_some_and(|b| b.config.replaceable); - if !existing_replaceable { + // Also check dynamic replacement for the existing block at the placement position + let existing_can_be_replaced = if !existing_replaceable { + let block_behaviors = &*BLOCK_BEHAVIORS; + if let Some(existing) = existing_block { + let behavior = block_behaviors.get_behavior(existing); + behavior.can_be_replaced( + existing_state, + context, + self.block, + place_pos, + replace_clicked, + ) + } else { + false + } + } else { + true + }; + + if !existing_can_be_replaced { return InteractionResult::Fail; } From c28551f54c78e964e2f15c0bbcfcae82105401ef Mon Sep 17 00:00:00 2001 From: Joniii11 <88550325+Joniii11@users.noreply.github.com> Date: Fri, 6 Feb 2026 16:47:26 +0100 Subject: [PATCH 2/3] fmt and clippy --- steel-core/src/behavior/items/block_item.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/steel-core/src/behavior/items/block_item.rs b/steel-core/src/behavior/items/block_item.rs index 8d315c2f7dc..79fbfb05b51 100644 --- a/steel-core/src/behavior/items/block_item.rs +++ b/steel-core/src/behavior/items/block_item.rs @@ -33,7 +33,9 @@ impl ItemBehavior for BlockItemBehavior { let clicked_replaceable = clicked_block.is_some_and(|b| b.config.replaceable); // Check if the clicked block can be dynamically replaced (e.g., slabs merging) - let clicked_can_be_replaced = if !clicked_replaceable { + let clicked_can_be_replaced = if clicked_replaceable { + true + } else { let block_behaviors = &*BLOCK_BEHAVIORS; if let Some(clicked) = clicked_block { let behavior = block_behaviors.get_behavior(clicked); @@ -41,8 +43,6 @@ impl ItemBehavior for BlockItemBehavior { } else { false } - } else { - true }; // Determine placement position: replace clicked block if replaceable, @@ -64,7 +64,9 @@ impl ItemBehavior for BlockItemBehavior { let existing_replaceable = existing_block.is_some_and(|b| b.config.replaceable); // Also check dynamic replacement for the existing block at the placement position - let existing_can_be_replaced = if !existing_replaceable { + let existing_can_be_replaced = if existing_replaceable { + true + } else { let block_behaviors = &*BLOCK_BEHAVIORS; if let Some(existing) = existing_block { let behavior = block_behaviors.get_behavior(existing); @@ -78,8 +80,6 @@ impl ItemBehavior for BlockItemBehavior { } else { false } - } else { - true }; if !existing_can_be_replaced { From ded3c3fc2c034265f538e26c029509d4dee6064f Mon Sep 17 00:00:00 2001 From: Joniii11 <88550325+Joniii11@users.noreply.github.com> Date: Fri, 6 Feb 2026 16:51:07 +0100 Subject: [PATCH 3/3] typos --- steel-core/src/behavior/block.rs | 2 +- steel-core/src/behavior/blocks/slab_block.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/steel-core/src/behavior/block.rs b/steel-core/src/behavior/block.rs index dedff1a4369..01c4138a01c 100644 --- a/steel-core/src/behavior/block.rs +++ b/steel-core/src/behavior/block.rs @@ -45,7 +45,7 @@ pub trait BlockBehaviour: Send + Sync { /// 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 vanillla logik in java the canBeReplaced thingy + /// 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. diff --git a/steel-core/src/behavior/blocks/slab_block.rs b/steel-core/src/behavior/blocks/slab_block.rs index 2f0fd32df82..a10a251b7c9 100644 --- a/steel-core/src/behavior/blocks/slab_block.rs +++ b/steel-core/src/behavior/blocks/slab_block.rs @@ -152,7 +152,7 @@ impl BlockBehaviour for SlabBlock { SlabType::Double => false, } } else { - // placing adjacent to a block into an exisitng slab position + // 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,