azalea_client/plugins/packet/game/
events.rs

1use std::sync::{Arc, Weak};
2
3use azalea_chat::FormattedText;
4use azalea_core::resource_location::ResourceLocation;
5use azalea_protocol::packets::{
6    Packet,
7    game::{ClientboundGamePacket, ClientboundPlayerCombatKill, ServerboundGamePacket},
8};
9use azalea_world::Instance;
10use bevy_ecs::prelude::*;
11use parking_lot::RwLock;
12use tracing::{error, trace};
13use uuid::Uuid;
14
15use crate::{client::InGameState, connection::RawConnection, player::PlayerInfo};
16
17/// An event that's sent when we receive a packet.
18/// ```
19/// # use azalea_client::packet::game::ReceiveGamePacketEvent;
20/// # use azalea_protocol::packets::game::ClientboundGamePacket;
21/// # use bevy_ecs::event::EventReader;
22///
23/// fn handle_packets(mut events: EventReader<ReceiveGamePacketEvent>) {
24///     for ReceiveGamePacketEvent { entity, packet } in events.read() {
25///         match packet.as_ref() {
26///             ClientboundGamePacket::LevelParticles(p) => {
27///                 // ...
28///             }
29///             _ => {}
30///         }
31///     }
32/// }
33/// ```
34#[derive(Event, Debug, Clone)]
35pub struct ReceiveGamePacketEvent {
36    /// The client entity that received the packet.
37    pub entity: Entity,
38    /// The packet that was actually received.
39    pub packet: Arc<ClientboundGamePacket>,
40}
41
42/// An event for sending a packet to the server while we're in the `game` state.
43#[derive(Event, Clone, Debug)]
44pub struct SendPacketEvent {
45    pub sent_by: Entity,
46    pub packet: ServerboundGamePacket,
47}
48impl SendPacketEvent {
49    pub fn new(sent_by: Entity, packet: impl Packet<ServerboundGamePacket>) -> Self {
50        let packet = packet.into_variant();
51        Self { sent_by, packet }
52    }
53}
54
55pub fn handle_outgoing_packets_observer(
56    trigger: Trigger<SendPacketEvent>,
57    mut query: Query<(&mut RawConnection, Option<&InGameState>)>,
58) {
59    let event = trigger.event();
60
61    if let Ok((mut raw_connection, in_game_state)) = query.get_mut(event.sent_by) {
62        if in_game_state.is_none() {
63            error!(
64                "Tried to send a game packet {:?} while not in game state",
65                event.packet
66            );
67            return;
68        }
69
70        trace!("Sending game packet: {:?}", event.packet);
71        if let Err(e) = raw_connection.write(event.packet.clone()) {
72            error!("Failed to send packet: {e}");
73        }
74    } else {
75        trace!("Not sending game packet: {:?}", event.packet);
76    }
77}
78
79/// A system that converts [`SendPacketEvent`] events into triggers so they get
80/// received by [`handle_outgoing_packets_observer`].
81pub fn handle_outgoing_packets(mut commands: Commands, mut events: EventReader<SendPacketEvent>) {
82    for event in events.read() {
83        commands.trigger(event.clone());
84    }
85}
86
87/// A player joined the game (or more specifically, was added to the tab
88/// list of a local player).
89#[derive(Event, Debug, Clone)]
90pub struct AddPlayerEvent {
91    /// The local player entity that received this event.
92    pub entity: Entity,
93    pub info: PlayerInfo,
94}
95/// A player left the game (or maybe is still in the game and was just
96/// removed from the tab list of a local player).
97#[derive(Event, Debug, Clone)]
98pub struct RemovePlayerEvent {
99    /// The local player entity that received this event.
100    pub entity: Entity,
101    pub info: PlayerInfo,
102}
103/// A player was updated in the tab list of a local player (gamemode, display
104/// name, or latency changed).
105#[derive(Event, Debug, Clone)]
106pub struct UpdatePlayerEvent {
107    /// The local player entity that received this event.
108    pub entity: Entity,
109    pub info: PlayerInfo,
110}
111
112/// Event for when an entity dies. dies. If it's a local player and there's a
113/// reason in the death screen, the [`ClientboundPlayerCombatKill`] will
114/// be included.
115#[derive(Event, Debug, Clone)]
116pub struct DeathEvent {
117    pub entity: Entity,
118    pub packet: Option<ClientboundPlayerCombatKill>,
119}
120
121/// A KeepAlive packet is sent from the server to verify that the client is
122/// still connected.
123#[derive(Event, Debug, Clone)]
124pub struct KeepAliveEvent {
125    pub entity: Entity,
126    /// The ID of the keepalive. This is an arbitrary number, but vanilla
127    /// servers use the time to generate this.
128    pub id: u64,
129}
130
131#[derive(Event, Debug, Clone)]
132pub struct ResourcePackEvent {
133    pub entity: Entity,
134    /// The random ID for this request to download the resource pack. The packet
135    /// for replying to a resource pack push must contain the same ID.
136    pub id: Uuid,
137    pub url: String,
138    pub hash: String,
139    pub required: bool,
140    pub prompt: Option<FormattedText>,
141}
142
143/// An instance (aka world, dimension) was loaded by a client.
144///
145/// Since the instance is given to you as a weak reference, it won't be able to
146/// be `upgrade`d if all local players leave it.
147#[derive(Event, Debug, Clone)]
148pub struct InstanceLoadedEvent {
149    pub entity: Entity,
150    pub name: ResourceLocation,
151    pub instance: Weak<RwLock<Instance>>,
152}
153
154/// A Bevy trigger that's sent when our client receives a [`ClientboundPing`]
155/// packet in the game state.
156///
157/// Also see [`ConfigPingEvent`] which is used for the config state.
158///
159/// This is not an event and can't be listened to from a normal system,
160///so  `EventReader<PingEvent>` will not work.
161///
162/// To use it, add your "system" with `add_observer` instead of `add_systems`
163/// and use `Trigger<PingEvent>` instead of `EventReader`.
164///
165/// The client Entity that received the packet will be attached to the trigger.
166///
167/// [`ClientboundPing`]: azalea_protocol::packets::game::ClientboundPing
168/// [`ConfigPingEvent`]: crate::packet::config::ConfigPingEvent
169#[derive(Event, Debug, Clone)]
170pub struct PingEvent(pub azalea_protocol::packets::game::ClientboundPing);