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