Skip to main content

steel_core/entity/
movement_sync.rs

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