From 7ab3b8924f64f7eadb6b8928b6fae73cb06e4c2f Mon Sep 17 00:00:00 2001 From: mat Date: Sun, 28 Dec 2025 14:31:41 +0500 Subject: move Event and auto_reconnect to the azalea crate --- azalea-client/src/account.rs | 4 +- azalea-client/src/lib.rs | 1 - azalea-client/src/local_player.rs | 28 +-- azalea-client/src/plugins/auto_reconnect.rs | 145 ------------- azalea-client/src/plugins/events.rs | 317 ---------------------------- azalea-client/src/plugins/join.rs | 8 - azalea-client/src/plugins/mod.rs | 4 - azalea-client/src/plugins/packet/mod.rs | 4 +- azalea/src/auto_reconnect.rs | 134 ++++++++++++ azalea/src/bot.rs | 6 +- azalea/src/builder.rs | 2 +- azalea/src/client_impl/mod.rs | 12 +- azalea/src/client_impl/movement.rs | 2 +- azalea/src/events.rs | 316 +++++++++++++++++++++++++++ azalea/src/lib.rs | 9 +- azalea/src/prelude.rs | 4 +- azalea/src/swarm/builder.rs | 7 +- azalea/src/swarm/mod.rs | 10 +- 18 files changed, 485 insertions(+), 528 deletions(-) delete mode 100644 azalea-client/src/plugins/auto_reconnect.rs delete mode 100644 azalea-client/src/plugins/events.rs create mode 100644 azalea/src/auto_reconnect.rs create mode 100644 azalea/src/events.rs diff --git a/azalea-client/src/account.rs b/azalea-client/src/account.rs index f988ade9..4db19d0e 100644 --- a/azalea-client/src/account.rs +++ b/azalea-client/src/account.rs @@ -14,7 +14,7 @@ use uuid::Uuid; /// Something that can join Minecraft servers. /// -/// To join a server using this account, use [`Client::join`] or +/// To join a server using this account, use [`StartJoinServerEvent`] or /// [`azalea::ClientBuilder`]. /// /// This is also an ECS component that is present on our client entities. @@ -31,7 +31,7 @@ use uuid::Uuid; /// # } /// ``` /// -/// [`Client::join`]: crate::Client::join +/// [`StartJoinServerEvent`]: crate::join::StartJoinServerEvent /// [`azalea::ClientBuilder`]: https://docs.rs/azalea/latest/azalea/struct.ClientBuilder.html #[derive(Clone, Component, Debug)] pub struct Account { diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs index d9e66333..6bdc2713 100644 --- a/azalea-client/src/lib.rs +++ b/azalea-client/src/lib.rs @@ -22,6 +22,5 @@ pub use bevy_tasks; pub use client::{ InConfigState, InGameState, JoinedClientBundle, LocalPlayerBundle, start_ecs_runner, }; -pub use events::Event; pub use movement::{StartSprintEvent, StartWalkEvent}; pub use plugins::*; diff --git a/azalea-client/src/local_player.rs b/azalea-client/src/local_player.rs index 977b38f3..cc2a28dc 100644 --- a/azalea-client/src/local_player.rs +++ b/azalea-client/src/local_player.rs @@ -1,19 +1,13 @@ -use std::{ - collections::HashMap, - error, io, - sync::{Arc, PoisonError}, -}; +use std::{collections::HashMap, sync::Arc}; use azalea_core::game_type::GameMode; use azalea_world::{Instance, PartialInstance}; use bevy_ecs::{component::Component, prelude::*}; use derive_more::{Deref, DerefMut}; use parking_lot::RwLock; -use thiserror::Error; -use tokio::sync::mpsc; use uuid::Uuid; -use crate::{ClientInformation, events::Event as AzaleaEvent, player::PlayerInfo}; +use crate::{ClientInformation, player::PlayerInfo}; /// A component that keeps strong references to our [`PartialInstance`] and /// [`Instance`] for local players. @@ -145,21 +139,3 @@ impl InstanceHolder { self.partial_instance.write().reset(); } } - -#[derive(Debug, Error)] -pub enum HandlePacketError { - #[error("{0}")] - Poison(String), - #[error(transparent)] - Io(#[from] io::Error), - #[error(transparent)] - Other(#[from] Box), - #[error("{0}")] - Send(#[from] mpsc::error::SendError), -} - -impl From> for HandlePacketError { - fn from(e: PoisonError) -> Self { - HandlePacketError::Poison(e.to_string()) - } -} diff --git a/azalea-client/src/plugins/auto_reconnect.rs b/azalea-client/src/plugins/auto_reconnect.rs deleted file mode 100644 index 4851a4e7..00000000 --- a/azalea-client/src/plugins/auto_reconnect.rs +++ /dev/null @@ -1,145 +0,0 @@ -//! Auto-reconnect to the server when the client is kicked. -//! -//! See [`AutoReconnectPlugin`] for more information. - -use std::time::{Duration, Instant}; - -use bevy_app::prelude::*; -use bevy_ecs::prelude::*; - -use super::{ - disconnect::DisconnectEvent, - events::LocalPlayerEvents, - join::{ConnectOpts, ConnectionFailedEvent, StartJoinServerEvent}, -}; -use crate::Account; - -/// The default delay that Azalea will use for reconnecting our clients. -/// -/// See [`AutoReconnectPlugin`] for more information. -pub const DEFAULT_RECONNECT_DELAY: Duration = Duration::from_secs(5); - -/// A default plugin that makes clients automatically rejoin the server when -/// they're disconnected. -/// -/// The reconnect delay is configurable globally or per-client with the -/// [`AutoReconnectDelay`] resource/component. Auto reconnecting can be disabled -/// by removing the resource from the ECS. -/// -/// The delay defaults to [`DEFAULT_RECONNECT_DELAY`]. -pub struct AutoReconnectPlugin; -impl Plugin for AutoReconnectPlugin { - fn build(&self, app: &mut App) { - app.insert_resource(AutoReconnectDelay::new(DEFAULT_RECONNECT_DELAY)) - .add_systems( - Update, - (start_rejoin_on_disconnect, rejoin_after_delay) - .chain() - .before(super::join::handle_start_join_server_event), - ); - } -} - -pub fn start_rejoin_on_disconnect( - mut commands: Commands, - mut disconnect_events: MessageReader, - mut connection_failed_events: MessageReader, - auto_reconnect_delay_res: Option>, - auto_reconnect_delay_query: Query<&AutoReconnectDelay>, -) { - for entity in disconnect_events - .read() - .map(|e| e.entity) - .chain(connection_failed_events.read().map(|e| e.entity)) - { - let Some(delay) = get_delay( - &auto_reconnect_delay_res, - auto_reconnect_delay_query, - entity, - ) else { - // no auto reconnect - continue; - }; - - let reconnect_after = Instant::now() + delay; - commands.entity(entity).insert(InternalReconnectAfter { - instant: reconnect_after, - }); - } -} - -fn get_delay( - auto_reconnect_delay_res: &Option>, - auto_reconnect_delay_query: Query<&AutoReconnectDelay>, - entity: Entity, -) -> Option { - let delay = if let Ok(c) = auto_reconnect_delay_query.get(entity) { - Some(c.delay) - } else { - auto_reconnect_delay_res.as_ref().map(|r| r.delay) - }; - - if delay == Some(Duration::MAX) { - // if the duration is set to max, treat that as autoreconnect being disabled - return None; - } - delay -} - -pub fn rejoin_after_delay( - mut commands: Commands, - mut join_events: MessageWriter, - query: Query<( - Entity, - &InternalReconnectAfter, - &Account, - &ConnectOpts, - Option<&LocalPlayerEvents>, - )>, -) { - for (entity, reconnect_after, account, connect_opts, local_player_events) in query.iter() { - if Instant::now() >= reconnect_after.instant { - // don't keep trying to reconnect - commands.entity(entity).remove::(); - - // our Entity will be reused since the account has the same uuid - join_events.write(StartJoinServerEvent { - account: account.clone(), - connect_opts: connect_opts.clone(), - // not actually necessary since we're reusing the same entity and LocalPlayerEvents - // isn't removed, but this is more readable and just in case it's changed in the - // future - event_sender: local_player_events.map(|e| e.0.clone()), - start_join_callback_tx: None, - }); - } - } -} - -/// A resource *and* component that indicates how long to wait before -/// reconnecting when we're kicked. -/// -/// Initially, it's a resource in the ECS set to 5 seconds. You can modify -/// the resource to update the global reconnect delay, or insert it as a -/// component to set the individual delay for a single client. -/// -/// You can also remove this resource from the ECS to disable the default -/// auto-reconnecting behavior. Inserting the resource/component again will not -/// make clients that were already disconnected automatically reconnect. -#[derive(Clone, Component, Debug, Resource)] -pub struct AutoReconnectDelay { - pub delay: Duration, -} -impl AutoReconnectDelay { - pub fn new(delay: Duration) -> Self { - Self { delay } - } -} - -/// This is inserted when we're disconnected and indicates when we'll reconnect. -/// -/// This is set based on [`AutoReconnectDelay`]. -#[derive(Clone, Component, Debug)] -pub struct InternalReconnectAfter { - pub instant: Instant, -} diff --git a/azalea-client/src/plugins/events.rs b/azalea-client/src/plugins/events.rs deleted file mode 100644 index bd0419cb..00000000 --- a/azalea-client/src/plugins/events.rs +++ /dev/null @@ -1,317 +0,0 @@ -//! 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. -// 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_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), - /// 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. - Disconnect(Option), - 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`]. -/// -/// [`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, - keepalive_listener, - death_listener, - 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>) { - 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( - 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::KeepAlive(event.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 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, - ))); - } - } -} diff --git a/azalea-client/src/plugins/join.rs b/azalea-client/src/plugins/join.rs index b1759992..058f6c10 100644 --- a/azalea-client/src/plugins/join.rs +++ b/azalea-client/src/plugins/join.rs @@ -19,7 +19,6 @@ use parking_lot::RwLock; use tokio::sync::mpsc; use tracing::{debug, warn}; -use super::events::LocalPlayerEvents; use crate::{ Account, LocalPlayerBundle, connection::RawConnection, @@ -51,7 +50,6 @@ impl Plugin for JoinPlugin { pub struct StartJoinServerEvent { pub account: Account, pub connect_opts: ConnectOpts, - pub event_sender: Option>, // this is mpsc instead of oneshot so it can be cloned (since it's sent in an event) pub start_join_callback_tx: Option>, @@ -147,12 +145,6 @@ pub fn handle_start_join_server_event( // immediately when the connection is created )); - if let Some(event_sender) = &event.event_sender { - // this is optional so we don't leak memory in case the user doesn't want to - // handle receiving packets - entity_mut.insert(LocalPlayerEvents(event_sender.clone())); - } - let task_pool = IoTaskPool::get(); let connect_opts = event.connect_opts.clone(); let task = task_pool.spawn(async_compat::Compat::new( diff --git a/azalea-client/src/plugins/mod.rs b/azalea-client/src/plugins/mod.rs index a4aec19f..35e26f9e 100644 --- a/azalea-client/src/plugins/mod.rs +++ b/azalea-client/src/plugins/mod.rs @@ -1,7 +1,6 @@ use bevy_app::{PluginGroup, PluginGroupBuilder}; pub mod attack; -pub mod auto_reconnect; pub mod block_update; pub mod brand; pub mod chat; @@ -12,7 +11,6 @@ pub mod client_information; pub mod connection; pub mod cookies; pub mod disconnect; -pub mod events; pub mod interact; pub mod inventory; pub mod join; @@ -41,7 +39,6 @@ impl PluginGroup for DefaultPlugins { .add(crate::client::AzaleaPlugin) .add(azalea_entity::EntityPlugin) .add(azalea_physics::PhysicsPlugin) - .add(events::EventsPlugin) .add(task_pool::TaskPoolPlugin::default()) .add(inventory::InventoryPlugin) .add(chat::ChatPlugin) @@ -62,7 +59,6 @@ impl PluginGroup for DefaultPlugins { .add(connection::ConnectionPlugin) .add(login::LoginPlugin) .add(join::JoinPlugin) - .add(auto_reconnect::AutoReconnectPlugin) .add(cookies::CookiesPlugin); #[cfg(feature = "online-mode")] { diff --git a/azalea-client/src/plugins/packet/mod.rs b/azalea-client/src/plugins/packet/mod.rs index 30503d50..9d842dc6 100644 --- a/azalea-client/src/plugins/packet/mod.rs +++ b/azalea-client/src/plugins/packet/mod.rs @@ -6,7 +6,7 @@ use bevy_ecs::{ }; use self::game::DeathEvent; -use crate::{chat::ChatReceivedEvent, events::death_listener}; +use crate::chat::ChatReceivedEvent; pub mod config; pub mod game; @@ -33,7 +33,7 @@ impl Plugin for PacketPlugin { app.add_observer(game::handle_outgoing_packets_observer) .add_observer(config::handle_outgoing_packets_observer) .add_observer(login::handle_outgoing_packets_observer) - .add_systems(Update, death_event_on_0_health.before(death_listener)) + .add_systems(Update, death_event_on_0_health) .add_message::() .add_message::() .add_message::() diff --git a/azalea/src/auto_reconnect.rs b/azalea/src/auto_reconnect.rs new file mode 100644 index 00000000..dd353b7a --- /dev/null +++ b/azalea/src/auto_reconnect.rs @@ -0,0 +1,134 @@ +//! Auto-reconnect to the server when the client is kicked. +//! +//! See [`AutoReconnectPlugin`] for more information. + +use std::time::{Duration, Instant}; + +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; + +use super::{ + disconnect::DisconnectEvent, + join::{ConnectOpts, ConnectionFailedEvent, StartJoinServerEvent}, +}; +use crate::Account; + +/// The default delay that Azalea will use for reconnecting our clients. +/// +/// See [`AutoReconnectPlugin`] for more information. +pub const DEFAULT_RECONNECT_DELAY: Duration = Duration::from_secs(5); + +/// A default plugin that makes clients automatically rejoin the server when +/// they're disconnected. +/// +/// The reconnect delay is configurable globally or per-client with the +/// [`AutoReconnectDelay`] resource/component. Auto reconnecting can be disabled +/// by removing the resource from the ECS. +/// +/// The delay defaults to [`DEFAULT_RECONNECT_DELAY`]. +pub struct AutoReconnectPlugin; +impl Plugin for AutoReconnectPlugin { + fn build(&self, app: &mut App) { + app.insert_resource(AutoReconnectDelay::new(DEFAULT_RECONNECT_DELAY)) + .add_systems( + Update, + (start_rejoin_on_disconnect, rejoin_after_delay) + .chain() + .before(super::join::handle_start_join_server_event), + ); + } +} + +pub fn start_rejoin_on_disconnect( + mut commands: Commands, + mut disconnect_events: MessageReader, + mut connection_failed_events: MessageReader, + auto_reconnect_delay_res: Option>, + auto_reconnect_delay_query: Query<&AutoReconnectDelay>, +) { + for entity in disconnect_events + .read() + .map(|e| e.entity) + .chain(connection_failed_events.read().map(|e| e.entity)) + { + let Some(delay) = get_delay( + &auto_reconnect_delay_res, + auto_reconnect_delay_query, + entity, + ) else { + // no auto reconnect + continue; + }; + + let reconnect_after = Instant::now() + delay; + commands.entity(entity).insert(InternalReconnectAfter { + instant: reconnect_after, + }); + } +} + +fn get_delay( + auto_reconnect_delay_res: &Option>, + auto_reconnect_delay_query: Query<&AutoReconnectDelay>, + entity: Entity, +) -> Option { + let delay = if let Ok(c) = auto_reconnect_delay_query.get(entity) { + Some(c.delay) + } else { + auto_reconnect_delay_res.as_ref().map(|r| r.delay) + }; + + if delay == Some(Duration::MAX) { + // if the duration is set to max, treat that as autoreconnect being disabled + return None; + } + delay +} + +pub fn rejoin_after_delay( + mut commands: Commands, + mut join_events: MessageWriter, + query: Query<(Entity, &InternalReconnectAfter, &Account, &ConnectOpts)>, +) { + for (entity, reconnect_after, account, connect_opts) in query.iter() { + if Instant::now() >= reconnect_after.instant { + // don't keep trying to reconnect + commands.entity(entity).remove::(); + + // our Entity will be reused since the account has the same uuid + join_events.write(StartJoinServerEvent { + account: account.clone(), + connect_opts: connect_opts.clone(), + start_join_callback_tx: None, + }); + } + } +} + +/// A resource *and* component that indicates how long to wait before +/// reconnecting when we're kicked. +/// +/// Initially, it's a resource in the ECS set to 5 seconds. You can modify +/// the resource to update the global reconnect delay, or insert it as a +/// component to set the individual delay for a single client. +/// +/// You can also remove this resource from the ECS to disable the default +/// auto-reconnecting behavior. Inserting the resource/component again will not +/// make clients that were already disconnected automatically reconnect. +#[derive(Clone, Component, Debug, Resource)] +pub struct AutoReconnectDelay { + pub delay: Duration, +} +impl AutoReconnectDelay { + pub fn new(delay: Duration) -> Self { + Self { delay } + } +} + +/// This is inserted when we're disconnected and indicates when we'll reconnect. +/// +/// This is set based on [`AutoReconnectDelay`]. +#[derive(Clone, Component, Debug)] +pub struct InternalReconnectAfter { + pub instant: Instant, +} diff --git a/azalea/src/bot.rs b/azalea/src/bot.rs index 6f39c2fe..73b225b2 100644 --- a/azalea/src/bot.rs +++ b/azalea/src/bot.rs @@ -130,9 +130,7 @@ impl Client { /// Mine a block. /// /// This won't turn the bot's head towards the block, so if that's necessary - /// you'll have to do that yourself with [`look_at`]. - /// - /// [`look_at`]: crate::prelude::BotClientExt::look_at + /// you'll have to do that yourself with [`look_at`](Client::look_at). pub async fn mine(&self, position: BlockPos) { self.start_mining(position); @@ -211,5 +209,7 @@ impl PluginGroup for DefaultBotPlugins { .add(crate::auto_respawn::AutoRespawnPlugin) .add(crate::accept_resource_packs::AcceptResourcePacksPlugin) .add(crate::tick_broadcast::TickBroadcastPlugin) + .add(crate::events::EventsPlugin) + .add(crate::auto_reconnect::AutoReconnectPlugin) } } diff --git a/azalea/src/builder.rs b/azalea/src/builder.rs index 91fb9d5e..8151b3a1 100644 --- a/azalea/src/builder.rs +++ b/azalea/src/builder.rs @@ -150,7 +150,7 @@ where /// If this function isn't called, then our client will reconnect after /// [`DEFAULT_RECONNECT_DELAY`]. /// - /// [`DEFAULT_RECONNECT_DELAY`]: azalea_client::auto_reconnect::DEFAULT_RECONNECT_DELAY + /// [`DEFAULT_RECONNECT_DELAY`]: crate::auto_reconnect::DEFAULT_RECONNECT_DELAY #[must_use] pub fn reconnect_after(mut self, delay: impl Into>) -> Self { self.swarm.reconnect_after = delay.into(); diff --git a/azalea/src/client_impl/mod.rs b/azalea/src/client_impl/mod.rs index 2f5fbf7d..36995656 100644 --- a/azalea/src/client_impl/mod.rs +++ b/azalea/src/client_impl/mod.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::Arc}; use azalea_auth::game_profile::GameProfile; use azalea_client::{ - Account, DefaultPlugins, Event, + Account, DefaultPlugins, connection::RawConnection, disconnect::DisconnectEvent, join::{ConnectOpts, StartJoinServerEvent}, @@ -40,6 +40,8 @@ use parking_lot::{Mutex, RwLock}; use tokio::sync::mpsc; use uuid::Uuid; +use crate::events::{Event, LocalPlayerEvents}; + pub mod attack; pub mod chat; pub mod client_information; @@ -210,7 +212,6 @@ impl Client { ecs_lock.lock().write_message(StartJoinServerEvent { account, connect_opts, - event_sender, start_join_callback_tx: Some(start_join_callback_tx), }); @@ -218,6 +219,13 @@ impl Client { "start_join_callback should not be dropped before sending a message, this is a bug in Azalea", ); + if let Some(event_sender) = event_sender { + ecs_lock + .lock() + .entity_mut(entity) + .insert(LocalPlayerEvents(event_sender)); + } + Client::new(entity, ecs_lock) } diff --git a/azalea/src/client_impl/movement.rs b/azalea/src/client_impl/movement.rs index 08624263..b47da9a7 100644 --- a/azalea/src/client_impl/movement.rs +++ b/azalea/src/client_impl/movement.rs @@ -28,7 +28,7 @@ impl Client { /// Whether the client is currently trying to sneak. /// - /// You may want to check the [`Pose`] instead. + /// You may want to check the [`Pose`](azalea_entity::Pose) instead. pub fn crouching(&self) -> bool { self.query_self::<&PhysicsState, _>(|p| p.trying_to_crouch) } 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. +// 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. + Disconnect(Option), + 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, + 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>) { + 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( + 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::KeepAlive(event.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 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, + ))); + } + } +} diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs index 2105daee..351c956a 100644 --- a/azalea/src/lib.rs +++ b/azalea/src/lib.rs @@ -2,12 +2,14 @@ #![feature(type_changing_struct_update)] pub mod accept_resource_packs; +pub mod auto_reconnect; pub mod auto_respawn; pub mod auto_tool; pub mod bot; mod builder; mod client_impl; pub mod container; +pub mod events; mod join_opts; pub mod nearest_entity; pub mod pathfinder; @@ -51,11 +53,10 @@ pub use builder::ClientBuilder; use futures::future::BoxFuture; pub use join_opts::JoinOpts; -pub use crate::client_impl::Client; +pub use crate::{client_impl::Client, events::Event}; -pub type BoxHandleFn = - Box BoxFuture<'static, R> + Send>; -pub type HandleFn = fn(Client, azalea_client::Event, S) -> Fut; +pub type BoxHandleFn = Box BoxFuture<'static, R> + Send>; +pub type HandleFn = fn(Client, Event, S) -> Fut; /// A marker that can be used in place of a State in [`ClientBuilder`] or /// [`SwarmBuilder`]. diff --git a/azalea/src/prelude.rs b/azalea/src/prelude.rs index 2675c8f5..d39e50c4 100644 --- a/azalea/src/prelude.rs +++ b/azalea/src/prelude.rs @@ -1,14 +1,14 @@ //! The Azalea prelude. Things that are necessary for a bare-bones bot are //! re-exported here. -pub use azalea_client::{Account, Event}; +pub use azalea_client::Account; pub use azalea_core::tick::GameTick; pub use bevy_app::AppExit; // this is necessary to make the macros that reference bevy_ecs work pub use crate::ecs as bevy_ecs; pub use crate::{ - Client, ClientBuilder, + Client, ClientBuilder, Event, ecs::{component::Component, resource::Resource}, pathfinder::PathfinderClientExt, }; diff --git a/azalea/src/swarm/builder.rs b/azalea/src/swarm/builder.rs index 4eae7eab..860e47be 100644 --- a/azalea/src/swarm/builder.rs +++ b/azalea/src/swarm/builder.rs @@ -8,11 +8,7 @@ use std::{ time::Duration, }; -use azalea_client::{ - Account, DefaultPlugins, - auto_reconnect::{AutoReconnectDelay, DEFAULT_RECONNECT_DELAY}, - start_ecs_runner, -}; +use azalea_client::{Account, DefaultPlugins, start_ecs_runner}; use azalea_protocol::address::{ResolvableAddr, ResolvedAddr}; use azalea_world::InstanceContainer; use bevy_app::{App, AppExit, Plugins, SubApp}; @@ -24,6 +20,7 @@ use tracing::{debug, error, warn}; use crate::{ BoxHandleFn, HandleFn, JoinOpts, NoState, + auto_reconnect::{AutoReconnectDelay, DEFAULT_RECONNECT_DELAY}, bot::DefaultBotPlugins, swarm::{ BoxSwarmHandleFn, DefaultSwarmPlugins, NoSwarmState, Swarm, SwarmEvent, SwarmHandleFn, diff --git a/azalea/src/swarm/mod.rs b/azalea/src/swarm/mod.rs index c0f9cbca..48ac20a3 100644 --- a/azalea/src/swarm/mod.rs +++ b/azalea/src/swarm/mod.rs @@ -12,7 +12,7 @@ use std::sync::{ atomic::{self, AtomicBool}, }; -use azalea_client::{Account, Event, chat::ChatPacket, join::ConnectOpts}; +use azalea_client::{Account, chat::ChatPacket, join::ConnectOpts}; use azalea_entity::LocalEntity; use azalea_protocol::address::ResolvedAddr; use azalea_world::InstanceContainer; @@ -45,7 +45,7 @@ pub struct Swarm { pub instance_container: Arc>, /// This is used internally to make the client handler function work. - pub(crate) bots_tx: mpsc::UnboundedSender<(Option, Client)>, + pub(crate) bots_tx: mpsc::UnboundedSender<(Option, Client)>, /// This is used internally to make the swarm handler function work. pub(crate) swarm_tx: mpsc::UnboundedSender, } @@ -207,9 +207,9 @@ impl Swarm { /// Copy the events from a client's receiver into bots_tx, until the bot is /// removed from the ECS. async fn event_copying_task( - mut rx: mpsc::UnboundedReceiver, + mut rx: mpsc::UnboundedReceiver, swarm_tx: mpsc::UnboundedSender, - bots_tx: mpsc::UnboundedSender<(Option, Client)>, + bots_tx: mpsc::UnboundedSender<(Option, Client)>, bot: Client, join_opts: JoinOpts, ) { @@ -246,7 +246,7 @@ impl Swarm { } } - if let Event::Disconnect(_) = event { + if let crate::Event::Disconnect(_) = event { debug!( "Sending SwarmEvent::Disconnect due to receiving an Event::Disconnect from client {}", bot.entity -- cgit v1.2.3