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::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
31pub struct EntityTracker {
33 entities: scc::HashMap<i32, TrackedEntity>,
35}
36
37pub struct EntityChangeSenders<
39 Movement,
40 SelfMovement,
41 EntityData,
42 Attributes,
43 MobEffects,
44 Equipment,
45 Passengers,
46 EntityLink,
47> {
48 pub movement: Movement,
50 pub self_movement: SelfMovement,
52 pub entity_data: EntityData,
54 pub attributes: Attributes,
56 pub mob_effects: MobEffects,
58 pub equipment: Equipment,
60 pub passengers: Passengers,
62 pub entity_link: EntityLink,
64}
65
66struct TrackedEntity {
68 entity: WeakEntity,
70 server_entity: SyncMutex<ServerEntityMovementSyncState>,
72 last_passenger_ids: SyncMutex<Vec<i32>>,
74 last_leash_holder_id: SyncMutex<Option<i32>>,
76 tracking_range: EntityTrackingRange,
78 registered_chunk: ChunkPos,
80 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 #[must_use]
115 pub fn new() -> Self {
116 Self {
117 entities: scc::HashMap::new(),
118 }
119 }
120
121 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 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 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 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 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 for entity_id in dead_entities {
287 self.remove_dead_entity(entity_id);
288 }
289 }
290
291 #[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 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 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 #[must_use]
662 pub fn count(&self) -> usize {
663 self.entities.len()
664 }
665
666 #[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 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 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;