Skip to main content

steel_core/behavior/blocks/vegetation/
stem_block.rs

1use std::sync::Arc;
2
3use rand::{Rng, RngExt};
4use steel_macros::block_behavior;
5use steel_registry::{
6    blocks::{
7        BlockRef,
8        block_state_ext::BlockStateExt,
9        properties::{BlockStateProperties, EnumProperty, IntProperty},
10    },
11    item_stack::ItemStack,
12    items::ItemRef,
13};
14use steel_utils::{BlockPos, BlockStateId, Direction, Identifier, types::UpdateFlags};
15
16use crate::{
17    behavior::{
18        BlockBehavior, BlockPlaceContext,
19        blocks::vegetation::{
20            Vegetation,
21            crop_block::{CROP_GROWTH_CHANCE_BASE, crop_growth_speed},
22            default_surviving_state,
23            vegetation_block::{survival_update_shape, vegetation_can_survive},
24        },
25    },
26    block_entity::SharedBlockEntity,
27    world::{LevelAccessor, LevelReader, ScheduledTickAccess, World},
28};
29
30const AGE: &IntProperty = &BlockStateProperties::AGE_7;
31const FACING: &EnumProperty<Direction> = &BlockStateProperties::HORIZONTAL_FACING;
32const MAX_AGE: u8 = 7;
33
34/// Vanilla pumpkin and melon stem behavior.
35#[block_behavior]
36pub struct StemBlock {
37    block: BlockRef,
38    #[json_arg(vanilla_blocks)]
39    fruit: BlockRef,
40    #[json_arg(vanilla_blocks)]
41    attached_stem: BlockRef,
42    #[json_arg(vanilla_items)]
43    seed: ItemRef,
44    #[json_arg(vanilla_block_tags)]
45    stem_support_blocks: Identifier,
46    #[json_arg(vanilla_block_tags)]
47    fruit_support_blocks: Identifier,
48}
49
50impl StemBlock {
51    /// Creates a stem with its extracted fruit, attached stem, seed, and support tags.
52    #[must_use]
53    pub const fn new(
54        block: BlockRef,
55        fruit: BlockRef,
56        attached_stem: BlockRef,
57        seed: ItemRef,
58        stem_support_blocks: Identifier,
59        fruit_support_blocks: Identifier,
60    ) -> Self {
61        Self {
62            block,
63            fruit,
64            attached_stem,
65            seed,
66            stem_support_blocks,
67            fruit_support_blocks,
68        }
69    }
70
71    fn age_after_bonemeal(age: u8, increase: u8) -> u8 {
72        age.saturating_add(increase).min(MAX_AGE)
73    }
74
75    fn random_tick_with_rng(
76        &self,
77        state: BlockStateId,
78        world: &dyn LevelAccessor,
79        pos: BlockPos,
80        rng: &mut dyn Rng,
81    ) {
82        if world.raw_brightness(pos, 0) < 9 {
83            return;
84        }
85
86        let growth_speed = crop_growth_speed(self.block, world, pos);
87        let growth_chance = (CROP_GROWTH_CHANCE_BASE / growth_speed) as u32 + 1;
88        if rng.random_range(0..growth_chance) != 0 {
89            return;
90        }
91
92        let age = state.get_value(AGE);
93        if age < MAX_AGE {
94            world.set_block_state(
95                pos,
96                state.set_value(AGE, age + 1),
97                UpdateFlags::UPDATE_CLIENTS,
98            );
99            return;
100        }
101
102        let direction = Direction::HORIZONTAL[rng.random_range(0..Direction::HORIZONTAL.len())];
103        let fruit_pos = pos.relative(direction);
104        if !world.get_block_state(fruit_pos).is_air()
105            || !world
106                .get_block_state(fruit_pos.below())
107                .get_block()
108                .has_tag(&self.fruit_support_blocks)
109        {
110            return;
111        }
112
113        world.set_block_state(
114            fruit_pos,
115            self.fruit.default_state(),
116            UpdateFlags::UPDATE_ALL,
117        );
118        world.set_block_state(
119            pos,
120            self.attached_stem
121                .default_state()
122                .set_value(FACING, direction),
123            UpdateFlags::UPDATE_ALL,
124        );
125    }
126
127    fn perform_bonemeal_with_rng(
128        &self,
129        state: BlockStateId,
130        world: &dyn LevelAccessor,
131        rng: &mut dyn Rng,
132        pos: BlockPos,
133    ) {
134        let age = state.get_value(AGE);
135        let new_age = Self::age_after_bonemeal(age, rng.random_range(2..=5));
136        let new_state = state.set_value(AGE, new_age);
137        world.set_block_state(pos, new_state, UpdateFlags::UPDATE_CLIENTS);
138
139        if new_age == MAX_AGE {
140            self.random_tick_with_rng(new_state, world, pos, rng);
141        }
142    }
143}
144
145impl BlockBehavior for StemBlock {
146    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
147        default_surviving_state(self.block, self, context)
148    }
149
150    fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
151        vegetation_can_survive(self, state, world, pos)
152    }
153
154    fn update_shape(
155        &self,
156        state: BlockStateId,
157        world: &dyn ScheduledTickAccess,
158        pos: BlockPos,
159        _direction: Direction,
160        _neighbor_pos: BlockPos,
161        _neighbor_state: BlockStateId,
162    ) -> BlockStateId {
163        survival_update_shape(self, state, world, pos)
164    }
165
166    fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
167        self.random_tick_with_rng(state, world, pos, &mut rand::rng());
168    }
169
170    fn get_clone_item_stack(
171        &self,
172        _block: BlockRef,
173        _state: BlockStateId,
174        _block_entity: Option<SharedBlockEntity>,
175        _include_data: bool,
176    ) -> Option<ItemStack> {
177        Some(ItemStack::new(self.seed))
178    }
179
180    fn as_bonemealable(&self) -> Option<&dyn super::bonemealable::Bonemealable> {
181        Some(self)
182    }
183}
184
185impl Vegetation for StemBlock {
186    fn may_place_on(&self, state: BlockStateId, _world: &dyn LevelReader, _pos: BlockPos) -> bool {
187        state.get_block().has_tag(&self.stem_support_blocks)
188    }
189}
190
191impl super::bonemealable::Bonemealable for StemBlock {
192    fn get_bonemeal_age_increase(&self, _world: &Arc<World>, rng: &mut dyn Rng) -> u8 {
193        rng.random_range(2..=5)
194    }
195
196    fn is_valid_bonemeal_target(
197        &self,
198        state: BlockStateId,
199        _world: &dyn LevelReader,
200        _pos: BlockPos,
201    ) -> bool {
202        state.get_value(AGE) != MAX_AGE
203    }
204
205    fn perform_bonemeal(
206        &self,
207        state: BlockStateId,
208        world: &Arc<World>,
209        rng: &mut dyn Rng,
210        pos: BlockPos,
211    ) {
212        self.perform_bonemeal_with_rng(state, world, rng, pos);
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use std::convert::Infallible;
219
220    use rand::TryRng;
221    use steel_registry::{
222        init_vanilla_registry, vanilla_block_tags::BlockTag, vanilla_blocks, vanilla_items,
223    };
224
225    use crate::{chunk::light::MAX_LIGHT_LEVEL, test_support::TestLevel};
226
227    use super::super::bonemealable::Bonemealable;
228    use super::*;
229
230    #[derive(Default)]
231    struct ZeroRng;
232
233    impl TryRng for ZeroRng {
234        type Error = Infallible;
235
236        fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
237            Ok(0)
238        }
239
240        fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
241            Ok(0)
242        }
243
244        fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
245            dst.fill(0);
246            Ok(())
247        }
248    }
249
250    fn pumpkin_stem() -> StemBlock {
251        StemBlock::new(
252            &vanilla_blocks::PUMPKIN_STEM,
253            &vanilla_blocks::PUMPKIN,
254            &vanilla_blocks::ATTACHED_PUMPKIN_STEM,
255            &vanilla_items::PUMPKIN_SEEDS,
256            BlockTag::SUPPORTS_PUMPKIN_STEM,
257            BlockTag::SUPPORTS_PUMPKIN_STEM_FRUIT,
258        )
259    }
260
261    fn melon_stem() -> StemBlock {
262        StemBlock::new(
263            &vanilla_blocks::MELON_STEM,
264            &vanilla_blocks::MELON,
265            &vanilla_blocks::ATTACHED_MELON_STEM,
266            &vanilla_items::MELON_SEEDS,
267            BlockTag::SUPPORTS_MELON_STEM,
268            BlockTag::SUPPORTS_MELON_STEM_FRUIT,
269        )
270    }
271
272    #[test]
273    fn survival_uses_extracted_support_without_a_light_requirement() {
274        init_vanilla_registry();
275        let stem = pumpkin_stem();
276        let state = vanilla_blocks::PUMPKIN_STEM.default_state();
277        let farmland = TestLevel::default()
278            .with_block(
279                BlockPos::ZERO.below(),
280                vanilla_blocks::FARMLAND.default_state(),
281            )
282            .with_raw_brightness(0);
283        let dirt = TestLevel::default()
284            .with_block(BlockPos::ZERO.below(), vanilla_blocks::DIRT.default_state())
285            .with_raw_brightness(MAX_LIGHT_LEVEL);
286
287        assert!(stem.can_survive(state, &farmland, BlockPos::ZERO));
288        assert!(!stem.can_survive(state, &dirt, BlockPos::ZERO));
289    }
290
291    #[test]
292    fn random_growth_requires_light_nine_and_advances_one_age() {
293        init_vanilla_registry();
294        let stem = pumpkin_stem();
295        let state = vanilla_blocks::PUMPKIN_STEM
296            .default_state()
297            .set_value(AGE, 3);
298        let dark = TestLevel::default()
299            .with_block(
300                BlockPos::ZERO.below(),
301                vanilla_blocks::FARMLAND.default_state(),
302            )
303            .with_raw_brightness(8);
304        stem.random_tick_with_rng(state, &dark, BlockPos::ZERO, &mut ZeroRng);
305        assert!(dark.placed_blocks.borrow().is_empty());
306
307        let bright = TestLevel::default()
308            .with_block(
309                BlockPos::ZERO.below(),
310                vanilla_blocks::FARMLAND.default_state(),
311            )
312            .with_raw_brightness(9);
313        stem.random_tick_with_rng(state, &bright, BlockPos::ZERO, &mut ZeroRng);
314        let placed = bright.placed_blocks.borrow();
315        assert_eq!(placed.len(), 1);
316        assert_eq!(placed[0].state.get_value(AGE), 4);
317        assert_eq!(placed[0].flags, UpdateFlags::UPDATE_CLIENTS);
318    }
319
320    #[test]
321    fn mature_stems_place_their_own_fruit_and_attached_family() {
322        init_vanilla_registry();
323
324        for (stem, mature_state, fruit, attached) in [
325            (
326                pumpkin_stem(),
327                vanilla_blocks::PUMPKIN_STEM
328                    .default_state()
329                    .set_value(AGE, MAX_AGE),
330                &vanilla_blocks::PUMPKIN,
331                &vanilla_blocks::ATTACHED_PUMPKIN_STEM,
332            ),
333            (
334                melon_stem(),
335                vanilla_blocks::MELON_STEM
336                    .default_state()
337                    .set_value(AGE, MAX_AGE),
338                &vanilla_blocks::MELON,
339                &vanilla_blocks::ATTACHED_MELON_STEM,
340            ),
341        ] {
342            let fruit_pos = BlockPos::ZERO.north();
343            let level = TestLevel::default()
344                .with_block(
345                    BlockPos::ZERO.below(),
346                    vanilla_blocks::FARMLAND.default_state(),
347                )
348                .with_block(fruit_pos.below(), vanilla_blocks::DIRT.default_state())
349                .with_raw_brightness(9);
350            stem.random_tick_with_rng(mature_state, &level, BlockPos::ZERO, &mut ZeroRng);
351
352            let placed = level.placed_blocks.borrow();
353            assert_eq!(placed.len(), 2);
354            assert_eq!(placed[0].pos, fruit_pos);
355            assert_eq!(placed[0].state.get_block(), fruit);
356            assert_eq!(placed[0].flags, UpdateFlags::UPDATE_ALL);
357            assert_eq!(placed[1].pos, BlockPos::ZERO);
358            assert_eq!(placed[1].state.get_block(), attached);
359            assert_eq!(placed[1].state.get_value(FACING), Direction::North);
360            assert_eq!(placed[1].flags, UpdateFlags::UPDATE_ALL);
361        }
362    }
363
364    #[test]
365    fn mature_growth_requires_air_and_fruit_support_and_does_not_scan() {
366        init_vanilla_registry();
367        let stem = pumpkin_stem();
368        let mature = vanilla_blocks::PUMPKIN_STEM
369            .default_state()
370            .set_value(AGE, MAX_AGE);
371
372        let unsupported = TestLevel::default()
373            .with_block(
374                BlockPos::ZERO.below(),
375                vanilla_blocks::FARMLAND.default_state(),
376            )
377            .with_block(
378                BlockPos::ZERO.north().below(),
379                vanilla_blocks::STONE.default_state(),
380            )
381            .with_raw_brightness(9);
382        stem.random_tick_with_rng(mature, &unsupported, BlockPos::ZERO, &mut ZeroRng);
383        assert!(unsupported.placed_blocks.borrow().is_empty());
384
385        let blocked_north = TestLevel::default()
386            .with_block(
387                BlockPos::ZERO.below(),
388                vanilla_blocks::FARMLAND.default_state(),
389            )
390            .with_block(
391                BlockPos::ZERO.north(),
392                vanilla_blocks::STONE.default_state(),
393            )
394            .with_block(
395                BlockPos::ZERO.east().below(),
396                vanilla_blocks::DIRT.default_state(),
397            )
398            .with_block(
399                BlockPos::ZERO.south().below(),
400                vanilla_blocks::DIRT.default_state(),
401            )
402            .with_block(
403                BlockPos::ZERO.west().below(),
404                vanilla_blocks::DIRT.default_state(),
405            )
406            .with_raw_brightness(9);
407        stem.random_tick_with_rng(mature, &blocked_north, BlockPos::ZERO, &mut ZeroRng);
408        assert!(blocked_north.placed_blocks.borrow().is_empty());
409    }
410
411    #[test]
412    fn bonemeal_bounds_age_and_runs_the_mature_tick_with_the_same_rng() {
413        init_vanilla_registry();
414        let stem = pumpkin_stem();
415        assert_eq!(StemBlock::age_after_bonemeal(0, 2), 2);
416        assert_eq!(StemBlock::age_after_bonemeal(6, 5), MAX_AGE);
417
418        let mature = vanilla_blocks::PUMPKIN_STEM
419            .default_state()
420            .set_value(AGE, MAX_AGE);
421        assert!(!stem.is_valid_bonemeal_target(mature, &TestLevel::default(), BlockPos::ZERO));
422
423        let state = vanilla_blocks::PUMPKIN_STEM
424            .default_state()
425            .set_value(AGE, 5);
426        let fruit_pos = BlockPos::ZERO.north();
427        let level = TestLevel::default()
428            .with_block(
429                BlockPos::ZERO.below(),
430                vanilla_blocks::FARMLAND.default_state(),
431            )
432            .with_block(fruit_pos.below(), vanilla_blocks::DIRT.default_state())
433            .with_raw_brightness(9);
434        stem.perform_bonemeal_with_rng(state, &level, &mut ZeroRng, BlockPos::ZERO);
435
436        let placed = level.placed_blocks.borrow();
437        assert_eq!(placed.len(), 3);
438        assert_eq!(placed[0].state.get_value(AGE), MAX_AGE);
439        assert_eq!(placed[0].flags, UpdateFlags::UPDATE_CLIENTS);
440        assert_eq!(placed[1].state.get_block(), &vanilla_blocks::PUMPKIN);
441        assert_eq!(
442            placed[2].state.get_block(),
443            &vanilla_blocks::ATTACHED_PUMPKIN_STEM
444        );
445    }
446
447    #[test]
448    fn clone_items_match_each_stem_family() {
449        init_vanilla_registry();
450        let pumpkin = pumpkin_stem()
451            .get_clone_item_stack(
452                &vanilla_blocks::PUMPKIN_STEM,
453                vanilla_blocks::PUMPKIN_STEM.default_state(),
454                None,
455                false,
456            )
457            .expect("pumpkin stem has a clone item");
458        let melon = melon_stem()
459            .get_clone_item_stack(
460                &vanilla_blocks::MELON_STEM,
461                vanilla_blocks::MELON_STEM.default_state(),
462                None,
463                false,
464            )
465            .expect("melon stem has a clone item");
466
467        assert_eq!(pumpkin.item(), &*vanilla_items::PUMPKIN_SEEDS);
468        assert_eq!(melon.item(), &*vanilla_items::MELON_SEEDS);
469    }
470
471    #[test]
472    fn extracted_constructor_mappings_cover_both_families() {
473        let classes: serde_json::Value =
474            serde_json::from_str(include_str!("../../../../build/classes.json"))
475                .expect("extracted classes.json is valid JSON");
476        let blocks = classes["blocks"]
477            .as_array()
478            .expect("classes.json contains blocks");
479
480        let expected = [
481            (
482                "pumpkin_stem",
483                "pumpkin",
484                "attached_pumpkin_stem",
485                "pumpkin_seeds",
486                "supports_pumpkin_stem",
487                "supports_pumpkin_stem_fruit",
488            ),
489            (
490                "melon_stem",
491                "melon",
492                "attached_melon_stem",
493                "melon_seeds",
494                "supports_melon_stem",
495                "supports_melon_stem_fruit",
496            ),
497        ];
498
499        for (name, fruit, attached, seed, stem_support, fruit_support) in expected {
500            let block = blocks
501                .iter()
502                .find(|block| block["name"] == name)
503                .expect("stem mapping exists");
504            assert_eq!(block["fruit"], fruit);
505            assert_eq!(block["attached_stem"], attached);
506            assert_eq!(block["seed"], seed);
507            assert_eq!(block["stem_support_blocks"], stem_support);
508            assert_eq!(block["fruit_support_blocks"], fruit_support);
509        }
510    }
511}