1use 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
30pub struct EntityTracker {
32 entities: scc::HashMap<i32, TrackedEntity>,
34}
35
36pub struct EntityChangeSenders<
38 Movement,
39 SelfMovement,
40 EntityData,
41 Attributes,
42 MobEffects,
43 Equipment,
44 Passengers,
45 EntityLink,
46> {
47 pub movement: Movement,
49 pub self_movement: SelfMovement,
51 pub entity_data: EntityData,
53 pub attributes: Attributes,
55 pub mob_effects: MobEffects,
57 pub equipment: Equipment,
59 pub passengers: Passengers,
61 pub entity_link: EntityLink,
63}
64
65struct TrackedEntity {
67 entity: WeakEntity,
69 server_entity: SyncMutex<ServerEntityMovementSyncState>,
71 last_passenger_ids: SyncMutex<Vec<i32>>,
73 last_leash_holder_id: SyncMutex<Option<i32>>,
75 tracking_range: EntityTrackingRange,
77 registered_chunk: ChunkPos,
79 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 #[must_use]
114 pub fn new() -> Self {
115 Self {
116 entities: scc::HashMap::new(),
117 }
118 }
119
120 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 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 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 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 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 for entity_id in dead_entities {
286 self.remove_dead_entity(entity_id);
287 }
288 }
289
290 #[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 pub fn on_player_leave(&self, player_id: i32) {
517 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 });
528
529 for entity_id in dead_entities {
531 self.remove_dead_entity(entity_id);
532 }
533 }
534
535 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 #[must_use]
653 pub fn count(&self) -> usize {
654 self.entities.len()
655 }
656
657 #[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 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 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;