Skip to main content

steel_core/behavior/blocks/redstone/
redstone_ore_block.rs

1//! Vanilla redstone and deepslate redstone ore behavior.
2
3use std::sync::Arc;
4
5use rand::RngExt as _;
6use steel_macros::block_behavior;
7use steel_registry::REGISTRY;
8use steel_registry::blocks::BlockRef;
9use steel_registry::blocks::block_state_ext::BlockStateExt as _;
10use steel_registry::blocks::properties::BlockStateProperties;
11use steel_registry::enchantment_effect::EnchantmentEffectComponent;
12use steel_registry::item_stack::ItemStack;
13use steel_utils::types::{InteractionHand, UpdateFlags};
14use steel_utils::{BlockPos, BlockStateId};
15
16use crate::behavior::{
17    BlockBehavior, BlockHitResult, BlockPlaceContext, InteractionResult, InventoryAccess,
18    PlacementSource,
19};
20use crate::entity::Entity;
21use crate::player::Player;
22use crate::world::World;
23
24/// Vanilla `RedStoneOreBlock` behavior shared by both ore variants.
25#[block_behavior]
26pub struct RedStoneOreBlock {
27    block: BlockRef,
28}
29
30impl RedStoneOreBlock {
31    /// Creates a redstone ore behavior.
32    #[must_use]
33    pub const fn new(block: BlockRef) -> Self {
34        Self { block }
35    }
36
37    fn interact(state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
38        if state.get_value(&BlockStateProperties::LIT) {
39            return;
40        }
41
42        world.set_block(
43            pos,
44            state.set_value(&BlockStateProperties::LIT, true),
45            UpdateFlags::UPDATE_ALL,
46        );
47    }
48
49    fn process_block_experience(tool: &ItemStack, base_experience: i32) -> i32 {
50        tool.apply_unconditional_enchantment_value_effects(
51            EnchantmentEffectComponent::BlockExperience,
52            base_experience as f32,
53        ) as i32
54    }
55}
56
57impl BlockBehavior for RedStoneOreBlock {
58    fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
59        Some(self.block.default_state())
60    }
61
62    fn attack(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos, _player: &Player) {
63        Self::interact(state, world, pos);
64    }
65
66    fn step_on(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos, entity: &dyn Entity) {
67        if !entity.is_stepping_carefully() {
68            Self::interact(state, world, pos);
69        }
70        self.default_step_on(state, world, pos, entity);
71    }
72
73    fn use_item_on(
74        &self,
75        state: BlockStateId,
76        world: &Arc<World>,
77        pos: BlockPos,
78        player: &Player,
79        _hand: InteractionHand,
80        hit_result: &BlockHitResult,
81        inv: &mut InventoryAccess,
82    ) -> InteractionResult {
83        Self::interact(state, world, pos);
84
85        let held_item = inv.with_item(|stack| stack.item());
86        if REGISTRY.items.is_block_item(held_item)
87            && BlockPlaceContext::new(world, PlacementSource::player_hand(player, inv), hit_result)
88                .can_place()
89        {
90            InteractionResult::Pass
91        } else {
92            InteractionResult::Success
93        }
94    }
95
96    fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
97        if state.get_value(&BlockStateProperties::LIT) {
98            world.set_block(
99                pos,
100                state.set_value(&BlockStateProperties::LIT, false),
101                UpdateFlags::UPDATE_ALL,
102            );
103        }
104    }
105
106    fn spawn_after_break(
107        &self,
108        _state: BlockStateId,
109        world: &Arc<World>,
110        pos: BlockPos,
111        tool: &ItemStack,
112        drop_experience: bool,
113    ) {
114        if !drop_experience {
115            return;
116        }
117
118        let base_experience = rand::rng().random_range(1..=5);
119        let experience = Self::process_block_experience(tool, base_experience);
120        world.pop_experience(pos, experience);
121    }
122
123    // `animateTick` and interaction particles use client-local `Level.addParticle`.
124}
125
126#[cfg(test)]
127mod tests {
128    use std::sync::Arc;
129
130    use glam::DVec3;
131    use steel_registry::entity_type::EntityTypeRef;
132    use steel_registry::init_vanilla_registry;
133    use steel_registry::{vanilla_blocks, vanilla_entities, vanilla_items};
134    use steel_utils::{ChunkPos, Identifier, WorldAabb};
135
136    use super::*;
137    use crate::behavior::{BLOCK_BEHAVIORS, init_behaviors};
138    use crate::entity::EntityBase;
139    use crate::test_support::{fresh_test_world, insert_ready_full_chunk};
140
141    struct StepEntity {
142        base: EntityBase,
143        careful: bool,
144    }
145
146    crate::entity::impl_test_downcast_type!(StepEntity);
147
148    impl StepEntity {
149        fn new(id: i32, world: &Arc<World>, careful: bool) -> Self {
150            Self {
151                base: EntityBase::new(
152                    id,
153                    DVec3::new(8.5, 65.0, 8.5),
154                    vanilla_entities::PIG.dimensions,
155                    Arc::downgrade(world),
156                ),
157                careful,
158            }
159        }
160    }
161
162    impl Entity for StepEntity {
163        fn base(&self) -> &EntityBase {
164            &self.base
165        }
166
167        fn entity_type(&self) -> EntityTypeRef {
168            &vanilla_entities::PIG
169        }
170
171        fn is_stepping_carefully(&self) -> bool {
172            self.careful
173        }
174    }
175
176    #[test]
177    fn both_ore_variants_light_from_steps_and_extinguish_on_random_ticks() {
178        init_vanilla_registry();
179        init_behaviors();
180        let world = fresh_test_world("redstone_ore_steps");
181        let first_pos = BlockPos::new(8, 64, 8);
182        insert_ready_full_chunk(&world, ChunkPos::from_block_pos(first_pos));
183
184        let careful_entity = StepEntity::new(7_003, &world, true);
185        let ordinary_entity = StepEntity::new(7_004, &world, false);
186
187        for (offset, block) in [
188            &vanilla_blocks::REDSTONE_ORE,
189            &vanilla_blocks::DEEPSLATE_REDSTONE_ORE,
190        ]
191        .into_iter()
192        .enumerate()
193        {
194            let pos = first_pos.offset(offset as i32, 0, 0);
195            let unlit = block.default_state();
196            assert!(!unlit.is_randomly_ticking());
197            assert!(world.set_block(pos, unlit, UpdateFlags::UPDATE_NONE));
198
199            let behavior = BLOCK_BEHAVIORS.get_behavior(block);
200            behavior.step_on(unlit, &world, pos, &careful_entity);
201            assert!(
202                !world
203                    .get_block_state(pos)
204                    .get_value(&BlockStateProperties::LIT)
205            );
206
207            behavior.step_on(unlit, &world, pos, &ordinary_entity);
208            let lit = world.get_block_state(pos);
209            assert!(lit.get_value(&BlockStateProperties::LIT));
210            assert!(lit.is_randomly_ticking());
211
212            behavior.random_tick(lit, &world, pos);
213            assert!(
214                !world
215                    .get_block_state(pos)
216                    .get_value(&BlockStateProperties::LIT)
217            );
218        }
219    }
220
221    #[test]
222    fn world_drop_resources_dispatches_redstone_ore_experience() {
223        init_vanilla_registry();
224        init_behaviors();
225        let world = fresh_test_world("redstone_ore_post_break");
226        let pos = BlockPos::new(8, 64, 8);
227        insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
228
229        world.drop_resources(vanilla_blocks::REDSTONE_ORE.default_state(), pos);
230
231        let query = WorldAabb::new(7.0, 63.0, 7.0, 10.0, 67.0, 10.0);
232        assert!(
233            world
234                .get_entities_in_aabb(&query)
235                .iter()
236                .any(|entity| entity.entity_type() == &vanilla_entities::EXPERIENCE_ORB)
237        );
238    }
239
240    #[test]
241    fn silk_touch_suppresses_redstone_ore_experience() {
242        init_vanilla_registry();
243        let plain_tool = ItemStack::new(&vanilla_items::DIAMOND_PICKAXE);
244        assert_eq!(
245            RedStoneOreBlock::process_block_experience(&plain_tool, 5),
246            5
247        );
248
249        let mut silk_touch_tool = ItemStack::new(&vanilla_items::DIAMOND_PICKAXE);
250        silk_touch_tool.set_enchantments(&[(Identifier::vanilla_static("silk_touch"), 1)], false);
251        assert_eq!(
252            RedStoneOreBlock::process_block_experience(&silk_touch_tool, 5),
253            0
254        );
255    }
256}