//! 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_client::join::ConnectionFailedEvent; use azalea_core::{entity_id::MinecraftEntityId, position::ChunkPos, tick::GameTick}; use azalea_entity::{Dead, InLoadedChunk}; use azalea_protocol::{ connect::ConnectionError, packets::game::c_player_combat_kill::ClientboundPlayerCombatKill, }; use azalea_world::WorldName; use bevy_app::{App, Plugin, PreUpdate, Update}; use bevy_ecs::prelude::*; use derive_more::{Deref, DerefMut}; use tokio::sync::mpsc; use crate::{ chunks::ReceiveChunkEvent, client_chat::{ChatPacket, ChatReceivedEvent}, 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. // 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), /// 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>), /// A `KeepAlive` packet was sent by the server. KeepAlive(u64), /// The client disconnected from the server. /// /// Also see [`Event::ConnectionFailed`]. Disconnect(Option), /// The initial connection to the server failed. /// /// Also see [`Event::Disconnect`], and the related ECS event /// [`ConnectionFailedEvent`]. ConnectionFailed(Arc), 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); 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, death_listener.after(azalea_client::packet::death_event_on_0_health), disconnect_listener, connection_failed_listener.after(azalea_client::join::poll_create_connection_task), receive_chunk_listener, ), ) .add_systems( PreUpdate, init_listener.before(super::connection::read_packets), ) .add_systems(GameTick, tick_listener) .add_observer(keepalive_listener); } } // when LocalPlayerEvents is added, it means the client just started pub fn init_listener(query: Query<&LocalPlayerEvents, Added>) { 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>, mut commands: Commands, ) { for (entity, local_player_events) in &query { let _ = local_player_events.send(Event::Login); commands.entity(entity).remove::(); } } /// 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, Without)>, 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, ) { 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>) { 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, ) { 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, ) { 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, ) { 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, ) { 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) { 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>) { for local_player_events in &query { local_player_events.send(Event::Death(None)).unwrap(); } } pub fn keepalive_listener(keep_alive: On, query: Query<&LocalPlayerEvents>) { if let Ok(local_player_events) = query.get(keep_alive.entity) { let _ = local_player_events.send(Event::KeepAlive(keep_alive.id)); } } pub fn disconnect_listener( query: Query<&LocalPlayerEvents>, mut events: MessageReader, ) { 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 connection_failed_listener( query: Query<&LocalPlayerEvents>, mut events: MessageReader, ) { for event in events.read() { if let Ok(local_player_events) = query.get(event.entity) { let _ = local_player_events.send(Event::ConnectionFailed(event.error.clone())); } } } pub fn receive_chunk_listener( query: Query<&LocalPlayerEvents>, mut events: MessageReader, ) { 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, ))); } } }