Skip to main content

steel_core/behavior/blocks/vegetation/
mangrove_propagule_block.rs

1use std::sync::Arc;
2
3use rand::{Rng, RngExt};
4use steel_macros::block_behavior;
5use steel_registry::blocks::block_state_ext::BlockStateExt;
6use steel_registry::blocks::properties::{
7    BlockStateProperties, BoolProperty, Direction, IntProperty,
8};
9use steel_registry::feature::ConfiguredFeatureKind;
10use steel_registry::vanilla_block_tags::BlockTag;
11use steel_registry::{REGISTRY, vanilla_blocks, vanilla_configured_features};
12use steel_utils::random::worldgen_random::WorldgenRandom;
13use steel_utils::types::UpdateFlags;
14use steel_utils::{BlockPos, BlockStateId};
15use steel_worldgen::biomes::obfuscate_biome_seed;
16
17use crate::behavior::block::{BlockBehavior, schedule_water_tick_if_waterlogged};
18use crate::behavior::blocks::vegetation::bonemealable::Bonemealable;
19use crate::behavior::context::BlockPlaceContext;
20use crate::world::{LevelAccessor, LevelReader, ScheduledTickAccess, World};
21use crate::worldgen::feature::FeatureDecorationRunner;
22use crate::worldgen::feature::no_nested_features;
23
24use super::BlockRef;
25
26/// Vanilla `MangrovePropaguleBlock` behavior.
27#[block_behavior]
28pub struct MangrovePropaguleBlock {
29    block: BlockRef,
30}
31
32const AGE: &IntProperty = &BlockStateProperties::AGE_4;
33const HANGING: &BoolProperty = &BlockStateProperties::HANGING;
34const STAGE: &IntProperty = &BlockStateProperties::STAGE;
35const WATERLOGGED: &BoolProperty = &BlockStateProperties::WATERLOGGED;
36const MAX_AGE: u8 = 4;
37const RANDOM_GROWTH_BOUND: u32 = 7;
38const BONEMEAL_SUCCESS_CHANCE: f32 = 0.45;
39const TALL_MANGROVE_CHANCE: f32 = 0.85;
40
41impl MangrovePropaguleBlock {
42    /// Creates a new mangrove propagule block behavior.
43    #[must_use]
44    pub const fn new(block: BlockRef) -> Self {
45        Self { block }
46    }
47
48    /// Creates vanilla's initial hanging propagule state.
49    pub(crate) fn create_new_hanging_propagule() -> BlockStateId {
50        vanilla_blocks::MANGROVE_PROPAGULE
51            .default_state()
52            .set_value(HANGING, true)
53            .set_value(AGE, 0)
54    }
55
56    fn advance_hanging(state: BlockStateId, world: &dyn LevelAccessor, pos: BlockPos) -> bool {
57        let age = state.get_value(AGE);
58        if age >= MAX_AGE {
59            return false;
60        }
61
62        world.set_block_state(
63            pos,
64            state.set_value(AGE, age + 1),
65            UpdateFlags::UPDATE_CLIENTS,
66        )
67    }
68
69    fn grow_tree(
70        world: &Arc<World>,
71        pos: BlockPos,
72        state: BlockStateId,
73        rng: &mut dyn Rng,
74    ) -> bool {
75        let feature = if rng.random::<f32>() < TALL_MANGROVE_CHANCE {
76            &*vanilla_configured_features::TALL_MANGROVE
77        } else {
78            &*vanilla_configured_features::MANGROVE
79        };
80        let ConfiguredFeatureKind::Tree(config) = &feature.kind else {
81            return false;
82        };
83        let replacement = if state.get_value(WATERLOGGED) {
84            vanilla_blocks::WATER.default_state()
85        } else {
86            vanilla_blocks::AIR.default_state()
87        };
88        world.set_block(pos, replacement, UpdateFlags::UPDATE_NONE);
89
90        let mut worldgen_random = WorldgenRandom::from_seed(rng.random());
91        let mut level = Arc::clone(world);
92        let placed = FeatureDecorationRunner::place_tree_feature(
93            &mut level,
94            &REGISTRY,
95            &mut worldgen_random,
96            config,
97            pos,
98            obfuscate_biome_seed(world.seed()),
99            no_nested_features,
100        );
101
102        if placed {
103            if world.get_block_state(pos) == replacement {
104                world.send_block_updated(pos);
105            }
106        } else {
107            world.set_block(pos, state, UpdateFlags::UPDATE_NONE);
108        }
109        placed
110    }
111
112    fn advance_tree(world: &Arc<World>, pos: BlockPos, state: BlockStateId, rng: &mut dyn Rng) {
113        if state.get_value(STAGE) == 0 {
114            world.set_block(pos, state.set_value(STAGE, 1), UpdateFlags::UPDATE_NONE);
115        } else {
116            Self::grow_tree(world, pos, state, rng);
117        }
118    }
119
120    fn random_tick_with_rng(
121        state: BlockStateId,
122        world: &Arc<World>,
123        pos: BlockPos,
124        rng: &mut dyn Rng,
125    ) {
126        if state.get_value(HANGING) {
127            Self::advance_hanging(state, world, pos);
128        } else if rng.random_range(0..RANDOM_GROWTH_BOUND) == 0 {
129            Self::advance_tree(world, pos, state, rng);
130        }
131    }
132}
133
134impl BlockBehavior for MangrovePropaguleBlock {
135    fn update_shape(
136        &self,
137        state: BlockStateId,
138        world: &dyn ScheduledTickAccess,
139        pos: BlockPos,
140        direction: Direction,
141        _neighbor_pos: BlockPos,
142        _neighbor_state: BlockStateId,
143    ) -> BlockStateId {
144        schedule_water_tick_if_waterlogged(state, world, pos);
145        if direction == Direction::Up && !self.can_survive(state, world, pos) {
146            return vanilla_blocks::AIR.default_state();
147        }
148
149        state
150    }
151
152    fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
153        if state.get_value(HANGING) {
154            let above = world.get_block_state(pos.above());
155            return above
156                .get_block()
157                .has_tag(&BlockTag::SUPPORTS_HANGING_MANGROVE_PROPAGULE);
158        }
159
160        let below = world.get_block_state(pos.below());
161        below
162            .get_block()
163            .has_tag(&BlockTag::SUPPORTS_MANGROVE_PROPAGULE)
164    }
165
166    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
167        let state = self
168            .block
169            .default_state()
170            .set_value(AGE, MAX_AGE)
171            .set_value(WATERLOGGED, context.is_water_source());
172        self.can_survive(state, context.world, context.place_pos())
173            .then_some(state)
174    }
175
176    fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
177        Self::random_tick_with_rng(state, world, pos, &mut rand::rng());
178    }
179
180    fn as_bonemealable(&self) -> Option<&dyn Bonemealable> {
181        Some(self)
182    }
183}
184
185impl Bonemealable for MangrovePropaguleBlock {
186    fn is_valid_bonemeal_target(
187        &self,
188        state: BlockStateId,
189        _world: &dyn LevelReader,
190        _pos: BlockPos,
191    ) -> bool {
192        if state.get_value(HANGING) {
193            return state.get_value(AGE) < MAX_AGE;
194        }
195
196        true
197    }
198
199    fn is_bonemeal_success(
200        &self,
201        state: BlockStateId,
202        _world: &Arc<World>,
203        rng: &mut dyn Rng,
204        _pos: BlockPos,
205    ) -> bool {
206        if state.get_value(HANGING) {
207            return state.get_value(AGE) < MAX_AGE;
208        }
209
210        rng.random::<f32>() < BONEMEAL_SUCCESS_CHANCE
211    }
212
213    fn perform_bonemeal(
214        &self,
215        state: BlockStateId,
216        world: &Arc<World>,
217        rng: &mut dyn Rng,
218        pos: BlockPos,
219    ) {
220        if state.get_value(HANGING) {
221            Self::advance_hanging(state, world, pos);
222        } else {
223            Self::advance_tree(world, pos, state, rng);
224        }
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use std::convert::Infallible;
231
232    use rand::{SeedableRng, TryRng, rngs::StdRng};
233    use steel_registry::item_stack::ItemStack;
234    use steel_registry::{init_vanilla_registry, vanilla_items};
235    use steel_utils::ChunkPos;
236
237    use super::*;
238    use crate::behavior::init_behaviors;
239    use crate::test_support::{TestLevel, fresh_test_world, insert_ready_full_chunk};
240
241    #[derive(Default)]
242    struct ZeroRng;
243
244    impl TryRng for ZeroRng {
245        type Error = Infallible;
246
247        fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
248            Ok(0)
249        }
250
251        fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
252            Ok(0)
253        }
254
255        fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
256            dst.fill(0);
257            Ok(())
258        }
259    }
260
261    #[test]
262    fn new_hanging_propagule_starts_at_age_zero() {
263        init_vanilla_registry();
264
265        let state = MangrovePropaguleBlock::create_new_hanging_propagule();
266
267        assert_eq!(state.get_block(), &vanilla_blocks::MANGROVE_PROPAGULE);
268        assert!(state.get_value(HANGING));
269        assert_eq!(state.get_value(AGE), 0);
270    }
271
272    #[test]
273    fn placed_propagules_are_mature_and_preserve_source_water() {
274        init_vanilla_registry();
275        init_behaviors();
276        let world = fresh_test_world("mangrove_propagule_placement");
277        let wet_pos = BlockPos::new(8, 64, 8);
278        let dry_pos = wet_pos.east();
279        insert_ready_full_chunk(&world, ChunkPos::from_block_pos(wet_pos));
280        assert!(world.set_block(
281            wet_pos.below(),
282            vanilla_blocks::DIRT.default_state(),
283            UpdateFlags::UPDATE_NONE,
284        ));
285        assert!(world.set_block(
286            dry_pos.below(),
287            vanilla_blocks::DIRT.default_state(),
288            UpdateFlags::UPDATE_NONE,
289        ));
290        assert!(world.set_block(
291            wet_pos,
292            vanilla_blocks::WATER.default_state(),
293            UpdateFlags::UPDATE_NONE,
294        ));
295        let behavior = MangrovePropaguleBlock::new(&vanilla_blocks::MANGROVE_PROPAGULE);
296
297        let wet_state = {
298            let mut stack = ItemStack::new(&vanilla_items::MANGROVE_PROPAGULE);
299            let context = BlockPlaceContext::directional(
300                &world,
301                wet_pos,
302                Direction::Down,
303                &mut stack,
304                Direction::Up,
305            );
306            behavior
307                .get_state_for_placement(&context)
308                .expect("wet propagule should have a placement state")
309        };
310        let dry_state = {
311            let mut stack = ItemStack::new(&vanilla_items::MANGROVE_PROPAGULE);
312            let context = BlockPlaceContext::directional(
313                &world,
314                dry_pos,
315                Direction::Down,
316                &mut stack,
317                Direction::Up,
318            );
319            behavior
320                .get_state_for_placement(&context)
321                .expect("dry propagule should have a placement state")
322        };
323
324        assert_eq!(wet_state.get_value(AGE), MAX_AGE);
325        assert!(wet_state.get_value(WATERLOGGED));
326        assert_eq!(dry_state.get_value(AGE), MAX_AGE);
327        assert!(!dry_state.get_value(WATERLOGGED));
328    }
329
330    #[test]
331    fn hanging_growth_stops_at_age_four() {
332        init_vanilla_registry();
333        let level = TestLevel::default();
334        let pos = BlockPos::ZERO;
335        let age_three = MangrovePropaguleBlock::create_new_hanging_propagule().set_value(AGE, 3);
336
337        assert!(MangrovePropaguleBlock::advance_hanging(
338            age_three, &level, pos
339        ));
340        assert_eq!(
341            level
342                .last_placed_state()
343                .expect("hanging growth should place an updated state")
344                .get_value(AGE),
345            MAX_AGE
346        );
347        assert!(!MangrovePropaguleBlock::advance_hanging(
348            age_three.set_value(AGE, MAX_AGE),
349            &level,
350            pos,
351        ));
352    }
353
354    #[test]
355    fn bonemeal_targets_match_hanging_age_and_tree_height() {
356        init_vanilla_registry();
357        let behavior = MangrovePropaguleBlock::new(&vanilla_blocks::MANGROVE_PROPAGULE);
358        let hanging = MangrovePropaguleBlock::create_new_hanging_propagule();
359        let level = TestLevel::default().with_min_y(0).with_height(10);
360
361        assert!(behavior.is_valid_bonemeal_target(hanging, &level, BlockPos::ZERO));
362        assert!(!behavior.is_valid_bonemeal_target(
363            hanging.set_value(AGE, MAX_AGE),
364            &level,
365            BlockPos::ZERO,
366        ));
367        assert!(behavior.is_valid_bonemeal_target(
368            vanilla_blocks::MANGROVE_PROPAGULE.default_state(),
369            &level,
370            BlockPos::new(0, 7, 0),
371        ));
372        assert!(behavior.is_valid_bonemeal_target(
373            vanilla_blocks::MANGROVE_PROPAGULE.default_state(),
374            &level,
375            BlockPos::new(0, 8, 0),
376        ));
377    }
378
379    #[test]
380    fn planted_bonemeal_advances_stage_before_growing_tree() {
381        init_vanilla_registry();
382        init_behaviors();
383        let world = fresh_test_world("mangrove_propagule_stage");
384        let pos = BlockPos::new(8, 64, 8);
385        insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
386        let state = vanilla_blocks::MANGROVE_PROPAGULE.default_state();
387        assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
388        let behavior = MangrovePropaguleBlock::new(&vanilla_blocks::MANGROVE_PROPAGULE);
389
390        behavior.perform_bonemeal(state, &world, &mut ZeroRng, pos);
391
392        assert_eq!(world.get_block_state(pos).get_value(STAGE), 1);
393    }
394
395    #[test]
396    fn mature_planted_propagule_generates_mangrove_tree() {
397        init_vanilla_registry();
398        init_behaviors();
399        let world = fresh_test_world("mangrove_propagule_tree");
400        let pos = BlockPos::new(8, 64, 8);
401        let center = ChunkPos::from_block_pos(pos);
402        for chunk_x in center.0.x - 1..=center.0.x + 1 {
403            for chunk_z in center.0.y - 1..=center.0.y + 1 {
404                insert_ready_full_chunk(&world, ChunkPos::new(chunk_x, chunk_z));
405            }
406        }
407        for x in pos.x() - 12..=pos.x() + 12 {
408            for z in pos.z() - 12..=pos.z() + 12 {
409                assert!(world.set_block(
410                    BlockPos::new(x, pos.y() - 1, z),
411                    vanilla_blocks::DIRT.default_state(),
412                    UpdateFlags::UPDATE_NONE,
413                ));
414            }
415        }
416        let state = vanilla_blocks::MANGROVE_PROPAGULE
417            .default_state()
418            .set_value(STAGE, 1)
419            .set_value(AGE, MAX_AGE);
420        assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
421        let placed = (0..64).any(|seed| {
422            let mut rng = StdRng::seed_from_u64(seed);
423            MangrovePropaguleBlock::grow_tree(&world, pos, state, &mut rng)
424        });
425
426        assert!(placed);
427        assert!((pos.y()..pos.y() + 24).any(|y| {
428            world
429                .get_block_state(BlockPos::new(pos.x(), y, pos.z()))
430                .get_block()
431                == &vanilla_blocks::MANGROVE_LOG
432        }));
433    }
434
435    #[test]
436    fn unsupported_waterlogged_propagule_schedules_water_before_breaking() {
437        init_vanilla_registry();
438        let behavior = MangrovePropaguleBlock::new(&vanilla_blocks::MANGROVE_PROPAGULE);
439        let state = vanilla_blocks::MANGROVE_PROPAGULE
440            .default_state()
441            .set_value(WATERLOGGED, true);
442        let level = TestLevel::default();
443
444        assert!(
445            behavior
446                .update_shape(
447                    state,
448                    &level,
449                    BlockPos::ZERO,
450                    Direction::Up,
451                    BlockPos::ZERO.above(),
452                    vanilla_blocks::AIR.default_state(),
453                )
454                .is_air()
455        );
456        assert!(level.scheduled_water_tick());
457
458        let side_update = behavior.update_shape(
459            state,
460            &TestLevel::default(),
461            BlockPos::ZERO,
462            Direction::North,
463            BlockPos::ZERO.north(),
464            vanilla_blocks::AIR.default_state(),
465        );
466        assert_eq!(side_update, state);
467    }
468}