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_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(Clone, Debug)]
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::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
133/// [`Client::start_client`](crate::Client::start_client).
134#[derive(Component, Deref, DerefMut)]
135pub struct LocalPlayerEvents(pub mpsc::UnboundedSender<Event>);
136
137pub struct EventsPlugin;
138impl Plugin for EventsPlugin {
139    fn build(&self, app: &mut App) {
140        app.add_systems(
141            Update,
142            (
143                chat_listener,
144                login_listener,
145                spawn_listener,
146                #[cfg(feature = "packet-event")]
147                packet_listener,
148                add_player_listener,
149                update_player_listener,
150                remove_player_listener,
151                keepalive_listener,
152                death_listener.after(azalea_client::packet::death_event_on_0_health),
153                disconnect_listener,
154                receive_chunk_listener,
155            ),
156        )
157        .add_systems(
158            PreUpdate,
159            init_listener.before(super::connection::read_packets),
160        )
161        .add_systems(GameTick, tick_listener);
162    }
163}
164
165// when LocalPlayerEvents is added, it means the client just started
166pub fn init_listener(query: Query<&LocalPlayerEvents, Added<LocalPlayerEvents>>) {
167    for local_player_events in &query {
168        let _ = local_player_events.send(Event::Init);
169    }
170}
171
172// when MinecraftEntityId is added, it means the player is now in the world
173pub fn login_listener(
174    query: Query<(Entity, &LocalPlayerEvents), Added<MinecraftEntityId>>,
175    mut commands: Commands,
176) {
177    for (entity, local_player_events) in &query {
178        let _ = local_player_events.send(Event::Login);
179        commands.entity(entity).remove::<SentSpawnEvent>();
180    }
181}
182
183/// A unit struct component that indicates that the entity has sent
184/// [`Event::Spawn`].
185///
186/// This is just used internally by the [`spawn_listener`] system to avoid
187/// sending the event twice if we stop being in an unloaded chunk. It's removed
188/// when we receive a login packet.
189#[derive(Component)]
190pub struct SentSpawnEvent;
191#[allow(clippy::type_complexity)]
192pub fn spawn_listener(
193    query: Query<(Entity, &LocalPlayerEvents), (Added<InLoadedChunk>, Without<SentSpawnEvent>)>,
194    mut commands: Commands,
195) {
196    for (entity, local_player_events) in &query {
197        let _ = local_player_events.send(Event::Spawn);
198        commands.entity(entity).insert(SentSpawnEvent);
199    }
200}
201
202pub fn chat_listener(
203    query: Query<&LocalPlayerEvents>,
204    mut events: MessageReader<ChatReceivedEvent>,
205) {
206    for event in events.read() {
207        if let Ok(local_player_events) = query.get(event.entity) {
208            let _ = local_player_events.send(Event::Chat(event.packet.clone()));
209        }
210    }
211}
212
213// only tick if we're in a world
214pub fn tick_listener(query: Query<&LocalPlayerEvents, With<InstanceName>>) {
215    for local_player_events in &query {
216        let _ = local_player_events.send(Event::Tick);
217    }
218}
219
220#[cfg(feature = "packet-event")]
221pub fn packet_listener(
222    query: Query<&LocalPlayerEvents>,
223    mut events: MessageReader<super::packet::game::ReceiveGamePacketEvent>,
224) {
225    for event in events.read() {
226        if let Ok(local_player_events) = query.get(event.entity) {
227            let _ = local_player_events.send(Event::Packet(event.packet.clone()));
228        }
229    }
230}
231
232pub fn add_player_listener(
233    query: Query<&LocalPlayerEvents>,
234    mut events: MessageReader<AddPlayerEvent>,
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::AddPlayer(event.info.clone()));
239        }
240    }
241}
242
243pub fn update_player_listener(
244    query: Query<&LocalPlayerEvents>,
245    mut events: MessageReader<UpdatePlayerEvent>,
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::UpdatePlayer(event.info.clone()));
250        }
251    }
252}
253
254pub fn remove_player_listener(
255    query: Query<&LocalPlayerEvents>,
256    mut events: MessageReader<RemovePlayerEvent>,
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::RemovePlayer(event.info.clone()));
261        }
262    }
263}
264
265pub fn death_listener(query: Query<&LocalPlayerEvents>, mut events: MessageReader<DeathEvent>) {
266    for event in events.read() {
267        if let Ok(local_player_events) = query.get(event.entity) {
268            let _ = local_player_events.send(Event::Death(event.packet.clone().map(|p| p.into())));
269        }
270    }
271}
272
273/// Send the "Death" event for [`LocalEntity`]s that died with no reason.
274///
275/// [`LocalEntity`]: azalea_entity::LocalEntity
276pub fn dead_component_listener(query: Query<&LocalPlayerEvents, Added<Dead>>) {
277    for local_player_events in &query {
278        local_player_events.send(Event::Death(None)).unwrap();
279    }
280}
281
282pub fn keepalive_listener(
283    query: Query<&LocalPlayerEvents>,
284    mut events: MessageReader<KeepAliveEvent>,
285) {
286    for event in events.read() {
287        if let Ok(local_player_events) = query.get(event.entity) {
288            let _ = local_player_events.send(Event::KeepAlive(event.id));
289        }
290    }
291}
292
293pub fn disconnect_listener(
294    query: Query<&LocalPlayerEvents>,
295    mut events: MessageReader<DisconnectEvent>,
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::Disconnect(event.reason.clone()));
300        }
301    }
302}
303
304pub fn receive_chunk_listener(
305    query: Query<&LocalPlayerEvents>,
306    mut events: MessageReader<ReceiveChunkEvent>,
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::ReceiveChunk(ChunkPos::new(
311                event.packet.x,
312                event.packet.z,
313            )));
314        }
315    }
316}