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, calc_delta, to_angle_byte,
7};
8
9/// Squared position delta needed before vanilla considers a movement worth syncing.
10pub const POSITION_SYNC_THRESHOLD: f64 = 7.629_394_5e-6;
11/// Squared velocity delta needed before vanilla sends an entity motion packet.
12pub const VELOCITY_SYNC_THRESHOLD: f64 = 1.0e-7;
13/// Vanilla `ServerEntity.FORCED_POS_UPDATE_PERIOD`.
14pub const FORCED_POS_UPDATE_PERIOD: i32 = 60;
15/// Vanilla `ServerEntity.FORCED_TELEPORT_PERIOD`.
16pub const FORCED_TELEPORT_PERIOD: i32 = 400;
17
18/// Packed body rotation used by entity movement packets.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct PackedEntityRotation {
21    yaw: i8,
22    pitch: i8,
23}
24
25impl PackedEntityRotation {
26    /// Packs yaw and pitch using vanilla's angle-byte representation.
27    #[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    /// Returns packed yaw.
36    #[must_use]
37    pub const fn yaw(self) -> i8 {
38        self.yaw
39    }
40
41    /// Returns packed pitch.
42    #[must_use]
43    pub const fn pitch(self) -> i8 {
44        self.pitch
45    }
46}
47
48/// Last packed rotation values known to tracking clients.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct EntityRotationSyncState {
51    last_body_rotation: PackedEntityRotation,
52    last_head_yaw: i8,
53}
54
55impl EntityRotationSyncState {
56    /// Creates rotation sync state for values already known to tracking clients.
57    #[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    /// Records a body rotation packet if the packed yaw or pitch changed.
66    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    /// Returns whether packed body yaw or pitch changed since the last sync.
77    #[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    /// Marks a body rotation as sent because a full position sync includes it.
83    pub const fn mark_body_rotation_sent(&mut self, rotation: (f32, f32)) {
84        self.last_body_rotation = PackedEntityRotation::from_degrees(rotation);
85    }
86
87    /// Records a head-rotation packet if the packed yaw changed.
88    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/// Encoded position sync selected for an entity movement update.
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum EntityPositionSyncDecision {
102    /// Delta-encoded movement update.
103    Delta {
104        /// Packed X delta.
105        dx: PackedEntityDelta,
106        /// Packed Y delta.
107        dy: PackedEntityDelta,
108        /// Packed Z delta.
109        dz: PackedEntityDelta,
110    },
111    /// Full absolute position sync.
112    Full,
113}
114
115/// Position sync packet for entities that do not include rotation in delta updates.
116#[derive(Clone, Debug)]
117pub enum EntityPositionSyncPacket {
118    /// Delta-encoded position update.
119    Delta(CMoveEntityPos),
120    /// Full absolute position sync.
121    Full(CEntityPositionSync),
122}
123
124/// Position sync packet for entities that include rotation in delta updates.
125#[derive(Clone, Debug)]
126pub enum EntityPositionRotSyncPacket {
127    /// Delta-encoded position and rotation update.
128    Delta(CMoveEntityPosRot),
129    /// Full absolute position sync.
130    Full(CEntityPositionSync),
131}
132
133/// Concrete movement sync packet ready for broadcast.
134#[derive(Clone, Debug)]
135pub enum EntityMovementSyncPacket {
136    /// Delta-encoded position update.
137    Position(CMoveEntityPos),
138    /// Delta-encoded position and body rotation update.
139    PositionRotation(CMoveEntityPosRot),
140    /// Body rotation-only update.
141    Rotation(CMoveEntityRot),
142    /// Head yaw update.
143    HeadRotation(CRotateHead),
144    /// Full absolute position sync.
145    PositionSync(CEntityPositionSync),
146    /// Velocity sync.
147    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/// At most two movement packets are emitted for one tracked movement update:
187/// one body movement/rotation packet and one head-rotation packet.
188#[derive(Clone, Debug, Default)]
189pub struct EntityMovementSyncPackets {
190    primary: Option<EntityMovementSyncPacket>,
191    head_rotation: Option<EntityMovementSyncPacket>,
192}
193
194impl EntityMovementSyncPackets {
195    /// Creates a bundle from body movement and head-rotation packets.
196    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    /// Returns whether no movement sync packets were selected.
204    #[must_use]
205    pub const fn is_empty(&self) -> bool {
206        self.primary.is_none() && self.head_rotation.is_none()
207    }
208
209    /// Visits selected packets in vanilla send order.
210    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/// Runtime values needed to build a vanilla position sync packet.
221#[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    /// Creates a packet snapshot for the current entity state.
232    #[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    /// Builds the protocol packet for a position-only entity update.
263    #[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    /// Builds the protocol packet for a position-and-rotation entity update.
281    #[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/// Per-entity position sync state shared by player and entity tracking.
302#[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    /// Creates sync state at the position/on-ground state already known to clients.
311    #[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    /// Returns the last absolute position used as the client's delta base.
321    #[must_use]
322    pub const fn last_sent_position(self) -> DVec3 {
323        self.last_sent_position
324    }
325
326    /// Returns the last on-ground state sent to tracking clients.
327    #[must_use]
328    pub const fn last_sent_on_ground(self) -> bool {
329        self.last_sent_on_ground
330    }
331
332    /// Returns the current delay since the last full position sync.
333    #[must_use]
334    pub const fn sync_delay(self) -> i32 {
335        self.sync_delay
336    }
337
338    /// Increments the full-sync delay and returns the previous value.
339    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    /// Returns whether `current_position` moved far enough to sync.
346    #[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    /// Encodes the delta from the last sent position to `current_position`.
353    ///
354    /// Returns `None` when any component overflows the protocol delta range and
355    /// the caller must send a full position sync instead.
356    #[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    /// Marks a delta movement packet as sent.
368    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    /// Marks a full position sync packet as sent and resets the full-sync delay.
374    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    /// Resets the delta base when vanilla updates `VecDeltaCodec` without a packet.
381    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    /// Selects and records the next movement sync form.
387    ///
388    /// Callers decide whether a sync is needed and whether vanilla forces a full
389    /// sync for their tracking mode. This method owns the shared protocol delta
390    /// overflow fallback and updates the sync base consistently.
391    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/// Per-entity velocity sync state for tracked entities.
408#[derive(Debug, Clone, Copy, PartialEq)]
409pub struct EntityVelocitySyncState {
410    last_sent_velocity: DVec3,
411}
412
413impl EntityVelocitySyncState {
414    /// Creates sync state at the velocity already known to clients.
415    #[must_use]
416    pub const fn new(velocity: DVec3) -> Self {
417        Self {
418            last_sent_velocity: velocity,
419        }
420    }
421
422    /// Returns the last velocity sent to tracking clients.
423    #[must_use]
424    pub const fn last_sent_velocity(self) -> DVec3 {
425        self.last_sent_velocity
426    }
427
428    /// Selects and records a velocity packet if vanilla requires one.
429    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/// Per-entity movement sync state for tracked position and rotation.
447#[derive(Debug, Clone, Copy, PartialEq)]
448pub struct EntityMovementSyncState {
449    position: EntityPositionSyncState,
450    rotation: EntityRotationSyncState,
451}
452
453/// Runtime values accepted by tracked entity movement sync.
454#[derive(Debug, Clone, Copy, PartialEq)]
455pub struct EntityMovementSyncUpdate {
456    /// Entity network id.
457    pub entity_id: i32,
458    /// Whether this update includes position.
459    pub has_position: bool,
460    /// Whether this update includes body/head rotation.
461    pub has_rotation: bool,
462    /// Current entity position, or the previous position for rotation-only updates.
463    pub position: DVec3,
464    /// Current entity velocity.
465    pub velocity: DVec3,
466    /// Current body yaw and pitch in degrees.
467    pub body_rotation: (f32, f32),
468    /// Current head yaw in degrees.
469    pub head_yaw: f32,
470    /// Current on-ground flag.
471    pub on_ground: bool,
472}
473
474/// Per-tracked-entity movement state owned by the entity tracker.
475#[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/// Runtime values accepted by vanilla `ServerEntity.sendChanges` movement sync.
489#[derive(Debug, Clone, Copy, PartialEq)]
490pub struct ServerEntityMovementSyncUpdate {
491    /// Entity network id.
492    pub entity_id: i32,
493    /// Whether the entity is currently riding another entity.
494    pub is_passenger: bool,
495    /// Current entity tracking position.
496    pub position: DVec3,
497    /// Current entity velocity.
498    pub velocity: DVec3,
499    /// Current body yaw and pitch in degrees.
500    pub body_rotation: (f32, f32),
501    /// Current head yaw in degrees.
502    pub head_yaw: f32,
503    /// Current on-ground flag.
504    pub on_ground: bool,
505    /// Vanilla `Entity.needsSync`.
506    pub needs_velocity_sync: bool,
507    /// Whether synced entity data is dirty this tick.
508    pub has_dirty_entity_data: bool,
509    /// Vanilla living fall-flying velocity sync exception.
510    pub force_velocity_sync: bool,
511}
512
513/// Movement packets and side effects selected by one `ServerEntity.sendChanges` pass.
514#[derive(Clone, Debug, Default)]
515pub struct ServerEntityMovementSyncResult {
516    packets: Vec<EntityMovementSyncPacket>,
517    clear_velocity_sync: bool,
518}
519
520impl ServerEntityMovementSyncResult {
521    /// Returns whether vanilla `Entity.needsSync` should be cleared after processing.
522    #[must_use]
523    pub const fn should_clear_velocity_sync(&self) -> bool {
524        self.clear_velocity_sync
525    }
526
527    /// Visits selected packets in vanilla send order.
528    pub fn for_each_packet(self, send: impl FnMut(EntityMovementSyncPacket)) {
529        self.packets.into_iter().for_each(send);
530    }
531}
532
533impl ServerEntityMovementSyncState {
534    /// Creates movement sync state for a newly tracked entity.
535    #[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    /// Selects packets for a vanilla `ServerEntity.sendChanges` movement pass.
559    #[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    /// Creates movement sync state for values already known to tracking clients.
708    #[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    /// Returns the last absolute position used as the client's delta base.
722    #[must_use]
723    pub const fn last_sent_position(self) -> DVec3 {
724        self.position.last_sent_position()
725    }
726
727    /// Selects and records a position sync that forces full packets after a delay.
728    ///
729    /// Vanilla player movement uses this form: delta packets are sent while the
730    /// packed delta base is fresh, then a full position sync refreshes that base.
731    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    /// Selects and records packets for a tracked movement update.
745    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    /// Records a body rotation packet when the packed yaw or pitch changed.
810    pub fn record_body_rotation(&mut self, rotation: (f32, f32)) -> Option<PackedEntityRotation> {
811        self.rotation.record_body_rotation(rotation)
812    }
813
814    /// Marks body rotation as sent because a full position sync includes it.
815    pub const fn mark_body_rotation_sent(&mut self, rotation: (f32, f32)) {
816        self.rotation.mark_body_rotation_sent(rotation);
817    }
818
819    /// Records a head-rotation packet when the packed yaw changed.
820    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}