Skip to main content

steel_core/world/explosion/server/
block_effects.rs

1//! Block callbacks, drops, and fire creation for server explosions.
2
3use steel_registry::blocks::block_state_ext::BlockStateExt as _;
4use steel_registry::item_stack::ItemStack;
5use steel_utils::BlockPos;
6use steel_utils::random::Random;
7use steel_utils::types::UpdateFlags;
8
9use crate::behavior::BLOCK_BEHAVIORS;
10use crate::behavior::blocks::FireBlock;
11use crate::chunk::gameplay_chunk_lookup_cache::LocalFullChunkHolderCache;
12use crate::entity::entities::ItemEntity;
13
14use super::ServerExplosion;
15
16const FIRE_CHANCE_DENOMINATOR: i32 = 3;
17const MAX_DROPS_PER_COMBINED_STACK: i32 = 16;
18
19impl ServerExplosion<'_> {
20    pub(super) fn interact_with_blocks(&self, affected: &mut [BlockPos], random: &mut impl Random) {
21        random.shuffle(affected);
22        let mut stacks = Vec::new();
23        let mut full_chunks = LocalFullChunkHolderCache::new();
24
25        for &pos in affected.iter() {
26            let state = self
27                .world
28                .get_block_state_with_local_holder_cache(pos, &mut full_chunks);
29            BLOCK_BEHAVIORS
30                .get_behavior(state.get_block())
31                .on_explosion_hit(state, self.world, pos, self, &mut |stack, stack_pos| {
32                    add_or_append_stack(&mut stacks, stack, stack_pos);
33                });
34        }
35
36        for stack in stacks {
37            self.world.pop_resource(stack.pos, stack.stack);
38        }
39    }
40
41    pub(super) fn create_fire(&self, affected: &[BlockPos], random: &mut impl Random) {
42        self.create_fire_with(affected, || {
43            random.next_i32_bounded(FIRE_CHANCE_DENOMINATOR)
44        });
45    }
46
47    fn create_fire_with(&self, affected: &[BlockPos], mut next_int: impl FnMut() -> i32) {
48        for &pos in affected {
49            if next_int() == 0
50                && self.world.get_block_state(pos).is_air()
51                && self.world.get_block_state(pos.below()).is_solid_render()
52            {
53                self.world.set_block(
54                    pos,
55                    FireBlock::get_state(self.world.as_ref(), pos),
56                    UpdateFlags::UPDATE_ALL,
57                );
58            }
59        }
60    }
61}
62
63struct StackCollector {
64    pos: BlockPos,
65    stack: ItemStack,
66}
67
68fn add_or_append_stack(stacks: &mut Vec<StackCollector>, mut stack: ItemStack, pos: BlockPos) {
69    for collector in stacks.iter_mut() {
70        if ItemEntity::are_mergeable(&collector.stack, &stack) {
71            let available = collector
72                .stack
73                .max_stack_size()
74                .min(MAX_DROPS_PER_COMBINED_STACK)
75                - collector.stack.count();
76            let transferred = available.min(stack.count());
77            collector.stack = collector
78                .stack
79                .copy_with_count(collector.stack.count() + transferred);
80            stack.shrink(transferred);
81            if stack.is_empty() {
82                return;
83            }
84        }
85    }
86    stacks.push(StackCollector { pos, stack });
87}
88
89#[cfg(test)]
90mod tests {
91    use glam::DVec3;
92    use steel_registry::item_stack::ItemStack;
93    use steel_registry::{init_vanilla_registry, vanilla_blocks, vanilla_items};
94    use steel_utils::types::UpdateFlags;
95    use steel_utils::{BlockPos, ChunkPos};
96
97    use super::*;
98    use crate::behavior::init_behaviors;
99    use crate::test_support::{fresh_test_world, insert_ready_full_chunk};
100    use crate::world::explosion::BlockInteraction;
101
102    #[test]
103    fn fire_creation_draws_before_testing_air_and_support() {
104        init_vanilla_registry();
105        init_behaviors();
106        let fixture = fresh_test_world("explosion_fire_order");
107        let world = &fixture.world;
108        insert_ready_full_chunk(world, ChunkPos::new(0, 0));
109        let supported = BlockPos::new(1, 64, 1);
110        let unsupported = supported.east();
111        let occupied = unsupported.east();
112        assert!(world.set_block(
113            supported.below(),
114            vanilla_blocks::STONE.default_state(),
115            UpdateFlags::UPDATE_NONE,
116        ));
117        assert!(world.set_block(
118            occupied,
119            vanilla_blocks::STONE.default_state(),
120            UpdateFlags::UPDATE_NONE,
121        ));
122        let explosion = ServerExplosion::new(
123            world,
124            None,
125            None,
126            None,
127            None,
128            DVec3::ZERO,
129            1.0,
130            true,
131            BlockInteraction::Destroy,
132        );
133        let mut draws = 0;
134
135        explosion.create_fire_with(&[unsupported, occupied, supported], || {
136            draws += 1;
137            0
138        });
139
140        assert_eq!(draws, 3);
141        assert!(world.get_block_state(unsupported).is_air());
142        assert_eq!(
143            world.get_block_state(occupied).get_block(),
144            &vanilla_blocks::STONE
145        );
146        assert!(!world.get_block_state(supported).is_air());
147    }
148
149    #[test]
150    fn combined_explosion_drops_never_exceed_vanilla_stack_limit() {
151        const INPUT_STACK_SIZE: i32 = 10;
152        const EXPECTED_STACK_COUNT: usize = 2;
153
154        init_vanilla_registry();
155        let stack = ItemStack::with_count(&vanilla_items::STONE, INPUT_STACK_SIZE);
156        let mut stacks = Vec::new();
157
158        add_or_append_stack(&mut stacks, stack.clone(), BlockPos::ZERO);
159        add_or_append_stack(&mut stacks, stack, BlockPos::ZERO);
160
161        assert_eq!(stacks.len(), EXPECTED_STACK_COUNT);
162        assert_eq!(stacks[0].stack.count(), MAX_DROPS_PER_COMBINED_STACK);
163        assert_eq!(
164            stacks[1].stack.count(),
165            INPUT_STACK_SIZE * 2 - MAX_DROPS_PER_COMBINED_STACK
166        );
167    }
168}