1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 14:26:04 +00:00

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
This commit is contained in:
mat 2023-01-08 23:30:09 -06:00
parent 109418de21
commit a928d83ade
12 changed files with 640 additions and 448 deletions

View file

@ -1,6 +1,6 @@
pub use crate::chat::ChatPacket; pub use crate::chat::ChatPacket;
use crate::{ 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}, movement::{local_player_ai_step, send_position},
packet_handling, packet_handling,
plugins::PluginStates, plugins::PluginStates,
@ -93,10 +93,6 @@ pub struct Client {
pub profile: GameProfile, pub profile: GameProfile,
/// The entity for this client in the ECS. /// The entity for this client in the ECS.
pub entity: Entity, 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<RwLock<WorldContainer>>,
/// The world that this client is in. /// The world that this client is in.
pub world: Arc<RwLock<PartialWorld>>, pub world: Arc<RwLock<PartialWorld>>,
@ -104,7 +100,6 @@ pub struct Client {
/// client and keep state. If you're not making a plugin and you're using /// client and keep state. If you're not making a plugin and you're using
/// the `azalea` crate. you can ignore this field. /// the `azalea` crate. you can ignore this field.
pub plugins: Arc<PluginStates>, pub plugins: Arc<PluginStates>,
tasks: Arc<Mutex<Vec<JoinHandle<()>>>>,
/// The entity component system. You probably don't need to access this /// 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 /// 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`]. /// defaults, otherwise use [`Client::join`].
pub fn new( pub fn new(
profile: GameProfile, profile: GameProfile,
world_container: Option<Arc<RwLock<WorldContainer>>>,
entity: Entity, entity: Entity,
ecs: Arc<Mutex<bevy_ecs::world::World>>, ecs: Arc<Mutex<bevy_ecs::world::World>>,
) -> Self { ) -> Self {
@ -149,12 +143,9 @@ impl Client {
// default our id to 0, it'll be set later // default our id to 0, it'll be set later
entity, entity,
world: Arc::new(RwLock::new(PartialWorld::default())), 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 // 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(). // field right after this. No Mutex so the user doesn't need to .lock().
plugins: Arc::new(PluginStates::default()), plugins: Arc::new(PluginStates::default()),
tasks: Arc::new(Mutex::new(Vec::new())),
ecs, ecs,
} }
@ -209,13 +200,13 @@ impl Client {
let entity = entity_mut.id(); let entity = entity_mut.id();
// we got the GameConnection, so the server is now connected :) // 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 world = client.world();
let (packet_writer_sender, packet_writer_receiver) = mpsc::unbounded_channel(); 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, entity,
game_profile, game_profile,
packet_writer_sender, packet_writer_sender,
@ -236,8 +227,8 @@ impl Client {
.clone() .clone()
.write_task(write_conn, packet_writer_receiver), .write_task(write_conn, packet_writer_receiver),
); );
client.tasks.lock().push(read_packets_task); local_player.tasks.push(read_packets_task);
client.tasks.lock().push(write_packets_task); local_player.tasks.push(write_packets_task);
ecs.entity_mut(entity) ecs.entity_mut(entity)
.insert((local_player, packet_receiver)); .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 /// The OwnedReadHalf for the TCP connection is in one of the tasks, so it
/// automatically closes the connection when that's dropped. /// automatically closes the connection when that's dropped.
pub async fn disconnect(&self) -> Result<(), std::io::Error> { pub fn disconnect(&self) {
let tasks = self.tasks.lock(); self.local_player_mut(&mut self.ecs.lock()).disconnect();
for task in tasks.iter() {
task.abort();
}
Ok(())
} }
pub fn local_player<'a>(&'a self, ecs: &'a mut bevy_ecs::world::World) -> &'a LocalPlayer { 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. /// superset of the client's world.
pub fn world(&self) -> Arc<RwLock<World>> { pub fn world(&self) -> Arc<RwLock<World>> {
let mut ecs = self.ecs.lock(); let mut ecs = self.ecs.lock();
let world_name = self
.local_player(&mut ecs) let world_name = {
let local_player = self.local_player(&mut ecs);
local_player
.world_name .world_name
.as_ref() .as_ref()
.expect("World name must be known if we're doing Client::world"); .expect("World name must be known if we're doing Client::world")
let world_container = self.world_container.read(); .clone()
};
let world_container = ecs.resource::<WorldContainer>();
world_container.get(&world_name).unwrap() world_container.get(&world_name).unwrap()
} }
@ -491,10 +483,12 @@ pub async fn start_ecs(
.with_system(update_in_loaded_chunk) .with_system(update_in_loaded_chunk)
.with_system(local_player_ai_step) .with_system(local_player_ai_step)
.with_system(send_tick_event), .with_system(send_tick_event),
) );
.add_system(packet_handling::handle_packets.label("handle_packets"))
// should happen last // fire the Death event when the player dies.
.add_system(packet_handling::clear_packets.after("handle_packets")); app.add_system(death_event.after("tick").after("packet"));
app.init_resource::<WorldContainer>();
// all resources should have been added by now so we can take the ecs from the // all resources should have been added by now so we can take the ecs from the
// app // app

View file

@ -9,14 +9,14 @@ use azalea_auth::game_profile::GameProfile;
use azalea_core::{ChunkPos, ResourceLocation}; use azalea_core::{ChunkPos, ResourceLocation};
use azalea_protocol::{connect::WriteConnection, packets::game::ServerboundGamePacket}; use azalea_protocol::{connect::WriteConnection, packets::game::ServerboundGamePacket};
use azalea_world::{ use azalea_world::{
entity::{self, Entity}, entity::{self, Dead, Entity},
EntityInfos, PartialWorld, World, 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 derive_more::{Deref, DerefMut};
use parking_lot::RwLock; use parking_lot::{Mutex, RwLock};
use thiserror::Error; use thiserror::Error;
use tokio::sync::mpsc; use tokio::{sync::mpsc, task::JoinHandle};
use uuid::Uuid; use uuid::Uuid;
use crate::{ClientInformation, Event, PlayerInfo, WalkDirection}; 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 /// A map of player uuids to their information in the tab list
pub players: HashMap<Uuid, PlayerInfo>, pub players: HashMap<Uuid, PlayerInfo>,
/// The partial world is the world this client currently has loaded. It has
/// a limited render distance.
pub partial_world: Arc<RwLock<PartialWorld>>, pub partial_world: Arc<RwLock<PartialWorld>>,
/// The world is the combined [`PartialWorld`]s of all clients in the same
/// world. (Only relevant if you're using a shared world, i.e. a swarm)
pub world: Arc<RwLock<World>>, pub world: Arc<RwLock<World>>,
pub world_name: Option<ResourceLocation>, pub world_name: Option<ResourceLocation>,
pub tx: mpsc::UnboundedSender<Event>, pub tx: mpsc::UnboundedSender<Event>,
}
/// Present if the player can be dead. /// A list of async tasks that are running and will stop running when this
#[derive(Component, Copy, Clone, Default, Deref, DerefMut)] /// LocalPlayer is dropped or disconnected with [`Self::disconnect`]
pub struct Dead(bool); pub(crate) tasks: Vec<JoinHandle<()>>,
}
#[derive(Default)] #[derive(Default)]
pub struct PhysicsState { pub struct PhysicsState {
@ -87,7 +91,6 @@ impl LocalPlayer {
physics_state: PhysicsState::default(), physics_state: PhysicsState::default(),
client_information: ClientInformation::default(), client_information: ClientInformation::default(),
dead: false,
players: HashMap::new(), players: HashMap::new(),
world, world,
@ -99,6 +102,8 @@ impl LocalPlayer {
world_name: None, world_name: None,
tx, tx,
tasks: Vec::new(),
} }
} }
@ -108,6 +113,16 @@ impl LocalPlayer {
.send(packet) .send(packet)
.expect("write_packet shouldn't be able to be called if the connection is closed"); .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>) { 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<Dead>>) {
for local_player in &query {
local_player.tx.send(Event::Death(None));
}
}
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum HandlePacketError { pub enum HandlePacketError {
#[error("{0}")] #[error("{0}")]

View file

@ -1,16 +1,54 @@
use std::sync::Arc; use std::{io::Cursor, sync::Arc};
use azalea_core::{ChunkPos, ResourceLocation, Vec3};
use azalea_protocol::{ use azalea_protocol::{
connect::{ReadConnection, WriteConnection}, 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 azalea_world::{
use bevy_ecs::{component::Component, prelude::Entity, query::Changed, system::Query}; entity::{
use log::{error, debug}; 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 parking_lot::Mutex;
use tokio::sync::mpsc; 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. /// Something that receives packets from the server.
#[derive(Component, Clone)] #[derive(Component, Clone)]
@ -19,135 +57,34 @@ pub struct PacketReceiver {
pub run_schedule_sender: mpsc::UnboundedSender<()>, pub run_schedule_sender: mpsc::UnboundedSender<()>,
} }
pub fn handle_packets( fn handle_packets(ecs: &mut bevy_ecs::world::World) {
ecs: &mut bevy_ecs::world::World,
// ecs: &mut bevy_ecs::world::World,
) {
let mut query = ecs.query_filtered::<(Entity, &PacketReceiver), Changed<PacketReceiver>>(); let mut query = ecs.query_filtered::<(Entity, &PacketReceiver), Changed<PacketReceiver>>();
for (entity, packet_events) in query.iter_mut(ecs) { let mut players_and_packets = Vec::new();
for packet in packet_events.packets.lock().iter() { for (player_entity, packet_events) in query.iter(ecs) {
handle_packet(ecs, entity, packet); 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 { match packet {
ClientboundGamePacket::Login(p) => { ClientboundGamePacket::Login(p) => {
debug!("Got login packet"); // handled by the handle_login_packet system
{
// // 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?;
} }
ClientboundGamePacket::SetChunkCacheRadius(p) => { ClientboundGamePacket::SetChunkCacheRadius(p) => {
debug!("Got set chunk cache radius packet {:?}", 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) => { ClientboundGamePacket::Disconnect(p) => {
debug!("Got disconnect packet {:?}", 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) => { ClientboundGamePacket::UpdateRecipes(_p) => {
debug!("Got update recipes packet"); debug!("Got update recipes packet");
@ -187,13 +130,15 @@ pub fn handle_packet(ecs: &mut bevy_ecs::world::World, entity: Entity, packet: &
// TODO: reply with teleport confirm // TODO: reply with teleport confirm
debug!("Got player position packet {:?}", p); debug!("Got player position packet {:?}", p);
let (new_pos, y_rot, x_rot) = { let (mut local_player, mut physics, mut position, mut last_sent_position) = ecs
let player_entity_id = *client.entity(); .query::<(
let world = client.world(); &mut LocalPlayer,
// let mut player_entity = &mut Physics,
world.entity_mut(player_entity_id).unwrap(); let (mut &mut Position,
physics, position) = client.query::<(&mut &mut LastSentPosition,
entity::Physics, &mut entity::Position)>(); )>()
.get_mut(ecs, player_entity)
.unwrap();
let delta_movement = physics.delta; let delta_movement = physics.delta;
@ -202,24 +147,24 @@ pub fn handle_packet(ecs: &mut bevy_ecs::world::World, entity: Entity, packet: &
let is_z_relative = p.relative_arguments.z; let is_z_relative = p.relative_arguments.z;
let (delta_x, new_pos_x) = if is_x_relative { let (delta_x, new_pos_x) = if is_x_relative {
physics.last_pos.x += p.x; last_sent_position.x += p.x;
(delta_movement.x, position.x + p.x) (delta_movement.x, position.x + p.x)
} else { } else {
physics.last_pos.x = p.x; last_sent_position.x = p.x;
(0.0, p.x) (0.0, p.x)
}; };
let (delta_y, new_pos_y) = if is_y_relative { let (delta_y, new_pos_y) = if is_y_relative {
physics.last_pos.y += p.y; last_sent_position.y += p.y;
(delta_movement.y, position.y + p.y) (delta_movement.y, position.y + p.y)
} else { } else {
physics.last_pos.y = p.y; last_sent_position.y = p.y;
(0.0, p.y) (0.0, p.y)
}; };
let (delta_z, new_pos_z) = if is_z_relative { let (delta_z, new_pos_z) = if is_z_relative {
physics.last_pos.z += p.z; last_sent_position.z += p.z;
(delta_movement.z, position.z + p.z) (delta_movement.z, position.z + p.z)
} else { } else {
physics.last_pos.z = p.z; last_sent_position.z = p.z;
(0.0, p.z) (0.0, p.z)
}; };
@ -237,31 +182,21 @@ pub fn handle_packet(ecs: &mut bevy_ecs::world::World, entity: Entity, packet: &
y: delta_y, y: delta_y,
z: delta_z, z: delta_z,
}; };
entity::set_rotation(physics.into_inner(), y_rot, x_rot); // we call a function instead of setting the fields ourself since the function
// TODO: minecraft sets "xo", "yo", and "zo" here but idk // makes sure the rotations stay in their ranges
what that means // so investigate that ig 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 { let new_pos = Vec3 {
x: new_pos_x, x: new_pos_x,
y: new_pos_y, y: new_pos_y,
z: new_pos_z, 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) **position = new_pos;
};
client local_player.write_packet(ServerboundAcceptTeleportationPacket { id: p.id }.get());
.write_packet(ServerboundAcceptTeleportationPacket { id: p.id local_player.write_packet(
}.get()) .await?;
client
.write_packet(
ServerboundMovePlayerPosRotPacket { ServerboundMovePlayerPosRotPacket {
x: new_pos.x, x: new_pos.x,
y: new_pos.y, y: new_pos.y,
@ -272,14 +207,16 @@ pub fn handle_packet(ecs: &mut bevy_ecs::world::World, entity: Entity, packet: &
on_ground: false, on_ground: false,
} }
.get(), .get(),
) );
.await?;
} }
ClientboundGamePacket::PlayerInfoUpdate(p) => { ClientboundGamePacket::PlayerInfoUpdate(p) => {
debug!("Got player info packet {:?}", p); debug!("Got player info packet {:?}", p);
let mut events = Vec::new();
{ let mut local_player = ecs
let mut players_lock = client.players.write(); .query::<&mut LocalPlayer>()
.get_mut(ecs, player_entity)
.unwrap();
for updated_info in &p.entries { for updated_info in &p.entries {
// add the new player maybe // add the new player maybe
if p.actions.add_player { if p.actions.add_player {
@ -290,10 +227,12 @@ pub fn handle_packet(ecs: &mut bevy_ecs::world::World, entity: Entity, packet: &
latency: updated_info.latency, latency: updated_info.latency,
display_name: updated_info.display_name.clone(), display_name: updated_info.display_name.clone(),
}; };
players_lock.insert(updated_info.profile.uuid, local_player
player_info.clone()); .players
events.push(Event::AddPlayer(player_info)); } else if .insert(updated_info.profile.uuid, player_info.clone());
let Some(info) = players_lock.get_mut(&updated_info.profile.uuid) { 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 // `else if` because the block for add_player above
// already sets all the fields // already sets all the fields
if p.actions.update_game_mode { if p.actions.update_game_mode {
@ -303,69 +242,76 @@ pub fn handle_packet(ecs: &mut bevy_ecs::world::World, entity: Entity, packet: &
info.latency = updated_info.latency; info.latency = updated_info.latency;
} }
if p.actions.update_display_name { if p.actions.update_display_name {
info.display_name = info.display_name = updated_info.display_name.clone();
updated_info.display_name.clone(); } }
events.push(Event::UpdatePlayer(info.clone())); let info = info.clone();
local_player.tx.send(Event::UpdatePlayer(info));
} else { } else {
warn!( warn!(
"Ignoring PlayerInfoUpdate for unknown player "Ignoring PlayerInfoUpdate for unknown player {}",
{}", updated_info.profile.uuid updated_info.profile.uuid
); );
} }
} }
} }
for event in events {
tx.send(event).await?;
}
}
ClientboundGamePacket::PlayerInfoRemove(p) => { ClientboundGamePacket::PlayerInfoRemove(p) => {
let mut events = Vec::new(); let mut local_player = ecs
{ .query::<&mut LocalPlayer>()
let mut players_lock = client.players.write(); .get_mut(ecs, player_entity)
.unwrap();
for uuid in &p.profile_ids { for uuid in &p.profile_ids {
if let Some(info) = players_lock.remove(uuid) { if let Some(info) = local_player.players.remove(uuid) {
events.push(Event::RemovePlayer(info)); local_player.tx.send(Event::RemovePlayer(info));
} }
} }
} }
for event in events {
tx.send(event).await?;
}
}
ClientboundGamePacket::SetChunkCacheCenter(p) => { ClientboundGamePacket::SetChunkCacheCenter(p) => {
debug!("Got chunk cache center packet {:?}", p); debug!("Got chunk cache center packet {:?}", p);
client
.world let mut local_player = ecs
.write() .query::<&mut LocalPlayer>()
.update_view_center(&ChunkPos::new(p.x, p.z)); .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) => { ClientboundGamePacket::LevelChunkWithLight(p) => {
// debug!("Got chunk with light packet {} {}", p.x, p.z); // debug!("Got chunk with light packet {} {}", p.x, p.z);
let pos = ChunkPos::new(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 // OPTIMIZATION: if we already know about the chunk from the
// shared world (and not ourselves), then we don't need to // shared world (and not ourselves), then we don't need to
// parse it again. This is only used when we have a shared // parse it again. This is only used when we have a shared
// world, since we check that the chunk isn't currently owned // world, since we check that the chunk isn't currently owned
// by this client. // by this client.
let shared_has_chunk = let shared_has_chunk = world.chunks.get(&pos).is_some();
client.world.read().get_chunk(&pos).is_some(); let let this_client_has_chunk = partial_world.chunks.limited_get(&pos).is_some();
this_client_has_chunk = if shared_has_chunk && !this_client_has_chunk {
client.world.read().chunks.limited_get(&pos).is_some(); if trace!(
shared_has_chunk && !this_client_has_chunk { trace!( "Skipping parsing chunk {:?} because we already know about it",
"Skipping parsing chunk {:?} because we already know pos
about it", pos
); );
return Ok(()); return;
} }
// let chunk = Chunk::read_with_world_height(&mut p.chunk_data); // ok we're sure we're going to mutate the world, so get exclusive write access
// debug("chunk {:?}") let mut partial_world = local_player.partial_world.write();
if let Err(e) = client let mut world = local_player.world.write();
.world
.write() if let Err(e) = partial_world.chunks.replace_with_packet_data(
.replace_with_packet_data(&pos, &mut &pos,
Cursor::new(&p.chunk_data.data)) { &mut Cursor::new(&p.chunk_data.data),
&mut world.chunks,
) {
error!("Couldn't set chunk data: {}", e); 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) => { ClientboundGamePacket::AddEntity(p) => {
debug!("Got add entity packet {:?}", p); debug!("Got add entity packet {:?}", p);
let bundle = p.as_entity_bundle();
let mut world = client.world.write(); let local_player = ecs.query::<&LocalPlayer>().get(ecs, player_entity).unwrap();
world.add_entity(EntityId(p.id), bundle); if let Some(world_name) = &local_player.world_name {
// the bundle doesn't include the default entity metadata so we let bundle = p.as_entity_bundle(world_name.clone());
add that // separately let mut entity_mut = ecs.spawn((MinecraftEntityId(p.id), bundle));
let mut entities = world.entity_infos.shared.write(); // the bundle doesn't include the default entity metadata so we add that
let mut entity = // separately
entities.ecs_entity_mut(EntityId(p.id)).unwrap(); p.apply_metadata(&mut entity_mut);
p.apply_metadata(&mut entity); } else {
warn!("got add player packet but we haven't gotten a login packet yet");
}
} }
ClientboundGamePacket::SetEntityData(p) => { ClientboundGamePacket::SetEntityData(p) => {
debug!("Got set entity data packet {:?}", p); debug!("Got set entity data packet {:?}", p);
let world = client.world.write();
let mut entities = world.entity_infos.shared.write(); let local_player = ecs
let entity = entities.ecs_entity_mut(EntityId(p.id)); .query::<&mut LocalPlayer>()
if let Some(mut entity) = entity { .get(ecs, player_entity)
entity::metadata::apply_metadata(&mut entity, .unwrap();
p.packed_items.0.clone()); } else { let partial_world = local_player.partial_world.write();
// warn!("Server sent an entity data packet for an let entity = partial_world
// entity id ({}) that we don't .entity_infos
// know about", p.id); .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) => { ClientboundGamePacket::UpdateAttributes(_p) => {
@ -408,11 +363,17 @@ pub fn handle_packet(ecs: &mut bevy_ecs::world::World, entity: Entity, packet: &
} }
ClientboundGamePacket::AddPlayer(p) => { ClientboundGamePacket::AddPlayer(p) => {
debug!("Got add player packet {:?}", p); debug!("Got add player packet {:?}", p);
let bundle = p.as_player_bundle();
let mut world = client.world.write(); let local_player = ecs
world.add_entity(EntityId(p.id), bundle); .query::<&mut LocalPlayer>()
// the default metadata was already included in the bundle .get(ecs, player_entity)
// for us .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) => { ClientboundGamePacket::InitializeBorder(p) => {
debug!("Got initialize border packet {:?}", 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) => { ClientboundGamePacket::SetHealth(p) => {
debug!("Got set health packet {:?}", p); debug!("Got set health packet {:?}", p);
if p.health == 0.0 {
// we can't define a variable here with client.dead.lock() let mut health = ecs
// because of https://github.com/rust-lang/rust/issues/57478 .query::<&mut Health>()
if !*client.dead.lock() { .get_mut(ecs, player_entity)
*client.dead.lock() = true; .unwrap();
tx.send(Event::Death(None)).await?; **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) => { ClientboundGamePacket::SetExperience(p) => {
debug!("Got set experience packet {:?}", p); debug!("Got set experience packet {:?}", p);
} }
ClientboundGamePacket::TeleportEntity(p) => { ClientboundGamePacket::TeleportEntity(p) => {
let mut world = client.world.write(); let local_player = ecs
let (pos, physics) = self.query::<(&entity::Position, .query::<&mut LocalPlayer>()
&entity::Physics)>(); let _ = world.set_entity_pos( .get(ecs, player_entity)
EntityId(p.id), .unwrap();
Vec3 { let partial_world = local_player.partial_world.read();
x: p.x, let partial_entity_infos = &partial_world.entity_infos;
y: p.y, let entity = partial_entity_infos.get_by_id(MinecraftEntityId(p.id));
z: p.z, drop(partial_world);
},
pos, if let Some(entity) = entity {
physics, 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) => { ClientboundGamePacket::UpdateAdvancements(p) => {
debug!("Got update advancements packet {:?}", 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); // debug!("Got rotate head packet {:?}", p);
} }
ClientboundGamePacket::MoveEntityPos(p) => { ClientboundGamePacket::MoveEntityPos(p) => {
let mut local_player = ecs.query::<&mut LocalPlayer>().get_mut(ecs, entity).unwrap(); let local_player = ecs
let mut partial_entity_infos = local_player.partial_world.write().entity_infos; .query::<&mut LocalPlayer>()
let entity = partial_entity_infos.entity_by_id(p.entity_id); .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(); let mut position = ecs.query::<&mut Position>().get_mut(ecs, entity).unwrap();
**position = position.with_delta(&p.delta); **position = position.with_delta(&p.delta);
}, } else {
warn!(
"Got move entity pos packet for unknown entity id {}",
p.entity_id
);
}
}
ClientboundGamePacket::MoveEntityPosRot(p) => { ClientboundGamePacket::MoveEntityPosRot(p) => {
let entity_infos = ecs.resource::<EntityInfos>(); let local_player = ecs
let entity = entity_infos.entity_by_id(p.entity_id); .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(); let mut position = ecs.query::<&mut Position>().get_mut(ecs, entity).unwrap();
**position += p.delta; **position = position.with_delta(&p.delta);
} else {
warn!(
"Got move entity pos rot packet for unknown entity id {}",
p.entity_id
);
}
} }
ClientboundGamePacket::MoveEntityRot(_p) => { ClientboundGamePacket::MoveEntityRot(_p) => {
// debug!("Got move entity rot packet {:?}", p); // debug!("Got move entity rot packet {:?}", p);
} }
ClientboundGamePacket::KeepAlive(p) => { ClientboundGamePacket::KeepAlive(p) => {
debug!("Got keep alive packet {:?}", p); debug!("Got keep alive packet {:?}", p);
client
.write_packet(ServerboundKeepAlivePacket { id: p.id }.get()) let mut local_player = ecs
.await?; .query::<&mut LocalPlayer>()
.get_mut(ecs, player_entity)
.unwrap();
local_player.write_packet(ServerboundKeepAlivePacket { id: p.id }.get());
} }
ClientboundGamePacket::RemoveEntities(p) => { ClientboundGamePacket::RemoveEntities(p) => {
debug!("Got remove entities packet {:?}", p); debug!("Got remove entities packet {:?}", p);
} }
ClientboundGamePacket::PlayerChat(p) => { ClientboundGamePacket::PlayerChat(p) => {
debug!("Got player chat packet {:?}", 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) => { ClientboundGamePacket::SystemChat(p) => {
debug!("Got system chat packet {:?}", 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) => { ClientboundGamePacket::Sound(_p) => {
// debug!("Got sound packet {:?}", 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) => { ClientboundGamePacket::BlockUpdate(p) => {
debug!("Got block update packet {:?}", 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) => { ClientboundGamePacket::Animate(p) => {
debug!("Got animate packet {:?}", p); debug!("Got animate packet {:?}", p);
} }
ClientboundGamePacket::SectionBlocksUpdate(p) => { ClientboundGamePacket::SectionBlocksUpdate(p) => {
debug!("Got section blocks update packet {:?}", 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 { for state in &p.states {
world.set_block_state(&(p.section_pos + state.pos.clone()), world
state.state); } .chunks
.set_block_state(&(p.section_pos + state.pos.clone()), state.state);
}
} }
ClientboundGamePacket::GameEvent(p) => { ClientboundGamePacket::GameEvent(p) => {
debug!("Got game event packet {:?}", 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::PlayerCombatEnter(_) => {}
ClientboundGamePacket::PlayerCombatKill(p) => { ClientboundGamePacket::PlayerCombatKill(p) => {
debug!("Got player kill packet {:?}", 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(); let (&entity_id, dead) = ecs
if **entity_id == p.player_id { .query::<(&MinecraftEntityId, Option<&Dead>)>()
if !**dead { .get(ecs, player_entity)
**dead = true; .unwrap();
local_player.tx.send(Event::Death(Some(Arc::new(p.clone()))));
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::ResourcePack(_) => {}
ClientboundGamePacket::Respawn(p) => { ClientboundGamePacket::Respawn(p) => {
debug!("Got respawn packet {:?}", p); debug!("Got respawn packet {:?}", p);
// Sets clients dead state to false. // Remove the Dead marker component from the player.
let mut dead = ecs.query::<&mut Dead>().get(ecs, entity).unwrap(); ecs.entity_mut(player_entity).remove::<Dead>();
**dead = false;
} }
ClientboundGamePacket::SelectAdvancementsTab(_) => {} ClientboundGamePacket::SelectAdvancementsTab(_) => {}
ClientboundGamePacket::SetActionBarText(_) => {} 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<PacketReceiver>>,
mut world_container: ResMut<WorldContainer>,
mut entity_infos: ResMut<EntityInfos>,
) {
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. /// 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() { for packets in query.iter_mut() {
packets.packets.lock().clear(); packets.packets.lock().clear();
} }

View file

@ -125,7 +125,7 @@ pub fn move_colliding(
// TODO: minecraft checks for a "minor" horizontal collision here // 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 // let _block_state_below = self
// .world // .world
// .get_block_state(&block_pos_below) // .get_block_state(&block_pos_below)

View file

@ -1,10 +1,7 @@
use azalea_buf::McBuf; use azalea_buf::McBuf;
use azalea_core::{ResourceLocation, Vec3}; use azalea_core::{ResourceLocation, Vec3};
use azalea_protocol_macros::ClientboundGamePacket; use azalea_protocol_macros::ClientboundGamePacket;
use azalea_world::entity::{ use azalea_world::entity::{metadata::apply_default_metadata, EntityBundle};
metadata::{apply_default_metadata, PlayerMetadataBundle, UpdateMetadataError},
EntityBundle,
};
use uuid::Uuid; use uuid::Uuid;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)] #[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
@ -48,6 +45,7 @@ impl ClientboundAddEntityPacket {
EntityBundle::new(self.uuid, self.position, self.entity_type, world_name) 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) { pub fn apply_metadata(&self, entity: &mut bevy_ecs::world::EntityMut) {
apply_default_metadata(entity, self.entity_type); apply_default_metadata(entity, self.entity_type);
} }

View file

@ -2,7 +2,7 @@ use azalea_buf::McBuf;
use azalea_core::{ResourceLocation, Vec3}; use azalea_core::{ResourceLocation, Vec3};
use azalea_protocol_macros::ClientboundGamePacket; use azalea_protocol_macros::ClientboundGamePacket;
use azalea_registry::EntityKind; use azalea_registry::EntityKind;
use azalea_world::entity::{metadata::PlayerMetadataBundle, EntityBundle, PlayerBundle}; use azalea_world::entity::{metadata::PlayerMetadataBundle, Dead, EntityBundle, PlayerBundle};
use uuid::Uuid; use uuid::Uuid;
/// This packet is sent by the server when a player comes into visible range, /// This packet is sent by the server when a player comes into visible range,

View file

@ -1,13 +1,12 @@
use azalea_buf::McBuf; use azalea_buf::McBuf;
use azalea_core::Vec3;
use azalea_protocol_macros::ClientboundGamePacket; use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)] #[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
pub struct ClientboundTeleportEntityPacket { pub struct ClientboundTeleportEntityPacket {
#[var] #[var]
pub id: u32, pub id: u32,
pub x: f64, pub position: Vec3,
pub y: f64,
pub z: f64,
pub y_rot: i8, pub y_rot: i8,
pub x_rot: i8, pub x_rot: i8,
pub on_ground: bool, pub on_ground: bool,

View file

@ -17,6 +17,8 @@ const SECTION_HEIGHT: u32 = 16;
/// An efficient storage of chunks for a client that has a limited render /// An efficient storage of chunks for a client that has a limited render
/// distance. This has support for using a shared [`ChunkStorage`]. /// distance. This has support for using a shared [`ChunkStorage`].
pub struct PartialChunkStorage { 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, pub view_center: ChunkPos,
chunk_radius: u32, chunk_radius: u32,
view_range: u32, view_range: u32,
@ -135,7 +137,7 @@ impl PartialChunkStorage {
} }
/// Get a [`Chunk`] within render distance, or `None` if it's not loaded. /// 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<RwLock<Chunk>>> { pub fn limited_get(&self, pos: &ChunkPos) -> Option<&Arc<RwLock<Chunk>>> {
if !self.in_range(pos) { if !self.in_range(pos) {
warn!( warn!(
@ -149,7 +151,7 @@ impl PartialChunkStorage {
self.chunks[index].as_ref() self.chunks[index].as_ref()
} }
/// Get a mutable reference to a [`Chunk`] within render distance, or /// 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. /// a chunk from the shared storage.
pub fn limited_get_mut(&mut self, pos: &ChunkPos) -> Option<&mut Option<Arc<RwLock<Chunk>>>> { pub fn limited_get_mut(&mut self, pos: &ChunkPos) -> Option<&mut Option<Arc<RwLock<Chunk>>>> {
if !self.in_range(pos) { if !self.in_range(pos) {

View file

@ -6,13 +6,14 @@ use azalea_buf::{McBuf, McBufReadable, McBufWritable};
use azalea_chat::FormattedText; use azalea_chat::FormattedText;
use azalea_core::{BlockPos, Direction, GlobalPos, Particle, Slot}; use azalea_core::{BlockPos, Direction, GlobalPos, Particle, Slot};
use bevy_ecs::component::Component; use bevy_ecs::component::Component;
use derive_more::Deref;
use enum_as_inner::EnumAsInner; use enum_as_inner::EnumAsInner;
use nohash_hasher::IntSet; use nohash_hasher::IntSet;
use std::io::{Cursor, Write}; use std::io::{Cursor, Write};
use uuid::Uuid; use uuid::Uuid;
#[derive(Clone, Debug)] #[derive(Clone, Debug, Deref)]
pub struct EntityMetadataItems(pub Vec<EntityDataItem>); pub struct EntityMetadataItems(Vec<EntityDataItem>);
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct EntityDataItem { pub struct EntityDataItem {

View file

@ -1,4 +1,7 @@
use azalea_core::{Vec3, AABB}; use azalea_core::{Vec3, AABB};
use bevy_ecs::{query::Changed, system::Query};
use super::{Physics, Position};
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct EntityDimensions { 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<Position>>) {
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)
}

View file

@ -5,14 +5,22 @@ pub mod metadata;
use crate::ChunkStorage; use crate::ChunkStorage;
use self::{attributes::AttributeInstance, metadata::UpdateMetadataError}; use self::{
attributes::AttributeInstance,
metadata::{Health, UpdateMetadataError},
};
pub use attributes::Attributes; pub use attributes::Attributes;
use azalea_block::BlockState; use azalea_block::BlockState;
use azalea_core::{BlockPos, ChunkPos, ResourceLocation, Vec3, AABB}; 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::*; pub use data::*;
use derive_more::{Deref, DerefMut}; use derive_more::{Deref, DerefMut};
pub use dimensions::EntityDimensions; pub use dimensions::{update_bounding_box, EntityDimensions};
use std::fmt::Debug; use std::fmt::Debug;
use uuid::Uuid; use uuid::Uuid;
@ -36,18 +44,6 @@ impl std::hash::Hash for MinecraftEntityId {
} }
impl nohash_hasher::IsEnabled 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<Position>>) {
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) { pub fn set_rotation(physics: &mut Physics, y_rot: f32, x_rot: f32) {
physics.y_rot = y_rot % 360.0; physics.y_rot = y_rot % 360.0;
physics.x_rot = x_rot.clamp(-90.0, 90.0) % 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<EntityDataItem>,
) -> 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. /// Get the position of the block below the entity, but a little lower.
pub fn on_pos_legacy( pub fn on_pos_legacy(chunk_storage: &ChunkStorage, position: &Position) -> BlockPos {
chunk_storage: &ChunkStorage, on_pos(0.2, chunk_storage, position)
position: &Position,
physics: &Physics,
) -> BlockPos {
on_pos(0.2, chunk_storage, position, physics)
} }
// int x = Mth.floor(this.position.x); // int x = Mth.floor(this.position.x);
@ -115,12 +93,7 @@ pub fn on_pos_legacy(
// } // }
// } // }
// return var5; // return var5;
pub fn on_pos( pub fn on_pos(offset: f32, chunk_storage: &ChunkStorage, pos: &Position) -> BlockPos {
offset: f32,
chunk_storage: &ChunkStorage,
pos: &Position,
physics: &Physics,
) -> BlockPos {
let x = pos.x.floor() as i32; let x = pos.x.floor() as i32;
let y = (pos.y - offset as f64).floor() as i32; let y = (pos.y - offset as f64).floor() as i32;
let z = pos.z.floor() as i32; let z = pos.z.floor() as i32;
@ -145,6 +118,8 @@ pub fn on_pos(
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)] #[derive(Component, Deref, DerefMut, Clone, Copy)]
pub struct EntityUuid(Uuid); pub struct EntityUuid(Uuid);
@ -239,6 +214,27 @@ pub struct Physics {
pub has_impulse: bool, 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<Health>>) {
for (entity, health) in query.iter() {
if **health <= 0.0 {
commands.entity(entity).insert(Dead);
}
}
}
/// A component NewType for [`azalea_registry::EntityKind`]. /// A component NewType for [`azalea_registry::EntityKind`].
/// ///
/// Most of the time, you should be using `azalea_registry::EntityKind` /// Most of the time, you should be using `azalea_registry::EntityKind`
@ -266,6 +262,7 @@ impl EntityBundle {
kind: azalea_registry::EntityKind, kind: azalea_registry::EntityKind,
world_name: ResourceLocation, world_name: ResourceLocation,
) -> Self { ) -> Self {
// TODO: get correct entity dimensions by having them codegened somewhere
let dimensions = EntityDimensions { let dimensions = EntityDimensions {
width: 0.6, width: 0.6,
height: 1.8, height: 1.8,

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
entity::{self, update_bounding_box, Entity, MinecraftEntityId}, entity::{self, add_dead, update_bounding_box, Entity, MinecraftEntityId},
MaybeRemovedEntity, World, WorldContainer, MaybeRemovedEntity, World, WorldContainer,
}; };
use azalea_core::ChunkPos; use azalea_core::ChunkPos;
@ -18,12 +18,15 @@ use uuid::Uuid;
pub struct EntityPlugin; pub struct EntityPlugin;
impl Plugin for EntityPlugin { impl Plugin for EntityPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_system_set_to_stage( // Since it's PostUpdate, these will run after every tick or packet
CoreStage::PostUpdate, app.add_system_set(
SystemSet::new() SystemSet::new()
.after("tick")
.after("packet")
.with_system(update_entity_chunk_positions) .with_system(update_entity_chunk_positions)
.with_system(remove_despawned_entities_from_indexes) .with_system(remove_despawned_entities_from_indexes)
.with_system(update_bounding_box), .with_system(update_bounding_box)
.with_system(add_dead),
); );
} }
} }