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#[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 #[must_use]
44 pub const fn new(block: BlockRef) -> Self {
45 Self { block }
46 }
47
48 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 ®ISTRY,
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_fixture = fresh_test_world("mangrove_propagule_placement");
277 let world = &world_fixture.world;
278 let wet_pos = BlockPos::new(8, 64, 8);
279 let dry_pos = wet_pos.east();
280 insert_ready_full_chunk(world, ChunkPos::from_block_pos(wet_pos));
281 assert!(world.set_block(
282 wet_pos.below(),
283 vanilla_blocks::DIRT.default_state(),
284 UpdateFlags::UPDATE_NONE,
285 ));
286 assert!(world.set_block(
287 dry_pos.below(),
288 vanilla_blocks::DIRT.default_state(),
289 UpdateFlags::UPDATE_NONE,
290 ));
291 assert!(world.set_block(
292 wet_pos,
293 vanilla_blocks::WATER.default_state(),
294 UpdateFlags::UPDATE_NONE,
295 ));
296 let behavior = MangrovePropaguleBlock::new(&vanilla_blocks::MANGROVE_PROPAGULE);
297
298 let wet_state = {
299 let mut stack = ItemStack::new(&vanilla_items::MANGROVE_PROPAGULE);
300 let context = BlockPlaceContext::directional(
301 world,
302 wet_pos,
303 Direction::Down,
304 &mut stack,
305 Direction::Up,
306 );
307 behavior
308 .get_state_for_placement(&context)
309 .expect("wet propagule should have a placement state")
310 };
311 let dry_state = {
312 let mut stack = ItemStack::new(&vanilla_items::MANGROVE_PROPAGULE);
313 let context = BlockPlaceContext::directional(
314 world,
315 dry_pos,
316 Direction::Down,
317 &mut stack,
318 Direction::Up,
319 );
320 behavior
321 .get_state_for_placement(&context)
322 .expect("dry propagule should have a placement state")
323 };
324
325 assert_eq!(wet_state.get_value(AGE), MAX_AGE);
326 assert!(wet_state.get_value(WATERLOGGED));
327 assert_eq!(dry_state.get_value(AGE), MAX_AGE);
328 assert!(!dry_state.get_value(WATERLOGGED));
329 }
330
331 #[test]
332 fn hanging_growth_stops_at_age_four() {
333 init_vanilla_registry();
334 let level = TestLevel::default();
335 let pos = BlockPos::ZERO;
336 let age_three = MangrovePropaguleBlock::create_new_hanging_propagule().set_value(AGE, 3);
337
338 assert!(MangrovePropaguleBlock::advance_hanging(
339 age_three, &level, pos
340 ));
341 assert_eq!(
342 level
343 .last_placed_state()
344 .expect("hanging growth should place an updated state")
345 .get_value(AGE),
346 MAX_AGE
347 );
348 assert!(!MangrovePropaguleBlock::advance_hanging(
349 age_three.set_value(AGE, MAX_AGE),
350 &level,
351 pos,
352 ));
353 }
354
355 #[test]
356 fn bonemeal_targets_match_hanging_age_and_tree_height() {
357 init_vanilla_registry();
358 let behavior = MangrovePropaguleBlock::new(&vanilla_blocks::MANGROVE_PROPAGULE);
359 let hanging = MangrovePropaguleBlock::create_new_hanging_propagule();
360 let level = TestLevel::default().with_min_y(0).with_height(10);
361
362 assert!(behavior.is_valid_bonemeal_target(hanging, &level, BlockPos::ZERO));
363 assert!(!behavior.is_valid_bonemeal_target(
364 hanging.set_value(AGE, MAX_AGE),
365 &level,
366 BlockPos::ZERO,
367 ));
368 assert!(behavior.is_valid_bonemeal_target(
369 vanilla_blocks::MANGROVE_PROPAGULE.default_state(),
370 &level,
371 BlockPos::new(0, 7, 0),
372 ));
373 assert!(behavior.is_valid_bonemeal_target(
374 vanilla_blocks::MANGROVE_PROPAGULE.default_state(),
375 &level,
376 BlockPos::new(0, 8, 0),
377 ));
378 }
379
380 #[test]
381 fn planted_bonemeal_advances_stage_before_growing_tree() {
382 init_vanilla_registry();
383 init_behaviors();
384 let world_fixture = fresh_test_world("mangrove_propagule_stage");
385 let world = &world_fixture.world;
386 let pos = BlockPos::new(8, 64, 8);
387 insert_ready_full_chunk(world, ChunkPos::from_block_pos(pos));
388 let state = vanilla_blocks::MANGROVE_PROPAGULE.default_state();
389 assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
390 let behavior = MangrovePropaguleBlock::new(&vanilla_blocks::MANGROVE_PROPAGULE);
391
392 behavior.perform_bonemeal(state, world, &mut ZeroRng, pos);
393
394 assert_eq!(world.get_block_state(pos).get_value(STAGE), 1);
395 }
396
397 #[test]
398 fn mature_planted_propagule_generates_mangrove_tree() {
399 init_vanilla_registry();
400 init_behaviors();
401 let world_fixture = fresh_test_world("mangrove_propagule_tree");
402 let world = &world_fixture.world;
403 let pos = BlockPos::new(8, 64, 8);
404 let center = ChunkPos::from_block_pos(pos);
405 for chunk_x in center.0.x - 1..=center.0.x + 1 {
406 for chunk_z in center.0.y - 1..=center.0.y + 1 {
407 insert_ready_full_chunk(world, ChunkPos::new(chunk_x, chunk_z));
408 }
409 }
410 for x in pos.x() - 12..=pos.x() + 12 {
411 for z in pos.z() - 12..=pos.z() + 12 {
412 assert!(world.set_block(
413 BlockPos::new(x, pos.y() - 1, z),
414 vanilla_blocks::DIRT.default_state(),
415 UpdateFlags::UPDATE_NONE,
416 ));
417 }
418 }
419 let state = vanilla_blocks::MANGROVE_PROPAGULE
420 .default_state()
421 .set_value(STAGE, 1)
422 .set_value(AGE, MAX_AGE);
423 assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
424 let placed = (0..64).any(|seed| {
425 let mut rng = StdRng::seed_from_u64(seed);
426 MangrovePropaguleBlock::grow_tree(world, pos, state, &mut rng)
427 });
428
429 assert!(placed);
430 assert!((pos.y()..pos.y() + 24).any(|y| {
431 world
432 .get_block_state(BlockPos::new(pos.x(), y, pos.z()))
433 .get_block()
434 == &vanilla_blocks::MANGROVE_LOG
435 }));
436 }
437
438 #[test]
439 fn unsupported_waterlogged_propagule_schedules_water_before_breaking() {
440 init_vanilla_registry();
441 let behavior = MangrovePropaguleBlock::new(&vanilla_blocks::MANGROVE_PROPAGULE);
442 let state = vanilla_blocks::MANGROVE_PROPAGULE
443 .default_state()
444 .set_value(WATERLOGGED, true);
445 let level = TestLevel::default();
446
447 assert!(
448 behavior
449 .update_shape(
450 state,
451 &level,
452 BlockPos::ZERO,
453 Direction::Up,
454 BlockPos::ZERO.above(),
455 vanilla_blocks::AIR.default_state(),
456 )
457 .is_air()
458 );
459 assert!(level.scheduled_water_tick());
460
461 let side_update = behavior.update_shape(
462 state,
463 &TestLevel::default(),
464 BlockPos::ZERO,
465 Direction::North,
466 BlockPos::ZERO.north(),
467 vanilla_blocks::AIR.default_state(),
468 );
469 assert_eq!(side_update, state);
470 }
471}