Skip to main content

steel_core/entity/
inside_block_effects.rs

1use std::array;
2
3use crate::entity::Entity;
4
5/// Callback queued around an inside-block effect.
6pub 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/// Vanilla inside-block effect kinds applied after entity/block intersections.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum InsideBlockEffectType {
21    /// Powder-snow freezing.
22    Freeze,
23    /// Clears accumulated freezing.
24    ClearFreeze,
25    /// Fire-block ignition.
26    FireIgnite,
27    /// Lava ignition.
28    LavaIgnite,
29    /// Clears fire.
30    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
45/// Step-scoped collector for vanilla inside-block side effects.
46///
47/// Vanilla lets blocks enqueue effects while the movement trace is still
48/// scanning intersections, then flushes those effects in a stable per-step
49/// order after the scan.
50pub 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    /// Creates an empty collector.
60    #[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    /// Advances to a new block-trace step, flushing the previous step.
72    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    /// Queues a vanilla inside-block effect kind for the current step.
82    pub const fn apply(&mut self, effect_type: InsideBlockEffectType) {
83        self.effects_in_step[effect_type.index()] = true;
84    }
85
86    /// Queues a callback to run before this effect kind in the current step.
87    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    /// Queues a callback to run after this effect kind in the current step.
96    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    /// Applies queued effects and resets the collector for the next scan.
105    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}