azalea_client/plugins/
events.rs

1//! Defines the [`enum@Event`] enum and makes those events trigger when they're
2//! sent in the ECS.
3
4use std::sync::Arc;
5
6use azalea_chat::FormattedText;
7use azalea_core::tick::GameTick;
8use azalea_entity::{Dead, InLoadedChunk};
9use azalea_protocol::packets::game::c_player_combat_kill::ClientboundPlayerCombatKill;
10use azalea_world::{InstanceName, MinecraftEntityId};
11use bevy_app::{App, Plugin, PreUpdate, Update};
12use bevy_ecs::prelude::*;
13use derive_more::{Deref, DerefMut};
14use tokio::sync::mpsc;
15
16use crate::{
17    PlayerInfo,
18    chat::{ChatPacket, ChatReceivedEvent},
19    disconnect::DisconnectEvent,
20    packet::game::{
21        AddPlayerEvent, DeathEvent, KeepAliveEvent, RemovePlayerEvent, UpdatePlayerEvent,
22    },
23};
24
25// (for contributors):
26// HOW TO ADD A NEW (packet based) EVENT:
27// - Add it as an ECS event first:
28//     - Make a struct that contains an entity field and some data fields (look
29//       in packet/game/events.rs for examples. These structs should always have
30//       their names end with "Event".
31//         - (the `entity` field is the local player entity that's receiving the
32//           event)
33//     - In the GamePacketHandler, you always have a `player` field that you can
34//       use.
35//     - Add the event struct in PacketPlugin::build
36//         - (in the `impl Plugin for PacketPlugin`)
37//     - To get the event writer, you have to get an EventWriter<ThingEvent>.
38//       Look at other packets in packet/game/mod.rs for examples.
39//
40// At this point, you've created a new ECS event. That's annoying for bots to
41// use though, so you might wanna add it to the Event enum too:
42//     - In this file, add a new variant to that Event enum with the same name
43//       as your event (without the "Event" suffix).
44//     - Create a new system function like the other ones here, and put that
45//       system function in the `impl Plugin for EventsPlugin`
46
47/// Something that happened in-game, such as a tick passing or chat message
48/// being sent.
49///
50/// Note: Events are sent before they're processed, so for example game ticks
51/// happen at the beginning of a tick before anything has happened.
52#[derive(Debug, Clone)]
53#[non_exhaustive]
54pub enum Event {
55    /// Happens right after the bot switches into the Game state, but before
56    /// it's actually spawned. This can be useful for setting the client
57    /// information with `Client::set_client_information`, so the packet
58    /// doesn't have to be sent twice.
59    ///
60    /// You may want to use [`Event::Spawn`] instead to wait for the bot to be
61    /// in the world.
62    Init,
63    /// Fired when we receive a login packet, which is after [`Event::Init`] but
64    /// before [`Event::Spawn`]. You usually want [`Event::Spawn`] instead.
65    ///
66    /// Your position may be [`Vec3::ZERO`] immediately after you receive this
67    /// event, but it'll be ready by the time you get [`Event::Spawn`].
68    ///
69    /// It's possible for this event to be sent multiple times per client if a
70    /// server sends multiple login packets (like when switching worlds).
71    ///
72    /// [`Vec3::ZERO`]: azalea_core::position::Vec3::ZERO
73    Login,
74    /// Fired when the player fully spawns into the world (is in a loaded chunk)
75    /// and is ready to interact with it.
76    ///
77    /// This is usually the event you should listen for when waiting for the bot
78    /// to be ready.
79    ///
80    /// This event will be sent every time the client respawns or switches
81    /// worlds, as long as the server sends chunks to the client.
82    Spawn,
83    /// A chat message was sent in the game chat.
84    Chat(ChatPacket),
85    /// Happens 20 times per second, but only when the world is loaded.
86    Tick,
87    #[cfg(feature = "packet-event")]
88    /// We received a packet from the server.
89    ///
90    /// ```
91    /// # use azalea_client::Event;
92    /// # use azalea_protocol::packets::game::ClientboundGamePacket;
93    /// # async fn example(event: Event) {
94    /// # match event {
95    /// Event::Packet(packet) => match *packet {
96    ///     ClientboundGamePacket::Login(_) => {
97    ///         println!("login packet");
98    ///     }
99    ///     _ => {}
100    /// },
101    /// # _ => {}
102    /// # }
103    /// # }
104    /// ```
105    Packet(Arc<azalea_protocol::packets::game::ClientboundGamePacket>),
106    /// A player joined the game (or more specifically, was added to the tab
107    /// list).
108    AddPlayer(PlayerInfo),
109    /// A player left the game (or maybe is still in the game and was just
110    /// removed from the tab list).
111    RemovePlayer(PlayerInfo),
112    /// A player was updated in the tab list (gamemode, display
113    /// name, or latency changed).
114    UpdatePlayer(PlayerInfo),
115    /// The client player died in-game.
116    Death(Option<Arc<ClientboundPlayerCombatKill>>),
117    /// A `KeepAlive` packet was sent by the server.
118    KeepAlive(u64),
119    /// The client disconnected from the server.
120    Disconnect(Option<FormattedText>),
121}
122
123/// A component that contains an event sender for events that are only
124/// received by local players. The receiver for this is returned by
125/// [`Client::start_client`].
126///
127/// [`Client::start_client`]: crate::Client::start_client
128#[derive(Component, Deref, DerefMut)]
129pub struct LocalPlayerEvents(pub mpsc::UnboundedSender<Event>);
130
131pub struct EventsPlugin;
132impl Plugin for EventsPlugin {
133    fn build(&self, app: &mut App) {
134        app.add_systems(
135            Update,
136            (
137                chat_listener,
138                login_listener,
139                spawn_listener,
140                #[cfg(feature = "packet-event")]
141                packet_listener,
142                add_player_listener,
143                update_player_listener,
144                remove_player_listener,
145                keepalive_listener,
146                death_listener,
147                disconnect_listener,
148            ),
149        )
150        .add_systems(
151            PreUpdate,
152            init_listener.before(super::connection::read_packets),
153        )
154        .add_systems(GameTick, tick_listener);
155    }
156}
157
158// when LocalPlayerEvents is added, it means the client just started
159pub fn init_listener(query: Query<&LocalPlayerEvents, Added<LocalPlayerEvents>>) {
160    for local_player_events in &query {
161        let _ = local_player_events.send(Event::Init);
162    }
163}
164
165// when MinecraftEntityId is added, it means the player is now in the world
166pub fn login_listener(
167    query: Query<(Entity, &LocalPlayerEvents), Added<MinecraftEntityId>>,
168    mut commands: Commands,
169) {
170    for (entity, local_player_events) in &query {
171        let _ = local_player_events.send(Event::Login);
172        commands.entity(entity).remove::<SentSpawnEvent>();
173    }
174}
175
176/// A unit struct component that indicates that the entity has sent
177/// [`Event::Spawn`].
178///
179/// This is just used internally by the [`spawn_listener`] system to avoid
180/// sending the event twice if we stop being in an unloaded chunk. It's removed
181/// when we receive a login packet.
182#[derive(Component)]
183pub struct SentSpawnEvent;
184#[allow(clippy::type_complexity)]
185pub fn spawn_listener(
186    query: Query<(Entity, &LocalPlayerEvents), (Added<InLoadedChunk>, Without<SentSpawnEvent>)>,
187    mut commands: Commands,
188) {
189    for (entity, local_player_events) in &query {
190        let _ = local_player_events.send(Event::Spawn);
191        commands.entity(entity).insert(SentSpawnEvent);
192    }
193}
194
195pub fn chat_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader<ChatReceivedEvent>) {
196    for event in events.read() {
197        if let Ok(local_player_events) = query.get(event.entity) {
198            let _ = local_player_events.send(Event::Chat(event.packet.clone()));
199        }
200    }
201}
202
203// only tick if we're in a world
204pub fn tick_listener(query: Query<&LocalPlayerEvents, With<InstanceName>>) {
205    for local_player_events in &query {
206        let _ = local_player_events.send(Event::Tick);
207    }
208}
209
210#[cfg(feature = "packet-event")]
211pub fn packet_listener(
212    query: Query<&LocalPlayerEvents>,
213    mut events: EventReader<super::packet::game::ReceiveGamePacketEvent>,
214) {
215    for event in events.read() {
216        if let Ok(local_player_events) = query.get(event.entity) {
217            let _ = local_player_events.send(Event::Packet(event.packet.clone()));
218        }
219    }
220}
221
222pub fn add_player_listener(
223    query: Query<&LocalPlayerEvents>,
224    mut events: EventReader<AddPlayerEvent>,
225) {
226    for event in events.read() {
227        let local_player_events = query
228            .get(event.entity)
229            .expect("Non-local entities shouldn't be able to receive add player events");
230        let _ = local_player_events.send(Event::AddPlayer(event.info.clone()));
231    }
232}
233
234pub fn update_player_listener(
235    query: Query<&LocalPlayerEvents>,
236    mut events: EventReader<UpdatePlayerEvent>,
237) {
238    for event in events.read() {
239        let local_player_events = query
240            .get(event.entity)
241            .expect("Non-local entities shouldn't be able to receive update player events");
242        let _ = local_player_events.send(Event::UpdatePlayer(event.info.clone()));
243    }
244}
245
246pub fn remove_player_listener(
247    query: Query<&LocalPlayerEvents>,
248    mut events: EventReader<RemovePlayerEvent>,
249) {
250    for event in events.read() {
251        let local_player_events = query
252            .get(event.entity)
253            .expect("Non-local entities shouldn't be able to receive remove player events");
254        let _ = local_player_events.send(Event::RemovePlayer(event.info.clone()));
255    }
256}
257
258pub fn death_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader<DeathEvent>) {
259    for event in events.read() {
260        if let Ok(local_player_events) = query.get(event.entity) {
261            let _ = local_player_events.send(Event::Death(event.packet.clone().map(|p| p.into())));
262        }
263    }
264}
265
266/// Send the "Death" event for [`LocalEntity`]s that died with no reason.
267///
268/// [`LocalEntity`]: azalea_entity::LocalEntity
269pub fn dead_component_listener(query: Query<&LocalPlayerEvents, Added<Dead>>) {
270    for local_player_events in &query {
271        local_player_events.send(Event::Death(None)).unwrap();
272    }
273}
274
275pub fn keepalive_listener(
276    query: Query<&LocalPlayerEvents>,
277    mut events: EventReader<KeepAliveEvent>,
278) {
279    for event in events.read() {
280        let local_player_events = query
281            .get(event.entity)
282            .expect("Non-local entities shouldn't be able to receive keepalive events");
283        let _ = local_player_events.send(Event::KeepAlive(event.id));
284    }
285}
286
287pub fn disconnect_listener(
288    query: Query<&LocalPlayerEvents>,
289    mut events: EventReader<DisconnectEvent>,
290) {
291    for event in events.read() {
292        if let Ok(local_player_events) = query.get(event.entity) {
293            let _ = local_player_events.send(Event::Disconnect(event.reason.clone()));
294        }
295    }
296}