diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index 66161053..c4cc14ca 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -1,6 +1,6 @@ pub use crate::chat::ChatPacket; use crate::{ - local_player::{send_tick_event, update_in_loaded_chunk, LocalPlayer}, + local_player::{death_event, send_tick_event, update_in_loaded_chunk, LocalPlayer}, movement::{local_player_ai_step, send_position}, packet_handling, plugins::PluginStates, @@ -93,10 +93,6 @@ pub struct Client { pub profile: GameProfile, /// The entity for this client in the ECS. pub entity: Entity, - /// 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>, /// The world that this client is in. pub world: Arc>, @@ -104,7 +100,6 @@ pub struct Client { /// 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, - tasks: Arc>>>, /// 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 @@ -140,7 +135,6 @@ impl Client { /// defaults, otherwise use [`Client::join`]. pub fn new( profile: GameProfile, - world_container: Option>>, entity: Entity, ecs: Arc>, ) -> Self { @@ -149,12 +143,9 @@ impl Client { // default our id to 0, it'll be set later entity, world: Arc::new(RwLock::new(PartialWorld::default())), - world_container: world_container - .unwrap_or_else(|| Arc::new(RwLock::new(WorldContainer::new()))), // 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()), - tasks: Arc::new(Mutex::new(Vec::new())), ecs, } @@ -209,13 +200,13 @@ impl Client { let entity = entity_mut.id(); // we got the GameConnection, so the server is now connected :) - let client = Client::new(game_profile.clone(), None, entity, ecs_lock.clone()); + let client = Client::new(game_profile.clone(), entity, ecs_lock.clone()); let world = client.world(); let (packet_writer_sender, packet_writer_receiver) = mpsc::unbounded_channel(); - let local_player = crate::local_player::LocalPlayer::new( + let mut local_player = crate::local_player::LocalPlayer::new( entity, game_profile, packet_writer_sender, @@ -236,8 +227,8 @@ impl Client { .clone() .write_task(write_conn, packet_writer_receiver), ); - client.tasks.lock().push(read_packets_task); - client.tasks.lock().push(write_packets_task); + local_player.tasks.push(read_packets_task); + local_player.tasks.push(write_packets_task); ecs.entity_mut(entity) .insert((local_player, packet_receiver)); @@ -379,12 +370,8 @@ impl Client { /// /// The OwnedReadHalf for the TCP connection is in one of the tasks, so it /// automatically closes the connection when that's dropped. - pub async fn disconnect(&self) -> Result<(), std::io::Error> { - let tasks = self.tasks.lock(); - for task in tasks.iter() { - task.abort(); - } - Ok(()) + pub fn disconnect(&self) { + self.local_player_mut(&mut self.ecs.lock()).disconnect(); } pub fn local_player<'a>(&'a self, ecs: &'a mut bevy_ecs::world::World) -> &'a LocalPlayer { @@ -405,12 +392,17 @@ impl Client { /// superset of the client's world. pub fn world(&self) -> Arc> { let mut ecs = self.ecs.lock(); - let world_name = self - .local_player(&mut ecs) - .world_name - .as_ref() - .expect("World name must be known if we're doing Client::world"); - let world_container = self.world_container.read(); + + 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_container = ecs.resource::(); world_container.get(&world_name).unwrap() } @@ -491,10 +483,12 @@ pub async fn start_ecs( .with_system(update_in_loaded_chunk) .with_system(local_player_ai_step) .with_system(send_tick_event), - ) - .add_system(packet_handling::handle_packets.label("handle_packets")) - // should happen last - .add_system(packet_handling::clear_packets.after("handle_packets")); + ); + + // fire the Death event when the player dies. + app.add_system(death_event.after("tick").after("packet")); + + app.init_resource::(); // all resources should have been added by now so we can take the ecs from the // app diff --git a/azalea-client/src/local_player.rs b/azalea-client/src/local_player.rs index a59c5e2f..93c89416 100644 --- a/azalea-client/src/local_player.rs +++ b/azalea-client/src/local_player.rs @@ -9,14 +9,14 @@ use azalea_auth::game_profile::GameProfile; use azalea_core::{ChunkPos, ResourceLocation}; use azalea_protocol::{connect::WriteConnection, packets::game::ServerboundGamePacket}; use azalea_world::{ - entity::{self, Entity}, - EntityInfos, PartialWorld, World, + entity::{self, Dead, Entity}, + EntityInfos, PartialWorld, World, WorldContainer, }; -use bevy_ecs::{component::Component, system::Query}; +use bevy_ecs::{component::Component, query::Added, system::Query}; use derive_more::{Deref, DerefMut}; -use parking_lot::RwLock; +use parking_lot::{Mutex, RwLock}; use thiserror::Error; -use tokio::sync::mpsc; +use tokio::{sync::mpsc, task::JoinHandle}; use uuid::Uuid; use crate::{ClientInformation, Event, PlayerInfo, WalkDirection}; @@ -34,16 +34,20 @@ pub struct LocalPlayer { /// A map of player uuids to their information in the tab list pub players: HashMap, + /// The partial world is the world this client currently has loaded. It has + /// a limited render distance. pub partial_world: Arc>, + /// 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>, pub world_name: Option, pub tx: mpsc::UnboundedSender, -} -/// Present if the player can be dead. -#[derive(Component, Copy, Clone, Default, Deref, DerefMut)] -pub struct Dead(bool); + /// 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>, +} #[derive(Default)] pub struct PhysicsState { @@ -87,7 +91,6 @@ impl LocalPlayer { physics_state: PhysicsState::default(), client_information: ClientInformation::default(), - dead: false, players: HashMap::new(), world, @@ -99,6 +102,8 @@ impl LocalPlayer { world_name: None, tx, + + tasks: Vec::new(), } } @@ -108,6 +113,16 @@ impl LocalPlayer { .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.iter() { + task.abort(); + } + } } pub fn send_tick_event(query: Query<&LocalPlayer>) { @@ -141,6 +156,13 @@ pub fn update_in_loaded_chunk( } } +/// Send the "Death" event for [`LocalPlayer`]s that died with no reason. +pub fn death_event(query: Query<&LocalPlayer, Added>) { + for local_player in &query { + local_player.tx.send(Event::Death(None)); + } +} + #[derive(Error, Debug)] pub enum HandlePacketError { #[error("{0}")] diff --git a/azalea-client/src/packet_handling.rs b/azalea-client/src/packet_handling.rs index 8a9a4bc5..438f56c0 100644 --- a/azalea-client/src/packet_handling.rs +++ b/azalea-client/src/packet_handling.rs @@ -1,16 +1,54 @@ -use std::sync::Arc; +use std::{io::Cursor, sync::Arc}; +use azalea_core::{ChunkPos, ResourceLocation, Vec3}; use azalea_protocol::{ connect::{ReadConnection, WriteConnection}, - packets::game::{ClientboundGamePacket, ServerboundGamePacket}, + packets::game::{ + 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::{MinecraftEntityId, Position}, EntityInfos}; -use bevy_ecs::{component::Component, prelude::Entity, query::Changed, system::Query}; -use log::{error, debug}; +use azalea_world::{ + entity::{ + metadata::{apply_metadata, Health, PlayerMetadataBundle}, + set_rotation, Dead, EntityBundle, LastSentPosition, MinecraftEntityId, Physics, + PlayerBundle, Position, + }, + EntityInfos, PartialWorld, WorldContainer, +}; +use bevy_app::{App, Plugin}; +use bevy_ecs::{ + component::Component, + prelude::Entity, + query::Changed, + schedule::{IntoSystemDescriptor, SystemSet}, + system::{Commands, Query, ResMut}, +}; +use iyes_loopless::prelude::*; +use log::{debug, error, trace, warn}; use parking_lot::Mutex; use tokio::sync::mpsc; -use crate::{local_player::{Dead, LocalPlayer}, Event}; +use crate::{local_player::LocalPlayer, ChatPacket, ClientInformation, Event, PlayerInfo}; + +pub struct PacketHandlerPlugin; +// .add_system(packet_handling::handle_packets.label("packet")) +// // should happen last +// .add_system(packet_handling::clear_packets.after("packet")); +impl Plugin for PacketHandlerPlugin { + fn build(&self, app: &mut App) { + app.add_system_set( + SystemSet::new() + .with_system(handle_packets.label("packet")) + .with_system(handle_login_packet.after("packet")) + // should happen last + .with_system(clear_packets.after("packet")), + ); + } +} /// Something that receives packets from the server. #[derive(Component, Clone)] @@ -19,135 +57,34 @@ pub struct PacketReceiver { pub run_schedule_sender: mpsc::UnboundedSender<()>, } -pub fn handle_packets( - ecs: &mut bevy_ecs::world::World, - // ecs: &mut bevy_ecs::world::World, -) { +fn handle_packets(ecs: &mut bevy_ecs::world::World) { let mut query = ecs.query_filtered::<(Entity, &PacketReceiver), Changed>(); - for (entity, packet_events) in query.iter_mut(ecs) { - for packet in packet_events.packets.lock().iter() { - handle_packet(ecs, entity, packet); + let mut players_and_packets = Vec::new(); + for (player_entity, packet_events) in query.iter(ecs) { + let packets = packet_events.packets.lock(); + if !packets.is_empty() { + players_and_packets.push((player_entity, packets.clone())); + } + } + for (player_entity, packets) in players_and_packets { + for packet in &packets { + handle_packet(ecs, player_entity, packet); } } } -pub fn handle_packet(ecs: &mut bevy_ecs::world::World, entity: Entity, packet: &ClientboundGamePacket) { +// TODO: i want to make it so each packet handler gets its own system (so it's +// not as annoying to query stuff) but idk how to make it good + +fn handle_packet( + ecs: &mut bevy_ecs::world::World, + player_entity: Entity, + packet: &ClientboundGamePacket, +) { 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(); - - 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, 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 = local_player.world.write(); *world_lock = PartialWorld::new( - local_player.client_information.view_distance.into(), - weak_world, Some(EntityId(p.player_id)), - ); - - let player_bundle = entity::PlayerBundle { - entity: entity::EntityBundle::new( - local_player.profile.uuid, - Vec3::default(), - azalea_registry::EntityKind::Player, - ), - metadata: PlayerMetadataBundle::default(), - }; - // let entity = EntityData::new( - // client.profile.uuid, - // Vec3::default(), - // EntityMetadata::Player(metadata::Player::default()), - // ); - // the first argument makes it so other entities don't update this entity in a shared world - world_lock.add_entity(EntityId(p.player_id), player_bundle); - - *client.entity_id.write() = EntityId(p.player_id); - } - - // send the client information that we have set - let client_information_packet: ClientInformation = - client.local_player().client_information.clone(); - log::debug!( - "Sending client information because login: {:?}", - client_information_packet - ); - client.write_packet(client_information_packet.get()); - - // 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?; + // handled by the handle_login_packet system } ClientboundGamePacket::SetChunkCacheRadius(p) => { debug!("Got set chunk cache radius packet {:?}", p); @@ -172,7 +109,13 @@ pub fn handle_packet(ecs: &mut bevy_ecs::world::World, entity: Entity, packet: & } ClientboundGamePacket::Disconnect(p) => { debug!("Got disconnect packet {:?}", p); - client.disconnect().await?; + + let (mut local_player,) = ecs + .query::<(&mut LocalPlayer,)>() + .get_mut(ecs, player_entity) + .unwrap(); + + local_player.disconnect(); } ClientboundGamePacket::UpdateRecipes(_p) => { debug!("Got update recipes packet"); @@ -187,185 +130,188 @@ pub fn handle_packet(ecs: &mut bevy_ecs::world::World, entity: Entity, packet: & // TODO: reply with teleport confirm debug!("Got player position packet {:?}", p); - let (new_pos, y_rot, x_rot) = { - let player_entity_id = *client.entity(); - let world = client.world(); - // let mut player_entity = - world.entity_mut(player_entity_id).unwrap(); let (mut - physics, position) = client.query::<(&mut - entity::Physics, &mut entity::Position)>(); + let (mut local_player, mut physics, mut position, mut last_sent_position) = ecs + .query::<( + &mut LocalPlayer, + &mut Physics, + &mut Position, + &mut LastSentPosition, + )>() + .get_mut(ecs, player_entity) + .unwrap(); - let delta_movement = physics.delta; + 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 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 { - physics.last_pos.x += p.x; - (delta_movement.x, position.x + p.x) - } else { - physics.last_pos.x = p.x; - (0.0, p.x) - }; - let (delta_y, new_pos_y) = if is_y_relative { - physics.last_pos.y += p.y; - (delta_movement.y, position.y + p.y) - } else { - physics.last_pos.y = p.y; - (0.0, p.y) - }; - let (delta_z, new_pos_z) = if is_z_relative { - physics.last_pos.z += p.z; - (delta_movement.z, position.z + p.z) - } else { - physics.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 += 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, - }; - entity::set_rotation(physics.into_inner(), 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 - .set_entity_pos( - player_entity_id, - new_pos, - position.into_inner(), - physics.into_inner(), - ) - .expect("The player entity should always exist"); - - (new_pos, y_rot, x_rot) + 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) }; - 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?; + 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(physics.into_inner(), 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 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 - ); + + let mut local_player = ecs + .query::<&mut LocalPlayer>() + .get_mut(ecs, player_entity) + .unwrap(); + + 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(), + }; + local_player + .players + .insert(updated_info.profile.uuid, player_info.clone()); + local_player.tx.send(Event::AddPlayer(player_info)); + } 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(); + } + let info = info.clone(); + local_player.tx.send(Event::UpdatePlayer(info)); + } 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)); - } + let mut local_player = ecs + .query::<&mut LocalPlayer>() + .get_mut(ecs, player_entity) + .unwrap(); + + for uuid in &p.profile_ids { + if let Some(info) = local_player.players.remove(uuid) { + local_player.tx.send(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)); + + let mut local_player = ecs + .query::<&mut LocalPlayer>() + .get(ecs, 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 local_player = ecs + .query::<&mut LocalPlayer>() + .get(ecs, player_entity) + .unwrap(); + let world = local_player.world.read(); + let partial_world = local_player.partial_world.read(); + // 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().chunks.limited_get(&pos).is_some(); if - shared_has_chunk && !this_client_has_chunk { trace!( - "Skipping parsing chunk {:?} because we already know - about it", pos + let shared_has_chunk = world.chunks.get(&pos).is_some(); + let this_client_has_chunk = partial_world.chunks.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(()); + return; } - // 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)) { + // ok we're sure we're going to mutate the world, so get exclusive write access + let mut partial_world = local_player.partial_world.write(); + let mut world = local_player.world.write(); + + 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); } } @@ -374,27 +320,36 @@ pub fn handle_packet(ecs: &mut bevy_ecs::world::World, entity: Entity, packet: & } ClientboundGamePacket::AddEntity(p) => { debug!("Got add entity packet {:?}", p); - let bundle = p.as_entity_bundle(); - let mut world = client.world.write(); - world.add_entity(EntityId(p.id), bundle); - // the bundle doesn't include the default entity metadata so we - add that // separately - let mut entities = world.entity_infos.shared.write(); - let mut entity = - entities.ecs_entity_mut(EntityId(p.id)).unwrap(); - p.apply_metadata(&mut entity); + + let local_player = ecs.query::<&LocalPlayer>().get(ecs, player_entity).unwrap(); + if let Some(world_name) = &local_player.world_name { + let bundle = p.as_entity_bundle(world_name.clone()); + let mut entity_mut = ecs.spawn((MinecraftEntityId(p.id), bundle)); + // the bundle doesn't include the default entity metadata so we add that + // separately + p.apply_metadata(&mut entity_mut); + } else { + warn!("got add player packet but we haven't gotten a login packet yet"); + } } ClientboundGamePacket::SetEntityData(p) => { debug!("Got set entity data packet {:?}", p); - let world = client.world.write(); - let mut entities = world.entity_infos.shared.write(); - let entity = entities.ecs_entity_mut(EntityId(p.id)); - if let Some(mut entity) = entity { - entity::metadata::apply_metadata(&mut entity, - p.packed_items.0.clone()); } else { - // warn!("Server sent an entity data packet for an - // entity id ({}) that we don't - // know about", p.id); + + let local_player = ecs + .query::<&mut LocalPlayer>() + .get(ecs, player_entity) + .unwrap(); + let partial_world = local_player.partial_world.write(); + let entity = partial_world + .entity_infos + .get_by_id(MinecraftEntityId(p.id)); + drop(partial_world); + + if let Some(entity) = entity { + let mut entity_mut = ecs.entity_mut(entity); + apply_metadata(&mut entity_mut, (*p.packed_items).clone()); + } else { + warn!("Server sent an entity data packet for an entity id ({}) that we don't know about", p.id); } } ClientboundGamePacket::UpdateAttributes(_p) => { @@ -408,11 +363,17 @@ pub fn handle_packet(ecs: &mut bevy_ecs::world::World, entity: Entity, packet: & } ClientboundGamePacket::AddPlayer(p) => { debug!("Got add player packet {:?}", p); - let bundle = p.as_player_bundle(); - let mut world = client.world.write(); - world.add_entity(EntityId(p.id), bundle); - // the default metadata was already included in the bundle - // for us + + let local_player = ecs + .query::<&mut LocalPlayer>() + .get(ecs, player_entity) + .unwrap(); + if let Some(world_name) = &local_player.world_name { + let bundle = p.as_player_bundle(world_name.clone()); + ecs.spawn((MinecraftEntityId(p.id), bundle)); + } else { + warn!("got add player packet but we haven't gotten a login packet yet"); + } } ClientboundGamePacket::InitializeBorder(p) => { debug!("Got initialize border packet {:?}", p); @@ -428,31 +389,35 @@ pub fn handle_packet(ecs: &mut bevy_ecs::world::World, entity: Entity, packet: & } 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?; - } - } + + let mut health = ecs + .query::<&mut Health>() + .get_mut(ecs, player_entity) + .unwrap(); + **health = p.health; + + // the `Dead` component is added by `update_dead` 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 world = client.world.write(); - let (pos, physics) = self.query::<(&entity::Position, - &entity::Physics)>(); let _ = world.set_entity_pos( - EntityId(p.id), - Vec3 { - x: p.x, - y: p.y, - z: p.z, - }, - pos, - physics, - ); + let local_player = ecs + .query::<&mut LocalPlayer>() + .get(ecs, player_entity) + .unwrap(); + let partial_world = local_player.partial_world.read(); + let partial_entity_infos = &partial_world.entity_infos; + let entity = partial_entity_infos.get_by_id(MinecraftEntityId(p.id)); + drop(partial_world); + + if let Some(entity) = entity { + let mut position = ecs.query::<&mut Position>().get_mut(ecs, entity).unwrap(); + **position = p.position; + } else { + warn!("Got teleport entity packet for unknown entity id {}", p.id); + } } ClientboundGamePacket::UpdateAdvancements(p) => { debug!("Got update advancements packet {:?}", p); @@ -461,39 +426,84 @@ pub fn handle_packet(ecs: &mut bevy_ecs::world::World, entity: Entity, packet: & // debug!("Got rotate head packet {:?}", p); } ClientboundGamePacket::MoveEntityPos(p) => { - let mut local_player = ecs.query::<&mut LocalPlayer>().get_mut(ecs, entity).unwrap(); - let mut partial_entity_infos = local_player.partial_world.write().entity_infos; - let entity = partial_entity_infos.entity_by_id(p.entity_id); - let mut position = ecs.query::<&mut Position>().get_mut(ecs, entity).unwrap(); - **position = position.with_delta(&p.delta); - }, + let local_player = ecs + .query::<&mut LocalPlayer>() + .get(ecs, player_entity) + .unwrap(); + let partial_world = local_player.partial_world.read(); + let partial_entity_infos = &partial_world.entity_infos; + let entity = partial_entity_infos.get_by_id(MinecraftEntityId(p.entity_id)); + drop(partial_world); + + if let Some(entity) = entity { + let mut position = ecs.query::<&mut Position>().get_mut(ecs, entity).unwrap(); + **position = position.with_delta(&p.delta); + } else { + warn!( + "Got move entity pos packet for unknown entity id {}", + p.entity_id + ); + } + } ClientboundGamePacket::MoveEntityPosRot(p) => { - let entity_infos = ecs.resource::(); - let entity = entity_infos.entity_by_id(p.entity_id); - let mut position = ecs.query::<&mut Position>().get_mut(ecs, entity).unwrap(); - **position += p.delta; + let local_player = ecs + .query::<&mut LocalPlayer>() + .get(ecs, player_entity) + .unwrap(); + let partial_world = local_player.partial_world.read(); + let partial_entity_infos = &partial_world.entity_infos; + let entity = partial_entity_infos.get_by_id(MinecraftEntityId(p.entity_id)); + drop(partial_world); + + if let Some(entity) = entity { + let mut position = ecs.query::<&mut Position>().get_mut(ecs, entity).unwrap(); + **position = position.with_delta(&p.delta); + } else { + warn!( + "Got move entity pos rot packet for unknown entity id {}", + p.entity_id + ); + } } 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?; + + let mut local_player = ecs + .query::<&mut LocalPlayer>() + .get_mut(ecs, player_entity) + .unwrap(); + + local_player.write_packet(ServerboundKeepAlivePacket { id: p.id }.get()); } 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?; + + let mut local_player = ecs + .query::<&mut LocalPlayer>() + .get(ecs, player_entity) + .unwrap(); + + local_player + .tx + .send(Event::Chat(ChatPacket::Player(Arc::new(p.clone())))); } ClientboundGamePacket::SystemChat(p) => { debug!("Got system chat packet {:?}", p); - tx.send(Event::Chat(ChatPacket::System(Arc::new(p.clone())))) - .await?; + + let mut local_player = ecs + .query::<&mut LocalPlayer>() + .get(ecs, player_entity) + .unwrap(); + + local_player + .tx + .send(Event::Chat(ChatPacket::System(Arc::new(p.clone())))); } ClientboundGamePacket::Sound(_p) => { // debug!("Got sound packet {:?}", p); @@ -503,18 +513,31 @@ pub fn handle_packet(ecs: &mut bevy_ecs::world::World, entity: Entity, packet: & } ClientboundGamePacket::BlockUpdate(p) => { debug!("Got block update packet {:?}", p); - let mut world = client.world.write(); - world.set_block_state(&p.pos, p.block_state); + + let mut local_player = ecs + .query::<&mut LocalPlayer>() + .get(ecs, player_entity) + .unwrap(); + let mut 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 world = client.world.write(); + let mut local_player = ecs + .query::<&mut LocalPlayer>() + .get(ecs, player_entity) + .unwrap(); + let mut world = local_player.world.write(); + for state in &p.states { - world.set_block_state(&(p.section_pos + state.pos.clone()), - state.state); } + world + .chunks + .set_block_state(&(p.section_pos + state.pos.clone()), state.state); + } } ClientboundGamePacket::GameEvent(p) => { debug!("Got game event packet {:?}", p); @@ -559,11 +582,22 @@ pub fn handle_packet(ecs: &mut bevy_ecs::world::World, entity: Entity, packet: & ClientboundGamePacket::PlayerCombatEnter(_) => {} ClientboundGamePacket::PlayerCombatKill(p) => { debug!("Got player kill packet {:?}", p); - let (entity_id, mut dead, mut local_player) = ecs.query::<(&MinecraftEntityId, &mut Dead, &mut LocalPlayer)>().get(ecs, entity).unwrap(); - if **entity_id == p.player_id { - if !**dead { - **dead = true; - local_player.tx.send(Event::Death(Some(Arc::new(p.clone())))); + let (&entity_id, dead) = ecs + .query::<(&MinecraftEntityId, Option<&Dead>)>() + .get(ecs, player_entity) + .unwrap(); + + if *entity_id == p.player_id { + if dead.is_none() { + ecs.entity_mut(player_entity).insert(Dead); + + let local_player = ecs + .query::<&mut LocalPlayer>() + .get_mut(ecs, player_entity) + .unwrap(); + local_player + .tx + .send(Event::Death(Some(Arc::new(p.clone())))); } } } @@ -572,9 +606,8 @@ pub fn handle_packet(ecs: &mut bevy_ecs::world::World, entity: Entity, packet: & ClientboundGamePacket::ResourcePack(_) => {} ClientboundGamePacket::Respawn(p) => { debug!("Got respawn packet {:?}", p); - // Sets clients dead state to false. - let mut dead = ecs.query::<&mut Dead>().get(ecs, entity).unwrap(); - **dead = false; + // Remove the Dead marker component from the player. + ecs.entity_mut(player_entity).remove::(); } ClientboundGamePacket::SelectAdvancementsTab(_) => {} ClientboundGamePacket::SetActionBarText(_) => {} @@ -604,8 +637,132 @@ pub fn handle_packet(ecs: &mut bevy_ecs::world::World, entity: Entity, packet: & } } +// this is probably inefficient since it has to iterate over every packet even +// if it's not the right one +fn handle_login_packet( + mut commands: Commands, + mut query: Query<(Entity, &PacketReceiver, &mut LocalPlayer), Changed>, + mut world_container: ResMut, + mut entity_infos: ResMut, +) { + for (player_entity, packet_events, mut local_player) in query.iter_mut() { + for packet in packet_events.packets.lock().iter() { + if let ClientboundGamePacket::Login(p) = packet { + debug!("Got login packet"); + + { + // 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) + let mut partial_world_lock = local_player.partial_world.write(); + + *partial_world_lock = 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), + &mut entity_infos, + ); + + let player_bundle = PlayerBundle { + entity: EntityBundle::new( + local_player.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(), + ); + + local_player.tx.send(Event::Login); + } + } + } +} + /// A system that clears all packets in the clientbound packet events. -pub fn clear_packets(mut query: Query<&mut PacketReceiver>) { +fn clear_packets(mut query: Query<&mut PacketReceiver>) { for packets in query.iter_mut() { packets.packets.lock().clear(); } diff --git a/azalea-physics/src/collision/mod.rs b/azalea-physics/src/collision/mod.rs index 64f24b95..7d934020 100644 --- a/azalea-physics/src/collision/mod.rs +++ b/azalea-physics/src/collision/mod.rs @@ -125,7 +125,7 @@ pub fn move_colliding( // TODO: minecraft checks for a "minor" horizontal collision here - let _block_pos_below = entity::on_pos_legacy(&world.chunks, position, physics); + let _block_pos_below = entity::on_pos_legacy(&world.chunks, position); // let _block_state_below = self // .world // .get_block_state(&block_pos_below) diff --git a/azalea-protocol/src/packets/game/clientbound_add_entity_packet.rs b/azalea-protocol/src/packets/game/clientbound_add_entity_packet.rs index 4069077e..64875741 100755 --- a/azalea-protocol/src/packets/game/clientbound_add_entity_packet.rs +++ b/azalea-protocol/src/packets/game/clientbound_add_entity_packet.rs @@ -1,10 +1,7 @@ use azalea_buf::McBuf; use azalea_core::{ResourceLocation, Vec3}; use azalea_protocol_macros::ClientboundGamePacket; -use azalea_world::entity::{ - metadata::{apply_default_metadata, PlayerMetadataBundle, UpdateMetadataError}, - EntityBundle, -}; +use azalea_world::entity::{metadata::apply_default_metadata, EntityBundle}; use uuid::Uuid; #[derive(Clone, Debug, McBuf, ClientboundGamePacket)] @@ -48,6 +45,7 @@ impl ClientboundAddEntityPacket { EntityBundle::new(self.uuid, self.position, self.entity_type, world_name) } + /// Apply the default metadata for the given entity. pub fn apply_metadata(&self, entity: &mut bevy_ecs::world::EntityMut) { apply_default_metadata(entity, self.entity_type); } diff --git a/azalea-protocol/src/packets/game/clientbound_add_player_packet.rs b/azalea-protocol/src/packets/game/clientbound_add_player_packet.rs index 4cbeb1b9..302b96d3 100755 --- a/azalea-protocol/src/packets/game/clientbound_add_player_packet.rs +++ b/azalea-protocol/src/packets/game/clientbound_add_player_packet.rs @@ -2,7 +2,7 @@ use azalea_buf::McBuf; use azalea_core::{ResourceLocation, Vec3}; use azalea_protocol_macros::ClientboundGamePacket; use azalea_registry::EntityKind; -use azalea_world::entity::{metadata::PlayerMetadataBundle, EntityBundle, PlayerBundle}; +use azalea_world::entity::{metadata::PlayerMetadataBundle, Dead, EntityBundle, PlayerBundle}; use uuid::Uuid; /// This packet is sent by the server when a player comes into visible range, diff --git a/azalea-protocol/src/packets/game/clientbound_teleport_entity_packet.rs b/azalea-protocol/src/packets/game/clientbound_teleport_entity_packet.rs index 05a912ba..eceaa3aa 100755 --- a/azalea-protocol/src/packets/game/clientbound_teleport_entity_packet.rs +++ b/azalea-protocol/src/packets/game/clientbound_teleport_entity_packet.rs @@ -1,13 +1,12 @@ use azalea_buf::McBuf; +use azalea_core::Vec3; use azalea_protocol_macros::ClientboundGamePacket; #[derive(Clone, Debug, McBuf, ClientboundGamePacket)] pub struct ClientboundTeleportEntityPacket { #[var] pub id: u32, - pub x: f64, - pub y: f64, - pub z: f64, + pub position: Vec3, pub y_rot: i8, pub x_rot: i8, pub on_ground: bool, diff --git a/azalea-world/src/chunk_storage.rs b/azalea-world/src/chunk_storage.rs index ae5ba75b..a5a24e1f 100755 --- a/azalea-world/src/chunk_storage.rs +++ b/azalea-world/src/chunk_storage.rs @@ -17,6 +17,8 @@ const SECTION_HEIGHT: u32 = 16; /// An efficient storage of chunks for a client that has a limited render /// distance. This has support for using a shared [`ChunkStorage`]. pub struct PartialChunkStorage { + /// The center of the view, i.e. the chunk the player is currently in. You + /// can safely modify this. pub view_center: ChunkPos, chunk_radius: u32, view_range: u32, @@ -135,7 +137,7 @@ impl PartialChunkStorage { } /// Get a [`Chunk`] within render distance, or `None` if it's not loaded. - /// Use [`PartialChunkStorage::get`] to get a chunk from the shared storage. + /// Use [`ChunkStorage::get`] to get a chunk from the shared storage. pub fn limited_get(&self, pos: &ChunkPos) -> Option<&Arc>> { if !self.in_range(pos) { warn!( @@ -149,7 +151,7 @@ impl PartialChunkStorage { self.chunks[index].as_ref() } /// Get a mutable reference to a [`Chunk`] within render distance, or - /// `None` if it's not loaded. Use [`PartialChunkStorage::get`] to get + /// `None` if it's not loaded. Use [`ChunkStorage::get`] to get /// a chunk from the shared storage. pub fn limited_get_mut(&mut self, pos: &ChunkPos) -> Option<&mut Option>>> { if !self.in_range(pos) { diff --git a/azalea-world/src/entity/data.rs b/azalea-world/src/entity/data.rs index e22f094e..f5af8a5e 100755 --- a/azalea-world/src/entity/data.rs +++ b/azalea-world/src/entity/data.rs @@ -6,13 +6,14 @@ use azalea_buf::{McBuf, McBufReadable, McBufWritable}; use azalea_chat::FormattedText; use azalea_core::{BlockPos, Direction, GlobalPos, Particle, Slot}; use bevy_ecs::component::Component; +use derive_more::Deref; use enum_as_inner::EnumAsInner; use nohash_hasher::IntSet; use std::io::{Cursor, Write}; use uuid::Uuid; -#[derive(Clone, Debug)] -pub struct EntityMetadataItems(pub Vec); +#[derive(Clone, Debug, Deref)] +pub struct EntityMetadataItems(Vec); #[derive(Clone, Debug)] pub struct EntityDataItem { diff --git a/azalea-world/src/entity/dimensions.rs b/azalea-world/src/entity/dimensions.rs index 1d013d10..9a886670 100755 --- a/azalea-world/src/entity/dimensions.rs +++ b/azalea-world/src/entity/dimensions.rs @@ -1,4 +1,7 @@ use azalea_core::{Vec3, AABB}; +use bevy_ecs::{query::Changed, system::Query}; + +use super::{Physics, Position}; #[derive(Debug, Default)] pub struct EntityDimensions { @@ -21,3 +24,19 @@ impl EntityDimensions { } } } + +/// Sets the position of the entity. This doesn't update the cache in +/// azalea-world, and should only be used within azalea-world! +/// +/// # Safety +/// Cached position in the world must be updated. +pub fn update_bounding_box(mut query: Query<(&Position, &mut Physics), Changed>) { + for (position, mut physics) in query.iter_mut() { + let bounding_box = physics.dimensions.make_bounding_box(&position); + physics.bounding_box = bounding_box; + } +} + +pub fn make_bounding_box(pos: &Position, physics: &Physics) -> AABB { + physics.dimensions.make_bounding_box(&pos) +} diff --git a/azalea-world/src/entity/mod.rs b/azalea-world/src/entity/mod.rs index 813f5139..7ec14613 100644 --- a/azalea-world/src/entity/mod.rs +++ b/azalea-world/src/entity/mod.rs @@ -5,14 +5,22 @@ pub mod metadata; use crate::ChunkStorage; -use self::{attributes::AttributeInstance, metadata::UpdateMetadataError}; +use self::{ + attributes::AttributeInstance, + metadata::{Health, UpdateMetadataError}, +}; pub use attributes::Attributes; use azalea_block::BlockState; use azalea_core::{BlockPos, ChunkPos, ResourceLocation, Vec3, AABB}; -use bevy_ecs::{bundle::Bundle, component::Component, query::Changed, system::Query}; +use bevy_ecs::{ + bundle::Bundle, + component::Component, + query::{Changed, Without}, + system::{Commands, Query}, +}; pub use data::*; use derive_more::{Deref, DerefMut}; -pub use dimensions::EntityDimensions; +pub use dimensions::{update_bounding_box, EntityDimensions}; use std::fmt::Debug; use uuid::Uuid; @@ -36,18 +44,6 @@ impl std::hash::Hash for MinecraftEntityId { } impl nohash_hasher::IsEnabled for MinecraftEntityId {} -/// Sets the position of the entity. This doesn't update the cache in -/// azalea-world, and should only be used within azalea-world! -/// -/// # Safety -/// Cached position in the world must be updated. -pub fn update_bounding_box(mut query: Query<(&Position, &mut Physics), Changed>) { - for (position, mut physics) in query.iter_mut() { - let bounding_box = physics.dimensions.make_bounding_box(&position); - physics.bounding_box = bounding_box; - } -} - pub fn set_rotation(physics: &mut Physics, y_rot: f32, x_rot: f32) { physics.y_rot = y_rot % 360.0; physics.x_rot = x_rot.clamp(-90.0, 90.0) % 360.0; @@ -80,27 +76,9 @@ pub fn input_vector(physics: &mut Physics, speed: f32, acceleration: &Vec3) -> V } } -/// Apply the given metadata items to the entity. Everything that isn't -/// included in items will be left unchanged. -pub fn apply_metadata( - ecs: bevy_ecs::world::World, - entity: &mut bevy_ecs::world::EntityMut, - items: Vec, -) -> Result<(), UpdateMetadataError> { - metadata::apply_metadata(entity, items) -} - -pub fn make_bounding_box(pos: &Position, physics: &Physics) -> AABB { - physics.dimensions.make_bounding_box(&pos) -} - /// Get the position of the block below the entity, but a little lower. -pub fn on_pos_legacy( - chunk_storage: &ChunkStorage, - position: &Position, - physics: &Physics, -) -> BlockPos { - on_pos(0.2, chunk_storage, position, physics) +pub fn on_pos_legacy(chunk_storage: &ChunkStorage, position: &Position) -> BlockPos { + on_pos(0.2, chunk_storage, position) } // int x = Mth.floor(this.position.x); @@ -115,12 +93,7 @@ pub fn on_pos_legacy( // } // } // return var5; -pub fn on_pos( - offset: f32, - chunk_storage: &ChunkStorage, - pos: &Position, - physics: &Physics, -) -> BlockPos { +pub fn on_pos(offset: f32, chunk_storage: &ChunkStorage, pos: &Position) -> BlockPos { let x = pos.x.floor() as i32; let y = (pos.y - offset as f64).floor() as i32; let z = pos.z.floor() as i32; @@ -145,6 +118,8 @@ pub fn on_pos( pos } +/// The Minecraft UUID of the entity. For players, this is their actual player +/// UUID, and for other entities it's just random. #[derive(Component, Deref, DerefMut, Clone, Copy)] pub struct EntityUuid(Uuid); @@ -239,6 +214,27 @@ pub struct Physics { pub has_impulse: bool, } +/// Marker component for entities that are dead. +/// +/// "Dead" means that the entity has 0 health. +#[derive(Component, Copy, Clone, Default)] +pub struct Dead; + +/// System that adds the [`Dead`] marker component if an entity's health is set +/// to 0 (or less than 0). This will be present if an entity is doing the death +/// animation. +/// +/// Entities that are dead can not be revived. +/// TODO: fact check this in-game by setting an entity's health to 0 and then +/// not 0 +pub fn add_dead(mut commands: Commands, query: Query<(Entity, &Health), Changed>) { + for (entity, health) in query.iter() { + if **health <= 0.0 { + commands.entity(entity).insert(Dead); + } + } +} + /// A component NewType for [`azalea_registry::EntityKind`]. /// /// Most of the time, you should be using `azalea_registry::EntityKind` @@ -266,6 +262,7 @@ impl EntityBundle { kind: azalea_registry::EntityKind, world_name: ResourceLocation, ) -> Self { + // TODO: get correct entity dimensions by having them codegened somewhere let dimensions = EntityDimensions { width: 0.6, height: 1.8, diff --git a/azalea-world/src/entity_info.rs b/azalea-world/src/entity_info.rs index ad548345..75db08d8 100644 --- a/azalea-world/src/entity_info.rs +++ b/azalea-world/src/entity_info.rs @@ -1,5 +1,5 @@ use crate::{ - entity::{self, update_bounding_box, Entity, MinecraftEntityId}, + entity::{self, add_dead, update_bounding_box, Entity, MinecraftEntityId}, MaybeRemovedEntity, World, WorldContainer, }; use azalea_core::ChunkPos; @@ -18,12 +18,15 @@ use uuid::Uuid; pub struct EntityPlugin; impl Plugin for EntityPlugin { fn build(&self, app: &mut App) { - app.add_system_set_to_stage( - CoreStage::PostUpdate, + // Since it's PostUpdate, these will run after every tick or packet + app.add_system_set( SystemSet::new() + .after("tick") + .after("packet") .with_system(update_entity_chunk_positions) .with_system(remove_despawned_entities_from_indexes) - .with_system(update_bounding_box), + .with_system(update_bounding_box) + .with_system(add_dead), ); } }