Skip to main content

steel_core/world/
entity_management.rs

1use super::{
2    AddEntityError, Arc, BLOCK_DROPS, BlockPos, ChunkPos, ChunkStatus, DVec3, Direction, Entity,
3    EntityChunkCallback, EntityLifecycleChanges, EntityOwnership, EntityTracker, EntityVisibility,
4    ExperienceOrbEntity, FxHashSet, GameEventContext, GameEventDispatcher, GameEventListenerCount,
5    GameEventListenerStorage, GameEventRef, InactiveEntityCallback, ItemEntity, ItemStack, Player,
6    RemovalReason, SectionPos, SharedEntity, SharedGameEventListener, SyncMutex, World, WorldAabb,
7    WorldChangeRequest, block_entity_ticker, mem, vanilla_entities,
8};
9
10use crate::entity::{EntityCollisionCandidates, selector::EntitySelector};
11
12pub(super) struct NavigatingMobTracker {
13    ids: SyncMutex<FxHashSet<i32>>,
14}
15
16impl NavigatingMobTracker {
17    pub(super) fn new() -> Self {
18        Self {
19            ids: SyncMutex::new(FxHashSet::default()),
20        }
21    }
22
23    pub(super) fn track(&self, entity: &SharedEntity) {
24        if entity.as_pathfinder_mob().is_some() {
25            self.ids.lock().insert(entity.id());
26        }
27    }
28
29    pub(super) fn untrack(&self, entity_id: i32) {
30        self.ids.lock().remove(&entity_id);
31    }
32
33    pub(super) fn ids(&self) -> Vec<i32> {
34        self.ids.lock().iter().copied().collect()
35    }
36}
37
38pub(super) fn nearest_player_distance_in_range(
39    distance_sqr: f64,
40    max_distance: f64,
41    max_distance_sqr: f64,
42) -> bool {
43    max_distance < 0.0 || distance_sqr < max_distance_sqr
44}
45
46impl World {
47    /// Returns the world-global block-entity ticker owner.
48    #[must_use]
49    pub(crate) const fn block_entity_tickers(
50        &self,
51    ) -> &block_entity_ticker::WorldBlockEntityTickers {
52        &self.block_entity_tickers
53    }
54
55    /// Shares the counter used to skip game-event dispatch when no chunk has listeners.
56    #[must_use]
57    pub(crate) fn game_event_listener_count(&self) -> Arc<GameEventListenerCount> {
58        Arc::clone(&self.game_event_listener_count)
59    }
60
61    /// Returns the entity tracker for managing player-entity visibility.
62    #[must_use]
63    pub const fn entity_tracker(&self) -> &EntityTracker {
64        &self.entity_tracker
65    }
66
67    pub(super) fn attach_managed_entity_callback(self: &Arc<Self>, entity: &SharedEntity) {
68        let callback = Arc::new(EntityChunkCallback::new(entity.id(), Arc::downgrade(self)));
69        entity.set_level_callback(callback);
70        self.entity_manager.commit_bounding_box_change(entity.id());
71    }
72
73    pub(crate) fn add_entity_to_tracker(self: &Arc<Self>, entity: &SharedEntity) {
74        self.entity_tracker.add(
75            entity,
76            |chunk| self.get_packet_tracking_players(chunk),
77            |id| self.players.get_by_entity_id(id),
78        );
79        self.track_navigating_mob(entity);
80    }
81
82    pub(crate) fn remove_entity_from_tracker(&self, entity_id: i32) {
83        self.entity_tracker.remove(entity_id, |player_id| {
84            self.players.get_by_entity_id(player_id)
85        });
86        self.untrack_navigating_mob(entity_id);
87    }
88
89    pub(crate) fn apply_entity_lifecycle_changes(
90        self: &Arc<Self>,
91        changes: EntityLifecycleChanges,
92    ) {
93        for entity in changes.tracking_stopped {
94            self.remove_entity_from_tracker(entity.id());
95        }
96        for entity in changes.tracking_started {
97            self.add_entity_to_tracker(&entity);
98        }
99    }
100
101    pub(super) fn track_navigating_mob(&self, entity: &SharedEntity) {
102        self.navigating_mobs.track(entity);
103    }
104
105    pub(super) fn untrack_navigating_mob(&self, entity_id: i32) {
106        self.navigating_mobs.untrack(entity_id);
107    }
108
109    pub(crate) fn register_loaded_entity(
110        self: &Arc<Self>,
111        entity: SharedEntity,
112    ) -> Result<(), AddEntityError> {
113        let lifecycle = self
114            .entity_manager
115            .add_live_entity(Arc::clone(&entity), EntityOwnership::ManagerOwned)?;
116        self.attach_managed_entity_callback(&entity);
117        self.apply_entity_lifecycle_changes(lifecycle);
118        Ok(())
119    }
120
121    pub(crate) fn register_loaded_entity_tree(
122        self: &Arc<Self>,
123        entities: &[SharedEntity],
124    ) -> Result<(), AddEntityError> {
125        let lifecycle = self
126            .entity_manager
127            .add_live_entity_tree(entities, EntityOwnership::ManagerOwned)?;
128        for entity in entities {
129            self.attach_managed_entity_callback(entity);
130        }
131        self.apply_entity_lifecycle_changes(lifecycle);
132        Ok(())
133    }
134
135    pub(crate) fn register_loaded_chunk_entities(
136        self: &Arc<Self>,
137        source_chunk: ChunkPos,
138        persisted_status: ChunkStatus,
139        entities: Vec<SharedEntity>,
140    ) {
141        for tree in Self::loaded_entity_trees(entities) {
142            let Some(root) = tree.first() else {
143                continue;
144            };
145            let root_id = root.id();
146            let root_uuid = root.uuid();
147            let root_type = root.entity_type();
148            let root_pos = root.position();
149            let root_chunk = ChunkPos::from_entity_pos(root_pos);
150            let mut dirty_chunks = FxHashSet::default();
151            for entity in &tree {
152                let entity_chunk = ChunkPos::from_entity_pos(entity.position());
153                if entity_chunk != source_chunk {
154                    dirty_chunks.insert(source_chunk);
155                    dirty_chunks.insert(entity_chunk);
156                }
157            }
158
159            if let Err(error) = self.register_loaded_entity_tree(&tree) {
160                tracing::warn!(
161                    source_chunk = ?source_chunk,
162                    ?persisted_status,
163                    root_id,
164                    uuid = ?root_uuid,
165                    entity_type = ?root_type.key,
166                    position = ?root_pos,
167                    entity_chunk = ?root_chunk,
168                    entity_count = tree.len(),
169                    "Discarding loaded chunk entity tree that could not be registered: {error}; source_chunk={source_chunk:?}, persisted_status={persisted_status:?}, root_id={root_id}, uuid={root_uuid}, entity_type={:?}, position={root_pos:?}, entity_chunk={root_chunk:?}, entity_count={}",
170                    root_type.key,
171                    tree.len(),
172                );
173                Self::discard_loaded_entity_tree(&tree);
174                self.mark_chunk_dirty(source_chunk);
175                continue;
176            }
177
178            for chunk in dirty_chunks {
179                self.mark_chunk_dirty(chunk);
180            }
181        }
182    }
183
184    pub(super) fn loaded_entity_trees(entities: Vec<SharedEntity>) -> Vec<Vec<SharedEntity>> {
185        let mut seen = FxHashSet::default();
186        let mut trees = Vec::new();
187
188        for entity in &entities {
189            if entity.is_passenger() {
190                continue;
191            }
192            let mut tree = Vec::new();
193            Self::collect_loaded_entity_tree(entity, &mut seen, &mut tree);
194            if !tree.is_empty() {
195                trees.push(tree);
196            }
197        }
198
199        for entity in &entities {
200            if seen.contains(&entity.id()) {
201                continue;
202            }
203            let mut tree = Vec::new();
204            Self::collect_loaded_entity_tree(entity, &mut seen, &mut tree);
205            if !tree.is_empty() {
206                trees.push(tree);
207            }
208        }
209
210        trees
211    }
212
213    pub(super) fn collect_loaded_entity_tree(
214        entity: &SharedEntity,
215        seen: &mut FxHashSet<i32>,
216        tree: &mut Vec<SharedEntity>,
217    ) {
218        if !seen.insert(entity.id()) {
219            return;
220        }
221        tree.push(Arc::clone(entity));
222        for passenger in entity.passengers() {
223            Self::collect_loaded_entity_tree(&passenger, seen, tree);
224        }
225    }
226
227    pub(super) fn discard_loaded_entity_tree(entities: &[SharedEntity]) {
228        for entity in entities {
229            entity.set_removed(RemovalReason::Discarded);
230        }
231    }
232
233    pub(crate) fn has_full_chunk(&self, chunk_pos: ChunkPos) -> bool {
234        self.chunk_map
235            .with_full_chunk(chunk_pos, |_| true)
236            .unwrap_or(false)
237    }
238
239    /// Adds a runtime entity to the world.
240    pub fn try_add_entity(self: &Arc<Self>, entity: SharedEntity) -> Result<(), AddEntityError> {
241        let chunk_pos = ChunkPos::from_entity_pos(entity.position());
242        if !self.has_full_chunk(chunk_pos) {
243            return Err(AddEntityError::ChunkNotLoaded {
244                entity_id: entity.id(),
245                chunk: chunk_pos,
246            });
247        }
248        self.register_loaded_entity(entity)?;
249        self.mark_chunk_dirty(chunk_pos);
250        Ok(())
251    }
252
253    /// Adds a fresh vehicle/passenger tree only after every member has passed
254    /// chunk and identity validation.
255    pub(crate) fn try_add_fresh_entity_with_passengers(
256        self: &Arc<Self>,
257        root: SharedEntity,
258    ) -> Result<(), AddEntityError> {
259        let mut entities = Vec::new();
260        let mut seen_ids = FxHashSet::default();
261        Self::collect_fresh_entity_tree(&root, &mut seen_ids, &mut entities)?;
262
263        let mut dirty_chunks = FxHashSet::default();
264        for entity in &entities {
265            let chunk = ChunkPos::from_entity_pos(entity.position());
266            if !self.entity_manager.is_chunk_loaded(chunk) {
267                return Err(AddEntityError::ChunkNotLoaded {
268                    entity_id: entity.id(),
269                    chunk,
270                });
271            }
272            dirty_chunks.insert(chunk);
273        }
274
275        self.register_loaded_entity_tree(&entities)?;
276        for chunk in dirty_chunks {
277            self.mark_chunk_dirty(chunk);
278        }
279        Ok(())
280    }
281
282    fn collect_fresh_entity_tree(
283        entity: &SharedEntity,
284        seen_ids: &mut FxHashSet<i32>,
285        entities: &mut Vec<SharedEntity>,
286    ) -> Result<(), AddEntityError> {
287        if !seen_ids.insert(entity.id()) {
288            return Err(AddEntityError::DuplicateId {
289                entity_id: entity.id(),
290            });
291        }
292        entities.push(Arc::clone(entity));
293        for passenger in entity.passengers() {
294            Self::collect_fresh_entity_tree(&passenger, seen_ids, entities)?;
295        }
296        Ok(())
297    }
298
299    pub(crate) fn on_entity_chunk_loaded(self: &Arc<Self>, pos: ChunkPos) {
300        // Runtime entity membership follows retained chunk holders, so it
301        // starts at Empty rather than waiting for full LevelChunk readiness.
302        // Durable entity persistence still starts at StructureStarts; entities
303        // in lower-status chunks can be lost if those chunks unload first.
304        let result = self.entity_manager.on_chunk_loaded(pos);
305        if result.needs_save {
306            self.mark_chunk_dirty(pos);
307        }
308        for entity in result.restored {
309            self.attach_managed_entity_callback(&entity);
310        }
311        self.apply_entity_lifecycle_changes(EntityLifecycleChanges {
312            tracking_started: result.tracking_started,
313            tracking_stopped: Vec::new(),
314            ticking_started: result.ticking_started,
315            ticking_stopped: Vec::new(),
316        });
317    }
318
319    pub(crate) fn update_entity_chunk_visibility(
320        self: &Arc<Self>,
321        pos: ChunkPos,
322        visibility: EntityVisibility,
323    ) {
324        let changes = self.entity_manager.update_chunk_visibility(pos, visibility);
325        self.apply_entity_lifecycle_changes(changes);
326    }
327
328    pub(crate) fn on_entity_chunk_unload_start(self: &Arc<Self>, pos: ChunkPos) {
329        let result = self.entity_manager.begin_chunk_unload(pos);
330        self.apply_entity_lifecycle_changes(EntityLifecycleChanges {
331            tracking_started: Vec::new(),
332            tracking_stopped: result.tracking_stopped,
333            ticking_started: Vec::new(),
334            ticking_stopped: result.ticking_stopped,
335        });
336        for entity in result.retained {
337            let entity_id = entity.id();
338            entity.set_level_callback(Arc::new(InactiveEntityCallback::new(entity_id)));
339        }
340    }
341
342    pub(crate) fn on_entity_chunk_unload_finalized(&self, pos: ChunkPos) {
343        self.entity_manager.finalize_chunk_unload(pos);
344    }
345
346    /// Spawns an item entity at the given position.
347    ///
348    /// This is a convenience method for dropping items in the world.
349    ///
350    /// Returns `None` if the item stack is empty.
351    pub fn spawn_item(self: &Arc<Self>, pos: DVec3, item: ItemStack) -> Option<Arc<ItemEntity>> {
352        self.spawn_item_with_velocity(pos, item, ItemEntity::default_spawn_velocity())
353    }
354
355    /// Spawns an item entity at the given position with initial velocity.
356    ///
357    /// Returns `None` if the item stack is empty.
358    pub fn spawn_item_with_velocity(
359        self: &Arc<Self>,
360        pos: DVec3,
361        item: ItemStack,
362        velocity: DVec3,
363    ) -> Option<Arc<ItemEntity>> {
364        use crate::entity::next_entity_id;
365
366        if item.is_empty() {
367            return None;
368        }
369
370        let entity_id = next_entity_id();
371        let entity = Arc::new(ItemEntity::with_item_and_velocity(
372            &vanilla_entities::ITEM,
373            entity_id,
374            pos,
375            item,
376            velocity,
377            Arc::downgrade(self),
378        ));
379        if let Err(error) = self.try_add_entity(Arc::<ItemEntity>::clone(&entity)) {
380            log::warn!("Failed to spawn item entity: {error}");
381            return None;
382        }
383        Some(entity)
384    }
385
386    /// Drops an item at a block position with random offset and velocity.
387    ///
388    /// Mirrors vanilla's `Block.popResource()`. Used for block drops.
389    /// The item spawns near the center of the block with slight random offset
390    /// and small random velocity.
391    pub fn pop_resource(
392        self: &Arc<Self>,
393        pos: BlockPos,
394        item: ItemStack,
395    ) -> Option<Arc<ItemEntity>> {
396        use steel_registry::vanilla_entities;
397
398        if item.is_empty() {
399            return None;
400        }
401
402        // Respect doTileDrops gamerule
403        if !self.get_game_rule(&BLOCK_DROPS) {
404            return None;
405        }
406
407        // Vanilla uses EntityType.ITEM dimensions for offset calculation
408        let half_height = f64::from(vanilla_entities::ITEM.dimensions.height) / 2.0;
409
410        // Random offset within block (vanilla: nextDouble(-0.25, 0.25))
411        let x = f64::from(pos.x()) + 0.5 + rand::random_range(-0.25..0.25);
412        let y = f64::from(pos.y()) + 0.5 + rand::random_range(-0.25..0.25) - half_height;
413        let z = f64::from(pos.z()) + 0.5 + rand::random_range(-0.25..0.25);
414
415        let entity = self.spawn_item(DVec3::new(x, y, z), item)?;
416        entity.set_default_pickup_delay();
417        Some(entity)
418    }
419
420    /// Spawns experience at a block position when block drops are enabled.
421    ///
422    /// Mirrors Vanilla's `Block.popExperience`.
423    pub fn pop_experience(self: &Arc<Self>, pos: BlockPos, amount: i32) {
424        if amount <= 0 || !self.get_game_rule(&BLOCK_DROPS) {
425            return;
426        }
427
428        ExperienceOrbEntity::award(
429            self,
430            DVec3::new(
431                f64::from(pos.x()) + 0.5,
432                f64::from(pos.y()) + 0.5,
433                f64::from(pos.z()) + 0.5,
434            ),
435            amount,
436        );
437    }
438
439    /// Drops an item from a block face with directional velocity.
440    ///
441    /// Mirrors vanilla's `Block.popResourceFromFace()`. Used for items ejected
442    /// from a specific side of a block.
443    pub fn pop_resource_from_face(
444        self: &Arc<Self>,
445        pos: BlockPos,
446        face: Direction,
447        item: ItemStack,
448    ) -> Option<Arc<ItemEntity>> {
449        use steel_registry::vanilla_entities;
450
451        if item.is_empty() {
452            return None;
453        }
454
455        if !self.get_game_rule(&BLOCK_DROPS) {
456            return None;
457        }
458
459        let half_width = f64::from(vanilla_entities::ITEM.dimensions.width) / 2.0;
460        let half_height = f64::from(vanilla_entities::ITEM.dimensions.height) / 2.0;
461
462        let (step_x, step_y, step_z) = face.offset();
463
464        // Position calculation (vanilla logic)
465        let x = f64::from(pos.x())
466            + 0.5
467            + if step_x == 0 {
468                rand::random_range(-0.25..0.25)
469            } else {
470                f64::from(step_x) * (0.5 + half_width)
471            };
472        let y = f64::from(pos.y())
473            + 0.5
474            + if step_y == 0 {
475                rand::random_range(-0.25..0.25)
476            } else {
477                f64::from(step_y) * (0.5 + half_height)
478            }
479            - half_height;
480        let z = f64::from(pos.z())
481            + 0.5
482            + if step_z == 0 {
483                rand::random_range(-0.25..0.25)
484            } else {
485                f64::from(step_z) * (0.5 + half_width)
486            };
487
488        // Velocity in direction of face
489        let delta_x = if step_x == 0 {
490            rand::random_range(-0.1..0.1)
491        } else {
492            f64::from(step_x) * 0.1
493        };
494        let delta_y = if step_y == 0 {
495            rand::random_range(0.0..0.1)
496        } else {
497            f64::from(step_y) * 0.1 + 0.1
498        };
499        let delta_z = if step_z == 0 {
500            rand::random_range(-0.1..0.1)
501        } else {
502            f64::from(step_z) * 0.1
503        };
504
505        let entity = self.spawn_item_with_velocity(
506            DVec3::new(x, y, z),
507            item,
508            DVec3::new(delta_x, delta_y, delta_z),
509        )?;
510        entity.set_default_pickup_delay();
511        Some(entity)
512    }
513
514    /// Gets an entity by its network ID.
515    ///
516    /// Returns `None` if the entity is not live in the world.
517    #[must_use]
518    pub fn get_entity_by_id(&self, id: i32) -> Option<SharedEntity> {
519        self.entity_manager.get_by_id(id)
520    }
521
522    /// Returns true if this exact entity is live or retained for chunk-unload recovery.
523    pub(crate) fn contains_live_or_unloading_entity(&self, entity: &SharedEntity) -> bool {
524        self.entity_manager
525            .contains_live_or_unloading_entity(entity)
526    }
527
528    /// Queues a world change from world-local code for server safe-point processing.
529    pub fn queue_world_change(&self, entity: SharedEntity, request: WorldChangeRequest) {
530        self.pending_world_changes.lock().push((entity, request));
531    }
532
533    pub(crate) fn drain_world_changes(&self) -> Vec<(SharedEntity, WorldChangeRequest)> {
534        mem::take(&mut *self.pending_world_changes.lock())
535    }
536
537    /// Gets an entity by its network ID if it is visible to vanilla gameplay lookups.
538    ///
539    /// Returns `None` if the entity is not live or is hidden in an inaccessible chunk.
540    #[must_use]
541    pub fn get_accessible_entity_by_id(&self, id: i32) -> Option<SharedEntity> {
542        self.entity_manager.get_accessible_by_id(id)
543    }
544
545    /// Gets an entity by its UUID.
546    ///
547    /// Returns `None` if the entity is not live in the world.
548    #[must_use]
549    pub fn get_entity_by_uuid(&self, uuid: &uuid::Uuid) -> Option<SharedEntity> {
550        self.entity_manager.get_by_uuid(uuid)
551    }
552
553    /// Gets all entities intersecting the given bounding box.
554    ///
555    /// Only returns entities in loaded chunks.
556    #[must_use]
557    pub fn get_entities_in_aabb(&self, aabb: &WorldAabb) -> Vec<SharedEntity> {
558        self.entity_manager.get_entities_in_aabb(aabb)
559    }
560
561    /// Gets entities intersecting the given bounding box and matching `predicate`.
562    ///
563    /// Only returns entities in loaded chunks.
564    #[must_use]
565    pub fn get_entities_in_aabb_matching(
566        &self,
567        aabb: &WorldAabb,
568        predicate: impl FnMut(&dyn Entity) -> bool,
569    ) -> Vec<SharedEntity> {
570        self.entity_manager
571            .get_entities_in_aabb_matching(aabb, predicate)
572    }
573
574    /// Returns whether any entity intersects the given bounding box and matches `predicate`.
575    ///
576    /// Only checks entities in loaded chunks.
577    #[must_use]
578    pub fn has_entity_in_aabb_matching(
579        &self,
580        aabb: &WorldAabb,
581        predicate: impl FnMut(&dyn Entity) -> bool,
582    ) -> bool {
583        self.entity_manager
584            .has_entity_in_aabb_matching(aabb, predicate)
585    }
586
587    /// Gets matching entity bounding boxes intersecting the given bounding box.
588    ///
589    /// Only checks entities in loaded chunks.
590    #[must_use]
591    pub fn get_entity_bounding_boxes_in_aabb_matching(
592        &self,
593        aabb: &WorldAabb,
594        predicate: impl FnMut(&dyn Entity) -> bool,
595    ) -> Vec<WorldAabb> {
596        self.entity_manager
597            .get_entity_bounding_boxes_in_aabb_matching(aabb, predicate)
598    }
599
600    /// Gets entity shapes for movement collision through the selected conservative broad phase.
601    #[must_use]
602    pub(crate) fn get_movement_collision_boxes_in_aabb_matching(
603        &self,
604        aabb: &WorldAabb,
605        candidates: EntityCollisionCandidates,
606        predicate: impl FnMut(&dyn Entity) -> bool,
607    ) -> Vec<WorldAabb> {
608        self.entity_manager
609            .get_movement_collision_boxes_in_aabb_matching(aabb, candidates, predicate)
610    }
611
612    /// Returns whether the selected movement-collision broad phase has a matching entity.
613    #[must_use]
614    pub(crate) fn has_movement_collision_in_aabb_matching(
615        &self,
616        aabb: &WorldAabb,
617        candidates: EntityCollisionCandidates,
618        predicate: impl FnMut(&dyn Entity) -> bool,
619    ) -> bool {
620        self.entity_manager
621            .has_movement_collision_in_aabb_matching(aabb, candidates, predicate)
622    }
623
624    /// Gets the nearest entity intersecting the given bounding box and matching `predicate`.
625    ///
626    /// Only returns entities in loaded chunks.
627    #[must_use]
628    pub fn nearest_entity_in_aabb_matching(
629        &self,
630        aabb: &WorldAabb,
631        origin: DVec3,
632        predicate: impl FnMut(&dyn Entity) -> bool,
633    ) -> Option<SharedEntity> {
634        self.entity_manager
635            .nearest_entity_in_aabb_matching(aabb, origin, predicate)
636    }
637
638    /// Gets the nearest player to `position` within `max_distance`.
639    #[must_use]
640    pub fn nearest_player(
641        &self,
642        position: DVec3,
643        max_distance: f64,
644        mut predicate: impl FnMut(&Player) -> bool,
645    ) -> Option<Arc<Player>> {
646        let max_distance_sqr = max_distance * max_distance;
647        let mut nearest: Option<(Arc<Player>, f64)> = None;
648        self.players.iter_players(|_, player| {
649            if predicate(player) {
650                let distance_sqr = player.position().distance_squared(position);
651                if nearest_player_distance_in_range(distance_sqr, max_distance, max_distance_sqr)
652                    && nearest
653                        .as_ref()
654                        .is_none_or(|(_, current)| distance_sqr < *current)
655                {
656                    nearest = Some((Arc::clone(player), distance_sqr));
657                }
658            }
659            true
660        });
661        nearest.map(|(player, _)| player)
662    }
663
664    /// Gets the squared distance to the nearest player, if any player is present.
665    #[must_use]
666    pub fn nearest_player_distance_sqr(&self, position: DVec3) -> Option<f64> {
667        let mut nearest = None;
668        self.players.iter_players(|_, player| {
669            if player.is_spectator() {
670                return true;
671            }
672            let distance_sqr = player.position().distance_squared(position);
673            if nearest.is_none_or(|current| distance_sqr < current) {
674                nearest = Some(distance_sqr);
675            }
676            true
677        });
678        nearest
679    }
680
681    /// Gets entities matching vanilla's pushable entity selector for `pusher`.
682    ///
683    /// Vanilla also checks team collision rules; Steel has no teams yet, so this
684    /// currently matches the null-team path where collision is allowed.
685    #[must_use]
686    pub fn get_pushable_entities(
687        &self,
688        pusher: &dyn Entity,
689        aabb: &WorldAabb,
690    ) -> Vec<SharedEntity> {
691        Self::filter_pushable_entities(pusher, self.get_entities_in_aabb(aabb))
692    }
693
694    pub(crate) fn filter_pushable_entities(
695        pusher: &dyn Entity,
696        entities: Vec<SharedEntity>,
697    ) -> Vec<SharedEntity> {
698        let predicate = EntitySelector::pushable_by(pusher);
699
700        entities
701            .into_iter()
702            .filter(|entity| predicate(entity.as_ref()))
703            .collect()
704    }
705
706    /// Registers a game event listener in a chunk section.
707    pub fn register_game_event_listener(
708        &self,
709        section_pos: SectionPos,
710        listener: SharedGameEventListener,
711    ) {
712        let chunk_pos = ChunkPos::new(section_pos.x(), section_pos.z());
713        let registry = self.game_event_listener_storage(chunk_pos);
714        if let Some(registry) = registry {
715            registry.register(section_pos.y(), listener);
716        }
717    }
718
719    /// Unregisters a game event listener from a chunk section.
720    pub fn unregister_game_event_listener(
721        &self,
722        section_pos: SectionPos,
723        listener: &SharedGameEventListener,
724    ) -> bool {
725        let chunk_pos = ChunkPos::new(section_pos.x(), section_pos.z());
726        let registry = self.game_event_listener_storage(chunk_pos);
727        registry.is_some_and(|registry| registry.unregister(section_pos.y(), listener))
728    }
729
730    /// Returns a stable registry handle without retaining the chunk holder read guard.
731    pub(super) fn game_event_listener_storage(
732        &self,
733        chunk_pos: ChunkPos,
734    ) -> Option<Arc<GameEventListenerStorage>> {
735        self.chunk_map.with_full_chunk(chunk_pos, |chunk| {
736            Arc::clone(&chunk.game_event_listeners().registry)
737        })
738    }
739
740    /// Dispatches a game event to all listeners in range.
741    pub fn game_event(
742        self: &Arc<Self>,
743        event: GameEventRef,
744        pos: BlockPos,
745        context: &GameEventContext,
746    ) {
747        self.game_event_at(
748            event,
749            DVec3::new(
750                f64::from(pos.x()) + 0.5,
751                f64::from(pos.y()) + 0.5,
752                f64::from(pos.z()) + 0.5,
753            ),
754            context,
755        );
756    }
757
758    /// Dispatches a game event from an exact world position.
759    pub fn game_event_at(
760        self: &Arc<Self>,
761        event: GameEventRef,
762        source_pos: DVec3,
763        context: &GameEventContext,
764    ) {
765        if !self.game_event_listener_count.has_any() {
766            return;
767        }
768        let radius = event.notification_radius.max(0);
769        let center = BlockPos::from(source_pos);
770        let section_min_x = SectionPos::block_to_section_coord(center.x() - radius);
771        let section_min_y = SectionPos::block_to_section_coord(center.y() - radius);
772        let section_min_z = SectionPos::block_to_section_coord(center.z() - radius);
773        let section_max_x = SectionPos::block_to_section_coord(center.x() + radius);
774        let section_max_y = SectionPos::block_to_section_coord(center.y() + radius);
775        let section_max_z = SectionPos::block_to_section_coord(center.z() + radius);
776        let mut dispatcher = GameEventDispatcher::new(self, event, source_pos, context);
777
778        for section_x in section_min_x..=section_max_x {
779            for section_z in section_min_z..=section_max_z {
780                let registry =
781                    self.game_event_listener_storage(ChunkPos::new(section_x, section_z));
782                if let Some(registry) = registry {
783                    dispatcher.visit_chunk(&registry, section_min_y, section_max_y);
784                }
785            }
786        }
787
788        dispatcher.finish();
789    }
790}