aboutsummaryrefslogtreecommitdiff
path: root/azalea-client/src/client.rs
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/client.rs
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/client.rs')
-rw-r--r--azalea-client/src/client.rs1135
1 files changed, 287 insertions, 848 deletions
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())
}
}