diff options
| author | mat <git@matdoes.dev> | 2025-12-28 14:31:41 +0500 |
|---|---|---|
| committer | mat <git@matdoes.dev> | 2025-12-28 14:31:41 +0500 |
| commit | 7ab3b8924f64f7eadb6b8928b6fae73cb06e4c2f (patch) | |
| tree | 5add5d27fd7c2638ebe6fab9bd7adcdae28a358d /azalea/src/events.rs | |
| parent | 9513f42e87f64c409cdb2a100500a50e5a713bac (diff) | |
| download | azalea-drasl-7ab3b8924f64f7eadb6b8928b6fae73cb06e4c2f.tar.xz | |
move Event and auto_reconnect to the azalea crate
Diffstat (limited to 'azalea/src/events.rs')
| -rw-r--r-- | azalea/src/events.rs | 316 |
1 files changed, 316 insertions, 0 deletions
diff --git a/azalea/src/events.rs b/azalea/src/events.rs new file mode 100644 index 00000000..0920dbd8 --- /dev/null +++ b/azalea/src/events.rs @@ -0,0 +1,316 @@ +//! Defines the [`enum@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::{position::ChunkPos, tick::GameTick}; +use azalea_entity::{Dead, InLoadedChunk}; +use azalea_protocol::packets::game::c_player_combat_kill::ClientboundPlayerCombatKill; +use azalea_world::{InstanceName, MinecraftEntityId}; +use bevy_app::{App, Plugin, PreUpdate, Update}; +use bevy_ecs::prelude::*; +use derive_more::{Deref, DerefMut}; +use tokio::sync::mpsc; + +use crate::{ + chat::{ChatPacket, ChatReceivedEvent}, + chunks::ReceiveChunkEvent, + disconnect::DisconnectEvent, + packet::game::{ + AddPlayerEvent, DeathEvent, KeepAliveEvent, RemovePlayerEvent, UpdatePlayerEvent, + }, + player::PlayerInfo, +}; + +// (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 MessageWriter<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(Clone, Debug)] +#[non_exhaustive] +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::Spawn`] instead to wait for the bot to be + /// in the world. + /// + /// [`Client::set_client_information`]: crate::Client::set_client_information + Init, + /// Fired when we receive a login packet, which is after [`Event::Init`] but + /// before [`Event::Spawn`]. You usually want [`Event::Spawn`] instead. + /// + /// Your position may be [`Vec3::ZERO`] immediately after you receive this + /// event, but it'll be ready by the time you get [`Event::Spawn`]. + /// + /// It's possible for this event to be sent multiple times per client if a + /// server sends multiple login packets (like when switching worlds). + /// + /// [`Vec3::ZERO`]: azalea_core::position::Vec3::ZERO + Login, + /// Fired when the player fully spawns into the world (is in a loaded chunk) + /// and is ready to interact with it. + /// + /// This is usually the event you should listen for when waiting for the bot + /// to be ready. + /// + /// This event will be sent every time the client respawns or switches + /// worlds, as long as the server sends chunks to the client. + Spawn, + /// A chat message was sent in the game chat. + Chat(ChatPacket), + /// Happens 20 times per second, but only when the world is loaded. + Tick, + #[cfg(feature = "packet-event")] + /// We received a packet from the server. + /// + /// ``` + /// # use azalea::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<azalea_protocol::packets::game::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>), + ReceiveChunk(ChunkPos), +} + +/// 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`](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, + spawn_listener, + #[cfg(feature = "packet-event")] + packet_listener, + add_player_listener, + update_player_listener, + remove_player_listener, + keepalive_listener, + death_listener.after(azalea_client::packet::death_event_on_0_health), + disconnect_listener, + receive_chunk_listener, + ), + ) + .add_systems( + PreUpdate, + init_listener.before(super::connection::read_packets), + ) + .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<(Entity, &LocalPlayerEvents), Added<MinecraftEntityId>>, + mut commands: Commands, +) { + for (entity, local_player_events) in &query { + let _ = local_player_events.send(Event::Login); + commands.entity(entity).remove::<SentSpawnEvent>(); + } +} + +/// A unit struct component that indicates that the entity has sent +/// [`Event::Spawn`]. +/// +/// This is just used internally by the [`spawn_listener`] system to avoid +/// sending the event twice if we stop being in an unloaded chunk. It's removed +/// when we receive a login packet. +#[derive(Component)] +pub struct SentSpawnEvent; +#[allow(clippy::type_complexity)] +pub fn spawn_listener( + query: Query<(Entity, &LocalPlayerEvents), (Added<InLoadedChunk>, Without<SentSpawnEvent>)>, + mut commands: Commands, +) { + for (entity, local_player_events) in &query { + let _ = local_player_events.send(Event::Spawn); + commands.entity(entity).insert(SentSpawnEvent); + } +} + +pub fn chat_listener( + query: Query<&LocalPlayerEvents>, + mut events: MessageReader<ChatReceivedEvent>, +) { + for event in events.read() { + if let Ok(local_player_events) = query.get(event.entity) { + 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); + } +} + +#[cfg(feature = "packet-event")] +pub fn packet_listener( + query: Query<&LocalPlayerEvents>, + mut events: MessageReader<super::packet::game::ReceiveGamePacketEvent>, +) { + for event in events.read() { + if let Ok(local_player_events) = query.get(event.entity) { + let _ = local_player_events.send(Event::Packet(event.packet.clone())); + } + } +} + +pub fn add_player_listener( + query: Query<&LocalPlayerEvents>, + mut events: MessageReader<AddPlayerEvent>, +) { + for event in events.read() { + if let Ok(local_player_events) = query.get(event.entity) { + let _ = local_player_events.send(Event::AddPlayer(event.info.clone())); + } + } +} + +pub fn update_player_listener( + query: Query<&LocalPlayerEvents>, + mut events: MessageReader<UpdatePlayerEvent>, +) { + for event in events.read() { + if let Ok(local_player_events) = query.get(event.entity) { + let _ = local_player_events.send(Event::UpdatePlayer(event.info.clone())); + } + } +} + +pub fn remove_player_listener( + query: Query<&LocalPlayerEvents>, + mut events: MessageReader<RemovePlayerEvent>, +) { + for event in events.read() { + if let Ok(local_player_events) = query.get(event.entity) { + let _ = local_player_events.send(Event::RemovePlayer(event.info.clone())); + } + } +} + +pub fn death_listener(query: Query<&LocalPlayerEvents>, mut events: MessageReader<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. +/// +/// [`LocalEntity`]: azalea_entity::LocalEntity +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: MessageReader<KeepAliveEvent>, +) { + for event in events.read() { + if let Ok(local_player_events) = query.get(event.entity) { + let _ = local_player_events.send(Event::KeepAlive(event.id)); + } + } +} + +pub fn disconnect_listener( + query: Query<&LocalPlayerEvents>, + mut events: MessageReader<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())); + } + } +} + +pub fn receive_chunk_listener( + query: Query<&LocalPlayerEvents>, + mut events: MessageReader<ReceiveChunkEvent>, +) { + for event in events.read() { + if let Ok(local_player_events) = query.get(event.entity) { + let _ = local_player_events.send(Event::ReceiveChunk(ChunkPos::new( + event.packet.x, + event.packet.z, + ))); + } + } +} |
