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(entity.clone());
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_id,
393                        is_passenger: entity.is_passenger(),
394                        position: entity.position(),
395                        velocity: entity.velocity(),
396                        body_rotation: entity.rotation(),
397                        head_yaw: entity.head_yaw(),
398                        on_ground: entity.on_ground(),
399                        needs_velocity_sync: entity.needs_velocity_sync(),
400                        has_dirty_entity_data,
401                        force_velocity_sync: entity.forces_fall_flying_velocity_sync(),
402                    });
403            if result.should_clear_velocity_sync() {
404                entity.clear_velocity_sync();
405            }
406            result.for_each_packet(|packet| packets_to_broadcast.push((entity_id, packet)));
407            if entity.hurt_marked() {
408                let velocity = entity.velocity();
409                packets_to_broadcast.push((
410                    entity_id,
411                    EntityMovementSyncPacket::from(CSetEntityMotion::new(entity_id, velocity)),
412                ));
413                if entity.entity_type() == &vanilla_entities::PLAYER {
414                    self_movement_packets.push((
415                        entity_id,
416                        EntityMovementSyncPacket::from(CSetEntityMotion::new(entity_id, velocity)),
417                    ));
418                }
419                entity.clear_hurt_mark();
420            }
421            if let Some(dirty_entity_data) = dirty_entity_data {
422                entity_data_to_broadcast.push((entity_id, dirty_entity_data));
423            }
424            if let Some(living) = entity.as_living_entity() {
425                let dirty_attributes = living.drain_dirty_syncable_attributes();
426                if !dirty_attributes.is_empty() {
427                    attributes_to_broadcast.push((entity_id, dirty_attributes));
428                }
429                let dirty_mob_effects = living.drain_dirty_mob_effects();
430                if !dirty_mob_effects.is_empty() {
431                    let mut recipient_ids = FxHashSet::default();
432                    if entity.entity_type() == &vanilla_entities::PLAYER
433                        && get_player(entity_id).is_some()
434                    {
435                        recipient_ids.insert(entity_id);
436                    }
437                    for passenger in entity.passengers() {
438                        let passenger_id = passenger.id();
439                        if passenger.entity_type() == &vanilla_entities::PLAYER
440                            && get_player(passenger_id).is_some()
441                        {
442                            recipient_ids.insert(passenger_id);
443                        }
444                    }
445                    for recipient_id in recipient_ids {
446                        for change in &dirty_mob_effects {
447                            mob_effect_packets_to_send.push((
448                                recipient_id,
449                                change.packet(entity_id, recipient_id == entity_id),
450                            ));
451                        }
452                    }
453                }
454                let dirty_equipment = living.drain_dirty_equipment();
455                if !dirty_equipment.is_empty() {
456                    equipment_to_broadcast.push((entity_id, dirty_equipment));
457                }
458            }
459
460            let leash_holder_id = leash_holder_id(entity.as_ref());
461            {
462                let mut last_leash_holder_id = tracked.last_leash_holder_id.lock();
463                if *last_leash_holder_id != leash_holder_id {
464                    entity_links_to_broadcast.push((
465                        entity_id,
466                        CSetEntityLink::new(entity_id, leash_holder_id.unwrap_or_default()),
467                    ));
468                    *last_leash_holder_id = leash_holder_id;
469                }
470            }
471
472            true
473        });
474
475        for entity_id in dead_entities {
476            self.remove_dead_entity(entity_id);
477        }
478
479        for (player_id, packet) in passenger_packets_to_send {
480            (senders.passengers)(player_id, packet);
481        }
482
483        for entity_id in entities_to_refresh {
484            self.refresh_entity_players(entity_id, &get_players_in_chunk, &get_player);
485        }
486
487        for (entity_id, packet) in packets_to_broadcast {
488            (senders.movement)(entity_id, packet);
489        }
490
491        for (player_id, packet) in self_movement_packets {
492            (senders.self_movement)(player_id, packet);
493        }
494
495        for (entity_id, dirty_entity_data) in entity_data_to_broadcast {
496            (senders.entity_data)(entity_id, dirty_entity_data);
497        }
498
499        for (entity_id, dirty_equipment) in equipment_to_broadcast {
500            (senders.equipment)(entity_id, CSetEquipment::new(entity_id, dirty_equipment));
501        }
502
503        for (entity_id, dirty_attributes) in attributes_to_broadcast {
504            (senders.attributes)(entity_id, dirty_attributes);
505        }
506
507        for (player_id, packet) in mob_effect_packets_to_send {
508            (senders.mob_effects)(player_id, packet);
509        }
510
511        for (entity_id, packet) in entity_links_to_broadcast {
512            (senders.entity_link)(entity_id, packet);
513        }
514    }
515
516    /// Removes a player from every entity pairing and despawns those entities from their client.
517    ///
518    /// Mirrors vanilla `ChunkMap.removeEntity` calling `TrackedEntity.removePlayer` for each
519    /// tracked entity before the player leaves the world.
520    pub fn on_player_leave(&self, player: &Player) {
521        let player_id = player.id();
522        let mut entities_to_despawn = Vec::new();
523        let mut dead_entities = Vec::new();
524
525        self.entities.iter_sync(|entity_id, tracked| {
526            if tracked.seen_by.write().remove(&player_id) {
527                entities_to_despawn.push(*entity_id);
528            }
529            if tracked.entity.strong_count() == 0 {
530                dead_entities.push(*entity_id);
531            }
532            true
533        });
534
535        for entity_id in entities_to_despawn {
536            player.send_packet(CRemoveEntities::single(entity_id));
537        }
538
539        for entity_id in dead_entities {
540            self.remove_dead_entity(entity_id);
541        }
542    }
543
544    /// Updates an entity's current chunk and visible players after a section move.
545    ///
546    /// Vanilla refreshes tracked players when an entity's section position changes.
547    /// The old and new chunks may be the same for purely vertical section moves.
548    pub fn on_entity_section_change(
549        &self,
550        entity_id: i32,
551        old_chunk: ChunkPos,
552        new_chunk: ChunkPos,
553        get_players_in_chunk: impl Fn(ChunkPos) -> Vec<i32>,
554        get_player: impl Fn(i32) -> Option<Arc<Player>>,
555    ) {
556        let mut players_to_remove = Vec::new();
557        let mut players_to_add = Vec::new();
558        let mut entity_to_spawn = None;
559
560        self.entities.update_sync(&entity_id, |_, tracked| {
561            if old_chunk != new_chunk {
562                tracked.registered_chunk = new_chunk;
563            }
564
565            let Some(entity) = tracked.entity.upgrade() else {
566                return;
567            };
568
569            let new_seen_by = Self::visible_players_for_entity(
570                entity_id,
571                entity.as_ref(),
572                new_chunk,
573                tracked.tracking_range,
574                &get_players_in_chunk,
575                &get_player,
576            );
577
578            let mut seen_by = tracked.seen_by.write();
579            players_to_remove.extend(seen_by.difference(&new_seen_by).copied());
580            players_to_add.extend(new_seen_by.difference(&seen_by).copied());
581            *seen_by = new_seen_by;
582            entity_to_spawn = Some(entity);
583        });
584
585        let Some(entity) = entity_to_spawn else {
586            return;
587        };
588
589        for player_id in players_to_remove {
590            if let Some(player) = get_player(player_id) {
591                if let Some(packet) =
592                    self.vehicle_passenger_packet_for_player(entity.as_ref(), player_id)
593                {
594                    player.send_packet(packet);
595                }
596                player.send_packet(CRemoveEntities::single(entity_id));
597            }
598        }
599
600        for player_id in players_to_add {
601            if let Some(player) = get_player(player_id) {
602                self.send_spawn_packets(&entity, &player);
603            }
604        }
605    }
606
607    fn refresh_entity_players(
608        &self,
609        entity_id: i32,
610        get_players_in_chunk: &impl Fn(ChunkPos) -> Vec<i32>,
611        get_player: &impl Fn(i32) -> Option<Arc<Player>>,
612    ) {
613        let mut players_to_remove = Vec::new();
614        let mut players_to_add = Vec::new();
615        let mut entity_to_spawn = None;
616
617        self.entities.update_sync(&entity_id, |_, tracked| {
618            let Some(entity) = tracked.entity.upgrade() else {
619                return;
620            };
621
622            let new_seen_by = Self::visible_players_for_entity(
623                entity_id,
624                entity.as_ref(),
625                tracked.registered_chunk,
626                tracked.tracking_range,
627                get_players_in_chunk,
628                get_player,
629            );
630
631            let mut seen_by = tracked.seen_by.write();
632            players_to_remove.extend(seen_by.difference(&new_seen_by).copied());
633            players_to_add.extend(new_seen_by.difference(&seen_by).copied());
634            *seen_by = new_seen_by;
635            entity_to_spawn = Some(entity);
636        });
637
638        let Some(entity) = entity_to_spawn else {
639            return;
640        };
641
642        for player_id in players_to_remove {
643            if let Some(player) = get_player(player_id) {
644                if let Some(packet) =
645                    self.vehicle_passenger_packet_for_player(entity.as_ref(), player_id)
646                {
647                    player.send_packet(packet);
648                }
649                player.send_packet(CRemoveEntities::single(entity_id));
650            }
651        }
652
653        for player_id in players_to_add {
654            if let Some(player) = get_player(player_id) {
655                self.send_spawn_packets(&entity, &player);
656            }
657        }
658    }
659
660    /// Gets the number of tracked entities.
661    #[must_use]
662    pub fn count(&self) -> usize {
663        self.entities.len()
664    }
665
666    /// Returns players currently tracking an entity.
667    #[must_use]
668    pub fn tracking_player_ids(&self, entity_id: i32) -> Vec<i32> {
669        self.entities
670            .read_sync(&entity_id, |_, tracked| {
671                tracked.seen_by.read().iter().copied().collect()
672            })
673            .unwrap_or_default()
674    }
675
676    fn remove_dead_entity(&self, entity_id: i32) {
677        // Note: We don't send despawn packets here because the players
678        // will get updated via player view changes or explicit removals.
679        let _ = self.entities.remove_sync(&entity_id);
680    }
681
682    fn visible_players_for_entity(
683        entity_id: i32,
684        entity: &dyn Entity,
685        entity_chunk: ChunkPos,
686        tracking_range: EntityTrackingRange,
687        get_players_in_chunk: &impl Fn(ChunkPos) -> Vec<i32>,
688        get_player: &impl Fn(i32) -> Option<Arc<Player>>,
689    ) -> FxHashSet<i32> {
690        let entity_pos = entity.position();
691        let tracking_range = effective_tracking_range(entity, tracking_range);
692        let mut players = FxHashSet::default();
693        if entity.is_removed() {
694            return players;
695        }
696
697        for player_id in get_players_in_chunk(entity_chunk) {
698            if player_id == entity_id {
699                continue;
700            }
701
702            let Some(player) = get_player(player_id) else {
703                continue;
704            };
705
706            if entity.broadcast_to_player(&player)
707                && is_within_tracking_distance(
708                    entity_pos,
709                    player.position(),
710                    tracking_range,
711                    player.view_distance(),
712                )
713            {
714                players.insert(player_id);
715            }
716        }
717
718        players
719    }
720
721    fn send_spawn_packets(&self, entity: &SharedEntity, player: &Player) {
722        let entity_id = entity.id();
723        self.spawn_pairing(entity, player.id())
724            .send_to(entity_id, player);
725    }
726
727    fn spawn_pairing(&self, entity: &SharedEntity, player_id: i32) -> EntitySpawnPairing {
728        EntitySpawnPairing::from_entity(
729            entity,
730            self.passenger_pairing_packets_for_player(entity.as_ref(), player_id),
731        )
732    }
733
734    fn passenger_pairing_packets_for_player(
735        &self,
736        entity: &dyn Entity,
737        player_id: i32,
738    ) -> Vec<CSetPassengers> {
739        let mut packets = Vec::new();
740        let passenger_ids = self.direct_passenger_ids_seen_by_player(entity, player_id);
741        if !passenger_ids.is_empty() {
742            packets.push(CSetPassengers::new(entity.id(), passenger_ids));
743        }
744
745        if let Some(vehicle) = entity.vehicle()
746            && self.entity_seen_by_player(vehicle.id(), player_id)
747        {
748            packets.push(CSetPassengers::new(
749                vehicle.id(),
750                self.direct_passenger_ids_seen_by_player(vehicle.as_ref(), player_id),
751            ));
752        }
753
754        packets
755    }
756
757    fn vehicle_passenger_packet_for_player(
758        &self,
759        entity: &dyn Entity,
760        player_id: i32,
761    ) -> Option<CSetPassengers> {
762        let vehicle = entity.vehicle()?;
763        if !self.entity_seen_by_player(vehicle.id(), player_id) {
764            return None;
765        }
766        Some(CSetPassengers::new(
767            vehicle.id(),
768            self.direct_passenger_ids_seen_by_player(vehicle.as_ref(), player_id),
769        ))
770    }
771
772    fn direct_tracked_passenger_ids(&self, entity: &dyn Entity) -> Vec<i32> {
773        entity
774            .passengers()
775            .into_iter()
776            .filter(|passenger| self.is_entity_tracked(passenger.id()))
777            .map(|passenger| passenger.id())
778            .collect()
779    }
780
781    fn direct_passenger_ids_seen_by_player(&self, entity: &dyn Entity, player_id: i32) -> Vec<i32> {
782        entity
783            .passengers()
784            .into_iter()
785            .filter(|passenger| {
786                passenger.id() == player_id || self.entity_seen_by_player(passenger.id(), player_id)
787            })
788            .map(|passenger| passenger.id())
789            .collect()
790    }
791
792    fn is_entity_tracked(&self, entity_id: i32) -> bool {
793        self.entities.read_sync(&entity_id, |_, _| ()).is_some()
794    }
795
796    fn entity_seen_by_player(&self, entity_id: i32, player_id: i32) -> bool {
797        self.entities
798            .read_sync(&entity_id, |_, tracked| {
799                tracked.seen_by.read().contains(&player_id)
800            })
801            .unwrap_or(false)
802    }
803}
804
805fn leash_holder_id(entity: &dyn Entity) -> Option<i32> {
806    entity
807        .as_leashable()
808        .and_then(Leashable::leash_holder)
809        .map(|holder| holder.id())
810}
811
812fn is_within_tracking_distance(
813    entity_pos: DVec3,
814    player_pos: DVec3,
815    tracking_range: EntityTrackingRange,
816    player_view_distance: u8,
817) -> bool {
818    let visible_radius = tracking_range.visible_radius(player_view_distance);
819    let x = player_pos.x - entity_pos.x;
820    let z = player_pos.z - entity_pos.z;
821    x * x + z * z <= visible_radius * visible_radius
822}
823
824fn effective_tracking_range(
825    entity: &dyn Entity,
826    base_range: EntityTrackingRange,
827) -> EntityTrackingRange {
828    let mut range = base_range;
829    let mut visited = FxHashSet::default();
830    visited.insert(entity.id());
831    add_passenger_tracking_ranges(entity, &mut range, &mut visited);
832    range
833}
834
835fn add_passenger_tracking_ranges(
836    entity: &dyn Entity,
837    range: &mut EntityTrackingRange,
838    visited: &mut FxHashSet<i32>,
839) {
840    for passenger in entity.passengers() {
841        if !visited.insert(passenger.id()) {
842            continue;
843        }
844        let passenger_range = EntityTrackingRange::from_client_chunk_range(
845            passenger.entity_type().client_tracking_range,
846        );
847        range.block_radius = range.block_radius.max(passenger_range.block_radius);
848        add_passenger_tracking_ranges(passenger.as_ref(), range, visited);
849    }
850}
851
852struct EntitySpawnPairing {
853    spawn_packet: CAddEntity,
854    entity_data: Vec<DataValue>,
855    attributes: Vec<AttributeSnapshot>,
856    equipment: Vec<EquipmentSlotItem>,
857    passenger_packets: Vec<CSetPassengers>,
858    entity_link_packet: Option<CSetEntityLink>,
859}
860
861impl EntitySpawnPairing {
862    fn from_entity(entity: &SharedEntity, passenger_packets: Vec<CSetPassengers>) -> Self {
863        entity.update_data_before_sync();
864
865        let pos = entity.spawn_position();
866        let vel = entity.velocity();
867        let (yaw, pitch) = entity.rotation();
868        let head_yaw = entity.head_yaw();
869        let entity_type_id = entity.entity_type().id() as i32;
870
871        // Convert rotation from degrees to protocol byte format (256th of a full rotation)
872        // Uses to_angle_byte which matches vanilla's Mth.packDegrees
873        let x_rot = to_angle_byte(pitch);
874        let y_rot = to_angle_byte(yaw);
875        let head_y_rot = to_angle_byte(head_yaw);
876
877        let (attributes, equipment) = entity.as_living_entity().map_or_else(
878            || (Vec::new(), Vec::new()),
879            |living| {
880                (
881                    living.pack_syncable_attributes(),
882                    living.pack_all_equipment(),
883                )
884            },
885        );
886
887        Self {
888            spawn_packet: CAddEntity {
889                id: entity.id(),
890                uuid: entity.uuid(),
891                entity_type: entity_type_id,
892                position: pos,
893                velocity: vel,
894                x_rot,
895                y_rot,
896                head_y_rot,
897                data: entity.spawn_data(),
898            },
899            entity_data: entity.pack_all_entity_data(),
900            attributes,
901            equipment,
902            passenger_packets,
903            entity_link_packet: leash_holder_id(entity.as_ref())
904                .map(|holder_id| CSetEntityLink::new(entity.id(), holder_id)),
905        }
906    }
907
908    fn send_to(self, entity_id: i32, player: &Player) {
909        player.send_bundle(|bundle| {
910            bundle.add(self.spawn_packet);
911
912            if !self.entity_data.is_empty() {
913                bundle.add(CSetEntityData::new(entity_id, self.entity_data));
914            }
915
916            if !self.attributes.is_empty() {
917                bundle.add(CUpdateAttributes::new(entity_id, self.attributes));
918            }
919
920            if !self.equipment.is_empty() {
921                bundle.add(CSetEquipment::new(entity_id, self.equipment));
922            }
923
924            for packet in self.passenger_packets {
925                bundle.add(packet);
926            }
927
928            if let Some(packet) = self.entity_link_packet {
929                bundle.add(packet);
930            }
931        });
932    }
933}
934
935fn direct_player_passenger_delta(
936    old_passenger_ids: &[i32],
937    new_passenger_ids: &[i32],
938    get_player: &impl Fn(i32) -> Option<Arc<Player>>,
939) -> Vec<i32> {
940    let old_passenger_ids = old_passenger_ids.iter().copied().collect::<FxHashSet<_>>();
941    let new_passenger_ids = new_passenger_ids.iter().copied().collect::<FxHashSet<_>>();
942    old_passenger_ids
943        .symmetric_difference(&new_passenger_ids)
944        .copied()
945        .filter(|entity_id| get_player(*entity_id).is_some())
946        .collect()
947}
948
949#[cfg(test)]
950mod tests;