diff --git a/steel-core/build/build.rs b/steel-core/build/build.rs index 58810ffa12e..38b6fd2351e 100644 --- a/steel-core/build/build.rs +++ b/steel-core/build/build.rs @@ -12,6 +12,7 @@ use std::fs; use syn::Ident; mod blocks; +mod candle_cakes; mod common; mod items; mod strippables; @@ -40,6 +41,8 @@ pub fn main() { blocks::build(&classes.blocks), ) .expect("Failed to write blocks.rs"); + fs::write(format!("{out_dir}/candle_cakes.rs"), candle_cakes::build()) + .expect("Failed to write candle_cakes.rs"); fs::write(format!("{out_dir}/items.rs"), items::build(&classes.items)) .expect("Failed to write items.rs"); fs::write(format!("{out_dir}/waxables.rs"), waxables::build()) diff --git a/steel-core/build/candle_cakes.json b/steel-core/build/candle_cakes.json new file mode 100644 index 00000000000..c183fafaf48 --- /dev/null +++ b/steel-core/build/candle_cakes.json @@ -0,0 +1,19 @@ +{ + "blue_candle": "blue_candle_cake", + "green_candle": "green_candle_cake", + "red_candle": "red_candle_cake", + "pink_candle": "pink_candle_cake", + "yellow_candle": "yellow_candle_cake", + "magenta_candle": "magenta_candle_cake", + "brown_candle": "brown_candle_cake", + "light_gray_candle": "light_gray_candle_cake", + "candle": "candle_cake", + "white_candle": "white_candle_cake", + "gray_candle": "gray_candle_cake", + "cyan_candle": "cyan_candle_cake", + "lime_candle": "lime_candle_cake", + "orange_candle": "orange_candle_cake", + "black_candle": "black_candle_cake", + "light_blue_candle": "light_blue_candle_cake", + "purple_candle": "purple_candle_cake" +} \ No newline at end of file diff --git a/steel-core/build/candle_cakes.rs b/steel-core/build/candle_cakes.rs new file mode 100644 index 00000000000..129e4bf91c2 --- /dev/null +++ b/steel-core/build/candle_cakes.rs @@ -0,0 +1,35 @@ +use std::{collections::BTreeMap, fs}; + +use proc_macro2::Span; +use quote::quote; +use syn::Ident; + +use crate::to_block_ident; + +pub fn build() -> String { + println!("cargo:rerun-if-changed=build/candle_cakes.json"); + + let candle_cakes_json = + fs::read_to_string("build/candle_cakes.json").expect("Failed to read candle_cakes.json"); + let candle_cakes_raw: BTreeMap = + serde_json::from_str(&candle_cakes_json).expect("Failed to parse candle_cakes.json"); + + let by_candle: Vec = candle_cakes_raw + .iter() + .map(|(key, value)| (Ident::new(key.to_lowercase().as_str(), Span::call_site()), to_block_ident(value))) + .map(|(key, value)| quote! { i if i == &vanilla_items::ITEMS.#key => Some(&vanilla_blocks::#value), }) + .collect(); + + let output = quote! { + use steel_registry::{blocks::BlockRef, items::ItemRef, vanilla_blocks, vanilla_items}; + + pub fn candle_to_candle_cake(item: ItemRef) -> Option { + match item { + #(#by_candle)* + _ => None + } + } + }; + + output.to_string() +} diff --git a/steel-core/src/behavior/block.rs b/steel-core/src/behavior/block.rs index 4ce3f9c9932..0495426d322 100644 --- a/steel-core/src/behavior/block.rs +++ b/steel-core/src/behavior/block.rs @@ -12,6 +12,7 @@ use steel_registry::{REGISTRY, RegistryEntry, RegistryExt}; use steel_utils::types::{InteractionHand, UpdateFlags}; use steel_utils::{BlockPos, BlockStateId}; +use crate::behavior::InventoryAccess; use crate::behavior::context::{BlockHitResult, BlockPlaceContext, InteractionResult}; use crate::block_entity::SharedBlockEntity; use crate::entity::Entity; @@ -144,13 +145,13 @@ pub trait BlockBehavior: Send + Sync { )] fn use_item_on( &self, - item_stack: &ItemStack, state: BlockStateId, world: &Arc, pos: BlockPos, player: &Player, hand: InteractionHand, hit_result: &BlockHitResult, + inv: &mut InventoryAccess, ) -> InteractionResult { InteractionResult::TryEmptyHandInteraction } @@ -171,6 +172,7 @@ pub trait BlockBehavior: Send + Sync { pos: BlockPos, player: &Player, hit_result: &BlockHitResult, + inv: &mut InventoryAccess, ) -> InteractionResult { InteractionResult::Pass } @@ -398,7 +400,7 @@ pub trait BlockBehavior: Send + Sync { /// Vanilla parity: `LiquidBlockContainer.canPlaceLiquid()`. /// /// Returns `true` if the given fluid type may be placed into this block at the - /// given state. Called by the fluid-spread logic; there is no player context + /// given state. Called by the fluid-spread logic; there is no player context /// here (fluid spreading has no associated player). /// /// Default (`SimpleWaterloggedBlock`): accepts water when the block has a diff --git a/steel-core/src/behavior/blocks/building/campfire.rs b/steel-core/src/behavior/blocks/building/campfire.rs new file mode 100644 index 00000000000..0d9ec0a733e --- /dev/null +++ b/steel-core/src/behavior/blocks/building/campfire.rs @@ -0,0 +1,221 @@ +use std::sync::{Arc, Weak}; + +use steel_macros::block_behavior; +use steel_registry::{ + REGISTRY, TaggedRegistryExt, + blocks::{BlockRef, block_state_ext::BlockStateExt, properties::BlockStateProperties}, + fluid::{FluidState, FluidStateExt}, + items::item::BlockHitResult, + sound_events, vanilla_block_entity_types, vanilla_block_tags, vanilla_blocks, + vanilla_damage_types, vanilla_fluids, vanilla_recipe_property_sets, +}; +use steel_utils::{ + BlockPos, BlockStateId, Direction, + types::{InteractionHand, UpdateFlags}, +}; + +use crate::{ + behavior::{BlockBehavior, BlockPlaceContext, InteractionResult, InventoryAccess}, + block_entity::{BLOCK_ENTITIES, SharedBlockEntity, entities::CampfireBlockEntity}, + entity::{Entity, damage::DamageSource}, + player::Player, + world::World, +}; + +/// Behavior for Campfires +/// - [ ] projectile hit +/// - [ ] getTicker to run cooking ticks +#[block_behavior] +pub struct CampfireBlock { + block: BlockRef, + #[json_arg(value)] + fire_damage: i32, +} + +impl CampfireBlock { + /// Creates a new Campfire Block Behavior + #[must_use] + pub const fn new(block: BlockRef, fire_damage: i32) -> Self { + Self { block, fire_damage } + } + + /// Whether or not the Campfire can be lit + #[must_use] + pub fn can_light(state: BlockStateId) -> bool { + REGISTRY + .blocks + .is_in_tag(state.get_block(), &vanilla_block_tags::CAMPFIRES_TAG) + && !state + .try_get_value(&BlockStateProperties::WATERLOGGED) + .unwrap_or(true) + && !state + .try_get_value(&BlockStateProperties::LIT) + .unwrap_or(true) + } +} + +impl BlockBehavior for CampfireBlock { + fn get_state_for_placement( + &self, + context: &BlockPlaceContext<'_>, + ) -> Option { + let is_replacing_water = context.is_water_source(); + + Some( + self.block + .default_state() + .set_value(&BlockStateProperties::WATERLOGGED, is_replacing_water) + .set_value( + &BlockStateProperties::SIGNAL_FIRE, + context + .world + .get_block_state(context.relative_pos.below()) + .get_block() + == &vanilla_blocks::HAY_BLOCK, + ) + .set_value(&BlockStateProperties::LIT, !is_replacing_water) + .set_value( + &BlockStateProperties::HORIZONTAL_FACING, + context.horizontal_direction, + ), + ) + } + + fn update_shape( + &self, + state: BlockStateId, + world: &Arc, + pos: BlockPos, + direction: Direction, + _neighbor_pos: BlockPos, + _neighbor_state: BlockStateId, + ) -> BlockStateId { + if state.get_value(&BlockStateProperties::WATERLOGGED) { + world.schedule_fluid_tick_default( + pos, + &vanilla_fluids::WATER, + vanilla_fluids::WATER.tick_delay as i32, + ); + } + + if direction == Direction::Down { + return state.set_value( + &BlockStateProperties::SIGNAL_FIRE, + world.get_block_state(pos.below()).get_block() == &vanilla_blocks::HAY_BLOCK, + ); + } + state + } + + fn entity_inside( + &self, + state: BlockStateId, + _world: &Arc, + _pos: BlockPos, + entity: &dyn Entity, + ) { + if state.get_value(&BlockStateProperties::LIT) + && let Some(living) = entity.as_living() + { + living.hurt( + &DamageSource::environment(&vanilla_damage_types::CAMPFIRE), + self.fire_damage as f32, + ); + } + } + + fn get_fluid_state(&self, state: BlockStateId) -> FluidState { + if state.get_value(&BlockStateProperties::WATERLOGGED) { + FluidState::source(&vanilla_fluids::WATER) + } else { + FluidState::EMPTY + } + } + + fn place_liquid( + &self, + world: &Arc, + pos: BlockPos, + state: BlockStateId, + fluid_state: FluidState, + ) -> bool { + if state.get_value(&BlockStateProperties::WATERLOGGED) || !fluid_state.is_water() { + return false; + } + + if !fluid_state.is_source() { + // this is a really weird fix for placing campfires not being destroyed when being placed under a water source + return true; + } + + if state.get_value(&BlockStateProperties::LIT) { + world.play_block_sound( + sound_events::ENTITY_GENERIC_EXTINGUISH_FIRE, + pos, + 1.0, + 1.0, + None, + ); + } + world.set_block( + pos, + state + .set_value(&BlockStateProperties::WATERLOGGED, true) + .set_value(&BlockStateProperties::LIT, false), + UpdateFlags::UPDATE_ALL, + ); + + world.schedule_fluid_tick_default( + pos, + fluid_state.fluid_id, + fluid_state.fluid_id.tick_delay as i32, + ); + + true + } + + fn has_block_entity(&self) -> bool { + true + } + + fn new_block_entity( + &self, + level: Weak, + pos: BlockPos, + state: BlockStateId, + ) -> Option { + BLOCK_ENTITIES.create(&vanilla_block_entity_types::CAMPFIRE, level, pos, state) + } + + fn use_item_on( + &self, + _state: BlockStateId, + world: &Arc, + pos: BlockPos, + player: &Player, + _hand: InteractionHand, + _hit_result: &BlockHitResult, + inv: &mut InventoryAccess, + ) -> InteractionResult { + let item_stack = inv.item(); + let Some(block_entity) = world.get_block_entity(pos) else { + return InteractionResult::Fail; + }; + + if !vanilla_recipe_property_sets::CAMPFIRE_INPUT.contains(&item_stack.item()) { + return InteractionResult::TryEmptyHandInteraction; + } + + let mut lock = block_entity.lock(); + + let Some(campfire_entity) = lock.as_any_mut().downcast_mut::() else { + return InteractionResult::Fail; + }; + + if campfire_entity.place_food(item_stack, player.has_infinite_materials()) { + return InteractionResult::Success; + } + + InteractionResult::TryEmptyHandInteraction + } +} diff --git a/steel-core/src/behavior/blocks/building/mod.rs b/steel-core/src/behavior/blocks/building/mod.rs index d82850d1fb3..c7e8b002ce0 100644 --- a/steel-core/src/behavior/blocks/building/mod.rs +++ b/steel-core/src/behavior/blocks/building/mod.rs @@ -1,7 +1,9 @@ +mod campfire; mod fence_block; mod rotated_pillar_block; mod weathering_block; +pub use campfire::CampfireBlock; pub use fence_block::FenceBlock; pub use rotated_pillar_block::RotatedPillarBlock; pub use weathering_block::{WeatherState, WeatheringCopper, WeatheringCopperFullBlock}; diff --git a/steel-core/src/behavior/blocks/container/barrel_block.rs b/steel-core/src/behavior/blocks/container/barrel_block.rs index 8b879b1ccd4..e33b61a8f44 100644 --- a/steel-core/src/behavior/blocks/container/barrel_block.rs +++ b/steel-core/src/behavior/blocks/container/barrel_block.rs @@ -12,6 +12,7 @@ use steel_registry::vanilla_block_entity_types; use steel_utils::{BlockPos, BlockStateId, translations}; use text_components::TextComponent; +use crate::behavior::InventoryAccess; use crate::behavior::block::BlockBehavior; use crate::behavior::context::{BlockHitResult, BlockPlaceContext, InteractionResult}; use crate::block_entity::{BLOCK_ENTITIES, SharedBlockEntity}; @@ -57,6 +58,7 @@ impl BlockBehavior for BarrelBlock { pos: BlockPos, player: &Player, _hit_result: &BlockHitResult, + _inv: &mut InventoryAccess, ) -> InteractionResult { // Get the block entity let Some(block_entity) = world.get_block_entity(pos) else { diff --git a/steel-core/src/behavior/blocks/container/crafting_table_block.rs b/steel-core/src/behavior/blocks/container/crafting_table_block.rs index 82723b8c11c..05d8abc8310 100644 --- a/steel-core/src/behavior/blocks/container/crafting_table_block.rs +++ b/steel-core/src/behavior/blocks/container/crafting_table_block.rs @@ -8,6 +8,7 @@ use steel_macros::block_behavior; use steel_registry::blocks::BlockRef; use steel_utils::{BlockPos, BlockStateId}; +use crate::behavior::InventoryAccess; use crate::behavior::block::BlockBehavior; use crate::behavior::context::{BlockHitResult, BlockPlaceContext, InteractionResult}; use crate::inventory::CraftingMenuProvider; @@ -43,6 +44,7 @@ impl BlockBehavior for CraftingTableBlock { pos: BlockPos, player: &Player, _hit_result: &BlockHitResult, + _inv: &mut InventoryAccess, ) -> InteractionResult { player.open_menu(&CraftingMenuProvider::new(player.inventory.clone(), pos)); // TODO: Award stat INTERACT_WITH_CRAFTING_TABLE diff --git a/steel-core/src/behavior/blocks/decoration/cake_block.rs b/steel-core/src/behavior/blocks/decoration/cake_block.rs new file mode 100644 index 00000000000..e9165a34b84 --- /dev/null +++ b/steel-core/src/behavior/blocks/decoration/cake_block.rs @@ -0,0 +1,175 @@ +use std::sync::Arc; + +use steel_macros::block_behavior; +use steel_registry::{ + REGISTRY, TaggedRegistryExt, + blocks::{BlockRef, block_state_ext::BlockStateExt, properties::BlockStateProperties}, + items::item::BlockHitResult, + sound_events, vanilla_blocks, vanilla_item_tags, +}; +use steel_utils::{ + BlockPos, BlockStateId, Direction, + types::{InteractionHand, UpdateFlags}, +}; + +use crate::{ + behavior::{ + BlockBehavior, BlockPlaceContext, InteractionResult, InventoryAccess, candle_cakes, + }, + player::Player, + world::World, +}; + +/// Behavior for Cakes +/// TODO: +/// - [ ] animation ticks +/// - [ ] onProjectile +/// - [ ] onExplosion +#[block_behavior] +pub struct CakeBlock { + block: BlockRef, +} + +impl CakeBlock { + /// Cakes a new Cake Block Behavior + #[must_use] + pub const fn new(block: BlockRef) -> Self { + Self { block } + } + + /// Eats a slice of the cake for the player and updates the block + pub fn eat( + world: &Arc, + pos: BlockPos, + state: BlockStateId, + player: &Player, + ) -> InteractionResult { + if player.can_eat(false) { + let mut food_data = player.food_data.lock(); + food_data.eat(2, 0.1); + let bites = state + .try_get_value(&BlockStateProperties::BITES) + .unwrap_or(0); + let new_state = if bites < 6 { + state.set_value(&BlockStateProperties::BITES, bites + 1) + } else { + vanilla_blocks::AIR.default_state() + }; + world.set_block(pos, new_state, UpdateFlags::UPDATE_ALL); + return InteractionResult::Success; + } + InteractionResult::Pass + } + + /// Analog Output Signal for the Amount of Bites + #[must_use] + pub const fn analog_output_signal(bites: i32) -> i32 { + (7 - bites) * 2 + } +} + +impl BlockBehavior for CakeBlock { + fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option { + if context + .world + .get_block_state(context.relative_pos.below()) + .is_solid() + { + Some(self.block.default_state()) + } else { + None + } + } + + fn can_survive(&self, _state: BlockStateId, world: &Arc, pos: BlockPos) -> bool { + world.get_block_state(pos.below()).is_solid() + } + + fn update_shape( + &self, + state: BlockStateId, + world: &Arc, + pos: BlockPos, + direction: Direction, + _neighbor_pos: BlockPos, + _neighbor_state: BlockStateId, + ) -> BlockStateId { + if direction == Direction::Down && !self.can_survive(state, world, pos) { + vanilla_blocks::AIR.default_state() + } else { + state + } + } + + fn use_without_item( + &self, + state: BlockStateId, + world: &Arc, + pos: BlockPos, + player: &Player, + _hit_result: &BlockHitResult, + inv: &mut InventoryAccess, + ) -> InteractionResult { + if Self::eat(world, pos, state, player).consumes_action() { + return InteractionResult::Success; + } + + if inv.item().is_empty() { + return InteractionResult::Fail; + } + InteractionResult::Pass + } + + fn use_item_on( + &self, + state: BlockStateId, + world: &Arc, + pos: BlockPos, + player: &Player, + _hand: InteractionHand, + _hit_result: &BlockHitResult, + inv: &mut InventoryAccess, + ) -> InteractionResult { + let item_stack = inv.item(); + if REGISTRY + .items + .is_in_tag(item_stack.item(), &vanilla_item_tags::CANDLES_TAG) + && state.get_value(&BlockStateProperties::BITES) == 0 + { + if !player.has_infinite_materials() { + item_stack.shrink(1); + } + world.play_block_sound( + sound_events::BLOCK_CAKE_ADD_CANDLE, + pos, + 1.0, + 1.0, + Some(player.id), + ); + world.set_block( + pos, + candle_cakes::candle_to_candle_cake(item_stack.item()) + .expect( + "Candle Item is in CANDLES_TAG but isnt in the candle_to_candle_cake map", + ) + .default_state(), + UpdateFlags::UPDATE_ALL, + ); + return InteractionResult::Success; + } + InteractionResult::TryEmptyHandInteraction + } + + fn get_analog_output_signal( + &self, + state: BlockStateId, + _world: &Arc, + _pos: BlockPos, + ) -> i32 { + Self::analog_output_signal(i32::from(state.get_value(&BlockStateProperties::BITES))) + } + + fn has_analog_output_signal(&self, _state: BlockStateId) -> bool { + true + } +} diff --git a/steel-core/src/behavior/blocks/decoration/candle_block.rs b/steel-core/src/behavior/blocks/decoration/candle_block.rs index 00792baf0cd..cdcbe6980dd 100644 --- a/steel-core/src/behavior/blocks/decoration/candle_block.rs +++ b/steel-core/src/behavior/blocks/decoration/candle_block.rs @@ -10,17 +10,16 @@ use steel_registry::{ shapes::SupportType, }, entity_data::Direction, - item_stack::ItemStack, items::item::BlockHitResult, - vanilla_blocks, vanilla_item_tags, + vanilla_block_tags, vanilla_blocks, vanilla_item_tags, }; use steel_utils::{ - BlockPos, + BlockPos, BlockStateId, types::{self, UpdateFlags}, }; use crate::{ - behavior::{BlockBehavior, BlockPlaceContext, InteractionResult}, + behavior::{BlockBehavior, BlockPlaceContext, InteractionResult, InventoryAccess}, player, world::World, }; @@ -42,6 +41,20 @@ impl CandleBlock { pub const fn new(block: BlockRef) -> Self { Self { block } } + + /// Whether or not the Candle can be lit + #[must_use] + pub fn can_light(state: BlockStateId) -> bool { + REGISTRY + .blocks + .is_in_tag(state.get_block(), &vanilla_block_tags::CANDLES_TAG) + && !state + .try_get_value(&BlockStateProperties::LIT) + .unwrap_or(true) + && !state + .try_get_value(&BlockStateProperties::WATERLOGGED) + .unwrap_or(true) + } } impl BlockBehavior for CandleBlock { @@ -85,14 +98,15 @@ impl BlockBehavior for CandleBlock { fn use_item_on( &self, - item_stack: &ItemStack, state: steel_utils::BlockStateId, world: &Arc, pos: BlockPos, _player: &player::Player, _hand: types::InteractionHand, _hit_result: &BlockHitResult, + inv: &mut InventoryAccess, ) -> InteractionResult { + let item_stack = inv.item(); if item_stack.is_empty() { if !state.get_value(&LIT_PROPERTY) { return InteractionResult::Pass; diff --git a/steel-core/src/behavior/blocks/decoration/candle_cake_block.rs b/steel-core/src/behavior/blocks/decoration/candle_cake_block.rs new file mode 100644 index 00000000000..82e4439ec84 --- /dev/null +++ b/steel-core/src/behavior/blocks/decoration/candle_cake_block.rs @@ -0,0 +1,160 @@ +use std::sync::Arc; + +use steel_macros::block_behavior; +use steel_registry::{ + REGISTRY, TaggedRegistryExt, + blocks::{BlockRef, block_state_ext::BlockStateExt, properties::BlockStateProperties}, + item_stack::ItemStack, + items::item::BlockHitResult, + sound_events, vanilla_block_tags, vanilla_blocks, vanilla_items, +}; +use steel_utils::{ + BlockPos, BlockStateId, Direction, + types::{InteractionHand, UpdateFlags}, +}; + +use crate::{ + behavior::{ + BlockBehavior, BlockPlaceContext, InteractionResult, InventoryAccess, blocks::CakeBlock, + }, + player::Player, + world::World, +}; + +/// Behavior for Candle Cakes +/// TODO: +/// - [ ] animation ticks +/// - [ ] onProjectile +/// - [ ] onExplosion +#[block_behavior] +pub struct CandleCakeBlock { + block: BlockRef, +} + +impl CandleCakeBlock { + /// Creates a new Candle Cake Block Behavior + #[must_use] + pub const fn new(block: BlockRef) -> Self { + Self { block } + } + + /// Whether or not the Candle Cake can be lit + #[must_use] + pub fn can_light(state: BlockStateId) -> bool { + REGISTRY + .blocks + .is_in_tag(state.get_block(), &vanilla_block_tags::CANDLE_CAKES_TAG) + && !state + .try_get_value(&BlockStateProperties::LIT) + .unwrap_or(true) + } +} + +impl BlockBehavior for CandleCakeBlock { + fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option { + if context + .world + .get_block_state(context.relative_pos.below()) + .is_solid() + { + Some(self.block.default_state()) + } else { + None + } + } + + fn use_item_on( + &self, + state: BlockStateId, + world: &Arc, + pos: BlockPos, + player: &Player, + _hand: InteractionHand, + hit_result: &BlockHitResult, + inv: &mut InventoryAccess, + ) -> InteractionResult { + let item_stack = inv.item(); + if item_stack.is(&vanilla_items::ITEMS.fire_charge) + || item_stack.is(&vanilla_items::ITEMS.flint_and_steel) + { + return InteractionResult::Pass; // lighting of candles and candle cakes is handled by the flint and steel/fire charge implementation + } else if (hit_result.location.y - f64::from(hit_result.block_pos.y())) > 0.5 + && item_stack.is_empty() + && state.get_value(&BlockStateProperties::LIT) + { + world.set_block( + pos, + state.set_value(&BlockStateProperties::LIT, false), + UpdateFlags::UPDATE_ALL, + ); + // TODO: particles! + world.play_block_sound( + sound_events::BLOCK_CANDLE_EXTINGUISH, + pos, + 1.0, + 1.0, + Some(player.id), + ); + return InteractionResult::Success; + } + InteractionResult::TryEmptyHandInteraction + } + + fn use_without_item( + &self, + state: BlockStateId, + world: &Arc, + pos: BlockPos, + player: &Player, + _hit_result: &BlockHitResult, + _inv: &mut InventoryAccess, + ) -> InteractionResult { + let result = CakeBlock::eat(world, pos, vanilla_blocks::CAKE.default_state(), player); + if result.consumes_action() { + world.drop_resources(state, pos); + } + result + } + + fn can_survive(&self, _state: BlockStateId, world: &Arc, pos: BlockPos) -> bool { + world.get_block_state(pos.below()).is_solid() + } + + fn update_shape( + &self, + state: BlockStateId, + world: &Arc, + pos: BlockPos, + direction: steel_utils::Direction, + _neighbor_pos: BlockPos, + _neighbor_state: BlockStateId, + ) -> BlockStateId { + if direction == Direction::Down && !self.can_survive(state, world, pos) { + vanilla_blocks::AIR.default_state() + } else { + state + } + } + + fn get_clone_item_stack( + &self, + _block: BlockRef, + _state: BlockStateId, + _include_data: bool, + ) -> Option { + Some(ItemStack::new(&vanilla_items::ITEMS.cake)) + } + + fn get_analog_output_signal( + &self, + _state: BlockStateId, + _world: &Arc, + _pos: BlockPos, + ) -> i32 { + CakeBlock::analog_output_signal(0) + } + + fn has_analog_output_signal(&self, _state: BlockStateId) -> bool { + true + } +} diff --git a/steel-core/src/behavior/blocks/decoration/mod.rs b/steel-core/src/behavior/blocks/decoration/mod.rs index cb48d0136fa..06dcd07e43c 100644 --- a/steel-core/src/behavior/blocks/decoration/mod.rs +++ b/steel-core/src/behavior/blocks/decoration/mod.rs @@ -1,8 +1,12 @@ +mod cake_block; mod candle_block; +mod candle_cake_block; mod sign_block; mod torch_block; +pub use cake_block::CakeBlock; pub use candle_block::CandleBlock; +pub use candle_cake_block::CandleCakeBlock; pub use sign_block::{ CeilingHangingSignBlock, StandingSignBlock, WallHangingSignBlock, WallSignBlock, }; diff --git a/steel-core/src/behavior/blocks/decoration/sign_block.rs b/steel-core/src/behavior/blocks/decoration/sign_block.rs index cef18838cca..c56fbfbd2c9 100644 --- a/steel-core/src/behavior/blocks/decoration/sign_block.rs +++ b/steel-core/src/behavior/blocks/decoration/sign_block.rs @@ -16,6 +16,7 @@ use steel_registry::vanilla_blocks; use steel_utils::locks::SyncMutex; use steel_utils::{BlockPos, BlockStateId}; +use crate::behavior::InventoryAccess; use crate::behavior::block::BlockBehavior; use crate::behavior::context::{BlockHitResult, BlockPlaceContext, InteractionResult}; use crate::block_entity::SharedBlockEntity; @@ -348,6 +349,7 @@ impl BlockBehavior for StandingSignBlock { pos: BlockPos, player: &Player, _hit_result: &BlockHitResult, + _inv: &mut InventoryAccess, ) -> InteractionResult { try_open_sign_editor(state, world, pos, player) } @@ -436,6 +438,7 @@ impl BlockBehavior for WallSignBlock { pos: BlockPos, player: &Player, _hit_result: &BlockHitResult, + _inv: &mut InventoryAccess, ) -> InteractionResult { try_open_sign_editor(state, world, pos, player) } @@ -558,6 +561,7 @@ impl BlockBehavior for CeilingHangingSignBlock { pos: BlockPos, player: &Player, _hit_result: &BlockHitResult, + _inv: &mut InventoryAccess, ) -> InteractionResult { try_open_sign_editor(state, world, pos, player) } @@ -664,6 +668,7 @@ impl BlockBehavior for WallHangingSignBlock { pos: BlockPos, player: &Player, _hit_result: &BlockHitResult, + _inv: &mut InventoryAccess, ) -> InteractionResult { try_open_sign_editor(state, world, pos, player) } diff --git a/steel-core/src/behavior/blocks/mod.rs b/steel-core/src/behavior/blocks/mod.rs index 5237595a796..23b4dcbcf39 100644 --- a/steel-core/src/behavior/blocks/mod.rs +++ b/steel-core/src/behavior/blocks/mod.rs @@ -12,12 +12,13 @@ mod portal; mod redstone; pub use building::{ - FenceBlock, RotatedPillarBlock, WeatherState, WeatheringCopper, WeatheringCopperFullBlock, + CampfireBlock, FenceBlock, RotatedPillarBlock, WeatherState, WeatheringCopper, + WeatheringCopperFullBlock, }; pub use container::{BarrelBlock, CraftingTableBlock}; pub use decoration::{ - CandleBlock, CeilingHangingSignBlock, StandingSignBlock, TorchBlock, WallHangingSignBlock, - WallSignBlock, WallTorchBlock, + CakeBlock, CandleBlock, CandleCakeBlock, CeilingHangingSignBlock, StandingSignBlock, + TorchBlock, WallHangingSignBlock, WallSignBlock, WallTorchBlock, }; pub use farming::{CactusBlock, CactusFlowerBlock, CropBlock, FarmlandBlock}; pub use fluid::LiquidBlock; diff --git a/steel-core/src/behavior/blocks/redstone/button_block.rs b/steel-core/src/behavior/blocks/redstone/button_block.rs index bfc5eb46a8a..7ef92723ba8 100644 --- a/steel-core/src/behavior/blocks/redstone/button_block.rs +++ b/steel-core/src/behavior/blocks/redstone/button_block.rs @@ -17,6 +17,7 @@ use steel_utils::math::Axis; use steel_utils::types::UpdateFlags; use steel_utils::{BlockPos, BlockStateId}; +use crate::behavior::InventoryAccess; use crate::behavior::block::BlockBehavior; use crate::behavior::context::{BlockHitResult, BlockPlaceContext, InteractionResult}; use crate::player::Player; @@ -154,6 +155,7 @@ impl BlockBehavior for ButtonBlock { pos: BlockPos, player: &Player, _hit_result: &BlockHitResult, + _inv: &mut InventoryAccess, ) -> InteractionResult { let powered: bool = state.get_value(&BlockStateProperties::POWERED); if powered { diff --git a/steel-core/src/behavior/context.rs b/steel-core/src/behavior/context.rs index c0cb698e2a5..cefe7e4fe32 100644 --- a/steel-core/src/behavior/context.rs +++ b/steel-core/src/behavior/context.rs @@ -116,7 +116,19 @@ pub struct InventoryAccess<'a> { inv_id: ContainerId, } -impl InventoryAccess<'_> { +impl<'a> InventoryAccess<'a> { + /// Creates a new `InventoryAccess` instance. + pub const fn new( + inv_guard: &'a mut ContainerLockGuard, + hand: InteractionHand, + inv_id: ContainerId, + ) -> Self { + Self { + inv_guard, + hand, + inv_id, + } + } /// Returns a mutable reference to the item in the player's hand. /// /// Cannot be held simultaneously with `inventory()` or `guard()`. diff --git a/steel-core/src/behavior/items/flint_and_steel.rs b/steel-core/src/behavior/items/flint_and_steel.rs index 3016df786f6..ebd417e4eac 100644 --- a/steel-core/src/behavior/items/flint_and_steel.rs +++ b/steel-core/src/behavior/items/flint_and_steel.rs @@ -1,9 +1,11 @@ //! Flint and steel item behavior with portal ignition. -use crate::behavior::blocks::FireBlock; +use crate::behavior::blocks::{CampfireBlock, CandleBlock, CandleCakeBlock, FireBlock}; use crate::behavior::context::{InteractionResult, UseOnContext}; use crate::behavior::item::ItemBehavior; use steel_macros::item_behavior; +use steel_registry::blocks::block_state_ext::BlockStateExt; +use steel_registry::blocks::properties::BlockStateProperties; use steel_registry::sound_events; use steel_registry::vanilla_blocks::FIRE; use steel_utils::Direction; @@ -15,9 +17,33 @@ pub struct FlintAndSteelItem; impl ItemBehavior for FlintAndSteelItem { fn use_on(&self, context: &mut UseOnContext) -> InteractionResult { - // TODO: light campfires, candles, and candle cakes (set LIT=true) before fire placement - let click_pos = context.hit_result.block_pos; + + let clicked_state = context.world.get_block_state(click_pos); + + if CampfireBlock::can_light(clicked_state) + || CandleBlock::can_light(clicked_state) + || CandleCakeBlock::can_light(clicked_state) + { + context.world.play_block_sound( + sound_events::ITEM_FLINTANDSTEEL_USE, + click_pos, + 1.0, + rand::random::() * 0.4 + 0.8, + Some(context.player.id), + ); + context.world.set_block( + click_pos, + clicked_state.set_value(&BlockStateProperties::LIT, true), + UpdateFlags::UPDATE_ALL_IMMEDIATE, + ); + context + .inv + .item() + .hurt_and_break(1, context.player.has_infinite_materials()); + return InteractionResult::Success; + } + let fire_pos = click_pos.relative(context.hit_result.direction); let (yaw, _) = context.player.rotation.load(); let forward_dir = Direction::from_yaw(yaw); diff --git a/steel-core/src/behavior/items/shovel.rs b/steel-core/src/behavior/items/shovel.rs index 9d5bd1eba6c..74751478bf5 100644 --- a/steel-core/src/behavior/items/shovel.rs +++ b/steel-core/src/behavior/items/shovel.rs @@ -6,7 +6,7 @@ use steel_registry::{ block_state_ext::BlockStateExt, properties::{BlockStateProperties, BoolProperty}, }, - vanilla_block_tags, vanilla_blocks, + sound_events, vanilla_block_tags, vanilla_blocks, }; use steel_utils::Direction; use steel_utils::types::UpdateFlags; @@ -37,7 +37,6 @@ impl ItemBehavior for ShovelItem { let block_state = context.world.get_block_state(context.hit_result.block_pos); let block = block_state.get_block(); - // Flattenables — vanilla checks these first if FLATTENABLES.contains(&block) { if !context .world @@ -46,9 +45,17 @@ impl ItemBehavior for ShovelItem { { return InteractionResult::Pass; } - // TODO: Play SoundEvents.SHOVEL_FLATTEN - let infinite_materials = context.player.has_infinite_materials(); - context.inv.item().hurt_and_break(1, infinite_materials); + context.world.play_block_sound( + sound_events::ITEM_SHOVEL_FLATTEN, + context.hit_result.block_pos, + 1.0, + 1.0, + None, + ); + context + .inv + .item() + .hurt_and_break(1, context.player.has_infinite_materials()); context.world.set_block( context.hit_result.block_pos, vanilla_blocks::DIRT_PATH.default_state(), @@ -58,7 +65,6 @@ impl ItemBehavior for ShovelItem { return InteractionResult::Success; } - // Campfire extinguishing if REGISTRY .blocks .is_in_tag(block, &vanilla_block_tags::CAMPFIRES_TAG) @@ -66,15 +72,15 @@ impl ItemBehavior for ShovelItem { if !block_state.get_value(&LIT_PROPERTY) { return InteractionResult::Pass; } - // TODO: level_event(1009, pos, 0) — extinguish particle/sound - // TODO: CampfireBlock::dowse() — eject cooking items context.world.set_block( context.hit_result.block_pos, block_state.set_value(&LIT_PROPERTY, false), UpdateFlags::UPDATE_ALL_IMMEDIATE, ); - // TODO: hurt_and_break(1, ...) — shovels take durability damage - // TODO: Emit GameEvent::BLOCK_CHANGE + context + .inv + .item() + .hurt_and_break(1, context.player.has_infinite_materials()); return InteractionResult::Success; } diff --git a/steel-core/src/behavior/mod.rs b/steel-core/src/behavior/mod.rs index e9150bfa872..960f30d63b5 100644 --- a/steel-core/src/behavior/mod.rs +++ b/steel-core/src/behavior/mod.rs @@ -38,6 +38,11 @@ pub mod block_behaviors; #[expect(warnings)] #[rustfmt::skip] +#[path = "generated/candle_cakes.rs"] +pub mod candle_cakes; + +#[allow(warnings)] +#[rustfmt::skip] #[path = "generated/items.rs"] pub mod item_behaviors; diff --git a/steel-core/src/block_entity/entities/campfire.rs b/steel-core/src/block_entity/entities/campfire.rs new file mode 100644 index 00000000000..83d376fbb60 --- /dev/null +++ b/steel-core/src/block_entity/entities/campfire.rs @@ -0,0 +1,292 @@ +use std::{ + any::Any, + ops::Sub, + sync::{Arc, Weak}, +}; + +use simdnbt::borrow::{BaseNbtCompound as BorrowedNbtCompound, NbtCompound as NbtCompoundView}; +use simdnbt::{ + ToNbtTag, + owned::{NbtCompound, NbtList, NbtTag}, +}; +use steel_registry::{ + REGISTRY, + block_entity_type::BlockEntityTypeRef, + blocks::{block_state_ext::BlockStateExt, properties::BlockStateProperties}, + item_stack::ItemStack, + vanilla_block_entity_types, +}; +use steel_utils::{BlockPos, BlockStateId}; + +use crate::{block_entity::BlockEntity, inventory::container::Container, world::World}; + +/// Number of slots in a campfire. +pub const CAMPFIRE_SLOTS: usize = 4; + +/// Campfire Block Entity +pub struct CampfireBlockEntity { + world: Weak, + pos: BlockPos, + state: BlockStateId, + removed: bool, + items: Vec, + cooking_times: Vec, + cooking_progress: Vec, +} + +impl CampfireBlockEntity { + /// Creates a new campfire block entity. + #[must_use] + pub fn new(world: Weak, pos: BlockPos, state: BlockStateId) -> Self { + Self { + world, + pos, + state, + removed: false, + items: vec![ItemStack::empty(); CAMPFIRE_SLOTS], + cooking_times: vec![0; CAMPFIRE_SLOTS], + cooking_progress: vec![0; CAMPFIRE_SLOTS], + } + } + + /// Places food in an available slot in the campfire + pub fn place_food(&mut self, input: &mut ItemStack, has_infinite_materials: bool) -> bool { + for ((item, cooking_time), cooking_progress) in self + .items + .iter_mut() + .zip(self.cooking_times.iter_mut()) + .zip(self.cooking_progress.iter_mut()) + { + if !item.is_empty() { + continue; + } + + let Some(recipe) = REGISTRY + .recipes + .iter_campfire() + .find(|recipe| recipe.matches(input)) + else { + return false; + }; + + *cooking_time = recipe.cooking_time; + *cooking_progress = 0; + *item = input.copy_with_count(1); + if !has_infinite_materials { + input.shrink(1); + } + self.set_changed(); + return true; + } + + false + } + + fn mark_updated(&self) { + self.set_changed(); + + let Some(world) = self.world.upgrade() else { + return; + }; + let mut nbt = NbtCompound::new(); + self.save_additional(&mut nbt); + world.broadcast_block_entity_update(self.pos, self.get_type(), nbt); + } +} + +impl BlockEntity for CampfireBlockEntity { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn get_type(&self) -> BlockEntityTypeRef { + &vanilla_block_entity_types::CAMPFIRE + } + + fn get_block_pos(&self) -> BlockPos { + self.pos + } + + fn get_block_state(&self) -> BlockStateId { + self.state + } + + fn set_block_state(&mut self, state: BlockStateId) { + self.state = state; + } + + fn is_removed(&self) -> bool { + self.removed + } + + fn set_removed(&mut self) { + self.removed = true; + } + + fn clear_removed(&mut self) { + self.removed = false; + } + + fn get_level(&self) -> Option> { + self.world.upgrade() + } + + fn pre_remove_side_effects(&mut self, pos: BlockPos, _state: BlockStateId) { + if let Some(world) = self.world.upgrade() { + for item in self.items.drain(..) { + world.drop_item_stack(pos, item); + } + } + } + + fn load_additional(&mut self, nbt: &BorrowedNbtCompound<'_>) { + let nbt_view: NbtCompoundView<'_, '_> = nbt.into(); + + if let Some(items_list) = nbt_view.list("Items") + && let Some(compounds) = items_list.compounds() + { + for compound in compounds { + if let Some(slot) = compound.byte("Slot") { + let slot = slot as usize; + if slot < CAMPFIRE_SLOTS + && let Some(item) = ItemStack::from_borrowed_compound(&compound) + { + self.items[slot] = item; + } + } + } + } + + if let Some(cooking_progress) = nbt_view.int_array("CookingTimes") { + self.cooking_progress = cooking_progress; + } + + if let Some(cooking_times) = nbt_view.int_array("CookingTotalTimes") { + self.cooking_times = cooking_times; + } + } + + fn save_additional(&self, nbt: &mut NbtCompound) { + let mut items: Vec = Vec::new(); + for (slot, item) in self.items.iter().enumerate() { + if !item.is_empty() + && let NbtTag::Compound(mut item_nbt) = item.clone().to_nbt_tag() + { + item_nbt.insert("Slot", slot as i8); + items.push(item_nbt); + } + } + nbt.insert("Items", NbtList::Compound(items)); + nbt.insert( + "CookingTimes", + NbtTag::IntArray(self.cooking_progress.clone()), + ); + nbt.insert( + "CookingTotalTimes", + NbtTag::IntArray(self.cooking_times.clone()), + ); + } + + fn tick(&mut self, world: &Arc) { + let mut changed = false; + if self.state.get_value(&BlockStateProperties::LIT) { + for ((item, cooking_time), cooking_progress) in self + .items + .iter_mut() + .zip(self.cooking_times.iter_mut()) + .zip(self.cooking_progress.iter_mut()) + { + if item.is_empty() { + continue; + } + + *cooking_progress += 1; + if cooking_progress < cooking_time { + if cooking_progress == &1 { + changed = true; + } + continue; + } + + let Some(recipe) = REGISTRY + .recipes + .iter_campfire() + .find(|recipe| recipe.matches(item)) + else { + continue; + }; + world.drop_item_stack(self.pos, recipe.assemble()); + *item = ItemStack::empty(); + *cooking_time = 0; + *cooking_progress = 0; + changed = true; + } + } else { + for (cooking_time, cooking_progress) in self + .cooking_times + .iter_mut() + .zip(self.cooking_progress.iter_mut()) + { + if *cooking_progress > 0 { + changed = true; + *cooking_progress = cooking_progress.sub(2).clamp(0, *cooking_time); + } + } + } + + if changed { + self.mark_updated(); + } + } + + fn is_ticking(&self) -> bool { + true + } + + fn as_container(&self) -> Option<&(dyn Container + 'static)> { + Some(self) + } + + fn as_container_mut(&mut self) -> Option<&mut (dyn Container + 'static)> { + Some(self) + } + + fn get_update_tag(&self) -> Option { + let mut nbt = NbtCompound::new(); + self.save_additional(&mut nbt); + Some(nbt) + } +} + +impl Container for CampfireBlockEntity { + fn get_container_size(&self) -> usize { + CAMPFIRE_SLOTS + } + + fn get_item(&self, slot: usize) -> &ItemStack { + &self.items[slot] + } + + fn get_item_mut(&mut self, slot: usize) -> &mut ItemStack { + &mut self.items[slot] + } + + fn set_item(&mut self, slot: usize, stack: ItemStack) { + if slot < CAMPFIRE_SLOTS { + self.items[slot] = stack; + self.set_changed(); + } + } + + fn get_max_stack_size(&self) -> i32 { + 1 + } + + fn set_changed(&mut self) { + BlockEntity::set_changed(self); + } +} diff --git a/steel-core/src/block_entity/entities/mod.rs b/steel-core/src/block_entity/entities/mod.rs index 3b7a6ba8ee0..b2249d61372 100644 --- a/steel-core/src/block_entity/entities/mod.rs +++ b/steel-core/src/block_entity/entities/mod.rs @@ -1,7 +1,9 @@ //! Block entity implementations. mod barrel; +mod campfire; mod sign; pub use barrel::{BARREL_SLOTS, BarrelBlockEntity}; +pub use campfire::{CAMPFIRE_SLOTS, CampfireBlockEntity}; pub use sign::{SIGN_LINES, SignBlockEntity, SignText}; diff --git a/steel-core/src/block_entity/registry.rs b/steel-core/src/block_entity/registry.rs index aac7d9fbc96..fbfb9e001a8 100644 --- a/steel-core/src/block_entity/registry.rs +++ b/steel-core/src/block_entity/registry.rs @@ -12,6 +12,7 @@ use steel_utils::{BlockPos, BlockStateId}; use super::SharedBlockEntity; use super::entities::{BarrelBlockEntity, SignBlockEntity}; +use crate::block_entity::entities::CampfireBlockEntity; use crate::world::World; /// Factory function type for creating block entities. @@ -151,6 +152,11 @@ pub fn init_block_entities() { Arc::new(SyncMutex::new(BarrelBlockEntity::new(level, pos, state))) }); + registry.register( + &vanilla_block_entity_types::CAMPFIRE, + |level, pos, state| Arc::new(SyncMutex::new(CampfireBlockEntity::new(level, pos, state))), + ); + assert!( BLOCK_ENTITIES.set(registry).is_ok(), "Block entity registry already initialized" diff --git a/steel-core/src/entity/mod.rs b/steel-core/src/entity/mod.rs index 03021abcbb7..7b9f20c717c 100644 --- a/steel-core/src/entity/mod.rs +++ b/steel-core/src/entity/mod.rs @@ -184,6 +184,11 @@ pub trait Entity: Send + Sync { None } + /// Gets the entity as a `dyn LivingEntity` + fn as_living(&self) -> Option<&dyn LivingEntity> { + None + } + /// Gets the entity's rotation as (yaw, pitch) in degrees. /// /// Yaw is horizontal rotation (0-360), pitch is vertical (-90 to 90). diff --git a/steel-core/src/player/game_mode.rs b/steel-core/src/player/game_mode.rs index 2bc6f345245..4111e48e52c 100644 --- a/steel-core/src/player/game_mode.rs +++ b/steel-core/src/player/game_mode.rs @@ -9,7 +9,8 @@ use steel_registry::REGISTRY; use steel_utils::types::{GameType, InteractionHand}; use crate::behavior::{ - BLOCK_BEHAVIORS, BlockHitResult, ITEM_BEHAVIORS, InteractionResult, UseOnContext, + BLOCK_BEHAVIORS, BlockHitResult, ITEM_BEHAVIORS, InteractionResult, InventoryAccess, + UseOnContext, }; use crate::inventory::lock::{ContainerLockGuard, ContainerRef}; use crate::player::Player; @@ -62,11 +63,20 @@ pub fn use_item_on( }; let behavior = block_behaviors.get_behavior(block); - // Brief lock for an immutable snapshot used during block interaction check - let item_snapshot = player.inventory.lock().get_item_in_hand(hand).clone(); - - let block_result = - behavior.use_item_on(&item_snapshot, state, world, pos, player, hand, hit_result); + let inv_ref = ContainerRef::PlayerInventory(player.inventory.clone()); + let mut guard = ContainerLockGuard::lock_all(&[&inv_ref]); + let inv_id = inv_ref.container_id(); + let mut inventory_access = InventoryAccess::new(&mut guard, hand, inv_id); + + let block_result = behavior.use_item_on( + state, + world, + pos, + player, + hand, + hit_result, + &mut inventory_access, + ); if block_result.consumes_action() { return block_result; @@ -75,7 +85,14 @@ pub fn use_item_on( if matches!(block_result, InteractionResult::TryEmptyHandInteraction) && hand == InteractionHand::MainHand { - let empty_result = behavior.use_without_item(state, world, pos, player, hit_result); + let empty_result = behavior.use_without_item( + state, + world, + pos, + player, + hit_result, + &mut inventory_access, + ); if empty_result.consumes_action() { return empty_result; diff --git a/steel-core/src/player/mod.rs b/steel-core/src/player/mod.rs index 479e9e845dc..b7fcbe987e2 100644 --- a/steel-core/src/player/mod.rs +++ b/steel-core/src/player/mod.rs @@ -309,7 +309,7 @@ pub struct Player { living_base: SyncMutex, /// Player food/hunger state (food level, saturation, exhaustion). - food_data: SyncMutex, + pub food_data: SyncMutex, /// Delta-tracking state for `CSetHealth` deduplication. health_sync: SyncMutex, @@ -3174,6 +3174,13 @@ impl Player { } } + /// Returns whether the Player can eat + pub fn can_eat(&self, can_always_eat: bool) -> bool { + let invulnerable = { self.abilities.lock().invulnerable }; + let needs_foods = { self.food_data.lock().needs_food() }; + invulnerable || can_always_eat || needs_foods + } + /// Cleans up player resources. #[expect(clippy::unused_self, reason = "this is an api function")] pub const fn cleanup(&self) {} @@ -3467,6 +3474,9 @@ impl Entity for Player { Player::hurt(self, source, amount) } + fn as_living(&self) -> Option<&dyn LivingEntity> { + Some(self) + } fn change_world(self: Arc, teleport_transition: &TeleportTransition) { let new_world = teleport_transition.target_world.clone(); self.reset(new_world, ResetReason::DimensionChange); diff --git a/steel-core/src/player/player_inventory.rs b/steel-core/src/player/player_inventory.rs index 3890bc14877..b005186af2d 100644 --- a/steel-core/src/player/player_inventory.rs +++ b/steel-core/src/player/player_inventory.rs @@ -116,13 +116,13 @@ impl PlayerInventory { f(&self.items[self.selected as usize]) } - /// Returns a clone of the currently selected item (main hand). + /// Returns the currently selected item (main hand). #[must_use] pub const fn get_selected_item(&self) -> &ItemStack { &self.items[self.selected as usize] } - /// Returns a clone of the currently selected item (main hand). + /// Returns the currently selected item (main hand). #[must_use] pub const fn get_selected_item_mut(&mut self) -> &mut ItemStack { &mut self.items[self.selected as usize] diff --git a/steel-core/src/world/mod.rs b/steel-core/src/world/mod.rs index 8688f29ad43..2d4273ef333 100644 --- a/steel-core/src/world/mod.rs +++ b/steel-core/src/world/mod.rs @@ -1245,7 +1245,7 @@ impl World { /// * `block_entity_type` - The type of block entity /// * `nbt` - The NBT data to send pub fn broadcast_block_entity_update( - &self, + self: &Arc, pos: BlockPos, block_entity_type: BlockEntityTypeRef, nbt: NbtCompound, diff --git a/steel-registry/build/build.rs b/steel-registry/build/build.rs index 052ff15d06a..8fa050e7dc5 100644 --- a/steel-registry/build/build.rs +++ b/steel-registry/build/build.rs @@ -36,6 +36,7 @@ mod painting_variants; mod pig_sound_variants; mod pig_variants; mod poi_types; +mod recipe_property_sets; mod recipes; mod sound_events; mod sound_types; @@ -105,6 +106,7 @@ const MENU_TYPES: &str = "menu_types"; const TIMELINES: &str = "timelines"; const TIMELINE_TAGS: &str = "timeline_tags"; const ZOMBIE_NAUTILUS_VARIANTS: &str = "zombie_nautilus_variants"; +const RECIPE_PROPERTY_SETS: &str = "recipe_property_sets"; const RECIPES: &str = "recipes"; const VANILLA_ENTITIES: &str = "entities"; const ENTITY_DATA: &str = "entity_data"; @@ -172,6 +174,7 @@ pub fn main() { (timelines::build(), TIMELINES), (timeline_tags::build(), TIMELINE_TAGS), (zombie_nautilus_variants::build(), ZOMBIE_NAUTILUS_VARIANTS), + (recipe_property_sets::build(), RECIPE_PROPERTY_SETS), (recipes::build(), RECIPES), (entities::build(), VANILLA_ENTITIES), (entity_data::build(), ENTITY_DATA), diff --git a/steel-registry/build/recipe_property_sets.rs b/steel-registry/build/recipe_property_sets.rs new file mode 100644 index 00000000000..92ebd6e7f68 --- /dev/null +++ b/steel-registry/build/recipe_property_sets.rs @@ -0,0 +1,35 @@ +use std::{collections::BTreeMap, fs}; + +use heck::ToSnakeCase; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; + +pub(crate) fn build() -> TokenStream { + println!("cargo:rerun-if-changed=build_assets/recipe_property_sets.json"); + + let recipe_property_sets_json = fs::read_to_string("build_assets/recipe_property_sets.json") + .expect("Failed to read recipe_property_sets.json"); + let recipe_property_sets_entries: BTreeMap> = + serde_json::from_str(&recipe_property_sets_json) + .expect("Failed to parse recipe_property_sets.json"); + + let recipe_property_sets: Vec = recipe_property_sets_entries + .iter() + .map(|(key, list)| { + let key = Ident::new(&key.to_snake_case().to_uppercase(), Span::call_site()); + let items = list.iter().map(|it| { + let ident = Ident::new(it.strip_prefix("minecraft:").unwrap_or(it), Span::call_site()); + quote! { &ITEMS.#ident } + }); + quote! { + pub static #key: LazyLock<&'static [ItemRef]> = LazyLock::new(|| Box::leak(vec![#(#items),*].into_boxed_slice())); + } + }) + .collect(); + + quote! { + use crate::{items::ItemRef, vanilla_items::ITEMS}; + use std::sync::LazyLock; + #(#recipe_property_sets)* + } +} diff --git a/steel-registry/build/recipes.rs b/steel-registry/build/recipes.rs index b8c85a48cab..ecb91b0c1d8 100644 --- a/steel-registry/build/recipes.rs +++ b/steel-registry/build/recipes.rs @@ -24,14 +24,26 @@ struct RecipeJson { recipe_type: String, #[serde(default)] category: Option, + // Shaped recipe fields #[serde(default)] key: Option>, #[serde(default)] pattern: Option>, + // Shapeless recipe fields #[serde(default)] ingredients: Option>, + + // Smelting recipe fields + #[serde(default)] + ingredient: Option, + + #[serde(default, rename = "cookingtime")] + cooking_time: Option, + #[serde(default)] + experience: Option, + // Common fields #[serde(default)] result: Option, @@ -51,7 +63,7 @@ fn default_count() -> i32 { } /// Represents a parsed ingredient from JSON. -#[derive(Clone)] +#[derive(Clone, Debug)] enum ParsedIngredient { Empty, Item(String), // item identifier @@ -168,12 +180,7 @@ fn parse_shaped_recipe(recipe_name: &str, recipe: &RecipeJson) -> Option quote! { CraftingCategory::Building }, - "redstone" => quote! { CraftingCategory::Redstone }, - "equipment" => quote! { CraftingCategory::Equipment }, - _ => quote! { CraftingCategory::Misc }, - }; + let category = category_to_tokens(category_str); let snake_name = recipe_name.to_snake_case(); @@ -222,12 +229,7 @@ fn parse_shapeless_recipe(recipe_name: &str, recipe: &RecipeJson) -> Option quote! { CraftingCategory::Building }, - "redstone" => quote! { CraftingCategory::Redstone }, - "equipment" => quote! { CraftingCategory::Equipment }, - _ => quote! { CraftingCategory::Misc }, - }; + let category = category_to_tokens(category_str); let snake_name = recipe_name.to_snake_case(); @@ -269,6 +271,184 @@ fn generate_ingredient_tokens(ingredient: &ParsedIngredient) -> TokenStream { } } +fn category_to_tokens(category: &str) -> TokenStream { + match category { + "building" => quote! { CraftingCategory::Building }, + "redstone" => quote! { CraftingCategory::Redstone }, + "equipment" => quote! { CraftingCategory::Equipment }, + "food" => quote! { CraftingCategory::Food }, + _ => quote! { CraftingCategory::Misc }, + } +} + +struct SmeltingRecipeData { + name: String, + ident: Ident, + category: TokenStream, + cooking_time: i32, + experience: f32, + ingredient: ParsedIngredient, + result: Ident, +} + +fn parse_smelting_recipe(recipe_name: &str, data: &RecipeJson) -> Option { + let category = data.category.as_ref()?; + + let category = category_to_tokens(category); + + let cooking_time = data.cooking_time?; + let experience = data.experience?; + let ingredient = data.ingredient.as_ref()?; + let result = data.result.as_ref()?; + + let result_item_id = result.id.strip_prefix("minecraft:").unwrap_or(&result.id); + let result_item_ident = Ident::new(result_item_id, Span::call_site()); + + let ingredient = parse_ingredient(ingredient); + + Some(SmeltingRecipeData { + name: recipe_name.to_string(), + ident: Ident::new(&recipe_name.to_snake_case(), Span::call_site()), + category, + cooking_time, + experience, + ingredient, + result: result_item_ident, + }) +} + +struct RecipeCodegen { + creator_fns: Vec, + fields: Vec, + field_inits: Vec, + registers: Vec, +} + +fn generate_codegen( + prefix: &str, + recipe_type: &str, + recipes: &[(Ident, TokenStream)], +) -> RecipeCodegen { + let type_ident = Ident::new(recipe_type, Span::call_site()); + + let mut cg = RecipeCodegen { + creator_fns: Vec::new(), + fields: Vec::new(), + field_inits: Vec::new(), + registers: Vec::new(), + }; + + for (ident, body) in recipes { + let fn_ident = Ident::new(&format!("create_{prefix}_{ident}"), Span::call_site()); + let register_method = Ident::new(&format!("register_{prefix}"), Span::call_site()); + let prefix_ident = Ident::new(prefix, Span::call_site()); + + cg.creator_fns.push(quote! { + #[inline(never)] + fn #fn_ident() -> #type_ident { #body } + }); + cg.fields.push(quote! { pub #ident: #type_ident, }); + cg.field_inits.push(quote! { #ident: #fn_ident(), }); + cg.registers.push(quote! { + registry.#register_method(&RECIPES.#prefix_ident.#ident); + }); + } + + cg +} + +fn generate_shaped_body(r: &ShapedRecipeData) -> TokenStream { + let name = &r.name; + let category = &r.category; + let width = r.width; + let height = r.height; + let result_item_ident = &r.result_item_ident; + let result_count = r.result_count; + let show_notification = r.show_notification; + let symmetrical = r.symmetrical; + + let pattern_tokens: Vec = r + .pattern_data + .iter() + .map(generate_ingredient_tokens) + .collect(); + + quote! { + let pattern: &'static [Ingredient] = Box::leak( + vec![#(#pattern_tokens),*].into_boxed_slice() + ); + ShapedRecipe { + id: Identifier::vanilla_static(#name), + category: #category, + width: #width, + height: #height, + pattern, + result: RecipeResult { + item: &ITEMS.#result_item_ident, + count: #result_count, + }, + show_notification: #show_notification, + symmetrical: #symmetrical, + } + } +} + +fn generate_shapeless_body(r: &ShapelessRecipeData) -> TokenStream { + let name = &r.name; + let category = &r.category; + let result_item_ident = &r.result_item_ident; + let result_count = r.result_count; + + let ingredient_tokens: Vec = r + .ingredient_data + .iter() + .map(generate_ingredient_tokens) + .collect(); + + quote! { + + // Box::leak creates a &'static [Ingredient] from the Vec. + // This is intentional: vanilla recipes live forever. + let ingredients: &'static [Ingredient] = Box::leak( + vec![#(#ingredient_tokens),*].into_boxed_slice() + ); + ShapelessRecipe { + id: Identifier::vanilla_static(#name), + category: #category, + ingredients, + result: RecipeResult { + item: &ITEMS.#result_item_ident, + count: #result_count, + }, + } + } +} + +fn generate_smelting_body(r: &SmeltingRecipeData) -> TokenStream { + let name = &r.name; + let category = &r.category; + let result_item_ident = &r.result; + + let ingredient_tokens: TokenStream = generate_ingredient_tokens(&r.ingredient); + + let cooking_time = r.cooking_time; + let experience = r.experience; + + quote! { + SmeltingRecipe { + ident: Identifier::vanilla_static(#name), + category: #category, + ingredient: #ingredient_tokens, + result: RecipeResult { + item: &ITEMS.#result_item_ident, + count: 1, + }, + cooking_time: #cooking_time, + experience: #experience, + } + } +} + pub(crate) fn build() -> TokenStream { println!("cargo:rerun-if-changed=build_assets/builtin_datapacks/minecraft/recipe/"); @@ -276,19 +456,25 @@ pub(crate) fn build() -> TokenStream { let mut shaped_recipes: Vec = Vec::new(); let mut shapeless_recipes: Vec = Vec::new(); + let mut smelting_recipes: Vec = Vec::new(); + let mut campfire_recipes: Vec = Vec::new(); + let mut smoking_recipes: Vec = Vec::new(); // Read all recipe files fn read_recipes( dir: &Path, shaped: &mut Vec, shapeless: &mut Vec, + smelting: &mut Vec, + campfire: &mut Vec, + smoking: &mut Vec, ) { for entry in fs::read_dir(dir).unwrap() { let entry = entry.unwrap(); let path = entry.path(); if path.is_dir() { - read_recipes(&path, shaped, shapeless); + read_recipes(&path, shaped, shapeless, smelting, campfire, smoking); } else if path.extension().and_then(|s| s.to_str()) == Some("json") { let recipe_name = path .file_stem() @@ -316,6 +502,21 @@ pub(crate) fn build() -> TokenStream { shapeless.push(r); } } + "minecraft:smelting" => { + if let Some(r) = parse_smelting_recipe(recipe_name, &recipe) { + smelting.push(r); + } + } + "minecraft:campfire_cooking" => { + if let Some(r) = parse_smelting_recipe(recipe_name, &recipe) { + campfire.push(r); + } + } + "minecraft:smoking_cooking" => { + if let Some(r) = parse_smelting_recipe(recipe_name, &recipe) { + smoking.push(r); + } + } // Skip other recipe types for now (smelting, stonecutting, smithing, etc.) _ => {} } @@ -327,153 +528,94 @@ pub(crate) fn build() -> TokenStream { Path::new(recipe_dir), &mut shaped_recipes, &mut shapeless_recipes, + &mut smelting_recipes, + &mut campfire_recipes, + &mut smoking_recipes, ); - // Generate individual creator functions for each shaped recipe. - // Each function creates just one recipe in its own stack frame, - // preventing stack overflow from large struct literals. - // Uses Box::leak to create &'static [Ingredient] slices. - let shaped_creator_fns: Vec = shaped_recipes - .iter() - .map(|r| { - let fn_ident = Ident::new(&format!("create_shaped_{}", r.ident), Span::call_site()); - let name = &r.name; - let category = &r.category; - let width = r.width; - let height = r.height; - let result_item_ident = &r.result_item_ident; - let result_count = r.result_count; - let show_notification = r.show_notification; - let symmetrical = r.symmetrical; - - let pattern_tokens: Vec = r - .pattern_data - .iter() - .map(generate_ingredient_tokens) - .collect(); + let shaped = generate_codegen( + "shaped", + "ShapedRecipe", + &shaped_recipes + .iter() + .map(|recipe| (recipe.ident.clone(), generate_shaped_body(recipe))) + .collect::>(), + ); - quote! { - #[inline(never)] - fn #fn_ident() -> ShapedRecipe { - // Box::leak creates a &'static [Ingredient] from the Vec. - // This is intentional: vanilla recipes live forever. - let pattern: &'static [Ingredient] = Box::leak( - vec![#(#pattern_tokens),*].into_boxed_slice() - ); - ShapedRecipe { - id: Identifier::vanilla_static(#name), - category: #category, - width: #width, - height: #height, - pattern, - result: RecipeResult { - item: &ITEMS.#result_item_ident, - count: #result_count, - }, - show_notification: #show_notification, - symmetrical: #symmetrical, - } - } - } - }) - .collect(); + let shapeless = generate_codegen( + "shapeless", + "ShapelessRecipe", + &shapeless_recipes + .iter() + .map(|recipe| (recipe.ident.clone(), generate_shapeless_body(recipe))) + .collect::>(), + ); - // Generate individual creator functions for each shapeless recipe. - let shapeless_creator_fns: Vec = shapeless_recipes - .iter() - .map(|r| { - let fn_ident = Ident::new(&format!("create_shapeless_{}", r.ident), Span::call_site()); - let name = &r.name; - let category = &r.category; - let result_item_ident = &r.result_item_ident; - let result_count = r.result_count; - - let ingredient_tokens: Vec = r - .ingredient_data - .iter() - .map(generate_ingredient_tokens) - .collect(); + let smelting = generate_codegen( + "smelting", + "SmeltingRecipe", + &smelting_recipes + .iter() + .map(|recipe| (recipe.ident.clone(), generate_smelting_body(recipe))) + .collect::>(), + ); - quote! { - #[inline(never)] - fn #fn_ident() -> ShapelessRecipe { - // Box::leak creates a &'static [Ingredient] from the Vec. - // This is intentional: vanilla recipes live forever. - let ingredients: &'static [Ingredient] = Box::leak( - vec![#(#ingredient_tokens),*].into_boxed_slice() - ); - ShapelessRecipe { - id: Identifier::vanilla_static(#name), - category: #category, - ingredients, - result: RecipeResult { - item: &ITEMS.#result_item_ident, - count: #result_count, - }, - } - } - } - }) - .collect(); + let campfire = generate_codegen( + "campfire", + "SmeltingRecipe", + &campfire_recipes + .iter() + .map(|recipe| (recipe.ident.clone(), generate_smelting_body(recipe))) + .collect::>(), + ); - // Generate struct fields - let shaped_fields: Vec = shaped_recipes - .iter() - .map(|r| { - let ident = &r.ident; - quote! { pub #ident: ShapedRecipe, } - }) - .collect(); + let smoking = generate_codegen( + "smoking", + "SmeltingRecipe", + &smoking_recipes + .iter() + .map(|recipe| (recipe.ident.clone(), generate_smelting_body(recipe))) + .collect::>(), + ); - let shapeless_fields: Vec = shapeless_recipes + let all_creator_fns: Vec<&TokenStream> = shaped + .creator_fns .iter() - .map(|r| { - let ident = &r.ident; - quote! { pub #ident: ShapelessRecipe, } - }) + .chain(shapeless.creator_fns.iter()) + .chain(smelting.creator_fns.iter()) + .chain(campfire.creator_fns.iter()) + .chain(smoking.creator_fns.iter()) .collect(); - // Generate field initializers that call the creator functions - let shaped_field_inits: Vec = shaped_recipes + let all_registers: Vec<&TokenStream> = shaped + .registers .iter() - .map(|r| { - let ident = &r.ident; - let fn_ident = Ident::new(&format!("create_shaped_{}", r.ident), Span::call_site()); - quote! { #ident: #fn_ident(), } - }) + .chain(shapeless.registers.iter()) + .chain(smelting.registers.iter()) + .chain(campfire.registers.iter()) + .chain(smoking.registers.iter()) .collect(); - let shapeless_field_inits: Vec = shapeless_recipes - .iter() - .map(|r| { - let ident = &r.ident; - let fn_ident = Ident::new(&format!("create_shapeless_{}", r.ident), Span::call_site()); - quote! { #ident: #fn_ident(), } - }) - .collect(); + let shaped_fields = &shaped.fields; + let shaped_field_inits = &shaped.field_inits; - // Generate registration calls - let shaped_registers: Vec = shaped_recipes - .iter() - .map(|r| { - let ident = &r.ident; - quote! { registry.register_shaped(&RECIPES.shaped.#ident); } - }) - .collect(); + let shapeless_fields = &shapeless.fields; + let shapeless_field_inits = &shapeless.field_inits; - let shapeless_registers: Vec = shapeless_recipes - .iter() - .map(|r| { - let ident = &r.ident; - quote! { registry.register_shapeless(&RECIPES.shapeless.#ident); } - }) - .collect(); + let smelting_fields = &smelting.fields; + let smelting_field_inits = &smelting.field_inits; + + let campfire_fields = &campfire.fields; + let campfire_field_inits = &campfire.field_inits; + + let smoking_fields = &smoking.fields; + let smoking_field_inits = &smoking.field_inits; quote! { use crate::{ recipe::{ CraftingCategory, Ingredient, RecipeRegistry, RecipeResult, - ShapedRecipe, ShapelessRecipe, + ShapedRecipe, ShapelessRecipe, SmeltingRecipe }, vanilla_items::ITEMS, }; @@ -487,17 +629,18 @@ pub(crate) fn build() -> TokenStream { /// `&'static` slices, providing zero-cost access after initialization. pub static RECIPES: LazyLock = LazyLock::new(Recipes::init); - pub struct ShapedRecipes { - #(#shaped_fields)* - } - - pub struct ShapelessRecipes { - #(#shapeless_fields)* - } + pub struct ShapedRecipes { #(#shaped_fields)* } + pub struct ShapelessRecipes { #(#shapeless_fields)* } + pub struct SmeltingRecipes { #(#smelting_fields)* } + pub struct CampfireRecipes { #(#campfire_fields)* } + pub struct SmokingRecipes { #(#smoking_fields)* } pub struct Recipes { pub shaped: ShapedRecipes, pub shapeless: ShapelessRecipes, + pub smelting: SmeltingRecipes, + pub campfire: CampfireRecipes, + pub smoking: SmokingRecipes, } // Individual recipe creator functions. @@ -511,8 +654,7 @@ pub(crate) fn build() -> TokenStream { // - Vanilla recipes live for the entire program lifetime // - The leaked memory is a one-time cost at startup // - Access to recipe data after init is zero-cost (just pointer + length) - #(#shaped_creator_fns)* - #(#shapeless_creator_fns)* + #(#all_creator_fns)* impl Recipes { fn init() -> Self { @@ -523,6 +665,15 @@ pub(crate) fn build() -> TokenStream { shapeless: ShapelessRecipes { #(#shapeless_field_inits)* }, + smelting: SmeltingRecipes { + #(#smelting_field_inits)* + }, + campfire: CampfireRecipes { + #(#campfire_field_inits)* + }, + smoking: SmokingRecipes { + #(#smoking_field_inits)* + } } } } @@ -531,8 +682,7 @@ pub(crate) fn build() -> TokenStream { pub fn register_recipes(registry: &mut RecipeRegistry) { // Force initialization of RECIPES let _ = &*RECIPES; - #(#shaped_registers)* - #(#shapeless_registers)* + #(#all_registers)* } } } diff --git a/steel-registry/build_assets/recipe_property_sets.json b/steel-registry/build_assets/recipe_property_sets.json new file mode 100644 index 00000000000..b9e62f6f787 --- /dev/null +++ b/steel-registry/build_assets/recipe_property_sets.json @@ -0,0 +1,319 @@ +{ + "campfire_input": [ + "minecraft:beef", + "minecraft:kelp", + "minecraft:salmon", + "minecraft:chicken", + "minecraft:cod", + "minecraft:rabbit", + "minecraft:potato", + "minecraft:porkchop", + "minecraft:mutton" + ], + "blast_furnace_input": [ + "minecraft:iron_hoe", + "minecraft:golden_leggings", + "minecraft:golden_sword", + "minecraft:golden_spear", + "minecraft:golden_shovel", + "minecraft:deepslate_diamond_ore", + "minecraft:chainmail_chestplate", + "minecraft:copper_axe", + "minecraft:deepslate_coal_ore", + "minecraft:copper_sword", + "minecraft:golden_nautilus_armor", + "minecraft:copper_ore", + "minecraft:ancient_debris", + "minecraft:deepslate_lapis_ore", + "minecraft:iron_shovel", + "minecraft:raw_copper", + "minecraft:deepslate_iron_ore", + "minecraft:golden_chestplate", + "minecraft:golden_hoe", + "minecraft:copper_leggings", + "minecraft:iron_pickaxe", + "minecraft:golden_pickaxe", + "minecraft:lapis_ore", + "minecraft:chainmail_boots", + "minecraft:iron_axe", + "minecraft:copper_horse_armor", + "minecraft:raw_iron", + "minecraft:iron_spear", + "minecraft:redstone_ore", + "minecraft:copper_boots", + "minecraft:iron_chestplate", + "minecraft:nether_gold_ore", + "minecraft:iron_nautilus_armor", + "minecraft:copper_chestplate", + "minecraft:copper_nautilus_armor", + "minecraft:copper_hoe", + "minecraft:golden_helmet", + "minecraft:deepslate_redstone_ore", + "minecraft:iron_boots", + "minecraft:iron_sword", + "minecraft:deepslate_copper_ore", + "minecraft:raw_gold", + "minecraft:golden_boots", + "minecraft:copper_helmet", + "minecraft:coal_ore", + "minecraft:deepslate_emerald_ore", + "minecraft:golden_horse_armor", + "minecraft:iron_leggings", + "minecraft:nether_quartz_ore", + "minecraft:chainmail_leggings", + "minecraft:golden_axe", + "minecraft:emerald_ore", + "minecraft:copper_pickaxe", + "minecraft:copper_shovel", + "minecraft:copper_spear", + "minecraft:iron_horse_armor", + "minecraft:iron_ore", + "minecraft:chainmail_helmet", + "minecraft:diamond_ore", + "minecraft:deepslate_gold_ore", + "minecraft:iron_helmet", + "minecraft:gold_ore" + ], + "furnace_input": [ + "minecraft:deepslate_iron_ore", + "minecraft:birch_log", + "minecraft:cactus", + "minecraft:cobblestone", + "minecraft:basalt", + "minecraft:chainmail_helmet", + "minecraft:mangrove_wood", + "minecraft:chorus_fruit", + "minecraft:raw_iron", + "minecraft:golden_chestplate", + "minecraft:mutton", + "minecraft:deepslate_gold_ore", + "minecraft:stripped_acacia_wood", + "minecraft:copper_boots", + "minecraft:stripped_acacia_log", + "minecraft:flowering_azalea_leaves", + "minecraft:pale_oak_wood", + "minecraft:stripped_jungle_wood", + "minecraft:stripped_spruce_wood", + "minecraft:deepslate_coal_ore", + "minecraft:cherry_leaves", + "minecraft:polished_blackstone_bricks", + "minecraft:stripped_pale_oak_wood", + "minecraft:golden_horse_armor", + "minecraft:jungle_log", + "minecraft:copper_chestplate", + "minecraft:oak_leaves", + "minecraft:copper_hoe", + "minecraft:spruce_leaves", + "minecraft:copper_ore", + "minecraft:pink_terracotta", + "minecraft:stone_bricks", + "minecraft:iron_shovel", + "minecraft:stripped_birch_wood", + "minecraft:iron_spear", + "minecraft:acacia_wood", + "minecraft:pale_oak_leaves", + "minecraft:iron_helmet", + "minecraft:acacia_log", + "minecraft:oak_wood", + "minecraft:cod", + "minecraft:emerald_ore", + "minecraft:copper_spear", + "minecraft:golden_pickaxe", + "minecraft:stripped_spruce_log", + "minecraft:dark_oak_wood", + "minecraft:stripped_oak_wood", + "minecraft:kelp", + "minecraft:green_terracotta", + "minecraft:potato", + "minecraft:deepslate_lapis_ore", + "minecraft:light_gray_terracotta", + "minecraft:iron_pickaxe", + "minecraft:diamond_ore", + "minecraft:lapis_ore", + "minecraft:stripped_mangrove_log", + "minecraft:stripped_dark_oak_wood", + "minecraft:deepslate_bricks", + "minecraft:yellow_terracotta", + "minecraft:nether_bricks", + "minecraft:cherry_log", + "minecraft:nether_quartz_ore", + "minecraft:copper_leggings", + "minecraft:clay_ball", + "minecraft:orange_terracotta", + "minecraft:iron_sword", + "minecraft:beef", + "minecraft:deepslate_emerald_ore", + "minecraft:golden_spear", + "minecraft:dark_oak_log", + "minecraft:quartz_block", + "minecraft:stripped_oak_log", + "minecraft:copper_pickaxe", + "minecraft:dark_oak_leaves", + "minecraft:white_terracotta", + "minecraft:copper_helmet", + "minecraft:chainmail_boots", + "minecraft:raw_copper", + "minecraft:pale_oak_log", + "minecraft:spruce_wood", + "minecraft:brown_terracotta", + "minecraft:copper_shovel", + "minecraft:copper_sword", + "minecraft:cyan_terracotta", + "minecraft:netherrack", + "minecraft:iron_horse_armor", + "minecraft:sea_pickle", + "minecraft:golden_nautilus_armor", + "minecraft:mangrove_leaves", + "minecraft:sand", + "minecraft:porkchop", + "minecraft:coal_ore", + "minecraft:iron_axe", + "minecraft:iron_chestplate", + "minecraft:stone", + "minecraft:lime_terracotta", + "minecraft:birch_wood", + "minecraft:golden_boots", + "minecraft:rabbit", + "minecraft:deepslate_tiles", + "minecraft:stripped_pale_oak_log", + "minecraft:copper_horse_armor", + "minecraft:acacia_leaves", + "minecraft:wet_sponge", + "minecraft:stripped_jungle_log", + "minecraft:blue_terracotta", + "minecraft:deepslate_redstone_ore", + "minecraft:birch_leaves", + "minecraft:clay", + "minecraft:golden_axe", + "minecraft:red_sand", + "minecraft:purple_terracotta", + "minecraft:golden_helmet", + "minecraft:redstone_ore", + "minecraft:magenta_terracotta", + "minecraft:jungle_wood", + "minecraft:chainmail_leggings", + "minecraft:salmon", + "minecraft:stripped_cherry_wood", + "minecraft:nether_gold_ore", + "minecraft:spruce_log", + "minecraft:iron_boots", + "minecraft:black_terracotta", + "minecraft:deepslate_diamond_ore", + "minecraft:raw_gold", + "minecraft:iron_ore", + "minecraft:sandstone", + "minecraft:cherry_wood", + "minecraft:light_blue_terracotta", + "minecraft:copper_nautilus_armor", + "minecraft:mangrove_log", + "minecraft:stripped_mangrove_wood", + "minecraft:red_sandstone", + "minecraft:golden_shovel", + "minecraft:ancient_debris", + "minecraft:golden_sword", + "minecraft:red_terracotta", + "minecraft:iron_hoe", + "minecraft:gray_terracotta", + "minecraft:stripped_dark_oak_log", + "minecraft:resin_clump", + "minecraft:azalea_leaves", + "minecraft:stripped_cherry_log", + "minecraft:gold_ore", + "minecraft:oak_log", + "minecraft:iron_nautilus_armor", + "minecraft:golden_hoe", + "minecraft:cobbled_deepslate", + "minecraft:iron_leggings", + "minecraft:jungle_leaves", + "minecraft:deepslate_copper_ore", + "minecraft:stripped_birch_log", + "minecraft:golden_leggings", + "minecraft:copper_axe", + "minecraft:chainmail_chestplate", + "minecraft:chicken" + ], + "smoker_input": [ + "minecraft:beef", + "minecraft:kelp", + "minecraft:salmon", + "minecraft:chicken", + "minecraft:cod", + "minecraft:rabbit", + "minecraft:potato", + "minecraft:porkchop", + "minecraft:mutton" + ], + "smithing_addition": [ + "minecraft:copper_ingot", + "minecraft:netherite_ingot", + "minecraft:quartz", + "minecraft:emerald", + "minecraft:redstone", + "minecraft:lapis_lazuli", + "minecraft:resin_brick", + "minecraft:amethyst_shard", + "minecraft:iron_ingot", + "minecraft:diamond", + "minecraft:gold_ingot" + ], + "smithing_template": [ + "minecraft:eye_armor_trim_smithing_template", + "minecraft:sentry_armor_trim_smithing_template", + "minecraft:wild_armor_trim_smithing_template", + "minecraft:rib_armor_trim_smithing_template", + "minecraft:bolt_armor_trim_smithing_template", + "minecraft:wayfinder_armor_trim_smithing_template", + "minecraft:flow_armor_trim_smithing_template", + "minecraft:snout_armor_trim_smithing_template", + "minecraft:vex_armor_trim_smithing_template", + "minecraft:spire_armor_trim_smithing_template", + "minecraft:ward_armor_trim_smithing_template", + "minecraft:coast_armor_trim_smithing_template", + "minecraft:raiser_armor_trim_smithing_template", + "minecraft:netherite_upgrade_smithing_template", + "minecraft:tide_armor_trim_smithing_template", + "minecraft:host_armor_trim_smithing_template", + "minecraft:dune_armor_trim_smithing_template", + "minecraft:shaper_armor_trim_smithing_template", + "minecraft:silence_armor_trim_smithing_template" + ], + "smithing_base": [ + "minecraft:copper_boots", + "minecraft:golden_leggings", + "minecraft:copper_leggings", + "minecraft:copper_helmet", + "minecraft:diamond_pickaxe", + "minecraft:netherite_helmet", + "minecraft:diamond_chestplate", + "minecraft:leather_leggings", + "minecraft:diamond_nautilus_armor", + "minecraft:chainmail_helmet", + "minecraft:diamond_horse_armor", + "minecraft:diamond_spear", + "minecraft:iron_boots", + "minecraft:iron_helmet", + "minecraft:diamond_hoe", + "minecraft:diamond_shovel", + "minecraft:golden_boots", + "minecraft:golden_helmet", + "minecraft:chainmail_leggings", + "minecraft:diamond_boots", + "minecraft:netherite_boots", + "minecraft:iron_chestplate", + "minecraft:diamond_sword", + "minecraft:leather_boots", + "minecraft:netherite_leggings", + "minecraft:iron_leggings", + "minecraft:diamond_helmet", + "minecraft:golden_chestplate", + "minecraft:diamond_leggings", + "minecraft:copper_chestplate", + "minecraft:leather_chestplate", + "minecraft:leather_helmet", + "minecraft:chainmail_boots", + "minecraft:turtle_helmet", + "minecraft:netherite_chestplate", + "minecraft:chainmail_chestplate", + "minecraft:diamond_axe" + ] +} \ No newline at end of file diff --git a/steel-registry/src/lib.rs b/steel-registry/src/lib.rs index 0cd14c0322f..b732ac05c9d 100644 --- a/steel-registry/src/lib.rs +++ b/steel-registry/src/lib.rs @@ -255,6 +255,11 @@ pub mod vanilla_timeline_tags; #[path = "generated/vanilla_recipes.rs"] pub mod vanilla_recipes; +#[expect(warnings)] +#[rustfmt::skip] +#[path = "generated/vanilla_recipe_property_sets.rs"] +pub mod vanilla_recipe_property_sets; + #[expect(warnings)] #[rustfmt::skip] #[path = "generated/vanilla_entities.rs"] diff --git a/steel-registry/src/recipe/crafting.rs b/steel-registry/src/recipe/crafting.rs index 3adf32bad68..f70eb505f43 100644 --- a/steel-registry/src/recipe/crafting.rs +++ b/steel-registry/src/recipe/crafting.rs @@ -2,9 +2,8 @@ use steel_utils::Identifier; -use crate::{item_stack::ItemStack, items::ItemRef}; - use super::ingredient::Ingredient; +use crate::{item_stack::ItemStack, items::ItemRef, recipe::SmeltingRecipe}; /// Category for crafting recipes (used by recipe book). #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -12,6 +11,7 @@ pub enum CraftingCategory { Building, Redstone, Equipment, + Food, // TODO: check for rest of categories Misc, } @@ -250,6 +250,7 @@ impl ShapelessRecipe { pub enum CraftingRecipe { Shaped(&'static ShapedRecipe), Shapeless(&'static ShapelessRecipe), + Smelting(&'static SmeltingRecipe), } impl CraftingRecipe { @@ -259,6 +260,7 @@ impl CraftingRecipe { match self { Self::Shaped(r) => &r.id, Self::Shapeless(r) => &r.id, + Self::Smelting(r) => &r.ident, } } @@ -268,6 +270,7 @@ impl CraftingRecipe { match self { Self::Shaped(r) => r.category, Self::Shapeless(r) => r.category, + Self::Smelting(r) => r.category, } } @@ -277,6 +280,7 @@ impl CraftingRecipe { match self { Self::Shaped(r) => &r.result, Self::Shapeless(r) => &r.result, + Self::Smelting(r) => &r.result, } } @@ -287,6 +291,7 @@ impl CraftingRecipe { match self { Self::Shaped(r) => r.matches(input), Self::Shapeless(r) => r.matches(input), + Self::Smelting(_r) => false, } } @@ -296,6 +301,7 @@ impl CraftingRecipe { match self { Self::Shaped(r) => r.assemble(), Self::Shapeless(r) => r.assemble(), + Self::Smelting(r) => r.assemble(), } } @@ -305,6 +311,7 @@ impl CraftingRecipe { match self { Self::Shaped(r) => r.get_remaining_items(input), Self::Shapeless(r) => r.get_remaining_items(input), + Self::Smelting(_r) => vec![], } } @@ -314,6 +321,7 @@ impl CraftingRecipe { match self { Self::Shaped(r) => r.fits_in_2x2(), Self::Shapeless(r) => r.fits_in_2x2(), + Self::Smelting(_r) => false, } } } diff --git a/steel-registry/src/recipe/mod.rs b/steel-registry/src/recipe/mod.rs index b425a628e0a..82b7cc82172 100644 --- a/steel-registry/src/recipe/mod.rs +++ b/steel-registry/src/recipe/mod.rs @@ -6,6 +6,7 @@ mod crafting; mod ingredient; mod registry; +mod smelting; pub use crafting::{ CraftingCategory, CraftingInput, CraftingRecipe, PositionedCraftingInput, RecipeResult, @@ -13,3 +14,4 @@ pub use crafting::{ }; pub use ingredient::Ingredient; pub use registry::RecipeRegistry; +pub use smelting::SmeltingRecipe; diff --git a/steel-registry/src/recipe/registry.rs b/steel-registry/src/recipe/registry.rs index 838acddef3f..464580c109c 100644 --- a/steel-registry/src/recipe/registry.rs +++ b/steel-registry/src/recipe/registry.rs @@ -3,6 +3,8 @@ use rustc_hash::FxHashMap; use steel_utils::Identifier; +use crate::recipe::SmeltingRecipe; + use super::crafting::{CraftingInput, CraftingRecipe, ShapedRecipe, ShapelessRecipe}; /// Registry for all recipes. @@ -15,6 +17,12 @@ pub struct RecipeRegistry { shaped_recipes: Vec<&'static ShapedRecipe>, /// All shapeless crafting recipes (for type-specific iteration). shapeless_recipes: Vec<&'static ShapelessRecipe>, + /// All smelting recipes (for type-specific iteration). + smelting_recipes: Vec<&'static SmeltingRecipe>, + /// All campfire recipes (for type-specific iteration). + campfire_recipes: Vec<&'static SmeltingRecipe>, + /// All smoking recipes (for type-specific iteration). + smoking_recipes: Vec<&'static SmeltingRecipe>, /// Whether registration is still allowed. allows_registering: bool, } @@ -35,6 +43,9 @@ impl RecipeRegistry { shaped_recipes: Vec::new(), shapeless_recipes: Vec::new(), allows_registering: true, + smelting_recipes: Vec::new(), + campfire_recipes: Vec::new(), + smoking_recipes: Vec::new(), } } @@ -64,6 +75,42 @@ impl RecipeRegistry { self.shapeless_recipes.push(recipe); } + pub fn register_smelting(&mut self, recipe: &'static SmeltingRecipe) { + assert!( + self.allows_registering, + "Cannot register recipes after the registry has been frozen" + ); + let id = self.recipes_by_id.len(); + self.recipes_by_key.insert(recipe.ident.clone(), id); + self.recipes_by_id + .push(Box::leak(Box::new(CraftingRecipe::Smelting(recipe)))); + self.smelting_recipes.push(recipe); + } + + pub fn register_campfire(&mut self, recipe: &'static SmeltingRecipe) { + assert!( + self.allows_registering, + "Cannot register recipes after the registry has been frozen" + ); + let id = self.recipes_by_id.len(); + self.recipes_by_key.insert(recipe.ident.clone(), id); + self.recipes_by_id + .push(Box::leak(Box::new(CraftingRecipe::Smelting(recipe)))); + self.campfire_recipes.push(recipe); + } + + pub fn register_smoking(&mut self, recipe: &'static SmeltingRecipe) { + assert!( + self.allows_registering, + "Cannot register recipes after the registry has been frozen" + ); + let id = self.recipes_by_id.len(); + self.recipes_by_key.insert(recipe.ident.clone(), id); + self.recipes_by_id + .push(Box::leak(Box::new(CraftingRecipe::Smelting(recipe)))); + self.smoking_recipes.push(recipe); + } + /// Finds a matching crafting recipe for the given positioned input. /// Returns the first matching recipe, or None if no recipe matches. #[must_use] @@ -139,6 +186,17 @@ impl RecipeRegistry { pub fn iter_shapeless(&self) -> impl Iterator + '_ { self.shapeless_recipes.iter().copied() } + + pub fn iter_smelting(&self) -> impl Iterator + '_ { + self.smelting_recipes.iter().copied() + } + + pub fn iter_campfire(&self) -> impl Iterator + '_ { + self.campfire_recipes.iter().copied() + } + pub fn iter_smoking(&self) -> impl Iterator + '_ { + self.smoking_recipes.iter().copied() + } } impl crate::RegistryExt for RecipeRegistry { diff --git a/steel-registry/src/recipe/smelting.rs b/steel-registry/src/recipe/smelting.rs new file mode 100644 index 00000000000..e0f80993706 --- /dev/null +++ b/steel-registry/src/recipe/smelting.rs @@ -0,0 +1,26 @@ +use steel_utils::Identifier; + +use crate::{ + item_stack::ItemStack, + recipe::{CraftingCategory, Ingredient, RecipeResult}, +}; + +#[derive(Debug, Clone)] +pub struct SmeltingRecipe { + pub ident: Identifier, + pub category: CraftingCategory, + pub result: RecipeResult, + pub ingredient: Ingredient, + pub cooking_time: i32, + pub experience: f32, +} + +impl SmeltingRecipe { + pub fn matches(&self, input: &ItemStack) -> bool { + self.ingredient.test(input) + } + + pub fn assemble(&self) -> ItemStack { + self.result.to_item_stack() + } +}