steel_core/world/explosion/server/
block_effects.rs1use 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}