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 = 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}