diff options
| author | mat <27899617+mat-1@users.noreply.github.com> | 2025-02-22 21:45:26 -0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-02-22 21:45:26 -0600 |
| commit | e21e1b97bf9337e9f4747cd1b545b1b3a03e2ce7 (patch) | |
| tree | add6f8bfce40d0c07845d8aa4c9945a0b918444c /azalea-client/src/plugins | |
| parent | f8130c3c92946d2293634ba4e252d6bc93026c3c (diff) | |
| download | azalea-drasl-e21e1b97bf9337e9f4747cd1b545b1b3a03e2ce7.tar.xz | |
Refactor azalea-client (#205)
* start organizing packet_handling more by moving packet handlers into their own functions
* finish writing all the handler functions for packets
* use macro for generating match statement for packet handler functions
* fix set_entity_data
* update config state to also use handler functions
* organize az-client file structure by moving things into plugins directory
* fix merge issues
Diffstat (limited to 'azalea-client/src/plugins')
21 files changed, 6022 insertions, 0 deletions
diff --git a/azalea-client/src/plugins/attack.rs b/azalea-client/src/plugins/attack.rs new file mode 100644 index 00000000..1b2bc1ee --- /dev/null +++ b/azalea-client/src/plugins/attack.rs @@ -0,0 +1,149 @@ +use azalea_core::{game_type::GameMode, tick::GameTick}; +use azalea_entity::{ + Attributes, Physics, + metadata::{ShiftKeyDown, Sprinting}, + update_bounding_box, +}; +use azalea_physics::PhysicsSet; +use azalea_protocol::packets::game::s_interact::{self, ServerboundInteract}; +use azalea_world::MinecraftEntityId; +use bevy_app::{App, Plugin, Update}; +use bevy_ecs::prelude::*; +use derive_more::{Deref, DerefMut}; + +use super::packet::game::SendPacketEvent; +use crate::{ + Client, interact::SwingArmEvent, local_player::LocalGameMode, movement::MoveEventsSet, + respawn::perform_respawn, +}; + +pub struct AttackPlugin; +impl Plugin for AttackPlugin { + fn build(&self, app: &mut App) { + app.add_event::<AttackEvent>() + .add_systems( + Update, + handle_attack_event + .before(update_bounding_box) + .before(MoveEventsSet) + .after(perform_respawn), + ) + .add_systems( + GameTick, + ( + increment_ticks_since_last_attack, + update_attack_strength_scale.after(PhysicsSet), + ) + .chain(), + ); + } +} + +impl Client { + /// Attack the entity with the given id. + pub fn attack(&mut self, entity_id: MinecraftEntityId) { + self.ecs.lock().send_event(AttackEvent { + entity: self.entity, + target: entity_id, + }); + } + + /// Whether the player has an attack cooldown. + pub fn has_attack_cooldown(&self) -> bool { + let Some(AttackStrengthScale(ticks_since_last_attack)) = + self.get_component::<AttackStrengthScale>() + else { + // they don't even have an AttackStrengthScale so they probably can't attack + // lmao, just return false + return false; + }; + ticks_since_last_attack < 1.0 + } +} + +#[derive(Event)] +pub struct AttackEvent { + pub entity: Entity, + pub target: MinecraftEntityId, +} +pub fn handle_attack_event( + mut events: EventReader<AttackEvent>, + mut query: Query<( + &LocalGameMode, + &mut TicksSinceLastAttack, + &mut Physics, + &mut Sprinting, + &mut ShiftKeyDown, + )>, + mut send_packet_events: EventWriter<SendPacketEvent>, + mut swing_arm_event: EventWriter<SwingArmEvent>, +) { + for event in events.read() { + let (game_mode, mut ticks_since_last_attack, mut physics, mut sprinting, sneaking) = + query.get_mut(event.entity).unwrap(); + + swing_arm_event.send(SwingArmEvent { + entity: event.entity, + }); + send_packet_events.send(SendPacketEvent::new( + event.entity, + ServerboundInteract { + entity_id: event.target, + action: s_interact::ActionType::Attack, + using_secondary_action: **sneaking, + }, + )); + + // we can't attack if we're in spectator mode but it still sends the attack + // packet + if game_mode.current == GameMode::Spectator { + continue; + }; + + ticks_since_last_attack.0 = 0; + + physics.velocity = physics.velocity.multiply(0.6, 1.0, 0.6); + **sprinting = false; + } +} + +#[derive(Default, Bundle)] +pub struct AttackBundle { + pub ticks_since_last_attack: TicksSinceLastAttack, + pub attack_strength_scale: AttackStrengthScale, +} + +#[derive(Default, Component, Clone, Deref, DerefMut)] +pub struct TicksSinceLastAttack(pub u32); +pub fn increment_ticks_since_last_attack(mut query: Query<&mut TicksSinceLastAttack>) { + for mut ticks_since_last_attack in query.iter_mut() { + **ticks_since_last_attack += 1; + } +} + +#[derive(Default, Component, Clone, Deref, DerefMut)] +pub struct AttackStrengthScale(pub f32); +pub fn update_attack_strength_scale( + mut query: Query<(&TicksSinceLastAttack, &Attributes, &mut AttackStrengthScale)>, +) { + for (ticks_since_last_attack, attributes, mut attack_strength_scale) in query.iter_mut() { + // look 0.5 ticks into the future because that's what vanilla does + **attack_strength_scale = + get_attack_strength_scale(ticks_since_last_attack.0, attributes, 0.5); + } +} + +/// Returns how long it takes for the attack cooldown to reset (in ticks). +pub fn get_attack_strength_delay(attributes: &Attributes) -> f32 { + ((1. / attributes.attack_speed.calculate()) * 20.) as f32 +} + +pub fn get_attack_strength_scale( + ticks_since_last_attack: u32, + attributes: &Attributes, + in_ticks: f32, +) -> f32 { + let attack_strength_delay = get_attack_strength_delay(attributes); + let attack_strength = (ticks_since_last_attack as f32 + in_ticks) / attack_strength_delay; + attack_strength.clamp(0., 1.) +} diff --git a/azalea-client/src/plugins/brand.rs b/azalea-client/src/plugins/brand.rs new file mode 100644 index 00000000..e15a6c67 --- /dev/null +++ b/azalea-client/src/plugins/brand.rs @@ -0,0 +1,63 @@ +use azalea_buf::AzaleaWrite; +use azalea_core::resource_location::ResourceLocation; +use azalea_protocol::{ + common::client_information::ClientInformation, + packets::config::{ + s_client_information::ServerboundClientInformation, + s_custom_payload::ServerboundCustomPayload, + }, +}; +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; +use tracing::{debug, warn}; + +use super::packet::config::SendConfigPacketEvent; +use crate::packet::login::InLoginState; + +pub struct BrandPlugin; +impl Plugin for BrandPlugin { + fn build(&self, app: &mut App) { + app.add_systems( + Update, + handle_end_login_state.before(crate::packet::config::handle_send_packet_event), + ); + } +} + +fn handle_end_login_state( + mut removed: RemovedComponents<InLoginState>, + query: Query<&ClientInformation>, + mut send_packet_events: EventWriter<SendConfigPacketEvent>, +) { + for entity in removed.read() { + let mut brand_data = Vec::new(); + // azalea pretends to be vanilla everywhere else so it makes sense to lie here + // too + "vanilla".azalea_write(&mut brand_data).unwrap(); + send_packet_events.send(SendConfigPacketEvent::new( + entity, + ServerboundCustomPayload { + identifier: ResourceLocation::new("brand"), + data: brand_data.into(), + }, + )); + + let client_information = match query.get(entity).ok() { + Some(i) => i, + None => { + warn!( + "ClientInformation component was not set before leaving login state, using a default" + ); + &ClientInformation::default() + } + }; + + debug!("Writing ClientInformation while in config state: {client_information:?}"); + send_packet_events.send(SendConfigPacketEvent::new( + entity, + ServerboundClientInformation { + information: client_information.clone(), + }, + )); + } +} diff --git a/azalea-client/src/plugins/chat/handler.rs b/azalea-client/src/plugins/chat/handler.rs new file mode 100644 index 00000000..d598acdb --- /dev/null +++ b/azalea-client/src/plugins/chat/handler.rs @@ -0,0 +1,61 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use azalea_protocol::packets::{ + game::{s_chat::LastSeenMessagesUpdate, ServerboundChat, ServerboundChatCommand}, + Packet, +}; +use bevy_ecs::prelude::*; + +use super::ChatKind; +use crate::packet::game::SendPacketEvent; + +/// Send a chat packet to the server of a specific kind (chat message or +/// command). Usually you just want [`SendChatEvent`] instead. +/// +/// Usually setting the kind to `Message` will make it send a chat message even +/// if it starts with a slash, but some server implementations will always do a +/// command if it starts with a slash. +/// +/// If you're wondering why this isn't two separate events, it's so ordering is +/// preserved if multiple chat messages and commands are sent at the same time. +#[derive(Event)] +pub struct SendChatKindEvent { + pub entity: Entity, + pub content: String, + pub kind: ChatKind, +} + +pub fn handle_send_chat_kind_event( + mut events: EventReader<SendChatKindEvent>, + mut send_packet_events: EventWriter<SendPacketEvent>, +) { + for event in events.read() { + let content = event + .content + .chars() + .filter(|c| !matches!(c, '\x00'..='\x1F' | '\x7F' | 'ยง')) + .take(256) + .collect::<String>(); + let packet = match event.kind { + ChatKind::Message => ServerboundChat { + message: content, + timestamp: SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time shouldn't be before epoch") + .as_millis() + .try_into() + .expect("Instant should fit into a u64"), + salt: azalea_crypto::make_salt(), + signature: None, + last_seen_messages: LastSeenMessagesUpdate::default(), + } + .into_variant(), + ChatKind::Command => { + // TODO: chat signing + ServerboundChatCommand { command: content }.into_variant() + } + }; + + send_packet_events.send(SendPacketEvent::new(event.entity, packet)); + } +} diff --git a/azalea-client/src/plugins/chat/mod.rs b/azalea-client/src/plugins/chat/mod.rs new file mode 100644 index 00000000..66c77b56 --- /dev/null +++ b/azalea-client/src/plugins/chat/mod.rs @@ -0,0 +1,240 @@ +//! Implementations of chat-related features. + +pub mod handler; + +use std::sync::Arc; + +use azalea_chat::FormattedText; +use azalea_protocol::packets::game::{ + c_disguised_chat::ClientboundDisguisedChat, c_player_chat::ClientboundPlayerChat, + c_system_chat::ClientboundSystemChat, +}; +use bevy_app::{App, Plugin, Update}; +use bevy_ecs::{ + entity::Entity, + event::{EventReader, EventWriter}, + prelude::Event, + schedule::IntoSystemConfigs, +}; +use handler::{SendChatKindEvent, handle_send_chat_kind_event}; +use uuid::Uuid; + +use super::packet::game::handle_outgoing_packets; +use crate::client::Client; + +pub struct ChatPlugin; +impl Plugin for ChatPlugin { + fn build(&self, app: &mut App) { + app.add_event::<SendChatEvent>() + .add_event::<SendChatKindEvent>() + .add_event::<ChatReceivedEvent>() + .add_systems( + Update, + ( + handle_send_chat_event, + handle_send_chat_kind_event.after(handle_outgoing_packets), + ) + .chain(), + ); + } +} + +/// A chat packet, either a system message or a chat message. +#[derive(Debug, Clone, PartialEq)] +pub enum ChatPacket { + System(Arc<ClientboundSystemChat>), + Player(Arc<ClientboundPlayerChat>), + Disguised(Arc<ClientboundDisguisedChat>), +} + +macro_rules! regex { + ($re:literal $(,)?) => {{ + static RE: std::sync::LazyLock<regex::Regex> = + std::sync::LazyLock::new(|| regex::Regex::new($re).unwrap()); + &RE + }}; +} + +impl ChatPacket { + /// Get the message shown in chat for this packet. + pub fn message(&self) -> FormattedText { + match self { + ChatPacket::System(p) => p.content.clone(), + ChatPacket::Player(p) => p.message(), + ChatPacket::Disguised(p) => p.message(), + } + } + + /// Determine the username of the sender and content of the message. This + /// does not preserve formatting codes. If it's not a player-sent chat + /// message or the sender couldn't be determined, the username part will be + /// None. + pub fn split_sender_and_content(&self) -> (Option<String>, String) { + match self { + ChatPacket::System(p) => { + let message = p.content.to_string(); + // Overlay messages aren't in chat + if p.overlay { + return (None, message); + } + // It's a system message, so we'll have to match the content + // with regex + if let Some(m) = regex!("^<([a-zA-Z_0-9]{1,16})> (.+)$").captures(&message) { + return (Some(m[1].to_string()), m[2].to_string()); + } + + (None, message) + } + ChatPacket::Player(p) => ( + // If it's a player chat packet, then the sender and content + // are already split for us. + Some(p.chat_type.name.to_string()), + p.body.content.clone(), + ), + ChatPacket::Disguised(p) => ( + // disguised chat packets are basically the same as player chat packets but without + // the chat signing things + Some(p.chat_type.name.to_string()), + p.message.to_string(), + ), + } + } + + /// Get the username of the sender of the message. If it's not a + /// player-sent chat message or the sender couldn't be determined, this + /// will be None. + pub fn username(&self) -> Option<String> { + self.split_sender_and_content().0 + } + + /// Get the UUID of the sender of the message. If it's not a + /// player-sent chat message, this will be None (this is sometimes the case + /// when a server uses a plugin to modify chat messages). + pub fn uuid(&self) -> Option<Uuid> { + match self { + ChatPacket::System(_) => None, + ChatPacket::Player(m) => Some(m.sender), + ChatPacket::Disguised(_) => None, + } + } + + /// Get the content part of the message as a string. This does not preserve + /// formatting codes. If it's not a player-sent chat message or the sender + /// couldn't be determined, this will contain the entire message. + pub fn content(&self) -> String { + self.split_sender_and_content().1 + } + + /// Create a new Chat from a string. This is meant to be used as a + /// convenience function for testing. + pub fn new(message: &str) -> Self { + ChatPacket::System(Arc::new(ClientboundSystemChat { + content: FormattedText::from(message), + overlay: false, + })) + } + + /// Whether this message was sent with /msg (or aliases). It works by + /// checking the translation key, so it won't work on servers that use their + /// own whisper system. + pub fn is_whisper(&self) -> bool { + match self.message() { + FormattedText::Text(_) => false, + FormattedText::Translatable(t) => t.key == "commands.message.display.incoming", + } + } +} + +impl Client { + /// Send a chat message to the server. This only sends the chat packet and + /// not the command packet, which means on some servers you can use this to + /// send chat messages that start with a `/`. 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 fn send_chat_packet(&self, message: &str) { + self.ecs.lock().send_event(SendChatKindEvent { + entity: self.entity, + content: message.to_string(), + kind: ChatKind::Message, + }); + self.run_schedule_sender.send(()).unwrap(); + } + + /// Send a command packet to the server. The `command` argument should not + /// include the slash at the front. + /// + /// You can also just use [`Client::chat`] and start your message with a `/` + /// to send a command. + pub fn send_command_packet(&self, command: &str) { + self.ecs.lock().send_event(SendChatKindEvent { + entity: self.entity, + content: command.to_string(), + kind: ChatKind::Command, + }); + self.run_schedule_sender.send(()).unwrap(); + } + + /// Send a message in chat. + /// + /// ```rust,no_run + /// # use azalea_client::Client; + /// # async fn example(bot: Client) -> anyhow::Result<()> { + /// bot.chat("Hello, world!"); + /// # Ok(()) + /// # } + /// ``` + pub fn chat(&self, content: &str) { + self.ecs.lock().send_event(SendChatEvent { + entity: self.entity, + content: content.to_string(), + }); + self.run_schedule_sender.send(()).unwrap(); + } +} + +/// A client received a chat message packet. +#[derive(Event, Debug, Clone)] +pub struct ChatReceivedEvent { + pub entity: Entity, + pub packet: ChatPacket, +} + +/// Send a chat message (or command, if it starts with a slash) to the server. +#[derive(Event)] +pub struct SendChatEvent { + pub entity: Entity, + pub content: String, +} + +pub fn handle_send_chat_event( + mut events: EventReader<SendChatEvent>, + mut send_chat_kind_events: EventWriter<SendChatKindEvent>, +) { + for event in events.read() { + if event.content.starts_with('/') { + send_chat_kind_events.send(SendChatKindEvent { + entity: event.entity, + content: event.content[1..].to_string(), + kind: ChatKind::Command, + }); + } else { + send_chat_kind_events.send(SendChatKindEvent { + entity: event.entity, + content: event.content.clone(), + kind: ChatKind::Message, + }); + } + } +} + +/// A kind of chat packet, either a chat message or a command. +pub enum ChatKind { + Message, + Command, +} + +// TODO +// MessageSigner, ChatMessageContent, LastSeenMessages +// fn sign_message() -> MessageSignature { +// MessageSignature::default() +// } diff --git a/azalea-client/src/plugins/chunks.rs b/azalea-client/src/plugins/chunks.rs new file mode 100644 index 00000000..cdda3eba --- /dev/null +++ b/azalea-client/src/plugins/chunks.rs @@ -0,0 +1,223 @@ +//! Used for Minecraft's chunk batching introduced in 23w31a (1.20.2). It's used +//! for making the server spread out how often it sends us chunk packets +//! depending on our receiving speed. + +use std::{ + io::Cursor, + ops::Deref, + time::{Duration, Instant}, +}; + +use azalea_core::position::ChunkPos; +use azalea_protocol::packets::game::{ + c_level_chunk_with_light::ClientboundLevelChunkWithLight, + s_chunk_batch_received::ServerboundChunkBatchReceived, +}; +use bevy_app::{App, Plugin, Update}; +use bevy_ecs::prelude::*; +use simdnbt::owned::BaseNbt; +use tracing::{error, trace}; + +use super::packet::game::handle_outgoing_packets; +use crate::{ + InstanceHolder, interact::handle_block_interact_event, inventory::InventorySet, + packet::game::SendPacketEvent, respawn::perform_respawn, +}; + +pub struct ChunksPlugin; +impl Plugin for ChunksPlugin { + fn build(&self, app: &mut App) { + app.add_systems( + Update, + ( + handle_chunk_batch_start_event, + handle_receive_chunk_events, + handle_chunk_batch_finished_event, + ) + .chain() + .before(handle_outgoing_packets) + .before(InventorySet) + .before(handle_block_interact_event) + .before(perform_respawn), + ) + .add_event::<ReceiveChunkEvent>() + .add_event::<ChunkBatchStartEvent>() + .add_event::<ChunkBatchFinishedEvent>(); + } +} + +#[derive(Event)] +pub struct ReceiveChunkEvent { + pub entity: Entity, + pub packet: ClientboundLevelChunkWithLight, +} + +#[derive(Component, Clone, Debug)] +pub struct ChunkBatchInfo { + pub start_time: Instant, + pub aggregated_duration_per_chunk: Duration, + pub old_samples_weight: u32, +} + +#[derive(Event)] +pub struct ChunkBatchStartEvent { + pub entity: Entity, +} +#[derive(Event)] +pub struct ChunkBatchFinishedEvent { + pub entity: Entity, + pub batch_size: u32, +} + +pub fn handle_receive_chunk_events( + mut events: EventReader<ReceiveChunkEvent>, + mut query: Query<&mut InstanceHolder>, +) { + for event in events.read() { + let pos = ChunkPos::new(event.packet.x, event.packet.z); + + let local_player = query.get_mut(event.entity).unwrap(); + + let mut instance = local_player.instance.write(); + let mut partial_instance = local_player.partial_instance.write(); + + // 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 = instance.chunks.get(&pos); + let this_client_has_chunk = partial_instance.chunks.limited_get(&pos).is_some(); + + if !this_client_has_chunk { + if let Some(shared_chunk) = shared_chunk { + trace!("Skipping parsing chunk {pos:?} because we already know about it"); + partial_instance + .chunks + .limited_set(&pos, Some(shared_chunk)); + continue; + } + } + + let heightmaps_nbt = &event.packet.chunk_data.heightmaps; + // necessary to make the unwrap_or work + let empty_nbt = BaseNbt::default(); + let heightmaps = heightmaps_nbt.unwrap_or(&empty_nbt).deref(); + + if let Err(e) = partial_instance.chunks.replace_with_packet_data( + &pos, + &mut Cursor::new(&event.packet.chunk_data.data), + heightmaps, + &mut instance.chunks, + ) { + error!( + "Couldn't set chunk data: {e}. World height: {}", + instance.chunks.height + ); + } + } +} + +impl ChunkBatchInfo { + pub fn batch_finished(&mut self, batch_size: u32) { + if batch_size == 0 { + return; + } + let batch_duration = self.start_time.elapsed(); + let duration_per_chunk = batch_duration / batch_size; + let clamped_duration = Duration::clamp( + duration_per_chunk, + self.aggregated_duration_per_chunk / 3, + self.aggregated_duration_per_chunk * 3, + ); + self.aggregated_duration_per_chunk = + ((self.aggregated_duration_per_chunk * self.old_samples_weight) + clamped_duration) + / (self.old_samples_weight + 1); + self.old_samples_weight = u32::min(49, self.old_samples_weight + 1); + } + + pub fn desired_chunks_per_tick(&self) -> f32 { + (7000000. / self.aggregated_duration_per_chunk.as_nanos() as f64) as f32 + } +} + +pub fn handle_chunk_batch_start_event( + mut query: Query<&mut ChunkBatchInfo>, + mut events: EventReader<ChunkBatchStartEvent>, +) { + for event in events.read() { + if let Ok(mut chunk_batch_info) = query.get_mut(event.entity) { + chunk_batch_info.start_time = Instant::now(); + } + } +} + +pub fn handle_chunk_batch_finished_event( + mut query: Query<&mut ChunkBatchInfo>, + mut events: EventReader<ChunkBatchFinishedEvent>, + mut send_packets: EventWriter<SendPacketEvent>, +) { + for event in events.read() { + if let Ok(mut chunk_batch_info) = query.get_mut(event.entity) { + chunk_batch_info.batch_finished(event.batch_size); + let desired_chunks_per_tick = chunk_batch_info.desired_chunks_per_tick(); + send_packets.send(SendPacketEvent::new( + event.entity, + ServerboundChunkBatchReceived { + desired_chunks_per_tick, + }, + )); + } + } +} + +#[derive(Clone, Debug)] +pub struct ChunkReceiveSpeedAccumulator { + batch_sizes: Vec<u32>, + /// as milliseconds + batch_durations: Vec<u32>, + index: usize, + filled_size: usize, +} +impl ChunkReceiveSpeedAccumulator { + pub fn new(capacity: usize) -> Self { + Self { + batch_sizes: vec![0; capacity], + batch_durations: vec![0; capacity], + index: 0, + filled_size: 0, + } + } + + pub fn accumulate(&mut self, batch_size: u32, batch_duration: Duration) { + self.batch_sizes[self.index] = batch_size; + self.batch_durations[self.index] = + f32::clamp(batch_duration.as_millis() as f32, 0., 15000.) as u32; + self.index = (self.index + 1) % self.batch_sizes.len(); + if self.filled_size < self.batch_sizes.len() { + self.filled_size += 1; + } + } + + pub fn get_millis_per_chunk(&self) -> f64 { + let mut total_batch_size = 0; + let mut total_batch_duration = 0; + for i in 0..self.filled_size { + total_batch_size += self.batch_sizes[i]; + total_batch_duration += self.batch_durations[i]; + } + if total_batch_size == 0 { + return 0.; + } + total_batch_duration as f64 / total_batch_size as f64 + } +} + +impl Default for ChunkBatchInfo { + fn default() -> Self { + Self { + start_time: Instant::now(), + aggregated_duration_per_chunk: Duration::from_millis(2), + old_samples_weight: 1, + } + } +} diff --git a/azalea-client/src/plugins/disconnect.rs b/azalea-client/src/plugins/disconnect.rs new file mode 100644 index 00000000..bd10ac75 --- /dev/null +++ b/azalea-client/src/plugins/disconnect.rs @@ -0,0 +1,103 @@ +//! Disconnect a client from the server. + +use azalea_chat::FormattedText; +use azalea_entity::{EntityBundle, InLoadedChunk, LocalEntity, metadata::PlayerMetadataBundle}; +use bevy_app::{App, Plugin, PostUpdate}; +use bevy_ecs::{ + component::Component, + entity::Entity, + event::{EventReader, EventWriter}, + prelude::Event, + query::{Changed, With}, + schedule::IntoSystemConfigs, + system::{Commands, Query}, +}; +use derive_more::Deref; +use tracing::trace; + +use crate::{ + InstanceHolder, client::JoinedClientBundle, events::LocalPlayerEvents, + raw_connection::RawConnection, +}; + +pub struct DisconnectPlugin; +impl Plugin for DisconnectPlugin { + fn build(&self, app: &mut App) { + app.add_event::<DisconnectEvent>().add_systems( + PostUpdate, + ( + update_read_packets_task_running_component, + disconnect_on_connection_dead, + remove_components_from_disconnected_players, + ) + .chain(), + ); + } +} + +/// An event sent when a client is getting disconnected. +#[derive(Event)] +pub struct DisconnectEvent { + pub entity: Entity, + pub reason: Option<FormattedText>, +} + +/// A system that removes the several components from our clients when they get +/// a [`DisconnectEvent`]. +pub fn remove_components_from_disconnected_players( + mut commands: Commands, + mut events: EventReader<DisconnectEvent>, + mut loaded_by_query: Query<&mut azalea_entity::LoadedBy>, +) { + for DisconnectEvent { entity, .. } in events.read() { + trace!("Got DisconnectEvent for {entity:?}"); + commands + .entity(*entity) + .remove::<JoinedClientBundle>() + .remove::<EntityBundle>() + .remove::<InstanceHolder>() + .remove::<PlayerMetadataBundle>() + .remove::<InLoadedChunk>() + // this makes it close the tcp connection + .remove::<RawConnection>() + // swarm detects when this tx gets dropped to fire SwarmEvent::Disconnect + .remove::<LocalPlayerEvents>(); + // note that we don't remove the client from the ECS, so if they decide + // to reconnect they'll keep their state + + // now we have to remove ourselves from the LoadedBy for every entity. + // in theory this could be inefficient if we have massive swarms... but in + // practice this is fine. + for mut loaded_by in &mut loaded_by_query.iter_mut() { + loaded_by.remove(entity); + } + } +} + +#[derive(Component, Clone, Copy, Debug, Deref)] +pub struct IsConnectionAlive(bool); + +fn update_read_packets_task_running_component( + query: Query<(Entity, &RawConnection)>, + mut commands: Commands, +) { + for (entity, raw_connection) in &query { + let running = raw_connection.is_alive(); + commands.entity(entity).insert(IsConnectionAlive(running)); + } +} + +#[allow(clippy::type_complexity)] +fn disconnect_on_connection_dead( + query: Query<(Entity, &IsConnectionAlive), (Changed<IsConnectionAlive>, With<LocalEntity>)>, + mut disconnect_events: EventWriter<DisconnectEvent>, +) { + for (entity, &is_connection_alive) in &query { + if !*is_connection_alive { + disconnect_events.send(DisconnectEvent { + entity, + reason: None, + }); + } + } +} diff --git a/azalea-client/src/plugins/events.rs b/azalea-client/src/plugins/events.rs new file mode 100644 index 00000000..3d34d75f --- /dev/null +++ b/azalea-client/src/plugins/events.rs @@ -0,0 +1,259 @@ +//! Defines the [`Event`] enum and makes those events trigger when they're sent +//! in the ECS. + +use std::sync::Arc; + +use azalea_chat::FormattedText; +use azalea_core::tick::GameTick; +use azalea_entity::Dead; +use azalea_protocol::packets::game::{ + ClientboundGamePacket, c_player_combat_kill::ClientboundPlayerCombatKill, +}; +use azalea_world::{InstanceName, MinecraftEntityId}; +use bevy_app::{App, Plugin, PreUpdate, Update}; +use bevy_ecs::{ + component::Component, + event::EventReader, + query::{Added, With}, + schedule::IntoSystemConfigs, + system::Query, +}; +use derive_more::{Deref, DerefMut}; +use tokio::sync::mpsc; + +use crate::{ + PlayerInfo, + chat::{ChatPacket, ChatReceivedEvent}, + disconnect::DisconnectEvent, + packet::game::{ + AddPlayerEvent, DeathEvent, KeepAliveEvent, ReceivePacketEvent, RemovePlayerEvent, + UpdatePlayerEvent, + }, +}; + +// (for contributors): +// HOW TO ADD A NEW (packet based) EVENT: +// - Add it as an ECS event first: +// - Make a struct that contains an entity field and some data fields (look +// in packet/game/events.rs for examples. These structs should always have +// their names end with "Event". +// - (the `entity` field is the local player entity that's receiving the +// event) +// - In the GamePacketHandler, you always have a `player` field that you can +// use. +// - Add the event struct in PacketPlugin::build +// - (in the `impl Plugin for PacketPlugin`) +// - To get the event writer, you have to get an EventWriter<ThingEvent>. +// Look at other packets in packet/game/mod.rs for examples. +// +// At this point, you've created a new ECS event. That's annoying for bots to +// use though, so you might wanna add it to the Event enum too: +// - In this file, add a new variant to that Event enum with the same name +// as your event (without the "Event" suffix). +// - Create a new system function like the other ones here, and put that +// system function in the `impl Plugin for EventsPlugin` + +/// Something that happened in-game, such as a tick passing or chat message +/// being sent. +/// +/// Note: Events are sent before they're processed, so for example game ticks +/// happen at the beginning of a tick before anything has happened. +#[derive(Debug, Clone)] +pub enum Event { + /// Happens right after the bot switches into the Game state, but before + /// it's actually spawned. This can be useful for setting the client + /// information with `Client::set_client_information`, so the packet + /// doesn't have to be sent twice. + /// + /// You may want to use [`Event::Login`] instead to wait for the bot to be + /// in the world. + Init, + /// The client is now in the world. Fired when we receive a login packet. + Login, + /// A chat message was sent in the game chat. + Chat(ChatPacket), + /// Happens 20 times per second, but only when the world is loaded. + Tick, + /// We received a packet from the server. + /// + /// ``` + /// # use azalea_client::Event; + /// # use azalea_protocol::packets::game::ClientboundGamePacket; + /// # async fn example(event: Event) { + /// # match event { + /// Event::Packet(packet) => match *packet { + /// ClientboundGamePacket::Login(_) => { + /// println!("login packet"); + /// } + /// _ => {} + /// }, + /// # _ => {} + /// # } + /// # } + /// ``` + Packet(Arc<ClientboundGamePacket>), + /// A player joined the game (or more specifically, was added to the tab + /// list). + AddPlayer(PlayerInfo), + /// A player left the game (or maybe is still in the game and was just + /// removed from the tab list). + RemovePlayer(PlayerInfo), + /// A player was updated in the tab list (gamemode, display + /// name, or latency changed). + UpdatePlayer(PlayerInfo), + /// The client player died in-game. + Death(Option<Arc<ClientboundPlayerCombatKill>>), + /// A `KeepAlive` packet was sent by the server. + KeepAlive(u64), + /// The client disconnected from the server. + Disconnect(Option<FormattedText>), +} + +/// A component that contains an event sender for events that are only +/// received by local players. The receiver for this is returned by +/// [`Client::start_client`]. +/// +/// [`Client::start_client`]: crate::Client::start_client +#[derive(Component, Deref, DerefMut)] +pub struct LocalPlayerEvents(pub mpsc::UnboundedSender<Event>); + +pub struct EventsPlugin; +impl Plugin for EventsPlugin { + fn build(&self, app: &mut App) { + app.add_systems( + Update, + ( + chat_listener, + login_listener, + packet_listener, + add_player_listener, + update_player_listener, + remove_player_listener, + keepalive_listener, + death_listener, + disconnect_listener, + ), + ) + .add_systems( + PreUpdate, + init_listener.before(crate::packet::game::process_packet_events), + ) + .add_systems(GameTick, tick_listener); + } +} + +// when LocalPlayerEvents is added, it means the client just started +pub fn init_listener(query: Query<&LocalPlayerEvents, Added<LocalPlayerEvents>>) { + for local_player_events in &query { + let _ = local_player_events.send(Event::Init); + } +} + +// when MinecraftEntityId is added, it means the player is now in the world +pub fn login_listener(query: Query<&LocalPlayerEvents, Added<MinecraftEntityId>>) { + for local_player_events in &query { + let _ = local_player_events.send(Event::Login); + } +} + +pub fn chat_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader<ChatReceivedEvent>) { + for event in events.read() { + let local_player_events = query + .get(event.entity) + .expect("Non-local entities shouldn't be able to receive chat events"); + let _ = local_player_events.send(Event::Chat(event.packet.clone())); + } +} + +// only tick if we're in a world +pub fn tick_listener(query: Query<&LocalPlayerEvents, With<InstanceName>>) { + for local_player_events in &query { + let _ = local_player_events.send(Event::Tick); + } +} + +pub fn packet_listener( + query: Query<&LocalPlayerEvents>, + mut events: EventReader<ReceivePacketEvent>, +) { + for event in events.read() { + let local_player_events = query + .get(event.entity) + .expect("Non-local entities shouldn't be able to receive packet events"); + let _ = local_player_events.send(Event::Packet(event.packet.clone())); + } +} + +pub fn add_player_listener( + query: Query<&LocalPlayerEvents>, + mut events: EventReader<AddPlayerEvent>, +) { + for event in events.read() { + let local_player_events = query + .get(event.entity) + .expect("Non-local entities shouldn't be able to receive add player events"); + let _ = local_player_events.send(Event::AddPlayer(event.info.clone())); + } +} + +pub fn update_player_listener( + query: Query<&LocalPlayerEvents>, + mut events: EventReader<UpdatePlayerEvent>, +) { + for event in events.read() { + let local_player_events = query + .get(event.entity) + .expect("Non-local entities shouldn't be able to receive update player events"); + let _ = local_player_events.send(Event::UpdatePlayer(event.info.clone())); + } +} + +pub fn remove_player_listener( + query: Query<&LocalPlayerEvents>, + mut events: EventReader<RemovePlayerEvent>, +) { + for event in events.read() { + let local_player_events = query + .get(event.entity) + .expect("Non-local entities shouldn't be able to receive remove player events"); + let _ = local_player_events.send(Event::RemovePlayer(event.info.clone())); + } +} + +pub fn death_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader<DeathEvent>) { + for event in events.read() { + if let Ok(local_player_events) = query.get(event.entity) { + let _ = local_player_events.send(Event::Death(event.packet.clone().map(|p| p.into()))); + } + } +} + +/// Send the "Death" event for [`LocalEntity`]s that died with no reason. +pub fn dead_component_listener(query: Query<&LocalPlayerEvents, Added<Dead>>) { + for local_player_events in &query { + local_player_events.send(Event::Death(None)).unwrap(); + } +} + +pub fn keepalive_listener( + query: Query<&LocalPlayerEvents>, + mut events: EventReader<KeepAliveEvent>, +) { + for event in events.read() { + let local_player_events = query + .get(event.entity) + .expect("Non-local entities shouldn't be able to receive keepalive events"); + let _ = local_player_events.send(Event::KeepAlive(event.id)); + } +} + +pub fn disconnect_listener( + query: Query<&LocalPlayerEvents>, + mut events: EventReader<DisconnectEvent>, +) { + for event in events.read() { + if let Ok(local_player_events) = query.get(event.entity) { + let _ = local_player_events.send(Event::Disconnect(event.reason.clone())); + } + } +} diff --git a/azalea-client/src/plugins/interact.rs b/azalea-client/src/plugins/interact.rs new file mode 100644 index 00000000..1a344cc8 --- /dev/null +++ b/azalea-client/src/plugins/interact.rs @@ -0,0 +1,370 @@ +use std::ops::AddAssign; + +use azalea_block::BlockState; +use azalea_core::{ + block_hit_result::BlockHitResult, + direction::Direction, + game_type::GameMode, + position::{BlockPos, Vec3}, +}; +use azalea_entity::{ + Attributes, EyeHeight, LocalEntity, LookDirection, Position, clamp_look_direction, view_vector, +}; +use azalea_inventory::{ItemStack, ItemStackData, components}; +use azalea_physics::clip::{BlockShapeType, ClipContext, FluidPickType}; +use azalea_protocol::packets::game::{ + s_interact::InteractionHand, + s_swing::ServerboundSwing, + s_use_item_on::{BlockHit, ServerboundUseItemOn}, +}; +use azalea_world::{Instance, InstanceContainer, InstanceName}; +use bevy_app::{App, Plugin, Update}; +use bevy_ecs::{ + component::Component, + entity::Entity, + event::{Event, EventReader, EventWriter}, + query::{Changed, With}, + schedule::IntoSystemConfigs, + system::{Commands, Query, Res}, +}; +use derive_more::{Deref, DerefMut}; +use tracing::warn; + +use super::packet::game::handle_outgoing_packets; +use crate::{ + Client, + attack::handle_attack_event, + inventory::{Inventory, InventorySet}, + local_player::{LocalGameMode, PermissionLevel, PlayerAbilities}, + movement::MoveEventsSet, + packet::game::SendPacketEvent, + respawn::perform_respawn, +}; + +/// A plugin that allows clients to interact with blocks in the world. +pub struct InteractPlugin; +impl Plugin for InteractPlugin { + fn build(&self, app: &mut App) { + app.add_event::<BlockInteractEvent>() + .add_event::<SwingArmEvent>() + .add_systems( + Update, + ( + ( + update_hit_result_component.after(clamp_look_direction), + handle_block_interact_event, + handle_swing_arm_event, + ) + .before(handle_outgoing_packets) + .after(InventorySet) + .after(perform_respawn) + .after(handle_attack_event) + .chain(), + update_modifiers_for_held_item + .after(InventorySet) + .after(MoveEventsSet), + ), + ); + } +} + +impl Client { + /// Right click a block. The behavior of this depends on the target block, + /// and it'll either place the block you're holding in your hand or use the + /// block you clicked (like toggling a lever). + /// + /// Note that this may trigger anticheats as it doesn't take into account + /// whether you're actually looking at the block. + pub fn block_interact(&mut self, position: BlockPos) { + self.ecs.lock().send_event(BlockInteractEvent { + entity: self.entity, + position, + }); + } +} + +/// Right click a block. The behavior of this depends on the target block, +/// and it'll either place the block you're holding in your hand or use the +/// block you clicked (like toggling a lever). +#[derive(Event)] +pub struct BlockInteractEvent { + /// The local player entity that's opening the container. + pub entity: Entity, + /// The coordinates of the container. + pub position: BlockPos, +} + +/// A component that contains the number of changes this client has made to +/// blocks. +#[derive(Component, Copy, Clone, Debug, Default, Deref)] +pub struct CurrentSequenceNumber(u32); + +impl AddAssign<u32> for CurrentSequenceNumber { + fn add_assign(&mut self, rhs: u32) { + self.0 += rhs; + } +} + +/// A component that contains the block that the player is currently looking at. +#[derive(Component, Clone, Debug, Deref, DerefMut)] +pub struct HitResultComponent(BlockHitResult); + +pub fn handle_block_interact_event( + mut events: EventReader<BlockInteractEvent>, + mut query: Query<(Entity, &mut CurrentSequenceNumber, &HitResultComponent)>, + mut send_packet_events: EventWriter<SendPacketEvent>, +) { + for event in events.read() { + let Ok((entity, mut sequence_number, hit_result)) = query.get_mut(event.entity) else { + warn!("Sent BlockInteractEvent for entity that doesn't have the required components"); + continue; + }; + + // TODO: check to make sure we're within the world border + + *sequence_number += 1; + + // minecraft also does the interaction client-side (so it looks like clicking a + // button is instant) but we don't really need that + + // the block_hit data will depend on whether we're looking at the block and + // whether we can reach it + + let block_hit = if hit_result.block_pos == event.position { + // we're looking at the block :) + BlockHit { + block_pos: hit_result.block_pos, + direction: hit_result.direction, + location: hit_result.location, + inside: hit_result.inside, + world_border: hit_result.world_border, + } + } else { + // we're not looking at the block, so make up some numbers + BlockHit { + block_pos: event.position, + direction: Direction::Up, + location: event.position.center(), + inside: false, + world_border: false, + } + }; + + send_packet_events.send(SendPacketEvent::new( + entity, + ServerboundUseItemOn { + hand: InteractionHand::MainHand, + block_hit, + sequence: sequence_number.0, + }, + )); + } +} + +#[allow(clippy::type_complexity)] +pub fn update_hit_result_component( + mut commands: Commands, + mut query: Query<( + Entity, + Option<&mut HitResultComponent>, + &LocalGameMode, + &Position, + &EyeHeight, + &LookDirection, + &InstanceName, + )>, + instance_container: Res<InstanceContainer>, +) { + for (entity, hit_result_ref, game_mode, position, eye_height, look_direction, world_name) in + &mut query + { + let pick_range = if game_mode.current == GameMode::Creative { + 6. + } else { + 4.5 + }; + let eye_position = Vec3 { + x: position.x, + y: position.y + **eye_height as f64, + z: position.z, + }; + + let Some(instance_lock) = instance_container.get(world_name) else { + continue; + }; + let instance = instance_lock.read(); + + let hit_result = pick(look_direction, &eye_position, &instance.chunks, pick_range); + if let Some(mut hit_result_ref) = hit_result_ref { + **hit_result_ref = hit_result; + } else { + commands + .entity(entity) + .insert(HitResultComponent(hit_result)); + } + } +} + +/// Get the block that a player would be looking at if their eyes were at the +/// given direction and position. +/// +/// If you need to get the block the player is looking at right now, use +/// [`HitResultComponent`]. +pub fn pick( + look_direction: &LookDirection, + eye_position: &Vec3, + chunks: &azalea_world::ChunkStorage, + pick_range: f64, +) -> BlockHitResult { + let view_vector = view_vector(look_direction); + let end_position = eye_position + &(view_vector * pick_range); + azalea_physics::clip::clip( + chunks, + ClipContext { + from: *eye_position, + to: end_position, + block_shape_type: BlockShapeType::Outline, + fluid_pick_type: FluidPickType::None, + }, + ) +} + +/// Whether we can't interact with the block, based on your gamemode. If +/// this is false, then we can interact with the block. +/// +/// Passing the inventory, block position, and instance is necessary for the +/// adventure mode check. +pub fn check_is_interaction_restricted( + instance: &Instance, + block_pos: &BlockPos, + game_mode: &GameMode, + inventory: &Inventory, +) -> bool { + match game_mode { + GameMode::Adventure => { + // vanilla checks for abilities.mayBuild here but servers have no + // way of modifying that + + let held_item = inventory.held_item(); + match &held_item { + ItemStack::Present(item) => { + let block = instance.chunks.get_block_state(block_pos); + let Some(block) = block else { + // block isn't loaded so just say that it is restricted + return true; + }; + check_block_can_be_broken_by_item_in_adventure_mode(item, &block) + } + _ => true, + } + } + GameMode::Spectator => true, + _ => false, + } +} + +/// Check if the item has the `CanDestroy` tag for the block. +pub fn check_block_can_be_broken_by_item_in_adventure_mode( + item: &ItemStackData, + _block: &BlockState, +) -> bool { + // minecraft caches the last checked block but that's kind of an unnecessary + // optimization and makes the code too complicated + + if !item.components.has::<components::CanBreak>() { + // no CanDestroy tag + return false; + }; + + false + + // for block_predicate in can_destroy { + // // TODO + // // defined in BlockPredicateArgument.java + // } + + // true +} + +pub fn can_use_game_master_blocks( + abilities: &PlayerAbilities, + permission_level: &PermissionLevel, +) -> bool { + abilities.instant_break && **permission_level >= 2 +} + +/// Swing your arm. This is purely a visual effect and won't interact with +/// anything in the world. +#[derive(Event)] +pub struct SwingArmEvent { + pub entity: Entity, +} +pub fn handle_swing_arm_event( + mut events: EventReader<SwingArmEvent>, + mut send_packet_events: EventWriter<SendPacketEvent>, +) { + for event in events.read() { + send_packet_events.send(SendPacketEvent::new( + event.entity, + ServerboundSwing { + hand: InteractionHand::MainHand, + }, + )); + } +} + +#[allow(clippy::type_complexity)] +fn update_modifiers_for_held_item( + mut query: Query<(&mut Attributes, &Inventory), (With<LocalEntity>, Changed<Inventory>)>, +) { + for (mut attributes, inventory) in &mut query { + let held_item = inventory.held_item(); + + use azalea_registry::Item; + let added_attack_speed = match held_item.kind() { + Item::WoodenSword => -2.4, + Item::WoodenShovel => -3.0, + Item::WoodenPickaxe => -2.8, + Item::WoodenAxe => -3.2, + Item::WoodenHoe => -3.0, + + Item::StoneSword => -2.4, + Item::StoneShovel => -3.0, + Item::StonePickaxe => -2.8, + Item::StoneAxe => -3.2, + Item::StoneHoe => -2.0, + + Item::GoldenSword => -2.4, + Item::GoldenShovel => -3.0, + Item::GoldenPickaxe => -2.8, + Item::GoldenAxe => -3.0, + Item::GoldenHoe => -3.0, + + Item::IronSword => -2.4, + Item::IronShovel => -3.0, + Item::IronPickaxe => -2.8, + Item::IronAxe => -3.1, + Item::IronHoe => -1.0, + + Item::DiamondSword => -2.4, + Item::DiamondShovel => -3.0, + Item::DiamondPickaxe => -2.8, + Item::DiamondAxe => -3.0, + Item::DiamondHoe => 0.0, + + Item::NetheriteSword => -2.4, + Item::NetheriteShovel => -3.0, + Item::NetheritePickaxe => -2.8, + Item::NetheriteAxe => -3.0, + Item::NetheriteHoe => 0.0, + + Item::Trident => -2.9, + _ => 0., + }; + attributes + .attack_speed + .insert(azalea_entity::attributes::base_attack_speed_modifier( + added_attack_speed, + )); + } +} diff --git a/azalea-client/src/plugins/inventory.rs b/azalea-client/src/plugins/inventory.rs new file mode 100644 index 00000000..3f823ca2 --- /dev/null +++ b/azalea-client/src/plugins/inventory.rs @@ -0,0 +1,766 @@ +use std::collections::{HashMap, HashSet}; + +use azalea_chat::FormattedText; +pub use azalea_inventory::*; +use azalea_inventory::{ + item::MaxStackSizeExt, + operations::{ + ClickOperation, CloneClick, PickupAllClick, PickupClick, QuickCraftKind, QuickCraftStatus, + QuickCraftStatusKind, QuickMoveClick, ThrowClick, + }, +}; +use azalea_protocol::packets::game::{ + s_container_click::ServerboundContainerClick, s_container_close::ServerboundContainerClose, + s_set_carried_item::ServerboundSetCarriedItem, +}; +use azalea_registry::MenuKind; +use bevy_app::{App, Plugin, Update}; +use bevy_ecs::{ + component::Component, + entity::Entity, + event::EventReader, + prelude::{Event, EventWriter}, + schedule::{IntoSystemConfigs, SystemSet}, + system::Query, +}; +use tracing::warn; + +use super::packet::game::handle_outgoing_packets; +use crate::{ + Client, local_player::PlayerAbilities, packet::game::SendPacketEvent, respawn::perform_respawn, +}; + +pub struct InventoryPlugin; +impl Plugin for InventoryPlugin { + fn build(&self, app: &mut App) { + app.add_event::<ClientSideCloseContainerEvent>() + .add_event::<MenuOpenedEvent>() + .add_event::<CloseContainerEvent>() + .add_event::<ContainerClickEvent>() + .add_event::<SetContainerContentEvent>() + .add_event::<SetSelectedHotbarSlotEvent>() + .add_systems( + Update, + ( + handle_set_selected_hotbar_slot_event, + handle_menu_opened_event, + handle_set_container_content_event, + handle_container_click_event, + handle_container_close_event.before(handle_outgoing_packets), + handle_client_side_close_container_event, + ) + .chain() + .in_set(InventorySet) + .before(perform_respawn), + ); + } +} + +#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] +pub struct InventorySet; + +impl Client { + /// Return the menu that is currently open. If no menu is open, this will + /// have the player's inventory. + pub fn menu(&self) -> Menu { + let mut ecs = self.ecs.lock(); + let inventory = self.query::<&Inventory>(&mut ecs); + inventory.menu().clone() + } +} + +/// A component present on all local players that have an inventory. +#[derive(Component, Debug, Clone)] +pub struct Inventory { + /// A component that contains the player's inventory menu. This is + /// guaranteed to be a `Menu::Player`. + /// + /// We keep it as a [`Menu`] since `Menu` has some useful functions that + /// bare [`azalea_inventory::Player`] doesn't have. + pub inventory_menu: azalea_inventory::Menu, + + /// The ID of the container that's currently open. Its value is not + /// guaranteed to be anything specific, and may change every time you open a + /// container (unless it's 0, in which case it means that no container is + /// open). + pub id: i32, + /// The current container menu that the player has open. If no container is + /// open, this will be `None`. + pub container_menu: Option<azalea_inventory::Menu>, + /// The custom name of the menu that's currently open. This is Some when + /// `container_menu` is Some. + pub container_menu_title: Option<FormattedText>, + /// The item that is currently held by the cursor. `Slot::Empty` if nothing + /// is currently being held. + /// + /// This is different from [`Self::selected_hotbar_slot`], which is the + /// item that's selected in the hotbar. + pub carried: ItemStack, + /// An identifier used by the server to track client inventory desyncs. This + /// is sent on every container click, and it's only ever updated when the + /// server sends a new container update. + pub state_id: u32, + + pub quick_craft_status: QuickCraftStatusKind, + pub quick_craft_kind: QuickCraftKind, + /// A set of the indexes of the slots that have been right clicked in + /// this "quick craft". + pub quick_craft_slots: HashSet<u16>, + + /// The index of the item in the hotbar that's currently being held by the + /// player. This MUST be in the range 0..9 (not including 9). + /// + /// In a vanilla client this is changed by pressing the number keys or using + /// the scroll wheel. + pub selected_hotbar_slot: u8, +} + +impl Inventory { + /// Returns a reference to the currently active menu. If a container is open + /// it'll return [`Self::container_menu`], otherwise + /// [`Self::inventory_menu`]. + /// + /// Use [`Self::menu_mut`] if you need a mutable reference. + pub fn menu(&self) -> &azalea_inventory::Menu { + match &self.container_menu { + Some(menu) => menu, + _ => &self.inventory_menu, + } + } + + /// Returns a mutable reference to the currently active menu. If a container + /// is open it'll return [`Self::container_menu`], otherwise + /// [`Self::inventory_menu`]. + /// + /// Use [`Self::menu`] if you don't need a mutable reference. + pub fn menu_mut(&mut self) -> &mut azalea_inventory::Menu { + match &mut self.container_menu { + Some(menu) => menu, + _ => &mut self.inventory_menu, + } + } + + /// Modify the inventory as if the given operation was performed on it. + pub fn simulate_click( + &mut self, + operation: &ClickOperation, + player_abilities: &PlayerAbilities, + ) { + if let ClickOperation::QuickCraft(quick_craft) = operation { + let last_quick_craft_status_tmp = self.quick_craft_status.clone(); + self.quick_craft_status = last_quick_craft_status_tmp.clone(); + let last_quick_craft_status = last_quick_craft_status_tmp; + + // no carried item, reset + if self.carried.is_empty() { + return self.reset_quick_craft(); + } + // if we were starting or ending, or now we aren't ending and the status + // changed, reset + if (last_quick_craft_status == QuickCraftStatusKind::Start + || last_quick_craft_status == QuickCraftStatusKind::End + || self.quick_craft_status != QuickCraftStatusKind::End) + && (self.quick_craft_status != last_quick_craft_status) + { + return self.reset_quick_craft(); + } + if self.quick_craft_status == QuickCraftStatusKind::Start { + self.quick_craft_kind = quick_craft.kind.clone(); + if self.quick_craft_kind == QuickCraftKind::Middle && player_abilities.instant_break + { + self.quick_craft_status = QuickCraftStatusKind::Add; + self.quick_craft_slots.clear(); + } else { + self.reset_quick_craft(); + } + return; + } + if let QuickCraftStatus::Add { slot } = quick_craft.status { + let slot_item = self.menu().slot(slot as usize); + if let Some(slot_item) = slot_item { + if let ItemStack::Present(carried) = &self.carried { + // minecraft also checks slot.may_place(carried) and + // menu.can_drag_to(slot) + // but they always return true so they're not relevant for us + if can_item_quick_replace(slot_item, &self.carried, true) + && (self.quick_craft_kind == QuickCraftKind::Right + || carried.count as usize > self.quick_craft_slots.len()) + { + self.quick_craft_slots.insert(slot); + } + } + } + return; + } + if self.quick_craft_status == QuickCraftStatusKind::End { + if !self.quick_craft_slots.is_empty() { + if self.quick_craft_slots.len() == 1 { + // if we only clicked one slot, then turn this + // QuickCraftClick into a PickupClick + let slot = *self.quick_craft_slots.iter().next().unwrap(); + self.reset_quick_craft(); + self.simulate_click( + &match self.quick_craft_kind { + QuickCraftKind::Left => { + PickupClick::Left { slot: Some(slot) }.into() + } + QuickCraftKind::Right => { + PickupClick::Left { slot: Some(slot) }.into() + } + QuickCraftKind::Middle => { + // idk just do nothing i guess + return; + } + }, + player_abilities, + ); + return; + } + + let ItemStack::Present(mut carried) = self.carried.clone() else { + // this should never happen + return self.reset_quick_craft(); + }; + + let mut carried_count = carried.count; + let mut quick_craft_slots_iter = self.quick_craft_slots.iter(); + + loop { + let mut slot: &ItemStack; + let mut slot_index: u16; + let mut item_stack: &ItemStack; + + loop { + let Some(&next_slot) = quick_craft_slots_iter.next() else { + carried.count = carried_count; + self.carried = ItemStack::Present(carried); + return self.reset_quick_craft(); + }; + + slot = self.menu().slot(next_slot as usize).unwrap(); + slot_index = next_slot; + item_stack = &self.carried; + + if slot.is_present() + && can_item_quick_replace(slot, item_stack, true) + // this always returns true in most cases + // && slot.may_place(item_stack) + && ( + self.quick_craft_kind == QuickCraftKind::Middle + || item_stack.count() >= self.quick_craft_slots.len() as i32 + ) + { + break; + } + } + + // get the ItemStackData for the slot + let ItemStack::Present(slot) = slot else { + unreachable!("the loop above requires the slot to be present to break") + }; + + // if self.can_drag_to(slot) { + let mut new_carried = carried.clone(); + let slot_item_count = slot.count; + get_quick_craft_slot_count( + &self.quick_craft_slots, + &self.quick_craft_kind, + &mut new_carried, + slot_item_count, + ); + let max_stack_size = i32::min( + new_carried.kind.max_stack_size(), + i32::min( + new_carried.kind.max_stack_size(), + slot.kind.max_stack_size(), + ), + ); + if new_carried.count > max_stack_size { + new_carried.count = max_stack_size; + } + + carried_count -= new_carried.count - slot_item_count; + // we have to inline self.menu_mut() here to avoid the borrow checker + // complaining + let menu = match &mut self.container_menu { + Some(menu) => menu, + _ => &mut self.inventory_menu, + }; + *menu.slot_mut(slot_index as usize).unwrap() = + ItemStack::Present(new_carried); + } + } + } else { + return self.reset_quick_craft(); + } + } + // the quick craft status should always be in start if we're not in quick craft + // mode + if self.quick_craft_status != QuickCraftStatusKind::Start { + return self.reset_quick_craft(); + } + + match operation { + // left clicking outside inventory + ClickOperation::Pickup(PickupClick::Left { slot: None }) => { + if self.carried.is_present() { + // vanilla has `player.drop`s but they're only used + // server-side + // they're included as comments here in case you want to adapt this for a server + // implementation + + // player.drop(self.carried, true); + self.carried = ItemStack::Empty; + } + } + ClickOperation::Pickup(PickupClick::Right { slot: None }) => { + if self.carried.is_present() { + let _item = self.carried.split(1); + // player.drop(item, true); + } + } + ClickOperation::Pickup( + PickupClick::Left { slot: Some(slot) } | PickupClick::Right { slot: Some(slot) }, + ) => { + let Some(slot_item) = self.menu().slot(*slot as usize) else { + return; + }; + let carried = &self.carried; + // vanilla does a check called tryItemClickBehaviourOverride + // here + // i don't understand it so i didn't implement it + match slot_item { + ItemStack::Empty => if carried.is_present() {}, + ItemStack::Present(_) => todo!(), + } + } + ClickOperation::QuickMove( + QuickMoveClick::Left { slot } | QuickMoveClick::Right { slot }, + ) => { + // in vanilla it also tests if QuickMove has a slot index of -999 + // but i don't think that's ever possible so it's not covered here + loop { + let new_slot_item = self.menu_mut().quick_move_stack(*slot as usize); + let slot_item = self.menu().slot(*slot as usize).unwrap(); + if new_slot_item.is_empty() || slot_item != &new_slot_item { + break; + } + } + } + ClickOperation::Swap(s) => { + let source_slot_index = s.source_slot as usize; + let target_slot_index = s.target_slot as usize; + + let Some(source_slot) = self.menu().slot(source_slot_index) else { + return; + }; + let Some(target_slot) = self.menu().slot(target_slot_index) else { + return; + }; + if source_slot.is_empty() && target_slot.is_empty() { + return; + } + + if target_slot.is_empty() { + if self.menu().may_pickup(source_slot_index) { + let source_slot = source_slot.clone(); + let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap(); + *target_slot = source_slot; + } + } else if source_slot.is_empty() { + let ItemStack::Present(target_item) = target_slot else { + unreachable!("target slot is not empty but is not present"); + }; + if self.menu().may_place(source_slot_index, target_item) { + // get the target_item but mutable + let source_max_stack_size = self.menu().max_stack_size(source_slot_index); + + let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap(); + let new_source_slot = target_slot.split(source_max_stack_size); + *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot; + } + } else if self.menu().may_pickup(source_slot_index) { + let ItemStack::Present(target_item) = target_slot else { + unreachable!("target slot is not empty but is not present"); + }; + if self.menu().may_place(source_slot_index, target_item) { + let source_max_stack = self.menu().max_stack_size(source_slot_index); + if target_slot.count() > source_max_stack as i32 { + // if there's more than the max stack size in the target slot + + let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap(); + let new_source_slot = target_slot.split(source_max_stack); + *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot; + // if !self.inventory_menu.add(new_source_slot) { + // player.drop(new_source_slot, true); + // } + } else { + // normal swap + let new_target_slot = source_slot.clone(); + let new_source_slot = target_slot.clone(); + + let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap(); + *target_slot = new_target_slot; + + let source_slot = self.menu_mut().slot_mut(source_slot_index).unwrap(); + *source_slot = new_source_slot; + } + } + } + } + ClickOperation::Clone(CloneClick { slot }) => { + if !player_abilities.instant_break || self.carried.is_present() { + return; + } + let Some(source_slot) = self.menu().slot(*slot as usize) else { + return; + }; + let ItemStack::Present(source_item) = source_slot else { + return; + }; + let mut new_carried = source_item.clone(); + new_carried.count = new_carried.kind.max_stack_size(); + self.carried = ItemStack::Present(new_carried); + } + ClickOperation::Throw(c) => { + if self.carried.is_present() { + return; + } + + let (ThrowClick::Single { slot: slot_index } + | ThrowClick::All { slot: slot_index }) = c; + let slot_index = *slot_index as usize; + + let Some(slot) = self.menu_mut().slot_mut(slot_index) else { + return; + }; + let ItemStack::Present(slot_item) = slot else { + return; + }; + + let dropping_count = match c { + ThrowClick::Single { .. } => 1, + ThrowClick::All { .. } => slot_item.count, + }; + + let _dropping = slot_item.split(dropping_count as u32); + // player.drop(dropping, true); + } + ClickOperation::PickupAll(PickupAllClick { + slot: source_slot_index, + reversed, + }) => { + let source_slot_index = *source_slot_index as usize; + + let source_slot = self.menu().slot(source_slot_index).unwrap(); + let target_slot = self.carried.clone(); + + if target_slot.is_empty() + || (source_slot.is_present() && self.menu().may_pickup(source_slot_index)) + { + return; + } + + let ItemStack::Present(target_slot_item) = &target_slot else { + unreachable!("target slot is not empty but is not present"); + }; + + for round in 0..2 { + let iterator: Box<dyn Iterator<Item = usize>> = if *reversed { + Box::new((0..self.menu().len()).rev()) + } else { + Box::new(0..self.menu().len()) + }; + + for i in iterator { + if target_slot_item.count < target_slot_item.kind.max_stack_size() { + let checking_slot = self.menu().slot(i).unwrap(); + if let ItemStack::Present(checking_item) = checking_slot { + if can_item_quick_replace(checking_slot, &target_slot, true) + && self.menu().may_pickup(i) + && (round != 0 + || checking_item.count + != checking_item.kind.max_stack_size()) + { + // get the checking_slot and checking_item again but mutable + let checking_slot = self.menu_mut().slot_mut(i).unwrap(); + + let taken_item = + checking_slot.split(checking_slot.count() as u32); + + // now extend the carried item + let target_slot = &mut self.carried; + let ItemStack::Present(target_slot_item) = target_slot else { + unreachable!("target slot is not empty but is not present"); + }; + target_slot_item.count += taken_item.count(); + } + } + } + } + } + } + _ => {} + } + } + + fn reset_quick_craft(&mut self) { + self.quick_craft_status = QuickCraftStatusKind::Start; + self.quick_craft_slots.clear(); + } + + /// Get the item in the player's hotbar that is currently being held. + pub fn held_item(&self) -> ItemStack { + let inventory = &self.inventory_menu; + let hotbar_items = &inventory.slots()[inventory.hotbar_slots_range()]; + hotbar_items[self.selected_hotbar_slot as usize].clone() + } +} + +fn can_item_quick_replace( + target_slot: &ItemStack, + item: &ItemStack, + ignore_item_count: bool, +) -> bool { + let ItemStack::Present(target_slot) = target_slot else { + return false; + }; + let ItemStack::Present(item) = item else { + // i *think* this is what vanilla does + // not 100% sure lol probably doesn't matter though + return false; + }; + + if !item.is_same_item_and_components(target_slot) { + return false; + } + let count = target_slot.count as u16 + + if ignore_item_count { + 0 + } else { + item.count as u16 + }; + count <= item.kind.max_stack_size() as u16 +} + +fn get_quick_craft_slot_count( + quick_craft_slots: &HashSet<u16>, + quick_craft_kind: &QuickCraftKind, + item: &mut ItemStackData, + slot_item_count: i32, +) { + item.count = match quick_craft_kind { + QuickCraftKind::Left => item.count / quick_craft_slots.len() as i32, + QuickCraftKind::Right => 1, + QuickCraftKind::Middle => item.kind.max_stack_size(), + }; + item.count += slot_item_count; +} + +impl Default for Inventory { + fn default() -> Self { + Inventory { + inventory_menu: Menu::Player(azalea_inventory::Player::default()), + id: 0, + container_menu: None, + container_menu_title: None, + carried: ItemStack::Empty, + state_id: 0, + quick_craft_status: QuickCraftStatusKind::Start, + quick_craft_kind: QuickCraftKind::Middle, + quick_craft_slots: HashSet::new(), + selected_hotbar_slot: 0, + } + } +} + +/// Sent from the server when a menu (like a chest or crafting table) was +/// opened by the client. +#[derive(Event, Debug)] +pub struct MenuOpenedEvent { + pub entity: Entity, + pub window_id: i32, + pub menu_type: MenuKind, + pub title: FormattedText, +} +fn handle_menu_opened_event( + mut events: EventReader<MenuOpenedEvent>, + mut query: Query<&mut Inventory>, +) { + for event in events.read() { + let mut inventory = query.get_mut(event.entity).unwrap(); + inventory.id = event.window_id; + inventory.container_menu = Some(Menu::from_kind(event.menu_type)); + inventory.container_menu_title = Some(event.title.clone()); + } +} + +/// Tell the server that we want to close a container. +/// +/// Note that this is also sent when the client closes its own inventory, even +/// though there is no packet for opening its inventory. +#[derive(Event)] +pub struct CloseContainerEvent { + pub entity: Entity, + /// The ID of the container to close. 0 for the player's inventory. If this + /// is not the same as the currently open inventory, nothing will happen. + pub id: i32, +} +fn handle_container_close_event( + query: Query<(Entity, &Inventory)>, + mut events: EventReader<CloseContainerEvent>, + mut client_side_events: EventWriter<ClientSideCloseContainerEvent>, + mut send_packet_events: EventWriter<SendPacketEvent>, +) { + for event in events.read() { + let (entity, inventory) = query.get(event.entity).unwrap(); + if event.id != inventory.id { + warn!( + "Tried to close container with ID {}, but the current container ID is {}", + event.id, inventory.id + ); + continue; + } + + send_packet_events.send(SendPacketEvent::new( + entity, + ServerboundContainerClose { + container_id: inventory.id, + }, + )); + client_side_events.send(ClientSideCloseContainerEvent { + entity: event.entity, + }); + } +} + +/// Close a container without notifying the server. +/// +/// Note that this also gets fired when we get a [`CloseContainerEvent`]. +#[derive(Event)] +pub struct ClientSideCloseContainerEvent { + pub entity: Entity, +} +pub fn handle_client_side_close_container_event( + mut events: EventReader<ClientSideCloseContainerEvent>, + mut query: Query<&mut Inventory>, +) { + for event in events.read() { + let mut inventory = query.get_mut(event.entity).unwrap(); + inventory.container_menu = None; + inventory.id = 0; + inventory.container_menu_title = None; + } +} + +#[derive(Event, Debug)] +pub struct ContainerClickEvent { + pub entity: Entity, + pub window_id: i32, + pub operation: ClickOperation, +} +pub fn handle_container_click_event( + mut query: Query<(Entity, &mut Inventory)>, + mut events: EventReader<ContainerClickEvent>, + mut send_packet_events: EventWriter<SendPacketEvent>, +) { + for event in events.read() { + let (entity, mut inventory) = query.get_mut(event.entity).unwrap(); + if inventory.id != event.window_id { + warn!( + "Tried to click container with ID {}, but the current container ID is {}", + event.window_id, inventory.id + ); + continue; + } + + let menu = inventory.menu_mut(); + let old_slots = menu.slots().clone(); + + // menu.click(&event.operation); + + // see which slots changed after clicking and put them in the hashmap + // the server uses this to check if we desynced + let mut changed_slots: HashMap<u16, ItemStack> = HashMap::new(); + for (slot_index, old_slot) in old_slots.iter().enumerate() { + let new_slot = &menu.slots()[slot_index]; + if old_slot != new_slot { + changed_slots.insert(slot_index as u16, new_slot.clone()); + } + } + + send_packet_events.send(SendPacketEvent::new( + entity, + ServerboundContainerClick { + container_id: event.window_id, + state_id: inventory.state_id, + slot_num: event.operation.slot_num().map(|n| n as i16).unwrap_or(-999), + button_num: event.operation.button_num(), + click_type: event.operation.click_type(), + changed_slots, + carried_item: inventory.carried.clone(), + }, + )); + } +} + +/// Sent from the server when the contents of a container are replaced. Usually +/// triggered by the `ContainerSetContent` packet. +#[derive(Event)] +pub struct SetContainerContentEvent { + pub entity: Entity, + pub slots: Vec<ItemStack>, + pub container_id: i32, +} +fn handle_set_container_content_event( + mut events: EventReader<SetContainerContentEvent>, + mut query: Query<&mut Inventory>, +) { + for event in events.read() { + let mut inventory = query.get_mut(event.entity).unwrap(); + + if event.container_id != inventory.id { + warn!( + "Tried to set container content with ID {}, but the current container ID is {}", + event.container_id, inventory.id + ); + continue; + } + + let menu = inventory.menu_mut(); + for (i, slot) in event.slots.iter().enumerate() { + if let Some(slot_mut) = menu.slot_mut(i) { + *slot_mut = slot.clone(); + } + } + } +} + +#[derive(Event)] +pub struct SetSelectedHotbarSlotEvent { + pub entity: Entity, + /// The hotbar slot to select. This should be in the range 0..=8. + pub slot: u8, +} +fn handle_set_selected_hotbar_slot_event( + mut events: EventReader<SetSelectedHotbarSlotEvent>, + mut send_packet_events: EventWriter<SendPacketEvent>, + mut query: Query<&mut Inventory>, +) { + for event in events.read() { + let mut inventory = query.get_mut(event.entity).unwrap(); + + // if the slot is already selected, don't send a packet + if inventory.selected_hotbar_slot == event.slot { + continue; + } + + inventory.selected_hotbar_slot = event.slot; + send_packet_events.send(SendPacketEvent::new( + event.entity, + ServerboundSetCarriedItem { + slot: event.slot as u16, + }, + )); + } +} diff --git a/azalea-client/src/plugins/mining.rs b/azalea-client/src/plugins/mining.rs new file mode 100644 index 00000000..beb380b7 --- /dev/null +++ b/azalea-client/src/plugins/mining.rs @@ -0,0 +1,644 @@ +use azalea_block::{Block, BlockState, fluid_state::FluidState}; +use azalea_core::{direction::Direction, game_type::GameMode, position::BlockPos, tick::GameTick}; +use azalea_entity::{FluidOnEyes, Physics, mining::get_mine_progress}; +use azalea_inventory::ItemStack; +use azalea_physics::PhysicsSet; +use azalea_protocol::packets::game::s_player_action::{self, ServerboundPlayerAction}; +use azalea_world::{InstanceContainer, InstanceName}; +use bevy_app::{App, Plugin, Update}; +use bevy_ecs::prelude::*; +use derive_more::{Deref, DerefMut}; + +use crate::{ + Client, + interact::{ + CurrentSequenceNumber, HitResultComponent, SwingArmEvent, can_use_game_master_blocks, + check_is_interaction_restricted, + }, + inventory::{Inventory, InventorySet}, + local_player::{LocalGameMode, PermissionLevel, PlayerAbilities}, + movement::MoveEventsSet, + packet::game::SendPacketEvent, +}; + +/// A plugin that allows clients to break blocks in the world. +pub struct MiningPlugin; +impl Plugin for MiningPlugin { + fn build(&self, app: &mut App) { + app.add_event::<StartMiningBlockEvent>() + .add_event::<StartMiningBlockWithDirectionEvent>() + .add_event::<FinishMiningBlockEvent>() + .add_event::<StopMiningBlockEvent>() + .add_event::<MineBlockProgressEvent>() + .add_event::<AttackBlockEvent>() + .add_systems( + GameTick, + (continue_mining_block, handle_auto_mine) + .chain() + .before(PhysicsSet), + ) + .add_systems( + Update, + ( + handle_start_mining_block_event, + handle_start_mining_block_with_direction_event, + handle_finish_mining_block_event, + handle_stop_mining_block_event, + ) + .chain() + .in_set(MiningSet) + .after(InventorySet) + .after(MoveEventsSet) + .before(azalea_entity::update_bounding_box) + .after(azalea_entity::update_fluid_on_eyes) + .after(crate::interact::update_hit_result_component) + .after(crate::attack::handle_attack_event) + .after(crate::interact::handle_block_interact_event) + .before(crate::interact::handle_swing_arm_event), + ); + } +} + +/// The Bevy system set for things related to mining. +#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] +pub struct MiningSet; + +impl Client { + pub fn start_mining(&mut self, position: BlockPos) { + self.ecs.lock().send_event(StartMiningBlockEvent { + entity: self.entity, + position, + }); + } + + /// When enabled, the bot will mine any block that it is looking at if it is + /// reachable. + pub fn left_click_mine(&self, enabled: bool) { + let mut ecs = self.ecs.lock(); + let mut entity_mut = ecs.entity_mut(self.entity); + + if enabled { + entity_mut.insert(LeftClickMine); + } else { + entity_mut.remove::<LeftClickMine>(); + } + } +} + +/// A component that simulates the client holding down left click to mine the +/// block that it's facing, but this only interacts with blocks and not +/// entities. +#[derive(Component)] +pub struct LeftClickMine; + +#[allow(clippy::type_complexity)] +fn handle_auto_mine( + mut query: Query< + ( + &HitResultComponent, + Entity, + Option<&Mining>, + &Inventory, + &MineBlockPos, + &MineItem, + ), + With<LeftClickMine>, + >, + mut start_mining_block_event: EventWriter<StartMiningBlockEvent>, + mut stop_mining_block_event: EventWriter<StopMiningBlockEvent>, +) { + for ( + hit_result_component, + entity, + mining, + inventory, + current_mining_pos, + current_mining_item, + ) in &mut query.iter_mut() + { + let block_pos = hit_result_component.block_pos; + + if (mining.is_none() + || !is_same_mining_target( + block_pos, + inventory, + current_mining_pos, + current_mining_item, + )) + && !hit_result_component.miss + { + start_mining_block_event.send(StartMiningBlockEvent { + entity, + position: block_pos, + }); + } else if mining.is_some() && hit_result_component.miss { + stop_mining_block_event.send(StopMiningBlockEvent { entity }); + } + } +} + +/// Information about the block we're currently mining. This is only present if +/// we're currently mining a block. +#[derive(Component)] +pub struct Mining { + pub pos: BlockPos, + pub dir: Direction, +} + +/// Start mining the block at the given position. +/// +/// If we're looking at the block then the correct direction will be used, +/// otherwise it'll be [`Direction::Down`]. +#[derive(Event)] +pub struct StartMiningBlockEvent { + pub entity: Entity, + pub position: BlockPos, +} +fn handle_start_mining_block_event( + mut events: EventReader<StartMiningBlockEvent>, + mut start_mining_events: EventWriter<StartMiningBlockWithDirectionEvent>, + mut query: Query<&HitResultComponent>, +) { + for event in events.read() { + let hit_result = query.get_mut(event.entity).unwrap(); + let direction = if hit_result.block_pos == event.position { + // we're looking at the block + hit_result.direction + } else { + // we're not looking at the block, arbitrary direction + Direction::Down + }; + start_mining_events.send(StartMiningBlockWithDirectionEvent { + entity: event.entity, + position: event.position, + direction, + }); + } +} + +#[derive(Event)] +pub struct StartMiningBlockWithDirectionEvent { + pub entity: Entity, + pub position: BlockPos, + pub direction: Direction, +} +#[allow(clippy::too_many_arguments, clippy::type_complexity)] +fn handle_start_mining_block_with_direction_event( + mut events: EventReader<StartMiningBlockWithDirectionEvent>, + mut finish_mining_events: EventWriter<FinishMiningBlockEvent>, + mut send_packet_events: EventWriter<SendPacketEvent>, + mut attack_block_events: EventWriter<AttackBlockEvent>, + mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>, + mut query: Query<( + &InstanceName, + &LocalGameMode, + &Inventory, + &FluidOnEyes, + &Physics, + Option<&Mining>, + &mut CurrentSequenceNumber, + &mut MineDelay, + &mut MineProgress, + &mut MineTicks, + &mut MineItem, + &mut MineBlockPos, + )>, + instances: Res<InstanceContainer>, + mut commands: Commands, +) { + for event in events.read() { + let ( + instance_name, + game_mode, + inventory, + fluid_on_eyes, + physics, + mining, + mut sequence_number, + mut mine_delay, + mut mine_progress, + mut mine_ticks, + mut current_mining_item, + mut current_mining_pos, + ) = query.get_mut(event.entity).unwrap(); + + let instance_lock = instances.get(instance_name).unwrap(); + let instance = instance_lock.read(); + if check_is_interaction_restricted( + &instance, + &event.position, + &game_mode.current, + inventory, + ) { + continue; + } + // TODO (when world border is implemented): vanilla ignores if the block + // is outside of the worldborder + + if game_mode.current == GameMode::Creative { + *sequence_number += 1; + finish_mining_events.send(FinishMiningBlockEvent { + entity: event.entity, + position: event.position, + }); + **mine_delay = 5; + } else if mining.is_none() + || !is_same_mining_target( + event.position, + inventory, + ¤t_mining_pos, + ¤t_mining_item, + ) + { + if mining.is_some() { + // send a packet to stop mining since we just changed target + send_packet_events.send(SendPacketEvent::new( + event.entity, + ServerboundPlayerAction { + action: s_player_action::Action::AbortDestroyBlock, + pos: current_mining_pos + .expect("IsMining is true so MineBlockPos must be present"), + direction: event.direction, + sequence: 0, + }, + )); + } + + let target_block_state = instance + .get_block_state(&event.position) + .unwrap_or_default(); + *sequence_number += 1; + let target_registry_block = azalea_registry::Block::from(target_block_state); + + // we can't break blocks if they don't have a bounding box + + // TODO: So right now azalea doesn't differenciate between different types of + // bounding boxes. See ClipContext::block_shape for more info. Ideally this + // should just call ClipContext::block_shape and check if it's empty. + let block_is_solid = !target_block_state.is_air() + // this is a hack to make sure we can't break water or lava + && !matches!( + target_registry_block, + azalea_registry::Block::Water | azalea_registry::Block::Lava + ); + + if block_is_solid && **mine_progress == 0. { + // interact with the block (like note block left click) here + attack_block_events.send(AttackBlockEvent { + entity: event.entity, + position: event.position, + }); + } + + let block = Box::<dyn Block>::from(target_block_state); + + let held_item = inventory.held_item(); + + if block_is_solid + && get_mine_progress( + block.as_ref(), + held_item.kind(), + &inventory.inventory_menu, + fluid_on_eyes, + physics, + ) >= 1. + { + // block was broken instantly + finish_mining_events.send(FinishMiningBlockEvent { + entity: event.entity, + position: event.position, + }); + } else { + commands.entity(event.entity).insert(Mining { + pos: event.position, + dir: event.direction, + }); + **current_mining_pos = Some(event.position); + **current_mining_item = held_item; + **mine_progress = 0.; + **mine_ticks = 0.; + mine_block_progress_events.send(MineBlockProgressEvent { + entity: event.entity, + position: event.position, + destroy_stage: mine_progress.destroy_stage(), + }); + } + + send_packet_events.send(SendPacketEvent::new( + event.entity, + ServerboundPlayerAction { + action: s_player_action::Action::StartDestroyBlock, + pos: event.position, + direction: event.direction, + sequence: **sequence_number, + }, + )); + } + } +} + +#[derive(Event)] +pub struct MineBlockProgressEvent { + pub entity: Entity, + pub position: BlockPos, + pub destroy_stage: Option<u32>, +} + +/// A player left clicked on a block, used for stuff like interacting with note +/// blocks. +#[derive(Event)] +pub struct AttackBlockEvent { + pub entity: Entity, + pub position: BlockPos, +} + +/// Returns whether the block and item are still the same as when we started +/// mining. +fn is_same_mining_target( + target_block: BlockPos, + inventory: &Inventory, + current_mining_pos: &MineBlockPos, + current_mining_item: &MineItem, +) -> bool { + let held_item = inventory.held_item(); + Some(target_block) == current_mining_pos.0 && held_item == current_mining_item.0 +} + +/// A component bundle for players that can mine blocks. +#[derive(Bundle, Default)] +pub struct MineBundle { + pub delay: MineDelay, + pub progress: MineProgress, + pub ticks: MineTicks, + pub mining_pos: MineBlockPos, + pub mine_item: MineItem, +} + +/// A component that counts down until we start mining the next block. +#[derive(Component, Debug, Default, Deref, DerefMut)] +pub struct MineDelay(pub u32); + +/// A component that stores the progress of the current mining operation. This +/// is a value between 0 and 1. +#[derive(Component, Debug, Default, Deref, DerefMut)] +pub struct MineProgress(pub f32); + +impl MineProgress { + pub fn destroy_stage(&self) -> Option<u32> { + if self.0 > 0. { + Some((self.0 * 10.) as u32) + } else { + None + } + } +} + +/// A component that stores the number of ticks that we've been mining the same +/// block for. This is a float even though it should only ever be a round +/// number. +#[derive(Component, Clone, Debug, Default, Deref, DerefMut)] +pub struct MineTicks(pub f32); + +/// A component that stores the position of the block we're currently mining. +#[derive(Component, Clone, Debug, Default, Deref, DerefMut)] +pub struct MineBlockPos(pub Option<BlockPos>); + +/// A component that contains the item we're currently using to mine. If we're +/// not mining anything, it'll be [`ItemStack::Empty`]. +#[derive(Component, Clone, Debug, Default, Deref, DerefMut)] +pub struct MineItem(pub ItemStack); + +/// Sent when we completed mining a block. +#[derive(Event)] +pub struct FinishMiningBlockEvent { + pub entity: Entity, + pub position: BlockPos, +} + +pub fn handle_finish_mining_block_event( + mut events: EventReader<FinishMiningBlockEvent>, + mut query: Query<( + &InstanceName, + &LocalGameMode, + &Inventory, + &PlayerAbilities, + &PermissionLevel, + &mut CurrentSequenceNumber, + )>, + instances: Res<InstanceContainer>, +) { + for event in events.read() { + let (instance_name, game_mode, inventory, abilities, permission_level, _sequence_number) = + query.get_mut(event.entity).unwrap(); + let instance_lock = instances.get(instance_name).unwrap(); + let instance = instance_lock.read(); + if check_is_interaction_restricted( + &instance, + &event.position, + &game_mode.current, + inventory, + ) { + continue; + } + + if game_mode.current == GameMode::Creative { + let held_item = inventory.held_item().kind(); + if matches!( + held_item, + azalea_registry::Item::Trident | azalea_registry::Item::DebugStick + ) || azalea_registry::tags::items::SWORDS.contains(&held_item) + { + continue; + } + } + + let Some(block_state) = instance.get_block_state(&event.position) else { + continue; + }; + + let registry_block = Box::<dyn Block>::from(block_state).as_registry_block(); + if !can_use_game_master_blocks(abilities, permission_level) + && matches!( + registry_block, + azalea_registry::Block::CommandBlock | azalea_registry::Block::StructureBlock + ) + { + continue; + } + if block_state == BlockState::AIR { + continue; + } + + // when we break a waterlogged block we want to keep the water there + let fluid_state = FluidState::from(block_state); + let block_state_for_fluid = BlockState::from(fluid_state); + instance.set_block_state(&event.position, block_state_for_fluid); + } +} + +/// Abort mining a block. +#[derive(Event)] +pub struct StopMiningBlockEvent { + pub entity: Entity, +} +pub fn handle_stop_mining_block_event( + mut events: EventReader<StopMiningBlockEvent>, + mut send_packet_events: EventWriter<SendPacketEvent>, + mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>, + mut query: Query<(&mut Mining, &MineBlockPos, &mut MineProgress)>, + mut commands: Commands, +) { + for event in events.read() { + let (mut _mining, mine_block_pos, mut mine_progress) = query.get_mut(event.entity).unwrap(); + + let mine_block_pos = + mine_block_pos.expect("IsMining is true so MineBlockPos must be present"); + send_packet_events.send(SendPacketEvent::new( + event.entity, + ServerboundPlayerAction { + action: s_player_action::Action::AbortDestroyBlock, + pos: mine_block_pos, + direction: Direction::Down, + sequence: 0, + }, + )); + commands.entity(event.entity).remove::<Mining>(); + **mine_progress = 0.; + mine_block_progress_events.send(MineBlockProgressEvent { + entity: event.entity, + position: mine_block_pos, + destroy_stage: None, + }); + } +} + +#[allow(clippy::too_many_arguments, clippy::type_complexity)] +pub fn continue_mining_block( + mut query: Query<( + Entity, + &InstanceName, + &LocalGameMode, + &Inventory, + &MineBlockPos, + &MineItem, + &FluidOnEyes, + &Physics, + &Mining, + &mut MineDelay, + &mut MineProgress, + &mut MineTicks, + &mut CurrentSequenceNumber, + )>, + mut send_packet_events: EventWriter<SendPacketEvent>, + mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>, + mut finish_mining_events: EventWriter<FinishMiningBlockEvent>, + mut start_mining_events: EventWriter<StartMiningBlockWithDirectionEvent>, + mut swing_arm_events: EventWriter<SwingArmEvent>, + instances: Res<InstanceContainer>, + mut commands: Commands, +) { + for ( + entity, + instance_name, + game_mode, + inventory, + current_mining_pos, + current_mining_item, + fluid_on_eyes, + physics, + mining, + mut mine_delay, + mut mine_progress, + mut mine_ticks, + mut sequence_number, + ) in query.iter_mut() + { + if **mine_delay > 0 { + **mine_delay -= 1; + continue; + } + + if game_mode.current == GameMode::Creative { + // TODO: worldborder check + **mine_delay = 5; + finish_mining_events.send(FinishMiningBlockEvent { + entity, + position: mining.pos, + }); + *sequence_number += 1; + send_packet_events.send(SendPacketEvent::new( + entity, + ServerboundPlayerAction { + action: s_player_action::Action::StartDestroyBlock, + pos: mining.pos, + direction: mining.dir, + sequence: **sequence_number, + }, + )); + swing_arm_events.send(SwingArmEvent { entity }); + } else if is_same_mining_target( + mining.pos, + inventory, + current_mining_pos, + current_mining_item, + ) { + let instance_lock = instances.get(instance_name).unwrap(); + let instance = instance_lock.read(); + let target_block_state = instance.get_block_state(&mining.pos).unwrap_or_default(); + + if target_block_state.is_air() { + commands.entity(entity).remove::<Mining>(); + continue; + } + let block = Box::<dyn Block>::from(target_block_state); + **mine_progress += get_mine_progress( + block.as_ref(), + current_mining_item.kind(), + &inventory.inventory_menu, + fluid_on_eyes, + physics, + ); + + if **mine_ticks % 4. == 0. { + // vanilla makes a mining sound here + } + **mine_ticks += 1.; + + if **mine_progress >= 1. { + commands.entity(entity).remove::<Mining>(); + *sequence_number += 1; + finish_mining_events.send(FinishMiningBlockEvent { + entity, + position: mining.pos, + }); + send_packet_events.send(SendPacketEvent::new( + entity, + ServerboundPlayerAction { + action: s_player_action::Action::StopDestroyBlock, + pos: mining.pos, + direction: mining.dir, + sequence: **sequence_number, + }, + )); + **mine_progress = 0.; + **mine_ticks = 0.; + **mine_delay = 0; + } + + mine_block_progress_events.send(MineBlockProgressEvent { + entity, + position: mining.pos, + destroy_stage: mine_progress.destroy_stage(), + }); + swing_arm_events.send(SwingArmEvent { entity }); + } else { + start_mining_events.send(StartMiningBlockWithDirectionEvent { + entity, + position: mining.pos, + direction: mining.dir, + }); + } + + swing_arm_events.send(SwingArmEvent { entity }); + } +} diff --git a/azalea-client/src/plugins/mod.rs b/azalea-client/src/plugins/mod.rs new file mode 100644 index 00000000..11794fb3 --- /dev/null +++ b/azalea-client/src/plugins/mod.rs @@ -0,0 +1,14 @@ +pub mod attack; +pub mod brand; +pub mod chat; +pub mod chunks; +pub mod disconnect; +pub mod events; +pub mod interact; +pub mod inventory; +pub mod mining; +pub mod movement; +pub mod packet; +pub mod respawn; +pub mod task_pool; +pub mod tick_end; diff --git a/azalea-client/src/plugins/movement.rs b/azalea-client/src/plugins/movement.rs new file mode 100644 index 00000000..17b92e65 --- /dev/null +++ b/azalea-client/src/plugins/movement.rs @@ -0,0 +1,580 @@ +use std::backtrace::Backtrace; + +use azalea_core::position::Vec3; +use azalea_core::tick::GameTick; +use azalea_entity::{Attributes, Jumping, metadata::Sprinting}; +use azalea_entity::{InLoadedChunk, LastSentPosition, LookDirection, Physics, Position}; +use azalea_physics::{PhysicsSet, ai_step}; +use azalea_protocol::packets::game::{ServerboundPlayerCommand, ServerboundPlayerInput}; +use azalea_protocol::packets::{ + Packet, + game::{ + s_move_player_pos::ServerboundMovePlayerPos, + s_move_player_pos_rot::ServerboundMovePlayerPosRot, + s_move_player_rot::ServerboundMovePlayerRot, + s_move_player_status_only::ServerboundMovePlayerStatusOnly, + }, +}; +use azalea_world::{MinecraftEntityId, MoveEntityError}; +use bevy_app::{App, Plugin, Update}; +use bevy_ecs::prelude::{Event, EventWriter}; +use bevy_ecs::schedule::SystemSet; +use bevy_ecs::system::Commands; +use bevy_ecs::{ + component::Component, entity::Entity, event::EventReader, query::With, + schedule::IntoSystemConfigs, system::Query, +}; +use thiserror::Error; + +use crate::client::Client; +use crate::packet::game::SendPacketEvent; + +#[derive(Error, Debug)] +pub enum MovePlayerError { + #[error("Player is not in world")] + PlayerNotInWorld(Backtrace), + #[error("{0}")] + Io(#[from] std::io::Error), +} + +impl From<MoveEntityError> for MovePlayerError { + fn from(err: MoveEntityError) -> Self { + match err { + MoveEntityError::EntityDoesNotExist(backtrace) => { + MovePlayerError::PlayerNotInWorld(backtrace) + } + } + } +} + +pub struct MovementPlugin; + +impl Plugin for MovementPlugin { + fn build(&self, app: &mut App) { + app.add_event::<StartWalkEvent>() + .add_event::<StartSprintEvent>() + .add_event::<KnockbackEvent>() + .add_systems( + Update, + (handle_sprint, handle_walk, handle_knockback) + .chain() + .in_set(MoveEventsSet), + ) + .add_systems( + GameTick, + ( + (tick_controls, local_player_ai_step) + .chain() + .in_set(PhysicsSet) + .before(ai_step) + .before(azalea_physics::fluids::update_in_water_state_and_do_fluid_pushing), + send_sprinting_if_needed.after(azalea_entity::update_in_loaded_chunk), + send_player_input_packet, + send_position.after(PhysicsSet), + ) + .chain(), + ); + } +} + +#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] +pub struct MoveEventsSet; + +impl Client { + /// 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 { + *self.component::<Jumping>() + } + + /// Sets the direction the client is looking. `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_direction(&mut self, y_rot: f32, x_rot: f32) { + let mut ecs = self.ecs.lock(); + let mut look_direction = self.query::<&mut LookDirection>(&mut ecs); + + (look_direction.y_rot, look_direction.x_rot) = (y_rot, x_rot); + } + + /// Returns the direction the client is looking. The first value is the y + /// rotation (ie. yaw, looking to the side) and the second value is the x + /// rotation (ie. pitch, looking up and down). + pub fn direction(&self) -> (f32, f32) { + let look_direction = self.component::<LookDirection>(); + (look_direction.y_rot, look_direction.x_rot) + } +} + +/// A component that contains the look direction that was last sent over the +/// network. +#[derive(Debug, Component, Clone, Default)] +pub struct LastSentLookDirection { + pub x_rot: f32, + pub y_rot: f32, +} + +/// Component for entities that can move and sprint. Usually only in +/// [`LocalEntity`]s. +/// +/// [`LocalEntity`]: azalea_entity::LocalEntity +#[derive(Default, Component, Clone)] +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, +} + +#[allow(clippy::type_complexity)] +pub fn send_position( + mut query: Query< + ( + Entity, + &Position, + &LookDirection, + &mut PhysicsState, + &mut LastSentPosition, + &mut Physics, + &mut LastSentLookDirection, + ), + With<InLoadedChunk>, + >, + mut send_packet_events: EventWriter<SendPacketEvent>, +) { + for ( + entity, + position, + direction, + mut physics_state, + mut last_sent_position, + mut physics, + mut last_direction, + ) in query.iter_mut() + { + let packet = { + // TODO: the camera being able to be controlled by other entities isn't + // implemented yet if !self.is_controlled_camera() { return }; + + 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 = (direction.y_rot - last_direction.y_rot) as f64; + let x_rot_delta = (direction.x_rot - last_direction.x_rot) as f64; + + physics_state.position_remainder += 1; + + // boolean sendingPosition = Mth.lengthSquared(xDelta, yDelta, zDelta) > + // Mth.square(2.0E-4D) || this.positionReminder >= 20; + let sending_position = ((x_delta.powi(2) + y_delta.powi(2) + z_delta.powi(2)) + > 2.0e-4f64.powi(2)) + || physics_state.position_remainder >= 20; + let sending_direction = y_rot_delta != 0.0 || x_rot_delta != 0.0; + + // if self.is_passenger() { + // TODO: posrot packet for being a passenger + // } + let packet = if sending_position && sending_direction { + Some( + ServerboundMovePlayerPosRot { + pos: **position, + look_direction: *direction, + on_ground: physics.on_ground(), + } + .into_variant(), + ) + } else if sending_position { + Some( + ServerboundMovePlayerPos { + pos: **position, + on_ground: physics.on_ground(), + } + .into_variant(), + ) + } else if sending_direction { + Some( + ServerboundMovePlayerRot { + look_direction: *direction, + on_ground: physics.on_ground(), + } + .into_variant(), + ) + } else if physics.last_on_ground() != physics.on_ground() { + Some( + ServerboundMovePlayerStatusOnly { + on_ground: physics.on_ground(), + } + .into_variant(), + ) + } else { + None + }; + + if sending_position { + **last_sent_position = **position; + physics_state.position_remainder = 0; + } + if sending_direction { + last_direction.y_rot = direction.y_rot; + last_direction.x_rot = direction.x_rot; + } + + let on_ground = physics.on_ground(); + physics.set_last_on_ground(on_ground); + // minecraft checks for autojump here, but also autojump is bad so + + packet + }; + + if let Some(packet) = packet { + send_packet_events.send(SendPacketEvent { + sent_by: entity, + packet, + }); + } + } +} + +#[derive(Debug, Default, Component, Clone, PartialEq, Eq)] +pub struct LastSentInput(pub ServerboundPlayerInput); +pub fn send_player_input_packet( + mut query: Query<(Entity, &PhysicsState, &Jumping, Option<&LastSentInput>)>, + mut send_packet_events: EventWriter<SendPacketEvent>, + mut commands: Commands, +) { + for (entity, physics_state, jumping, last_sent_input) in query.iter_mut() { + let dir = physics_state.move_direction; + type D = WalkDirection; + let input = ServerboundPlayerInput { + forward: matches!(dir, D::Forward | D::ForwardLeft | D::ForwardRight), + backward: matches!(dir, D::Backward | D::BackwardLeft | D::BackwardRight), + left: matches!(dir, D::Left | D::ForwardLeft | D::BackwardLeft), + right: matches!(dir, D::Right | D::ForwardRight | D::BackwardRight), + jump: **jumping, + // TODO: implement sneaking + shift: false, + sprint: physics_state.trying_to_sprint, + }; + + // if LastSentInput isn't present, we default to assuming we're not pressing any + // keys and insert it anyways every time it changes + let last_sent_input = last_sent_input.cloned().unwrap_or_default(); + + if input != last_sent_input.0 { + send_packet_events.send(SendPacketEvent { + sent_by: entity, + packet: input.clone().into_variant(), + }); + commands.entity(entity).insert(LastSentInput(input)); + } + } +} + +fn send_sprinting_if_needed( + mut query: Query<(Entity, &MinecraftEntityId, &Sprinting, &mut PhysicsState)>, + mut send_packet_events: EventWriter<SendPacketEvent>, +) { + for (entity, minecraft_entity_id, sprinting, mut physics_state) in query.iter_mut() { + let was_sprinting = physics_state.was_sprinting; + if **sprinting != was_sprinting { + let sprinting_action = if **sprinting { + azalea_protocol::packets::game::s_player_command::Action::StartSprinting + } else { + azalea_protocol::packets::game::s_player_command::Action::StopSprinting + }; + send_packet_events.send(SendPacketEvent::new( + entity, + ServerboundPlayerCommand { + id: *minecraft_entity_id, + action: sprinting_action, + data: 0, + }, + )); + physics_state.was_sprinting = **sprinting; + } + } +} + +/// Update the impulse from self.move_direction. The multiplier is used for +/// sneaking. +pub(crate) fn tick_controls(mut query: Query<&mut PhysicsState>) { + for mut physics_state in query.iter_mut() { + let multiplier: Option<f32> = None; + + let mut forward_impulse: f32 = 0.; + let mut left_impulse: f32 = 0.; + let move_direction = physics_state.move_direction; + match move_direction { + WalkDirection::Forward | WalkDirection::ForwardRight | WalkDirection::ForwardLeft => { + forward_impulse += 1.; + } + WalkDirection::Backward + | WalkDirection::BackwardRight + | WalkDirection::BackwardLeft => { + forward_impulse -= 1.; + } + _ => {} + }; + match move_direction { + WalkDirection::Right | WalkDirection::ForwardRight | WalkDirection::BackwardRight => { + left_impulse += 1.; + } + WalkDirection::Left | WalkDirection::ForwardLeft | WalkDirection::BackwardLeft => { + left_impulse -= 1.; + } + _ => {} + }; + physics_state.forward_impulse = forward_impulse; + physics_state.left_impulse = left_impulse; + + if let Some(multiplier) = multiplier { + physics_state.forward_impulse *= multiplier; + 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< + (&PhysicsState, &mut Physics, &mut Sprinting, &mut Attributes), + With<InLoadedChunk>, + >, +) { + for (physics_state, mut physics, mut sprinting, mut attributes) in query.iter_mut() { + // server ai step + physics.x_acceleration = physics_state.left_impulse; + physics.z_acceleration = 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`. + /// + /// # Examples + /// + /// Walk for 1 second + /// ```rust,no_run + /// # use azalea_client::{Client, WalkDirection}; + /// # use std::time::Duration; + /// # async fn example(mut bot: Client) { + /// bot.walk(WalkDirection::Forward); + /// tokio::time::sleep(Duration::from_secs(1)).await; + /// bot.walk(WalkDirection::None); + /// # } + /// ``` + pub fn walk(&mut self, direction: WalkDirection) { + let mut ecs = self.ecs.lock(); + ecs.send_event(StartWalkEvent { + entity: self.entity, + direction, + }); + } + + /// Start sprinting in the given direction. To stop moving, call + /// [`Client::walk(WalkDirection::None)`] + /// + /// # Examples + /// + /// Sprint for 1 second + /// ```rust,no_run + /// # use azalea_client::{Client, WalkDirection, SprintDirection}; + /// # use std::time::Duration; + /// # async fn example(mut bot: Client) { + /// bot.sprint(SprintDirection::Forward); + /// tokio::time::sleep(Duration::from_secs(1)).await; + /// bot.walk(WalkDirection::None); + /// # } + /// ``` + pub fn sprint(&mut self, direction: SprintDirection) { + let mut ecs = self.ecs.lock(); + ecs.send_event(StartSprintEvent { + entity: self.entity, + direction, + }); + } +} + +/// An event sent when the client starts walking. This does not get sent for +/// non-local entities. +/// +/// To stop walking or sprinting, send this event with `WalkDirection::None`. +#[derive(Event, Debug)] +pub struct StartWalkEvent { + pub entity: Entity, + pub direction: WalkDirection, +} + +/// The system that makes the player start walking when they receive a +/// [`StartWalkEvent`]. +pub fn handle_walk( + mut events: EventReader<StartWalkEvent>, + mut query: Query<(&mut PhysicsState, &mut Sprinting, &mut Attributes)>, +) { + for event in events.read() { + if let Ok((mut physics_state, mut sprinting, mut attributes)) = query.get_mut(event.entity) + { + physics_state.move_direction = event.direction; + physics_state.trying_to_sprint = false; + set_sprinting(false, &mut sprinting, &mut attributes); + } + } +} + +/// An event sent when the client starts sprinting. This does not get sent for +/// non-local entities. +#[derive(Event)] +pub struct StartSprintEvent { + pub entity: Entity, + pub direction: SprintDirection, +} +/// The system that makes the player start sprinting when they receive a +/// [`StartSprintEvent`]. +pub fn handle_sprint( + mut query: Query<&mut PhysicsState>, + mut events: EventReader<StartSprintEvent>, +) { + for event in events.read() { + 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; + } + } +} + +/// 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 + .try_insert(azalea_entity::attributes::sprinting_modifier()) + .is_ok() + } else { + attributes + .speed + .remove(&azalea_entity::attributes::sprinting_modifier().id) + .is_none() + } +} + +// 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 + // } +} + +/// An event sent by the server that sets or adds to our velocity. Usually +/// `KnockbackKind::Set` is used for normal knockback and `KnockbackKind::Add` +/// is used for explosions, but some servers (notably Hypixel) use explosions +/// for knockback. +#[derive(Event)] +pub struct KnockbackEvent { + pub entity: Entity, + pub knockback: KnockbackType, +} + +pub enum KnockbackType { + Set(Vec3), + Add(Vec3), +} + +pub fn handle_knockback(mut query: Query<&mut Physics>, mut events: EventReader<KnockbackEvent>) { + for event in events.read() { + if let Ok(mut physics) = query.get_mut(event.entity) { + match event.knockback { + KnockbackType::Set(velocity) => { + physics.velocity = velocity; + } + KnockbackType::Add(velocity) => { + physics.velocity += velocity; + } + } + } + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum WalkDirection { + #[default] + None, + Forward, + Backward, + Left, + Right, + ForwardRight, + ForwardLeft, + BackwardRight, + BackwardLeft, +} + +/// The directions that we can sprint in. It's a subset of [`WalkDirection`]. +#[derive(Clone, Copy, Debug)] +pub enum SprintDirection { + Forward, + ForwardRight, + ForwardLeft, +} + +impl From<SprintDirection> for WalkDirection { + fn from(d: SprintDirection) -> Self { + match d { + SprintDirection::Forward => WalkDirection::Forward, + SprintDirection::ForwardRight => WalkDirection::ForwardRight, + SprintDirection::ForwardLeft => WalkDirection::ForwardLeft, + } + } +} diff --git a/azalea-client/src/plugins/packet/config/events.rs b/azalea-client/src/plugins/packet/config/events.rs new file mode 100644 index 00000000..6b647d74 --- /dev/null +++ b/azalea-client/src/plugins/packet/config/events.rs @@ -0,0 +1,90 @@ +use std::io::Cursor; + +use azalea_protocol::{ + packets::{ + config::{ClientboundConfigPacket, ServerboundConfigPacket}, + Packet, + }, + read::deserialize_packet, +}; +use bevy_ecs::prelude::*; +use tracing::{debug, error}; + +use crate::{raw_connection::RawConnection, InConfigState}; + +#[derive(Event, Debug, Clone)] +pub struct ReceiveConfigPacketEvent { + /// The client entity that received the packet. + pub entity: Entity, + /// The packet that was actually received. + pub packet: ClientboundConfigPacket, +} + +/// An event for sending a packet to the server while we're in the +/// `configuration` state. +#[derive(Event)] +pub struct SendConfigPacketEvent { + pub sent_by: Entity, + pub packet: ServerboundConfigPacket, +} +impl SendConfigPacketEvent { + pub fn new(sent_by: Entity, packet: impl Packet<ServerboundConfigPacket>) -> Self { + let packet = packet.into_variant(); + Self { sent_by, packet } + } +} + +pub fn handle_send_packet_event( + mut send_packet_events: EventReader<SendConfigPacketEvent>, + mut query: Query<(&mut RawConnection, Option<&InConfigState>)>, +) { + for event in send_packet_events.read() { + if let Ok((raw_conn, in_configuration_state)) = query.get_mut(event.sent_by) { + if in_configuration_state.is_none() { + error!( + "Tried to send a configuration packet {:?} while not in configuration state", + event.packet + ); + continue; + } + debug!("Sending packet: {:?}", event.packet); + if let Err(e) = raw_conn.write_packet(event.packet.clone()) { + error!("Failed to send packet: {e}"); + } + } + } +} + +pub fn send_packet_events( + query: Query<(Entity, &RawConnection), With<InConfigState>>, + mut packet_events: ResMut<Events<ReceiveConfigPacketEvent>>, +) { + // we manually clear and send the events at the beginning of each update + // since otherwise it'd cause issues with events in process_packet_events + // running twice + packet_events.clear(); + for (player_entity, raw_conn) in &query { + let packets_lock = raw_conn.incoming_packet_queue(); + let mut packets = packets_lock.lock(); + if !packets.is_empty() { + for raw_packet in packets.iter() { + let packet = match deserialize_packet::<ClientboundConfigPacket>(&mut Cursor::new( + raw_packet, + )) { + Ok(packet) => packet, + Err(err) => { + error!("failed to read packet: {err:?}"); + debug!("packet bytes: {raw_packet:?}"); + continue; + } + }; + packet_events.send(ReceiveConfigPacketEvent { + entity: player_entity, + packet, + }); + } + // clear the packets right after we read them + packets.clear(); + } + } +} diff --git a/azalea-client/src/plugins/packet/config/mod.rs b/azalea-client/src/plugins/packet/config/mod.rs new file mode 100644 index 00000000..5cb19b9d --- /dev/null +++ b/azalea-client/src/plugins/packet/config/mod.rs @@ -0,0 +1,223 @@ +mod events; + +use azalea_protocol::packets::config::*; +use azalea_protocol::packets::ConnectionProtocol; +use bevy_ecs::prelude::*; +use bevy_ecs::system::SystemState; +pub use events::*; +use tracing::{debug, warn}; + +use super::as_system; +use crate::client::InConfigState; +use crate::disconnect::DisconnectEvent; +use crate::packet::game::KeepAliveEvent; +use crate::raw_connection::RawConnection; +use crate::{declare_packet_handlers, InstanceHolder}; + +pub fn process_packet_events(ecs: &mut World) { + let mut events_owned = Vec::new(); + let mut system_state: SystemState<EventReader<ReceiveConfigPacketEvent>> = + SystemState::new(ecs); + let mut events = system_state.get_mut(ecs); + for ReceiveConfigPacketEvent { + entity: player_entity, + packet, + } in events.read() + { + // we do this so `ecs` isn't borrowed for the whole loop + events_owned.push((*player_entity, packet.clone())); + } + for (player_entity, packet) in events_owned { + let mut handler = ConfigPacketHandler { + player: player_entity, + ecs, + }; + + declare_packet_handlers!( + ClientboundConfigPacket, + packet, + handler, + [ + cookie_request, + custom_payload, + disconnect, + finish_configuration, + keep_alive, + ping, + reset_chat, + registry_data, + resource_pack_pop, + resource_pack_push, + store_cookie, + transfer, + update_enabled_features, + update_tags, + select_known_packs, + custom_report_details, + server_links, + ] + ); + } +} + +pub struct ConfigPacketHandler<'a> { + pub ecs: &'a mut World, + pub player: Entity, +} +impl ConfigPacketHandler<'_> { + pub fn registry_data(&mut self, p: ClientboundRegistryData) { + as_system::<Query<&mut InstanceHolder>>(self.ecs, |mut query| { + let instance_holder = query.get_mut(self.player).unwrap(); + let mut instance = instance_holder.instance.write(); + + // add the new registry data + instance.registries.append(p.registry_id, p.entries); + }); + } + + pub fn custom_payload(&mut self, p: ClientboundCustomPayload) { + debug!("Got custom payload packet {p:?}"); + } + + pub fn disconnect(&mut self, p: ClientboundDisconnect) { + warn!("Got disconnect packet {p:?}"); + as_system::<EventWriter<_>>(self.ecs, |mut events| { + events.send(DisconnectEvent { + entity: self.player, + reason: Some(p.reason), + }); + }); + } + + pub fn finish_configuration(&mut self, p: ClientboundFinishConfiguration) { + debug!("got FinishConfiguration packet: {p:?}"); + + as_system::<(Commands, Query<&mut RawConnection>)>( + self.ecs, + |(mut commands, mut query)| { + let mut raw_conn = query.get_mut(self.player).unwrap(); + + raw_conn + .write_packet(ServerboundFinishConfiguration) + .expect( + "we should be in the right state and encoding this packet shouldn't fail", + ); + raw_conn.set_state(ConnectionProtocol::Game); + + // these components are added now that we're going to be in the Game state + commands + .entity(self.player) + .remove::<InConfigState>() + .insert(crate::JoinedClientBundle::default()); + }, + ); + } + + pub fn keep_alive(&mut self, p: ClientboundKeepAlive) { + debug!( + "Got keep alive packet (in configuration) {p:?} for {:?}", + self.player + ); + + as_system::<(Query<&RawConnection>, EventWriter<_>)>(self.ecs, |(query, mut events)| { + let raw_conn = query.get(self.player).unwrap(); + + events.send(KeepAliveEvent { + entity: self.player, + id: p.id, + }); + raw_conn + .write_packet(ServerboundKeepAlive { id: p.id }) + .unwrap(); + }); + } + + pub fn ping(&mut self, p: ClientboundPing) { + debug!("Got ping packet (in configuration) {p:?}"); + + as_system::<Query<&RawConnection>>(self.ecs, |query| { + let raw_conn = query.get(self.player).unwrap(); + + raw_conn.write_packet(ServerboundPong { id: p.id }).unwrap(); + }); + } + + pub fn resource_pack_push(&mut self, p: ClientboundResourcePackPush) { + debug!("Got resource pack push packet {p:?}"); + + as_system::<Query<&RawConnection>>(self.ecs, |query| { + let raw_conn = query.get(self.player).unwrap(); + + // always accept resource pack + raw_conn + .write_packet(ServerboundResourcePack { + id: p.id, + action: s_resource_pack::Action::Accepted, + }) + .unwrap(); + }); + } + + pub fn resource_pack_pop(&mut self, p: ClientboundResourcePackPop) { + debug!("Got resource pack pop packet {p:?}"); + } + + pub fn update_enabled_features(&mut self, p: ClientboundUpdateEnabledFeatures) { + debug!("Got update enabled features packet {p:?}"); + } + + pub fn update_tags(&mut self, _p: ClientboundUpdateTags) { + debug!("Got update tags packet"); + } + + pub fn cookie_request(&mut self, p: ClientboundCookieRequest) { + debug!("Got cookie request packet {p:?}"); + + as_system::<Query<&RawConnection>>(self.ecs, |query| { + let raw_conn = query.get(self.player).unwrap(); + + raw_conn + .write_packet(ServerboundCookieResponse { + key: p.key, + // cookies aren't implemented + payload: None, + }) + .unwrap(); + }); + } + + pub fn reset_chat(&mut self, p: ClientboundResetChat) { + debug!("Got reset chat packet {p:?}"); + } + + pub fn store_cookie(&mut self, p: ClientboundStoreCookie) { + debug!("Got store cookie packet {p:?}"); + } + + pub fn transfer(&mut self, p: ClientboundTransfer) { + debug!("Got transfer packet {p:?}"); + } + + pub fn select_known_packs(&mut self, p: ClientboundSelectKnownPacks) { + debug!("Got select known packs packet {p:?}"); + + as_system::<Query<&RawConnection>>(self.ecs, |query| { + let raw_conn = query.get(self.player).unwrap(); + + // resource pack management isn't implemented + raw_conn + .write_packet(ServerboundSelectKnownPacks { + known_packs: vec![], + }) + .unwrap(); + }); + } + + pub fn server_links(&mut self, p: ClientboundServerLinks) { + debug!("Got server links packet {p:?}"); + } + + pub fn custom_report_details(&mut self, p: ClientboundCustomReportDetails) { + debug!("Got custom report details packet {p:?}"); + } +} diff --git a/azalea-client/src/plugins/packet/game/events.rs b/azalea-client/src/plugins/packet/game/events.rs new file mode 100644 index 00000000..19f2a571 --- /dev/null +++ b/azalea-client/src/plugins/packet/game/events.rs @@ -0,0 +1,178 @@ +use std::{ + io::Cursor, + sync::{Arc, Weak}, +}; + +use azalea_chat::FormattedText; +use azalea_core::resource_location::ResourceLocation; +use azalea_entity::LocalEntity; +use azalea_protocol::{ + packets::{ + Packet, + game::{ClientboundGamePacket, ClientboundPlayerCombatKill, ServerboundGamePacket}, + }, + read::deserialize_packet, +}; +use azalea_world::Instance; +use bevy_ecs::prelude::*; +use parking_lot::RwLock; +use tracing::{debug, error}; +use uuid::Uuid; + +use crate::{PlayerInfo, raw_connection::RawConnection}; + +/// An event that's sent when we receive a packet. +/// ``` +/// # use azalea_client::packet::game::ReceivePacketEvent; +/// # use azalea_protocol::packets::game::ClientboundGamePacket; +/// # use bevy_ecs::event::EventReader; +/// +/// fn handle_packets(mut events: EventReader<ReceivePacketEvent>) { +/// for ReceivePacketEvent { +/// entity, +/// packet, +/// } in events.read() { +/// match packet.as_ref() { +/// ClientboundGamePacket::LevelParticles(p) => { +/// // ... +/// } +/// _ => {} +/// } +/// } +/// } +/// ``` +#[derive(Event, Debug, Clone)] +pub struct ReceivePacketEvent { + /// The client entity that received the packet. + pub entity: Entity, + /// The packet that was actually received. + pub packet: Arc<ClientboundGamePacket>, +} + +/// An event for sending a packet to the server while we're in the `game` state. +#[derive(Event)] +pub struct SendPacketEvent { + pub sent_by: Entity, + pub packet: ServerboundGamePacket, +} +impl SendPacketEvent { + pub fn new(sent_by: Entity, packet: impl Packet<ServerboundGamePacket>) -> Self { + let packet = packet.into_variant(); + Self { sent_by, packet } + } +} + +pub fn handle_outgoing_packets( + mut send_packet_events: EventReader<SendPacketEvent>, + mut query: Query<&mut RawConnection>, +) { + for event in send_packet_events.read() { + if let Ok(raw_connection) = query.get_mut(event.sent_by) { + // debug!("Sending packet: {:?}", event.packet); + if let Err(e) = raw_connection.write_packet(event.packet.clone()) { + error!("Failed to send packet: {e}"); + } + } + } +} + +pub fn send_receivepacketevent( + query: Query<(Entity, &RawConnection), With<LocalEntity>>, + mut packet_events: ResMut<Events<ReceivePacketEvent>>, +) { + // we manually clear and send the events at the beginning of each update + // since otherwise it'd cause issues with events in process_packet_events + // running twice + packet_events.clear(); + for (player_entity, raw_connection) in &query { + let packets_lock = raw_connection.incoming_packet_queue(); + let mut packets = packets_lock.lock(); + if !packets.is_empty() { + for raw_packet in packets.iter() { + let packet = + match deserialize_packet::<ClientboundGamePacket>(&mut Cursor::new(raw_packet)) + { + Ok(packet) => packet, + Err(err) => { + error!("failed to read packet: {err:?}"); + debug!("packet bytes: {raw_packet:?}"); + continue; + } + }; + packet_events.send(ReceivePacketEvent { + entity: player_entity, + packet: Arc::new(packet), + }); + } + // clear the packets right after we read them + packets.clear(); + } + } +} + +/// A player joined the game (or more specifically, was added to the tab +/// list of a local player). +#[derive(Event, Debug, Clone)] +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(Event, Debug, Clone)] +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(Event, Debug, Clone)] +pub struct UpdatePlayerEvent { + /// The local player entity that received this event. + pub entity: Entity, + pub info: PlayerInfo, +} + +/// Event for when an entity dies. dies. If it's a local player and there's a +/// reason in the death screen, the [`ClientboundPlayerCombatKill`] will +/// be included. +#[derive(Event, Debug, Clone)] +pub struct DeathEvent { + pub entity: Entity, + pub packet: Option<ClientboundPlayerCombatKill>, +} + +/// A KeepAlive packet is sent from the server to verify that the client is +/// still connected. +#[derive(Event, Debug, Clone)] +pub struct KeepAliveEvent { + pub entity: Entity, + /// The ID of the keepalive. This is an arbitrary number, but vanilla + /// servers use the time to generate this. + pub id: u64, +} + +#[derive(Event, Debug, Clone)] +pub struct ResourcePackEvent { + pub entity: Entity, + /// The random ID for this request to download the resource pack. The packet + /// for replying to a resource pack push must contain the same ID. + pub id: Uuid, + pub url: String, + pub hash: String, + pub required: bool, + pub prompt: Option<FormattedText>, +} + +/// An instance (aka world, dimension) was loaded by a client. +/// +/// Since the instance is given to you as a weak reference, it won't be able to +/// be `upgrade`d if all local players leave it. +#[derive(Event, Debug, Clone)] +pub struct InstanceLoadedEvent { + pub entity: Entity, + pub name: ResourceLocation, + pub instance: Weak<RwLock<Instance>>, +} diff --git a/azalea-client/src/plugins/packet/game/mod.rs b/azalea-client/src/plugins/packet/game/mod.rs new file mode 100644 index 00000000..98f76d13 --- /dev/null +++ b/azalea-client/src/plugins/packet/game/mod.rs @@ -0,0 +1,1583 @@ +mod events; + +use std::{collections::HashSet, ops::Add, sync::Arc}; + +use azalea_core::{ + game_type::GameMode, + math, + position::{ChunkPos, Vec3}, +}; +use azalea_entity::{ + Dead, EntityBundle, EntityKind, LastSentPosition, LoadedBy, LocalEntity, LookDirection, + Physics, Position, RelativeEntityUpdate, + indexing::{EntityIdIndex, EntityUuidIndex}, + metadata::{Health, apply_metadata}, +}; +use azalea_protocol::packets::game::*; +use azalea_world::{InstanceContainer, InstanceName, MinecraftEntityId, PartialInstance}; +use bevy_ecs::{prelude::*, system::SystemState}; +pub use events::*; +use tracing::{debug, error, trace, warn}; + +use crate::{ + ClientInformation, PlayerInfo, + chat::{ChatPacket, ChatReceivedEvent}, + chunks, declare_packet_handlers, + disconnect::DisconnectEvent, + inventory::{ + ClientSideCloseContainerEvent, Inventory, MenuOpenedEvent, SetContainerContentEvent, + }, + local_player::{ + GameProfileComponent, Hunger, InstanceHolder, LocalGameMode, PlayerAbilities, TabList, + }, + movement::{KnockbackEvent, KnockbackType}, + packet::as_system, +}; + +pub fn process_packet_events(ecs: &mut World) { + let mut events_owned = Vec::<(Entity, Arc<ClientboundGamePacket>)>::new(); + + { + let mut system_state = SystemState::<EventReader<ReceivePacketEvent>>::new(ecs); + let mut events = system_state.get_mut(ecs); + for ReceivePacketEvent { + entity: player_entity, + packet, + } in events.read() + { + // we do this so `ecs` isn't borrowed for the whole loop + events_owned.push((*player_entity, packet.clone())); + } + } + + for (player_entity, packet) in events_owned { + let mut handler = GamePacketHandler { + player: player_entity, + ecs, + }; + + declare_packet_handlers!( + ClientboundGamePacket, + packet.as_ref(), + handler, + [ + login, + set_chunk_cache_radius, + chunk_batch_start, + chunk_batch_finished, + custom_payload, + change_difficulty, + commands, + player_abilities, + set_cursor_item, + update_tags, + disconnect, + update_recipes, + entity_event, + player_position, + player_info_update, + player_info_remove, + set_chunk_cache_center, + chunks_biomes, + light_update, + level_chunk_with_light, + add_entity, + set_entity_data, + update_attributes, + set_entity_motion, + set_entity_link, + initialize_border, + set_time, + set_default_spawn_position, + set_health, + set_experience, + teleport_entity, + update_advancements, + rotate_head, + move_entity_pos, + move_entity_pos_rot, + move_entity_rot, + keep_alive, + remove_entities, + player_chat, + system_chat, + disguised_chat, + sound, + level_event, + block_update, + animate, + section_blocks_update, + game_event, + level_particles, + server_data, + set_equipment, + update_mob_effect, + add_experience_orb, + award_stats, + block_changed_ack, + block_destruction, + block_entity_data, + block_event, + boss_event, + command_suggestions, + container_set_content, + container_set_data, + container_set_slot, + container_close, + cooldown, + custom_chat_completions, + delete_chat, + explode, + forget_level_chunk, + horse_screen_open, + map_item_data, + merchant_offers, + move_vehicle, + open_book, + open_screen, + open_sign_editor, + ping, + place_ghost_recipe, + player_combat_end, + player_combat_enter, + player_combat_kill, + player_look_at, + remove_mob_effect, + resource_pack_push, + resource_pack_pop, + respawn, + start_configuration, + entity_position_sync, + select_advancements_tab, + set_action_bar_text, + set_border_center, + set_border_lerp_size, + set_border_size, + set_border_warning_delay, + set_border_warning_distance, + set_camera, + set_display_objective, + set_objective, + set_passengers, + set_player_team, + set_score, + set_simulation_distance, + set_subtitle_text, + set_title_text, + set_titles_animation, + clear_titles, + sound_entity, + stop_sound, + tab_list, + tag_query, + take_item_entity, + bundle_delimiter, + damage_event, + hurt_animation, + ticking_state, + ticking_step, + reset_score, + cookie_request, + debug_sample, + pong_response, + store_cookie, + transfer, + move_minecart_along_track, + set_held_slot, + set_player_inventory, + projectile_power, + custom_report_details, + server_links, + player_rotation, + recipe_book_add, + recipe_book_remove, + recipe_book_settings, + ] + ); + } +} + +pub struct GamePacketHandler<'a> { + pub ecs: &'a mut World, + pub player: Entity, +} +impl GamePacketHandler<'_> { + pub fn login(&mut self, p: &ClientboundLogin) { + debug!("Got login packet"); + + as_system::<( + Commands, + Query<( + &GameProfileComponent, + &ClientInformation, + Option<&mut InstanceName>, + Option<&mut LoadedBy>, + &mut EntityIdIndex, + &mut InstanceHolder, + )>, + EventWriter<InstanceLoadedEvent>, + ResMut<InstanceContainer>, + ResMut<EntityUuidIndex>, + EventWriter<SendPacketEvent>, + )>( + self.ecs, + |( + mut commands, + mut query, + mut instance_loaded_events, + mut instance_container, + mut entity_uuid_index, + mut send_packet_events, + )| { + let ( + game_profile, + client_information, + instance_name, + loaded_by, + mut entity_id_index, + mut instance_holder, + ) = query.get_mut(self.player).unwrap(); + + let new_instance_name = p.common.dimension.clone(); + + if let Some(mut instance_name) = instance_name { + *instance_name = instance_name.clone(); + } else { + commands + .entity(self.player) + .insert(InstanceName(new_instance_name.clone())); + } + + let Some((_dimension_type, dimension_data)) = p + .common + .dimension_type(&instance_holder.instance.read().registries) + else { + return; + }; + + // add this world to the instance_container (or don't if it's already + // there) + let weak_instance = instance_container.insert( + new_instance_name.clone(), + dimension_data.height, + dimension_data.min_y, + &instance_holder.instance.read().registries, + ); + instance_loaded_events.send(InstanceLoadedEvent { + entity: self.player, + name: new_instance_name.clone(), + instance: Arc::downgrade(&weak_instance), + }); + + // set the partial_world to an empty world + // (when we add chunks or entities those will be in the + // instance_container) + + *instance_holder.partial_instance.write() = PartialInstance::new( + azalea_world::chunk_storage::calculate_chunk_storage_range( + client_information.view_distance.into(), + ), + // this argument makes it so other clients don't update this player entity + // in a shared instance + Some(self.player), + ); + { + let map = instance_holder.instance.read().registries.map.clone(); + let new_registries = &mut weak_instance.write().registries; + // add the registries from this instance to the weak instance + for (registry_name, registry) in map { + new_registries.map.insert(registry_name, registry); + } + } + instance_holder.instance = weak_instance; + + let entity_bundle = EntityBundle::new( + game_profile.uuid, + Vec3::default(), + azalea_registry::EntityKind::Player, + new_instance_name, + ); + let entity_id = p.player_id; + // insert our components into the ecs :) + commands.entity(self.player).insert(( + entity_id, + LocalGameMode { + current: p.common.game_type, + previous: p.common.previous_game_type.into(), + }, + entity_bundle, + )); + + azalea_entity::indexing::add_entity_to_indexes( + entity_id, + self.player, + Some(game_profile.uuid), + &mut entity_id_index, + &mut entity_uuid_index, + &mut instance_holder.instance.write(), + ); + + // update or insert loaded_by + if let Some(mut loaded_by) = loaded_by { + loaded_by.insert(self.player); + } else { + commands + .entity(self.player) + .insert(LoadedBy(HashSet::from_iter(vec![self.player]))); + } + + // send the client information that we have set + debug!( + "Sending client information because login: {:?}", + client_information + ); + send_packet_events.send(SendPacketEvent::new(self.player, + azalea_protocol::packets::game::s_client_information::ServerboundClientInformation { information: client_information.clone() }, + )); + }, + ); + } + + pub fn set_chunk_cache_radius(&mut self, p: &ClientboundSetChunkCacheRadius) { + debug!("Got set chunk cache radius packet {p:?}"); + } + + pub fn chunk_batch_start(&mut self, _p: &ClientboundChunkBatchStart) { + // the packet is empty, it's just a marker to tell us when the batch starts and + // ends + debug!("Got chunk batch start"); + + as_system::<EventWriter<_>>(self.ecs, |mut events| { + events.send(chunks::ChunkBatchStartEvent { + entity: self.player, + }); + }); + } + + pub fn chunk_batch_finished(&mut self, p: &ClientboundChunkBatchFinished) { + debug!("Got chunk batch finished {p:?}"); + + as_system::<EventWriter<_>>(self.ecs, |mut events| { + events.send(chunks::ChunkBatchFinishedEvent { + entity: self.player, + batch_size: p.batch_size, + }); + }); + } + + pub fn custom_payload(&mut self, p: &ClientboundCustomPayload) { + debug!("Got custom payload packet {p:?}"); + } + + pub fn change_difficulty(&mut self, p: &ClientboundChangeDifficulty) { + debug!("Got difficulty packet {p:?}"); + } + + pub fn commands(&mut self, _p: &ClientboundCommands) { + debug!("Got declare commands packet"); + } + + pub fn player_abilities(&mut self, p: &ClientboundPlayerAbilities) { + debug!("Got player abilities packet {p:?}"); + + as_system::<Query<&mut PlayerAbilities>>(self.ecs, |mut query| { + let mut player_abilities = query.get_mut(self.player).unwrap(); + + *player_abilities = PlayerAbilities::from(p); + }); + } + + pub fn set_cursor_item(&mut self, p: &ClientboundSetCursorItem) { + debug!("Got set cursor item packet {p:?}"); + } + + pub fn update_tags(&mut self, _p: &ClientboundUpdateTags) { + debug!("Got update tags packet"); + } + + pub fn disconnect(&mut self, p: &ClientboundDisconnect) { + warn!("Got disconnect packet {p:?}"); + + as_system::<EventWriter<_>>(self.ecs, |mut events| { + events.send(DisconnectEvent { + entity: self.player, + reason: Some(p.reason.clone()), + }); + }); + } + + pub fn update_recipes(&mut self, _p: &ClientboundUpdateRecipes) { + debug!("Got update recipes packet"); + } + + pub fn entity_event(&mut self, _p: &ClientboundEntityEvent) { + // debug!("Got entity event packet {p:?}"); + } + + pub fn player_position(&mut self, p: &ClientboundPlayerPosition) { + debug!("Got player position packet {p:?}"); + + as_system::<( + Query<( + &mut Physics, + &mut LookDirection, + &mut Position, + &mut LastSentPosition, + )>, + EventWriter<SendPacketEvent>, + )>(self.ecs, |(mut query, mut send_packet_events)| { + let Ok((mut physics, mut direction, mut position, mut last_sent_position)) = + query.get_mut(self.player) + else { + return; + }; + + **last_sent_position = **position; + + fn apply_change<T: Add<Output = T>>(base: T, condition: bool, change: T) -> T { + if condition { base + change } else { change } + } + + let new_x = apply_change(position.x, p.relative.x, p.change.pos.x); + let new_y = apply_change(position.y, p.relative.y, p.change.pos.y); + let new_z = apply_change(position.z, p.relative.z, p.change.pos.z); + + let new_y_rot = apply_change( + direction.y_rot, + p.relative.y_rot, + p.change.look_direction.y_rot, + ); + let new_x_rot = apply_change( + direction.x_rot, + p.relative.x_rot, + p.change.look_direction.x_rot, + ); + + let mut new_delta_from_rotations = physics.velocity; + if p.relative.rotate_delta { + let y_rot_delta = direction.y_rot - new_y_rot; + let x_rot_delta = direction.x_rot - new_x_rot; + new_delta_from_rotations = new_delta_from_rotations + .x_rot(math::to_radians(x_rot_delta as f64) as f32) + .y_rot(math::to_radians(y_rot_delta as f64) as f32); + } + + let new_delta = Vec3::new( + apply_change( + new_delta_from_rotations.x, + p.relative.delta_x, + p.change.delta.x, + ), + apply_change( + new_delta_from_rotations.y, + p.relative.delta_y, + p.change.delta.y, + ), + apply_change( + new_delta_from_rotations.z, + p.relative.delta_z, + p.change.delta.z, + ), + ); + + // apply the updates + + physics.velocity = new_delta; + + (direction.y_rot, direction.x_rot) = (new_y_rot, new_x_rot); + + let new_pos = Vec3::new(new_x, new_y, new_z); + if new_pos != **position { + **position = new_pos; + } + + // old_pos is set to the current position when we're teleported + physics.set_old_pos(&position); + + // send the relevant packets + + send_packet_events.send(SendPacketEvent::new( + self.player, + ServerboundAcceptTeleportation { id: p.id }, + )); + send_packet_events.send(SendPacketEvent::new( + self.player, + ServerboundMovePlayerPosRot { + pos: new_pos, + look_direction: LookDirection::new(new_y_rot, new_x_rot), + // this is always false + on_ground: false, + }, + )); + }); + } + + pub fn player_info_update(&mut self, p: &ClientboundPlayerInfoUpdate) { + debug!("Got player info packet {p:?}"); + + as_system::<( + Query<&mut TabList>, + EventWriter<AddPlayerEvent>, + EventWriter<UpdatePlayerEvent>, + ResMut<TabList>, + )>( + self.ecs, + |( + mut query, + mut add_player_events, + mut update_player_events, + mut tab_list_resource, + )| { + let mut tab_list = query.get_mut(self.player).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(), + }; + tab_list.insert(updated_info.profile.uuid, info.clone()); + add_player_events.send(AddPlayerEvent { + entity: self.player, + info: info.clone(), + }); + } else if let Some(info) = tab_list.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.clone_from(&updated_info.display_name); + } + update_player_events.send(UpdatePlayerEvent { + entity: self.player, + info: info.clone(), + }); + } else { + let uuid = updated_info.profile.uuid; + debug!("Ignoring PlayerInfoUpdate for unknown player {uuid}"); + } + } + + *tab_list_resource = tab_list.clone(); + }, + ); + } + + pub fn player_info_remove(&mut self, p: &ClientboundPlayerInfoRemove) { + debug!("Got chunk cache center packet {p:?}"); + + as_system::<( + Query<&mut TabList>, + EventWriter<RemovePlayerEvent>, + ResMut<TabList>, + )>( + self.ecs, + |(mut query, mut remove_player_events, mut tab_list_resource)| { + let mut tab_list = query.get_mut(self.player).unwrap(); + + for uuid in &p.profile_ids { + if let Some(info) = tab_list.remove(uuid) { + remove_player_events.send(RemovePlayerEvent { + entity: self.player, + info, + }); + } + tab_list_resource.remove(uuid); + } + }, + ); + } + + pub fn set_chunk_cache_center(&mut self, p: &ClientboundSetChunkCacheCenter) { + debug!("Got chunk cache center packet {p:?}"); + + as_system::<Query<&mut InstanceHolder>>(self.ecs, |mut query| { + let instance_holder = query.get_mut(self.player).unwrap(); + let mut partial_world = instance_holder.partial_instance.write(); + + partial_world + .chunks + .update_view_center(ChunkPos::new(p.x, p.z)); + }); + } + + pub fn chunks_biomes(&mut self, _p: &ClientboundChunksBiomes) {} + + pub fn light_update(&mut self, _p: &ClientboundLightUpdate) { + // debug!("Got light update packet {p:?}"); + } + + pub fn level_chunk_with_light(&mut self, p: &ClientboundLevelChunkWithLight) { + debug!("Got chunk with light packet {} {}", p.x, p.z); + + as_system::<EventWriter<_>>(self.ecs, |mut events| { + events.send(chunks::ReceiveChunkEvent { + entity: self.player, + packet: p.clone(), + }); + }); + } + + pub fn add_entity(&mut self, p: &ClientboundAddEntity) { + debug!("Got add entity packet {p:?}"); + + as_system::<( + Commands, + Query<(&mut EntityIdIndex, Option<&InstanceName>, Option<&TabList>)>, + Query<&mut LoadedBy>, + Query<Entity>, + Res<InstanceContainer>, + ResMut<EntityUuidIndex>, + )>( + self.ecs, + |( + mut commands, + mut query, + mut loaded_by_query, + entity_query, + instance_container, + mut entity_uuid_index, + )| { + let (mut entity_id_index, instance_name, tab_list) = + query.get_mut(self.player).unwrap(); + + let entity_id = p.id; + + let Some(instance_name) = instance_name else { + warn!("got add player packet but we haven't gotten a login packet yet"); + return; + }; + + // check if the entity already exists, and if it does then only add to LoadedBy + let instance = instance_container.get(instance_name).unwrap(); + if let Some(&ecs_entity) = instance.read().entity_by_id.get(&entity_id) { + // entity already exists + let Ok(mut loaded_by) = loaded_by_query.get_mut(ecs_entity) else { + // LoadedBy for this entity isn't in the ecs! figure out what went wrong + // and print an error + + let entity_in_ecs = entity_query.get(ecs_entity).is_ok(); + + if entity_in_ecs { + error!( + "LoadedBy for entity {entity_id:?} ({ecs_entity:?}) isn't in the ecs, but the entity is in entity_by_id" + ); + } else { + error!( + "Entity {entity_id:?} ({ecs_entity:?}) isn't in the ecs, but the entity is in entity_by_id" + ); + } + return; + }; + loaded_by.insert(self.player); + + // per-client id index + entity_id_index.insert(entity_id, ecs_entity); + + debug!("added to LoadedBy of entity {ecs_entity:?} with id {entity_id:?}"); + return; + }; + + // entity doesn't exist in the global index! + + let bundle = p.as_entity_bundle((**instance_name).clone()); + let mut spawned = + commands.spawn((entity_id, LoadedBy(HashSet::from([self.player])), bundle)); + let ecs_entity: Entity = spawned.id(); + debug!("spawned entity {ecs_entity:?} with id {entity_id:?}"); + + azalea_entity::indexing::add_entity_to_indexes( + entity_id, + ecs_entity, + Some(p.uuid), + &mut entity_id_index, + &mut entity_uuid_index, + &mut instance.write(), + ); + + // add the GameProfileComponent if the uuid is in the tab list + if let Some(tab_list) = tab_list { + // (technically this makes it possible for non-player entities to have + // GameProfileComponents but the server would have to be doing something + // really weird) + if let Some(player_info) = tab_list.get(&p.uuid) { + spawned.insert(GameProfileComponent(player_info.profile.clone())); + } + } + + // the bundle doesn't include the default entity metadata so we add that + // separately + p.apply_metadata(&mut spawned); + }, + ); + } + + pub fn set_entity_data(&mut self, p: &ClientboundSetEntityData) { + as_system::<( + Commands, + Query<(&EntityIdIndex, &InstanceHolder)>, + // this is a separate query since it's applied on the entity id that's being updated + // instead of the player that received the packet + Query<&EntityKind>, + )>(self.ecs, |(mut commands, query, entity_kind_query)| { + let (entity_id_index, instance_holder) = query.get(self.player).unwrap(); + + let entity = entity_id_index.get(p.id); + + let Some(entity) = entity else { + // some servers like hypixel trigger this a lot :( + debug!( + "Server sent an entity data packet for an entity id ({}) that we don't know about", + p.id + ); + return; + }; + + let entity_kind = *entity_kind_query + .get(entity) + .expect("EntityKind component should always be present for entities"); + + debug!("Got set entity data packet {p:?} for entity of kind {entity_kind:?}"); + + let packed_items = p.packed_items.clone().to_vec(); + + // we use RelativeEntityUpdate because it makes sure changes aren't made + // multiple times + commands.entity(entity).queue(RelativeEntityUpdate { + partial_world: instance_holder.partial_instance.clone(), + update: Box::new(move |entity| { + let entity_id = entity.id(); + entity.world_scope(|world| { + let mut commands_system_state = SystemState::<Commands>::new(world); + let mut commands = commands_system_state.get_mut(world); + let mut entity_commands = commands.entity(entity_id); + if let Err(e) = + apply_metadata(&mut entity_commands, *entity_kind, packed_items) + { + warn!("{e}"); + } + commands_system_state.apply(world); + }); + }), + }); + }); + } + + pub fn update_attributes(&mut self, _p: &ClientboundUpdateAttributes) { + // debug!("Got update attributes packet {p:?}"); + } + + pub fn set_entity_motion(&mut self, p: &ClientboundSetEntityMotion) { + // vanilla servers use this packet for knockback, but note that the Explode + // packet is also sometimes used by servers for knockback + + as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>( + self.ecs, + |(mut commands, query)| { + let (entity_id_index, instance_holder) = query.get(self.player).unwrap(); + + let Some(entity) = entity_id_index.get(p.id) else { + // note that this log (and some other ones like the one in RemoveEntities) + // sometimes happens when killing mobs. it seems to be a vanilla bug, which is + // why it's a debug log instead of a warning + debug!( + "Got set entity motion packet for unknown entity id {}", + p.id + ); + return; + }; + + // this is to make sure the same entity velocity update doesn't get sent + // multiple times when in swarms + + let knockback = KnockbackType::Set(Vec3 { + x: p.delta.xa as f64 / 8000., + y: p.delta.ya as f64 / 8000., + z: p.delta.za as f64 / 8000., + }); + + commands.entity(entity).queue(RelativeEntityUpdate { + partial_world: instance_holder.partial_instance.clone(), + update: Box::new(move |entity_mut| { + entity_mut.world_scope(|world| { + world.send_event(KnockbackEvent { entity, knockback }) + }); + }), + }); + }, + ); + } + + pub fn set_entity_link(&mut self, p: &ClientboundSetEntityLink) { + debug!("Got set entity link packet {p:?}"); + } + + pub fn initialize_border(&mut self, p: &ClientboundInitializeBorder) { + debug!("Got initialize border packet {p:?}"); + } + + pub fn set_time(&mut self, _p: &ClientboundSetTime) { + // debug!("Got set time packet {p:?}"); + } + + pub fn set_default_spawn_position(&mut self, p: &ClientboundSetDefaultSpawnPosition) { + debug!("Got set default spawn position packet {p:?}"); + } + + pub fn set_health(&mut self, p: &ClientboundSetHealth) { + debug!("Got set health packet {p:?}"); + + as_system::<Query<(&mut Health, &mut Hunger)>>(self.ecs, |mut query| { + let (mut health, mut hunger) = query.get_mut(self.player).unwrap(); + + **health = p.health; + (hunger.food, hunger.saturation) = (p.food, p.saturation); + + // the `Dead` component is added by the `update_dead` system + // in azalea-world and then the `dead_event` system fires + // the Death event. + }); + } + + pub fn set_experience(&mut self, p: &ClientboundSetExperience) { + debug!("Got set experience packet {p:?}"); + } + + pub fn teleport_entity(&mut self, p: &ClientboundTeleportEntity) { + as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>( + self.ecs, + |(mut commands, mut query)| { + let (entity_id_index, instance_holder) = query.get_mut(self.player).unwrap(); + + let Some(entity) = entity_id_index.get(p.id) else { + warn!("Got teleport entity packet for unknown entity id {}", p.id); + return; + }; + + let new_pos = p.change.pos; + let new_look_direction = LookDirection { + x_rot: (p.change.look_direction.x_rot as i32 * 360) as f32 / 256., + y_rot: (p.change.look_direction.y_rot as i32 * 360) as f32 / 256., + }; + commands.entity(entity).queue(RelativeEntityUpdate { + partial_world: instance_holder.partial_instance.clone(), + update: Box::new(move |entity| { + let mut position = entity.get_mut::<Position>().unwrap(); + if new_pos != **position { + **position = new_pos; + } + let position = *position; + let mut look_direction = entity.get_mut::<LookDirection>().unwrap(); + if new_look_direction != *look_direction { + *look_direction = new_look_direction; + } + // old_pos is set to the current position when we're teleported + let mut physics = entity.get_mut::<Physics>().unwrap(); + physics.set_old_pos(&position); + }), + }); + }, + ); + } + + pub fn update_advancements(&mut self, p: &ClientboundUpdateAdvancements) { + debug!("Got update advancements packet {p:?}"); + } + + pub fn rotate_head(&mut self, _p: &ClientboundRotateHead) {} + + pub fn move_entity_pos(&mut self, p: &ClientboundMoveEntityPos) { + as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>( + self.ecs, + |(mut commands, mut query)| { + let (entity_id_index, instance_holder) = query.get_mut(self.player).unwrap(); + + debug!("Got move entity pos packet {p:?}"); + + let Some(entity) = entity_id_index.get(p.entity_id) else { + debug!( + "Got move entity pos packet for unknown entity id {}", + p.entity_id + ); + return; + }; + + let new_delta = p.delta.clone(); + let new_on_ground = p.on_ground; + commands.entity(entity).queue(RelativeEntityUpdate { + partial_world: instance_holder.partial_instance.clone(), + update: Box::new(move |entity_mut| { + let mut physics = entity_mut.get_mut::<Physics>().unwrap(); + let new_pos = physics.vec_delta_codec.decode( + new_delta.xa as i64, + new_delta.ya as i64, + new_delta.za as i64, + ); + physics.vec_delta_codec.set_base(new_pos); + physics.set_on_ground(new_on_ground); + + let mut position = entity_mut.get_mut::<Position>().unwrap(); + if new_pos != **position { + **position = new_pos; + } + }), + }); + }, + ); + } + + pub fn move_entity_pos_rot(&mut self, p: &ClientboundMoveEntityPosRot) { + as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>( + self.ecs, + |(mut commands, mut query)| { + let (entity_id_index, instance_holder) = query.get_mut(self.player).unwrap(); + + debug!("Got move entity pos rot packet {p:?}"); + + let entity = entity_id_index.get(p.entity_id); + + let Some(entity) = entity else { + // often triggered by hypixel :( + debug!( + "Got move entity pos rot packet for unknown entity id {}", + p.entity_id + ); + return; + }; + + let new_delta = p.delta.clone(); + let new_look_direction = LookDirection { + x_rot: (p.x_rot as i32 * 360) as f32 / 256., + y_rot: (p.y_rot as i32 * 360) as f32 / 256., + }; + + let new_on_ground = p.on_ground; + + commands.entity(entity).queue(RelativeEntityUpdate { + partial_world: instance_holder.partial_instance.clone(), + update: Box::new(move |entity_mut| { + let mut physics = entity_mut.get_mut::<Physics>().unwrap(); + let new_pos = physics.vec_delta_codec.decode( + new_delta.xa as i64, + new_delta.ya as i64, + new_delta.za as i64, + ); + physics.vec_delta_codec.set_base(new_pos); + physics.set_on_ground(new_on_ground); + + let mut position = entity_mut.get_mut::<Position>().unwrap(); + if new_pos != **position { + **position = new_pos; + } + + let mut look_direction = entity_mut.get_mut::<LookDirection>().unwrap(); + if new_look_direction != *look_direction { + *look_direction = new_look_direction; + } + }), + }); + }, + ); + } + + pub fn move_entity_rot(&mut self, p: &ClientboundMoveEntityRot) { + as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>( + self.ecs, + |(mut commands, mut query)| { + let (entity_id_index, instance_holder) = query.get_mut(self.player).unwrap(); + + let entity = entity_id_index.get(p.entity_id); + if let Some(entity) = entity { + let new_look_direction = LookDirection { + x_rot: (p.x_rot as i32 * 360) as f32 / 256., + y_rot: (p.y_rot as i32 * 360) as f32 / 256., + }; + let new_on_ground = p.on_ground; + + commands.entity(entity).queue(RelativeEntityUpdate { + partial_world: instance_holder.partial_instance.clone(), + update: Box::new(move |entity_mut| { + let mut physics = entity_mut.get_mut::<Physics>().unwrap(); + physics.set_on_ground(new_on_ground); + + let mut look_direction = entity_mut.get_mut::<LookDirection>().unwrap(); + if new_look_direction != *look_direction { + *look_direction = new_look_direction; + } + }), + }); + } else { + warn!( + "Got move entity rot packet for unknown entity id {}", + p.entity_id + ); + } + }, + ); + } + pub fn keep_alive(&mut self, p: &ClientboundKeepAlive) { + debug!("Got keep alive packet {p:?} for {:?}", self.player); + + as_system::<(EventWriter<KeepAliveEvent>, EventWriter<SendPacketEvent>)>( + self.ecs, + |(mut keepalive_events, mut send_packet_events)| { + keepalive_events.send(KeepAliveEvent { + entity: self.player, + id: p.id, + }); + send_packet_events.send(SendPacketEvent::new( + self.player, + ServerboundKeepAlive { id: p.id }, + )); + }, + ); + } + + pub fn remove_entities(&mut self, p: &ClientboundRemoveEntities) { + debug!("Got remove entities packet {p:?}"); + + as_system::<(Query<&mut EntityIdIndex>, Query<&mut LoadedBy>)>( + self.ecs, + |(mut query, mut entity_query)| { + let Ok(mut entity_id_index) = query.get_mut(self.player) else { + warn!("our local player doesn't have EntityIdIndex"); + return; + }; + + for &id in &p.entity_ids { + let Some(entity) = entity_id_index.remove(id) else { + debug!( + "Tried to remove entity with id {id} but it wasn't in the EntityIdIndex" + ); + continue; + }; + let Ok(mut loaded_by) = entity_query.get_mut(entity) else { + warn!( + "tried to despawn entity {id} but it doesn't have a LoadedBy component", + ); + continue; + }; + + // the [`remove_despawned_entities_from_indexes`] system will despawn the entity + // if it's not loaded by anything anymore + + // also we can't just ecs.despawn because if we're in a swarm then the entity + // might still be loaded by another client + + loaded_by.remove(&self.player); + } + }, + ); + } + pub fn player_chat(&mut self, p: &ClientboundPlayerChat) { + debug!("Got player chat packet {p:?}"); + + as_system::<EventWriter<_>>(self.ecs, |mut events| { + events.send(ChatReceivedEvent { + entity: self.player, + packet: ChatPacket::Player(Arc::new(p.clone())), + }); + }); + } + + pub fn system_chat(&mut self, p: &ClientboundSystemChat) { + debug!("Got system chat packet {p:?}"); + + as_system::<EventWriter<_>>(self.ecs, |mut events| { + events.send(ChatReceivedEvent { + entity: self.player, + packet: ChatPacket::System(Arc::new(p.clone())), + }); + }); + } + + pub fn disguised_chat(&mut self, p: &ClientboundDisguisedChat) { + debug!("Got disguised chat packet {p:?}"); + + as_system::<EventWriter<_>>(self.ecs, |mut events| { + events.send(ChatReceivedEvent { + entity: self.player, + packet: ChatPacket::Disguised(Arc::new(p.clone())), + }); + }); + } + + pub fn sound(&mut self, _p: &ClientboundSound) {} + + pub fn level_event(&mut self, p: &ClientboundLevelEvent) { + debug!("Got level event packet {p:?}"); + } + + pub fn block_update(&mut self, p: &ClientboundBlockUpdate) { + debug!("Got block update packet {p:?}"); + + as_system::<Query<&mut InstanceHolder>>(self.ecs, |mut query| { + let local_player = query.get_mut(self.player).unwrap(); + + let world = local_player.instance.write(); + + world.chunks.set_block_state(&p.pos, p.block_state); + }); + } + + pub fn animate(&mut self, p: &ClientboundAnimate) { + debug!("Got animate packet {p:?}"); + } + + pub fn section_blocks_update(&mut self, p: &ClientboundSectionBlocksUpdate) { + debug!("Got section blocks update packet {p:?}"); + + as_system::<Query<&mut InstanceHolder>>(self.ecs, |mut query| { + let local_player = query.get_mut(self.player).unwrap(); + let world = local_player.instance.write(); + for state in &p.states { + world + .chunks + .set_block_state(&(p.section_pos + state.pos), state.state); + } + }); + } + + pub fn game_event(&mut self, p: &ClientboundGameEvent) { + use azalea_protocol::packets::game::c_game_event::EventType; + + debug!("Got game event packet {p:?}"); + + #[allow(clippy::single_match)] + match p.event { + EventType::ChangeGameMode => { + as_system::<Query<&mut LocalGameMode>>(self.ecs, |mut query| { + let mut local_game_mode = query.get_mut(self.player).unwrap(); + if let Some(new_game_mode) = GameMode::from_id(p.param as u8) { + local_game_mode.current = new_game_mode; + } + }); + } + _ => {} + } + } + + pub fn level_particles(&mut self, p: &ClientboundLevelParticles) { + debug!("Got level particles packet {p:?}"); + } + + pub fn server_data(&mut self, p: &ClientboundServerData) { + debug!("Got server data packet {p:?}"); + } + + pub fn set_equipment(&mut self, p: &ClientboundSetEquipment) { + debug!("Got set equipment packet {p:?}"); + } + + pub fn update_mob_effect(&mut self, p: &ClientboundUpdateMobEffect) { + debug!("Got update mob effect packet {p:?}"); + } + + pub fn add_experience_orb(&mut self, _p: &ClientboundAddExperienceOrb) {} + + pub fn award_stats(&mut self, _p: &ClientboundAwardStats) {} + + pub fn block_changed_ack(&mut self, _p: &ClientboundBlockChangedAck) {} + + pub fn block_destruction(&mut self, _p: &ClientboundBlockDestruction) {} + + pub fn block_entity_data(&mut self, _p: &ClientboundBlockEntityData) {} + + pub fn block_event(&mut self, p: &ClientboundBlockEvent) { + debug!("Got block event packet {p:?}"); + } + + pub fn boss_event(&mut self, _p: &ClientboundBossEvent) {} + + pub fn command_suggestions(&mut self, _p: &ClientboundCommandSuggestions) {} + + pub fn container_set_content(&mut self, p: &ClientboundContainerSetContent) { + debug!("Got container set content packet {p:?}"); + + as_system::<(Query<&mut Inventory>, EventWriter<_>)>( + self.ecs, + |(mut query, mut events)| { + let mut inventory = query.get_mut(self.player).unwrap(); + + // container id 0 is always the player's inventory + if p.container_id == 0 { + // this is just so it has the same type as the `else` block + for (i, slot) in p.items.iter().enumerate() { + if let Some(slot_mut) = inventory.inventory_menu.slot_mut(i) { + *slot_mut = slot.clone(); + } + } + } else { + events.send(SetContainerContentEvent { + entity: self.player, + slots: p.items.clone(), + container_id: p.container_id, + }); + } + }, + ); + } + + pub fn container_set_data(&mut self, p: &ClientboundContainerSetData) { + debug!("Got container set data packet {p:?}"); + + // TODO: handle ContainerSetData packet + // this is used for various things like the furnace progress + // bar + // see https://wiki.vg/Protocol#Set_Container_Property + + // as_system::<Query<&mut Inventory>>(self.ecs, |mut query| { + // let inventory = query.get_mut(self.player).unwrap(); + // }); + } + + pub fn container_set_slot(&mut self, p: &ClientboundContainerSetSlot) { + debug!("Got container set slot packet {p:?}"); + + as_system::<Query<&mut Inventory>>(self.ecs, |mut query| { + let mut inventory = query.get_mut(self.player).unwrap(); + + if p.container_id == -1 { + // -1 means carried item + inventory.carried = p.item_stack.clone(); + } else if p.container_id == -2 { + if let Some(slot) = inventory.inventory_menu.slot_mut(p.slot.into()) { + *slot = p.item_stack.clone(); + } + } else { + let is_creative_mode_and_inventory_closed = false; + // technically minecraft has slightly different behavior here if you're in + // creative mode and have your inventory open + if p.container_id == 0 && azalea_inventory::Player::is_hotbar_slot(p.slot.into()) { + // minecraft also sets a "pop time" here which is used for an animation + // but that's not really necessary + if let Some(slot) = inventory.inventory_menu.slot_mut(p.slot.into()) { + *slot = p.item_stack.clone(); + } + } else if p.container_id == inventory.id + && (p.container_id != 0 || !is_creative_mode_and_inventory_closed) + { + // var2.containerMenu.setItem(var4, var1.getStateId(), var3); + if let Some(slot) = inventory.menu_mut().slot_mut(p.slot.into()) { + *slot = p.item_stack.clone(); + inventory.state_id = p.state_id; + } + } + } + }); + } + + pub fn container_close(&mut self, p: &ClientboundContainerClose) { + // there's a container_id field in the packet, but minecraft doesn't actually + // check it + + debug!("Got container close packet {p:?}"); + + as_system::<EventWriter<_>>(self.ecs, |mut events| { + events.send(ClientSideCloseContainerEvent { + entity: self.player, + }); + }); + } + + pub fn cooldown(&mut self, _p: &ClientboundCooldown) {} + + pub fn custom_chat_completions(&mut self, _p: &ClientboundCustomChatCompletions) {} + + pub fn delete_chat(&mut self, _p: &ClientboundDeleteChat) {} + + pub fn explode(&mut self, p: &ClientboundExplode) { + trace!("Got explode packet {p:?}"); + + as_system::<EventWriter<_>>(self.ecs, |mut knockback_events| { + if let Some(knockback) = p.knockback { + knockback_events.send(KnockbackEvent { + entity: self.player, + knockback: KnockbackType::Set(knockback), + }); + } + }); + } + + pub fn forget_level_chunk(&mut self, p: &ClientboundForgetLevelChunk) { + debug!("Got forget level chunk packet {p:?}"); + + as_system::<Query<&mut InstanceHolder>>(self.ecs, |mut query| { + let local_player = query.get_mut(self.player).unwrap(); + + let mut partial_instance = local_player.partial_instance.write(); + + partial_instance.chunks.limited_set(&p.pos, None); + }); + } + + pub fn horse_screen_open(&mut self, _p: &ClientboundHorseScreenOpen) {} + + pub fn map_item_data(&mut self, _p: &ClientboundMapItemData) {} + + pub fn merchant_offers(&mut self, _p: &ClientboundMerchantOffers) {} + + pub fn move_vehicle(&mut self, _p: &ClientboundMoveVehicle) {} + + pub fn open_book(&mut self, _p: &ClientboundOpenBook) {} + + pub fn open_screen(&mut self, p: &ClientboundOpenScreen) { + debug!("Got open screen packet {p:?}"); + + as_system::<EventWriter<_>>(self.ecs, |mut events| { + events.send(MenuOpenedEvent { + entity: self.player, + window_id: p.container_id, + menu_type: p.menu_type, + title: p.title.to_owned(), + }); + }); + } + + pub fn open_sign_editor(&mut self, _p: &ClientboundOpenSignEditor) {} + + pub fn ping(&mut self, p: &ClientboundPing) { + debug!("Got ping packet {p:?}"); + + as_system::<EventWriter<_>>(self.ecs, |mut events| { + events.send(SendPacketEvent::new( + self.player, + ServerboundPong { id: p.id }, + )); + }); + } + + pub fn place_ghost_recipe(&mut self, _p: &ClientboundPlaceGhostRecipe) {} + + pub fn player_combat_end(&mut self, _p: &ClientboundPlayerCombatEnd) {} + + pub fn player_combat_enter(&mut self, _p: &ClientboundPlayerCombatEnter) {} + + pub fn player_combat_kill(&mut self, p: &ClientboundPlayerCombatKill) { + debug!("Got player kill packet {p:?}"); + + as_system::<( + Commands, + Query<(&MinecraftEntityId, Option<&Dead>)>, + EventWriter<_>, + )>(self.ecs, |(mut commands, mut query, mut events)| { + let (entity_id, dead) = query.get_mut(self.player).unwrap(); + + if *entity_id == p.player_id && dead.is_none() { + commands.entity(self.player).insert(Dead); + events.send(DeathEvent { + entity: self.player, + packet: Some(p.clone()), + }); + } + }); + } + + pub fn player_look_at(&mut self, _p: &ClientboundPlayerLookAt) {} + + pub fn remove_mob_effect(&mut self, _p: &ClientboundRemoveMobEffect) {} + + pub fn resource_pack_push(&mut self, p: &ClientboundResourcePackPush) { + debug!("Got resource pack packet {p:?}"); + + as_system::<EventWriter<_>>(self.ecs, |mut events| { + events.send(ResourcePackEvent { + entity: self.player, + id: p.id, + url: p.url.to_owned(), + hash: p.hash.to_owned(), + required: p.required, + prompt: p.prompt.to_owned(), + }); + }); + } + + pub fn resource_pack_pop(&mut self, _p: &ClientboundResourcePackPop) {} + + pub fn respawn(&mut self, p: &ClientboundRespawn) { + debug!("Got respawn packet {p:?}"); + + as_system::<( + Commands, + Query<( + &mut InstanceHolder, + &GameProfileComponent, + &ClientInformation, + )>, + EventWriter<_>, + ResMut<InstanceContainer>, + )>( + self.ecs, + |(mut commands, mut query, mut events, mut instance_container)| { + let (mut instance_holder, game_profile, client_information) = + query.get_mut(self.player).unwrap(); + + let new_instance_name = p.common.dimension.clone(); + + let Some((_dimension_type, dimension_data)) = p + .common + .dimension_type(&instance_holder.instance.read().registries) + else { + return; + }; + + // add this world to the instance_container (or don't if it's already + // there) + let weak_instance = instance_container.insert( + new_instance_name.clone(), + dimension_data.height, + dimension_data.min_y, + &instance_holder.instance.read().registries, + ); + events.send(InstanceLoadedEvent { + entity: self.player, + name: new_instance_name.clone(), + instance: Arc::downgrade(&weak_instance), + }); + + // set the partial_world to an empty world + // (when we add chunks or entities those will be in the + // instance_container) + + *instance_holder.partial_instance.write() = PartialInstance::new( + azalea_world::chunk_storage::calculate_chunk_storage_range( + client_information.view_distance.into(), + ), + Some(self.player), + ); + instance_holder.instance = weak_instance; + + // this resets a bunch of our components like physics and stuff + let entity_bundle = EntityBundle::new( + game_profile.uuid, + Vec3::default(), + azalea_registry::EntityKind::Player, + new_instance_name, + ); + // update the local gamemode and metadata things + commands.entity(self.player).insert(( + LocalGameMode { + current: p.common.game_type, + previous: p.common.previous_game_type.into(), + }, + entity_bundle, + )); + + // Remove the Dead marker component from the player. + commands.entity(self.player).remove::<Dead>(); + }, + ) + } + + pub fn start_configuration(&mut self, _p: &ClientboundStartConfiguration) { + as_system::<(Commands, EventWriter<_>)>(self.ecs, |(mut commands, mut events)| { + events.send(SendPacketEvent::new( + self.player, + ServerboundConfigurationAcknowledged {}, + )); + + commands + .entity(self.player) + .insert(crate::client::InConfigState) + .remove::<crate::JoinedClientBundle>(); + }); + } + + pub fn entity_position_sync(&mut self, p: &ClientboundEntityPositionSync) { + as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>( + self.ecs, + |(mut commands, mut query)| { + let (entity_id_index, instance_holder) = query.get_mut(self.player).unwrap(); + + let Some(entity) = entity_id_index.get(p.id) else { + debug!("Got teleport entity packet for unknown entity id {}", p.id); + return; + }; + + let new_position = p.values.pos; + let new_on_ground = p.on_ground; + let new_look_direction = p.values.look_direction; + + commands.entity(entity).queue(RelativeEntityUpdate { + partial_world: instance_holder.partial_instance.clone(), + update: Box::new(move |entity_mut| { + let is_local_entity = entity_mut.get::<LocalEntity>().is_some(); + let mut physics = entity_mut.get_mut::<Physics>().unwrap(); + + physics.vec_delta_codec.set_base(new_position); + + if is_local_entity { + debug!("Ignoring entity position sync packet for local player"); + return; + } + + physics.set_on_ground(new_on_ground); + + let mut last_sent_position = + entity_mut.get_mut::<LastSentPosition>().unwrap(); + **last_sent_position = new_position; + let mut position = entity_mut.get_mut::<Position>().unwrap(); + **position = new_position; + + let mut look_direction = entity_mut.get_mut::<LookDirection>().unwrap(); + *look_direction = new_look_direction; + }), + }); + }, + ); + } + + pub fn select_advancements_tab(&mut self, _p: &ClientboundSelectAdvancementsTab) {} + pub fn set_action_bar_text(&mut self, _p: &ClientboundSetActionBarText) {} + pub fn set_border_center(&mut self, _p: &ClientboundSetBorderCenter) {} + pub fn set_border_lerp_size(&mut self, _p: &ClientboundSetBorderLerpSize) {} + pub fn set_border_size(&mut self, _p: &ClientboundSetBorderSize) {} + pub fn set_border_warning_delay(&mut self, _p: &ClientboundSetBorderWarningDelay) {} + pub fn set_border_warning_distance(&mut self, _p: &ClientboundSetBorderWarningDistance) {} + pub fn set_camera(&mut self, _p: &ClientboundSetCamera) {} + pub fn set_display_objective(&mut self, _p: &ClientboundSetDisplayObjective) {} + pub fn set_objective(&mut self, _p: &ClientboundSetObjective) {} + pub fn set_passengers(&mut self, _p: &ClientboundSetPassengers) {} + pub fn set_player_team(&mut self, _p: &ClientboundSetPlayerTeam) {} + pub fn set_score(&mut self, _p: &ClientboundSetScore) {} + pub fn set_simulation_distance(&mut self, _p: &ClientboundSetSimulationDistance) {} + pub fn set_subtitle_text(&mut self, _p: &ClientboundSetSubtitleText) {} + pub fn set_title_text(&mut self, _p: &ClientboundSetTitleText) {} + pub fn set_titles_animation(&mut self, _p: &ClientboundSetTitlesAnimation) {} + pub fn clear_titles(&mut self, _p: &ClientboundClearTitles) {} + pub fn sound_entity(&mut self, _p: &ClientboundSoundEntity) {} + pub fn stop_sound(&mut self, _p: &ClientboundStopSound) {} + pub fn tab_list(&mut self, _p: &ClientboundTabList) {} + pub fn tag_query(&mut self, _p: &ClientboundTagQuery) {} + pub fn take_item_entity(&mut self, _p: &ClientboundTakeItemEntity) {} + pub fn bundle_delimiter(&mut self, _p: &ClientboundBundleDelimiter) {} + pub fn damage_event(&mut self, _p: &ClientboundDamageEvent) {} + pub fn hurt_animation(&mut self, _p: &ClientboundHurtAnimation) {} + pub fn ticking_state(&mut self, _p: &ClientboundTickingState) {} + pub fn ticking_step(&mut self, _p: &ClientboundTickingStep) {} + pub fn reset_score(&mut self, _p: &ClientboundResetScore) {} + pub fn cookie_request(&mut self, _p: &ClientboundCookieRequest) {} + pub fn debug_sample(&mut self, _p: &ClientboundDebugSample) {} + pub fn pong_response(&mut self, _p: &ClientboundPongResponse) {} + pub fn store_cookie(&mut self, _p: &ClientboundStoreCookie) {} + pub fn transfer(&mut self, _p: &ClientboundTransfer) {} + pub fn move_minecart_along_track(&mut self, _p: &ClientboundMoveMinecartAlongTrack) {} + pub fn set_held_slot(&mut self, _p: &ClientboundSetHeldSlot) {} + pub fn set_player_inventory(&mut self, _p: &ClientboundSetPlayerInventory) {} + pub fn projectile_power(&mut self, _p: &ClientboundProjectilePower) {} + pub fn custom_report_details(&mut self, _p: &ClientboundCustomReportDetails) {} + pub fn server_links(&mut self, _p: &ClientboundServerLinks) {} + pub fn player_rotation(&mut self, _p: &ClientboundPlayerRotation) {} + pub fn recipe_book_add(&mut self, _p: &ClientboundRecipeBookAdd) {} + pub fn recipe_book_remove(&mut self, _p: &ClientboundRecipeBookRemove) {} + pub fn recipe_book_settings(&mut self, _p: &ClientboundRecipeBookSettings) {} +} diff --git a/azalea-client/src/plugins/packet/login.rs b/azalea-client/src/plugins/packet/login.rs new file mode 100644 index 00000000..1bb07266 --- /dev/null +++ b/azalea-client/src/plugins/packet/login.rs @@ -0,0 +1,114 @@ +// login packets aren't actually handled here because compression/encryption +// would make packet handling a lot messier + +use std::{collections::HashSet, sync::Arc}; + +use azalea_protocol::packets::{ + Packet, + login::{ + ClientboundLoginPacket, ServerboundLoginPacket, + s_custom_query_answer::ServerboundCustomQueryAnswer, + }, +}; +use bevy_ecs::{prelude::*, system::SystemState}; +use derive_more::{Deref, DerefMut}; +use tokio::sync::mpsc; +use tracing::error; + +// this struct is defined here anyways though so it's consistent with the other +// ones + +/// An event that's sent when we receive a login packet from the server. Note +/// that if you want to handle this in a system, you must add +/// `.before(azalea::packet::login::process_packet_events)` to it +/// because that system clears the events. +#[derive(Event, Debug, Clone)] +pub struct LoginPacketEvent { + /// The client entity that received the packet. + pub entity: Entity, + /// The packet that was actually received. + pub packet: Arc<ClientboundLoginPacket>, +} + +/// Event for sending a login packet to the server. +#[derive(Event)] +pub struct SendLoginPacketEvent { + pub entity: Entity, + pub packet: ServerboundLoginPacket, +} +impl SendLoginPacketEvent { + pub fn new(entity: Entity, packet: impl Packet<ServerboundLoginPacket>) -> Self { + let packet = packet.into_variant(); + Self { entity, packet } + } +} + +#[derive(Component)] +pub struct LoginSendPacketQueue { + pub tx: mpsc::UnboundedSender<ServerboundLoginPacket>, +} + +/// A marker component for local players that are currently in the +/// `login` state. +#[derive(Component, Clone, Debug)] +pub struct InLoginState; + +pub fn handle_send_packet_event( + mut send_packet_events: EventReader<SendLoginPacketEvent>, + mut query: Query<&mut LoginSendPacketQueue>, +) { + for event in send_packet_events.read() { + if let Ok(queue) = query.get_mut(event.entity) { + let _ = queue.tx.send(event.packet.clone()); + } else { + error!("Sent SendPacketEvent for entity that doesn't have a LoginSendPacketQueue"); + } + } +} + +/// Plugins can add to this set if they want to handle a custom query packet +/// themselves. This component removed after the login state ends. +#[derive(Component, Default, Debug, Deref, DerefMut)] +pub struct IgnoreQueryIds(HashSet<u32>); + +pub fn process_packet_events(ecs: &mut World) { + let mut events_owned = Vec::new(); + let mut system_state: SystemState<ResMut<Events<LoginPacketEvent>>> = SystemState::new(ecs); + let mut events = system_state.get_mut(ecs); + for LoginPacketEvent { + entity: player_entity, + packet, + } in events.drain() + { + // we do this so `ecs` isn't borrowed for the whole loop + events_owned.push((player_entity, packet)); + } + for (player_entity, packet) in events_owned { + #[allow(clippy::single_match)] + match packet.as_ref() { + ClientboundLoginPacket::CustomQuery(p) => { + let mut system_state: SystemState<( + EventWriter<SendLoginPacketEvent>, + Query<&IgnoreQueryIds>, + )> = SystemState::new(ecs); + let (mut send_packet_events, query) = system_state.get_mut(ecs); + + let ignore_query_ids = query.get(player_entity).ok().map(|x| x.0.clone()); + if let Some(ignore_query_ids) = ignore_query_ids { + if ignore_query_ids.contains(&p.transaction_id) { + continue; + } + } + + send_packet_events.send(SendLoginPacketEvent::new( + player_entity, + ServerboundCustomQueryAnswer { + transaction_id: p.transaction_id, + data: None, + }, + )); + } + _ => {} + } + } +} diff --git a/azalea-client/src/plugins/packet/mod.rs b/azalea-client/src/plugins/packet/mod.rs new file mode 100644 index 00000000..cbd8a175 --- /dev/null +++ b/azalea-client/src/plugins/packet/mod.rs @@ -0,0 +1,109 @@ +use azalea_entity::{EntityUpdateSet, metadata::Health}; +use bevy_app::{App, First, Plugin, PreUpdate, Update}; +use bevy_ecs::{ + prelude::*, + system::{SystemParam, SystemState}, +}; + +use self::{ + game::{ + AddPlayerEvent, DeathEvent, InstanceLoadedEvent, KeepAliveEvent, RemovePlayerEvent, + ResourcePackEvent, UpdatePlayerEvent, + }, + login::{LoginPacketEvent, SendLoginPacketEvent}, +}; +use crate::{chat::ChatReceivedEvent, events::death_listener}; + +pub mod config; +pub mod game; +pub mod login; + +pub struct PacketPlugin; + +pub fn death_event_on_0_health( + query: Query<(Entity, &Health), Changed<Health>>, + mut death_events: EventWriter<DeathEvent>, +) { + for (entity, health) in query.iter() { + if **health == 0. { + death_events.send(DeathEvent { + entity, + packet: None, + }); + } + } +} + +impl Plugin for PacketPlugin { + fn build(&self, app: &mut App) { + app.add_systems( + First, + (game::send_receivepacketevent, config::send_packet_events), + ) + .add_systems( + PreUpdate, + ( + game::process_packet_events + // we want to index and deindex right after + .before(EntityUpdateSet::Deindex), + config::process_packet_events, + login::handle_send_packet_event, + login::process_packet_events, + ), + ) + .add_systems( + Update, + ( + ( + config::handle_send_packet_event, + game::handle_outgoing_packets, + ) + .chain(), + death_event_on_0_health.before(death_listener), + ), + ) + // we do this instead of add_event so we can handle the events ourselves + .init_resource::<Events<game::ReceivePacketEvent>>() + .init_resource::<Events<config::ReceiveConfigPacketEvent>>() + .add_event::<game::SendPacketEvent>() + .add_event::<config::SendConfigPacketEvent>() + .add_event::<AddPlayerEvent>() + .add_event::<RemovePlayerEvent>() + .add_event::<UpdatePlayerEvent>() + .add_event::<ChatReceivedEvent>() + .add_event::<DeathEvent>() + .add_event::<KeepAliveEvent>() + .add_event::<ResourcePackEvent>() + .add_event::<InstanceLoadedEvent>() + .add_event::<LoginPacketEvent>() + .add_event::<SendLoginPacketEvent>(); + } +} + +#[macro_export] +macro_rules! declare_packet_handlers { + ( + $packetenum:ident, + $packetvar:expr, + $handler:ident, + [$($packet:path),+ $(,)?] + ) => { + paste::paste! { + match $packetvar { + $( + $packetenum::[< $packet:camel >](p) => $handler.$packet(p), + )+ + } + } + }; +} + +pub(crate) fn as_system<T>(ecs: &mut World, f: impl FnOnce(T::Item<'_, '_>)) +where + T: SystemParam + 'static, +{ + let mut system_state = SystemState::<T>::new(ecs); + let values = system_state.get_mut(ecs); + f(values); + system_state.apply(ecs); +} diff --git a/azalea-client/src/plugins/respawn.rs b/azalea-client/src/plugins/respawn.rs new file mode 100644 index 00000000..5797406b --- /dev/null +++ b/azalea-client/src/plugins/respawn.rs @@ -0,0 +1,35 @@ +use azalea_protocol::packets::game::s_client_command::{self, ServerboundClientCommand}; +use bevy_app::{App, Plugin, Update}; +use bevy_ecs::prelude::*; + +use super::packet::game::handle_outgoing_packets; +use crate::packet::game::SendPacketEvent; + +/// Tell the server that we're respawning. +#[derive(Event, Debug, Clone)] +pub struct PerformRespawnEvent { + pub entity: Entity, +} + +/// A plugin that makes [`PerformRespawnEvent`] send the packet to respawn. +pub struct RespawnPlugin; +impl Plugin for RespawnPlugin { + fn build(&self, app: &mut App) { + app.add_event::<PerformRespawnEvent>() + .add_systems(Update, perform_respawn.before(handle_outgoing_packets)); + } +} + +pub fn perform_respawn( + mut events: EventReader<PerformRespawnEvent>, + mut send_packets: EventWriter<SendPacketEvent>, +) { + for event in events.read() { + send_packets.send(SendPacketEvent::new( + event.entity, + ServerboundClientCommand { + action: s_client_command::Action::PerformRespawn, + }, + )); + } +} diff --git a/azalea-client/src/plugins/task_pool.rs b/azalea-client/src/plugins/task_pool.rs new file mode 100644 index 00000000..ab56bf69 --- /dev/null +++ b/azalea-client/src/plugins/task_pool.rs @@ -0,0 +1,182 @@ +//! Borrowed from `bevy_core`. + +use std::marker::PhantomData; + +use bevy_app::{App, Last, Plugin}; +use bevy_ecs::system::{NonSend, Resource}; +use bevy_tasks::{ + AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool, TaskPoolBuilder, + tick_global_task_pools_on_main_thread, +}; + +/// 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_systems(Last, tick_global_task_pools); + } +} + +pub struct NonSendMarker(PhantomData<*mut ()>); +#[cfg(not(target_arch = "wasm32"))] +fn tick_global_task_pools(_main_thread_marker: Option<NonSend<NonSendMarker>>) { + tick_global_task_pools_on_main_thread(); +} + +/// Helper for configuring and creating the default task pools. For end-users +/// who want full control, set up [`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: 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: 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::get_or_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::get_or_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::get_or_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) + } +} diff --git a/azalea-client/src/plugins/tick_end.rs b/azalea-client/src/plugins/tick_end.rs new file mode 100644 index 00000000..c7737eb1 --- /dev/null +++ b/azalea-client/src/plugins/tick_end.rs @@ -0,0 +1,36 @@ +//! Clients send a [`ServerboundClientTickEnd`] packet every tick. + +use azalea_core::tick::GameTick; +use azalea_entity::LocalEntity; +use azalea_physics::PhysicsSet; +use azalea_protocol::packets::game::ServerboundClientTickEnd; +use azalea_world::InstanceName; +use bevy_app::{App, Plugin}; +use bevy_ecs::prelude::*; + +use crate::{mining::MiningSet, packet::game::SendPacketEvent}; + +/// A plugin that makes clients send a [`ServerboundClientTickEnd`] packet every +/// tick. +pub struct TickEndPlugin; +impl Plugin for TickEndPlugin { + fn build(&self, app: &mut App) { + app.add_systems( + GameTick, + // this has to happen after every other event that might send packets + game_tick_packet + .after(PhysicsSet) + .after(MiningSet) + .after(crate::movement::send_position), + ); + } +} + +pub fn game_tick_packet( + query: Query<Entity, (With<LocalEntity>, With<InstanceName>)>, + mut send_packets: EventWriter<SendPacketEvent>, +) { + for entity in query.iter() { + send_packets.send(SendPacketEvent::new(entity, ServerboundClientTickEnd)); + } +} |
