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