Skip to main content

steel_core/entity/tracker/
mod.rs

1//! Entity tracking system for managing which players can see which entities.
2//!
3//! Keeps the vanilla visibility predicate in block space. Vanilla stores an
4//! entity tracking range as client chunks, multiplies it by 16, caps it by the
5//! player's view distance, and then checks horizontal squared distance.
6
7use std::sync::Arc;
8
9use glam::DVec3;
10use rustc_hash::FxHashSet;
11use steel_protocol::packets::game::{
12    AttributeSnapshot, CAddEntity, CRemoveEntities, CSetEntityData, CSetEntityLink,
13    CSetEntityMotion, CSetEquipment, CSetPassengers, CUpdateAttributes, EquipmentSlotItem,
14    to_angle_byte,
15};
16use steel_registry::entity_data::DataValue;
17use steel_registry::{RegistryEntry, vanilla_entities};
18use steel_utils::ChunkPos;
19use steel_utils::locks::{SyncMutex, SyncRwLock};
20
21use crate::chunk::player_chunk_view::PlayerChunkView;
22use crate::entity::leash::Leashable;
23use crate::entity::{
24    Entity, EntityMovementSyncPacket, MobEffectSyncPacket, ServerEntityMovementSyncState,
25    ServerEntityMovementSyncUpdate, SharedEntity, WeakEntity,
26};
27use crate::player::Player;
28
29const BLOCKS_PER_CHUNK: f64 = 16.0;
30
31/// World-level entity tracker.
32pub struct EntityTracker {
33    /// Maps entity ID to its tracking data.
34    entities: scc::HashMap<i32, TrackedEntity>,
35}
36
37/// Packet sinks used by [`EntityTracker::send_changes`].
38pub struct EntityChangeSenders<
39    Movement,
40    SelfMovement,
41    EntityData,
42    Attributes,
43    MobEffects,
44    Equipment,
45    Passengers,
46    EntityLink,
47> {
48    /// Broadcasts movement and velocity sync packets.
49    pub movement: Movement,
50    /// Sends vanilla self-inclusive hurt motion sync to one player.
51    pub self_movement: SelfMovement,
52    /// Broadcasts entity data watcher changes.
53    pub entity_data: EntityData,
54    /// Broadcasts dirty syncable attributes.
55    pub attributes: Attributes,
56    /// Sends mob-effect add/remove packets to a specific player.
57    pub mob_effects: MobEffects,
58    /// Broadcasts dirty equipment slots.
59    pub equipment: Equipment,
60    /// Sends passenger updates to a specific player.
61    pub passengers: Passengers,
62    /// Broadcasts leash/link holder updates.
63    pub entity_link: EntityLink,
64}
65
66/// Tracking data for a single entity.
67struct TrackedEntity {
68    /// Weak reference to the entity. When this fails to upgrade, entity is dead.
69    entity: WeakEntity,
70    /// Vanilla `ServerEntity` movement sync state owned by the tracker.
71    server_entity: SyncMutex<ServerEntityMovementSyncState>,
72    /// Last direct passenger ids sent to tracking clients.
73    last_passenger_ids: SyncMutex<Vec<i32>>,
74    /// Last leash holder id sent to tracking clients.
75    last_leash_holder_id: SyncMutex<Option<i32>>,
76    /// Vanilla client tracking range converted to blocks.
77    tracking_range: EntityTrackingRange,
78    /// Current chunk used by the player-view predicate.
79    registered_chunk: ChunkPos,
80    /// Players currently tracking this entity (interior mutable for concurrent access).
81    seen_by: SyncRwLock<FxHashSet<i32>>,
82}
83
84#[derive(Debug, Clone, Copy)]
85struct EntityTrackingRange {
86    block_radius: f64,
87}
88
89impl EntityTrackingRange {
90    fn from_client_chunk_range(client_chunk_range: i32) -> Self {
91        Self {
92            block_radius: f64::from(client_chunk_range) * BLOCKS_PER_CHUNK,
93        }
94    }
95
96    fn is_disabled(self) -> bool {
97        self.block_radius <= 0.0
98    }
99
100    fn visible_radius(self, player_view_distance: u8) -> f64 {
101        self.block_radius
102            .min(f64::from(player_view_distance) * BLOCKS_PER_CHUNK)
103    }
104}
105
106impl Default for EntityTracker {
107    fn default() -> Self {
108        Self::new()
109    }
110}
111
112impl EntityTracker {
113    /// Creates a new empty entity tracker.
114    #[must_use]
115    pub fn new() -> Self {
116        Self {
117            entities: scc::HashMap::new(),
118        }
119    }
120
121    /// Starts tracking an entity.
122    ///
123    /// Sends spawn packets to players already watching the entity chunk and
124    /// inside the vanilla tracking range.
125    ///
126    /// The `get_players_in_chunk` callback should return player IDs in a given chunk
127    /// (typically from `PlayerAreaMap::get_tracking_players`).
128    /// The `get_player` callback should resolve a player ID to a `Player` reference.
129    ///
130    /// # Panics
131    /// Panics if the entity is removed or is already tracked.
132    pub fn add(
133        &self,
134        entity: &SharedEntity,
135        get_players_in_chunk: impl Fn(ChunkPos) -> Vec<i32>,
136        get_player: impl Fn(i32) -> Option<Arc<Player>>,
137    ) {
138        assert!(
139            !entity.is_removed(),
140            "cannot add removed entity {} to tracker",
141            entity.id()
142        );
143
144        let entity_id = entity.id();
145        let tracking_range = EntityTrackingRange::from_client_chunk_range(
146            entity.entity_type().client_tracking_range,
147        );
148        if tracking_range.is_disabled() {
149            return;
150        }
151
152        let pos = entity.position();
153        let registered_chunk = ChunkPos::from_entity_pos(pos);
154
155        let players_to_notify = Self::visible_players_for_entity(
156            entity_id,
157            entity.as_ref(),
158            registered_chunk,
159            tracking_range,
160            &get_players_in_chunk,
161            &get_player,
162        );
163        let player_ids_to_notify: Vec<i32> = players_to_notify.iter().copied().collect();
164
165        let tracked_entity = TrackedEntity {
166            entity: Arc::downgrade(entity),
167            server_entity: SyncMutex::new(ServerEntityMovementSyncState::new(
168                pos,
169                entity.velocity(),
170                entity.on_ground(),
171                entity.rotation(),
172                entity.head_yaw(),
173                entity.entity_type().update_interval,
174                entity.entity_type().track_deltas,
175            )),
176            last_passenger_ids: SyncMutex::new(self.direct_tracked_passenger_ids(entity.as_ref())),
177            last_leash_holder_id: SyncMutex::new(leash_holder_id(entity.as_ref())),
178            tracking_range,
179            registered_chunk,
180            seen_by: SyncRwLock::new(players_to_notify),
181        };
182
183        assert!(
184            self.entities.insert_sync(entity_id, tracked_entity).is_ok(),
185            "entity {entity_id} is already tracked"
186        );
187
188        // Send spawn packets to all nearby players
189        for player_id in player_ids_to_notify {
190            if let Some(player) = get_player(player_id) {
191                self.send_spawn_packets(entity, &player);
192            }
193        }
194    }
195
196    /// Stops tracking an entity and sends despawn to all tracking players.
197    pub fn remove(&self, entity_id: i32, get_player: impl Fn(i32) -> Option<Arc<Player>>) {
198        if let Some((_, tracked)) = self.entities.remove_sync(&entity_id) {
199            let entity = tracked.entity.upgrade();
200            // Send despawn to all tracking players
201            for player_id in tracked.seen_by.read().iter() {
202                if let Some(player) = get_player(*player_id) {
203                    if let Some(entity) = &entity
204                        && let Some(packet) =
205                            self.vehicle_passenger_packet_for_player(entity.as_ref(), *player_id)
206                    {
207                        player.send_packet(packet);
208                    }
209                    player.send_packet(CRemoveEntities::single(entity_id));
210                }
211            }
212        }
213    }
214
215    /// Refreshes the tracked-entity set for one player.
216    ///
217    /// Mirrors vanilla `TrackedEntity.updatePlayer`: each tracked entity checks
218    /// whether the player tracks the entity chunk, passes the entity-specific
219    /// broadcast predicate, and is inside the effective horizontal range.
220    pub fn update_player(
221        &self,
222        player: &Player,
223        view: &PlayerChunkView,
224        is_chunk_sent: impl Fn(ChunkPos) -> bool,
225    ) {
226        let player_id = player.id();
227        let player_pos = player.position();
228        let player_view_distance = view.view_distance;
229
230        let mut entities_to_despawn = Vec::new();
231        let mut entities_to_spawn = Vec::new();
232        let mut dead_entities = Vec::new();
233
234        self.entities.iter_sync(|entity_id, tracked| {
235            let entity_id = *entity_id;
236            let Some(entity) = tracked.entity.upgrade() else {
237                dead_entities.push(entity_id);
238                return true;
239            };
240
241            let visible = !entity.is_removed()
242                && entity_id != player_id
243                && view.contains(tracked.registered_chunk)
244                && is_chunk_sent(tracked.registered_chunk)
245                && entity.broadcast_to_player(player)
246                && is_within_tracking_distance(
247                    entity.position(),
248                    player_pos,
249                    effective_tracking_range(entity.as_ref(), tracked.tracking_range),
250                    player_view_distance,
251                );
252
253            let mut despawn = false;
254            {
255                let mut seen_by = tracked.seen_by.write();
256                if visible {
257                    if seen_by.insert(player_id) {
258                        entities_to_spawn.push(Arc::clone(&entity));
259                    }
260                } else if seen_by.remove(&player_id) {
261                    despawn = true;
262                }
263            }
264
265            if despawn {
266                let vehicle_packet =
267                    self.vehicle_passenger_packet_for_player(entity.as_ref(), player_id);
268                entities_to_despawn.push((entity_id, vehicle_packet));
269            }
270
271            true
272        });
273
274        for (entity_id, vehicle_packet) in entities_to_despawn {
275            if let Some(packet) = vehicle_packet {
276                player.send_packet(packet);
277            }
278            player.send_packet(CRemoveEntities::single(entity_id));
279        }
280
281        for entity in entities_to_spawn {
282            self.send_spawn_packets(&entity, player);
283        }
284
285        // Clean up dead entities we encountered
286        for entity_id in dead_entities {
287            self.remove_dead_entity(entity_id);
288        }
289    }
290
291    /// Sends tracker-owned movement changes for all tracked entities.
292    ///
293    /// Mirrors vanilla `ChunkMap.tick` driving `ServerEntity.sendChanges`.
294    #[expect(
295        clippy::too_many_lines,
296        reason = "entity tracker fanout mirrors vanilla sendChanges ordering"
297    )]
298    pub fn send_changes<
299        Movement,
300        SelfMovement,
301        EntityData,
302        Attributes,
303        MobEffects,
304        Equipment,
305        Passengers,
306        EntityLink,
307    >(
308        &self,
309        get_players_in_chunk: impl Fn(ChunkPos) -> Vec<i32>,
310        get_player: impl Fn(i32) -> Option<Arc<Player>>,
311        mut senders: EntityChangeSenders<
312            Movement,
313            SelfMovement,
314            EntityData,
315            Attributes,
316            MobEffects,
317            Equipment,
318            Passengers,
319            EntityLink,
320        >,
321    ) where
322        Movement: FnMut(i32, EntityMovementSyncPacket),
323        SelfMovement: FnMut(i32, EntityMovementSyncPacket),
324        EntityData: FnMut(i32, Vec<DataValue>),
325        Attributes: FnMut(i32, Vec<AttributeSnapshot>),
326        MobEffects: FnMut(i32, MobEffectSyncPacket),
327        Equipment: FnMut(i32, CSetEquipment),
328        Passengers: FnMut(i32, CSetPassengers),
329        EntityLink: FnMut(i32, CSetEntityLink),
330    {
331        let mut dead_entities = Vec::new();
332        let mut entities_to_refresh = Vec::new();
333        let mut passenger_packets_to_send = Vec::new();
334        let mut packets_to_broadcast = Vec::new();
335        let mut self_movement_packets = Vec::new();
336        let mut entity_data_to_broadcast = Vec::new();
337        let mut attributes_to_broadcast = Vec::new();
338        let mut mob_effect_packets_to_send = Vec::new();
339        let mut equipment_to_broadcast = Vec::new();
340        let mut entity_links_to_broadcast = Vec::new();
341
342        self.entities.iter_sync(|entity_id, tracked| {
343            let entity_id = *entity_id;
344            let Some(entity) = tracked.entity.upgrade() else {
345                dead_entities.push(entity_id);
346                return true;
347            };
348
349            if entity.is_removed() {
350                return true;
351            }
352
353            entity.update_data_before_sync();
354
355            let passenger_ids = self.direct_tracked_passenger_ids(entity.as_ref());
356            {
357                let mut last_passenger_ids = tracked.last_passenger_ids.lock();
358                if *last_passenger_ids != passenger_ids {
359                    let changed_player_passenger_ids = direct_player_passenger_delta(
360                        &last_passenger_ids,
361                        &passenger_ids,
362                        &get_player,
363                    );
364                    let seen_by = tracked.seen_by.read();
365                    for player_id in seen_by.iter() {
366                        if changed_player_passenger_ids.contains(player_id) {
367                            continue;
368                        }
369                        passenger_packets_to_send.push((
370                            *player_id,
371                            CSetPassengers::new(
372                                entity_id,
373                                self.direct_passenger_ids_seen_by_player(
374                                    entity.as_ref(),
375                                    *player_id,
376                                ),
377                            ),
378                        ));
379                    }
380                    *last_passenger_ids = passenger_ids;
381                    entities_to_refresh.push(entity_id);
382                }
383            }
384
385            let dirty_entity_data = entity.pack_dirty_entity_data();
386            let has_dirty_entity_data = dirty_entity_data.is_some();
387            let result =
388                tracked
389                    .server_entity
390                    .lock()
391                    .record_send_changes(ServerEntityMovementSyncUpdate {
392                        entity_type: entity.entity_type(),
393                        vertical_collision: entity.vertical_collision(),
394                        horizontal_collision: entity.horizontal_collision(),
395                        requires_precise_position: entity.base().requires_precise_position(),
396                        entity_id,
397                        is_passenger: entity.is_passenger(),
398                        position: entity.position(),
399                        velocity: entity.velocity(),
400                        body_rotation: entity.rotation(),
401                        head_yaw: entity.head_yaw(),
402                        on_ground: entity.on_ground(),
403                        needs_velocity_sync: entity.needs_velocity_sync(),
404                        has_dirty_entity_data,
405                        force_velocity_sync: entity.forces_fall_flying_velocity_sync(),
406                    });
407            if result.should_clear_velocity_sync() {
408                entity.clear_velocity_sync();
409            }
410            result.for_each_packet(|packet| packets_to_broadcast.push((entity_id, packet)));
411            if entity.hurt_marked() {
412                let velocity = entity.velocity();
413                packets_to_broadcast.push((
414                    entity_id,
415                    EntityMovementSyncPacket::from(CSetEntityMotion::new(entity_id, velocity)),
416                ));
417                if entity.entity_type() == &vanilla_entities::PLAYER {
418                    self_movement_packets.push((
419                        entity_id,
420                        EntityMovementSyncPacket::from(CSetEntityMotion::new(entity_id, velocity)),
421                    ));
422                }
423                entity.clear_hurt_mark();
424            }
425            if let Some(dirty_entity_data) = dirty_entity_data {
426                entity_data_to_broadcast.push((entity_id, dirty_entity_data));
427            }
428            if let Some(living) = entity.as_living_entity() {
429                let dirty_attributes = living.drain_dirty_syncable_attributes();
430                if !dirty_attributes.is_empty() {
431                    attributes_to_broadcast.push((entity_id, dirty_attributes));
432                }
433                let dirty_mob_effects = living.drain_dirty_mob_effects();
434                if !dirty_mob_effects.is_empty() {
435                    let mut recipient_ids = FxHashSet::default();
436                    if entity.entity_type() == &vanilla_entities::PLAYER
437                        && get_player(entity_id).is_some()
438                    {
439                        recipient_ids.insert(entity_id);
440                    }
441                    for passenger in entity.passengers() {
442                        let passenger_id = passenger.id();
443                        if passenger.entity_type() == &vanilla_entities::PLAYER
444                            && get_player(passenger_id).is_some()
445                        {
446                            recipient_ids.insert(passenger_id);
447                        }
448                    }
449                    for recipient_id in recipient_ids {
450                        for change in &dirty_mob_effects {
451                            mob_effect_packets_to_send.push((
452                                recipient_id,
453                                change.packet(entity_id, recipient_id == entity_id),
454                            ));
455                        }
456                    }
457                }
458                let dirty_equipment = living.drain_dirty_equipment();
459                if !dirty_equipment.is_empty() {
460                    equipment_to_broadcast.push((entity_id, dirty_equipment));
461                }
462            }
463
464            let leash_holder_id = leash_holder_id(entity.as_ref());
465            {
466                let mut last_leash_holder_id = tracked.last_leash_holder_id.lock();
467                if *last_leash_holder_id != leash_holder_id {
468                    entity_links_to_broadcast.push((
469                        entity_id,
470                        CSetEntityLink::new(entity_id, leash_holder_id.unwrap_or_default()),
471                    ));
472                    *last_leash_holder_id = leash_holder_id;
473                }
474            }
475
476            true
477        });
478
479        for entity_id in dead_entities {
480            self.remove_dead_entity(entity_id);
481        }
482
483        for (player_id, packet) in passenger_packets_to_send {
484            (senders.passengers)(player_id, packet);
485        }
486
487        for entity_id in entities_to_refresh {
488            self.refresh_entity_players(entity_id, &get_players_in_chunk, &get_player);
489        }
490
491        for (entity_id, packet) in packets_to_broadcast {
492            (senders.movement)(entity_id, packet);
493        }
494
495        for (player_id, packet) in self_movement_packets {
496            (senders.self_movement)(player_id, packet);
497        }
498
499        for (entity_id, dirty_entity_data) in entity_data_to_broadcast {
500            (senders.entity_data)(entity_id, dirty_entity_data);
501        }
502
503        for (entity_id, dirty_equipment) in equipment_to_broadcast {
504            (senders.equipment)(entity_id, CSetEquipment::new(entity_id, dirty_equipment));
505        }
506
507        for (entity_id, dirty_attributes) in attributes_to_broadcast {
508            (senders.attributes)(entity_id, dirty_attributes);
509        }
510
511        for (player_id, packet) in mob_effect_packets_to_send {
512            (senders.mob_effects)(player_id, packet);
513        }
514
515        for (entity_id, packet) in entity_links_to_broadcast {
516            (senders.entity_link)(entity_id, packet);
517        }
518    }
519
520    /// Removes a player from every entity pairing and despawns those entities from their client.
521    ///
522    /// Mirrors vanilla `ChunkMap.removeEntity` calling `TrackedEntity.removePlayer` for each
523    /// tracked entity before the player leaves the world.
524    pub fn on_player_leave(&self, player: &Player) {
525        let player_id = player.id();
526        let mut entities_to_despawn = Vec::new();
527        let mut dead_entities = Vec::new();
528
529        self.entities.iter_sync(|entity_id, tracked| {
530            if tracked.seen_by.write().remove(&player_id) {
531                entities_to_despawn.push(*entity_id);
532            }
533            if tracked.entity.strong_count() == 0 {
534                dead_entities.push(*entity_id);
535            }
536            true
537        });
538
539        for entity_id in entities_to_despawn {
540            player.send_packet(CRemoveEntities::single(entity_id));
541        }
542
543        for entity_id in dead_entities {
544            self.remove_dead_entity(entity_id);
545        }
546    }
547
548    /// Updates an entity's current chunk and visible players after a section move.
549    ///
550    /// Vanilla refreshes tracked players when an entity's section position changes.
551    /// The old and new chunks may be the same for purely vertical section moves.
552    pub fn on_entity_section_change(
553        &self,
554        entity_id: i32,
555        old_chunk: ChunkPos,
556        new_chunk: ChunkPos,
557        get_players_in_chunk: impl Fn(ChunkPos) -> Vec<i32>,
558        get_player: impl Fn(i32) -> Option<Arc<Player>>,
559    ) {
560        let mut players_to_remove = Vec::new();
561        let mut players_to_add = Vec::new();
562        let mut entity_to_spawn = None;
563
564        self.entities.update_sync(&entity_id, |_, tracked| {
565            if old_chunk != new_chunk {
566                tracked.registered_chunk = new_chunk;
567            }
568
569            let Some(entity) = tracked.entity.upgrade() else {
570                return;
571            };
572
573            let new_seen_by = Self::visible_players_for_entity(
574                entity_id,
575                entity.as_ref(),
576                new_chunk,
577                tracked.tracking_range,
578                &get_players_in_chunk,
579                &get_player,
580            );
581
582            let mut seen_by = tracked.seen_by.write();
583            players_to_remove.extend(seen_by.difference(&new_seen_by).copied());
584            players_to_add.extend(new_seen_by.difference(&seen_by).copied());
585            *seen_by = new_seen_by;
586            entity_to_spawn = Some(entity);
587        });
588
589        let Some(entity) = entity_to_spawn else {
590            return;
591        };
592
593        for player_id in players_to_remove {
594            if let Some(player) = get_player(player_id) {
595                if let Some(packet) =
596                    self.vehicle_passenger_packet_for_player(entity.as_ref(), player_id)
597                {
598                    player.send_packet(packet);
599                }
600                player.send_packet(CRemoveEntities::single(entity_id));
601            }
602        }
603
604        for player_id in players_to_add {
605            if let Some(player) = get_player(player_id) {
606                self.send_spawn_packets(&entity, &player);
607            }
608        }
609    }
610
611    fn refresh_entity_players(
612        &self,
613        entity_id: i32,
614        get_players_in_chunk: &impl Fn(ChunkPos) -> Vec<i32>,
615        get_player: &impl Fn(i32) -> Option<Arc<Player>>,
616    ) {
617        let mut players_to_remove = Vec::new();
618        let mut players_to_add = Vec::new();
619        let mut entity_to_spawn = None;
620
621        self.entities.update_sync(&entity_id, |_, tracked| {
622            let Some(entity) = tracked.entity.upgrade() else {
623                return;
624            };
625
626            let new_seen_by = Self::visible_players_for_entity(
627                entity_id,
628                entity.as_ref(),
629                tracked.registered_chunk,
630                tracked.tracking_range,
631                get_players_in_chunk,
632                get_player,
633            );
634
635            let mut seen_by = tracked.seen_by.write();
636            players_to_remove.extend(seen_by.difference(&new_seen_by).copied());
637            players_to_add.extend(new_seen_by.difference(&seen_by).copied());
638            *seen_by = new_seen_by;
639            entity_to_spawn = Some(entity);
640        });
641
642        let Some(entity) = entity_to_spawn else {
643            return;
644        };
645
646        for player_id in players_to_remove {
647            if let Some(player) = get_player(player_id) {
648                if let Some(packet) =
649                    self.vehicle_passenger_packet_for_player(entity.as_ref(), player_id)
650                {
651                    player.send_packet(packet);
652                }
653                player.send_packet(CRemoveEntities::single(entity_id));
654            }
655        }
656
657        for player_id in players_to_add {
658            if let Some(player) = get_player(player_id) {
659                self.send_spawn_packets(&entity, &player);
660            }
661        }
662    }
663
664    /// Gets the number of tracked entities.
665    #[must_use]
666    pub fn count(&self) -> usize {
667        self.entities.len()
668    }
669
670    /// Returns players currently tracking an entity.
671    #[must_use]
672    pub fn tracking_player_ids(&self, entity_id: i32) -> Vec<i32> {
673        self.entities
674            .read_sync(&entity_id, |_, tracked| {
675                tracked.seen_by.read().iter().copied().collect()
676            })
677            .unwrap_or_default()
678    }
679
680    fn remove_dead_entity(&self, entity_id: i32) {
681        // Note: We don't send despawn packets here because the players
682        // will get updated via player view changes or explicit removals.
683        let _ = self.entities.remove_sync(&entity_id);
684    }
685
686    fn visible_players_for_entity(
687        entity_id: i32,
688        entity: &dyn Entity,
689        entity_chunk: ChunkPos,
690        tracking_range: EntityTrackingRange,
691        get_players_in_chunk: &impl Fn(ChunkPos) -> Vec<i32>,
692        get_player: &impl Fn(i32) -> Option<Arc<Player>>,
693    ) -> FxHashSet<i32> {
694        let entity_pos = entity.position();
695        let tracking_range = effective_tracking_range(entity, tracking_range);
696        let mut players = FxHashSet::default();
697        if entity.is_removed() {
698            return players;
699        }
700
701        for player_id in get_players_in_chunk(entity_chunk) {
702            if player_id == entity_id {
703                continue;
704            }
705
706            let Some(player) = get_player(player_id) else {
707                continue;
708            };
709
710            if entity.broadcast_to_player(&player)
711                && is_within_tracking_distance(
712                    entity_pos,
713                    player.position(),
714                    tracking_range,
715                    player.view_distance(),
716                )
717            {
718                players.insert(player_id);
719            }
720        }
721
722        players
723    }
724
725    fn send_spawn_packets(&self, entity: &SharedEntity, player: &Player) {
726        let entity_id = entity.id();
727        self.spawn_pairing(entity, player.id())
728            .send_to(entity_id, player);
729    }
730
731    fn spawn_pairing(&self, entity: &SharedEntity, player_id: i32) -> EntitySpawnPairing {
732        EntitySpawnPairing::from_entity(
733            entity,
734            self.passenger_pairing_packets_for_player(entity.as_ref(), player_id),
735        )
736    }
737
738    fn passenger_pairing_packets_for_player(
739        &self,
740        entity: &dyn Entity,
741        player_id: i32,
742    ) -> Vec<CSetPassengers> {
743        let mut packets = Vec::new();
744        let passenger_ids = self.direct_passenger_ids_seen_by_player(entity, player_id);
745        if !passenger_ids.is_empty() {
746            packets.push(CSetPassengers::new(entity.id(), passenger_ids));
747        }
748
749        if let Some(vehicle) = entity.vehicle()
750            && self.entity_seen_by_player(vehicle.id(), player_id)
751        {
752            packets.push(CSetPassengers::new(
753                vehicle.id(),
754                self.direct_passenger_ids_seen_by_player(vehicle.as_ref(), player_id),
755            ));
756        }
757
758        packets
759    }
760
761    fn vehicle_passenger_packet_for_player(
762        &self,
763        entity: &dyn Entity,
764        player_id: i32,
765    ) -> Option<CSetPassengers> {
766        let vehicle = entity.vehicle()?;
767        if !self.entity_seen_by_player(vehicle.id(), player_id) {
768            return None;
769        }
770        Some(CSetPassengers::new(
771            vehicle.id(),
772            self.direct_passenger_ids_seen_by_player(vehicle.as_ref(), player_id),
773        ))
774    }
775
776    fn direct_tracked_passenger_ids(&self, entity: &dyn Entity) -> Vec<i32> {
777        entity
778            .passengers()
779            .into_iter()
780            .filter(|passenger| self.is_entity_tracked(passenger.id()))
781            .map(|passenger| passenger.id())
782            .collect()
783    }
784
785    fn direct_passenger_ids_seen_by_player(&self, entity: &dyn Entity, player_id: i32) -> Vec<i32> {
786        entity
787            .passengers()
788            .into_iter()
789            .filter(|passenger| {
790                passenger.id() == player_id || self.entity_seen_by_player(passenger.id(), player_id)
791            })
792            .map(|passenger| passenger.id())
793            .collect()
794    }
795
796    fn is_entity_tracked(&self, entity_id: i32) -> bool {
797        self.entities.read_sync(&entity_id, |_, _| ()).is_some()
798    }
799
800    fn entity_seen_by_player(&self, entity_id: i32, player_id: i32) -> bool {
801        self.entities
802            .read_sync(&entity_id, |_, tracked| {
803                tracked.seen_by.read().contains(&player_id)
804            })
805            .unwrap_or(false)
806    }
807}
808
809fn leash_holder_id(entity: &dyn Entity) -> Option<i32> {
810    entity
811        .as_leashable()
812        .and_then(Leashable::leash_holder)
813        .map(|holder| holder.id())
814}
815
816fn is_within_tracking_distance(
817    entity_pos: DVec3,
818    player_pos: DVec3,
819    tracking_range: EntityTrackingRange,
820    player_view_distance: u8,
821) -> bool {
822    let visible_radius = tracking_range.visible_radius(player_view_distance);
823    let x = player_pos.x - entity_pos.x;
824    let z = player_pos.z - entity_pos.z;
825    x * x + z * z <= visible_radius * visible_radius
826}
827
828fn effective_tracking_range(
829    entity: &dyn Entity,
830    base_range: EntityTrackingRange,
831) -> EntityTrackingRange {
832    let mut range = base_range;
833    let mut visited = FxHashSet::default();
834    visited.insert(entity.id());
835    add_passenger_tracking_ranges(entity, &mut range, &mut visited);
836    range
837}
838
839fn add_passenger_tracking_ranges(
840    entity: &dyn Entity,
841    range: &mut EntityTrackingRange,
842    visited: &mut FxHashSet<i32>,
843) {
844    for passenger in entity.passengers() {
845        if !visited.insert(passenger.id()) {
846            continue;
847        }
848        let passenger_range = EntityTrackingRange::from_client_chunk_range(
849            passenger.entity_type().client_tracking_range,
850        );
851        range.block_radius = range.block_radius.max(passenger_range.block_radius);
852        add_passenger_tracking_ranges(passenger.as_ref(), range, visited);
853    }
854}
855
856struct EntitySpawnPairing {
857    spawn_packet: CAddEntity,
858    entity_data: Vec<DataValue>,
859    attributes: Vec<AttributeSnapshot>,
860    equipment: Vec<EquipmentSlotItem>,
861    passenger_packets: Vec<CSetPassengers>,
862    entity_link_packet: Option<CSetEntityLink>,
863}
864
865impl EntitySpawnPairing {
866    fn from_entity(entity: &SharedEntity, passenger_packets: Vec<CSetPassengers>) -> Self {
867        entity.update_data_before_sync();
868
869        let pos = entity.spawn_position();
870        let vel = entity.velocity();
871        let (yaw, pitch) = entity.rotation();
872        let head_yaw = entity.head_yaw();
873        let entity_type_id = entity.entity_type().id() as i32;
874
875        // Convert rotation from degrees to protocol byte format (256th of a full rotation)
876        // Uses to_angle_byte which matches vanilla's Mth.packDegrees
877        let x_rot = to_angle_byte(pitch);
878        let y_rot = to_angle_byte(yaw);
879        let head_y_rot = to_angle_byte(head_yaw);
880
881        let (attributes, equipment) = entity.as_living_entity().map_or_else(
882            || (Vec::new(), Vec::new()),
883            |living| {
884                (
885                    living.pack_syncable_attributes(),
886                    living.pack_all_equipment(),
887                )
888            },
889        );
890
891        Self {
892            spawn_packet: CAddEntity {
893                id: entity.id(),
894                uuid: entity.uuid(),
895                entity_type: entity_type_id,
896                position: pos,
897                velocity: vel,
898                x_rot,
899                y_rot,
900                head_y_rot,
901                data: entity.spawn_data(),
902            },
903            entity_data: entity.pack_all_entity_data(),
904            attributes,
905            equipment,
906            passenger_packets,
907            entity_link_packet: leash_holder_id(entity.as_ref())
908                .map(|holder_id| CSetEntityLink::new(entity.id(), holder_id)),
909        }
910    }
911
912    fn send_to(self, entity_id: i32, player: &Player) {
913        player.send_bundle(|bundle| {
914            bundle.add(self.spawn_packet);
915
916            if !self.entity_data.is_empty() {
917                bundle.add(CSetEntityData::new(entity_id, self.entity_data));
918            }
919
920            if !self.attributes.is_empty() {
921                bundle.add(CUpdateAttributes::new(entity_id, self.attributes));
922            }
923
924            if !self.equipment.is_empty() {
925                bundle.add(CSetEquipment::new(entity_id, self.equipment));
926            }
927
928            for packet in self.passenger_packets {
929                bundle.add(packet);
930            }
931
932            if let Some(packet) = self.entity_link_packet {
933                bundle.add(packet);
934            }
935        });
936    }
937}
938
939fn direct_player_passenger_delta(
940    old_passenger_ids: &[i32],
941    new_passenger_ids: &[i32],
942    get_player: &impl Fn(i32) -> Option<Arc<Player>>,
943) -> Vec<i32> {
944    let old_passenger_ids = old_passenger_ids.iter().copied().collect::<FxHashSet<_>>();
945    let new_passenger_ids = new_passenger_ids.iter().copied().collect::<FxHashSet<_>>();
946    old_passenger_ids
947        .symmetric_difference(&new_passenger_ids)
948        .copied()
949        .filter(|entity_id| get_player(*entity_id).is_some())
950        .collect()
951}
952
953#[cfg(test)]
954mod tests;