azalea_client/plugins/packet/game/events.rs
1use std::{
2 io::Cursor,
3 sync::{Arc, Weak},
4};
5
6use azalea_chat::FormattedText;
7use azalea_core::resource_location::ResourceLocation;
8use azalea_protocol::{
9 packets::{
10 Packet,
11 game::{ClientboundGamePacket, ClientboundPlayerCombatKill, ServerboundGamePacket},
12 },
13 read::deserialize_packet,
14};
15use azalea_world::Instance;
16use bevy_ecs::prelude::*;
17use parking_lot::RwLock;
18use tracing::{debug, error};
19use uuid::Uuid;
20
21use crate::{PlayerInfo, client::InGameState, raw_connection::RawConnection};
22
23/// An event that's sent when we receive a packet.
24/// ```
25/// # use azalea_client::packet::game::ReceivePacketEvent;
26/// # use azalea_protocol::packets::game::ClientboundGamePacket;
27/// # use bevy_ecs::event::EventReader;
28///
29/// fn handle_packets(mut events: EventReader<ReceivePacketEvent>) {
30/// for ReceivePacketEvent {
31/// entity,
32/// packet,
33/// } in events.read() {
34/// match packet.as_ref() {
35/// ClientboundGamePacket::LevelParticles(p) => {
36/// // ...
37/// }
38/// _ => {}
39/// }
40/// }
41/// }
42/// ```
43#[derive(Event, Debug, Clone)]
44pub struct ReceivePacketEvent {
45 /// The client entity that received the packet.
46 pub entity: Entity,
47 /// The packet that was actually received.
48 pub packet: Arc<ClientboundGamePacket>,
49}
50
51/// An event for sending a packet to the server while we're in the `game` state.
52#[derive(Event, Clone, Debug)]
53pub struct SendPacketEvent {
54 pub sent_by: Entity,
55 pub packet: ServerboundGamePacket,
56}
57impl SendPacketEvent {
58 pub fn new(sent_by: Entity, packet: impl Packet<ServerboundGamePacket>) -> Self {
59 let packet = packet.into_variant();
60 Self { sent_by, packet }
61 }
62}
63
64pub fn handle_outgoing_packets_observer(
65 trigger: Trigger<SendPacketEvent>,
66 mut query: Query<(&mut RawConnection, Option<&InGameState>)>,
67) {
68 let event = trigger.event();
69
70 if let Ok((raw_connection, in_game_state)) = query.get_mut(event.sent_by) {
71 if in_game_state.is_none() {
72 error!(
73 "Tried to send a game packet {:?} while not in game state",
74 event.packet
75 );
76 return;
77 }
78
79 // debug!("Sending packet: {:?}", event.packet);
80 if let Err(e) = raw_connection.write_packet(event.packet.clone()) {
81 error!("Failed to send packet: {e}");
82 }
83 }
84}
85
86/// A system that converts [`SendPacketEvent`] events into triggers so they get
87/// received by [`handle_outgoing_packets_observer`].
88pub fn handle_outgoing_packets(mut commands: Commands, mut events: EventReader<SendPacketEvent>) {
89 for event in events.read() {
90 commands.trigger(event.clone());
91 }
92}
93
94pub fn emit_receive_packet_events(
95 query: Query<(Entity, &RawConnection), With<InGameState>>,
96 mut packet_events: ResMut<Events<ReceivePacketEvent>>,
97) {
98 // we manually clear and send the events at the beginning of each update
99 // since otherwise it'd cause issues with events in process_packet_events
100 // running twice
101 packet_events.clear();
102 for (player_entity, raw_connection) in &query {
103 let packets_lock = raw_connection.incoming_packet_queue();
104 let mut packets = packets_lock.lock();
105 if !packets.is_empty() {
106 let mut packets_read = 0;
107 for raw_packet in packets.iter() {
108 packets_read += 1;
109 let packet =
110 match deserialize_packet::<ClientboundGamePacket>(&mut Cursor::new(raw_packet))
111 {
112 Ok(packet) => packet,
113 Err(err) => {
114 error!("failed to read packet: {err:?}");
115 debug!("packet bytes: {raw_packet:?}");
116 continue;
117 }
118 };
119
120 let should_interrupt = packet_interrupts(&packet);
121
122 packet_events.send(ReceivePacketEvent {
123 entity: player_entity,
124 packet: Arc::new(packet),
125 });
126
127 if should_interrupt {
128 break;
129 }
130 }
131 packets.drain(0..packets_read);
132 }
133 }
134}
135
136/// Whether the given packet should make us stop deserializing the received
137/// packets until next update.
138///
139/// This is used for packets that can switch the client state.
140fn packet_interrupts(packet: &ClientboundGamePacket) -> bool {
141 matches!(
142 packet,
143 ClientboundGamePacket::StartConfiguration(_)
144 | ClientboundGamePacket::Disconnect(_)
145 | ClientboundGamePacket::Transfer(_)
146 )
147}
148
149/// A player joined the game (or more specifically, was added to the tab
150/// list of a local player).
151#[derive(Event, Debug, Clone)]
152pub struct AddPlayerEvent {
153 /// The local player entity that received this event.
154 pub entity: Entity,
155 pub info: PlayerInfo,
156}
157/// A player left the game (or maybe is still in the game and was just
158/// removed from the tab list of a local player).
159#[derive(Event, Debug, Clone)]
160pub struct RemovePlayerEvent {
161 /// The local player entity that received this event.
162 pub entity: Entity,
163 pub info: PlayerInfo,
164}
165/// A player was updated in the tab list of a local player (gamemode, display
166/// name, or latency changed).
167#[derive(Event, Debug, Clone)]
168pub struct UpdatePlayerEvent {
169 /// The local player entity that received this event.
170 pub entity: Entity,
171 pub info: PlayerInfo,
172}
173
174/// Event for when an entity dies. dies. If it's a local player and there's a
175/// reason in the death screen, the [`ClientboundPlayerCombatKill`] will
176/// be included.
177#[derive(Event, Debug, Clone)]
178pub struct DeathEvent {
179 pub entity: Entity,
180 pub packet: Option<ClientboundPlayerCombatKill>,
181}
182
183/// A KeepAlive packet is sent from the server to verify that the client is
184/// still connected.
185#[derive(Event, Debug, Clone)]
186pub struct KeepAliveEvent {
187 pub entity: Entity,
188 /// The ID of the keepalive. This is an arbitrary number, but vanilla
189 /// servers use the time to generate this.
190 pub id: u64,
191}
192
193#[derive(Event, Debug, Clone)]
194pub struct ResourcePackEvent {
195 pub entity: Entity,
196 /// The random ID for this request to download the resource pack. The packet
197 /// for replying to a resource pack push must contain the same ID.
198 pub id: Uuid,
199 pub url: String,
200 pub hash: String,
201 pub required: bool,
202 pub prompt: Option<FormattedText>,
203}
204
205/// An instance (aka world, dimension) was loaded by a client.
206///
207/// Since the instance is given to you as a weak reference, it won't be able to
208/// be `upgrade`d if all local players leave it.
209#[derive(Event, Debug, Clone)]
210pub struct InstanceLoadedEvent {
211 pub entity: Entity,
212 pub name: ResourceLocation,
213 pub instance: Weak<RwLock<Instance>>,
214}
215
216/// A Bevy trigger that's sent when our client receives a [`ClientboundPing`]
217/// packet in the game state.
218///
219/// Also see [`ConfigPingEvent`] which is used for the config state.
220///
221/// This is not an event and can't be listened to from a normal system,
222///so `EventReader<PingEvent>` will not work.
223///
224/// To use it, add your "system" with `add_observer` instead of `add_systems`
225/// and use `Trigger<PingEvent>` instead of `EventReader`.
226///
227/// The client Entity that received the packet will be attached to the trigger.
228///
229/// [`ClientboundPing`]: azalea_protocol::packets::game::ClientboundPing
230/// [`ConfigPingEvent`]: crate::packet::config::ConfigPingEvent
231#[derive(Event, Debug, Clone)]
232pub struct PingEvent(pub azalea_protocol::packets::game::ClientboundPing);