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