1use glam::DVec3;
4use steel_protocol::packets::game::{
5 CEntityPositionSync, CMoveEntityPos, CMoveEntityPosRot, CMoveEntityRot, CRotateHead,
6 CSetEntityMotion, PackedEntityDelta, calc_delta, to_angle_byte,
7};
8
9pub const POSITION_SYNC_THRESHOLD: f64 = 7.629_394_5e-6;
11pub const VELOCITY_SYNC_THRESHOLD: f64 = 1.0e-7;
13pub const FORCED_POS_UPDATE_PERIOD: i32 = 60;
15pub const FORCED_TELEPORT_PERIOD: i32 = 400;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct PackedEntityRotation {
21 yaw: i8,
22 pitch: i8,
23}
24
25impl PackedEntityRotation {
26 #[must_use]
28 pub const fn from_degrees(rotation: (f32, f32)) -> Self {
29 Self {
30 yaw: to_angle_byte(rotation.0),
31 pitch: to_angle_byte(rotation.1),
32 }
33 }
34
35 #[must_use]
37 pub const fn yaw(self) -> i8 {
38 self.yaw
39 }
40
41 #[must_use]
43 pub const fn pitch(self) -> i8 {
44 self.pitch
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct EntityRotationSyncState {
51 last_body_rotation: PackedEntityRotation,
52 last_head_yaw: i8,
53}
54
55impl EntityRotationSyncState {
56 #[must_use]
58 pub const fn new(body_rotation: (f32, f32), head_yaw: f32) -> Self {
59 Self {
60 last_body_rotation: PackedEntityRotation::from_degrees(body_rotation),
61 last_head_yaw: to_angle_byte(head_yaw),
62 }
63 }
64
65 pub fn record_body_rotation(&mut self, rotation: (f32, f32)) -> Option<PackedEntityRotation> {
67 let packed = PackedEntityRotation::from_degrees(rotation);
68 if packed == self.last_body_rotation {
69 return None;
70 }
71
72 self.last_body_rotation = packed;
73 Some(packed)
74 }
75
76 #[must_use]
78 pub fn body_rotation_changed(self, rotation: (f32, f32)) -> bool {
79 PackedEntityRotation::from_degrees(rotation) != self.last_body_rotation
80 }
81
82 pub const fn mark_body_rotation_sent(&mut self, rotation: (f32, f32)) {
84 self.last_body_rotation = PackedEntityRotation::from_degrees(rotation);
85 }
86
87 pub const fn record_head_yaw(&mut self, head_yaw: f32) -> Option<i8> {
89 let packed = to_angle_byte(head_yaw);
90 if packed == self.last_head_yaw {
91 return None;
92 }
93
94 self.last_head_yaw = packed;
95 Some(packed)
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum EntityPositionSyncDecision {
102 Delta {
104 dx: PackedEntityDelta,
106 dy: PackedEntityDelta,
108 dz: PackedEntityDelta,
110 },
111 Full,
113}
114
115#[derive(Clone, Debug)]
117pub enum EntityPositionSyncPacket {
118 Delta(CMoveEntityPos),
120 Full(CEntityPositionSync),
122}
123
124#[derive(Clone, Debug)]
126pub enum EntityPositionRotSyncPacket {
127 Delta(CMoveEntityPosRot),
129 Full(CEntityPositionSync),
131}
132
133#[derive(Clone, Debug)]
135pub enum EntityMovementSyncPacket {
136 Position(CMoveEntityPos),
138 PositionRotation(CMoveEntityPosRot),
140 Rotation(CMoveEntityRot),
142 HeadRotation(CRotateHead),
144 PositionSync(CEntityPositionSync),
146 Velocity(CSetEntityMotion),
148}
149
150impl From<EntityPositionSyncPacket> for EntityMovementSyncPacket {
151 fn from(packet: EntityPositionSyncPacket) -> Self {
152 match packet {
153 EntityPositionSyncPacket::Delta(packet) => Self::Position(packet),
154 EntityPositionSyncPacket::Full(packet) => Self::PositionSync(packet),
155 }
156 }
157}
158
159impl From<EntityPositionRotSyncPacket> for EntityMovementSyncPacket {
160 fn from(packet: EntityPositionRotSyncPacket) -> Self {
161 match packet {
162 EntityPositionRotSyncPacket::Delta(packet) => Self::PositionRotation(packet),
163 EntityPositionRotSyncPacket::Full(packet) => Self::PositionSync(packet),
164 }
165 }
166}
167
168impl From<CMoveEntityRot> for EntityMovementSyncPacket {
169 fn from(packet: CMoveEntityRot) -> Self {
170 Self::Rotation(packet)
171 }
172}
173
174impl From<CRotateHead> for EntityMovementSyncPacket {
175 fn from(packet: CRotateHead) -> Self {
176 Self::HeadRotation(packet)
177 }
178}
179
180impl From<CSetEntityMotion> for EntityMovementSyncPacket {
181 fn from(packet: CSetEntityMotion) -> Self {
182 Self::Velocity(packet)
183 }
184}
185
186#[derive(Clone, Debug, Default)]
189pub struct EntityMovementSyncPackets {
190 primary: Option<EntityMovementSyncPacket>,
191 head_rotation: Option<EntityMovementSyncPacket>,
192}
193
194impl EntityMovementSyncPackets {
195 fn new(primary: Option<EntityMovementSyncPacket>, head_rotation: Option<CRotateHead>) -> Self {
197 Self {
198 primary,
199 head_rotation: head_rotation.map(EntityMovementSyncPacket::from),
200 }
201 }
202
203 #[must_use]
205 pub const fn is_empty(&self) -> bool {
206 self.primary.is_none() && self.head_rotation.is_none()
207 }
208
209 pub fn for_each(self, mut send: impl FnMut(EntityMovementSyncPacket)) {
211 if let Some(packet) = self.primary {
212 send(packet);
213 }
214 if let Some(packet) = self.head_rotation {
215 send(packet);
216 }
217 }
218}
219
220#[derive(Debug, Clone, Copy, PartialEq)]
222pub struct EntityPositionSyncSnapshot {
223 entity_id: i32,
224 position: DVec3,
225 velocity: DVec3,
226 rotation: (f32, f32),
227 on_ground: bool,
228}
229
230impl EntityPositionSyncSnapshot {
231 #[must_use]
233 pub const fn new(
234 entity_id: i32,
235 position: DVec3,
236 velocity: DVec3,
237 rotation: (f32, f32),
238 on_ground: bool,
239 ) -> Self {
240 Self {
241 entity_id,
242 position,
243 velocity,
244 rotation,
245 on_ground,
246 }
247 }
248
249 const fn full_sync_packet(self) -> CEntityPositionSync {
250 CEntityPositionSync {
251 entity_id: self.entity_id,
252 pos: self.position,
253 vel: self.velocity,
254 yaw: self.rotation.0,
255 pitch: self.rotation.1,
256 on_ground: self.on_ground,
257 }
258 }
259}
260
261impl EntityPositionSyncDecision {
262 #[must_use]
264 pub const fn into_position_packet(
265 self,
266 snapshot: EntityPositionSyncSnapshot,
267 ) -> EntityPositionSyncPacket {
268 match self {
269 Self::Delta { dx, dy, dz } => EntityPositionSyncPacket::Delta(CMoveEntityPos {
270 entity_id: snapshot.entity_id,
271 dx,
272 dy,
273 dz,
274 on_ground: snapshot.on_ground,
275 }),
276 Self::Full => EntityPositionSyncPacket::Full(snapshot.full_sync_packet()),
277 }
278 }
279
280 #[must_use]
282 pub const fn into_position_rot_packet(
283 self,
284 snapshot: EntityPositionSyncSnapshot,
285 ) -> EntityPositionRotSyncPacket {
286 match self {
287 Self::Delta { dx, dy, dz } => EntityPositionRotSyncPacket::Delta(CMoveEntityPosRot {
288 entity_id: snapshot.entity_id,
289 dx,
290 dy,
291 dz,
292 y_rot: to_angle_byte(snapshot.rotation.0),
293 x_rot: to_angle_byte(snapshot.rotation.1),
294 on_ground: snapshot.on_ground,
295 }),
296 Self::Full => EntityPositionRotSyncPacket::Full(snapshot.full_sync_packet()),
297 }
298 }
299}
300
301#[derive(Debug, Clone, Copy, PartialEq)]
303pub struct EntityPositionSyncState {
304 last_sent_position: DVec3,
305 last_sent_on_ground: bool,
306 sync_delay: i32,
307}
308
309impl EntityPositionSyncState {
310 #[must_use]
312 pub const fn new(position: DVec3, on_ground: bool) -> Self {
313 Self {
314 last_sent_position: position,
315 last_sent_on_ground: on_ground,
316 sync_delay: 0,
317 }
318 }
319
320 #[must_use]
322 pub const fn last_sent_position(self) -> DVec3 {
323 self.last_sent_position
324 }
325
326 #[must_use]
328 pub const fn last_sent_on_ground(self) -> bool {
329 self.last_sent_on_ground
330 }
331
332 #[must_use]
334 pub const fn sync_delay(self) -> i32 {
335 self.sync_delay
336 }
337
338 pub const fn advance_sync_delay(&mut self) -> i32 {
340 let delay = self.sync_delay;
341 self.sync_delay += 1;
342 delay
343 }
344
345 #[must_use]
347 pub fn position_changed(self, current_position: DVec3) -> bool {
348 let diff = current_position - self.last_sent_position;
349 diff.length_squared() >= POSITION_SYNC_THRESHOLD
350 }
351
352 #[must_use]
357 pub fn packed_delta(
358 self,
359 current_position: DVec3,
360 ) -> Option<(PackedEntityDelta, PackedEntityDelta, PackedEntityDelta)> {
361 let dx = calc_delta(current_position.x, self.last_sent_position.x)?;
362 let dy = calc_delta(current_position.y, self.last_sent_position.y)?;
363 let dz = calc_delta(current_position.z, self.last_sent_position.z)?;
364 Some((dx, dy, dz))
365 }
366
367 pub const fn mark_delta_sent(&mut self, position: DVec3, on_ground: bool) {
369 self.last_sent_position = position;
370 self.last_sent_on_ground = on_ground;
371 }
372
373 pub const fn mark_full_sent(&mut self, position: DVec3, on_ground: bool) {
375 self.last_sent_position = position;
376 self.last_sent_on_ground = on_ground;
377 self.sync_delay = 0;
378 }
379
380 pub const fn reset_base_without_packet(&mut self, position: DVec3, on_ground: bool) {
382 self.last_sent_position = position;
383 self.last_sent_on_ground = on_ground;
384 }
385
386 pub fn record_movement_sync(
392 &mut self,
393 position: DVec3,
394 on_ground: bool,
395 force_full: bool,
396 ) -> EntityPositionSyncDecision {
397 if !force_full && let Some((dx, dy, dz)) = self.packed_delta(position) {
398 self.mark_delta_sent(position, on_ground);
399 return EntityPositionSyncDecision::Delta { dx, dy, dz };
400 }
401
402 self.mark_full_sent(position, on_ground);
403 EntityPositionSyncDecision::Full
404 }
405}
406
407#[derive(Debug, Clone, Copy, PartialEq)]
409pub struct EntityVelocitySyncState {
410 last_sent_velocity: DVec3,
411}
412
413impl EntityVelocitySyncState {
414 #[must_use]
416 pub const fn new(velocity: DVec3) -> Self {
417 Self {
418 last_sent_velocity: velocity,
419 }
420 }
421
422 #[must_use]
424 pub const fn last_sent_velocity(self) -> DVec3 {
425 self.last_sent_velocity
426 }
427
428 pub fn record_velocity_sync(
430 &mut self,
431 entity_id: i32,
432 current_velocity: DVec3,
433 ) -> Option<CSetEntityMotion> {
434 let diff = current_velocity - self.last_sent_velocity;
435 let diff_sq = diff.length_squared();
436 let became_stationary = diff_sq > 0.0 && current_velocity == DVec3::ZERO;
437 if diff_sq <= VELOCITY_SYNC_THRESHOLD && !became_stationary {
438 return None;
439 }
440
441 self.last_sent_velocity = current_velocity;
442 Some(CSetEntityMotion::new(entity_id, current_velocity))
443 }
444}
445
446#[derive(Debug, Clone, Copy, PartialEq)]
448pub struct EntityMovementSyncState {
449 position: EntityPositionSyncState,
450 rotation: EntityRotationSyncState,
451}
452
453#[derive(Debug, Clone, Copy, PartialEq)]
455pub struct EntityMovementSyncUpdate {
456 pub entity_id: i32,
458 pub has_position: bool,
460 pub has_rotation: bool,
462 pub position: DVec3,
464 pub velocity: DVec3,
466 pub body_rotation: (f32, f32),
468 pub head_yaw: f32,
470 pub on_ground: bool,
472}
473
474#[derive(Debug, Clone, Copy, PartialEq)]
476pub struct ServerEntityMovementSyncState {
477 position: EntityPositionSyncState,
478 rotation: EntityRotationSyncState,
479 velocity: EntityVelocitySyncState,
480 update_interval: i32,
481 track_delta: bool,
482 tick_count: i32,
483 teleport_delay: i32,
484 was_riding: bool,
485 was_on_ground: bool,
486}
487
488#[derive(Debug, Clone, Copy, PartialEq)]
490pub struct ServerEntityMovementSyncUpdate {
491 pub entity_id: i32,
493 pub is_passenger: bool,
495 pub position: DVec3,
497 pub velocity: DVec3,
499 pub body_rotation: (f32, f32),
501 pub head_yaw: f32,
503 pub on_ground: bool,
505 pub needs_velocity_sync: bool,
507 pub has_dirty_entity_data: bool,
509 pub force_velocity_sync: bool,
511}
512
513#[derive(Clone, Debug, Default)]
515pub struct ServerEntityMovementSyncResult {
516 packets: Vec<EntityMovementSyncPacket>,
517 clear_velocity_sync: bool,
518}
519
520impl ServerEntityMovementSyncResult {
521 #[must_use]
523 pub const fn should_clear_velocity_sync(&self) -> bool {
524 self.clear_velocity_sync
525 }
526
527 pub fn for_each_packet(self, send: impl FnMut(EntityMovementSyncPacket)) {
529 self.packets.into_iter().for_each(send);
530 }
531}
532
533impl ServerEntityMovementSyncState {
534 #[must_use]
536 pub const fn new(
537 position: DVec3,
538 velocity: DVec3,
539 on_ground: bool,
540 body_rotation: (f32, f32),
541 head_yaw: f32,
542 update_interval: i32,
543 track_delta: bool,
544 ) -> Self {
545 Self {
546 position: EntityPositionSyncState::new(position, on_ground),
547 rotation: EntityRotationSyncState::new(body_rotation, head_yaw),
548 velocity: EntityVelocitySyncState::new(velocity),
549 update_interval,
550 track_delta,
551 tick_count: 0,
552 teleport_delay: 0,
553 was_riding: false,
554 was_on_ground: on_ground,
555 }
556 }
557
558 #[must_use]
560 pub fn record_send_changes(
561 &mut self,
562 update: ServerEntityMovementSyncUpdate,
563 ) -> ServerEntityMovementSyncResult {
564 let mut result = ServerEntityMovementSyncResult::default();
565 let should_process =
566 self.should_process(update.needs_velocity_sync, update.has_dirty_entity_data);
567 if should_process {
568 result.clear_velocity_sync = update.needs_velocity_sync;
569 let should_send_rotation = self.rotation.body_rotation_changed(update.body_rotation);
570
571 if update.is_passenger {
572 self.record_passenger_update(update, should_send_rotation, &mut result);
573 } else {
574 self.record_non_passenger_update(update, should_send_rotation, &mut result);
575 }
576
577 if let Some(head_y_rot) = self.rotation.record_head_yaw(update.head_yaw) {
578 result
579 .packets
580 .push(EntityMovementSyncPacket::from(CRotateHead {
581 entity_id: update.entity_id,
582 head_y_rot,
583 }));
584 }
585 }
586
587 self.tick_count = self.tick_count.wrapping_add(1);
588 result
589 }
590
591 const fn should_process(self, needs_velocity_sync: bool, has_dirty_entity_data: bool) -> bool {
592 needs_velocity_sync
593 || has_dirty_entity_data
594 || (self.update_interval > 0 && self.tick_count % self.update_interval == 0)
595 }
596
597 fn record_passenger_update(
598 &mut self,
599 update: ServerEntityMovementSyncUpdate,
600 should_send_rotation: bool,
601 result: &mut ServerEntityMovementSyncResult,
602 ) {
603 if should_send_rotation
604 && let Some(body_rotation) = self.rotation.record_body_rotation(update.body_rotation)
605 {
606 result
607 .packets
608 .push(EntityMovementSyncPacket::from(CMoveEntityRot {
609 entity_id: update.entity_id,
610 y_rot: body_rotation.yaw(),
611 x_rot: body_rotation.pitch(),
612 on_ground: update.on_ground,
613 }));
614 }
615
616 self.position
617 .reset_base_without_packet(update.position, update.on_ground);
618 self.was_riding = true;
619 }
620
621 fn record_non_passenger_update(
622 &mut self,
623 update: ServerEntityMovementSyncUpdate,
624 should_send_rotation: bool,
625 result: &mut ServerEntityMovementSyncResult,
626 ) {
627 self.teleport_delay = self.teleport_delay.wrapping_add(1);
628
629 let position_changed = self.position.position_changed(update.position);
630 let should_send_position =
631 position_changed || self.tick_count % FORCED_POS_UPDATE_PERIOD == 0;
632 let delta_too_big = self.position.packed_delta(update.position).is_none();
633 let force_full = delta_too_big
634 || self.teleport_delay > FORCED_TELEPORT_PERIOD
635 || self.was_riding
636 || self.was_on_ground != update.on_ground;
637
638 if (update.needs_velocity_sync || self.track_delta || update.force_velocity_sync)
639 && let Some(packet) = self
640 .velocity
641 .record_velocity_sync(update.entity_id, update.velocity)
642 {
643 result.packets.push(EntityMovementSyncPacket::from(packet));
644 }
645
646 if force_full {
647 self.was_on_ground = update.on_ground;
648 self.teleport_delay = 0;
649 let decision =
650 self.position
651 .record_movement_sync(update.position, update.on_ground, true);
652 self.rotation.mark_body_rotation_sent(update.body_rotation);
653 result.packets.push(EntityMovementSyncPacket::from(
654 decision.into_position_rot_packet(EntityPositionSyncSnapshot::new(
655 update.entity_id,
656 update.position,
657 update.velocity,
658 update.body_rotation,
659 update.on_ground,
660 )),
661 ));
662 } else if should_send_position && should_send_rotation {
663 let decision =
664 self.position
665 .record_movement_sync(update.position, update.on_ground, false);
666 self.rotation.mark_body_rotation_sent(update.body_rotation);
667 result.packets.push(EntityMovementSyncPacket::from(
668 decision.into_position_rot_packet(EntityPositionSyncSnapshot::new(
669 update.entity_id,
670 update.position,
671 update.velocity,
672 update.body_rotation,
673 update.on_ground,
674 )),
675 ));
676 } else if should_send_position {
677 let decision =
678 self.position
679 .record_movement_sync(update.position, update.on_ground, false);
680 result.packets.push(EntityMovementSyncPacket::from(
681 decision.into_position_packet(EntityPositionSyncSnapshot::new(
682 update.entity_id,
683 update.position,
684 update.velocity,
685 update.body_rotation,
686 update.on_ground,
687 )),
688 ));
689 } else if should_send_rotation
690 && let Some(body_rotation) = self.rotation.record_body_rotation(update.body_rotation)
691 {
692 result
693 .packets
694 .push(EntityMovementSyncPacket::from(CMoveEntityRot {
695 entity_id: update.entity_id,
696 y_rot: body_rotation.yaw(),
697 x_rot: body_rotation.pitch(),
698 on_ground: update.on_ground,
699 }));
700 }
701
702 self.was_riding = false;
703 }
704}
705
706impl EntityMovementSyncState {
707 #[must_use]
709 pub const fn new(
710 position: DVec3,
711 on_ground: bool,
712 body_rotation: (f32, f32),
713 head_yaw: f32,
714 ) -> Self {
715 Self {
716 position: EntityPositionSyncState::new(position, on_ground),
717 rotation: EntityRotationSyncState::new(body_rotation, head_yaw),
718 }
719 }
720
721 #[must_use]
723 pub const fn last_sent_position(self) -> DVec3 {
724 self.position.last_sent_position()
725 }
726
727 pub fn record_position_sync_with_full_delay(
732 &mut self,
733 position: DVec3,
734 on_ground: bool,
735 full_sync_delay: i32,
736 ) -> EntityPositionSyncDecision {
737 let delay = self.position.advance_sync_delay();
738 let on_ground_changed = self.position.last_sent_on_ground() != on_ground;
739 let force_full = delay > full_sync_delay || on_ground_changed;
740 self.position
741 .record_movement_sync(position, on_ground, force_full)
742 }
743
744 pub fn record_update_with_full_delay(
746 &mut self,
747 update: EntityMovementSyncUpdate,
748 full_sync_delay: i32,
749 ) -> EntityMovementSyncPackets {
750 let head_rotation = if update.has_rotation {
751 self.record_head_yaw(update.head_yaw)
752 .map(|head_y_rot| CRotateHead {
753 entity_id: update.entity_id,
754 head_y_rot,
755 })
756 } else {
757 None
758 };
759
760 let primary = if update.has_position {
761 let decision = self.record_position_sync_with_full_delay(
762 update.position,
763 update.on_ground,
764 full_sync_delay,
765 );
766 let position_includes_rotation = matches!(decision, EntityPositionSyncDecision::Full);
767 let body_rotation = if position_includes_rotation {
768 self.mark_body_rotation_sent(update.body_rotation);
769 None
770 } else if update.has_rotation {
771 self.record_body_rotation(update.body_rotation)
772 } else {
773 None
774 };
775 let snapshot = EntityPositionSyncSnapshot::new(
776 update.entity_id,
777 update.position,
778 update.velocity,
779 update.body_rotation,
780 update.on_ground,
781 );
782
783 if position_includes_rotation || body_rotation.is_some() {
784 Some(EntityMovementSyncPacket::from(
785 decision.into_position_rot_packet(snapshot),
786 ))
787 } else {
788 Some(EntityMovementSyncPacket::from(
789 decision.into_position_packet(snapshot),
790 ))
791 }
792 } else if update.has_rotation {
793 self.record_body_rotation(update.body_rotation)
794 .map(|body_rotation| {
795 EntityMovementSyncPacket::from(CMoveEntityRot {
796 entity_id: update.entity_id,
797 y_rot: body_rotation.yaw(),
798 x_rot: body_rotation.pitch(),
799 on_ground: update.on_ground,
800 })
801 })
802 } else {
803 None
804 };
805
806 EntityMovementSyncPackets::new(primary, head_rotation)
807 }
808
809 pub fn record_body_rotation(&mut self, rotation: (f32, f32)) -> Option<PackedEntityRotation> {
811 self.rotation.record_body_rotation(rotation)
812 }
813
814 pub const fn mark_body_rotation_sent(&mut self, rotation: (f32, f32)) {
816 self.rotation.mark_body_rotation_sent(rotation);
817 }
818
819 pub const fn record_head_yaw(&mut self, head_yaw: f32) -> Option<i8> {
821 self.rotation.record_head_yaw(head_yaw)
822 }
823}
824
825#[cfg(test)]
826mod tests {
827 use glam::DVec3;
828 use steel_protocol::packets::game::{calc_delta, to_angle_byte};
829
830 use super::{
831 EntityMovementSyncPacket, EntityMovementSyncState, EntityMovementSyncUpdate,
832 EntityPositionRotSyncPacket, EntityPositionSyncDecision, EntityPositionSyncPacket,
833 EntityPositionSyncSnapshot, EntityPositionSyncState, EntityRotationSyncState,
834 EntityVelocitySyncState, PackedEntityRotation, ServerEntityMovementSyncState,
835 ServerEntityMovementSyncUpdate,
836 };
837
838 #[test]
839 fn movement_sync_records_delta_when_packed_delta_fits() {
840 let mut state = EntityPositionSyncState::new(DVec3::ZERO, false);
841 state.advance_sync_delay();
842
843 let position = DVec3::new(0.25, -0.125, 0.5);
844 let decision = state.record_movement_sync(position, true, false);
845
846 assert_eq!(
847 decision,
848 EntityPositionSyncDecision::Delta {
849 dx: calc_delta(position.x, 0.0).expect("delta should fit"),
850 dy: calc_delta(position.y, 0.0).expect("delta should fit"),
851 dz: calc_delta(position.z, 0.0).expect("delta should fit"),
852 }
853 );
854 assert_eq!(state.last_sent_position(), position);
855 assert!(state.last_sent_on_ground());
856 assert_eq!(state.sync_delay(), 1);
857 }
858
859 #[test]
860 fn movement_sync_records_full_when_forced() {
861 let mut state = EntityPositionSyncState::new(DVec3::ZERO, false);
862 state.advance_sync_delay();
863
864 let decision = state.record_movement_sync(DVec3::new(0.25, 0.0, 0.0), true, true);
865
866 assert_eq!(decision, EntityPositionSyncDecision::Full);
867 assert_eq!(state.last_sent_position(), DVec3::new(0.25, 0.0, 0.0));
868 assert!(state.last_sent_on_ground());
869 assert_eq!(state.sync_delay(), 0);
870 }
871
872 #[test]
873 fn movement_sync_records_full_when_delta_overflows() {
874 let mut state = EntityPositionSyncState::new(DVec3::ZERO, false);
875
876 let decision = state.record_movement_sync(DVec3::new(10.0, 0.0, 0.0), false, false);
877
878 assert_eq!(decision, EntityPositionSyncDecision::Full);
879 assert_eq!(state.last_sent_position(), DVec3::new(10.0, 0.0, 0.0));
880 }
881
882 #[test]
883 fn rotation_sync_records_body_rotation_only_when_packed_angle_changes() {
884 let mut state = EntityRotationSyncState::new((0.0, 0.0), 0.0);
885
886 assert_eq!(state.record_body_rotation((0.5, 0.5)), None);
887 assert_eq!(
888 state.record_body_rotation((2.0, 0.0)),
889 Some(PackedEntityRotation {
890 yaw: to_angle_byte(2.0),
891 pitch: to_angle_byte(0.0),
892 })
893 );
894 assert_eq!(state.record_body_rotation((2.5, 0.0)), None);
895 assert_eq!(
896 state.record_body_rotation((2.5, 2.0)),
897 Some(PackedEntityRotation {
898 yaw: to_angle_byte(2.5),
899 pitch: to_angle_byte(2.0),
900 })
901 );
902 }
903
904 #[test]
905 fn rotation_sync_records_head_rotation_only_when_packed_angle_changes() {
906 let mut state = EntityRotationSyncState::new((0.0, 0.0), 0.0);
907
908 assert_eq!(state.record_head_yaw(0.5), None);
909 assert_eq!(state.record_head_yaw(2.0), Some(to_angle_byte(2.0)));
910 assert_eq!(state.record_head_yaw(2.5), None);
911 }
912
913 #[test]
914 fn velocity_sync_records_packet_when_delta_exceeds_threshold() {
915 let mut state = EntityVelocitySyncState::new(DVec3::ZERO);
916
917 let packet = state
918 .record_velocity_sync(12, DVec3::new(0.001, 0.0, 0.0))
919 .expect("velocity should sync");
920
921 assert_eq!(packet.entity_id, 12);
922 assert_eq!(packet.vel.x.to_bits(), 0.001_f64.to_bits());
923 assert_eq!(packet.vel.y.to_bits(), 0.0_f64.to_bits());
924 assert_eq!(packet.vel.z.to_bits(), 0.0_f64.to_bits());
925 assert_eq!(state.last_sent_velocity(), DVec3::new(0.001, 0.0, 0.0));
926 }
927
928 #[test]
929 fn velocity_sync_skips_sub_threshold_non_zero_delta() {
930 let mut state = EntityVelocitySyncState::new(DVec3::ZERO);
931
932 assert!(
933 state
934 .record_velocity_sync(12, DVec3::new(0.000_1, 0.0, 0.0))
935 .is_none()
936 );
937 assert_eq!(state.last_sent_velocity(), DVec3::ZERO);
938 }
939
940 #[test]
941 fn velocity_sync_records_packet_when_entity_becomes_stationary() {
942 let mut state = EntityVelocitySyncState::new(DVec3::new(0.000_1, 0.0, 0.0));
943
944 let packet = state
945 .record_velocity_sync(12, DVec3::ZERO)
946 .expect("stationary transition should sync");
947
948 assert_eq!(packet.entity_id, 12);
949 assert_eq!(packet.vel.x.to_bits(), 0.0_f64.to_bits());
950 assert_eq!(packet.vel.y.to_bits(), 0.0_f64.to_bits());
951 assert_eq!(packet.vel.z.to_bits(), 0.0_f64.to_bits());
952 assert_eq!(state.last_sent_velocity(), DVec3::ZERO);
953 }
954
955 #[test]
956 fn movement_sync_state_tracks_position_and_rotation_together() {
957 let mut state = EntityMovementSyncState::new(DVec3::ZERO, false, (0.0, 0.0), 0.0);
958
959 let decision =
960 state.record_position_sync_with_full_delay(DVec3::new(0.25, 0.0, 0.0), true, 400);
961 assert_eq!(decision, EntityPositionSyncDecision::Full);
962 assert_eq!(state.last_sent_position(), DVec3::new(0.25, 0.0, 0.0));
963
964 assert_eq!(state.record_body_rotation((0.5, 0.5)), None);
965 assert_eq!(
966 state.record_body_rotation((2.0, 0.0)),
967 Some(PackedEntityRotation {
968 yaw: to_angle_byte(2.0),
969 pitch: to_angle_byte(0.0),
970 })
971 );
972 state.mark_body_rotation_sent((90.0, 45.0));
973 assert_eq!(state.record_body_rotation((90.0, 45.0)), None);
974 assert_eq!(state.record_head_yaw(2.0), Some(to_angle_byte(2.0)));
975 }
976
977 #[test]
978 fn movement_sync_update_emits_position_rotation_before_head_rotation() {
979 let mut state = EntityMovementSyncState::new(DVec3::ZERO, false, (0.0, 0.0), 0.0);
980 let position = DVec3::new(0.25, 0.0, 0.0);
981 let update = EntityMovementSyncUpdate {
982 entity_id: 12,
983 has_position: true,
984 has_rotation: true,
985 position,
986 velocity: DVec3::new(1.0, 2.0, 3.0),
987 body_rotation: (2.0, 0.0),
988 head_yaw: 2.0,
989 on_ground: false,
990 };
991
992 let packets = state.record_update_with_full_delay(update, 400);
993 let mut emitted = Vec::new();
994 packets.for_each(|packet| emitted.push(packet));
995
996 assert_eq!(emitted.len(), 2);
997 let EntityMovementSyncPacket::PositionRotation(packet) = &emitted[0] else {
998 panic!("expected position-rotation packet");
999 };
1000 assert_eq!(packet.entity_id, 12);
1001 assert_eq!(
1002 packet.dx,
1003 calc_delta(position.x, 0.0).expect("delta should fit")
1004 );
1005 assert_eq!(packet.y_rot, to_angle_byte(2.0));
1006 assert_eq!(packet.x_rot, to_angle_byte(0.0));
1007
1008 let EntityMovementSyncPacket::HeadRotation(packet) = &emitted[1] else {
1009 panic!("expected head-rotation packet");
1010 };
1011 assert_eq!(packet.entity_id, 12);
1012 assert_eq!(packet.head_y_rot, to_angle_byte(2.0));
1013 }
1014
1015 #[test]
1016 fn movement_sync_update_full_position_marks_body_and_head_rotation_sent() {
1017 let mut state = EntityMovementSyncState::new(DVec3::ZERO, false, (0.0, 0.0), 0.0);
1018 let full_update = EntityMovementSyncUpdate {
1019 entity_id: 12,
1020 has_position: true,
1021 has_rotation: true,
1022 position: DVec3::new(0.25, 0.0, 0.0),
1023 velocity: DVec3::ZERO,
1024 body_rotation: (90.0, 45.0),
1025 head_yaw: 90.0,
1026 on_ground: true,
1027 };
1028
1029 let packets = state.record_update_with_full_delay(full_update, 400);
1030 let mut emitted = Vec::new();
1031 packets.for_each(|packet| emitted.push(packet));
1032
1033 assert_eq!(emitted.len(), 2);
1034 assert!(matches!(
1035 emitted[0],
1036 EntityMovementSyncPacket::PositionSync(_)
1037 ));
1038 assert!(matches!(
1039 emitted[1],
1040 EntityMovementSyncPacket::HeadRotation(_)
1041 ));
1042
1043 let rotation_only_update = EntityMovementSyncUpdate {
1044 has_position: false,
1045 position: full_update.position,
1046 ..full_update
1047 };
1048 let packets = state.record_update_with_full_delay(rotation_only_update, 400);
1049
1050 assert!(packets.is_empty());
1051 }
1052
1053 fn collect_server_packets(
1054 state: &mut ServerEntityMovementSyncState,
1055 update: ServerEntityMovementSyncUpdate,
1056 ) -> Vec<EntityMovementSyncPacket> {
1057 let result = state.record_send_changes(update);
1058 let mut packets = Vec::new();
1059 result.for_each_packet(|packet| packets.push(packet));
1060 packets
1061 }
1062
1063 fn server_update(position: DVec3, velocity: DVec3) -> ServerEntityMovementSyncUpdate {
1064 ServerEntityMovementSyncUpdate {
1065 entity_id: 12,
1066 is_passenger: false,
1067 position,
1068 velocity,
1069 body_rotation: (0.0, 0.0),
1070 head_yaw: 0.0,
1071 on_ground: false,
1072 needs_velocity_sync: false,
1073 has_dirty_entity_data: false,
1074 force_velocity_sync: false,
1075 }
1076 }
1077
1078 #[test]
1079 fn server_entity_sync_sends_track_delta_velocity_before_position() {
1080 let mut state = ServerEntityMovementSyncState::new(
1081 DVec3::ZERO,
1082 DVec3::ZERO,
1083 false,
1084 (0.0, 0.0),
1085 0.0,
1086 20,
1087 true,
1088 );
1089 let packets = collect_server_packets(
1090 &mut state,
1091 server_update(DVec3::new(0.25, 0.0, 0.0), DVec3::new(0.001, 0.0, 0.0)),
1092 );
1093
1094 assert_eq!(packets.len(), 2);
1095 assert!(matches!(packets[0], EntityMovementSyncPacket::Velocity(_)));
1096 assert!(matches!(packets[1], EntityMovementSyncPacket::Position(_)));
1097 }
1098
1099 #[test]
1100 fn server_entity_sync_skips_velocity_for_non_track_delta_without_needs_sync() {
1101 let mut state = ServerEntityMovementSyncState::new(
1102 DVec3::ZERO,
1103 DVec3::ZERO,
1104 false,
1105 (0.0, 0.0),
1106 0.0,
1107 2,
1108 false,
1109 );
1110 let packets = collect_server_packets(
1111 &mut state,
1112 server_update(DVec3::new(0.25, 0.0, 0.0), DVec3::new(0.001, 0.0, 0.0)),
1113 );
1114
1115 assert_eq!(packets.len(), 1);
1116 assert!(matches!(packets[0], EntityMovementSyncPacket::Position(_)));
1117 }
1118
1119 #[test]
1120 fn server_entity_sync_processes_and_clears_explicit_needs_sync() {
1121 let mut state = ServerEntityMovementSyncState::new(
1122 DVec3::ZERO,
1123 DVec3::ZERO,
1124 false,
1125 (0.0, 0.0),
1126 0.0,
1127 20,
1128 false,
1129 );
1130 let mut update = server_update(DVec3::new(0.25, 0.0, 0.0), DVec3::new(0.001, 0.0, 0.0));
1131 update.needs_velocity_sync = true;
1132
1133 let result = state.record_send_changes(update);
1134 assert!(result.should_clear_velocity_sync());
1135 let mut packets = Vec::new();
1136 result.for_each_packet(|packet| packets.push(packet));
1137
1138 assert_eq!(packets.len(), 2);
1139 assert!(matches!(packets[0], EntityMovementSyncPacket::Velocity(_)));
1140 assert!(matches!(packets[1], EntityMovementSyncPacket::Position(_)));
1141 }
1142
1143 #[test]
1144 fn server_entity_sync_processes_dirty_data_gate_between_intervals() {
1145 let mut state = ServerEntityMovementSyncState::new(
1146 DVec3::ZERO,
1147 DVec3::ZERO,
1148 false,
1149 (0.0, 0.0),
1150 0.0,
1151 20,
1152 false,
1153 );
1154 let first_packets =
1155 collect_server_packets(&mut state, server_update(DVec3::ZERO, DVec3::ZERO));
1156 assert_eq!(first_packets.len(), 1);
1157
1158 let mut update = server_update(DVec3::ZERO, DVec3::ZERO);
1159 update.body_rotation = (2.0, 0.0);
1160 update.has_dirty_entity_data = true;
1161
1162 let packets = collect_server_packets(&mut state, update);
1163
1164 assert_eq!(packets.len(), 1);
1165 assert!(matches!(packets[0], EntityMovementSyncPacket::Rotation(_)));
1166 }
1167
1168 #[test]
1169 fn server_entity_sync_forces_full_position_when_on_ground_changes() {
1170 let mut state = ServerEntityMovementSyncState::new(
1171 DVec3::ZERO,
1172 DVec3::ZERO,
1173 false,
1174 (0.0, 0.0),
1175 0.0,
1176 20,
1177 false,
1178 );
1179 let mut update = server_update(DVec3::new(0.25, 0.0, 0.0), DVec3::ZERO);
1180 update.on_ground = true;
1181
1182 let packets = collect_server_packets(&mut state, update);
1183
1184 assert_eq!(packets.len(), 1);
1185 assert!(matches!(
1186 packets[0],
1187 EntityMovementSyncPacket::PositionSync(_)
1188 ));
1189 }
1190
1191 #[test]
1192 fn server_entity_sync_rotates_passenger_without_position_packet() {
1193 let mut state = ServerEntityMovementSyncState::new(
1194 DVec3::ZERO,
1195 DVec3::ZERO,
1196 false,
1197 (0.0, 0.0),
1198 0.0,
1199 1,
1200 true,
1201 );
1202 let mut update = server_update(DVec3::new(0.25, 0.0, 0.0), DVec3::new(0.001, 0.0, 0.0));
1203 update.is_passenger = true;
1204 update.body_rotation = (2.0, 0.0);
1205
1206 let packets = collect_server_packets(&mut state, update);
1207
1208 assert_eq!(packets.len(), 1);
1209 assert!(matches!(packets[0], EntityMovementSyncPacket::Rotation(_)));
1210 }
1211
1212 #[test]
1213 fn sync_decision_builds_position_delta_packet() {
1214 let position = DVec3::new(0.25, 0.0, -0.5);
1215 let decision = EntityPositionSyncDecision::Delta {
1216 dx: calc_delta(position.x, 0.0).expect("delta should fit"),
1217 dy: calc_delta(position.y, 0.0).expect("delta should fit"),
1218 dz: calc_delta(position.z, 0.0).expect("delta should fit"),
1219 };
1220
1221 let packet = decision.into_position_packet(EntityPositionSyncSnapshot::new(
1222 12,
1223 position,
1224 DVec3::new(1.0, 2.0, 3.0),
1225 (90.0, 45.0),
1226 true,
1227 ));
1228
1229 let EntityPositionSyncPacket::Delta(packet) = packet else {
1230 panic!("expected delta packet");
1231 };
1232 assert_eq!(packet.entity_id, 12);
1233 assert_eq!(
1234 packet.dx,
1235 calc_delta(position.x, 0.0).expect("delta should fit")
1236 );
1237 assert_eq!(
1238 packet.dy,
1239 calc_delta(position.y, 0.0).expect("delta should fit")
1240 );
1241 assert_eq!(
1242 packet.dz,
1243 calc_delta(position.z, 0.0).expect("delta should fit")
1244 );
1245 assert!(packet.on_ground);
1246 }
1247
1248 #[test]
1249 fn sync_decision_builds_position_rotation_delta_packet() {
1250 let position = DVec3::new(0.25, 0.0, -0.5);
1251 let decision = EntityPositionSyncDecision::Delta {
1252 dx: calc_delta(position.x, 0.0).expect("delta should fit"),
1253 dy: calc_delta(position.y, 0.0).expect("delta should fit"),
1254 dz: calc_delta(position.z, 0.0).expect("delta should fit"),
1255 };
1256
1257 let packet = decision.into_position_rot_packet(EntityPositionSyncSnapshot::new(
1258 12,
1259 position,
1260 DVec3::new(1.0, 2.0, 3.0),
1261 (90.0, 45.0),
1262 true,
1263 ));
1264
1265 let EntityPositionRotSyncPacket::Delta(packet) = packet else {
1266 panic!("expected position-rotation delta packet");
1267 };
1268 assert_eq!(packet.entity_id, 12);
1269 assert_eq!(
1270 packet.dx,
1271 calc_delta(position.x, 0.0).expect("delta should fit")
1272 );
1273 assert_eq!(
1274 packet.dy,
1275 calc_delta(position.y, 0.0).expect("delta should fit")
1276 );
1277 assert_eq!(
1278 packet.dz,
1279 calc_delta(position.z, 0.0).expect("delta should fit")
1280 );
1281 assert_eq!(packet.y_rot, to_angle_byte(90.0));
1282 assert_eq!(packet.x_rot, to_angle_byte(45.0));
1283 assert!(packet.on_ground);
1284 }
1285
1286 #[test]
1287 fn sync_decision_builds_full_position_sync_packet() {
1288 let snapshot = EntityPositionSyncSnapshot::new(
1289 12,
1290 DVec3::new(10.0, 20.0, 30.0),
1291 DVec3::new(1.0, 2.0, 3.0),
1292 (90.0, 45.0),
1293 true,
1294 );
1295
1296 let packet = EntityPositionSyncDecision::Full.into_position_packet(snapshot);
1297
1298 let EntityPositionSyncPacket::Full(packet) = packet else {
1299 panic!("expected full packet");
1300 };
1301 assert_eq!(packet.entity_id, 12);
1302 assert_eq!(packet.pos.x.to_bits(), 10.0_f64.to_bits());
1303 assert_eq!(packet.pos.y.to_bits(), 20.0_f64.to_bits());
1304 assert_eq!(packet.pos.z.to_bits(), 30.0_f64.to_bits());
1305 assert_eq!(packet.vel.x.to_bits(), 1.0_f64.to_bits());
1306 assert_eq!(packet.vel.y.to_bits(), 2.0_f64.to_bits());
1307 assert_eq!(packet.vel.z.to_bits(), 3.0_f64.to_bits());
1308 assert_eq!(packet.yaw.to_bits(), 90.0_f32.to_bits());
1309 assert_eq!(packet.pitch.to_bits(), 45.0_f32.to_bits());
1310 assert!(packet.on_ground);
1311 }
1312}