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