aboutsummaryrefslogtreecommitdiff
path: root/azalea-client/src
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2023-02-04 19:32:27 -0600
committerGitHub <noreply@github.com>2023-02-04 19:32:27 -0600
commita5672815ccef520b433363ac622dbb6d6af60c91 (patch)
treef9bb1b41876d81423ac3f188f4d368e6d362eed1 /azalea-client/src
parent7c7446ab1e467c29f86e9bfba260741fc469389a (diff)
downloadazalea-drasl-a5672815ccef520b433363ac622dbb6d6af60c91.tar.xz
Use an ECS (#52)
* add EntityData::kind * start making metadata use hecs * make entity codegen generate ecs stuff * fix registry codegen * get rid of worldhaver it's not even used * add bevy_ecs to deps * rename Component to FormattedText also start making the metadata use bevy_ecs but bevy_ecs doesn't let you query on Bundles so it's annoying * generate metadata.rs correctly for bevy_ecs * start switching more entity stuff to use ecs * more ecs stuff for entity storage * ok well it compiles but it definitely doesn't work * random fixes * change a bunch of entity things to use the components * some ecs stuff in az-client * packet handler uses the ecs now and other fun changes i still need to make ticking use the ecs but that's tricker, i'm considering using bevy_ecs systems for those bevy_ecs systems can't be async but the only async things in ticking is just sending packets which can just be done as a tokio task so that's not a big deal * start converting some functions in az-client into systems committing because i'm about to try something that might go horribly wrong * start splitting client i'm probably gonna change it so azalea entity ids are separate from minecraft entity ids next (so stuff like player ids can be consistent and we don't have to wait for the login packet) * separate minecraft entity ids from azalea entity ids + more ecs stuff i guess i'm using bevy_app now too huh it's necessary for plugins and it lets us control the tick rate anyways so it's fine i think i'm still not 100% sure how packet handling that interacts with the world will work, but i think if i can sneak the ecs world into there it'll be fine. Can't put packet handling in the schedule because that'd make it tick-bound, which it's not (technically it'd still work but it'd be wrong and anticheats might realize). * packet handling now it runs the schedule only when we get a tick or packet :smile: also i systemified some more functions and did other random fixes so az-world and az-physics compile making azalea-client use the ecs is almost done! all the hard parts are done now i hope, i just have to finish writing all the code so it actually works * start figuring out how functions in Client will work generally just lifetimes being annoying but i think i can get it all to work * make writing packets work synchronously* * huh az-client compiles * start fixing stuff * start fixing some packets * make packet handler work i still haven't actually tested any of this yet lol but in theory it should all work i'll probably either actually test az-client and fix all the remaining issues or update the azalea crate next ok also one thing that i'm not particularly happy with is how the packet handlers are doing ugly queries like ```rs let local_player = ecs .query::<&LocalPlayer>() .get_mut(ecs, player_entity) .unwrap(); ``` i think the right way to solve it would be by putting every packet handler in its own system but i haven't come up with a way to make that not be really annoying yet * fix warnings * ok what if i just have a bunch of queries and a single packet handler system * simple example for azalea-client * :bug: * maybe fix deadlock idk can't test it rn lmao * make physicsstate its own component * use the default plugins * azalea compiles lol * use systemstate for packet handler * fix entities basically moved some stuff from being in the world to just being components * physics (ticking) works * try to add a .entity_by function still doesn't work because i want to make the predicate magic * try to make entity_by work well it does work but i couldn't figure out how to make it look not terrible. Will hopefully change in the future * everything compiles * start converting swarm to use builder * continue switching swarm to builder and fix stuff * make swarm use builder still have to fix some stuff and make client use builder * fix death event * client builder * fix some warnings * document plugins a bit * start trying to fix tests * azalea-ecs * azalea-ecs stuff compiles * az-physics tests pass :tada: * fix all the tests * clippy on azalea-ecs-macros * remove now-unnecessary trait_upcasting feature * fix some clippy::pedantic warnings lol * why did cargo fmt not remove the trailing spaces * FIX ALL THE THINGS * when i said 'all' i meant non-swarm bugs * start adding task pool * fix entity deduplication * fix pathfinder not stopping * fix some more random bugs * fix panic that sometimes happens in swarms * make pathfinder run in task * fix some tests * fix doctests and clippy * deadlock * fix systems running in wrong order * fix non-swarm bots
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)
+ }
+}