Skip to main content

steel_core/worldgen/template/
processors.rs

1use super::*;
2
3impl StructureTemplate {
4    /// `StructureLayoutOptimizer`: skip out-of-bounds blocks before processors run.
5    /// Disabled when a `Capped` processor is present — it needs the full block list
6    /// in `finalize_processing` (Trail Ruins).
7    pub(super) fn pre_filters_placement_bounds(processors: &[StructureProcessorKind]) -> bool {
8        !processors
9            .iter()
10            .any(|processor| matches!(processor, StructureProcessorKind::Capped { .. }))
11    }
12
13    pub(super) fn palette_blocks_for_placement<F: FnMut(&StructureBlockInfo, BlockPos)>(
14        blocks: &[StructureBlockInfo],
15        position: BlockPos,
16        settings: &StructurePlaceSettings<'_>,
17        mut f: F,
18    ) {
19        if !Self::pre_filters_placement_bounds(settings.processors) {
20            for block in blocks {
21                f(
22                    block,
23                    Self::transformed_position(position, block.pos, settings),
24                );
25            }
26            return;
27        }
28
29        for block in blocks {
30            let world_pos = Self::transformed_position(position, block.pos, settings);
31            if settings.bounding_box.contains_blockpos(world_pos) {
32                f(block, world_pos);
33            }
34        }
35    }
36
37    pub(super) const fn transformed_position(
38        position: BlockPos,
39        template_pos: BlockPos,
40        settings: &StructurePlaceSettings<'_>,
41    ) -> BlockPos {
42        let transformed = Self::calculate_relative_position(
43            template_pos,
44            settings.mirror,
45            settings.rotation,
46            settings.rotation_pivot,
47        );
48        position.offset(transformed.x(), transformed.y(), transformed.z())
49    }
50
51    pub(super) fn process_block(
52        region: &WorldGenRegion<'_>,
53        registry: &Registry,
54        original: &ProcessedBlockInfo,
55        initial: ProcessedBlockInfo,
56        settings: &StructurePlaceSettings<'_>,
57        reference_pos: BlockPos,
58        random: &mut WorldgenRandom,
59    ) -> Option<ProcessedBlockInfo> {
60        let mut current = initial;
61        if settings.block_ignore.ignores(registry, current.state) {
62            return None;
63        }
64
65        if settings.replace_jigsaws {
66            current = Self::replace_jigsaw_block(registry, current)?;
67        }
68
69        for processor in settings.processors {
70            current = Self::process_block_with_processor(
71                region,
72                registry,
73                processor,
74                original,
75                current,
76                settings,
77                reference_pos,
78                random,
79            )?;
80        }
81        if settings.projection == Some(Projection::TerrainMatching) {
82            current = Self::apply_terrain_matching_projection(region, original, current);
83        }
84        if settings.late_block_ignore.ignores(registry, current.state) {
85            return None;
86        }
87        Some(current)
88    }
89
90    #[expect(
91        clippy::too_many_arguments,
92        reason = "processor calls mirror vanilla StructureProcessor.processBlock inputs"
93    )]
94    pub(super) fn process_block_with_processor(
95        region: &WorldGenRegion<'_>,
96        registry: &Registry,
97        processor: &StructureProcessorKind,
98        original: &ProcessedBlockInfo,
99        current: ProcessedBlockInfo,
100        settings: &StructurePlaceSettings<'_>,
101        reference_pos: BlockPos,
102        random: &mut WorldgenRandom,
103    ) -> Option<ProcessedBlockInfo> {
104        match processor {
105            StructureProcessorKind::BlockRot {
106                rottable_blocks,
107                integrity,
108            } => {
109                if rottable_blocks.as_ref().is_some_and(|tag| {
110                    !registry
111                        .blocks
112                        .is_in_tag(Self::block_for_state(registry, original.state), tag)
113                }) {
114                    return Some(current);
115                }
116                (Self::processor_next_f32(settings, current.world_pos, random) <= *integrity)
117                    .then_some(current)
118            }
119            StructureProcessorKind::ProtectedBlocks { cannot_replace } => {
120                let existing =
121                    Self::block_for_state(registry, region.block_state(current.world_pos));
122                (!existing.has_tag(cannot_replace)).then_some(current)
123            }
124            StructureProcessorKind::Rule { rules } => {
125                let mut rule_random =
126                    LegacyRandom::from_seed(Self::block_pos_seed(current.world_pos) as u64);
127                let location_state = region.block_state(current.world_pos);
128                for rule in rules {
129                    if Self::rule_matches(
130                        registry,
131                        rule,
132                        current.state,
133                        location_state,
134                        original.template_pos,
135                        current.world_pos,
136                        reference_pos,
137                        &mut rule_random,
138                    ) {
139                        return Some(Self::apply_rule(registry, rule, current, &mut rule_random));
140                    }
141                }
142                Some(current)
143            }
144            StructureProcessorKind::BlockAge { mossiness } => Some(Self::process_block_age(
145                registry, current, *mossiness, settings, random,
146            )),
147            StructureProcessorKind::LavaSubmergedBlock => Some(Self::process_lava_submerged_block(
148                registry,
149                region.block_state(current.world_pos),
150                current,
151            )),
152            StructureProcessorKind::BlackstoneReplace => {
153                Some(Self::process_blackstone_replace(registry, current))
154            }
155            StructureProcessorKind::Capped { .. } => Some(current),
156        }
157    }
158
159    pub(super) fn process_block_age(
160        registry: &Registry,
161        current: ProcessedBlockInfo,
162        mossiness: f32,
163        settings: &StructurePlaceSettings<'_>,
164        random: &mut WorldgenRandom,
165    ) -> ProcessedBlockInfo {
166        match settings.processor_random {
167            StructureProcessorRandom::Placement => {
168                Self::process_block_age_with_random(registry, current, mossiness, random)
169            }
170            StructureProcessorRandom::Positional => {
171                let mut random =
172                    LegacyRandom::from_seed(Self::block_pos_seed(current.world_pos) as u64);
173                Self::process_block_age_with_random(registry, current, mossiness, &mut random)
174            }
175        }
176    }
177
178    pub(super) fn process_block_age_with_random(
179        registry: &Registry,
180        mut current: ProcessedBlockInfo,
181        mossiness: f32,
182        random: &mut impl Random,
183    ) -> ProcessedBlockInfo {
184        let block = Self::block_for_state(registry, current.state);
185        let new_state = if block == &vanilla_blocks::STONE_BRICKS
186            || block == &vanilla_blocks::STONE
187            || block == &vanilla_blocks::CHISELED_STONE_BRICKS
188        {
189            Self::maybe_replace_full_stone_block(registry, mossiness, random)
190        } else if block.has_tag(&BlockTag::STAIRS) {
191            Self::maybe_replace_stairs(registry, current.state, mossiness, random)
192        } else if block.has_tag(&BlockTag::SLABS) {
193            Self::maybe_replace_slab(registry, current.state, mossiness, random)
194        } else if block.has_tag(&BlockTag::WALLS) {
195            Self::maybe_replace_wall(registry, current.state, mossiness, random)
196        } else if block == &vanilla_blocks::OBSIDIAN {
197            Self::maybe_replace_obsidian(registry, random)
198        } else {
199            None
200        };
201
202        if let Some(new_state) = new_state {
203            current.state = new_state;
204        }
205        current
206    }
207
208    pub(super) fn maybe_replace_full_stone_block(
209        registry: &Registry,
210        mossiness: f32,
211        random: &mut impl Random,
212    ) -> Option<BlockStateId> {
213        if random.next_f32() >= 0.5 {
214            return None;
215        }
216
217        let non_mossy = [
218            registry
219                .blocks
220                .get_default_state_id(&vanilla_blocks::CRACKED_STONE_BRICKS),
221            Self::random_facing_stairs(registry, &vanilla_blocks::STONE_BRICK_STAIRS, random),
222        ];
223        let mossy = [
224            registry
225                .blocks
226                .get_default_state_id(&vanilla_blocks::MOSSY_STONE_BRICKS),
227            Self::random_facing_stairs(registry, &vanilla_blocks::MOSSY_STONE_BRICK_STAIRS, random),
228        ];
229        let candidates = if random.next_f32() < mossiness {
230            mossy
231        } else {
232            non_mossy
233        };
234        Some(candidates[random.next_i32_bounded(2) as usize])
235    }
236
237    pub(super) fn maybe_replace_stairs(
238        registry: &Registry,
239        state: BlockStateId,
240        mossiness: f32,
241        random: &mut impl Random,
242    ) -> Option<BlockStateId> {
243        if random.next_f32() >= 0.5 {
244            return None;
245        }
246
247        let non_mossy = [
248            registry
249                .blocks
250                .get_default_state_id(&vanilla_blocks::STONE_SLAB),
251            registry
252                .blocks
253                .get_default_state_id(&vanilla_blocks::STONE_BRICK_SLAB),
254        ];
255        let mossy = [
256            registry
257                .blocks
258                .copy_matching_properties(state, &vanilla_blocks::MOSSY_STONE_BRICK_STAIRS),
259            registry
260                .blocks
261                .get_default_state_id(&vanilla_blocks::MOSSY_STONE_BRICK_SLAB),
262        ];
263        let candidates = if random.next_f32() < mossiness {
264            mossy
265        } else {
266            non_mossy
267        };
268        Some(candidates[random.next_i32_bounded(2) as usize])
269    }
270
271    pub(super) fn maybe_replace_slab(
272        registry: &Registry,
273        state: BlockStateId,
274        mossiness: f32,
275        random: &mut impl Random,
276    ) -> Option<BlockStateId> {
277        (random.next_f32() < mossiness).then(|| {
278            registry
279                .blocks
280                .copy_matching_properties(state, &vanilla_blocks::MOSSY_STONE_BRICK_SLAB)
281        })
282    }
283
284    pub(super) fn maybe_replace_wall(
285        registry: &Registry,
286        state: BlockStateId,
287        mossiness: f32,
288        random: &mut impl Random,
289    ) -> Option<BlockStateId> {
290        (random.next_f32() < mossiness).then(|| {
291            registry
292                .blocks
293                .copy_matching_properties(state, &vanilla_blocks::MOSSY_STONE_BRICK_WALL)
294        })
295    }
296
297    pub(super) fn maybe_replace_obsidian(
298        registry: &Registry,
299        random: &mut impl Random,
300    ) -> Option<BlockStateId> {
301        (random.next_f32() < 0.15).then(|| {
302            registry
303                .blocks
304                .get_default_state_id(&vanilla_blocks::CRYING_OBSIDIAN)
305        })
306    }
307
308    pub(super) fn random_facing_stairs(
309        registry: &Registry,
310        block: BlockRef,
311        random: &mut impl Random,
312    ) -> BlockStateId {
313        const HORIZONTAL_DIRECTIONS: [BlockPropertyDirection; 4] = [
314            BlockPropertyDirection::North,
315            BlockPropertyDirection::East,
316            BlockPropertyDirection::South,
317            BlockPropertyDirection::West,
318        ];
319
320        let facing = HORIZONTAL_DIRECTIONS[random.next_i32_bounded(4) as usize];
321        let half = if random.next_i32_bounded(2) == 0 {
322            Half::Top
323        } else {
324            Half::Bottom
325        };
326        let state = registry.blocks.get_default_state_id(block);
327        let state = registry
328            .blocks
329            .set_property(state, &BlockStateProperties::FACING, facing);
330        registry
331            .blocks
332            .set_property(state, &BlockStateProperties::HALF, half)
333    }
334
335    pub(super) fn process_lava_submerged_block(
336        registry: &Registry,
337        existing_state: BlockStateId,
338        mut current: ProcessedBlockInfo,
339    ) -> ProcessedBlockInfo {
340        if Self::block_for_state(registry, existing_state) == &vanilla_blocks::LAVA
341            && !blocks::shapes::is_offset_shape_full_block(
342                registry
343                    .blocks
344                    .get_outline_shape_at(current.state, current.world_pos),
345            )
346        {
347            current.state = registry.blocks.get_default_state_id(&vanilla_blocks::LAVA);
348        }
349        current
350    }
351
352    pub(super) fn process_blackstone_replace(
353        registry: &Registry,
354        mut current: ProcessedBlockInfo,
355    ) -> ProcessedBlockInfo {
356        let Some(block) =
357            Self::blackstone_replacement_block(Self::block_for_state(registry, current.state))
358        else {
359            return current;
360        };
361
362        let mut new_state = registry.blocks.get_default_state_id(block);
363        if let Some(facing) = registry
364            .blocks
365            .try_get_property(current.state, &BlockStateProperties::FACING)
366            && registry
367                .blocks
368                .try_get_property(new_state, &BlockStateProperties::FACING)
369                .is_some()
370        {
371            new_state =
372                registry
373                    .blocks
374                    .set_property(new_state, &BlockStateProperties::FACING, facing);
375        }
376        if let Some(half) = registry
377            .blocks
378            .try_get_property(current.state, &BlockStateProperties::HALF)
379            && registry
380                .blocks
381                .try_get_property(new_state, &BlockStateProperties::HALF)
382                .is_some()
383        {
384            new_state = registry
385                .blocks
386                .set_property(new_state, &BlockStateProperties::HALF, half);
387        }
388        if let Some(slab_type) = registry
389            .blocks
390            .try_get_property(current.state, &BlockStateProperties::SLAB_TYPE)
391            && registry
392                .blocks
393                .try_get_property(new_state, &BlockStateProperties::SLAB_TYPE)
394                .is_some()
395        {
396            new_state = registry.blocks.set_property(
397                new_state,
398                &BlockStateProperties::SLAB_TYPE,
399                slab_type,
400            );
401        }
402
403        current.state = new_state;
404        current
405    }
406
407    pub(super) fn blackstone_replacement_block(block: BlockRef) -> Option<BlockRef> {
408        if block == &vanilla_blocks::COBBLESTONE || block == &vanilla_blocks::MOSSY_COBBLESTONE {
409            Some(&vanilla_blocks::BLACKSTONE)
410        } else if block == &vanilla_blocks::STONE {
411            Some(&vanilla_blocks::POLISHED_BLACKSTONE)
412        } else if block == &vanilla_blocks::STONE_BRICKS
413            || block == &vanilla_blocks::MOSSY_STONE_BRICKS
414        {
415            Some(&vanilla_blocks::POLISHED_BLACKSTONE_BRICKS)
416        } else if block == &vanilla_blocks::COBBLESTONE_STAIRS
417            || block == &vanilla_blocks::MOSSY_COBBLESTONE_STAIRS
418        {
419            Some(&vanilla_blocks::BLACKSTONE_STAIRS)
420        } else if block == &vanilla_blocks::STONE_STAIRS {
421            Some(&vanilla_blocks::POLISHED_BLACKSTONE_STAIRS)
422        } else if block == &vanilla_blocks::STONE_BRICK_STAIRS
423            || block == &vanilla_blocks::MOSSY_STONE_BRICK_STAIRS
424        {
425            Some(&vanilla_blocks::POLISHED_BLACKSTONE_BRICK_STAIRS)
426        } else if block == &vanilla_blocks::COBBLESTONE_SLAB
427            || block == &vanilla_blocks::MOSSY_COBBLESTONE_SLAB
428        {
429            Some(&vanilla_blocks::BLACKSTONE_SLAB)
430        } else if block == &vanilla_blocks::SMOOTH_STONE_SLAB
431            || block == &vanilla_blocks::STONE_SLAB
432        {
433            Some(&vanilla_blocks::POLISHED_BLACKSTONE_SLAB)
434        } else if block == &vanilla_blocks::STONE_BRICK_SLAB
435            || block == &vanilla_blocks::MOSSY_STONE_BRICK_SLAB
436        {
437            Some(&vanilla_blocks::POLISHED_BLACKSTONE_BRICK_SLAB)
438        } else if block == &vanilla_blocks::STONE_BRICK_WALL
439            || block == &vanilla_blocks::MOSSY_STONE_BRICK_WALL
440        {
441            Some(&vanilla_blocks::POLISHED_BLACKSTONE_BRICK_WALL)
442        } else if block == &vanilla_blocks::COBBLESTONE_WALL
443            || block == &vanilla_blocks::MOSSY_COBBLESTONE_WALL
444        {
445            Some(&vanilla_blocks::BLACKSTONE_WALL)
446        } else if block == &vanilla_blocks::CHISELED_STONE_BRICKS {
447            Some(&vanilla_blocks::CHISELED_POLISHED_BLACKSTONE)
448        } else if block == &vanilla_blocks::CRACKED_STONE_BRICKS {
449            Some(&vanilla_blocks::CRACKED_POLISHED_BLACKSTONE_BRICKS)
450        } else if block == &vanilla_blocks::IRON_BARS {
451            Some(&vanilla_blocks::IRON_CHAIN)
452        } else {
453            None
454        }
455    }
456
457    #[expect(
458        clippy::too_many_arguments,
459        reason = "processor finalization receives vanilla's full template processing context"
460    )]
461    pub(super) fn finalize_processing(
462        region: &WorldGenRegion<'_>,
463        registry: &Registry,
464        position: BlockPos,
465        reference_pos: BlockPos,
466        settings: &StructurePlaceSettings<'_>,
467        original_blocks: &[ProcessedBlockInfo],
468        mut processed_blocks: Vec<ProcessedBlockInfo>,
469        random: &mut WorldgenRandom,
470    ) -> Vec<ProcessedBlockInfo> {
471        for processor in settings.processors {
472            if let StructureProcessorKind::Capped { delegate, limit } = processor {
473                processed_blocks = Self::finalize_capped_processing(
474                    region,
475                    registry,
476                    position,
477                    reference_pos,
478                    delegate,
479                    limit,
480                    original_blocks,
481                    processed_blocks,
482                    settings,
483                    random,
484                );
485            }
486        }
487        processed_blocks
488    }
489
490    #[expect(
491        clippy::too_many_arguments,
492        reason = "matches vanilla CappedProcessor.finalizeProcessing inputs"
493    )]
494    pub(super) fn finalize_capped_processing(
495        region: &WorldGenRegion<'_>,
496        registry: &Registry,
497        position: BlockPos,
498        reference_pos: BlockPos,
499        delegate: &StructureProcessorKind,
500        limit: &IntProvider,
501        original_blocks: &[ProcessedBlockInfo],
502        mut processed_blocks: Vec<ProcessedBlockInfo>,
503        settings: &StructurePlaceSettings<'_>,
504        random: &mut WorldgenRandom,
505    ) -> Vec<ProcessedBlockInfo> {
506        if limit.max() == 0 || processed_blocks.is_empty() {
507            return processed_blocks;
508        }
509        if original_blocks.len() != processed_blocks.len() {
510            return processed_blocks;
511        }
512
513        let Ok(processed_len_i32) = i32::try_from(processed_blocks.len()) else {
514            panic!(
515                "processed structure block list length {} exceeds i32 range",
516                processed_blocks.len()
517            );
518        };
519
520        let mut cap_random = Self::capped_processor_random(region.seed(), position);
521        let max_to_replace = limit.sample(&mut cap_random).min(processed_len_i32);
522        if max_to_replace < 1 {
523            return processed_blocks;
524        }
525
526        let mut indices = (0..processed_blocks.len()).collect::<Vec<_>>();
527        Self::vanilla_shuffle(&mut indices, &mut cap_random);
528
529        let mut replaced = 0;
530        for index in indices {
531            if replaced >= max_to_replace {
532                break;
533            }
534
535            let current = processed_blocks[index].clone();
536            let Some(altered) = Self::process_block_with_processor(
537                region,
538                registry,
539                delegate,
540                &original_blocks[index],
541                current,
542                settings,
543                reference_pos,
544                random,
545            ) else {
546                continue;
547            };
548
549            if altered != processed_blocks[index] {
550                processed_blocks[index] = altered;
551                replaced += 1;
552            }
553        }
554
555        processed_blocks
556    }
557
558    pub(super) fn processor_next_f32(
559        settings: &StructurePlaceSettings<'_>,
560        pos: BlockPos,
561        random: &mut WorldgenRandom,
562    ) -> f32 {
563        match settings.processor_random {
564            StructureProcessorRandom::Placement => random.next_f32(),
565            StructureProcessorRandom::Positional => {
566                let mut random = LegacyRandom::from_seed(Self::block_pos_seed(pos) as u64);
567                random.next_f32()
568            }
569        }
570    }
571
572    pub(super) fn capped_processor_random(world_seed: i64, position: BlockPos) -> RandomSource {
573        LegacyRandom::from_seed(world_seed as u64)
574            .next_positional()
575            .at(position.x(), position.y(), position.z())
576    }
577
578    pub(super) fn vanilla_shuffle<T>(items: &mut [T], random: &mut impl Random) {
579        for i in (1..items.len()).rev() {
580            let Ok(bound) = i32::try_from(i + 1) else {
581                panic!(
582                    "structure processor shuffle length {} exceeds i32 range",
583                    items.len()
584                );
585            };
586            let j = random.next_i32_bounded(bound) as usize;
587            items.swap(i, j);
588        }
589    }
590
591    pub(super) fn replace_jigsaw_block(
592        registry: &Registry,
593        mut current: ProcessedBlockInfo,
594    ) -> Option<ProcessedBlockInfo> {
595        if Self::block_for_state(registry, current.state) != &vanilla_blocks::JIGSAW {
596            return Some(current);
597        }
598
599        let Some(nbt) = current.nbt.as_ref() else {
600            return Some(current);
601        };
602        let final_state = nbt
603            .string("final_state")
604            .map_or_else(|| "minecraft:air".into(), |value| value.to_str());
605        current.state = Self::parse_block_state_string(registry, final_state.as_ref())
606            .unwrap_or_else(|| vanilla_blocks::AIR.default_state());
607        current.nbt = None;
608
609        (Self::block_for_state(registry, current.state) != &vanilla_blocks::STRUCTURE_VOID)
610            .then_some(current)
611    }
612
613    pub(super) fn parse_block_state_string(
614        registry: &Registry,
615        value: &str,
616    ) -> Option<BlockStateId> {
617        let (name, rest) = Self::read_block_identifier_prefix(value)?;
618        let id = Identifier::from_str(name).ok()?;
619        let block = registry.blocks.by_key(&id)?;
620
621        let mut parsed_properties = Vec::new();
622        if rest.starts_with('[') {
623            let properties = Self::read_block_state_properties_prefix(rest)?;
624            if !properties.is_empty() {
625                for property in properties.split(',') {
626                    let (key, value) = property.split_once('=')?;
627                    parsed_properties.push((key, value));
628                }
629            }
630        }
631
632        registry
633            .blocks
634            .state_id_from_block_defaulted_properties(block, parsed_properties)
635    }
636
637    pub(super) fn read_block_identifier_prefix(value: &str) -> Option<(&str, &str)> {
638        let end = value
639            .char_indices()
640            .find_map(|(index, char)| {
641                (char != ':' && !Identifier::valid_char(char)).then_some(index)
642            })
643            .unwrap_or(value.len());
644        (end > 0).then_some((&value[..end], &value[end..]))
645    }
646
647    pub(super) fn read_block_state_properties_prefix(rest: &str) -> Option<&str> {
648        let rest = rest.strip_prefix('[')?;
649        let end = rest.find(']')?;
650        Some(&rest[..end])
651    }
652
653    pub(super) fn apply_terrain_matching_projection(
654        region: &WorldGenRegion<'_>,
655        original: &ProcessedBlockInfo,
656        mut current: ProcessedBlockInfo,
657    ) -> ProcessedBlockInfo {
658        let height = region.height_at(
659            HeightmapType::WorldSurfaceWg,
660            current.world_pos.x(),
661            current.world_pos.z(),
662        ) - 1;
663        current.world_pos = BlockPos::new(
664            current.world_pos.x(),
665            height + original.template_pos.y(),
666            current.world_pos.z(),
667        );
668        current
669    }
670
671    #[expect(
672        clippy::too_many_arguments,
673        reason = "processor rules receive the same state and position tuple as vanilla"
674    )]
675    pub(super) fn rule_matches(
676        registry: &Registry,
677        rule: &ProcessorRuleData,
678        input_state: BlockStateId,
679        location_state: BlockStateId,
680        template_pos: BlockPos,
681        world_pos: BlockPos,
682        reference_pos: BlockPos,
683        random: &mut LegacyRandom,
684    ) -> bool {
685        Self::rule_test_matches(registry, &rule.input_predicate, input_state, random)
686            && Self::rule_test_matches(registry, &rule.location_predicate, location_state, random)
687            && Self::pos_rule_test_matches(
688                &rule.position_predicate,
689                template_pos,
690                world_pos,
691                reference_pos,
692                random,
693            )
694    }
695
696    pub(super) fn rule_test_matches(
697        registry: &Registry,
698        test: &StructureRuleTestData,
699        state: BlockStateId,
700        random: &mut LegacyRandom,
701    ) -> bool {
702        match test {
703            StructureRuleTestData::AlwaysTrue => true,
704            StructureRuleTestData::BlockMatch { block } => registry
705                .blocks
706                .by_key(block)
707                .is_some_and(|block_ref| Self::block_for_state(registry, state) == block_ref),
708            StructureRuleTestData::RandomBlockMatch { block, probability } => {
709                registry
710                    .blocks
711                    .by_key(block)
712                    .is_some_and(|block_ref| Self::block_for_state(registry, state) == block_ref)
713                    && random.next_f32() < *probability
714            }
715            StructureRuleTestData::TagMatch { tag } => registry
716                .blocks
717                .is_in_tag(Self::block_for_state(registry, state), tag),
718            StructureRuleTestData::BlockStateMatch { block_state } => {
719                state
720                    == WorldgenStateResolver::block_state_from_data(
721                        registry,
722                        block_state,
723                        "structure processor block-state predicate",
724                    )
725            }
726        }
727    }
728
729    pub(super) fn pos_rule_test_matches(
730        test: &PosRuleTestData,
731        _template_pos: BlockPos,
732        world_pos: BlockPos,
733        reference_pos: BlockPos,
734        random: &mut LegacyRandom,
735    ) -> bool {
736        match test {
737            PosRuleTestData::AlwaysTrue => true,
738            PosRuleTestData::AxisAlignedLinearPos {
739                axis,
740                min_chance,
741                max_chance,
742                min_dist,
743                max_dist,
744            } => {
745                let dist = match axis {
746                    StructureProcessorAxis::X => (world_pos.x() - reference_pos.x()).abs(),
747                    StructureProcessorAxis::Y => (world_pos.y() - reference_pos.y()).abs(),
748                    StructureProcessorAxis::Z => (world_pos.z() - reference_pos.z()).abs(),
749                };
750                random.next_f32()
751                    <= Self::clamped_lerp_inverse(
752                        dist,
753                        *min_dist,
754                        *max_dist,
755                        *min_chance,
756                        *max_chance,
757                    )
758            }
759        }
760    }
761
762    pub(super) fn apply_rule(
763        registry: &Registry,
764        rule: &ProcessorRuleData,
765        mut current: ProcessedBlockInfo,
766        random: &mut LegacyRandom,
767    ) -> ProcessedBlockInfo {
768        current.state = WorldgenStateResolver::block_state_from_data(
769            registry,
770            &rule.output_state,
771            "structure processor output state",
772        );
773        current.nbt = match &rule.block_entity_modifier {
774            RuleBlockEntityModifierData::Passthrough => current.nbt,
775            RuleBlockEntityModifierData::AppendLoot { loot_table } => {
776                let mut nbt = current.nbt.unwrap_or_default();
777                nbt.insert("LootTable", NbtTag::String(loot_table.to_string().into()));
778                nbt.insert("LootTableSeed", NbtTag::Long(random.next_i64()));
779                Some(nbt)
780            }
781        };
782        current
783    }
784}