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;
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<RwLock<WorldContainer>>,
/// The world that this client is in.
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
/// the `azalea` crate. you can ignore this field.
pub plugins: Arc<PluginStates>,
tasks: Arc<Mutex<Vec<JoinHandle<()>>>>,
/// 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<Arc<RwLock<WorldContainer>>>,
entity: Entity,
ecs: Arc<Mutex<bevy_ecs::world::World>>,
) -> 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<RwLock<World>> {
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
.as_ref()
.expect("World name must be known if we're doing Client::world");
let world_container = self.world_container.read();
.expect("World name must be known if we're doing Client::world")
.clone()
};
let world_container = ecs.resource::<WorldContainer>();
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::<WorldContainer>();
// all resources should have been added by now so we can take the ecs from the
// app

View file

@ -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<Uuid, PlayerInfo>,
/// The partial world is the world this client currently has loaded. It has
/// a limited render distance.
pub partial_world: Arc<RwLock<PartialWorld>>,
/// The world is the combined [`PartialWorld`]s of all clients in the same
/// world. (Only relevant if you're using a shared world, i.e. a swarm)
pub world: Arc<RwLock<World>>,
pub world_name: Option<ResourceLocation>,
pub tx: mpsc::UnboundedSender<Event>,
}
/// 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<JoinHandle<()>>,
}
#[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<Dead>>) {
for local_player in &query {
local_player.tx.send(Event::Death(None));
}
}
#[derive(Error, Debug)]
pub enum HandlePacketError {
#[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::{
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<PacketReceiver>>();
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,13 +130,15 @@ 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;
@ -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 (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)
} else {
physics.last_pos.x = p.x;
last_sent_position.x = p.x;
(0.0, p.x)
};
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)
} else {
physics.last_pos.y = p.y;
last_sent_position.y = p.y;
(0.0, p.y)
};
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)
} else {
physics.last_pos.z = p.z;
last_sent_position.z = 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,
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
// 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,
};
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
.write_packet(ServerboundAcceptTeleportationPacket { id: p.id
}.get()) .await?;
client
.write_packet(
local_player.write_packet(ServerboundAcceptTeleportationPacket { id: p.id }.get());
local_player.write_packet(
ServerboundMovePlayerPosRotPacket {
x: new_pos.x,
y: new_pos.y,
@ -272,14 +207,16 @@ pub fn handle_packet(ecs: &mut bevy_ecs::world::World, entity: Entity, packet: &
on_ground: false,
}
.get(),
)
.await?;
);
}
ClientboundGamePacket::PlayerInfoUpdate(p) => {
debug!("Got player info packet {:?}", p);
let mut events = Vec::new();
{
let mut players_lock = client.players.write();
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 {
@ -290,10 +227,12 @@ pub fn handle_packet(ecs: &mut bevy_ecs::world::World, entity: Entity, packet: &
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) {
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 {
@ -303,69 +242,76 @@ pub fn handle_packet(ecs: &mut bevy_ecs::world::World, entity: Entity, packet: &
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()));
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
"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();
let mut local_player = ecs
.query::<&mut LocalPlayer>()
.get_mut(ecs, player_entity)
.unwrap();
for uuid in &p.profile_ids {
if let Some(info) = players_lock.remove(uuid) {
events.push(Event::RemovePlayer(info));
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 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::<EntityInfos>();
let entity = entity_infos.entity_by_id(p.entity_id);
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 += 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) => {
// 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::<Dead>();
}
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<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.
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();
}

View file

@ -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)

View file

@ -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);
}

View file

@ -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,

View file

@ -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,

View file

@ -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<RwLock<Chunk>>> {
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<Arc<RwLock<Chunk>>>> {
if !self.in_range(pos) {

View file

@ -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<EntityDataItem>);
#[derive(Clone, Debug, Deref)]
pub struct EntityMetadataItems(Vec<EntityDataItem>);
#[derive(Clone, Debug)]
pub struct EntityDataItem {

View file

@ -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<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 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<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) {
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<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.
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<Health>>) {
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,

View file

@ -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),
);
}
}