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