Skip to main content

chunkedge_server/
teleport.rs

1use bevy_app::prelude::*;
2use bevy_ecs::prelude::*;
3use chunkedge_entity::{Look, Position, Velocity};
4use chunkedge_math::DVec3;
5use chunkedge_protocol::WritePacket;
6use chunkedge_protocol::packets::play::player_position_s2c::TeleportRelativeFlags;
7use chunkedge_protocol::packets::play::{AcceptTeleportationC2s, PlayerPositionS2c};
8use tracing::warn;
9
10use crate::client::{Client, UpdateClientsSet, update_view_and_layers};
11use crate::event_loop::{EventLoopPreUpdate, PacketMessage};
12use crate::spawn::update_respawn_position;
13
14pub struct TeleportPlugin;
15
16impl Plugin for TeleportPlugin {
17    fn build(&self, app: &mut App) {
18        app.add_systems(
19            PostUpdate,
20            teleport
21                .after(update_view_and_layers)
22                .before(update_respawn_position)
23                .in_set(UpdateClientsSet),
24        )
25        .add_systems(EventLoopPreUpdate, handle_teleport_confirmations);
26    }
27}
28
29#[derive(Component, Debug)]
30pub struct TeleportState {
31    /// Counts up as teleports are made.
32    teleport_id_counter: u32,
33    /// The number of pending client teleports that have yet to receive a
34    /// confirmation. Inbound client position packets should be ignored while
35    /// this is nonzero.
36    pending_teleports: u32,
37    pub(super) synced_pos: DVec3,
38    pub(super) synced_velocity: DVec3,
39    pub(super) synced_look: Look,
40}
41
42impl TeleportState {
43    pub(super) fn new() -> Self {
44        Self {
45            teleport_id_counter: 0,
46            pending_teleports: 0,
47            // Set initial synced pos and look to NaN so a teleport always happens when first
48            // joining.
49            synced_pos: DVec3::NAN,
50            synced_velocity: DVec3::NAN,
51            synced_look: Look {
52                yaw: f32::NAN,
53                pitch: f32::NAN,
54            },
55        }
56    }
57
58    pub fn teleport_id_counter(&self) -> u32 {
59        self.teleport_id_counter
60    }
61
62    pub fn pending_teleports(&self) -> u32 {
63        self.pending_teleports
64    }
65}
66
67/// Syncs the client's position and look with the server.
68///
69/// This should happen after chunks are loaded so the client doesn't fall though
70/// the floor.
71#[allow(clippy::type_complexity)]
72fn teleport(
73    mut clients: Query<
74        (&mut Client, &mut TeleportState, &Position, &Velocity, &Look),
75        Or<(Changed<Position>, Changed<Velocity>, Changed<Look>)>,
76    >,
77) {
78    for (mut client, mut state, pos, velocity, look) in &mut clients {
79        let changed_pos = pos.0 != state.synced_pos;
80        let changed_velocity = velocity.0 != state.synced_velocity;
81        let changed_yaw = look.yaw != state.synced_look.yaw;
82        let changed_pitch = look.pitch != state.synced_look.pitch;
83
84        if changed_pos || changed_velocity || changed_yaw || changed_pitch {
85            state.synced_pos = pos.0;
86            state.synced_velocity = velocity.0;
87            state.synced_look = *look;
88
89            let flags = TeleportRelativeFlags::new()
90                .with_x(!changed_pos)
91                .with_y(!changed_pos)
92                .with_z(!changed_pos)
93                .with_x_vel(!changed_velocity)
94                .with_y_vel(!changed_velocity)
95                .with_z_vel(!changed_velocity)
96                .with_y_rot(!changed_yaw)
97                .with_x_rot(!changed_pitch);
98
99            client.write_packet(&PlayerPositionS2c {
100                position: if changed_pos { pos.0 } else { DVec3::ZERO },
101                velocity: if changed_velocity {
102                    velocity.0
103                } else {
104                    DVec3::ZERO
105                },
106                yaw: if changed_yaw { look.yaw } else { 0.0 },
107                pitch: if changed_pitch { look.pitch } else { 0.0 },
108                flags,
109                teleport_id: (state.teleport_id_counter as i32).into(),
110            });
111
112            state.pending_teleports = state.pending_teleports.wrapping_add(1);
113            state.teleport_id_counter = state.teleport_id_counter.wrapping_add(1);
114        }
115    }
116}
117
118fn handle_teleport_confirmations(
119    mut packets: MessageReader<PacketMessage>,
120    mut clients: Query<&mut TeleportState>,
121    mut commands: Commands,
122) {
123    for packet in packets.read() {
124        if let Some(pkt) = packet.decode::<AcceptTeleportationC2s>()
125            && let Ok(mut state) = clients.get_mut(packet.client)
126        {
127            if state.pending_teleports == 0 {
128                warn!(
129                    "unexpected teleport confirmation from client {:?}",
130                    packet.client
131                );
132                commands.entity(packet.client).remove::<Client>();
133            }
134
135            let got = pkt.teleport_id.0 as u32;
136            let expected = state
137                .teleport_id_counter
138                .wrapping_sub(state.pending_teleports);
139
140            if got == expected {
141                state.pending_teleports -= 1;
142            } else {
143                warn!(
144                    "unexpected teleport ID for client {:?} (expected {expected}, got {got}",
145                    packet.client
146                );
147                commands.entity(packet.client).remove::<Client>();
148            }
149        }
150    }
151}