aboutsummaryrefslogtreecommitdiff
path: root/azalea-client/src/plugins/events.rs
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2025-02-22 21:45:26 -0600
committerGitHub <noreply@github.com>2025-02-22 21:45:26 -0600
commite21e1b97bf9337e9f4747cd1b545b1b3a03e2ce7 (patch)
treeadd6f8bfce40d0c07845d8aa4c9945a0b918444c /azalea-client/src/plugins/events.rs
parentf8130c3c92946d2293634ba4e252d6bc93026c3c (diff)
downloadazalea-drasl-e21e1b97bf9337e9f4747cd1b545b1b3a03e2ce7.tar.xz
Refactor azalea-client (#205)
* start organizing packet_handling more by moving packet handlers into their own functions * finish writing all the handler functions for packets * use macro for generating match statement for packet handler functions * fix set_entity_data * update config state to also use handler functions * organize az-client file structure by moving things into plugins directory * fix merge issues
Diffstat (limited to 'azalea-client/src/plugins/events.rs')
-rw-r--r--azalea-client/src/plugins/events.rs259
1 files changed, 259 insertions, 0 deletions
diff --git a/azalea-client/src/plugins/events.rs b/azalea-client/src/plugins/events.rs
new file mode 100644
index 00000000..3d34d75f
--- /dev/null
+++ b/azalea-client/src/plugins/events.rs
@@ -0,0 +1,259 @@
+//! Defines the [`Event`] enum and makes those events trigger when they're sent
+//! in the ECS.
+
+use std::sync::Arc;
+
+use azalea_chat::FormattedText;
+use azalea_core::tick::GameTick;
+use azalea_entity::Dead;
+use azalea_protocol::packets::game::{
+ ClientboundGamePacket, c_player_combat_kill::ClientboundPlayerCombatKill,
+};
+use azalea_world::{InstanceName, MinecraftEntityId};
+use bevy_app::{App, Plugin, PreUpdate, Update};
+use bevy_ecs::{
+ component::Component,
+ event::EventReader,
+ query::{Added, With},
+ schedule::IntoSystemConfigs,
+ system::Query,
+};
+use derive_more::{Deref, DerefMut};
+use tokio::sync::mpsc;
+
+use crate::{
+ PlayerInfo,
+ chat::{ChatPacket, ChatReceivedEvent},
+ disconnect::DisconnectEvent,
+ packet::game::{
+ AddPlayerEvent, DeathEvent, KeepAliveEvent, ReceivePacketEvent, RemovePlayerEvent,
+ UpdatePlayerEvent,
+ },
+};
+
+// (for contributors):
+// HOW TO ADD A NEW (packet based) EVENT:
+// - Add it as an ECS event first:
+// - Make a struct that contains an entity field and some data fields (look
+// in packet/game/events.rs for examples. These structs should always have
+// their names end with "Event".
+// - (the `entity` field is the local player entity that's receiving the
+// event)
+// - In the GamePacketHandler, you always have a `player` field that you can
+// use.
+// - Add the event struct in PacketPlugin::build
+// - (in the `impl Plugin for PacketPlugin`)
+// - To get the event writer, you have to get an EventWriter<ThingEvent>.
+// Look at other packets in packet/game/mod.rs for examples.
+//
+// At this point, you've created a new ECS event. That's annoying for bots to
+// use though, so you might wanna add it to the Event enum too:
+// - In this file, add a new variant to that Event enum with the same name
+// as your event (without the "Event" suffix).
+// - Create a new system function like the other ones here, and put that
+// system function in the `impl Plugin for EventsPlugin`
+
+/// Something that happened in-game, such as a tick passing or chat message
+/// being sent.
+///
+/// Note: Events are sent before they're processed, so for example game ticks
+/// happen at the beginning of a tick before anything has happened.
+#[derive(Debug, Clone)]
+pub enum Event {
+ /// Happens right after the bot switches into the Game state, but before
+ /// it's actually spawned. This can be useful for setting the client
+ /// information with `Client::set_client_information`, so the packet
+ /// doesn't have to be sent twice.
+ ///
+ /// You may want to use [`Event::Login`] instead to wait for the bot to be
+ /// in the world.
+ Init,
+ /// The client is now in the world. Fired when we receive a login packet.
+ Login,
+ /// A chat message was sent in the game chat.
+ Chat(ChatPacket),
+ /// Happens 20 times per second, but only when the world is loaded.
+ Tick,
+ /// We received a packet from the server.
+ ///
+ /// ```
+ /// # use azalea_client::Event;
+ /// # use azalea_protocol::packets::game::ClientboundGamePacket;
+ /// # async fn example(event: Event) {
+ /// # match event {
+ /// Event::Packet(packet) => match *packet {
+ /// ClientboundGamePacket::Login(_) => {
+ /// println!("login packet");
+ /// }
+ /// _ => {}
+ /// },
+ /// # _ => {}
+ /// # }
+ /// # }
+ /// ```
+ Packet(Arc<ClientboundGamePacket>),
+ /// A player joined the game (or more specifically, was added to the tab
+ /// list).
+ AddPlayer(PlayerInfo),
+ /// A player left the game (or maybe is still in the game and was just
+ /// removed from the tab list).
+ RemovePlayer(PlayerInfo),
+ /// A player was updated in the tab list (gamemode, display
+ /// name, or latency changed).
+ UpdatePlayer(PlayerInfo),
+ /// The client player died in-game.
+ Death(Option<Arc<ClientboundPlayerCombatKill>>),
+ /// A `KeepAlive` packet was sent by the server.
+ KeepAlive(u64),
+ /// The client disconnected from the server.
+ Disconnect(Option<FormattedText>),
+}
+
+/// A component that contains an event sender for events that are only
+/// received by local players. The receiver for this is returned by
+/// [`Client::start_client`].
+///
+/// [`Client::start_client`]: crate::Client::start_client
+#[derive(Component, Deref, DerefMut)]
+pub struct LocalPlayerEvents(pub mpsc::UnboundedSender<Event>);
+
+pub struct EventsPlugin;
+impl Plugin for EventsPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_systems(
+ Update,
+ (
+ chat_listener,
+ login_listener,
+ packet_listener,
+ add_player_listener,
+ update_player_listener,
+ remove_player_listener,
+ keepalive_listener,
+ death_listener,
+ disconnect_listener,
+ ),
+ )
+ .add_systems(
+ PreUpdate,
+ init_listener.before(crate::packet::game::process_packet_events),
+ )
+ .add_systems(GameTick, tick_listener);
+ }
+}
+
+// when LocalPlayerEvents is added, it means the client just started
+pub fn init_listener(query: Query<&LocalPlayerEvents, Added<LocalPlayerEvents>>) {
+ for local_player_events in &query {
+ let _ = local_player_events.send(Event::Init);
+ }
+}
+
+// when MinecraftEntityId is added, it means the player is now in the world
+pub fn login_listener(query: Query<&LocalPlayerEvents, Added<MinecraftEntityId>>) {
+ for local_player_events in &query {
+ let _ = local_player_events.send(Event::Login);
+ }
+}
+
+pub fn chat_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader<ChatReceivedEvent>) {
+ for event in events.read() {
+ let local_player_events = query
+ .get(event.entity)
+ .expect("Non-local entities shouldn't be able to receive chat events");
+ let _ = local_player_events.send(Event::Chat(event.packet.clone()));
+ }
+}
+
+// only tick if we're in a world
+pub fn tick_listener(query: Query<&LocalPlayerEvents, With<InstanceName>>) {
+ for local_player_events in &query {
+ let _ = local_player_events.send(Event::Tick);
+ }
+}
+
+pub fn packet_listener(
+ query: Query<&LocalPlayerEvents>,
+ mut events: EventReader<ReceivePacketEvent>,
+) {
+ for event in events.read() {
+ let local_player_events = query
+ .get(event.entity)
+ .expect("Non-local entities shouldn't be able to receive packet events");
+ let _ = local_player_events.send(Event::Packet(event.packet.clone()));
+ }
+}
+
+pub fn add_player_listener(
+ query: Query<&LocalPlayerEvents>,
+ mut events: EventReader<AddPlayerEvent>,
+) {
+ for event in events.read() {
+ let local_player_events = query
+ .get(event.entity)
+ .expect("Non-local entities shouldn't be able to receive add player events");
+ let _ = local_player_events.send(Event::AddPlayer(event.info.clone()));
+ }
+}
+
+pub fn update_player_listener(
+ query: Query<&LocalPlayerEvents>,
+ mut events: EventReader<UpdatePlayerEvent>,
+) {
+ for event in events.read() {
+ let local_player_events = query
+ .get(event.entity)
+ .expect("Non-local entities shouldn't be able to receive update player events");
+ let _ = local_player_events.send(Event::UpdatePlayer(event.info.clone()));
+ }
+}
+
+pub fn remove_player_listener(
+ query: Query<&LocalPlayerEvents>,
+ mut events: EventReader<RemovePlayerEvent>,
+) {
+ for event in events.read() {
+ let local_player_events = query
+ .get(event.entity)
+ .expect("Non-local entities shouldn't be able to receive remove player events");
+ let _ = local_player_events.send(Event::RemovePlayer(event.info.clone()));
+ }
+}
+
+pub fn death_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader<DeathEvent>) {
+ for event in events.read() {
+ if let Ok(local_player_events) = query.get(event.entity) {
+ let _ = local_player_events.send(Event::Death(event.packet.clone().map(|p| p.into())));
+ }
+ }
+}
+
+/// Send the "Death" event for [`LocalEntity`]s that died with no reason.
+pub fn dead_component_listener(query: Query<&LocalPlayerEvents, Added<Dead>>) {
+ for local_player_events in &query {
+ local_player_events.send(Event::Death(None)).unwrap();
+ }
+}
+
+pub fn keepalive_listener(
+ query: Query<&LocalPlayerEvents>,
+ mut events: EventReader<KeepAliveEvent>,
+) {
+ for event in events.read() {
+ let local_player_events = query
+ .get(event.entity)
+ .expect("Non-local entities shouldn't be able to receive keepalive events");
+ let _ = local_player_events.send(Event::KeepAlive(event.id));
+ }
+}
+
+pub fn disconnect_listener(
+ query: Query<&LocalPlayerEvents>,
+ mut events: EventReader<DisconnectEvent>,
+) {
+ for event in events.read() {
+ if let Ok(local_player_events) = query.get(event.entity) {
+ let _ = local_player_events.send(Event::Disconnect(event.reason.clone()));
+ }
+ }
+}