Skip to main content

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