1#![expect(
2 clippy::too_many_arguments,
3 reason = "tree decorators mirror vanilla decorator context"
4)]
5
6use steel_registry::vanilla_block_tags::BlockTag;
7use steel_utils::Downcast as _;
8
9use super::super::super::prelude::*;
10use super::super::super::runner::FeatureDecorationRunner;
11use super::super::super::vanilla_collections::JavaBlockPosSet;
12use super::{NestedFeaturePlacer, TreePlacement};
13
14use crate::block_entity::entities::BeehiveBlockEntity;
15
16const BEEHIVE_WORLDGEN_FACING: Direction = Direction::South;
17const BEEHIVE_SPAWN_DIRECTIONS: [Direction; 3] =
18 [Direction::East, Direction::South, Direction::West];
19
20impl FeatureDecorationRunner {
21 pub(super) fn place_tree_decorators<L: LevelAccessor>(
22 region: &mut L,
23 registry: &Registry,
24 random: &mut WorldgenRandom,
25 decorators: &[TreeDecorator],
26 placement: &mut TreePlacement,
27 biome_zoom_seed: i64,
28 place_nested: NestedFeaturePlacer<L>,
29 ) {
30 for decorator in decorators {
31 match decorator {
32 TreeDecorator::AlterGround { provider } => {
33 Self::place_alter_ground_tree_decorator(
34 region, registry, random, provider, placement,
35 );
36 }
37 TreeDecorator::Beehive { probability } => {
38 Self::place_beehive_tree_decorator(
39 region,
40 registry,
41 random,
42 *probability,
43 placement,
44 );
45 }
46 TreeDecorator::Cocoa { probability } => {
47 Self::place_cocoa_tree_decorator(
48 region,
49 registry,
50 random,
51 *probability,
52 placement,
53 );
54 }
55 TreeDecorator::LeaveVine { probability } => {
56 Self::place_leave_vine_tree_decorator(region, random, *probability, placement);
57 }
58 TreeDecorator::TrunkVine => {
59 Self::place_trunk_vine_tree_decorator(region, random, placement);
60 }
61 TreeDecorator::PlaceOnGround(decorator) => {
62 Self::place_on_ground_tree_decorator(
63 region, registry, random, decorator, placement,
64 );
65 }
66 TreeDecorator::AttachedToLeaves(decorator) => {
67 Self::place_attached_to_leaves_tree_decorator(
68 region, registry, random, decorator, placement,
69 );
70 }
71 TreeDecorator::AttachedToLogs(decorator) => {
72 Self::place_attached_to_logs_tree_decorator(
73 region, registry, random, decorator, placement,
74 );
75 }
76 TreeDecorator::PaleMoss {
77 leaves_probability,
78 trunk_probability,
79 ground_probability,
80 } => {
81 Self::place_pale_moss_tree_decorator(
82 region,
83 registry,
84 random,
85 *leaves_probability,
86 *trunk_probability,
87 *ground_probability,
88 placement,
89 biome_zoom_seed,
90 place_nested,
91 );
92 }
93 TreeDecorator::CreakingHeart { probability } => {
94 Self::place_creaking_heart_tree_decorator(
95 region,
96 registry,
97 random,
98 *probability,
99 placement,
100 );
101 }
102 }
103 }
104 }
105
106 fn place_alter_ground_tree_decorator(
107 region: &mut impl LevelAccessor,
108 registry: &Registry,
109 random: &mut WorldgenRandom,
110 provider: &BlockStateProvider,
111 placement: &mut TreePlacement,
112 ) {
113 let positions = Self::lowest_tree_trunks_or_roots(placement);
114 let Some(first_pos) = positions.first() else {
115 return;
116 };
117 let min_y = first_pos.y();
118
119 for pos in positions.into_iter().filter(|pos| pos.y() == min_y) {
120 Self::place_alter_ground_circle(
121 region,
122 registry,
123 random,
124 provider,
125 pos.offset(-1, 0, -1),
126 placement,
127 );
128 Self::place_alter_ground_circle(
129 region,
130 registry,
131 random,
132 provider,
133 pos.offset(2, 0, -1),
134 placement,
135 );
136 Self::place_alter_ground_circle(
137 region,
138 registry,
139 random,
140 provider,
141 pos.offset(-1, 0, 2),
142 placement,
143 );
144 Self::place_alter_ground_circle(
145 region,
146 registry,
147 random,
148 provider,
149 pos.offset(2, 0, 2),
150 placement,
151 );
152
153 for _ in 0..5 {
154 let placement_offset = random.next_i32_bounded(64);
155 let x = placement_offset % 8;
156 let z = placement_offset / 8;
157 if x == 0 || x == 7 || z == 0 || z == 7 {
158 Self::place_alter_ground_circle(
159 region,
160 registry,
161 random,
162 provider,
163 pos.offset(-3 + x, 0, -3 + z),
164 placement,
165 );
166 }
167 }
168 }
169 }
170
171 fn place_alter_ground_circle(
172 region: &mut impl LevelAccessor,
173 registry: &Registry,
174 random: &mut WorldgenRandom,
175 provider: &BlockStateProvider,
176 pos: BlockPos,
177 placement: &mut TreePlacement,
178 ) {
179 for x in -2i32..=2 {
180 for z in -2i32..=2 {
181 if x.abs() != 2 || z.abs() != 2 {
182 Self::place_alter_ground_block_at(
183 region,
184 registry,
185 random,
186 provider,
187 pos.offset(x, 0, z),
188 placement,
189 );
190 }
191 }
192 }
193 }
194
195 fn place_alter_ground_block_at(
196 region: &mut impl LevelAccessor,
197 registry: &Registry,
198 random: &mut WorldgenRandom,
199 provider: &BlockStateProvider,
200 pos: BlockPos,
201 placement: &mut TreePlacement,
202 ) {
203 for y in (-3..=2).rev() {
204 let cursor = pos.above_n(y);
205 if let Some(state) = Self::sample_block_state_provider_optional(
206 region, registry, random, provider, cursor,
207 ) {
208 placement.set_decoration(region, cursor, state);
209 break;
210 }
211
212 if !region.get_block_state(cursor).is_air() && y < 0 {
213 break;
214 }
215 }
216 }
217
218 fn place_on_ground_tree_decorator(
219 region: &mut impl LevelAccessor,
220 registry: &Registry,
221 random: &mut WorldgenRandom,
222 decorator: &PlaceOnGroundDecorator,
223 placement: &mut TreePlacement,
224 ) {
225 let positions = Self::lowest_tree_trunks_or_roots(placement);
226 let Some(origin) = positions.first() else {
227 return;
228 };
229 let min_y = origin.y();
230 let mut min_x = origin.x();
231 let mut max_x = origin.x();
232 let mut min_z = origin.z();
233 let mut max_z = origin.z();
234
235 for position in positions {
236 if position.y() == min_y {
237 min_x = min_x.min(position.x());
238 max_x = max_x.max(position.x());
239 min_z = min_z.min(position.z());
240 max_z = max_z.max(position.z());
241 }
242 }
243
244 min_x -= decorator.radius;
245 max_x += decorator.radius;
246 min_z -= decorator.radius;
247 max_z += decorator.radius;
248 let min_y = min_y - decorator.height;
249 let max_y = min_y + decorator.height * 2;
250
251 for _ in 0..decorator.tries {
252 let pos = BlockPos::new(
253 random.next_i32_between(min_x, max_x),
254 random.next_i32_between(min_y, max_y),
255 random.next_i32_between(min_z, max_z),
256 );
257 Self::attempt_place_tree_ground_decorator(
258 region,
259 registry,
260 random,
261 &decorator.block_state_provider,
262 pos,
263 placement,
264 );
265 }
266 }
267
268 fn attempt_place_tree_ground_decorator(
269 region: &mut impl LevelAccessor,
270 registry: &Registry,
271 random: &mut WorldgenRandom,
272 provider: &BlockStateProvider,
273 pos: BlockPos,
274 placement: &mut TreePlacement,
275 ) {
276 let above = pos.above();
277 let above_state = region.get_block_state(above);
278 if !above_state.is_air() && above_state.get_block() != &vanilla_blocks::VINE {
279 return;
280 }
281 if !region.get_block_state(pos).is_solid_render() {
282 return;
283 }
284 if region.height_at(HeightmapType::MotionBlockingNoLeaves, pos.x(), pos.z()) > above.y() {
285 return;
286 }
287
288 let state = Self::sample_block_state_provider(region, registry, random, provider, above);
289 placement.set_decoration(region, above, state);
290 }
291
292 fn place_trunk_vine_tree_decorator(
293 region: &mut impl LevelAccessor,
294 random: &mut WorldgenRandom,
295 placement: &mut TreePlacement,
296 ) {
297 for log in Self::sorted_tree_positions(&placement.trunks) {
298 if random.next_i32_bounded(3) > 0 {
299 Self::try_place_tree_vine(
300 region,
301 placement,
302 log.relative(Direction::West),
303 Direction::East,
304 );
305 }
306 if random.next_i32_bounded(3) > 0 {
307 Self::try_place_tree_vine(
308 region,
309 placement,
310 log.relative(Direction::East),
311 Direction::West,
312 );
313 }
314 if random.next_i32_bounded(3) > 0 {
315 Self::try_place_tree_vine(
316 region,
317 placement,
318 log.relative(Direction::North),
319 Direction::South,
320 );
321 }
322 if random.next_i32_bounded(3) > 0 {
323 Self::try_place_tree_vine(
324 region,
325 placement,
326 log.relative(Direction::South),
327 Direction::North,
328 );
329 }
330 }
331 }
332
333 fn place_leave_vine_tree_decorator(
334 region: &mut impl LevelAccessor,
335 random: &mut WorldgenRandom,
336 probability: f32,
337 placement: &mut TreePlacement,
338 ) {
339 for leaf in Self::sorted_tree_positions(&placement.foliage) {
340 if random.next_f32() < probability {
341 Self::try_place_hanging_tree_vine(
342 region,
343 placement,
344 leaf.relative(Direction::West),
345 Direction::East,
346 );
347 }
348 if random.next_f32() < probability {
349 Self::try_place_hanging_tree_vine(
350 region,
351 placement,
352 leaf.relative(Direction::East),
353 Direction::West,
354 );
355 }
356 if random.next_f32() < probability {
357 Self::try_place_hanging_tree_vine(
358 region,
359 placement,
360 leaf.relative(Direction::North),
361 Direction::South,
362 );
363 }
364 if random.next_f32() < probability {
365 Self::try_place_hanging_tree_vine(
366 region,
367 placement,
368 leaf.relative(Direction::South),
369 Direction::North,
370 );
371 }
372 }
373 }
374
375 fn try_place_hanging_tree_vine(
376 region: &mut impl LevelAccessor,
377 placement: &mut TreePlacement,
378 pos: BlockPos,
379 vine_face: Direction,
380 ) {
381 if !Self::try_place_tree_vine(region, placement, pos, vine_face) {
382 return;
383 }
384
385 let mut pos = pos.below();
386 let mut max_length = 4;
387 while region.get_block_state(pos).is_air() && max_length > 0 {
388 Self::place_tree_vine(region, placement, pos, vine_face);
389 pos = pos.below();
390 max_length -= 1;
391 }
392 }
393
394 fn place_cocoa_tree_decorator(
395 region: &mut impl LevelAccessor,
396 registry: &Registry,
397 random: &mut WorldgenRandom,
398 probability: f32,
399 placement: &mut TreePlacement,
400 ) {
401 if random.next_f32() >= probability {
402 return;
403 }
404
405 let logs = Self::sorted_tree_positions(&placement.trunks);
406 let Some(first_log) = logs.first() else {
407 return;
408 };
409 let tree_y = first_log.y();
410
411 for log in logs.into_iter().filter(|pos| pos.y() - tree_y <= 2) {
412 for direction in Self::VANILLA_HORIZONTAL_DIRECTIONS {
413 if random.next_f32() > 0.25 {
414 continue;
415 }
416
417 let cocoa_pos = log.relative(direction.opposite());
418 if !region.get_block_state(cocoa_pos).is_air() {
419 continue;
420 }
421
422 let age = random.next_i32_bounded(3) as u8;
423 let cocoa_state = registry
424 .blocks
425 .get_default_state_id(&vanilla_blocks::COCOA)
426 .set_value(&BlockStateProperties::AGE_2, age)
427 .set_value(&BlockStateProperties::HORIZONTAL_FACING, direction);
428 placement.set_decoration(region, cocoa_pos, cocoa_state);
429 }
430 }
431 }
432
433 fn try_place_tree_vine(
434 region: &mut impl LevelAccessor,
435 placement: &mut TreePlacement,
436 pos: BlockPos,
437 vine_face: Direction,
438 ) -> bool {
439 if !region.get_block_state(pos).is_air() {
440 return false;
441 }
442
443 Self::place_tree_vine(region, placement, pos, vine_face);
444 true
445 }
446
447 fn place_tree_vine(
448 region: &mut impl LevelAccessor,
449 placement: &mut TreePlacement,
450 pos: BlockPos,
451 vine_face: Direction,
452 ) {
453 placement.set_decoration(region, pos, Self::vine_state_for_face(vine_face));
454 }
455
456 fn lowest_tree_trunks_or_roots(placement: &TreePlacement) -> Vec<BlockPos> {
457 let roots = Self::sorted_tree_positions(&placement.roots);
458 let logs = Self::sorted_tree_positions(&placement.trunks);
459 if roots.is_empty() {
460 return logs;
461 }
462
463 if logs.first().is_some_and(|log| roots[0].y() == log.y()) {
464 let mut positions = logs;
465 positions.extend(roots);
466 positions
467 } else {
468 roots
469 }
470 }
471
472 fn place_beehive_tree_decorator(
473 region: &mut impl LevelAccessor,
474 registry: &Registry,
475 random: &mut WorldgenRandom,
476 probability: f32,
477 placement: &mut TreePlacement,
478 ) {
479 let logs = Self::sorted_tree_positions(&placement.trunks);
480 if logs.is_empty() || random.next_f32() >= probability {
481 return;
482 }
483
484 let leaves = Self::sorted_tree_positions(&placement.foliage);
485 let hive_y = if let Some(first_leaf) = leaves.first() {
486 (first_leaf.y() - 1).max(logs[0].y() + 1)
487 } else {
488 let log_y = logs[0].y() + 1 + random.next_i32_bounded(3);
489 let last_log_y = logs[logs.len() - 1].y();
490 log_y.min(last_log_y)
491 };
492
493 let mut hive_placements = Vec::new();
494 for log in logs.iter().copied().filter(|pos| pos.y() == hive_y) {
495 for direction in BEEHIVE_SPAWN_DIRECTIONS {
496 hive_placements.push(log.relative(direction));
497 }
498 }
499
500 if hive_placements.is_empty() {
501 return;
502 }
503
504 Self::shuffle_tree_positions(random, &mut hive_placements);
505 let hive_pos = hive_placements.into_iter().find(|pos| {
506 region.get_block_state(*pos).is_air()
507 && region
508 .get_block_state(pos.relative(BEEHIVE_WORLDGEN_FACING))
509 .is_air()
510 });
511 let Some(hive_pos) = hive_pos else {
512 return;
513 };
514
515 let hive_state = registry
516 .blocks
517 .get_default_state_id(&vanilla_blocks::BEE_NEST)
518 .set_value(
519 &BlockStateProperties::HORIZONTAL_FACING,
520 BEEHIVE_WORLDGEN_FACING,
521 );
522 placement.set_decoration(region, hive_pos, hive_state);
523
524 let Some(block_entity) = region.get_block_entity(hive_pos) else {
525 return;
526 };
527 let Some(beehive) = block_entity.downcast_ref::<BeehiveBlockEntity>() else {
528 return;
529 };
530
531 let num_bees = 2 + random.next_i32_bounded(2);
532 for _ in 0..num_bees {
533 beehive.store_worldgen_bee(random.next_i32_bounded(599));
534 }
535 }
536
537 fn place_attached_to_leaves_tree_decorator(
538 region: &mut impl LevelAccessor,
539 registry: &Registry,
540 random: &mut WorldgenRandom,
541 decorator: &AttachedToLeavesDecorator,
542 placement: &mut TreePlacement,
543 ) {
544 let mut blacklist = FxHashSet::default();
545 let mut leaves = Self::sorted_tree_positions(&placement.foliage);
546 Self::shuffle_tree_positions(random, &mut leaves);
547
548 for leaf in leaves {
549 let direction = Self::random_tree_decorator_direction(random, &decorator.directions);
550 let place_pos = leaf.relative(direction);
551 if blacklist.contains(&place_pos)
552 || random.next_f32() >= decorator.probability
553 || !Self::tree_decorator_has_required_empty_blocks(
554 region,
555 leaf,
556 direction,
557 decorator.required_empty_blocks,
558 )
559 {
560 continue;
561 }
562
563 Self::blacklist_attached_tree_decoration_area(
564 &mut blacklist,
565 place_pos,
566 decorator.exclusion_radius_xz,
567 decorator.exclusion_radius_y,
568 );
569 let state = Self::sample_block_state_provider(
570 region,
571 registry,
572 random,
573 &decorator.block_provider,
574 place_pos,
575 );
576 placement.set_decoration(region, place_pos, state);
577 }
578 }
579
580 fn place_attached_to_logs_tree_decorator(
581 region: &mut impl LevelAccessor,
582 registry: &Registry,
583 random: &mut WorldgenRandom,
584 decorator: &AttachedToLogsDecorator,
585 placement: &mut TreePlacement,
586 ) {
587 let mut logs = Self::sorted_tree_positions(&placement.trunks);
588 Self::shuffle_tree_positions(random, &mut logs);
589
590 for log in logs {
591 let direction = Self::random_tree_decorator_direction(random, &decorator.directions);
592 let place_pos = log.relative(direction);
593 if random.next_f32() > decorator.probability
594 || !region.get_block_state(place_pos).is_air()
595 {
596 continue;
597 }
598
599 let state = Self::sample_block_state_provider(
600 region,
601 registry,
602 random,
603 &decorator.block_provider,
604 place_pos,
605 );
606 placement.set_decoration(region, place_pos, state);
607 }
608 }
609
610 fn place_pale_moss_tree_decorator<L: LevelAccessor>(
611 region: &mut L,
612 registry: &Registry,
613 random: &mut WorldgenRandom,
614 leaves_probability: f32,
615 trunk_probability: f32,
616 ground_probability: f32,
617 placement: &mut TreePlacement,
618 biome_zoom_seed: i64,
619 place_nested: NestedFeaturePlacer<L>,
620 ) {
621 let mut shuffled_logs = Self::sorted_tree_positions(&placement.trunks);
622 Self::shuffle_tree_positions(random, &mut shuffled_logs);
623 let Some(origin) = shuffled_logs.into_iter().min_by_key(BlockPos::y) else {
624 return;
625 };
626
627 if random.next_f32() < ground_probability {
628 let pale_moss_patch_key = Identifier::vanilla_static("pale_moss_patch");
629 let Some(pale_moss_patch) = registry.configured_features.by_key(&pale_moss_patch_key)
630 else {
631 panic!(
632 "pale moss tree decorator references unknown configured feature {pale_moss_patch_key}"
633 );
634 };
635 place_nested(
636 region,
637 registry,
638 random,
639 &pale_moss_patch.kind,
640 origin.above(),
641 biome_zoom_seed,
642 );
643 }
644
645 for log in Self::sorted_tree_positions(&placement.trunks) {
646 if random.next_f32() < trunk_probability {
647 let down = log.below();
648 if region.get_block_state(down).is_air() {
649 Self::add_pale_moss_hanger(region, random, down, placement);
650 }
651 }
652 }
653
654 for leaf in Self::sorted_tree_positions(&placement.foliage) {
655 if random.next_f32() < leaves_probability {
656 let down = leaf.below();
657 if region.get_block_state(down).is_air() {
658 Self::add_pale_moss_hanger(region, random, down, placement);
659 }
660 }
661 }
662 }
663
664 fn place_creaking_heart_tree_decorator(
665 region: &mut impl LevelAccessor,
666 registry: &Registry,
667 random: &mut WorldgenRandom,
668 probability: f32,
669 placement: &mut TreePlacement,
670 ) {
671 if placement.trunks.is_empty() || random.next_f32() >= probability {
672 return;
673 }
674
675 let mut heart_placements = Self::sorted_tree_positions(&placement.trunks);
676 Self::shuffle_tree_positions(random, &mut heart_placements);
677 let Some(target_pos) = heart_placements.into_iter().find(|pos| {
678 Self::VANILLA_DIRECTION_VALUES.iter().all(|direction| {
679 region
680 .get_block_state(pos.relative(*direction))
681 .get_block()
682 .has_tag(&BlockTag::LOGS)
683 })
684 }) else {
685 return;
686 };
687
688 let state = registry
689 .blocks
690 .get_default_state_id(&vanilla_blocks::CREAKING_HEART)
691 .set_value(
692 &BlockStateProperties::CREAKING_HEART_STATE,
693 CreakingHeartState::Dormant,
694 )
695 .set_value(&BlockStateProperties::NATURAL, true);
696 placement.set_decoration(region, target_pos, state);
697 }
698
699 fn add_pale_moss_hanger(
700 region: &mut impl LevelAccessor,
701 random: &mut WorldgenRandom,
702 mut pos: BlockPos,
703 placement: &mut TreePlacement,
704 ) {
705 while region.get_block_state(pos.below()).is_air() {
706 if random.next_f32() < 0.5 {
707 break;
708 }
709
710 let state = vanilla_blocks::PALE_HANGING_MOSS
711 .default_state()
712 .set_value(&BlockStateProperties::TIP, false);
713 placement.set_decoration(region, pos, state);
714 pos = pos.below();
715 }
716
717 let state = vanilla_blocks::PALE_HANGING_MOSS
718 .default_state()
719 .set_value(&BlockStateProperties::TIP, true);
720 placement.set_decoration(region, pos, state);
721 }
722
723 fn tree_decorator_has_required_empty_blocks(
724 region: &mut impl LevelAccessor,
725 leaf: BlockPos,
726 direction: Direction,
727 required_empty_blocks: i32,
728 ) -> bool {
729 (1..=required_empty_blocks).all(|offset| {
730 region
731 .get_block_state(leaf.relative_n(direction, offset))
732 .is_air()
733 })
734 }
735
736 fn blacklist_attached_tree_decoration_area(
737 blacklist: &mut FxHashSet<BlockPos>,
738 center: BlockPos,
739 radius_xz: i32,
740 radius_y: i32,
741 ) {
742 for x in -radius_xz..=radius_xz {
743 for y in -radius_y..=radius_y {
744 for z in -radius_xz..=radius_xz {
745 blacklist.insert(center.offset(x, y, z));
746 }
747 }
748 }
749 }
750
751 fn random_tree_decorator_direction(
752 random: &mut WorldgenRandom,
753 directions: &[Direction],
754 ) -> Direction {
755 assert!(
756 !directions.is_empty(),
757 "attached tree decorator direction list must not be empty"
758 );
759 let Ok(direction_count) = i32::try_from(directions.len()) else {
760 panic!("attached tree decorator direction count exceeds i32 range");
761 };
762 directions[random.next_i32_bounded(direction_count) as usize]
763 }
764
765 fn sorted_tree_positions(positions: &JavaBlockPosSet) -> Vec<BlockPos> {
766 let mut positions = positions.java_ordered_positions();
767 positions.sort_by_key(BlockPos::y);
768 positions
769 }
770
771 fn shuffle_tree_positions(random: &mut WorldgenRandom, positions: &mut [BlockPos]) {
772 for i in (1..positions.len()).rev() {
773 let Ok(bound) = i32::try_from(i + 1) else {
774 panic!("tree decorator shuffle length exceeds i32 range");
775 };
776 let j = random.next_i32_bounded(bound) as usize;
777 positions.swap(i, j);
778 }
779 }
780}