diff --git a/steel-core/build/blocks.rs b/steel-core/build/blocks.rs index f74beaa52c9..2bbcbeee306 100644 --- a/steel-core/build/blocks.rs +++ b/steel-core/build/blocks.rs @@ -52,6 +52,7 @@ 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 sugar_cane_blocks = Vec::new(); for block in blocks { let const_ident = to_const_ident(&block.name); @@ -77,6 +78,7 @@ 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), + "SugarCaneBlock" => sugar_cane_blocks.push(const_ident), _ => {} } } @@ -97,6 +99,7 @@ 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 sugar_cane_type = Ident::new("SugarCaneBlock", 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 +138,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 sugar_cane_registrations = + generate_registrations(sugar_cane_blocks.iter(), &sugar_cane_type); let output = quote! { //! Generated block behavior assignments. @@ -145,7 +150,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,SugarCaneBlock, }; pub fn register_block_behaviors(registry: &mut BlockBehaviorRegistry) { @@ -166,6 +171,7 @@ pub fn build(blocks: &[BlockClass]) -> String { #wall_torch_registrations #redstone_torch_registrations #redstone_wall_torch_registrations + #sugar_cane_registrations } }; diff --git a/steel-core/src/behavior/blocks/mod.rs b/steel-core/src/behavior/blocks/mod.rs index 0b4c8a19859..af53d0993f6 100644 --- a/steel-core/src/behavior/blocks/mod.rs +++ b/steel-core/src/behavior/blocks/mod.rs @@ -14,6 +14,7 @@ mod liquid_block; mod redstone_torch_block; mod rotated_pillar_block; mod sign_block; +mod sugar_cane_block; mod torch_block; pub use barrel_block::BarrelBlock; @@ -29,4 +30,5 @@ pub use rotated_pillar_block::RotatedPillarBlock; pub use sign_block::{ CeilingHangingSignBlock, StandingSignBlock, WallHangingSignBlock, WallSignBlock, }; +pub use sugar_cane_block::SugarCaneBlock; pub use torch_block::{TorchBlock, WallTorchBlock}; diff --git a/steel-core/src/behavior/blocks/sugar_cane_block.rs b/steel-core/src/behavior/blocks/sugar_cane_block.rs new file mode 100644 index 00000000000..a7d783bff68 --- /dev/null +++ b/steel-core/src/behavior/blocks/sugar_cane_block.rs @@ -0,0 +1,176 @@ +//! Sugar Cane block behavior. +//! +//! Sugar cane grows up to 3 blocks tall via random ticks. It requires water adjacent +//! to the block it is planted on (or frosted ice). + +use std::ptr; + +use steel_registry::REGISTRY; +use steel_registry::blocks::BlockRef; +use steel_registry::blocks::block_state_ext::BlockStateExt; +use steel_registry::blocks::properties::{BlockStateProperties, Direction}; +use steel_registry::vanilla_blocks; +use steel_utils::{BlockPos, BlockStateId, Identifier, types::UpdateFlags}; + +use crate::behavior::BlockStateBehaviorExt; +use crate::behavior::block::BlockBehaviour; +use crate::behavior::context::BlockPlaceContext; +use crate::world::World; + +/// Maximum sugar cane stack height (vanilla: 3 blocks). +const MAX_SUGAR_CANE_HEIGHT: i32 = 3; + +/// Behavior for sugar cane blocks. +pub struct SugarCaneBlock { + block: BlockRef, +} + +impl SugarCaneBlock { + /// Creates a new sugar cane block behavior. + #[must_use] + pub const fn new(block: BlockRef) -> Self { + Self { block } + } + + /// Checks if sugar cane can survive at the given position. + /// + /// Survival requirements: + /// 1. Block below is Sugar Cane (stacking), OR + /// 2. Block below is Dirt/Grass/Sand AND adjacent to Water/FrostedIce. + fn can_survive(world: &World, pos: BlockPos) -> bool { + let below_pos = pos.below(); + let below_state = world.get_block_state(&below_pos); + let below_block = below_state.get_block(); + + // 1. Check if placing on top of another sugar cane + if ptr::eq(below_block, vanilla_blocks::SUGAR_CANE) { + return true; + } + + // 2. Check if placing on valid ground (Dirt or Sand tags) + let is_valid_ground = REGISTRY + .blocks + .is_in_tag(below_block, &Identifier::vanilla_static("dirt")) + || REGISTRY + .blocks + .is_in_tag(below_block, &Identifier::vanilla_static("sand")); + + if is_valid_ground { + // Check for adjacent water or frosted ice around the *ground* block + for dir in [ + Direction::North, + Direction::South, + Direction::East, + Direction::West, + ] { + let neighbor_pos = dir.relative(&below_pos); + let neighbor_state = world.get_block_state(&neighbor_pos); + let neighbor_block = neighbor_state.get_block(); + + // Check for water fluid or frosted ice block + // TODO: Proper fluid check using fluid tags + if !neighbor_state.get_fluid_state().is_empty() { + return true; + } + + if ptr::eq(neighbor_block, vanilla_blocks::FROSTED_ICE) { + return true; + } + } + } + + false + } +} + +impl BlockBehaviour for SugarCaneBlock { + fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option { + let pos = context.relative_pos; + if Self::can_survive(context.world, pos) { + Some(self.block.default_state()) + } else { + None + } + } + + /// Called when this block is placed. + fn on_place( + &self, + state: BlockStateId, + world: &World, + pos: BlockPos, + old_state: BlockStateId, + _moved_by_piston: bool, + ) { + if ptr::eq(state.get_block(), old_state.get_block()) { + return; + } + + // HACK: Immediate destruction instead of vanilla's scheduled tick + if !Self::can_survive(world, pos) { + world.destroy_block_effect(pos, u32::from(state.0), None); + world.set_block( + pos, + vanilla_blocks::AIR.default_state(), + UpdateFlags::UPDATE_ALL, + ); + } + } + + fn is_randomly_ticking(&self, _state: BlockStateId) -> bool { + true + } + + fn random_tick(&self, state: BlockStateId, world: &World, pos: BlockPos) { + let above_pos = pos.above(); + + if !world.get_block_state(&above_pos).is_air() { + return; + } + + // Count sugar cane blocks below to determine height + let mut height = 1i32; + while ptr::eq( + world.get_block_state(&pos.below_n(height)).get_block(), + self.block, + ) { + height += 1; + } + + if height < MAX_SUGAR_CANE_HEIGHT { + let age = state.get_value(&BlockStateProperties::AGE_15); + + if age == 15 { + world.set_block( + above_pos, + self.block.default_state(), + UpdateFlags::UPDATE_ALL, + ); + // Reset age + let new_state = state.set_value(&BlockStateProperties::AGE_15, 0); + world.set_block(pos, new_state, UpdateFlags::UPDATE_CLIENTS); + } else { + let new_state = state.set_value(&BlockStateProperties::AGE_15, age + 1); + world.set_block(pos, new_state, UpdateFlags::UPDATE_CLIENTS); + } + } + } + + fn update_shape( + &self, + state: BlockStateId, + world: &World, + pos: BlockPos, + _direction: Direction, + _neighbor_pos: BlockPos, + _neighbor_state: BlockStateId, + ) -> BlockStateId { + if !Self::can_survive(world, pos) { + world.destroy_block_effect(pos, u32::from(state.0), None); + + // TODO: Drop sugar cane item via pop_resource + return vanilla_blocks::AIR.default_state(); + } + state + } +}