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::packets::play::player_position_s2c::TeleportRelativeFlags;
6use chunkedge_protocol::packets::play::{AcceptTeleportationC2s, PlayerPositionS2c};
7use chunkedge_protocol::WritePacket;
8use tracing::warn;
9
10use crate::client::{update_view_and_layers, Client, UpdateClientsSet};
11use crate::event_loop::{EventLoopPreUpdate, PacketEvent};
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: EventReader<PacketEvent>,
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            if let Ok(mut state) = clients.get_mut(packet.client) {
126                if state.pending_teleports == 0 {
127                    warn!(
128                        "unexpected teleport confirmation from client {:?}",
129                        packet.client
130                    );
131                    commands.entity(packet.client).remove::<Client>();
132                }
133
134                let got = pkt.teleport_id.0 as u32;
135                let expected = state
136                    .teleport_id_counter
137                    .wrapping_sub(state.pending_teleports);
138
139                if got == expected {
140                    state.pending_teleports -= 1;
141                } else {
142                    warn!(
143                        "unexpected teleport ID for client {:?} (expected {expected}, got {got}",
144                        packet.client
145                    );
146                    commands.entity(packet.client).remove::<Client>();
147                }
148            }
149        }
150    }
151}