diff options
Diffstat (limited to 'azalea-client/src')
| -rwxr-xr-x | azalea-client/src/account.rs | 2 | ||||
| -rwxr-xr-x | azalea-client/src/chat.rs | 25 | ||||
| -rw-r--r-- | azalea-client/src/client.rs | 1135 | ||||
| -rw-r--r-- | azalea-client/src/entity_query.rs | 100 | ||||
| -rw-r--r-- | azalea-client/src/events.rs | 189 | ||||
| -rw-r--r-- | azalea-client/src/lib.rs | 24 | ||||
| -rw-r--r-- | azalea-client/src/local_player.rs | 164 | ||||
| -rw-r--r-- | azalea-client/src/movement.rs | 415 | ||||
| -rw-r--r-- | azalea-client/src/packet_handling.rs | 935 | ||||
| -rwxr-xr-x | azalea-client/src/player.rs | 32 | ||||
| -rw-r--r-- | azalea-client/src/plugins.rs | 144 | ||||
| -rw-r--r-- | azalea-client/src/task_pool.rs | 177 |
12 files changed, 2125 insertions, 1217 deletions
diff --git a/azalea-client/src/account.rs b/azalea-client/src/account.rs index 79feb1a7..3c2c7d1b 100755 --- a/azalea-client/src/account.rs +++ b/azalea-client/src/account.rs @@ -28,7 +28,7 @@ pub struct Account { /// The access token for authentication. You can obtain one of these /// manually from azalea-auth. /// - /// This is an Arc<Mutex> so it can be modified by [`Self::refresh`]. + /// This is an `Arc<Mutex>` so it can be modified by [`Self::refresh`]. pub access_token: Option<Arc<Mutex<String>>>, /// Only required for online-mode accounts. pub uuid: Option<Uuid>, diff --git a/azalea-client/src/chat.rs b/azalea-client/src/chat.rs index 91dcf63e..3fa0ceec 100755 --- a/azalea-client/src/chat.rs +++ b/azalea-client/src/chat.rs @@ -1,7 +1,6 @@ //! Implementations of chat-related features. -use crate::Client; -use azalea_chat::Component; +use azalea_chat::FormattedText; use azalea_protocol::packets::game::{ clientbound_player_chat_packet::ClientboundPlayerChatPacket, clientbound_system_chat_packet::ClientboundSystemChatPacket, @@ -14,6 +13,8 @@ use std::{ }; use uuid::Uuid; +use crate::client::Client; + /// A chat packet, either a system message or a chat message. #[derive(Debug, Clone, PartialEq)] pub enum ChatPacket { @@ -30,7 +31,7 @@ macro_rules! regex { impl ChatPacket { /// Get the message shown in chat for this packet. - pub fn message(&self) -> Component { + pub fn message(&self) -> FormattedText { match self { ChatPacket::System(p) => p.content.clone(), ChatPacket::Player(p) => p.message(), @@ -94,7 +95,7 @@ impl ChatPacket { /// convenience function for testing. pub fn new(message: &str) -> Self { ChatPacket::System(Arc::new(ClientboundSystemChatPacket { - content: Component::from(message), + content: FormattedText::from(message), overlay: false, })) } @@ -105,7 +106,7 @@ impl Client { /// not the command packet. The [`Client::chat`] function handles checking /// whether the message is a command and using the proper packet for you, /// so you should use that instead. - pub async fn send_chat_packet(&self, message: &str) -> Result<(), std::io::Error> { + pub fn send_chat_packet(&self, message: &str) { // TODO: chat signing // let signature = sign_message(); let packet = ServerboundChatPacket { @@ -121,12 +122,12 @@ impl Client { last_seen_messages: LastSeenMessagesUpdate::default(), } .get(); - self.write_packet(packet).await + self.write_packet(packet); } /// Send a command packet to the server. The `command` argument should not /// include the slash at the front. - pub async fn send_command_packet(&self, command: &str) -> Result<(), std::io::Error> { + pub fn send_command_packet(&self, command: &str) { // TODO: chat signing let packet = ServerboundChatCommandPacket { command: command.to_string(), @@ -141,7 +142,7 @@ impl Client { last_seen_messages: LastSeenMessagesUpdate::default(), } .get(); - self.write_packet(packet).await + self.write_packet(packet); } /// Send a message in chat. @@ -149,15 +150,15 @@ impl Client { /// ```rust,no_run /// # use azalea_client::{Client, Event}; /// # async fn handle(bot: Client, event: Event) -> anyhow::Result<()> { - /// bot.chat("Hello, world!").await.unwrap(); + /// bot.chat("Hello, world!"); /// # Ok(()) /// # } /// ``` - pub async fn chat(&self, message: &str) -> Result<(), std::io::Error> { + pub fn chat(&self, message: &str) { if let Some(command) = message.strip_prefix('/') { - self.send_command_packet(command).await + self.send_command_packet(command); } else { - self.send_chat_packet(message).await + self.send_chat_packet(message); } } } diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index 125facda..bc1d8d62 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -1,18 +1,32 @@ pub use crate::chat::ChatPacket; -use crate::{movement::WalkDirection, plugins::PluginStates, Account, PlayerInfo}; +use crate::{ + events::{Event, EventPlugin, LocalPlayerEvents}, + local_player::{ + death_event, update_in_loaded_chunk, GameProfileComponent, LocalPlayer, PhysicsState, + }, + movement::{local_player_ai_step, send_position, sprint_listener, walk_listener}, + packet_handling::{self, PacketHandlerPlugin}, + player::retroactively_add_game_profile_component, + task_pool::TaskPoolPlugin, + Account, PlayerInfo, StartSprintEvent, StartWalkEvent, +}; + use azalea_auth::{game_profile::GameProfile, sessionserver::ClientSessionServerError}; -use azalea_chat::Component; -use azalea_core::{ChunkPos, ResourceLocation, Vec3}; +use azalea_chat::FormattedText; +use azalea_ecs::{ + app::{App, Plugin, PluginGroup, PluginGroupBuilder}, + component::Component, + entity::Entity, + schedule::{IntoSystemDescriptor, Schedule, Stage, SystemSet}, + AppTickExt, +}; +use azalea_ecs::{ecs::Ecs, TickPlugin}; +use azalea_physics::PhysicsPlugin; use azalea_protocol::{ - connect::{Connection, ConnectionError, ReadConnection, WriteConnection}, + connect::{Connection, ConnectionError}, packets::{ game::{ - clientbound_player_combat_kill_packet::ClientboundPlayerCombatKillPacket, - serverbound_accept_teleportation_packet::ServerboundAcceptTeleportationPacket, serverbound_client_information_packet::ServerboundClientInformationPacket, - serverbound_custom_payload_packet::ServerboundCustomPayloadPacket, - serverbound_keep_alive_packet::ServerboundKeepAlivePacket, - serverbound_move_player_pos_rot_packet::ServerboundMovePlayerPosRotPacket, ClientboundGamePacket, ServerboundGamePacket, }, handshake::{ @@ -26,109 +40,40 @@ use azalea_protocol::{ }, ConnectionProtocol, PROTOCOL_VERSION, }, - read::ReadPacketError, resolver, ServerAddress, }; -use azalea_world::{ - entity::{metadata, Entity, EntityData, EntityMetadata}, - PartialWorld, WeakWorld, WeakWorldContainer, -}; -use log::{debug, error, info, trace, warn}; +use azalea_world::{EntityPlugin, Local, PartialWorld, World, WorldContainer}; +use log::{debug, error}; use parking_lot::{Mutex, RwLock}; -use std::{ - any, - backtrace::Backtrace, - collections::HashMap, - fmt::Debug, - io::{self, Cursor}, - sync::Arc, -}; +use std::{collections::HashMap, fmt::Debug, io, net::SocketAddr, sync::Arc}; use thiserror::Error; -use tokio::{ - sync::mpsc::{self, Receiver, Sender}, - task::JoinHandle, - time::{self}, -}; +use tokio::{sync::mpsc, time}; use uuid::Uuid; pub type ClientInformation = ServerboundClientInformationPacket; -/// 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. - 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, - 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<ClientboundPlayerCombatKillPacket>>), -} - -/// A player that you control that is currently in a Minecraft server. +/// Client has the things that a user interacting with the library will want. +/// Things that a player in the world will want to know are in [`LocalPlayer`]. #[derive(Clone)] pub struct Client { + /// The [`GameProfile`] for our client. This contains your username, UUID, + /// and skin data. + /// + /// This is immutable; the server cannot change it. To get the username and + /// skin the server chose for you, get your player from + /// [`Self::players`]. pub profile: GameProfile, - pub read_conn: Arc<tokio::sync::Mutex<ReadConnection<ClientboundGamePacket>>>, - pub write_conn: Arc<tokio::sync::Mutex<WriteConnection<ServerboundGamePacket>>>, - pub entity_id: Arc<RwLock<u32>>, - /// The world that this client has access to. This supports shared worlds. + /// The entity for this client in the ECS. + pub entity: Entity, + /// The world that this client is in. pub world: Arc<RwLock<PartialWorld>>, - /// A container of world names to worlds. If we're not using a shared world - /// (i.e. not a swarm), then this will only contain data about the world - /// we're currently in. - world_container: Arc<RwLock<WeakWorldContainer>>, - pub world_name: Arc<RwLock<Option<ResourceLocation>>>, - pub physics_state: Arc<Mutex<PhysicsState>>, - pub client_information: Arc<RwLock<ClientInformation>>, - pub dead: Arc<Mutex<bool>>, - /// Plugins are a way for other crates to add custom functionality to the - /// client and keep state. If you're not making a plugin and you're using - /// the `azalea` crate. you can ignore this field. - pub plugins: Arc<PluginStates>, - /// A map of player uuids to their information in the tab list - pub players: Arc<RwLock<HashMap<Uuid, PlayerInfo>>>, - tasks: Arc<Mutex<Vec<JoinHandle<()>>>>, -} - -#[derive(Default)] -pub struct PhysicsState { - /// Minecraft only sends a movement packet either after 20 ticks or if the - /// player moved enough. This is that tick counter. - pub position_remainder: u32, - pub was_sprinting: bool, - // Whether we're going to try to start sprinting this tick. Equivalent to - // holding down ctrl for a tick. - pub trying_to_sprint: bool, - pub move_direction: WalkDirection, - pub forward_impulse: f32, - pub left_impulse: f32, + /// The entity component system. You probably don't need to access this + /// directly. Note that if you're using a shared world (i.e. a swarm), this + /// will contain all entities in all worlds. + pub ecs: Arc<Mutex<Ecs>>, } -/// Whether we should ignore errors when decoding packets. -const IGNORE_ERRORS: bool = !cfg!(debug_assertions); - /// An error that happened while joining the server. #[derive(Error, Debug)] pub enum JoinError { @@ -147,54 +92,21 @@ pub enum JoinError { #[error("Couldn't refresh access token: {0}")] Auth(#[from] azalea_auth::AuthError), #[error("Disconnected: {reason}")] - Disconnect { reason: Component }, -} - -#[derive(Error, Debug)] -pub enum HandleError { - #[error("{0}")] - Poison(String), - #[error(transparent)] - Io(#[from] io::Error), - #[error(transparent)] - Other(#[from] anyhow::Error), - #[error("{0}")] - Send(#[from] mpsc::error::SendError<Event>), + Disconnect { reason: FormattedText }, } impl Client { /// Create a new client from the given GameProfile, Connection, and World. /// You should only use this if you want to change these fields from the /// defaults, otherwise use [`Client::join`]. - pub fn new( - profile: GameProfile, - conn: Connection<ClientboundGamePacket, ServerboundGamePacket>, - world_container: Option<Arc<RwLock<WeakWorldContainer>>>, - ) -> Self { - let (read_conn, write_conn) = conn.into_split(); - let (read_conn, write_conn) = ( - Arc::new(tokio::sync::Mutex::new(read_conn)), - Arc::new(tokio::sync::Mutex::new(write_conn)), - ); - + pub fn new(profile: GameProfile, entity: Entity, ecs: Arc<Mutex<Ecs>>) -> Self { Self { profile, - read_conn, - write_conn, // default our id to 0, it'll be set later - entity_id: Arc::new(RwLock::new(0)), + entity, world: Arc::new(RwLock::new(PartialWorld::default())), - world_container: world_container - .unwrap_or_else(|| Arc::new(RwLock::new(WeakWorldContainer::new()))), - world_name: Arc::new(RwLock::new(None)), - physics_state: Arc::new(Mutex::new(PhysicsState::default())), - client_information: Arc::new(RwLock::new(ClientInformation::default())), - dead: Arc::new(Mutex::new(false)), - // The plugins can be modified by the user by replacing the plugins - // field right after this. No Mutex so the user doesn't need to .lock(). - plugins: Arc::new(PluginStates::default()), - players: Arc::new(RwLock::new(HashMap::new())), - tasks: Arc::new(Mutex::new(Vec::new())), + + ecs, } } @@ -213,34 +125,90 @@ impl Client { /// async fn main() -> Result<(), Box<dyn std::error::Error>> { /// let account = Account::offline("bot"); /// let (client, rx) = Client::join(&account, "localhost").await?; - /// client.chat("Hello, world!").await?; - /// client.disconnect().await?; + /// client.chat("Hello, world!"); + /// client.disconnect(); /// Ok(()) /// } /// ``` pub async fn join( account: &Account, address: impl TryInto<ServerAddress>, - ) -> Result<(Self, Receiver<Event>), JoinError> { + ) -> Result<(Self, mpsc::UnboundedReceiver<Event>), JoinError> { let address: ServerAddress = address.try_into().map_err(|_| JoinError::InvalidAddress)?; let resolved_address = resolver::resolve_address(&address).await?; - let conn = Connection::new(&resolved_address).await?; - let (conn, game_profile) = Self::handshake(conn, account, &address).await?; + // An event that causes the schedule to run. This is only used internally. + let (run_schedule_sender, run_schedule_receiver) = mpsc::channel(1); + let app = init_ecs_app(); + let ecs_lock = start_ecs(app, run_schedule_receiver, run_schedule_sender.clone()); + + Self::start_client( + ecs_lock, + account, + &address, + &resolved_address, + run_schedule_sender, + ) + .await + } + + /// Create a [`Client`] when you already have the ECS made with + /// [`start_ecs`]. You'd usually want to use [`Self::join`] instead. + pub async fn start_client( + ecs_lock: Arc<Mutex<Ecs>>, + account: &Account, + address: &ServerAddress, + resolved_address: &SocketAddr, + run_schedule_sender: mpsc::Sender<()>, + ) -> Result<(Self, mpsc::UnboundedReceiver<Event>), JoinError> { + let conn = Connection::new(resolved_address).await?; + let (conn, game_profile) = Self::handshake(conn, account, address).await?; + let (read_conn, write_conn) = conn.into_split(); - // The buffer has to be 1 to avoid a bug where if it lags events are - // received a bit later instead of the instant they were fired. - // That bug especially causes issues with the pathfinder. - let (tx, rx) = mpsc::channel(1); + let (tx, rx) = mpsc::unbounded_channel(); + + let mut ecs = ecs_lock.lock(); + + // Make the ecs entity for this client + let entity_mut = ecs.spawn_empty(); + let entity = entity_mut.id(); // we got the GameConnection, so the server is now connected :) - let client = Client::new(game_profile, conn, None); + let client = Client::new(game_profile.clone(), entity, ecs_lock.clone()); - tx.send(Event::Init).await.expect("Failed to send event"); + let (packet_writer_sender, packet_writer_receiver) = mpsc::unbounded_channel(); - // just start up the game loop and we're ready! + let mut local_player = crate::local_player::LocalPlayer::new( + entity, + packet_writer_sender, + // default to an empty world, it'll be set correctly later when we + // get the login packet + Arc::new(RwLock::new(World::default())), + ); - client.start_tasks(tx); + // start receiving packets + let packet_receiver = packet_handling::PacketReceiver { + packets: Arc::new(Mutex::new(Vec::new())), + run_schedule_sender: run_schedule_sender.clone(), + }; + + let read_packets_task = tokio::spawn(packet_receiver.clone().read_task(read_conn)); + let write_packets_task = tokio::spawn( + packet_receiver + .clone() + .write_task(write_conn, packet_writer_receiver), + ); + local_player.tasks.push(read_packets_task); + local_player.tasks.push(write_packets_task); + + ecs.entity_mut(entity).insert(( + local_player, + packet_receiver, + GameProfileComponent(game_profile), + PhysicsState::default(), + Local, + LocalPlayerEvents(tx), + )); Ok((client, rx)) } @@ -369,712 +337,61 @@ impl Client { } /// Write a packet directly to the server. - pub async fn write_packet(&self, packet: ServerboundGamePacket) -> Result<(), std::io::Error> { - self.write_conn.lock().await.write(packet).await?; - Ok(()) + pub fn write_packet(&self, packet: ServerboundGamePacket) { + self.local_player_mut(&mut self.ecs.lock()) + .write_packet(packet); } - /// Disconnect this client from the server, ending all tasks. - pub async fn disconnect(&self) -> Result<(), std::io::Error> { - if let Err(e) = self.write_conn.lock().await.shutdown().await { - warn!( - "Error shutting down connection, but it might be fine: {}", - e - ); - } - let tasks = self.tasks.lock(); - for task in tasks.iter() { - task.abort(); - } - Ok(()) + /// Disconnect this client from the server by ending all tasks. + /// + /// The OwnedReadHalf for the TCP connection is in one of the tasks, so it + /// automatically closes the connection when that's dropped. + pub fn disconnect(&self) { + self.local_player_mut(&mut self.ecs.lock()).disconnect(); } - /// Start the protocol and game tick loop. - #[doc(hidden)] - pub fn start_tasks(&self, tx: Sender<Event>) { - // if you get an error right here that means you're doing something with locks - // wrong read the error to see where the issue is - // you might be able to just drop the lock or put it in its own scope to fix - - let mut tasks = self.tasks.lock(); - tasks.push(tokio::spawn(Client::protocol_loop( - self.clone(), - tx.clone(), - ))); - tasks.push(tokio::spawn(Client::game_tick_loop(self.clone(), tx))); + pub fn local_player<'a>(&'a self, ecs: &'a mut Ecs) -> &'a LocalPlayer { + self.query::<&LocalPlayer>(ecs) } - - async fn protocol_loop(client: Client, tx: Sender<Event>) { - loop { - let r = client.read_conn.lock().await.read().await; - match r { - Ok(packet) => match Self::handle(&packet, &client, &tx).await { - Ok(_) => {} - Err(e) => { - error!("Error handling packet: {}", e); - if !IGNORE_ERRORS { - panic!("Error handling packet: {e}"); - } - } - }, - Err(e) => { - let e = *e; - if let ReadPacketError::ConnectionClosed = e { - info!("Connection closed"); - if let Err(e) = client.disconnect().await { - error!("Error shutting down connection: {:?}", e); - } - break; - } - let default_backtrace = Backtrace::capture(); - if IGNORE_ERRORS { - let backtrace = - any::request_ref::<Backtrace>(&e).unwrap_or(&default_backtrace); - warn!("{e}\n{backtrace}"); - match e { - ReadPacketError::FrameSplitter { .. } => panic!("Error: {e:?}"), - _ => continue, - } - } else { - let backtrace = - any::request_ref::<Backtrace>(&e).unwrap_or(&default_backtrace); - panic!("{e}\n{backtrace}") - } - } - }; - } + pub fn local_player_mut<'a>( + &'a self, + ecs: &'a mut Ecs, + ) -> azalea_ecs::ecs::Mut<'a, LocalPlayer> { + self.query::<&mut LocalPlayer>(ecs) } - async fn handle( - packet: &ClientboundGamePacket, - client: &Client, - tx: &Sender<Event>, - ) -> Result<(), HandleError> { - let packet = Arc::new(packet.clone()); - tx.send(Event::Packet(packet.clone())).await?; - match &*packet { - ClientboundGamePacket::Login(p) => { - debug!("Got login packet"); - - { - // // write p into login.txt - // std::io::Write::write_all( - // &mut std::fs::File::create("login.txt").unwrap(), - // format!("{:#?}", p).as_bytes(), - // ) - // .unwrap(); - - // TODO: have registry_holder be a struct because this sucks rn - // best way would be to add serde support to azalea-nbt - - let registry_holder = p - .registry_holder - .as_compound() - .expect("Registry holder is not a compound") - .get("") - .expect("No \"\" tag") - .as_compound() - .expect("\"\" tag is not a compound"); - let dimension_types = registry_holder - .get("minecraft:dimension_type") - .expect("No dimension_type tag") - .as_compound() - .expect("dimension_type is not a compound") - .get("value") - .expect("No dimension_type value") - .as_list() - .expect("dimension_type value is not a list"); - let dimension_type = dimension_types - .iter() - .find(|t| { - t.as_compound() - .expect("dimension_type value is not a compound") - .get("name") - .expect("No name tag") - .as_string() - .expect("name is not a string") - == p.dimension_type.to_string() - }) - .unwrap_or_else(|| { - panic!("No dimension_type with name {}", p.dimension_type) - }) - .as_compound() - .unwrap() - .get("element") - .expect("No element tag") - .as_compound() - .expect("element is not a compound"); - let height = (*dimension_type - .get("height") - .expect("No height tag") - .as_int() - .expect("height tag is not an int")) - .try_into() - .expect("height is not a u32"); - let min_y = *dimension_type - .get("min_y") - .expect("No min_y tag") - .as_int() - .expect("min_y tag is not an int"); - - let world_name = p.dimension.clone(); - - *client.world_name.write() = Some(world_name.clone()); - // add this world to the world_container (or don't if it's already there) - let weak_world = client - .world_container - .write() - .insert(world_name, height, min_y); - // set the loaded_world to an empty world - // (when we add chunks or entities those will be in the world_container) - let mut world_lock = client.world.write(); - *world_lock = PartialWorld::new( - client.client_information.read().view_distance.into(), - weak_world, - Some(p.player_id), - ); - - let entity = EntityData::new( - client.profile.uuid, - Vec3::default(), - EntityMetadata::Player(metadata::Player::default()), - ); - // make it so other entities don't update this entity in a shared world - world_lock.add_entity(p.player_id, entity); - - *client.entity_id.write() = p.player_id; - } - - // send the client information that we have set - let client_information_packet: ClientInformation = - client.client_information.read().clone(); - log::debug!( - "Sending client information because login: {:?}", - client_information_packet - ); - client.write_packet(client_information_packet.get()).await?; - - // brand - client - .write_packet( - ServerboundCustomPayloadPacket { - identifier: ResourceLocation::new("brand").unwrap(), - // they don't have to know :) - data: "vanilla".into(), - } - .get(), - ) - .await?; - - tx.send(Event::Login).await?; - } - ClientboundGamePacket::SetChunkCacheRadius(p) => { - debug!("Got set chunk cache radius packet {:?}", p); - } - ClientboundGamePacket::CustomPayload(p) => { - debug!("Got custom payload packet {:?}", p); - } - ClientboundGamePacket::ChangeDifficulty(p) => { - debug!("Got difficulty packet {:?}", p); - } - ClientboundGamePacket::Commands(_p) => { - debug!("Got declare commands packet"); - } - ClientboundGamePacket::PlayerAbilities(p) => { - debug!("Got player abilities packet {:?}", p); - } - ClientboundGamePacket::SetCarriedItem(p) => { - debug!("Got set carried item packet {:?}", p); - } - ClientboundGamePacket::UpdateTags(_p) => { - debug!("Got update tags packet"); - } - ClientboundGamePacket::Disconnect(p) => { - debug!("Got disconnect packet {:?}", p); - client.disconnect().await?; - } - ClientboundGamePacket::UpdateRecipes(_p) => { - debug!("Got update recipes packet"); - } - ClientboundGamePacket::EntityEvent(_p) => { - // debug!("Got entity event packet {:?}", p); - } - ClientboundGamePacket::Recipe(_p) => { - debug!("Got recipe packet"); - } - ClientboundGamePacket::PlayerPosition(p) => { - // TODO: reply with teleport confirm - debug!("Got player position packet {:?}", p); - - let (new_pos, y_rot, x_rot) = { - let player_entity_id = *client.entity_id.read(); - - let mut world_lock = client.world.write(); - - let mut player_entity = world_lock.entity_mut(player_entity_id).unwrap(); - - let delta_movement = player_entity.delta; - - let is_x_relative = p.relative_arguments.x; - let is_y_relative = p.relative_arguments.y; - let is_z_relative = p.relative_arguments.z; - - let (delta_x, new_pos_x) = if is_x_relative { - player_entity.last_pos.x += p.x; - (delta_movement.x, player_entity.pos().x + p.x) - } else { - player_entity.last_pos.x = p.x; - (0.0, p.x) - }; - let (delta_y, new_pos_y) = if is_y_relative { - player_entity.last_pos.y += p.y; - (delta_movement.y, player_entity.pos().y + p.y) - } else { - player_entity.last_pos.y = p.y; - (0.0, p.y) - }; - let (delta_z, new_pos_z) = if is_z_relative { - player_entity.last_pos.z += p.z; - (delta_movement.z, player_entity.pos().z + p.z) - } else { - player_entity.last_pos.z = p.z; - (0.0, p.z) - }; - - let mut y_rot = p.y_rot; - let mut x_rot = p.x_rot; - if p.relative_arguments.x_rot { - x_rot += player_entity.x_rot; - } - if p.relative_arguments.y_rot { - y_rot += player_entity.y_rot; - } - - player_entity.delta = Vec3 { - x: delta_x, - y: delta_y, - z: delta_z, - }; - player_entity.set_rotation(y_rot, x_rot); - // TODO: minecraft sets "xo", "yo", and "zo" here but idk what that means - // so investigate that ig - let new_pos = Vec3 { - x: new_pos_x, - y: new_pos_y, - z: new_pos_z, - }; - world_lock - .set_entity_pos(player_entity_id, new_pos) - .expect("The player entity should always exist"); - - (new_pos, y_rot, x_rot) - }; - - client - .write_packet(ServerboundAcceptTeleportationPacket { id: p.id }.get()) - .await?; - client - .write_packet( - ServerboundMovePlayerPosRotPacket { - x: new_pos.x, - y: new_pos.y, - z: new_pos.z, - y_rot, - x_rot, - // this is always false - on_ground: false, - } - .get(), - ) - .await?; - } - ClientboundGamePacket::PlayerInfoUpdate(p) => { - debug!("Got player info packet {:?}", p); - let mut events = Vec::new(); - { - let mut players_lock = client.players.write(); - for updated_info in &p.entries { - // add the new player maybe - if p.actions.add_player { - let player_info = PlayerInfo { - profile: updated_info.profile.clone(), - uuid: updated_info.profile.uuid, - gamemode: updated_info.game_mode, - latency: updated_info.latency, - display_name: updated_info.display_name.clone(), - }; - players_lock.insert(updated_info.profile.uuid, player_info.clone()); - events.push(Event::AddPlayer(player_info)); - } else if let Some(info) = players_lock.get_mut(&updated_info.profile.uuid) - { - // `else if` because the block for add_player above - // already sets all the fields - if p.actions.update_game_mode { - info.gamemode = updated_info.game_mode; - } - if p.actions.update_latency { - info.latency = updated_info.latency; - } - if p.actions.update_display_name { - info.display_name = updated_info.display_name.clone(); - } - events.push(Event::UpdatePlayer(info.clone())); - } else { - warn!( - "Ignoring PlayerInfoUpdate for unknown player {}", - updated_info.profile.uuid - ); - } - } - } - for event in events { - tx.send(event).await?; - } - } - ClientboundGamePacket::PlayerInfoRemove(p) => { - let mut events = Vec::new(); - { - let mut players_lock = client.players.write(); - for uuid in &p.profile_ids { - if let Some(info) = players_lock.remove(uuid) { - events.push(Event::RemovePlayer(info)); - } - } - } - for event in events { - tx.send(event).await?; - } - } - ClientboundGamePacket::SetChunkCacheCenter(p) => { - debug!("Got chunk cache center packet {:?}", p); - client - .world - .write() - .update_view_center(&ChunkPos::new(p.x, p.z)); - } - ClientboundGamePacket::LevelChunkWithLight(p) => { - // debug!("Got chunk with light packet {} {}", p.x, p.z); - let pos = ChunkPos::new(p.x, p.z); - - // OPTIMIZATION: if we already know about the chunk from the - // shared world (and not ourselves), then we don't need to - // parse it again. This is only used when we have a shared - // world, since we check that the chunk isn't currently owned - // by this client. - let shared_has_chunk = client.world.read().get_chunk(&pos).is_some(); - let this_client_has_chunk = client - .world - .read() - .chunk_storage - .limited_get(&pos) - .is_some(); - if shared_has_chunk && !this_client_has_chunk { - trace!( - "Skipping parsing chunk {:?} because we already know about it", - pos - ); - return Ok(()); - } - - // let chunk = Chunk::read_with_world_height(&mut p.chunk_data); - // debug("chunk {:?}") - if let Err(e) = client - .world - .write() - .replace_with_packet_data(&pos, &mut Cursor::new(&p.chunk_data.data)) - { - error!("Couldn't set chunk data: {}", e); - } - } - ClientboundGamePacket::LightUpdate(_p) => { - // debug!("Got light update packet {:?}", p); - } - ClientboundGamePacket::AddEntity(p) => { - debug!("Got add entity packet {:?}", p); - let entity = EntityData::from(p); - client.world.write().add_entity(p.id, entity); - } - ClientboundGamePacket::SetEntityData(p) => { - debug!("Got set entity data packet {:?}", p); - let mut world = client.world.write(); - if let Some(mut entity) = world.entity_mut(p.id) { - entity.apply_metadata(&p.packed_items.0); - } else { - // warn!("Server sent an entity data packet for an entity id - // ({}) that we don't know about", p.id); - } - } - ClientboundGamePacket::UpdateAttributes(_p) => { - // debug!("Got update attributes packet {:?}", p); - } - ClientboundGamePacket::SetEntityMotion(_p) => { - // debug!("Got entity velocity packet {:?}", p); - } - ClientboundGamePacket::SetEntityLink(p) => { - debug!("Got set entity link packet {:?}", p); - } - ClientboundGamePacket::AddPlayer(p) => { - debug!("Got add player packet {:?}", p); - let entity = EntityData::from(p); - client.world.write().add_entity(p.id, entity); - } - ClientboundGamePacket::InitializeBorder(p) => { - debug!("Got initialize border packet {:?}", p); - } - ClientboundGamePacket::SetTime(p) => { - debug!("Got set time packet {:?}", p); - } - ClientboundGamePacket::SetDefaultSpawnPosition(p) => { - debug!("Got set default spawn position packet {:?}", p); - } - ClientboundGamePacket::ContainerSetContent(p) => { - debug!("Got container set content packet {:?}", p); - } - ClientboundGamePacket::SetHealth(p) => { - debug!("Got set health packet {:?}", p); - if p.health == 0.0 { - // we can't define a variable here with client.dead.lock() - // because of https://github.com/rust-lang/rust/issues/57478 - if !*client.dead.lock() { - *client.dead.lock() = true; - tx.send(Event::Death(None)).await?; - } - } - } - ClientboundGamePacket::SetExperience(p) => { - debug!("Got set experience packet {:?}", p); - } - ClientboundGamePacket::TeleportEntity(p) => { - let mut world_lock = client.world.write(); - let _ = world_lock.set_entity_pos( - p.id, - Vec3 { - x: p.x, - y: p.y, - z: p.z, - }, - ); - } - ClientboundGamePacket::UpdateAdvancements(p) => { - debug!("Got update advancements packet {:?}", p); - } - ClientboundGamePacket::RotateHead(_p) => { - // debug!("Got rotate head packet {:?}", p); - } - ClientboundGamePacket::MoveEntityPos(p) => { - let mut world_lock = client.world.write(); - - let _ = world_lock.move_entity_with_delta(p.entity_id, &p.delta); - } - ClientboundGamePacket::MoveEntityPosRot(p) => { - let mut world_lock = client.world.write(); - - let _ = world_lock.move_entity_with_delta(p.entity_id, &p.delta); - } - ClientboundGamePacket::MoveEntityRot(_p) => { - // debug!("Got move entity rot packet {:?}", p); - } - ClientboundGamePacket::KeepAlive(p) => { - debug!("Got keep alive packet {:?}", p); - client - .write_packet(ServerboundKeepAlivePacket { id: p.id }.get()) - .await?; - } - ClientboundGamePacket::RemoveEntities(p) => { - debug!("Got remove entities packet {:?}", p); - } - ClientboundGamePacket::PlayerChat(p) => { - debug!("Got player chat packet {:?}", p); - tx.send(Event::Chat(ChatPacket::Player(Arc::new(p.clone())))) - .await?; - } - ClientboundGamePacket::SystemChat(p) => { - debug!("Got system chat packet {:?}", p); - tx.send(Event::Chat(ChatPacket::System(Arc::new(p.clone())))) - .await?; - } - ClientboundGamePacket::Sound(_p) => { - // debug!("Got sound packet {:?}", p); - } - ClientboundGamePacket::LevelEvent(p) => { - debug!("Got level event packet {:?}", p); - } - ClientboundGamePacket::BlockUpdate(p) => { - debug!("Got block update packet {:?}", p); - let mut world = client.world.write(); - world.set_block_state(&p.pos, p.block_state); - } - ClientboundGamePacket::Animate(p) => { - debug!("Got animate packet {:?}", p); - } - ClientboundGamePacket::SectionBlocksUpdate(p) => { - debug!("Got section blocks update packet {:?}", p); - let mut world = client.world.write(); - for state in &p.states { - world.set_block_state(&(p.section_pos + state.pos.clone()), state.state); - } - } - ClientboundGamePacket::GameEvent(p) => { - debug!("Got game event packet {:?}", p); - } - ClientboundGamePacket::LevelParticles(p) => { - debug!("Got level particles packet {:?}", p); - } - ClientboundGamePacket::ServerData(p) => { - debug!("Got server data packet {:?}", p); - } - ClientboundGamePacket::SetEquipment(p) => { - debug!("Got set equipment packet {:?}", p); - } - ClientboundGamePacket::UpdateMobEffect(p) => { - debug!("Got update mob effect packet {:?}", p); - } - ClientboundGamePacket::AddExperienceOrb(_) => {} - ClientboundGamePacket::AwardStats(_) => {} - ClientboundGamePacket::BlockChangedAck(_) => {} - ClientboundGamePacket::BlockDestruction(_) => {} - ClientboundGamePacket::BlockEntityData(_) => {} - ClientboundGamePacket::BlockEvent(_) => {} - ClientboundGamePacket::BossEvent(_) => {} - ClientboundGamePacket::CommandSuggestions(_) => {} - ClientboundGamePacket::ContainerSetData(_) => {} - ClientboundGamePacket::ContainerSetSlot(_) => {} - ClientboundGamePacket::Cooldown(_) => {} - ClientboundGamePacket::CustomChatCompletions(_) => {} - ClientboundGamePacket::DeleteChat(_) => {} - ClientboundGamePacket::Explode(_) => {} - ClientboundGamePacket::ForgetLevelChunk(_) => {} - ClientboundGamePacket::HorseScreenOpen(_) => {} - ClientboundGamePacket::MapItemData(_) => {} - ClientboundGamePacket::MerchantOffers(_) => {} - ClientboundGamePacket::MoveVehicle(_) => {} - ClientboundGamePacket::OpenBook(_) => {} - ClientboundGamePacket::OpenScreen(_) => {} - ClientboundGamePacket::OpenSignEditor(_) => {} - ClientboundGamePacket::Ping(_) => {} - ClientboundGamePacket::PlaceGhostRecipe(_) => {} - ClientboundGamePacket::PlayerCombatEnd(_) => {} - ClientboundGamePacket::PlayerCombatEnter(_) => {} - ClientboundGamePacket::PlayerCombatKill(p) => { - debug!("Got player kill packet {:?}", p); - if *client.entity_id.read() == p.player_id { - // we can't define a variable here with client.dead.lock() - // because of https://github.com/rust-lang/rust/issues/57478 - if !*client.dead.lock() { - *client.dead.lock() = true; - tx.send(Event::Death(Some(Arc::new(p.clone())))).await?; - } - } - } - ClientboundGamePacket::PlayerLookAt(_) => {} - ClientboundGamePacket::RemoveMobEffect(_) => {} - ClientboundGamePacket::ResourcePack(_) => {} - ClientboundGamePacket::Respawn(p) => { - debug!("Got respawn packet {:?}", p); - // Sets clients dead state to false. - let mut dead_lock = client.dead.lock(); - *dead_lock = false; - } - ClientboundGamePacket::SelectAdvancementsTab(_) => {} - ClientboundGamePacket::SetActionBarText(_) => {} - ClientboundGamePacket::SetBorderCenter(_) => {} - ClientboundGamePacket::SetBorderLerpSize(_) => {} - ClientboundGamePacket::SetBorderSize(_) => {} - ClientboundGamePacket::SetBorderWarningDelay(_) => {} - ClientboundGamePacket::SetBorderWarningDistance(_) => {} - ClientboundGamePacket::SetCamera(_) => {} - ClientboundGamePacket::SetDisplayObjective(_) => {} - ClientboundGamePacket::SetObjective(_) => {} - ClientboundGamePacket::SetPassengers(_) => {} - ClientboundGamePacket::SetPlayerTeam(_) => {} - ClientboundGamePacket::SetScore(_) => {} - ClientboundGamePacket::SetSimulationDistance(_) => {} - ClientboundGamePacket::SetSubtitleText(_) => {} - ClientboundGamePacket::SetTitleText(_) => {} - ClientboundGamePacket::SetTitlesAnimation(_) => {} - ClientboundGamePacket::SoundEntity(_) => {} - ClientboundGamePacket::StopSound(_) => {} - ClientboundGamePacket::TabList(_) => {} - ClientboundGamePacket::TagQuery(_) => {} - ClientboundGamePacket::TakeItemEntity(_) => {} - ClientboundGamePacket::DisguisedChat(_) => {} - ClientboundGamePacket::UpdateEnabledFeatures(_) => {} - ClientboundGamePacket::ContainerClose(_) => {} - } - - Ok(()) - } - - /// Runs game_tick every 50 milliseconds. - async fn game_tick_loop(mut client: Client, tx: Sender<Event>) { - let mut game_tick_interval = time::interval(time::Duration::from_millis(50)); - // TODO: Minecraft bursts up to 10 ticks and then skips, we should too - game_tick_interval.set_missed_tick_behavior(time::MissedTickBehavior::Burst); - loop { - game_tick_interval.tick().await; - Self::game_tick(&mut client, &tx).await; - } - } - - /// Runs every 50 milliseconds. - async fn game_tick(client: &mut Client, tx: &Sender<Event>) { - // return if there's no chunk at the player's position - - { - let world_lock = client.world(); - let player_entity_id = *client.entity_id.read(); - let player_entity = world_lock.entity(player_entity_id); - let Some(player_entity) = player_entity else { - return; - }; - let player_chunk_pos: ChunkPos = player_entity.pos().into(); - if world_lock.get_chunk(&player_chunk_pos).is_none() { - return; - } - } - - tx.send(Event::Tick) - .await - .expect("Sending tick event should never fail"); - - // TODO: if we're a passenger, send the required packets - - if let Err(e) = client.send_position().await { - warn!("Error sending position: {:?}", e); - } - client.ai_step(); - - // TODO: minecraft does ambient sounds here + /// Get a component from this client. This will clone the component and + /// return it. + pub fn component<T: Component + Clone>(&self) -> T { + self.query::<&T>(&mut self.ecs.lock()).clone() } /// Get a reference to our (potentially shared) world. /// - /// This gets the [`WeakWorld`] from our world container. If it's a normal + /// This gets the [`World`] from our world container. If it's a normal /// client, then it'll be the same as the world the client has loaded. /// If the client using a shared world, then the shared world will be a /// superset of the client's world. - pub fn world(&self) -> Arc<WeakWorld> { - self.world.read().shared.clone() - } - - /// Returns the entity associated to the player. - pub fn entity(&self) -> Entity<Arc<WeakWorld>> { - let entity_id = *self.entity_id.read(); + pub fn world(&self) -> Arc<RwLock<World>> { + let mut ecs = self.ecs.lock(); + + let world_name = { + let local_player = self.local_player(&mut ecs); + local_player + .world_name + .as_ref() + .expect("World name must be known if we're doing Client::world") + .clone() + }; - let world = self.world(); - let entity_data = world - .entity_storage - .read() - .get_by_id(entity_id) - .expect("Player entity should be in the given world"); - let entity_ptr = unsafe { entity_data.as_ptr() }; - Entity::new(world, entity_id, entity_ptr) + let world_container = ecs.resource::<WorldContainer>(); + world_container.get(&world_name).unwrap() } /// Returns whether we have a received the login packet yet. pub fn logged_in(&self) -> bool { // the login packet tells us the world name - self.world_name.read().is_some() + self.local_player(&mut self.ecs.lock()).world_name.is_some() } /// Tell the server we changed our game options (i.e. render distance, main @@ -1097,34 +414,156 @@ impl Client { client_information: ServerboundClientInformationPacket, ) -> Result<(), std::io::Error> { { - let mut client_information_lock = self.client_information.write(); - *client_information_lock = client_information; + self.local_player_mut(&mut self.ecs.lock()) + .client_information = client_information; } if self.logged_in() { - let client_information_packet = { - let client_information = self.client_information.read(); - client_information.clone().get() - }; + let client_information_packet = self + .local_player(&mut self.ecs.lock()) + .client_information + .clone() + .get(); log::debug!( "Sending client information (already logged in): {:?}", client_information_packet ); - self.write_packet(client_information_packet).await?; + self.write_packet(client_information_packet); } Ok(()) } - /// Get your player entity's metadata. You can use this to get your health, - /// xp score, and other useful information. - pub fn metadata(&self) -> metadata::Player { - self.entity().metadata.clone().into_player().unwrap() + /// Get a HashMap of all the players in the tab list. + pub fn players(&mut self) -> HashMap<Uuid, PlayerInfo> { + self.local_player(&mut self.ecs.lock()).players.clone() + } +} + +pub struct AzaleaPlugin; +impl Plugin for AzaleaPlugin { + fn build(&self, app: &mut App) { + app.add_event::<StartWalkEvent>() + .add_event::<StartSprintEvent>(); + + app.add_plugins(DefaultPlugins); + + app.add_tick_system_set( + SystemSet::new() + .with_system(send_position) + .with_system(update_in_loaded_chunk) + .with_system( + local_player_ai_step + .before("ai_step") + .after("sprint_listener"), + ), + ); + + // fire the Death event when the player dies. + app.add_system(death_event.after("tick").after("packet")); + + // walk and sprint event listeners + app.add_system(walk_listener.label("walk_listener").before("travel")) + .add_system( + sprint_listener + .label("sprint_listener") + .before("travel") + .before("walk_listener"), + ); + + // add GameProfileComponent when we get an AddPlayerEvent + app.add_system( + retroactively_add_game_profile_component + .after("tick") + .after("packet"), + ); + + app.init_resource::<WorldContainer>(); + } +} + +/// Create the [`App`]. This won't actually run anything yet. +/// +/// Note that you usually only need this if you're creating a client manually, +/// otherwise use [`Client::join`]. +/// +/// Use [`start_ecs`] to actually start running the app and then +/// [`Client::start_client`] to add a client to the ECS and make it join a +/// server. +#[doc(hidden)] +pub fn init_ecs_app() -> App { + // if you get an error right here that means you're doing something with locks + // wrong read the error to see where the issue is + // you might be able to just drop the lock or put it in its own scope to fix + + let mut app = App::new(); + app.add_plugin(AzaleaPlugin); + app +} + +/// Start running the ECS loop! You must create your `App` from [`init_ecs_app`] +/// first. +#[doc(hidden)] +pub fn start_ecs( + app: App, + run_schedule_receiver: mpsc::Receiver<()>, + run_schedule_sender: mpsc::Sender<()>, +) -> Arc<Mutex<Ecs>> { + // all resources should have been added by now so we can take the ecs from the + // app + let ecs = Arc::new(Mutex::new(app.world)); + + tokio::spawn(run_schedule_loop( + ecs.clone(), + app.schedule, + run_schedule_receiver, + )); + tokio::spawn(tick_run_schedule_loop(run_schedule_sender)); + + ecs +} + +async fn run_schedule_loop( + ecs: Arc<Mutex<Ecs>>, + mut schedule: Schedule, + mut run_schedule_receiver: mpsc::Receiver<()>, +) { + loop { + // whenever we get an event from run_schedule_receiver, run the schedule + run_schedule_receiver.recv().await; + schedule.run(&mut ecs.lock()); + } +} + +/// Send an event to run the schedule every 50 milliseconds. It will stop when +/// the receiver is dropped. +pub async fn tick_run_schedule_loop(run_schedule_sender: mpsc::Sender<()>) { + let mut game_tick_interval = time::interval(time::Duration::from_millis(50)); + // TODO: Minecraft bursts up to 10 ticks and then skips, we should too + game_tick_interval.set_missed_tick_behavior(time::MissedTickBehavior::Burst); + + loop { + game_tick_interval.tick().await; + if let Err(e) = run_schedule_sender.send(()).await { + println!("tick_run_schedule_loop error: {e}"); + // the sender is closed so end the task + return; + } } } -impl<T> From<std::sync::PoisonError<T>> for HandleError { - fn from(e: std::sync::PoisonError<T>) -> Self { - HandleError::Poison(e.to_string()) +/// This plugin group will add all the default plugins necessary for Azalea to +/// work. +pub struct DefaultPlugins; + +impl PluginGroup for DefaultPlugins { + fn build(self) -> PluginGroupBuilder { + PluginGroupBuilder::start::<Self>() + .add(TickPlugin::default()) + .add(PacketHandlerPlugin) + .add(EntityPlugin) + .add(PhysicsPlugin) + .add(EventPlugin) + .add(TaskPoolPlugin::default()) } } diff --git a/azalea-client/src/entity_query.rs b/azalea-client/src/entity_query.rs new file mode 100644 index 00000000..0e486741 --- /dev/null +++ b/azalea-client/src/entity_query.rs @@ -0,0 +1,100 @@ +use std::sync::Arc; + +use azalea_ecs::{ + component::Component, + ecs::Ecs, + entity::Entity, + query::{ROQueryItem, ReadOnlyWorldQuery, WorldQuery}, +}; +use parking_lot::Mutex; + +use crate::Client; + +impl Client { + /// A convenience function for getting components of our player's entity. + pub fn query<'w, Q: WorldQuery>(&self, ecs: &'w mut Ecs) -> <Q as WorldQuery>::Item<'w> { + ecs.query::<Q>() + .get_mut(ecs, self.entity) + .expect("Our client is missing a required component.") + } + + /// Return a lightweight [`Entity`] for the entity that matches the given + /// predicate function. + /// + /// You can then use [`Self::entity_component`] to get components from this + /// entity. + /// + /// # Example + /// Note that this will very likely change in the future. + /// ``` + /// use azalea_client::{Client, GameProfileComponent}; + /// use azalea_ecs::query::With; + /// use azalea_world::entity::{Position, metadata::Player}; + /// + /// # fn example(mut bot: Client, sender_name: String) { + /// let entity = bot.entity_by::<With<Player>, (&GameProfileComponent,)>( + /// |profile: &&GameProfileComponent| profile.name == sender_name, + /// ); + /// if let Some(entity) = entity { + /// let position = bot.entity_component::<Position>(entity); + /// // ... + /// } + /// # } + /// ``` + pub fn entity_by<F: ReadOnlyWorldQuery, Q: ReadOnlyWorldQuery>( + &mut self, + predicate: impl EntityPredicate<Q, F>, + ) -> Option<Entity> { + predicate.find(self.ecs.clone()) + } + + /// Get a component from an entity. Note that this will return an owned type + /// (i.e. not a reference) so it may be expensive for larger types. + /// + /// If you're trying to get a component for this client, use + /// [`Self::component`]. + pub fn entity_component<Q: Component + Clone>(&mut self, entity: Entity) -> Q { + let mut ecs = self.ecs.lock(); + let mut q = ecs.query::<&Q>(); + let components = q + .get(&ecs, entity) + .expect("Entity components must be present in Client::entity)components."); + components.clone() + } +} + +pub trait EntityPredicate<Q: ReadOnlyWorldQuery, Filter: ReadOnlyWorldQuery> { + fn find(&self, ecs_lock: Arc<Mutex<Ecs>>) -> Option<Entity>; +} +impl<F, Q, Filter> EntityPredicate<(Q,), Filter> for F +where + F: Fn(&ROQueryItem<Q>) -> bool, + Q: ReadOnlyWorldQuery, + Filter: ReadOnlyWorldQuery, +{ + fn find(&self, ecs_lock: Arc<Mutex<Ecs>>) -> Option<Entity> { + let mut ecs = ecs_lock.lock(); + let mut query = ecs.query_filtered::<(Entity, Q), Filter>(); + let entity = query.iter(&ecs).find(|(_, q)| (self)(q)).map(|(e, _)| e); + + entity + } +} + +// impl<'a, F, Q1, Q2> EntityPredicate<'a, (Q1, Q2)> for F +// where +// F: Fn(&<Q1 as WorldQuery>::Item<'_>, &<Q2 as WorldQuery>::Item<'_>) -> +// bool, Q1: ReadOnlyWorldQuery, +// Q2: ReadOnlyWorldQuery, +// { +// fn find(&self, ecs: &mut Ecs) -> Option<Entity> { +// // (self)(query) +// let mut query = ecs.query_filtered::<(Entity, Q1, Q2), ()>(); +// let entity = query +// .iter(ecs) +// .find(|(_, q1, q2)| (self)(q1, q2)) +// .map(|(e, _, _)| e); + +// entity +// } +// } diff --git a/azalea-client/src/events.rs b/azalea-client/src/events.rs new file mode 100644 index 00000000..f8b9f434 --- /dev/null +++ b/azalea-client/src/events.rs @@ -0,0 +1,189 @@ +//! Defines the [`Event`] enum and makes those events trigger when they're sent +//! in the ECS. + +use std::sync::Arc; + +use azalea_ecs::{ + app::{App, Plugin}, + component::Component, + event::EventReader, + query::{Added, Changed}, + system::Query, + AppTickExt, +}; +use azalea_protocol::packets::game::{ + clientbound_player_combat_kill_packet::ClientboundPlayerCombatKillPacket, ClientboundGamePacket, +}; +use azalea_world::entity::MinecraftEntityId; +use derive_more::{Deref, DerefMut}; +use tokio::sync::mpsc; + +use crate::{ + packet_handling::{ + AddPlayerEvent, ChatReceivedEvent, DeathEvent, PacketReceiver, RemovePlayerEvent, + UpdatePlayerEvent, + }, + ChatPacket, PlayerInfo, +}; + +// (for contributors): +// HOW TO ADD A NEW (packet based) EVENT: +// - make a struct that contains an entity field and a data field (look in +// packet_handling.rs for examples, also you should end the struct name with +// "Event") +// - the entity field is the local player entity that's receiving the event +// - in packet_handling, you always have a variable called player_entity that +// you can use +// - add the event struct in the `impl Plugin for PacketHandlerPlugin` +// - to get the event writer, you have to get an +// EventWriter<SomethingHappenedEvent> from the SystemState (the convention is +// to end your variable with the word "events", like "something_events") +// +// - then here in this file, add it to the Event enum +// - and make an event listener system/function like the other ones and put the +// function in the `impl Plugin for EventPlugin` + +/// 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. + 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, + 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<ClientboundPlayerCombatKillPacket>>), +} + +/// 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 EventPlugin; +impl Plugin for EventPlugin { + fn build(&self, app: &mut App) { + app.add_system(chat_listener) + .add_system(login_listener) + .add_system(init_listener) + .add_system(packet_listener) + .add_system(add_player_listener) + .add_system(update_player_listener) + .add_system(remove_player_listener) + .add_system(death_listener) + .add_tick_system(tick_listener); + } +} + +// when LocalPlayerEvents is added, it means the client just started +fn init_listener(query: Query<&LocalPlayerEvents, Added<LocalPlayerEvents>>) { + for local_player_events in &query { + local_player_events.send(Event::Init).unwrap(); + } +} + +// when MinecraftEntityId is added, it means the player is now in the world +fn login_listener(query: Query<&LocalPlayerEvents, Added<MinecraftEntityId>>) { + for local_player_events in &query { + local_player_events.send(Event::Login).unwrap(); + } +} + +fn chat_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader<ChatReceivedEvent>) { + for event in events.iter() { + let local_player_events = query + .get(event.entity) + .expect("Non-localplayer entities shouldn't be able to receive chat events"); + local_player_events + .send(Event::Chat(event.packet.clone())) + .unwrap(); + } +} + +fn tick_listener(query: Query<&LocalPlayerEvents>) { + for local_player_events in &query { + local_player_events.send(Event::Tick).unwrap(); + } +} + +fn packet_listener(query: Query<(&LocalPlayerEvents, &PacketReceiver), Changed<PacketReceiver>>) { + for (local_player_events, packet_receiver) in &query { + for packet in packet_receiver.packets.lock().iter() { + local_player_events + .send(Event::Packet(packet.clone().into())) + .unwrap(); + } + } +} + +fn add_player_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader<AddPlayerEvent>) { + for event in events.iter() { + let local_player_events = query + .get(event.entity) + .expect("Non-localplayer entities shouldn't be able to receive add player events"); + local_player_events + .send(Event::AddPlayer(event.info.clone())) + .unwrap(); + } +} + +fn update_player_listener( + query: Query<&LocalPlayerEvents>, + mut events: EventReader<UpdatePlayerEvent>, +) { + for event in events.iter() { + let local_player_events = query + .get(event.entity) + .expect("Non-localplayer entities shouldn't be able to receive add player events"); + local_player_events + .send(Event::UpdatePlayer(event.info.clone())) + .unwrap(); + } +} + +fn remove_player_listener( + query: Query<&LocalPlayerEvents>, + mut events: EventReader<RemovePlayerEvent>, +) { + for event in events.iter() { + let local_player_events = query + .get(event.entity) + .expect("Non-localplayer entities shouldn't be able to receive add player events"); + local_player_events + .send(Event::RemovePlayer(event.info.clone())) + .unwrap(); + } +} + +fn death_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader<DeathEvent>) { + for event in events.iter() { + if let Ok(local_player_events) = query.get(event.entity) { + local_player_events + .send(Event::Death(event.packet.clone().map(|p| p.into()))) + .unwrap(); + } + } +} diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs index f2952248..d46516be 100644 --- a/azalea-client/src/lib.rs +++ b/azalea-client/src/lib.rs @@ -9,27 +9,25 @@ #![allow(incomplete_features)] #![feature(trait_upcasting)] #![feature(error_generic_member_access)] +#![feature(type_alias_impl_trait)] mod account; mod chat; mod client; +mod entity_query; +mod events; mod get_mc_dir; +mod local_player; mod movement; +pub mod packet_handling; pub mod ping; mod player; -mod plugins; +mod task_pool; pub use account::Account; -pub use client::{ChatPacket, Client, ClientInformation, Event, JoinError, PhysicsState}; -pub use movement::{SprintDirection, WalkDirection}; +pub use azalea_ecs as ecs; +pub use client::{init_ecs_app, start_ecs, ChatPacket, Client, ClientInformation, JoinError}; +pub use events::Event; +pub use local_player::{GameProfileComponent, LocalPlayer}; +pub use movement::{SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection}; pub use player::PlayerInfo; -pub use plugins::{Plugin, PluginState, PluginStates, Plugins}; - -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - let result = 2 + 2; - assert_eq!(result, 4); - } -} diff --git a/azalea-client/src/local_player.rs b/azalea-client/src/local_player.rs new file mode 100644 index 00000000..0165a5f5 --- /dev/null +++ b/azalea-client/src/local_player.rs @@ -0,0 +1,164 @@ +use std::{collections::HashMap, io, sync::Arc}; + +use azalea_auth::game_profile::GameProfile; +use azalea_core::{ChunkPos, ResourceLocation}; +use azalea_ecs::component::Component; +use azalea_ecs::entity::Entity; +use azalea_ecs::{query::Added, system::Query}; +use azalea_protocol::packets::game::ServerboundGamePacket; +use azalea_world::{ + entity::{self, Dead}, + PartialWorld, World, +}; +use derive_more::{Deref, DerefMut}; +use parking_lot::RwLock; +use thiserror::Error; +use tokio::{sync::mpsc, task::JoinHandle}; +use uuid::Uuid; + +use crate::{ + events::{Event, LocalPlayerEvents}, + ClientInformation, PlayerInfo, WalkDirection, +}; + +/// A player that you control that is currently in a Minecraft server. +#[derive(Component)] +pub struct LocalPlayer { + pub packet_writer: mpsc::UnboundedSender<ServerboundGamePacket>, + + pub client_information: ClientInformation, + /// A map of player uuids to their information in the tab list + pub players: HashMap<Uuid, PlayerInfo>, + + /// The partial world is the world this client currently has loaded. It has + /// a limited render distance. + pub partial_world: Arc<RwLock<PartialWorld>>, + /// The world is the combined [`PartialWorld`]s of all clients in the same + /// world. (Only relevant if you're using a shared world, i.e. a swarm) + pub world: Arc<RwLock<World>>, + pub world_name: Option<ResourceLocation>, + + /// A list of async tasks that are running and will stop running when this + /// LocalPlayer is dropped or disconnected with [`Self::disconnect`] + pub(crate) tasks: Vec<JoinHandle<()>>, +} + +/// Component for entities that can move and sprint. Usually only in +/// [`LocalPlayer`] entities. +#[derive(Default, Component)] +pub struct PhysicsState { + /// Minecraft only sends a movement packet either after 20 ticks or if the + /// player moved enough. This is that tick counter. + pub position_remainder: u32, + pub was_sprinting: bool, + // Whether we're going to try to start sprinting this tick. Equivalent to + // holding down ctrl for a tick. + pub trying_to_sprint: bool, + + pub move_direction: WalkDirection, + pub forward_impulse: f32, + pub left_impulse: f32, +} + +/// A component only present in players that contains the [`GameProfile`] (which +/// you can use to get a player's name). +/// +/// Note that it's possible for this to be missing in a player if the server +/// never sent the player info for them (though this is uncommon). +#[derive(Component, Clone, Debug, Deref, DerefMut)] +pub struct GameProfileComponent(pub GameProfile); + +/// Marks a [`LocalPlayer`] that's in a loaded chunk. This is updated at the +/// beginning of every tick. +#[derive(Component)] +pub struct LocalPlayerInLoadedChunk; + +impl LocalPlayer { + /// Create a new `LocalPlayer`. + pub fn new( + entity: Entity, + packet_writer: mpsc::UnboundedSender<ServerboundGamePacket>, + world: Arc<RwLock<World>>, + ) -> Self { + let client_information = ClientInformation::default(); + + LocalPlayer { + packet_writer, + + client_information: ClientInformation::default(), + players: HashMap::new(), + + world, + partial_world: Arc::new(RwLock::new(PartialWorld::new( + client_information.view_distance.into(), + Some(entity), + ))), + world_name: None, + + tasks: Vec::new(), + } + } + + /// Spawn a task to write a packet directly to the server. + pub fn write_packet(&mut self, packet: ServerboundGamePacket) { + self.packet_writer + .send(packet) + .expect("write_packet shouldn't be able to be called if the connection is closed"); + } + + /// Disconnect this client from the server by ending all tasks. + /// + /// The OwnedReadHalf for the TCP connection is in one of the tasks, so it + /// automatically closes the connection when that's dropped. + pub fn disconnect(&self) { + for task in &self.tasks { + task.abort(); + } + } +} + +/// Update the [`LocalPlayerInLoadedChunk`] component for all [`LocalPlayer`]s. +pub fn update_in_loaded_chunk( + mut commands: azalea_ecs::system::Commands, + query: Query<(Entity, &LocalPlayer, &entity::Position)>, +) { + for (entity, local_player, position) in &query { + let player_chunk_pos = ChunkPos::from(position); + let in_loaded_chunk = local_player + .world + .read() + .chunks + .get(&player_chunk_pos) + .is_some(); + if in_loaded_chunk { + commands.entity(entity).insert(LocalPlayerInLoadedChunk); + } else { + commands.entity(entity).remove::<LocalPlayerInLoadedChunk>(); + } + } +} + +/// Send the "Death" event for [`LocalPlayer`]s that died with no reason. +pub fn death_event(query: Query<&LocalPlayerEvents, Added<Dead>>) { + for local_player_events in &query { + local_player_events.send(Event::Death(None)).unwrap(); + } +} + +#[derive(Error, Debug)] +pub enum HandlePacketError { + #[error("{0}")] + Poison(String), + #[error(transparent)] + Io(#[from] io::Error), + #[error(transparent)] + Other(#[from] anyhow::Error), + #[error("{0}")] + Send(#[from] mpsc::error::SendError<Event>), +} + +impl<T> From<std::sync::PoisonError<T>> for HandlePacketError { + fn from(e: std::sync::PoisonError<T>) -> Self { + HandlePacketError::Poison(e.to_string()) + } +} diff --git a/azalea-client/src/movement.rs b/azalea-client/src/movement.rs index d9cab1d4..8d6faabe 100644 --- a/azalea-client/src/movement.rs +++ b/azalea-client/src/movement.rs @@ -1,9 +1,7 @@ -use std::backtrace::Backtrace; - -use crate::Client; -use azalea_core::Vec3; -use azalea_physics::collision::{MovableEntity, MoverType}; -use azalea_physics::HasPhysics; +use crate::client::Client; +use crate::local_player::{LocalPlayer, LocalPlayerInLoadedChunk, PhysicsState}; +use azalea_ecs::entity::Entity; +use azalea_ecs::{event::EventReader, query::With, system::Query}; use azalea_protocol::packets::game::serverbound_player_command_packet::ServerboundPlayerCommandPacket; use azalea_protocol::packets::game::{ serverbound_move_player_pos_packet::ServerboundMovePlayerPosPacket, @@ -11,7 +9,11 @@ use azalea_protocol::packets::game::{ serverbound_move_player_rot_packet::ServerboundMovePlayerRotPacket, serverbound_move_player_status_only_packet::ServerboundMovePlayerStatusOnlyPacket, }; -use azalea_world::MoveEntityError; +use azalea_world::{ + entity::{self, metadata::Sprinting, Attributes, Jumping, MinecraftEntityId}, + MoveEntityError, +}; +use std::backtrace::Backtrace; use thiserror::Error; #[derive(Error, Debug)] @@ -33,24 +35,72 @@ impl From<MoveEntityError> for MovePlayerError { } impl Client { - /// This gets called automatically every tick. - pub(crate) async fn send_position(&mut self) -> Result<(), MovePlayerError> { + /// Set whether we're jumping. This acts as if you held space in + /// vanilla. If you want to jump once, use the `jump` function. + /// + /// If you're making a realistic client, calling this function every tick is + /// recommended. + pub fn set_jumping(&mut self, jumping: bool) { + let mut ecs = self.ecs.lock(); + let mut jumping_mut = self.query::<&mut Jumping>(&mut ecs); + **jumping_mut = jumping; + } + + /// Returns whether the player will try to jump next tick. + pub fn jumping(&self) -> bool { + let mut ecs = self.ecs.lock(); + let jumping_ref = self.query::<&Jumping>(&mut ecs); + **jumping_ref + } + + /// Sets your rotation. `y_rot` is yaw (looking to the side), `x_rot` is + /// pitch (looking up and down). You can get these numbers from the vanilla + /// f3 screen. + /// `y_rot` goes from -180 to 180, and `x_rot` goes from -90 to 90. + pub fn set_rotation(&mut self, y_rot: f32, x_rot: f32) { + let mut ecs = self.ecs.lock(); + let mut physics = self.query::<&mut entity::Physics>(&mut ecs); + + entity::set_rotation(&mut physics, y_rot, x_rot); + } +} + +#[allow(clippy::type_complexity)] +pub(crate) fn send_position( + mut query: Query< + ( + &MinecraftEntityId, + &mut LocalPlayer, + &mut PhysicsState, + &entity::Position, + &mut entity::LastSentPosition, + &mut entity::Physics, + &entity::metadata::Sprinting, + ), + &LocalPlayerInLoadedChunk, + >, +) { + for ( + id, + mut local_player, + mut physics_state, + position, + mut last_sent_position, + mut physics, + sprinting, + ) in query.iter_mut() + { + local_player.send_sprinting_if_needed(id, sprinting, &mut physics_state); + let packet = { - self.send_sprinting_if_needed().await?; // TODO: the camera being able to be controlled by other entities isn't // implemented yet if !self.is_controlled_camera() { return }; - let mut physics_state = self.physics_state.lock(); - - let player_entity = self.entity(); - let player_pos = player_entity.pos(); - let player_old_pos = player_entity.last_pos; - - let x_delta = player_pos.x - player_old_pos.x; - let y_delta = player_pos.y - player_old_pos.y; - let z_delta = player_pos.z - player_old_pos.z; - let y_rot_delta = (player_entity.y_rot - player_entity.y_rot_last) as f64; - let x_rot_delta = (player_entity.x_rot - player_entity.x_rot_last) as f64; + let x_delta = position.x - last_sent_position.x; + let y_delta = position.y - last_sent_position.y; + let z_delta = position.z - last_sent_position.z; + let y_rot_delta = (physics.y_rot - physics.y_rot_last) as f64; + let x_rot_delta = (physics.x_rot - physics.x_rot_last) as f64; physics_state.position_remainder += 1; @@ -67,38 +117,38 @@ impl Client { let packet = if sending_position && sending_rotation { Some( ServerboundMovePlayerPosRotPacket { - x: player_pos.x, - y: player_pos.y, - z: player_pos.z, - x_rot: player_entity.x_rot, - y_rot: player_entity.y_rot, - on_ground: player_entity.on_ground, + x: position.x, + y: position.y, + z: position.z, + x_rot: physics.x_rot, + y_rot: physics.y_rot, + on_ground: physics.on_ground, } .get(), ) } else if sending_position { Some( ServerboundMovePlayerPosPacket { - x: player_pos.x, - y: player_pos.y, - z: player_pos.z, - on_ground: player_entity.on_ground, + x: position.x, + y: position.y, + z: position.z, + on_ground: physics.on_ground, } .get(), ) } else if sending_rotation { Some( ServerboundMovePlayerRotPacket { - x_rot: player_entity.x_rot, - y_rot: player_entity.y_rot, - on_ground: player_entity.on_ground, + x_rot: physics.x_rot, + y_rot: physics.y_rot, + on_ground: physics.on_ground, } .get(), ) - } else if player_entity.last_on_ground != player_entity.on_ground { + } else if physics.last_on_ground != physics.on_ground { Some( ServerboundMovePlayerStatusOnlyPacket { - on_ground: player_entity.on_ground, + on_ground: physics.on_ground, } .get(), ) @@ -106,131 +156,56 @@ impl Client { None }; - drop(player_entity); - let mut player_entity = self.entity(); - if sending_position { - player_entity.last_pos = *player_entity.pos(); + **last_sent_position = **position; physics_state.position_remainder = 0; } if sending_rotation { - player_entity.y_rot_last = player_entity.y_rot; - player_entity.x_rot_last = player_entity.x_rot; + physics.y_rot_last = physics.y_rot; + physics.x_rot_last = physics.x_rot; } - player_entity.last_on_ground = player_entity.on_ground; + physics.last_on_ground = physics.on_ground; // minecraft checks for autojump here, but also autojump is bad so packet }; if let Some(packet) = packet { - self.write_packet(packet).await?; + local_player.write_packet(packet); } - - Ok(()) } +} - async fn send_sprinting_if_needed(&mut self) -> Result<(), MovePlayerError> { - let is_sprinting = self.entity().metadata.sprinting; - let was_sprinting = self.physics_state.lock().was_sprinting; - if is_sprinting != was_sprinting { - let sprinting_action = if is_sprinting { +impl LocalPlayer { + fn send_sprinting_if_needed( + &mut self, + id: &MinecraftEntityId, + sprinting: &entity::metadata::Sprinting, + physics_state: &mut PhysicsState, + ) { + let was_sprinting = physics_state.was_sprinting; + if **sprinting != was_sprinting { + let sprinting_action = if **sprinting { azalea_protocol::packets::game::serverbound_player_command_packet::Action::StartSprinting } else { azalea_protocol::packets::game::serverbound_player_command_packet::Action::StopSprinting }; - let player_entity_id = self.entity().id; self.write_packet( ServerboundPlayerCommandPacket { - id: player_entity_id, + id: **id, action: sprinting_action, data: 0, } .get(), - ) - .await?; - self.physics_state.lock().was_sprinting = is_sprinting; - } - - Ok(()) - } - - // Set our current position to the provided Vec3, potentially clipping through - // blocks. - pub async fn set_position(&mut self, new_pos: Vec3) -> Result<(), MovePlayerError> { - let player_entity_id = *self.entity_id.read(); - let mut world_lock = self.world.write(); - - world_lock.set_entity_pos(player_entity_id, new_pos)?; - - Ok(()) - } - - pub async fn move_entity(&mut self, movement: &Vec3) -> Result<(), MovePlayerError> { - let mut world_lock = self.world.write(); - let player_entity_id = *self.entity_id.read(); - - let mut entity = world_lock - .entity_mut(player_entity_id) - .ok_or(MovePlayerError::PlayerNotInWorld(Backtrace::capture()))?; - log::trace!( - "move entity bounding box: {} {:?}", - entity.id, - entity.bounding_box - ); - - entity.move_colliding(&MoverType::Own, movement)?; - - Ok(()) - } - - /// Makes the bot do one physics tick. Note that this is already handled - /// automatically by the client. - pub fn ai_step(&mut self) { - self.tick_controls(None); - - // server ai step - { - let mut player_entity = self.entity(); - - let physics_state = self.physics_state.lock(); - player_entity.xxa = physics_state.left_impulse; - player_entity.zza = physics_state.forward_impulse; - } - - // TODO: food data and abilities - // let has_enough_food_to_sprint = self.food_data().food_level || - // self.abilities().may_fly; - let has_enough_food_to_sprint = true; - - // TODO: double tapping w to sprint i think - - let trying_to_sprint = self.physics_state.lock().trying_to_sprint; - - if !self.sprinting() - && ( - // !self.is_in_water() - // || self.is_underwater() && - self.has_enough_impulse_to_start_sprinting() - && has_enough_food_to_sprint - // && !self.using_item() - // && !self.has_effect(MobEffects.BLINDNESS) - && trying_to_sprint - ) - { - self.set_sprinting(true); + ); + physics_state.was_sprinting = **sprinting; } - - let mut player_entity = self.entity(); - player_entity.ai_step(); } /// Update the impulse from self.move_direction. The multipler is used for /// sneaking. - pub(crate) fn tick_controls(&mut self, multiplier: Option<f32>) { - let mut physics_state = self.physics_state.lock(); - + pub(crate) fn tick_controls(multiplier: Option<f32>, physics_state: &mut PhysicsState) { let mut forward_impulse: f32 = 0.; let mut left_impulse: f32 = 0.; let move_direction = physics_state.move_direction; @@ -262,7 +237,54 @@ impl Client { physics_state.left_impulse *= multiplier; } } +} + +/// Makes the bot do one physics tick. Note that this is already handled +/// automatically by the client. +pub fn local_player_ai_step( + mut query: Query< + ( + &mut PhysicsState, + &mut entity::Physics, + &mut entity::metadata::Sprinting, + &mut entity::Attributes, + ), + With<LocalPlayerInLoadedChunk>, + >, +) { + for (mut physics_state, mut physics, mut sprinting, mut attributes) in query.iter_mut() { + LocalPlayer::tick_controls(None, &mut physics_state); + + // server ai step + physics.xxa = physics_state.left_impulse; + physics.zza = physics_state.forward_impulse; + + // TODO: food data and abilities + // let has_enough_food_to_sprint = self.food_data().food_level || + // self.abilities().may_fly; + let has_enough_food_to_sprint = true; + + // TODO: double tapping w to sprint i think + + let trying_to_sprint = physics_state.trying_to_sprint; + + if !**sprinting + && ( + // !self.is_in_water() + // || self.is_underwater() && + has_enough_impulse_to_start_sprinting(&physics_state) + && has_enough_food_to_sprint + // && !self.using_item() + // && !self.has_effect(MobEffects.BLINDNESS) + && trying_to_sprint + ) + { + set_sprinting(true, &mut sprinting, &mut attributes); + } + } +} +impl Client { /// Start walking in the given direction. To sprint, use /// [`Client::sprint`]. To stop walking, call walk with /// `WalkDirection::None`. @@ -280,12 +302,11 @@ impl Client { /// # } /// ``` pub fn walk(&mut self, direction: WalkDirection) { - { - let mut physics_state = self.physics_state.lock(); - physics_state.move_direction = direction; - } - - self.set_sprinting(false); + let mut ecs = self.ecs.lock(); + ecs.send_event(StartWalkEvent { + entity: self.entity, + direction, + }); } /// Start sprinting in the given direction. To stop moving, call @@ -304,71 +325,81 @@ impl Client { /// # } /// ``` pub fn sprint(&mut self, direction: SprintDirection) { - let mut physics_state = self.physics_state.lock(); - physics_state.move_direction = WalkDirection::from(direction); - physics_state.trying_to_sprint = true; + let mut ecs = self.ecs.lock(); + ecs.send_event(StartSprintEvent { + entity: self.entity, + direction, + }); } +} - // Whether we're currently sprinting. - pub fn sprinting(&self) -> bool { - self.entity().metadata.sprinting - } +pub struct StartWalkEvent { + pub entity: Entity, + pub direction: WalkDirection, +} - /// Change whether we're sprinting by adding an attribute modifier to the - /// player. You should use the [`walk`] and [`sprint`] methods instead. - /// Returns if the operation was successful. - fn set_sprinting(&mut self, sprinting: bool) -> bool { - let mut player_entity = self.entity(); - player_entity.metadata.sprinting = sprinting; - if sprinting { - player_entity - .attributes - .speed - .insert(azalea_world::entity::attributes::sprinting_modifier()) - .is_ok() - } else { - player_entity - .attributes - .speed - .remove(&azalea_world::entity::attributes::sprinting_modifier().uuid) - .is_none() +/// Start walking in the given direction. To sprint, use +/// [`Client::sprint`]. To stop walking, call walk with +/// `WalkDirection::None`. +pub fn walk_listener( + mut events: EventReader<StartWalkEvent>, + mut query: Query<(&mut PhysicsState, &mut Sprinting, &mut Attributes)>, +) { + for event in events.iter() { + if let Ok((mut physics_state, mut sprinting, mut attributes)) = query.get_mut(event.entity) + { + physics_state.move_direction = event.direction; + set_sprinting(false, &mut sprinting, &mut attributes); } } +} - /// Set whether we're jumping. This acts as if you held space in - /// vanilla. If you want to jump once, use the `jump` function. - /// - /// If you're making a realistic client, calling this function every tick is - /// recommended. - pub fn set_jumping(&mut self, jumping: bool) { - let mut player_entity = self.entity(); - player_entity.jumping = jumping; - } - - /// Returns whether the player will try to jump next tick. - pub fn jumping(&self) -> bool { - let player_entity = self.entity(); - player_entity.jumping +pub struct StartSprintEvent { + pub entity: Entity, + pub direction: SprintDirection, +} +/// Start sprinting in the given direction. +pub fn sprint_listener( + mut query: Query<&mut PhysicsState>, + mut events: EventReader<StartSprintEvent>, +) { + for event in events.iter() { + if let Ok(mut physics_state) = query.get_mut(event.entity) { + physics_state.move_direction = WalkDirection::from(event.direction); + physics_state.trying_to_sprint = true; + } } +} - /// Sets your rotation. `y_rot` is yaw (looking to the side), `x_rot` is - /// pitch (looking up and down). You can get these numbers from the vanilla - /// f3 screen. - /// `y_rot` goes from -180 to 180, and `x_rot` goes from -90 to 90. - pub fn set_rotation(&mut self, y_rot: f32, x_rot: f32) { - let mut player_entity = self.entity(); - player_entity.set_rotation(y_rot, x_rot); +/// Change whether we're sprinting by adding an attribute modifier to the +/// player. You should use the [`walk`] and [`sprint`] methods instead. +/// Returns if the operation was successful. +fn set_sprinting( + sprinting: bool, + currently_sprinting: &mut Sprinting, + attributes: &mut Attributes, +) -> bool { + **currently_sprinting = sprinting; + if sprinting { + attributes + .speed + .insert(entity::attributes::sprinting_modifier()) + .is_ok() + } else { + attributes + .speed + .remove(&entity::attributes::sprinting_modifier().uuid) + .is_none() } +} - // Whether the player is moving fast enough to be able to start sprinting. - fn has_enough_impulse_to_start_sprinting(&self) -> bool { - // if self.underwater() { - // self.has_forward_impulse() - // } else { - let physics_state = self.physics_state.lock(); - physics_state.forward_impulse > 0.8 - // } - } +// Whether the player is moving fast enough to be able to start sprinting. +fn has_enough_impulse_to_start_sprinting(physics_state: &PhysicsState) -> bool { + // if self.underwater() { + // self.has_forward_impulse() + // } else { + physics_state.forward_impulse > 0.8 + // } } #[derive(Clone, Copy, Debug, Default)] diff --git a/azalea-client/src/packet_handling.rs b/azalea-client/src/packet_handling.rs new file mode 100644 index 00000000..db2c3c45 --- /dev/null +++ b/azalea-client/src/packet_handling.rs @@ -0,0 +1,935 @@ +use std::{collections::HashSet, io::Cursor, sync::Arc}; + +use azalea_core::{ChunkPos, ResourceLocation, Vec3}; +use azalea_ecs::{ + app::{App, Plugin}, + component::Component, + ecs::Ecs, + entity::Entity, + event::EventWriter, + query::Changed, + schedule::{IntoSystemDescriptor, SystemSet}, + system::{Commands, Query, ResMut, SystemState}, +}; +use azalea_protocol::{ + connect::{ReadConnection, WriteConnection}, + packets::game::{ + clientbound_player_combat_kill_packet::ClientboundPlayerCombatKillPacket, + serverbound_accept_teleportation_packet::ServerboundAcceptTeleportationPacket, + serverbound_custom_payload_packet::ServerboundCustomPayloadPacket, + serverbound_keep_alive_packet::ServerboundKeepAlivePacket, + serverbound_move_player_pos_rot_packet::ServerboundMovePlayerPosRotPacket, + ClientboundGamePacket, ServerboundGamePacket, + }, +}; +use azalea_world::{ + entity::{ + metadata::{apply_metadata, Health, PlayerMetadataBundle}, + set_rotation, Dead, EntityBundle, EntityKind, LastSentPosition, MinecraftEntityId, Physics, + PlayerBundle, Position, + }, + LoadedBy, PartialWorld, RelativeEntityUpdate, WorldContainer, +}; +use log::{debug, error, trace, warn}; +use parking_lot::Mutex; +use tokio::sync::mpsc; + +use crate::{ + local_player::{GameProfileComponent, LocalPlayer}, + ChatPacket, ClientInformation, PlayerInfo, +}; + +pub struct PacketHandlerPlugin; + +impl Plugin for PacketHandlerPlugin { + fn build(&self, app: &mut App) { + app.add_system_set( + SystemSet::new().with_system(handle_packets.label("packet").before("tick")), + ) + .add_event::<AddPlayerEvent>() + .add_event::<RemovePlayerEvent>() + .add_event::<UpdatePlayerEvent>() + .add_event::<ChatReceivedEvent>() + .add_event::<DeathEvent>(); + } +} + +/// A player joined the game (or more specifically, was added to the tab +/// list of a local player). +#[derive(Debug)] +pub struct AddPlayerEvent { + /// The local player entity that received this event. + pub entity: Entity, + pub info: PlayerInfo, +} +/// A player left the game (or maybe is still in the game and was just +/// removed from the tab list of a local player). +#[derive(Debug)] +pub struct RemovePlayerEvent { + /// The local player entity that received this event. + pub entity: Entity, + pub info: PlayerInfo, +} +/// A player was updated in the tab list of a local player (gamemode, display +/// name, or latency changed). +#[derive(Debug)] +pub struct UpdatePlayerEvent { + /// The local player entity that received this event. + pub entity: Entity, + pub info: PlayerInfo, +} + +/// A client received a chat message packet. +#[derive(Debug)] +pub struct ChatReceivedEvent { + pub entity: Entity, + pub packet: ChatPacket, +} + +/// Event for when an entity dies. dies. If it's a local player and there's a +/// reason in the death screen, the [`ClientboundPlayerCombatKillPacket`] will +/// be included. +pub struct DeathEvent { + pub entity: Entity, + pub packet: Option<ClientboundPlayerCombatKillPacket>, +} + +/// Something that receives packets from the server. +#[derive(Component, Clone)] +pub struct PacketReceiver { + pub packets: Arc<Mutex<Vec<ClientboundGamePacket>>>, + pub run_schedule_sender: mpsc::Sender<()>, +} + +fn handle_packets(ecs: &mut Ecs) { + let mut events_owned = Vec::new(); + + { + let mut system_state: SystemState< + Query<(Entity, &PacketReceiver), Changed<PacketReceiver>>, + > = SystemState::new(ecs); + let query = system_state.get(ecs); + for (player_entity, packet_events) in &query { + let mut packets = packet_events.packets.lock(); + if !packets.is_empty() { + events_owned.push((player_entity, packets.clone())); + // clear the packets right after we read them + packets.clear(); + } + } + } + + for (player_entity, packets) in events_owned { + for packet in &packets { + match packet { + ClientboundGamePacket::Login(p) => { + debug!("Got login packet"); + + #[allow(clippy::type_complexity)] + let mut system_state: SystemState<( + Commands, + Query<(&mut LocalPlayer, &GameProfileComponent)>, + ResMut<WorldContainer>, + )> = SystemState::new(ecs); + let (mut commands, mut query, mut world_container) = system_state.get_mut(ecs); + let (mut local_player, game_profile) = query.get_mut(player_entity).unwrap(); + + { + // TODO: have registry_holder be a struct because this sucks rn + // best way would be to add serde support to azalea-nbt + + let registry_holder = p + .registry_holder + .as_compound() + .expect("Registry holder is not a compound") + .get("") + .expect("No \"\" tag") + .as_compound() + .expect("\"\" tag is not a compound"); + let dimension_types = registry_holder + .get("minecraft:dimension_type") + .expect("No dimension_type tag") + .as_compound() + .expect("dimension_type is not a compound") + .get("value") + .expect("No dimension_type value") + .as_list() + .expect("dimension_type value is not a list"); + let dimension_type = dimension_types + .iter() + .find(|t| { + t.as_compound() + .expect("dimension_type value is not a compound") + .get("name") + .expect("No name tag") + .as_string() + .expect("name is not a string") + == p.dimension_type.to_string() + }) + .unwrap_or_else(|| { + panic!("No dimension_type with name {}", p.dimension_type) + }) + .as_compound() + .unwrap() + .get("element") + .expect("No element tag") + .as_compound() + .expect("element is not a compound"); + let height = (*dimension_type + .get("height") + .expect("No height tag") + .as_int() + .expect("height tag is not an int")) + .try_into() + .expect("height is not a u32"); + let min_y = *dimension_type + .get("min_y") + .expect("No min_y tag") + .as_int() + .expect("min_y tag is not an int"); + + let world_name = p.dimension.clone(); + + local_player.world_name = Some(world_name.clone()); + // add this world to the world_container (or don't if it's already + // there) + let weak_world = world_container.insert(world_name.clone(), height, min_y); + // set the partial_world to an empty world + // (when we add chunks or entities those will be in the + // world_container) + + *local_player.partial_world.write() = PartialWorld::new( + local_player.client_information.view_distance.into(), + // this argument makes it so other clients don't update this + // player entity + // in a shared world + Some(player_entity), + ); + local_player.world = weak_world; + + let player_bundle = PlayerBundle { + entity: EntityBundle::new( + game_profile.uuid, + Vec3::default(), + azalea_registry::EntityKind::Player, + world_name, + ), + metadata: PlayerMetadataBundle::default(), + }; + // insert our components into the ecs :) + commands + .entity(player_entity) + .insert((MinecraftEntityId(p.player_id), player_bundle)); + } + + // send the client information that we have set + let client_information_packet: ClientInformation = + local_player.client_information.clone(); + log::debug!( + "Sending client information because login: {:?}", + client_information_packet + ); + local_player.write_packet(client_information_packet.get()); + + // brand + local_player.write_packet( + ServerboundCustomPayloadPacket { + identifier: ResourceLocation::new("brand").unwrap(), + // they don't have to know :) + data: "vanilla".into(), + } + .get(), + ); + + system_state.apply(ecs); + } + ClientboundGamePacket::SetChunkCacheRadius(p) => { + debug!("Got set chunk cache radius packet {:?}", p); + } + ClientboundGamePacket::CustomPayload(p) => { + debug!("Got custom payload packet {:?}", p); + } + ClientboundGamePacket::ChangeDifficulty(p) => { + debug!("Got difficulty packet {:?}", p); + } + ClientboundGamePacket::Commands(_p) => { + debug!("Got declare commands packet"); + } + ClientboundGamePacket::PlayerAbilities(p) => { + debug!("Got player abilities packet {:?}", p); + } + ClientboundGamePacket::SetCarriedItem(p) => { + debug!("Got set carried item packet {:?}", p); + } + ClientboundGamePacket::UpdateTags(_p) => { + debug!("Got update tags packet"); + } + ClientboundGamePacket::Disconnect(p) => { + debug!("Got disconnect packet {:?}", p); + let mut system_state: SystemState<Query<&LocalPlayer>> = SystemState::new(ecs); + let query = system_state.get(ecs); + let local_player = query.get(player_entity).unwrap(); + local_player.disconnect(); + } + ClientboundGamePacket::UpdateRecipes(_p) => { + debug!("Got update recipes packet"); + } + ClientboundGamePacket::EntityEvent(_p) => { + // debug!("Got entity event packet {:?}", p); + } + ClientboundGamePacket::Recipe(_p) => { + debug!("Got recipe packet"); + } + ClientboundGamePacket::PlayerPosition(p) => { + // TODO: reply with teleport confirm + debug!("Got player position packet {:?}", p); + + let mut system_state: SystemState< + Query<( + &mut LocalPlayer, + &mut Physics, + &mut Position, + &mut LastSentPosition, + )>, + > = SystemState::new(ecs); + let mut query = system_state.get_mut(ecs); + let Ok((mut local_player, mut physics, mut position, mut last_sent_position)) = + query.get_mut(player_entity) else { + continue; + }; + + let delta_movement = physics.delta; + + let is_x_relative = p.relative_arguments.x; + let is_y_relative = p.relative_arguments.y; + let is_z_relative = p.relative_arguments.z; + + let (delta_x, new_pos_x) = if is_x_relative { + last_sent_position.x += p.x; + (delta_movement.x, position.x + p.x) + } else { + last_sent_position.x = p.x; + (0.0, p.x) + }; + let (delta_y, new_pos_y) = if is_y_relative { + last_sent_position.y += p.y; + (delta_movement.y, position.y + p.y) + } else { + last_sent_position.y = p.y; + (0.0, p.y) + }; + let (delta_z, new_pos_z) = if is_z_relative { + last_sent_position.z += p.z; + (delta_movement.z, position.z + p.z) + } else { + last_sent_position.z = p.z; + (0.0, p.z) + }; + + let mut y_rot = p.y_rot; + let mut x_rot = p.x_rot; + if p.relative_arguments.x_rot { + x_rot += physics.x_rot; + } + if p.relative_arguments.y_rot { + y_rot += physics.y_rot; + } + + physics.delta = Vec3 { + x: delta_x, + y: delta_y, + z: delta_z, + }; + // we call a function instead of setting the fields ourself since the + // function makes sure the rotations stay in their + // ranges + set_rotation(&mut physics, y_rot, x_rot); + // TODO: minecraft sets "xo", "yo", and "zo" here but idk what that means + // so investigate that ig + let new_pos = Vec3 { + x: new_pos_x, + y: new_pos_y, + z: new_pos_z, + }; + + **position = new_pos; + + local_player + .write_packet(ServerboundAcceptTeleportationPacket { id: p.id }.get()); + local_player.write_packet( + ServerboundMovePlayerPosRotPacket { + x: new_pos.x, + y: new_pos.y, + z: new_pos.z, + y_rot, + x_rot, + // this is always false + on_ground: false, + } + .get(), + ); + } + ClientboundGamePacket::PlayerInfoUpdate(p) => { + debug!("Got player info packet {:?}", p); + + let mut system_state: SystemState<( + Query<&mut LocalPlayer>, + EventWriter<AddPlayerEvent>, + EventWriter<UpdatePlayerEvent>, + )> = SystemState::new(ecs); + let (mut query, mut add_player_events, mut update_player_events) = + system_state.get_mut(ecs); + let mut local_player = query.get_mut(player_entity).unwrap(); + + for updated_info in &p.entries { + // add the new player maybe + if p.actions.add_player { + let info = PlayerInfo { + profile: updated_info.profile.clone(), + uuid: updated_info.profile.uuid, + gamemode: updated_info.game_mode, + latency: updated_info.latency, + display_name: updated_info.display_name.clone(), + }; + local_player + .players + .insert(updated_info.profile.uuid, info.clone()); + add_player_events.send(AddPlayerEvent { + entity: player_entity, + info: info.clone(), + }); + } else if let Some(info) = + local_player.players.get_mut(&updated_info.profile.uuid) + { + // `else if` because the block for add_player above + // already sets all the fields + if p.actions.update_game_mode { + info.gamemode = updated_info.game_mode; + } + if p.actions.update_latency { + info.latency = updated_info.latency; + } + if p.actions.update_display_name { + info.display_name = updated_info.display_name.clone(); + } + update_player_events.send(UpdatePlayerEvent { + entity: player_entity, + info: info.clone(), + }); + } else { + warn!( + "Ignoring PlayerInfoUpdate for unknown player {}", + updated_info.profile.uuid + ); + } + } + } + ClientboundGamePacket::PlayerInfoRemove(p) => { + let mut system_state: SystemState<( + Query<&mut LocalPlayer>, + EventWriter<RemovePlayerEvent>, + )> = SystemState::new(ecs); + let (mut query, mut remove_player_events) = system_state.get_mut(ecs); + let mut local_player = query.get_mut(player_entity).unwrap(); + + for uuid in &p.profile_ids { + if let Some(info) = local_player.players.remove(uuid) { + remove_player_events.send(RemovePlayerEvent { + entity: player_entity, + info, + }); + } + } + } + ClientboundGamePacket::SetChunkCacheCenter(p) => { + debug!("Got chunk cache center packet {:?}", p); + + let mut system_state: SystemState<Query<&mut LocalPlayer>> = + SystemState::new(ecs); + let mut query = system_state.get_mut(ecs); + let local_player = query.get_mut(player_entity).unwrap(); + let mut partial_world = local_player.partial_world.write(); + + partial_world.chunks.view_center = ChunkPos::new(p.x, p.z); + } + ClientboundGamePacket::LevelChunkWithLight(p) => { + debug!("Got chunk with light packet {} {}", p.x, p.z); + let pos = ChunkPos::new(p.x, p.z); + + let mut system_state: SystemState<Query<&mut LocalPlayer>> = + SystemState::new(ecs); + let mut query = system_state.get_mut(ecs); + let local_player = query.get_mut(player_entity).unwrap(); + + // OPTIMIZATION: if we already know about the chunk from the + // shared world (and not ourselves), then we don't need to + // parse it again. This is only used when we have a shared + // world, since we check that the chunk isn't currently owned + // by this client. + let shared_chunk = local_player.world.read().chunks.get(&pos); + let this_client_has_chunk = local_player + .partial_world + .read() + .chunks + .limited_get(&pos) + .is_some(); + + let mut world = local_player.world.write(); + let mut partial_world = local_player.partial_world.write(); + + if !this_client_has_chunk { + if let Some(shared_chunk) = shared_chunk { + trace!( + "Skipping parsing chunk {:?} because we already know about it", + pos + ); + partial_world.chunks.set_with_shared_reference( + &pos, + Some(shared_chunk.clone()), + &mut world.chunks, + ); + continue; + } + } + + if let Err(e) = partial_world.chunks.replace_with_packet_data( + &pos, + &mut Cursor::new(&p.chunk_data.data), + &mut world.chunks, + ) { + error!("Couldn't set chunk data: {}", e); + } + } + ClientboundGamePacket::LightUpdate(_p) => { + // debug!("Got light update packet {:?}", p); + } + ClientboundGamePacket::AddEntity(p) => { + debug!("Got add entity packet {:?}", p); + + let mut system_state: SystemState<(Commands, Query<&mut LocalPlayer>)> = + SystemState::new(ecs); + let (mut commands, mut query) = system_state.get_mut(ecs); + let local_player = query.get_mut(player_entity).unwrap(); + + if let Some(world_name) = &local_player.world_name { + let bundle = p.as_entity_bundle(world_name.clone()); + let mut entity_commands = commands.spawn(( + MinecraftEntityId(p.id), + LoadedBy(HashSet::from([player_entity])), + bundle, + )); + // the bundle doesn't include the default entity metadata so we add that + // separately + p.apply_metadata(&mut entity_commands); + } else { + warn!("got add player packet but we haven't gotten a login packet yet"); + } + + system_state.apply(ecs); + } + ClientboundGamePacket::SetEntityData(p) => { + debug!("Got set entity data packet {:?}", p); + + let mut system_state: SystemState<( + Commands, + Query<&mut LocalPlayer>, + Query<&EntityKind>, + )> = SystemState::new(ecs); + let (mut commands, mut query, entity_kind_query) = system_state.get_mut(ecs); + let local_player = query.get_mut(player_entity).unwrap(); + + let world = local_player.world.read(); + let entity = world.entity_by_id(&MinecraftEntityId(p.id)); + drop(world); + + if let Some(entity) = entity { + let entity_kind = entity_kind_query.get(entity).unwrap(); + let mut entity_commands = commands.entity(entity); + if let Err(e) = apply_metadata( + &mut entity_commands, + **entity_kind, + (*p.packed_items).clone(), + ) { + warn!("{e}"); + } + } else { + warn!("Server sent an entity data packet for an entity id ({}) that we don't know about", p.id); + } + + system_state.apply(ecs); + } + ClientboundGamePacket::UpdateAttributes(_p) => { + // debug!("Got update attributes packet {:?}", p); + } + ClientboundGamePacket::SetEntityMotion(_p) => { + // debug!("Got entity velocity packet {:?}", p); + } + ClientboundGamePacket::SetEntityLink(p) => { + debug!("Got set entity link packet {:?}", p); + } + ClientboundGamePacket::AddPlayer(p) => { + debug!("Got add player packet {:?}", p); + + let mut system_state: SystemState<(Commands, Query<&mut LocalPlayer>)> = + SystemState::new(ecs); + let (mut commands, mut query) = system_state.get_mut(ecs); + let local_player = query.get_mut(player_entity).unwrap(); + + if let Some(world_name) = &local_player.world_name { + let bundle = p.as_player_bundle(world_name.clone()); + let mut spawned = commands.spawn(( + MinecraftEntityId(p.id), + LoadedBy(HashSet::from([player_entity])), + bundle, + )); + + if let Some(player_info) = local_player.players.get(&p.uuid) { + spawned.insert(GameProfileComponent(player_info.profile.clone())); + } + } else { + warn!("got add player packet but we haven't gotten a login packet yet"); + } + + system_state.apply(ecs); + } + ClientboundGamePacket::InitializeBorder(p) => { + debug!("Got initialize border packet {:?}", p); + } + ClientboundGamePacket::SetTime(_p) => { + // debug!("Got set time packet {:?}", p); + } + ClientboundGamePacket::SetDefaultSpawnPosition(p) => { + debug!("Got set default spawn position packet {:?}", p); + } + ClientboundGamePacket::ContainerSetContent(p) => { + debug!("Got container set content packet {:?}", p); + } + ClientboundGamePacket::SetHealth(p) => { + debug!("Got set health packet {:?}", p); + + let mut system_state: SystemState<( + Query<&mut Health>, + EventWriter<DeathEvent>, + )> = SystemState::new(ecs); + let (mut query, mut death_events) = system_state.get_mut(ecs); + let mut health = query.get_mut(player_entity).unwrap(); + + if p.health == 0. && **health != 0. { + death_events.send(DeathEvent { + entity: player_entity, + packet: None, + }); + } + + **health = p.health; + + // the `Dead` component is added by the `update_dead` system + // in azalea-world and then the `dead_event` system fires + // the Death event. + } + ClientboundGamePacket::SetExperience(p) => { + debug!("Got set experience packet {:?}", p); + } + ClientboundGamePacket::TeleportEntity(p) => { + let mut system_state: SystemState<(Commands, Query<&mut LocalPlayer>)> = + SystemState::new(ecs); + let (mut commands, mut query) = system_state.get_mut(ecs); + let local_player = query.get_mut(player_entity).unwrap(); + + let world = local_player.world.read(); + let entity = world.entity_by_id(&MinecraftEntityId(p.id)); + drop(world); + + if let Some(entity) = entity { + let new_position = p.position; + commands.add(RelativeEntityUpdate { + entity, + partial_world: local_player.partial_world.clone(), + update: Box::new(move |entity| { + let mut position = entity.get_mut::<Position>().unwrap(); + **position = new_position; + }), + }); + } else { + warn!("Got teleport entity packet for unknown entity id {}", p.id); + } + + system_state.apply(ecs); + } + ClientboundGamePacket::UpdateAdvancements(p) => { + debug!("Got update advancements packet {:?}", p); + } + ClientboundGamePacket::RotateHead(_p) => { + // debug!("Got rotate head packet {:?}", p); + } + ClientboundGamePacket::MoveEntityPos(p) => { + let mut system_state: SystemState<(Commands, Query<&LocalPlayer>)> = + SystemState::new(ecs); + let (mut commands, mut query) = system_state.get_mut(ecs); + let local_player = query.get_mut(player_entity).unwrap(); + + let world = local_player.world.read(); + let entity = world.entity_by_id(&MinecraftEntityId(p.entity_id)); + drop(world); + + if let Some(entity) = entity { + let delta = p.delta.clone(); + commands.add(RelativeEntityUpdate { + entity, + partial_world: local_player.partial_world.clone(), + update: Box::new(move |entity_mut| { + let mut position = entity_mut.get_mut::<Position>().unwrap(); + **position = position.with_delta(&delta); + }), + }); + } else { + warn!( + "Got move entity pos packet for unknown entity id {}", + p.entity_id + ); + } + + system_state.apply(ecs); + } + ClientboundGamePacket::MoveEntityPosRot(p) => { + let mut system_state: SystemState<(Commands, Query<&mut LocalPlayer>)> = + SystemState::new(ecs); + let (mut commands, mut query) = system_state.get_mut(ecs); + let local_player = query.get_mut(player_entity).unwrap(); + + let world = local_player.world.read(); + let entity = world.entity_by_id(&MinecraftEntityId(p.entity_id)); + drop(world); + + if let Some(entity) = entity { + let delta = p.delta.clone(); + commands.add(RelativeEntityUpdate { + entity, + partial_world: local_player.partial_world.clone(), + update: Box::new(move |entity_mut| { + let mut position = entity_mut.get_mut::<Position>().unwrap(); + **position = position.with_delta(&delta); + }), + }); + } else { + warn!( + "Got move entity pos rot packet for unknown entity id {}", + p.entity_id + ); + } + + system_state.apply(ecs); + } + + ClientboundGamePacket::MoveEntityRot(_p) => { + // debug!("Got move entity rot packet {:?}", p); + } + ClientboundGamePacket::KeepAlive(p) => { + debug!("Got keep alive packet {p:?} for {player_entity:?}"); + + let mut system_state: SystemState<Query<&mut LocalPlayer>> = + SystemState::new(ecs); + let mut query = system_state.get_mut(ecs); + let mut local_player = query.get_mut(player_entity).unwrap(); + + local_player.write_packet(ServerboundKeepAlivePacket { id: p.id }.get()); + debug!("Sent keep alive packet {p:?} for {player_entity:?}"); + } + ClientboundGamePacket::RemoveEntities(p) => { + debug!("Got remove entities packet {:?}", p); + } + ClientboundGamePacket::PlayerChat(p) => { + debug!("Got player chat packet {:?}", p); + + let mut system_state: SystemState<EventWriter<ChatReceivedEvent>> = + SystemState::new(ecs); + let mut chat_events = system_state.get_mut(ecs); + + chat_events.send(ChatReceivedEvent { + entity: player_entity, + packet: ChatPacket::Player(Arc::new(p.clone())), + }); + } + ClientboundGamePacket::SystemChat(p) => { + debug!("Got system chat packet {:?}", p); + + let mut system_state: SystemState<EventWriter<ChatReceivedEvent>> = + SystemState::new(ecs); + let mut chat_events = system_state.get_mut(ecs); + + chat_events.send(ChatReceivedEvent { + entity: player_entity, + packet: ChatPacket::System(Arc::new(p.clone())), + }); + } + ClientboundGamePacket::Sound(_p) => { + // debug!("Got sound packet {:?}", p); + } + ClientboundGamePacket::LevelEvent(p) => { + debug!("Got level event packet {:?}", p); + } + ClientboundGamePacket::BlockUpdate(p) => { + debug!("Got block update packet {:?}", p); + + let mut system_state: SystemState<Query<&mut LocalPlayer>> = + SystemState::new(ecs); + let mut query = system_state.get_mut(ecs); + let local_player = query.get_mut(player_entity).unwrap(); + + let world = local_player.world.write(); + + world.chunks.set_block_state(&p.pos, p.block_state); + } + ClientboundGamePacket::Animate(p) => { + debug!("Got animate packet {:?}", p); + } + ClientboundGamePacket::SectionBlocksUpdate(p) => { + debug!("Got section blocks update packet {:?}", p); + let mut system_state: SystemState<Query<&mut LocalPlayer>> = + SystemState::new(ecs); + let mut query = system_state.get_mut(ecs); + let local_player = query.get_mut(player_entity).unwrap(); + + let world = local_player.world.write(); + + for state in &p.states { + world + .chunks + .set_block_state(&(p.section_pos + state.pos.clone()), state.state); + } + } + ClientboundGamePacket::GameEvent(p) => { + debug!("Got game event packet {:?}", p); + } + ClientboundGamePacket::LevelParticles(p) => { + debug!("Got level particles packet {:?}", p); + } + ClientboundGamePacket::ServerData(p) => { + debug!("Got server data packet {:?}", p); + } + ClientboundGamePacket::SetEquipment(p) => { + debug!("Got set equipment packet {:?}", p); + } + ClientboundGamePacket::UpdateMobEffect(p) => { + debug!("Got update mob effect packet {:?}", p); + } + ClientboundGamePacket::AddExperienceOrb(_) => {} + ClientboundGamePacket::AwardStats(_) => {} + ClientboundGamePacket::BlockChangedAck(_) => {} + ClientboundGamePacket::BlockDestruction(_) => {} + ClientboundGamePacket::BlockEntityData(_) => {} + ClientboundGamePacket::BlockEvent(_) => {} + ClientboundGamePacket::BossEvent(_) => {} + ClientboundGamePacket::CommandSuggestions(_) => {} + ClientboundGamePacket::ContainerSetData(_) => {} + ClientboundGamePacket::ContainerSetSlot(_) => {} + ClientboundGamePacket::Cooldown(_) => {} + ClientboundGamePacket::CustomChatCompletions(_) => {} + ClientboundGamePacket::DeleteChat(_) => {} + ClientboundGamePacket::Explode(_) => {} + ClientboundGamePacket::ForgetLevelChunk(_) => {} + ClientboundGamePacket::HorseScreenOpen(_) => {} + ClientboundGamePacket::MapItemData(_) => {} + ClientboundGamePacket::MerchantOffers(_) => {} + ClientboundGamePacket::MoveVehicle(_) => {} + ClientboundGamePacket::OpenBook(_) => {} + ClientboundGamePacket::OpenScreen(_) => {} + ClientboundGamePacket::OpenSignEditor(_) => {} + ClientboundGamePacket::Ping(_) => {} + ClientboundGamePacket::PlaceGhostRecipe(_) => {} + ClientboundGamePacket::PlayerCombatEnd(_) => {} + ClientboundGamePacket::PlayerCombatEnter(_) => {} + ClientboundGamePacket::PlayerCombatKill(p) => { + debug!("Got player kill packet {:?}", p); + + #[allow(clippy::type_complexity)] + let mut system_state: SystemState<( + Commands, + Query<(&MinecraftEntityId, Option<&Dead>)>, + EventWriter<DeathEvent>, + )> = SystemState::new(ecs); + let (mut commands, mut query, mut death_events) = system_state.get_mut(ecs); + let (entity_id, dead) = query.get_mut(player_entity).unwrap(); + + if **entity_id == p.player_id && dead.is_none() { + commands.entity(player_entity).insert(Dead); + death_events.send(DeathEvent { + entity: player_entity, + packet: Some(p.clone()), + }); + } + + system_state.apply(ecs); + } + ClientboundGamePacket::PlayerLookAt(_) => {} + ClientboundGamePacket::RemoveMobEffect(_) => {} + ClientboundGamePacket::ResourcePack(_) => {} + ClientboundGamePacket::Respawn(p) => { + debug!("Got respawn packet {:?}", p); + + let mut system_state: SystemState<Commands> = SystemState::new(ecs); + let mut commands = system_state.get(ecs); + + // Remove the Dead marker component from the player. + commands.entity(player_entity).remove::<Dead>(); + + system_state.apply(ecs); + } + ClientboundGamePacket::SelectAdvancementsTab(_) => {} + ClientboundGamePacket::SetActionBarText(_) => {} + ClientboundGamePacket::SetBorderCenter(_) => {} + ClientboundGamePacket::SetBorderLerpSize(_) => {} + ClientboundGamePacket::SetBorderSize(_) => {} + ClientboundGamePacket::SetBorderWarningDelay(_) => {} + ClientboundGamePacket::SetBorderWarningDistance(_) => {} + ClientboundGamePacket::SetCamera(_) => {} + ClientboundGamePacket::SetDisplayObjective(_) => {} + ClientboundGamePacket::SetObjective(_) => {} + ClientboundGamePacket::SetPassengers(_) => {} + ClientboundGamePacket::SetPlayerTeam(_) => {} + ClientboundGamePacket::SetScore(_) => {} + ClientboundGamePacket::SetSimulationDistance(_) => {} + ClientboundGamePacket::SetSubtitleText(_) => {} + ClientboundGamePacket::SetTitleText(_) => {} + ClientboundGamePacket::SetTitlesAnimation(_) => {} + ClientboundGamePacket::SoundEntity(_) => {} + ClientboundGamePacket::StopSound(_) => {} + ClientboundGamePacket::TabList(_) => {} + ClientboundGamePacket::TagQuery(_) => {} + ClientboundGamePacket::TakeItemEntity(_) => {} + ClientboundGamePacket::DisguisedChat(_) => {} + ClientboundGamePacket::UpdateEnabledFeatures(_) => {} + ClientboundGamePacket::ContainerClose(_) => {} + } + } + } +} + +impl PacketReceiver { + /// Loop that reads from the connection and adds the packets to the queue + + /// runs the schedule. + pub async fn read_task(self, mut read_conn: ReadConnection<ClientboundGamePacket>) { + while let Ok(packet) = read_conn.read().await { + self.packets.lock().push(packet); + // tell the client to run all the systems + self.run_schedule_sender.send(()).await.unwrap(); + } + } + + /// Consume the [`ServerboundGamePacket`] queue and actually write the + /// packets to the server. It's like this so writing packets doesn't need to + /// be awaited. + pub async fn write_task( + self, + mut write_conn: WriteConnection<ServerboundGamePacket>, + mut write_receiver: mpsc::UnboundedReceiver<ServerboundGamePacket>, + ) { + while let Some(packet) = write_receiver.recv().await { + if let Err(err) = write_conn.write(packet).await { + error!("Disconnecting because we couldn't write a packet: {err}."); + break; + }; + } + // receiver is automatically closed when it's dropped + } +} diff --git a/azalea-client/src/player.rs b/azalea-client/src/player.rs index 650fb58c..00b6b25e 100755 --- a/azalea-client/src/player.rs +++ b/azalea-client/src/player.rs @@ -1,13 +1,14 @@ use azalea_auth::game_profile::GameProfile; -use azalea_chat::Component; +use azalea_chat::FormattedText; use azalea_core::GameType; -use azalea_world::PartialWorld; +use azalea_ecs::{ + event::EventReader, + system::{Commands, Res}, +}; +use azalea_world::EntityInfos; use uuid::Uuid; -/// Something that has a world associated to it. this is usually a `Client`. -pub trait WorldHaver { - fn world(&self) -> &PartialWorld; -} +use crate::{packet_handling::AddPlayerEvent, GameProfileComponent}; /// A player in the tab list. #[derive(Debug, Clone)] @@ -18,5 +19,22 @@ pub struct PlayerInfo { pub gamemode: GameType, pub latency: i32, /// The player's display name in the tab list. - pub display_name: Option<Component>, + pub display_name: Option<FormattedText>, +} + +/// Add a [`GameProfileComponent`] when an [`AddPlayerEvent`] is received. +/// Usually the `GameProfileComponent` will be added from the +/// `ClientboundGamePacket::AddPlayer` handler though. +pub fn retroactively_add_game_profile_component( + mut commands: Commands, + mut events: EventReader<AddPlayerEvent>, + entity_infos: Res<EntityInfos>, +) { + for event in events.iter() { + if let Some(entity) = entity_infos.get_entity_by_uuid(&event.info.uuid) { + commands + .entity(entity) + .insert(GameProfileComponent(event.info.profile.clone())); + } + } } diff --git a/azalea-client/src/plugins.rs b/azalea-client/src/plugins.rs deleted file mode 100644 index 93641906..00000000 --- a/azalea-client/src/plugins.rs +++ /dev/null @@ -1,144 +0,0 @@ -use crate::{Client, Event}; -use async_trait::async_trait; -use nohash_hasher::NoHashHasher; -use std::{ - any::{Any, TypeId}, - collections::HashMap, - hash::BuildHasherDefault, -}; - -type U64Hasher = BuildHasherDefault<NoHashHasher<u64>>; - -// kind of based on https://docs.rs/http/latest/src/http/extensions.rs.html -#[derive(Clone, Default)] -pub struct PluginStates { - map: Option<HashMap<TypeId, Box<dyn PluginState>, U64Hasher>>, -} - -/// A map of PluginState TypeIds to AnyPlugin objects. This can then be built -/// into a [`PluginStates`] object to get a fresh new state based on this -/// plugin. -/// -/// If you're using the azalea crate, you should generate this from the -/// `plugins!` macro. -#[derive(Clone, Default)] -pub struct Plugins { - map: Option<HashMap<TypeId, Box<dyn AnyPlugin>, U64Hasher>>, -} - -impl PluginStates { - pub fn get<T: PluginState>(&self) -> Option<&T> { - self.map - .as_ref() - .and_then(|map| map.get(&TypeId::of::<T>())) - .and_then(|boxed| (boxed.as_ref() as &dyn Any).downcast_ref::<T>()) - } -} - -impl Plugins { - /// Create a new empty set of plugins. - pub fn new() -> Self { - Self::default() - } - - /// Add a new plugin to this set. - pub fn add<T: Plugin + Clone>(&mut self, plugin: T) { - if self.map.is_none() { - self.map = Some(HashMap::with_hasher(BuildHasherDefault::default())); - } - self.map - .as_mut() - .unwrap() - .insert(TypeId::of::<T::State>(), Box::new(plugin)); - } - - /// Build our plugin states from this set of plugins. Note that if you're - /// using `azalea` you'll probably never need to use this as it's called - /// for you. - pub fn build(self) -> PluginStates { - let mut map = HashMap::with_hasher(BuildHasherDefault::default()); - for (id, plugin) in self.map.unwrap().into_iter() { - map.insert(id, plugin.build()); - } - PluginStates { map: Some(map) } - } -} - -impl IntoIterator for PluginStates { - type Item = Box<dyn PluginState>; - type IntoIter = std::vec::IntoIter<Self::Item>; - - /// Iterate over the plugin states. - fn into_iter(self) -> Self::IntoIter { - self.map - .map(|map| map.into_values().collect::<Vec<_>>()) - .unwrap_or_default() - .into_iter() - } -} - -/// A `PluginState` keeps the current state of a plugin for a client. All the -/// fields must be atomic. Unique `PluginState`s are built from [`Plugin`]s. -#[async_trait] -pub trait PluginState: Send + Sync + PluginStateClone + Any + 'static { - async fn handle(self: Box<Self>, event: Event, bot: Client); -} - -/// Plugins can keep their own personal state, listen to [`Event`]s, and add -/// new functions to [`Client`]. -pub trait Plugin: Send + Sync + Any + 'static { - type State: PluginState; - - fn build(&self) -> Self::State; -} - -/// AnyPlugin is basically a Plugin but without the State associated type -/// it has to exist so we can do a hashmap with Box<dyn AnyPlugin> -#[doc(hidden)] -pub trait AnyPlugin: Send + Sync + Any + AnyPluginClone + 'static { - fn build(&self) -> Box<dyn PluginState>; -} - -impl<S: PluginState, B: Plugin<State = S> + Clone> AnyPlugin for B { - fn build(&self) -> Box<dyn PluginState> { - Box::new(self.build()) - } -} - -/// An internal trait that allows PluginState to be cloned. -#[doc(hidden)] -pub trait PluginStateClone { - fn clone_box(&self) -> Box<dyn PluginState>; -} -impl<T> PluginStateClone for T -where - T: 'static + PluginState + Clone, -{ - fn clone_box(&self) -> Box<dyn PluginState> { - Box::new(self.clone()) - } -} -impl Clone for Box<dyn PluginState> { - fn clone(&self) -> Self { - self.clone_box() - } -} - -/// An internal trait that allows AnyPlugin to be cloned. -#[doc(hidden)] -pub trait AnyPluginClone { - fn clone_box(&self) -> Box<dyn AnyPlugin>; -} -impl<T> AnyPluginClone for T -where - T: 'static + Plugin + Clone, -{ - fn clone_box(&self) -> Box<dyn AnyPlugin> { - Box::new(self.clone()) - } -} -impl Clone for Box<dyn AnyPlugin> { - fn clone(&self) -> Self { - self.clone_box() - } -} diff --git a/azalea-client/src/task_pool.rs b/azalea-client/src/task_pool.rs new file mode 100644 index 00000000..2a3afbbc --- /dev/null +++ b/azalea-client/src/task_pool.rs @@ -0,0 +1,177 @@ +//! Borrowed from `bevy_core`. + +use azalea_ecs::{ + app::{App, Plugin}, + schedule::IntoSystemDescriptor, + system::Resource, +}; +use bevy_tasks::{AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool, TaskPoolBuilder}; + +/// Setup of default task pools: `AsyncComputeTaskPool`, `ComputeTaskPool`, +/// `IoTaskPool`. +#[derive(Default)] +pub struct TaskPoolPlugin { + /// Options for the [`TaskPool`](bevy_tasks::TaskPool) created at + /// application start. + pub task_pool_options: TaskPoolOptions, +} + +impl Plugin for TaskPoolPlugin { + fn build(&self, app: &mut App) { + // Setup the default bevy task pools + self.task_pool_options.create_default_pools(); + + #[cfg(not(target_arch = "wasm32"))] + app.add_system_to_stage( + azalea_ecs::app::CoreStage::Last, + bevy_tasks::tick_global_task_pools_on_main_thread.at_end(), + ); + } +} + +/// Helper for configuring and creating the default task pools. For end-users +/// who want full control, set up [`TaskPoolPlugin`](super::TaskPoolPlugin) +#[derive(Clone, Resource)] +pub struct TaskPoolOptions { + /// If the number of physical cores is less than min_total_threads, force + /// using min_total_threads + pub min_total_threads: usize, + /// If the number of physical cores is greater than max_total_threads, force + /// using max_total_threads + pub max_total_threads: usize, + + /// Used to determine number of IO threads to allocate + pub io: TaskPoolThreadAssignmentPolicy, + /// Used to determine number of async compute threads to allocate + pub async_compute: TaskPoolThreadAssignmentPolicy, + /// Used to determine number of compute threads to allocate + pub compute: TaskPoolThreadAssignmentPolicy, +} + +impl Default for TaskPoolOptions { + fn default() -> Self { + TaskPoolOptions { + // By default, use however many cores are available on the system + min_total_threads: 1, + max_total_threads: std::usize::MAX, + + // Use 25% of cores for IO, at least 1, no more than 4 + io: TaskPoolThreadAssignmentPolicy { + min_threads: 1, + max_threads: 4, + percent: 0.25, + }, + + // Use 25% of cores for async compute, at least 1, no more than 4 + async_compute: TaskPoolThreadAssignmentPolicy { + min_threads: 1, + max_threads: 4, + percent: 0.25, + }, + + // Use all remaining cores for compute (at least 1) + compute: TaskPoolThreadAssignmentPolicy { + min_threads: 1, + max_threads: std::usize::MAX, + percent: 1.0, // This 1.0 here means "whatever is left over" + }, + } + } +} + +impl TaskPoolOptions { + // /// Create a configuration that forces using the given number of threads. + // pub fn with_num_threads(thread_count: usize) -> Self { + // TaskPoolOptions { + // min_total_threads: thread_count, + // max_total_threads: thread_count, + // ..Default::default() + // } + // } + + /// Inserts the default thread pools into the given resource map based on + /// the configured values + pub fn create_default_pools(&self) { + let total_threads = bevy_tasks::available_parallelism() + .clamp(self.min_total_threads, self.max_total_threads); + + let mut remaining_threads = total_threads; + + { + // Determine the number of IO threads we will use + let io_threads = self + .io + .get_number_of_threads(remaining_threads, total_threads); + + remaining_threads = remaining_threads.saturating_sub(io_threads); + + IoTaskPool::init(|| { + TaskPoolBuilder::default() + .num_threads(io_threads) + .thread_name("IO Task Pool".to_string()) + .build() + }); + } + + { + // Determine the number of async compute threads we will use + let async_compute_threads = self + .async_compute + .get_number_of_threads(remaining_threads, total_threads); + + remaining_threads = remaining_threads.saturating_sub(async_compute_threads); + + AsyncComputeTaskPool::init(|| { + TaskPoolBuilder::default() + .num_threads(async_compute_threads) + .thread_name("Async Compute Task Pool".to_string()) + .build() + }); + } + + { + // Determine the number of compute threads we will use + // This is intentionally last so that an end user can specify 1.0 as the percent + let compute_threads = self + .compute + .get_number_of_threads(remaining_threads, total_threads); + + ComputeTaskPool::init(|| { + TaskPoolBuilder::default() + .num_threads(compute_threads) + .thread_name("Compute Task Pool".to_string()) + .build() + }); + } + } +} + +/// Defines a simple way to determine how many threads to use given the number +/// of remaining cores and number of total cores +#[derive(Clone)] +pub struct TaskPoolThreadAssignmentPolicy { + /// Force using at least this many threads + pub min_threads: usize, + /// Under no circumstance use more than this many threads for this pool + pub max_threads: usize, + /// Target using this percentage of total cores, clamped by min_threads and + /// max_threads. It is permitted to use 1.0 to try to use all remaining + /// threads + pub percent: f32, +} + +impl TaskPoolThreadAssignmentPolicy { + /// Determine the number of threads to use for this task pool + fn get_number_of_threads(&self, remaining_threads: usize, total_threads: usize) -> usize { + assert!(self.percent >= 0.0); + let mut desired = (total_threads as f32 * self.percent).round() as usize; + + // Limit ourselves to the number of cores available + desired = desired.min(remaining_threads); + + // Clamp by min_threads, max_threads. (This may result in us using more threads + // than are available, this is intended. An example case where this + // might happen is a device with <= 2 threads. + desired.clamp(self.min_threads, self.max_threads) + } +} |
