Skip to main content

steel_core/entity/pushing/
mod.rs

1//! Entity pushing, with a batched fast path that keeps vanilla's contact order.
2
3use std::cell::RefCell;
4
5use steel_registry::vanilla_game_rules::MAX_ENTITY_CRAMMING;
6
7use super::selector::EntitySelector;
8use super::{Entity, LivingEntity, SharedEntity};
9use crate::world::World;
10
11pub use snapshot::{DefaultEntityPush, EntityPushSnapshot};
12
13mod impulse;
14mod snapshot;
15
16thread_local! {
17    static SELECTED_PUSH_STATES: RefCell<Vec<Option<EntityPushSnapshot>>> = const { RefCell::new(Vec::new()) };
18}
19
20pub(crate) struct EntityPush;
21
22impl EntityPush {
23    pub(crate) fn push_entities<E: LivingEntity + ?Sized>(pusher: &E) {
24        let Some(world) = pusher.level() else {
25            return;
26        };
27
28        if !world.tick_runs_normally() {
29            return;
30        }
31
32        let target = pusher.as_entity_event_source();
33        let candidates = if let Some(default_push) = target.as_default_entity_push() {
34            let max_cramming = world.get_game_rule(&MAX_ENTITY_CRAMMING);
35            let target_id = target.id();
36            let borrow_limit = usize::try_from(max_cramming)
37                .ok()
38                .filter(|&limit| limit > 0);
39            let fallback = world.entity_manager().try_with_entities_in_aabb(
40                &target.bounding_box(),
41                borrow_limit,
42                |entities| {
43                    let below_cramming_limit = max_cramming <= 0
44                        || entities.len() < max_cramming as usize
45                        || (entities.len() == max_cramming as usize
46                            && entities.iter().any(|entity| entity.id() == target_id));
47
48                    below_cramming_limit
49                        && entities
50                            .iter()
51                            .all(|entity| entity.as_default_entity_push().is_some())
52                        && Self::push_selected_entities(default_push, entities)
53                },
54            );
55
56            let Some(candidates) = fallback else {
57                return;
58            };
59
60            candidates
61        } else {
62            world.get_entities_in_aabb(&target.bounding_box())
63        };
64
65        let entities = World::filter_pushable_entities(target, candidates);
66
67        if entities.is_empty() {
68            return;
69        }
70
71        pusher.apply_entity_cramming_damage(&world, &entities);
72        pusher.push_entities_in_order(&entities);
73    }
74
75    fn push_selected_entities(target: &dyn DefaultEntityPush, entities: &[&SharedEntity]) -> bool {
76        SELECTED_PUSH_STATES.with(|states| {
77            let mut states = states.borrow_mut();
78            states.clear();
79            let predicate = EntitySelector::pushable_by(target);
80
81            for entity in entities {
82                let Some(source) = entity.as_default_entity_push() else {
83                    return false;
84                };
85
86                if !predicate(entity.as_ref()) {
87                    states.push(None);
88                    continue;
89                }
90
91                let Some(state) = source.push_snapshot(true) else {
92                    return false;
93                };
94
95                states.push(Some(state));
96            }
97
98            if states.iter().all(Option::is_none) {
99                return true;
100            }
101
102            let Some(target_state) = target.push_snapshot(false) else {
103                return false;
104            };
105
106            // Select and validate every contact before applying any impulse. Default contacts
107            // run under the manager lock; custom callbacks run after it is released.
108            for (entity, source) in entities.iter().zip(states.iter()) {
109                if let Some(source) = source {
110                    Self::apply_contact(entity.as_ref(), *source, target, target_state);
111                }
112            }
113
114            true
115        })
116    }
117
118    pub(crate) fn push_entities_in_order(target: &dyn Entity, entities: &[SharedEntity]) {
119        let Some(default_target) = target.as_default_entity_push() else {
120            for entity in entities {
121                entity.push_entity(target);
122            }
123
124            return;
125        };
126
127        let mut cached = None;
128
129        for entity in entities {
130            if let Some(source) = entity.as_default_entity_push()
131                && let Some(target_state) =
132                    *cached.get_or_insert_with(|| default_target.push_snapshot(false))
133                && let Some(source_state) = source.push_snapshot(false)
134            {
135                if entity.id() != target.id() {
136                    Self::apply_contact(entity.as_ref(), source_state, target, target_state);
137                }
138
139                continue;
140            }
141
142            cached = None;
143            entity.push_entity(target);
144        }
145    }
146}
147
148#[cfg(test)]
149mod test_fixtures;
150#[cfg(test)]
151mod tests;