diff --git a/.github/workflows/flint.yml b/.github/workflows/flint.yml index a372d917eee..69c38417df7 100644 --- a/.github/workflows/flint.yml +++ b/.github/workflows/flint.yml @@ -2,7 +2,7 @@ name: Flint Tests on: push: - branches: ['**'] + branches: ['master'] tags: ['**'] pull_request: @@ -30,6 +30,7 @@ jobs: uses: actions/checkout@v4 with: repository: JunkyDeveloper/FlintBenchmark + ref: walls path: FlintBenchmark - name: Setup Rust nightly diff --git a/steel-core/build/blocks.rs b/steel-core/build/blocks.rs index fcc25e4cfd6..859f01abd24 100644 --- a/steel-core/build/blocks.rs +++ b/steel-core/build/blocks.rs @@ -74,6 +74,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 fire_blocks = Vec::new(); + let mut nether_portal_blocks = Vec::new(); let mut weathering_full_blocks: Vec<(Ident, Ident)> = Vec::new(); let mut cactus_blocks = Vec::new(); let mut cactus_flower_blocks: Vec = Vec::new(); @@ -128,6 +130,8 @@ pub fn build(blocks: &[BlockClass]) -> String { } "CactusBlock" => cactus_blocks.push(const_ident), "CactusFlowerBlock" => cactus_flower_blocks.push(const_ident), + "FireBlock" => fire_blocks.push(const_ident), + "NetherPortalBlock" => nether_portal_blocks.push(const_ident), _ => {} } } @@ -148,6 +152,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 fire_type = Ident::new("FireBlock", Span::call_site()); + let nether_portal_block = Ident::new("NetherPortalBlock", Span::call_site()); let cactus_type = Ident::new("CactusBlock", Span::call_site()); let cactus_flower_type = Ident::new("CactusFlowerBlock", Span::call_site()); @@ -226,6 +232,9 @@ pub fn build(blocks: &[BlockClass]) -> String { let cactus_registrations = generate_registrations(cactus_blocks.iter(), &cactus_type); let cactus_flower_registrations = generate_registrations(cactus_flower_blocks.iter(), &cactus_flower_type); + let fire_registrations = generate_registrations(fire_blocks.iter(), &fire_type); + let nether_portal_registrations = + generate_registrations(nether_portal_blocks.iter(), &nether_portal_block); let output = quote! { //! Generated block behavior assignments. @@ -233,10 +242,12 @@ pub fn build(blocks: &[BlockClass]) -> String { use steel_registry::{sound_events, vanilla_blocks, vanilla_fluids}; use crate::behavior::BlockBehaviorRegistry; use crate::behavior::blocks::{ - BarrelBlock, ButtonBlock, CandleBlock, CraftingTableBlock, CropBlock, EndPortalFrameBlock, - FarmlandBlock, FenceBlock, LiquidBlock, RotatedPillarBlock, StandingSignBlock, WallSignBlock, - CeilingHangingSignBlock, WallHangingSignBlock, TorchBlock, WallTorchBlock, - RedstoneTorchBlock, RedstoneWallTorchBlock, WeatherState, WeatheringCopperFullBlock, CactusBlock, CactusFlowerBlock, + BarrelBlock, ButtonBlock, CactusBlock, CactusFlowerBlock, CandleBlock, + CeilingHangingSignBlock, CraftingTableBlock, CropBlock, EndPortalFrameBlock, + FarmlandBlock, FenceBlock, FireBlock, LiquidBlock, NetherPortalBlock, + RedstoneTorchBlock, RedstoneWallTorchBlock, RotatedPillarBlock, + StandingSignBlock, TorchBlock, WallHangingSignBlock, WallSignBlock, + WallTorchBlock, WeatherState, WeatheringCopperFullBlock, }; pub fn register_block_behaviors(registry: &mut BlockBehaviorRegistry) { @@ -261,6 +272,8 @@ pub fn build(blocks: &[BlockClass]) -> String { #weathering_full_block_registrations #cactus_registrations #cactus_flower_registrations + #fire_registrations + #nether_portal_registrations } }; diff --git a/steel-core/build/items.rs b/steel-core/build/items.rs index 7fbae170580..8efe697364b 100644 --- a/steel-core/build/items.rs +++ b/steel-core/build/items.rs @@ -124,6 +124,7 @@ fn generate_simple_registrations<'a>( quote! { #(#registrations)* } } +#[allow(clippy::too_many_lines)] pub fn build(items: &[ItemClass]) -> String { let mut block_items: Vec<(Ident, Ident)> = Vec::new(); let mut sign_items: Vec<(Ident, Ident, Ident)> = Vec::new(); @@ -131,6 +132,7 @@ pub fn build(items: &[ItemClass]) -> String { let mut standing_and_wall_items: Vec<(Ident, Ident, Ident)> = Vec::new(); let mut ender_eye_items: Vec = Vec::new(); let mut shovel_items: Vec = Vec::new(); + let mut flint_and_steel_items: Vec = Vec::new(); let mut filled_bucket_items: Vec<(Ident, Ident)> = Vec::new(); let mut empty_bucket_items: Vec = Vec::new(); @@ -181,6 +183,7 @@ pub fn build(items: &[ItemClass]) -> String { } "EnderEyeItem" => ender_eye_items.push(item_field), "ShovelItem" => shovel_items.push(item_field), + "FlintAndSteelItem" => flint_and_steel_items.push(item_field), "BucketItem" => { let fluid = item.fluid.as_ref().expect("BucketItem missing `fluid`"); if fluid == "empty" { @@ -205,6 +208,9 @@ pub fn build(items: &[ItemClass]) -> String { generate_simple_registrations(ender_eye_items.iter(), &ender_eye_type); let shovel_type = Ident::new("ShovelBehaviour", Span::call_site()); let shovel_registrations = generate_simple_registrations(shovel_items.iter(), &shovel_type); + let flint_and_steel_type = Ident::new("FlintAndSteelBehavior", Span::call_site()); + let flint_and_steel_registrations = + generate_simple_registrations(flint_and_steel_items.iter(), &flint_and_steel_type); let filled_bucket_registrations = generate_filled_bucket_item_registrations(filled_bucket_items.iter()); let empty_bucket_type = Ident::new("EmptyBucketBehavior", Span::call_site()); @@ -216,7 +222,7 @@ pub fn build(items: &[ItemClass]) -> String { use steel_registry::{vanilla_blocks, vanilla_items}; use crate::behavior::ItemBehaviorRegistry; - use crate::behavior::items::{BlockItemBehavior, EnderEyeBehavior, HangingSignItemBehavior, SignItemBehavior, StandingAndWallBlockItem, ShovelBehaviour, FilledBucketBehavior, EmptyBucketBehavior}; + use crate::behavior::items::{BlockItemBehavior, EnderEyeBehavior, HangingSignItemBehavior, SignItemBehavior, StandingAndWallBlockItem, ShovelBehaviour, FilledBucketBehavior, FlintAndSteelBehavior, EmptyBucketBehavior}; pub fn register_item_behaviors(registry: &mut ItemBehaviorRegistry) { #block_item_registrations @@ -225,6 +231,7 @@ pub fn build(items: &[ItemClass]) -> String { #standing_and_wall_item_registrations #ender_eye_registrations #shovel_registrations + #flint_and_steel_registrations #filled_bucket_registrations #empty_bucket_registrations } diff --git a/steel-core/src/behavior/blocks/fire.rs b/steel-core/src/behavior/blocks/fire.rs new file mode 100644 index 00000000000..8631dbeec87 --- /dev/null +++ b/steel-core/src/behavior/blocks/fire.rs @@ -0,0 +1,44 @@ +//! fire block behavior implementation. + +use steel_registry::blocks::BlockRef; +use steel_utils::{BlockPos, BlockStateId}; + +use crate::behavior::block::BlockBehaviour; +use crate::behavior::context::BlockPlaceContext; +use crate::portal::portal_shape::{PortalShape, nether_portal_config}; +use crate::world::World; + +/// Behavior for fire blocks. +/// +/// Fire burns, makes hot, and hurts +pub struct FireBlock { + block: BlockRef, +} + +impl FireBlock { + /// Creates a new fire block behavior. + #[must_use] + pub const fn new(block: BlockRef) -> Self { + Self { block } + } +} + +impl BlockBehaviour for FireBlock { + fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option { + Some(self.block.default_state()) + } + + fn on_place( + &self, + _state: BlockStateId, + world: &World, + pos: BlockPos, + _old_state: BlockStateId, + _moved_by_piston: bool, + ) { + if let Some(tester) = PortalShape::find_portal_shape(world, pos, &nether_portal_config()) { + tester.place_portal_blocks(world); + // TODO: Play ignite sound, damage item + } + } +} diff --git a/steel-core/src/behavior/blocks/mod.rs b/steel-core/src/behavior/blocks/mod.rs index 6b42b812da9..6cdb92ef81f 100644 --- a/steel-core/src/behavior/blocks/mod.rs +++ b/steel-core/src/behavior/blocks/mod.rs @@ -13,7 +13,9 @@ mod crop_block; mod end_portal_frame_block; mod farmland_block; mod fence_block; +mod fire; mod liquid_block; +mod nether_portal_block; mod redstone_torch_block; mod rotated_pillar_block; mod sign_block; @@ -30,7 +32,9 @@ pub use crop_block::CropBlock; pub use end_portal_frame_block::EndPortalFrameBlock; pub use farmland_block::FarmlandBlock; pub use fence_block::FenceBlock; +pub use fire::FireBlock; pub use liquid_block::LiquidBlock; +pub use nether_portal_block::NetherPortalBlock; pub use redstone_torch_block::{RedstoneTorchBlock, RedstoneWallTorchBlock}; pub use rotated_pillar_block::RotatedPillarBlock; pub use sign_block::{ diff --git a/steel-core/src/behavior/blocks/nether_portal_block.rs b/steel-core/src/behavior/blocks/nether_portal_block.rs new file mode 100644 index 00000000000..1d01937391c --- /dev/null +++ b/steel-core/src/behavior/blocks/nether_portal_block.rs @@ -0,0 +1,52 @@ +//! Nether portal block behavior. + +use crate::behavior::block::BlockBehaviour; +use crate::behavior::context::BlockPlaceContext; +use crate::portal::portal_shape::{PortalShape, nether_portal_config}; +use crate::world::World; +use steel_registry::blocks::BlockRef; +use steel_registry::blocks::block_state_ext::BlockStateExt; +use steel_registry::blocks::properties::BlockStateProperties; +use steel_registry::vanilla_blocks::AIR; +use steel_utils::math::Axis; +use steel_utils::{BlockPos, BlockStateId, Direction}; + +/// Behavior for the nether portal block. +pub struct NetherPortalBlock { + block: BlockRef, +} +impl NetherPortalBlock { + /// Create a new `NetherPortalBlock` + #[must_use] + pub const fn new(block: BlockRef) -> Self { + Self { block } + } +} + +impl BlockBehaviour for NetherPortalBlock { + fn update_shape( + &self, + state: BlockStateId, + world: &World, + pos: BlockPos, + direction: Direction, + _neighbor_pos: BlockPos, + neighbor_state: BlockStateId, + ) -> BlockStateId { + let update_axis = direction.get_axis(); + let axis: Axis = state.get_value(&BlockStateProperties::HORIZONTAL_AXIS); + let wrong_axis = axis != update_axis && update_axis != Axis::Y; + + if !wrong_axis + && neighbor_state.get_block() != self.block + && PortalShape::find_any_shape(world, pos, axis, &nether_portal_config()).is_none() + { + return AIR.default_state(); + } + state + } + + fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option { + None // TODO: add this functionality but has low priority + } +} diff --git a/steel-core/src/behavior/items/flint_and_steel.rs b/steel-core/src/behavior/items/flint_and_steel.rs new file mode 100644 index 00000000000..090ed38b7ce --- /dev/null +++ b/steel-core/src/behavior/items/flint_and_steel.rs @@ -0,0 +1,25 @@ +//! Flint and steel item behavior with portal ignition. + +use crate::behavior::context::{InteractionResult, UseOnContext}; +use crate::behavior::item::ItemBehavior; +use steel_registry::vanilla_blocks::FIRE; +use steel_utils::types::UpdateFlags; + +/// Behavior for flint and steel items. +pub struct FlintAndSteelBehavior; + +impl ItemBehavior for FlintAndSteelBehavior { + fn use_on(&self, context: &mut UseOnContext) -> InteractionResult { + let click_pos = context.hit_result.block_pos; + let fire_pos = click_pos.relative(context.hit_result.direction); + + context.world.set_block( + fire_pos, + FIRE.default_state(), + UpdateFlags::UPDATE_NEIGHBORS, + ); + + // TODO: Place fire block at fire_pos if it's air on a solid block + InteractionResult::Pass + } +} diff --git a/steel-core/src/behavior/items/mod.rs b/steel-core/src/behavior/items/mod.rs index db21109a3aa..7f77fb8c0f7 100644 --- a/steel-core/src/behavior/items/mod.rs +++ b/steel-core/src/behavior/items/mod.rs @@ -11,10 +11,13 @@ mod shovel; mod sign_item; mod standing_and_wall_block_item; +mod flint_and_steel; + pub use block_item::BlockItemBehavior; pub use bucket::{EmptyBucketBehavior, FilledBucketBehavior}; pub use default::DefaultItemBehavior; pub use ender_eye::EnderEyeBehavior; +pub use flint_and_steel::FlintAndSteelBehavior; pub use shovel::ShovelBehaviour; pub use sign_item::{HangingSignItemBehavior, SignItemBehavior}; pub use standing_and_wall_block_item::StandingAndWallBlockItem; diff --git a/steel-core/src/lib.rs b/steel-core/src/lib.rs index 0379b83b861..52f8aa83457 100644 --- a/steel-core/src/lib.rs +++ b/steel-core/src/lib.rs @@ -17,6 +17,7 @@ pub mod level_data; pub mod physics; pub mod player; pub mod poi; +pub mod portal; pub mod server; pub mod world; pub mod worldgen; diff --git a/steel-core/src/portal/mod.rs b/steel-core/src/portal/mod.rs new file mode 100644 index 00000000000..fb0d23bba17 --- /dev/null +++ b/steel-core/src/portal/mod.rs @@ -0,0 +1,2 @@ +//! Dimension portal system for nether/end portals and future portal types. +pub mod portal_shape; diff --git a/steel-core/src/portal/portal_shape.rs b/steel-core/src/portal/portal_shape.rs new file mode 100644 index 00000000000..e64a03c11e8 --- /dev/null +++ b/steel-core/src/portal/portal_shape.rs @@ -0,0 +1,261 @@ +//! Portal shape detection for validating obsidian frames. + +use steel_registry::blocks::BlockRef; +use steel_registry::blocks::block_state_ext::BlockStateExt; +use steel_registry::blocks::properties::BlockStateProperties; +use steel_registry::vanilla_blocks; +use steel_utils::math::Axis; +use steel_utils::types::UpdateFlags; +use steel_utils::{BlockPos, Direction}; + +use crate::world::World; + +/// A detected portal shape with axis, position, and dimensions. +pub struct PortalShape { + /// The axis of the portal (X or Z). + pub axis: Axis, + /// Bottom-left corner of the portal interior. + pub bottom_left: BlockPos, + /// Width of the interior (2-21). + pub width: u32, + /// Height of the interior (3-21). + pub height: u32, + /// The block type of the frame. + pub portal: BlockRef, +} + +/// Definition of a portal shape in rectangular form, like the nether portal frame. +pub struct PortalFrameConfig { + /// min size of the portal in x direction + pub min_width: u32, + /// max size of the portal in x direction + pub max_width: u32, + /// min size of the portal in y direction + pub min_height: u32, + /// max size of the portal in y direction + pub max_height: u32, + /// The block type of the frame. + pub frame: BlockRef, + /// The block type of the portal. + pub portal: BlockRef, +} + +/// Returns the standard nether portal frame configuration. +#[must_use] +pub fn nether_portal_config() -> PortalFrameConfig { + PortalFrameConfig { + min_width: 2, + max_width: 21, + min_height: 3, + max_height: 21, + frame: vanilla_blocks::OBSIDIAN, + portal: vanilla_blocks::NETHER_PORTAL, + } +} + +/// Interior check: air or fire only (used when creating a new portal). +fn is_empty_interior(world: &World, pos: BlockPos, _config: &PortalFrameConfig) -> bool { + let block = world.get_block_state(&pos).get_block(); + block == vanilla_blocks::AIR || block == vanilla_blocks::FIRE +} + +/// Interior check: air, fire, or existing portal blocks (used when validating an existing portal). +fn is_portal_or_empty_interior(world: &World, pos: BlockPos, config: &PortalFrameConfig) -> bool { + let block = world.get_block_state(&pos).get_block(); + block == vanilla_blocks::AIR || block == vanilla_blocks::FIRE || block == config.portal +} + +/// Interior validator function signature. +type InteriorCheck = fn(&World, BlockPos, &PortalFrameConfig) -> bool; + +impl PortalShape { + /// Tries to find a valid portal shape from a position inside or adjacent to a frame. + pub fn find_portal_shape( + world: &World, + fire_pos: BlockPos, + config: &PortalFrameConfig, + ) -> Option { + Self::try_axis(world, fire_pos, Axis::X, config, is_empty_interior) + .or_else(|| Self::try_axis(world, fire_pos, Axis::Z, config, is_empty_interior)) + } + + /// Finds a portal shape on a specific axis, treating existing portal blocks as valid interior. + /// Used by `update_shape` to check if the portal frame is still complete. + pub fn find_any_shape( + world: &World, + pos: BlockPos, + axis: Axis, + config: &PortalFrameConfig, + ) -> Option { + Self::try_axis(world, pos, axis, config, is_portal_or_empty_interior) + } + + /// Tries to find a valid portal on a single axis. + /// It loops over the interior (not frame blocks) to determine the portal dimensions. + fn try_axis( + world: &World, + pos: BlockPos, + axis: Axis, + config: &PortalFrameConfig, + interior_check: InteriorCheck, + ) -> Option { + // Width direction: portal axis=X means width along Z, axis=Z means width along X + let dir: Direction = match axis { + Axis::X => Direction::East, + Axis::Z => Direction::North, + Axis::Y => return None, + }; + + // searches the bottom obsidian + let mut cur = pos; + for _ in 0..=config.max_height as i32 { + let next = BlockPos::new(cur.x(), cur.y() - 1, cur.z()); + if Self::is_frame_block(world, next, config) { + break; + } + cur = next; + } + + // searches for the left obsidian (-1) because we don't want to be at the obsidian block + let to_left = Self::get_width(world, cur, dir, config, interior_check); + cur = cur.relative_n(dir, to_left as i32); + + let width = Self::get_width(world, cur, dir.opposite(), config, interior_check) + 1; + if width < config.min_width { + return None; + } + let height = Self::get_height(world, cur, dir, config, interior_check); + if height < config.min_height { + return None; + } + + // Validate entire frame + if !Self::validate_frame( + world, + cur, + width, + height, + dir.opposite(), + config, + interior_check, + ) { + return None; + } + + Some(Self { + axis, + bottom_left: cur, + width, + height, + portal: config.portal, + }) + } + + /// Returns the width - 1 of the portal interior starting from the given position. + fn get_width( + world: &World, + pos: BlockPos, + direction: Direction, + config: &PortalFrameConfig, + interior_check: InteriorCheck, + ) -> u32 { + for i in 1..config.max_width { + let next = pos.relative_n(direction, i as i32); + if !interior_check(world, next, config) && Self::is_frame_block(world, next, config) { + return i - 1; + } + if !Self::is_frame_block(world, next.below(), config) { + return 0; + } + } + 0 + } + + fn get_height( + world: &World, + pos: BlockPos, + direction: Direction, + config: &PortalFrameConfig, + interior_check: InteriorCheck, + ) -> u32 { + let mut cur = pos; + for i in 1..config.max_height { + let next = cur.above(); + if !interior_check(world, next, config) && Self::is_frame_block(world, next, config) { + return i; + } + if !Self::is_frame_block(world, next.relative(direction), config) { + return 0; + } + cur = next; + } + 0 + } + + fn is_frame_block(world: &World, pos: BlockPos, config: &PortalFrameConfig) -> bool { + world.get_block_state(&pos).get_block() == config.frame + } + + fn validate_frame( + world: &World, + bottom_left: BlockPos, + width: u32, + height: u32, + direction: Direction, + config: &PortalFrameConfig, + interior_check: InteriorCheck, + ) -> bool { + // Check top frame row + let top_row = bottom_left.above_n(height as i32); + for w in 0..width as i32 { + if !Self::is_frame_block(world, top_row.relative_n(direction, w), config) { + return false; + } + } + + // Check right columns + interior + for h in 0..height as i32 { + // Right column + let height_pos = bottom_left.above_n(h); + if !Self::is_frame_block( + world, + height_pos.relative_n(direction, width as i32), + config, + ) { + return false; + } + + // Interior blocks + for w in 0..width as i32 { + if !interior_check(world, height_pos.relative_n(direction, w), config) { + return false; + } + } + } + + true + } + + /// Fills the interior with nether portal blocks. + pub fn place_portal_blocks(&self, world: &World) { + let portal_state = self + .portal + .default_state() + .set_value(&BlockStateProperties::HORIZONTAL_AXIS, self.axis); + let dir = match self.axis { + Axis::X => Direction::West, + Axis::Z => Direction::South, + Axis::Y => return, + }; + let flags = UpdateFlags::UPDATE_ALL; + for w in 0..self.width { + for h in 0..self.height { + world.set_block( + self.bottom_left.above_n(h as i32).relative_n(dir, w as i32), + portal_state, + flags, + ); + } + } + } +}