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