steel_core/entity/
inside_block_effects.rs1use std::array;
2
3use crate::entity::Entity;
4
5pub type InsideBlockEffectCallback = Box<dyn Fn(&dyn Entity) + Send + Sync + 'static>;
7
8const NO_STEP: i32 = -1;
9const EFFECT_TYPE_COUNT: usize = 5;
10const APPLY_ORDER: [InsideBlockEffectType; EFFECT_TYPE_COUNT] = [
11 InsideBlockEffectType::Freeze,
12 InsideBlockEffectType::ClearFreeze,
13 InsideBlockEffectType::FireIgnite,
14 InsideBlockEffectType::LavaIgnite,
15 InsideBlockEffectType::Extinguish,
16];
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum InsideBlockEffectType {
21 Freeze,
23 ClearFreeze,
25 FireIgnite,
27 LavaIgnite,
29 Extinguish,
31}
32
33impl InsideBlockEffectType {
34 const fn index(self) -> usize {
35 match self {
36 Self::Freeze => 0,
37 Self::ClearFreeze => 1,
38 Self::FireIgnite => 2,
39 Self::LavaIgnite => 3,
40 Self::Extinguish => 4,
41 }
42 }
43}
44
45pub struct InsideBlockEffectCollector {
51 effects_in_step: [bool; EFFECT_TYPE_COUNT],
52 before_effects_in_step: [Vec<InsideBlockEffectCallback>; EFFECT_TYPE_COUNT],
53 after_effects_in_step: [Vec<InsideBlockEffectCallback>; EFFECT_TYPE_COUNT],
54 final_effects: Vec<InsideBlockEffectCallback>,
55 last_step: i32,
56}
57
58impl InsideBlockEffectCollector {
59 #[must_use]
61 pub fn new() -> Self {
62 Self {
63 effects_in_step: [false; EFFECT_TYPE_COUNT],
64 before_effects_in_step: array::from_fn(|_| Vec::new()),
65 after_effects_in_step: array::from_fn(|_| Vec::new()),
66 final_effects: Vec::new(),
67 last_step: NO_STEP,
68 }
69 }
70
71 pub fn advance_step(&mut self, step: i32) {
73 if self.last_step == step {
74 return;
75 }
76
77 self.last_step = step;
78 self.flush_step();
79 }
80
81 pub const fn apply(&mut self, effect_type: InsideBlockEffectType) {
83 self.effects_in_step[effect_type.index()] = true;
84 }
85
86 pub fn run_before(
88 &mut self,
89 effect_type: InsideBlockEffectType,
90 effect: InsideBlockEffectCallback,
91 ) {
92 self.before_effects_in_step[effect_type.index()].push(effect);
93 }
94
95 pub fn run_after(
97 &mut self,
98 effect_type: InsideBlockEffectType,
99 effect: InsideBlockEffectCallback,
100 ) {
101 self.after_effects_in_step[effect_type.index()].push(effect);
102 }
103
104 pub fn apply_and_clear(&mut self, entity: &dyn Entity) {
106 self.flush_step();
107
108 for effect in self.final_effects.drain(..) {
109 if !entity.is_alive() {
110 break;
111 }
112 effect(entity);
113 }
114
115 self.last_step = NO_STEP;
116 }
117
118 fn flush_step(&mut self) {
119 for effect_type in APPLY_ORDER {
120 let index = effect_type.index();
121 self.final_effects
122 .append(&mut self.before_effects_in_step[index]);
123 if self.effects_in_step[index] {
124 self.effects_in_step[index] = false;
125 self.final_effects.push(Box::new(move |entity| {
126 entity.apply_inside_block_effect(effect_type);
127 }));
128 }
129 self.final_effects
130 .append(&mut self.after_effects_in_step[index]);
131 }
132 }
133}
134
135impl Default for InsideBlockEffectCollector {
136 fn default() -> Self {
137 Self::new()
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use std::sync::{Arc, Weak};
144
145 use glam::DVec3;
146 use steel_registry::entity_type::EntityTypeRef;
147 use steel_registry::vanilla_entities;
148 use steel_utils::locks::SyncMutex;
149
150 use crate::entity::{Entity, EntityBase};
151
152 use super::{InsideBlockEffectCollector, InsideBlockEffectType};
153
154 struct EffectTestEntity {
155 base: EntityBase,
156 calls: Arc<SyncMutex<Vec<&'static str>>>,
157 alive: Arc<SyncMutex<bool>>,
158 }
159
160 impl EffectTestEntity {
161 fn new() -> Arc<Self> {
162 let calls = Arc::new(SyncMutex::new(Vec::new()));
163 let alive = Arc::new(SyncMutex::new(true));
164
165 Arc::new(Self {
166 base: EntityBase::new(
167 1,
168 DVec3::ZERO,
169 vanilla_entities::ITEM.dimensions,
170 Weak::new(),
171 ),
172 calls,
173 alive,
174 })
175 }
176 }
177
178 crate::entity::impl_test_downcast_type!(EffectTestEntity);
179
180 impl Entity for EffectTestEntity {
181 fn base(&self) -> &EntityBase {
182 &self.base
183 }
184
185 fn entity_type(&self) -> EntityTypeRef {
186 &vanilla_entities::ITEM
187 }
188
189 fn is_alive(&self) -> bool {
190 !self.is_removed() && *self.alive.lock()
191 }
192
193 fn apply_inside_block_effect(&self, effect_type: InsideBlockEffectType) {
194 self.calls.lock().push(match effect_type {
195 InsideBlockEffectType::Freeze => "freeze",
196 InsideBlockEffectType::ClearFreeze => "clear_freeze",
197 InsideBlockEffectType::FireIgnite => "fire_ignite",
198 InsideBlockEffectType::LavaIgnite => "lava_ignite",
199 InsideBlockEffectType::Extinguish => "extinguish",
200 });
201 }
202 }
203
204 #[test]
205 fn collector_flushes_effects_in_vanilla_type_order_per_step() {
206 let entity = EffectTestEntity::new();
207 let mut collector = InsideBlockEffectCollector::new();
208
209 collector.advance_step(0);
210 collector.apply(InsideBlockEffectType::Extinguish);
211 collector.apply(InsideBlockEffectType::Freeze);
212 collector.advance_step(1);
213 collector.apply(InsideBlockEffectType::LavaIgnite);
214 collector.apply_and_clear(entity.as_ref());
215
216 assert_eq!(
217 *entity.calls.lock(),
218 vec!["freeze", "extinguish", "lava_ignite"]
219 );
220 }
221
222 #[test]
223 fn collector_runs_before_and_after_callbacks_around_default_effect() {
224 let entity = EffectTestEntity::new();
225 let mut collector = InsideBlockEffectCollector::new();
226
227 collector.advance_step(0);
228 {
229 let calls = entity.calls.clone();
230 collector.run_before(
231 InsideBlockEffectType::FireIgnite,
232 Box::new(move |_| calls.lock().push("before")),
233 );
234 }
235 collector.apply(InsideBlockEffectType::FireIgnite);
236 {
237 let calls = entity.calls.clone();
238 collector.run_after(
239 InsideBlockEffectType::FireIgnite,
240 Box::new(move |_| calls.lock().push("after")),
241 );
242 }
243 collector.apply_and_clear(entity.as_ref());
244
245 assert_eq!(*entity.calls.lock(), vec!["before", "fire_ignite", "after"]);
246 }
247
248 #[test]
249 fn collector_stops_after_effect_makes_entity_not_alive() {
250 let entity = EffectTestEntity::new();
251 let mut collector = InsideBlockEffectCollector::new();
252
253 collector.advance_step(0);
254 collector.apply(InsideBlockEffectType::FireIgnite);
255 {
256 let calls = Arc::clone(&entity.calls);
257 let alive = Arc::clone(&entity.alive);
258 collector.run_after(
259 InsideBlockEffectType::FireIgnite,
260 Box::new(move |_| {
261 calls.lock().push("kill");
262 *alive.lock() = false;
263 }),
264 );
265 }
266 collector.apply(InsideBlockEffectType::LavaIgnite);
267 collector.apply_and_clear(entity.as_ref());
268
269 assert_eq!(*entity.calls.lock(), vec!["fire_ignite", "kill"]);
270 }
271}