mirror of
https://github.com/mat-1/azalea.git
synced 2025-08-02 14:26:04 +00:00
start splitting client
i'm probably gonna change it so azalea entity ids are separate from minecraft entity ids next (so stuff like player ids can be consistent and we don't have to wait for the login packet)
This commit is contained in:
parent
12269a9dc0
commit
2de5a39f31
12 changed files with 1206 additions and 1122 deletions
|
@ -1,6 +1,6 @@
|
|||
//! Implementations of chat-related features.
|
||||
|
||||
use crate::Client;
|
||||
use crate::LocalPlayer;
|
||||
use azalea_chat::FormattedText;
|
||||
use azalea_protocol::packets::game::{
|
||||
clientbound_player_chat_packet::ClientboundPlayerChatPacket,
|
||||
|
@ -89,7 +89,7 @@ impl ChatPacket {
|
|||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
impl LocalPlayer {
|
||||
/// Sends chat message to the server. This only sends the chat packet and
|
||||
/// not the command packet. The [`Client::chat`] function handles checking
|
||||
/// whether the message is a command and using the proper packet for you,
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
pub use crate::chat::ChatPacket;
|
||||
use crate::{movement::WalkDirection, plugins::PluginStates, Account, PlayerInfo};
|
||||
use crate::{
|
||||
local_player::LocalPlayer, movement::WalkDirection, plugins::PluginStates, Account, PlayerInfo,
|
||||
};
|
||||
use azalea_auth::{game_profile::GameProfile, sessionserver::SessionServerError};
|
||||
use azalea_core::{ChunkPos, ResourceLocation, Vec3};
|
||||
use azalea_protocol::{
|
||||
|
@ -34,11 +36,13 @@ use azalea_world::{
|
|||
metadata::{self, PlayerMetadataBundle},
|
||||
EntityId,
|
||||
},
|
||||
PartialWorld, WeakWorld, WeakWorldContainer,
|
||||
PartialChunkStorage, PartialWorld, WeakWorld, WeakWorldContainer,
|
||||
};
|
||||
use bevy_ecs::{
|
||||
schedule::{Schedule, StageLabel, SystemStage},
|
||||
system::{Query, SystemParam, SystemState},
|
||||
prelude::Component,
|
||||
query::{QueryState, WorldQuery},
|
||||
schedule::{Schedule, Stage, StageLabel, SystemStage},
|
||||
system::{Query, SystemState},
|
||||
};
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
|
@ -48,7 +52,6 @@ use std::{
|
|||
collections::HashMap,
|
||||
fmt::Debug,
|
||||
io::{self, Cursor},
|
||||
ops::DerefMut,
|
||||
sync::Arc,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
@ -93,47 +96,23 @@ pub enum Event {
|
|||
Death(Option<Arc<ClientboundPlayerCombatKillPacket>>),
|
||||
}
|
||||
|
||||
/// A player that you control that is currently in a Minecraft server.
|
||||
#[derive(Clone)]
|
||||
pub struct Client {
|
||||
pub profile: GameProfile,
|
||||
pub read_conn: Arc<tokio::sync::Mutex<ReadConnection<ClientboundGamePacket>>>,
|
||||
pub write_conn: Arc<tokio::sync::Mutex<WriteConnection<ServerboundGamePacket>>>,
|
||||
pub entity_id: Arc<RwLock<EntityId>>,
|
||||
/// The world that this client has access to. This supports shared worlds.
|
||||
pub world: Arc<RwLock<PartialWorld>>,
|
||||
/// A container of world names to worlds. If we're not using a shared world
|
||||
/// (i.e. not a swarm), then this will only contain data about the world
|
||||
/// we're currently in.
|
||||
world_container: Arc<RwLock<WeakWorldContainer>>,
|
||||
pub world_name: Arc<RwLock<Option<ResourceLocation>>>,
|
||||
pub physics_state: Arc<Mutex<PhysicsState>>,
|
||||
pub client_information: Arc<RwLock<ClientInformation>>,
|
||||
pub dead: Arc<Mutex<bool>>,
|
||||
/// The world that this client is in.
|
||||
pub world: Arc<RwLock<PartialWorld>>,
|
||||
|
||||
/// Plugins are a way for other crates to add custom functionality to the
|
||||
/// client and keep state. If you're not making a plugin and you're using
|
||||
/// the `azalea` crate. you can ignore this field.
|
||||
pub plugins: Arc<PluginStates>,
|
||||
/// A map of player uuids to their information in the tab list
|
||||
pub players: Arc<RwLock<HashMap<Uuid, PlayerInfo>>>,
|
||||
tasks: Arc<Mutex<Vec<JoinHandle<()>>>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PhysicsState {
|
||||
/// Minecraft only sends a movement packet either after 20 ticks or if the
|
||||
/// player moved enough. This is that tick counter.
|
||||
pub position_remainder: u32,
|
||||
pub was_sprinting: bool,
|
||||
// Whether we're going to try to start sprinting this tick. Equivalent to
|
||||
// holding down ctrl for a tick.
|
||||
pub trying_to_sprint: bool,
|
||||
|
||||
pub move_direction: WalkDirection,
|
||||
pub forward_impulse: f32,
|
||||
pub left_impulse: f32,
|
||||
}
|
||||
|
||||
/// Whether we should ignore errors when decoding packets.
|
||||
const IGNORE_ERRORS: bool = !cfg!(debug_assertions);
|
||||
|
||||
|
@ -156,50 +135,25 @@ pub enum JoinError {
|
|||
Auth(#[from] azalea_auth::AuthError),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum HandleError {
|
||||
#[error("{0}")]
|
||||
Poison(String),
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
#[error("{0}")]
|
||||
Send(#[from] mpsc::error::SendError<Event>),
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new client from the given GameProfile, Connection, and World.
|
||||
/// You should only use this if you want to change these fields from the
|
||||
/// defaults, otherwise use [`Client::join`].
|
||||
pub fn new(
|
||||
profile: GameProfile,
|
||||
conn: Connection<ClientboundGamePacket, ServerboundGamePacket>,
|
||||
world_container: Option<Arc<RwLock<WeakWorldContainer>>>,
|
||||
) -> Self {
|
||||
let (read_conn, write_conn) = conn.into_split();
|
||||
let (read_conn, write_conn) = (
|
||||
Arc::new(tokio::sync::Mutex::new(read_conn)),
|
||||
Arc::new(tokio::sync::Mutex::new(write_conn)),
|
||||
);
|
||||
|
||||
Self {
|
||||
profile,
|
||||
read_conn,
|
||||
write_conn,
|
||||
// default our id to 0, it'll be set later
|
||||
entity_id: Arc::new(RwLock::new(EntityId(0))),
|
||||
world: Arc::new(RwLock::new(PartialWorld::default())),
|
||||
world_container: world_container
|
||||
.unwrap_or_else(|| Arc::new(RwLock::new(WeakWorldContainer::new()))),
|
||||
world_name: Arc::new(RwLock::new(None)),
|
||||
physics_state: Arc::new(Mutex::new(PhysicsState::default())),
|
||||
client_information: Arc::new(RwLock::new(ClientInformation::default())),
|
||||
dead: Arc::new(Mutex::new(false)),
|
||||
// The plugins can be modified by the user by replacing the plugins
|
||||
// field right after this. No Mutex so the user doesn't need to .lock().
|
||||
plugins: Arc::new(PluginStates::default()),
|
||||
players: Arc::new(RwLock::new(HashMap::new())),
|
||||
tasks: Arc::new(Mutex::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
@ -238,15 +192,16 @@ impl Client {
|
|||
// received a bit later instead of the instant they were fired.
|
||||
// That bug especially causes issues with the pathfinder.
|
||||
let (tx, rx) = mpsc::channel(1);
|
||||
tx.send(Event::Init).await.expect("Failed to send event");
|
||||
|
||||
// we got the GameConnection, so the server is now connected :)
|
||||
let client = Client::new(game_profile, conn, None);
|
||||
|
||||
tx.send(Event::Init).await.expect("Failed to send event");
|
||||
let client = Client::new(game_profile.clone(), None);
|
||||
let local_player =
|
||||
crate::local_player::LocalPlayer::new(game_profile, conn, client.world.clone(), tx);
|
||||
|
||||
// just start up the game loop and we're ready!
|
||||
|
||||
client.start_tasks(tx);
|
||||
client.start_tasks();
|
||||
|
||||
Ok((client, rx))
|
||||
}
|
||||
|
@ -371,13 +326,12 @@ impl Client {
|
|||
|
||||
/// Write a packet directly to the server.
|
||||
pub async fn write_packet(&self, packet: ServerboundGamePacket) -> Result<(), std::io::Error> {
|
||||
self.write_conn.lock().await.write(packet).await?;
|
||||
Ok(())
|
||||
self.local_player_mut().write_packet(packet).await
|
||||
}
|
||||
|
||||
/// Disconnect this client from the server, ending all tasks.
|
||||
pub async fn disconnect(&self) -> Result<(), std::io::Error> {
|
||||
if let Err(e) = self.write_conn.lock().await.shutdown().await {
|
||||
if let Err(e) = self.local_player_mut().write_conn.shutdown().await {
|
||||
warn!(
|
||||
"Error shutting down connection, but it might be fine: {}",
|
||||
e
|
||||
|
@ -390,9 +344,16 @@ impl Client {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn local_player(&self) -> &LocalPlayer {
|
||||
self.query::<&LocalPlayer>()
|
||||
}
|
||||
pub fn local_player_mut(&self) -> &mut LocalPlayer {
|
||||
self.query::<&mut LocalPlayer>().into_inner()
|
||||
}
|
||||
|
||||
/// Start the protocol and game tick loop.
|
||||
#[doc(hidden)]
|
||||
pub fn start_tasks(&self, tx: Sender<Event>) {
|
||||
pub fn start_tasks(&self) {
|
||||
// if you get an error right here that means you're doing something with locks
|
||||
// wrong read the error to see where the issue is
|
||||
// you might be able to just drop the lock or put it in its own scope to fix
|
||||
|
@ -402,14 +363,19 @@ impl Client {
|
|||
self.clone(),
|
||||
tx.clone(),
|
||||
)));
|
||||
tasks.push(tokio::spawn(Client::game_tick_loop(self.clone(), tx)));
|
||||
let ecs = self.world_container.clone().read().ecs;
|
||||
tasks.push(tokio::spawn(Client::game_tick_loop(ecs.clone())));
|
||||
}
|
||||
|
||||
async fn protocol_loop(client: Client, tx: Sender<Event>) {
|
||||
async fn protocol_loop(local_player: LocalPlayer, tx: Sender<Event>) {
|
||||
loop {
|
||||
let r = client.read_conn.lock().await.read().await;
|
||||
let r = local_player.read_conn.lock().await.read().await;
|
||||
match r {
|
||||
Ok(packet) => match Self::handle(&packet, &client, &tx).await {
|
||||
Ok(packet) => {
|
||||
match {
|
||||
LocalPlayer::send_event(Event::Packet(packet.clone()), &tx);
|
||||
LocalPlayer::handle_packet(&packet, &client, &tx)
|
||||
} {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!("Error handling packet: {}", e);
|
||||
|
@ -446,629 +412,31 @@ impl Client {
|
|||
}
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
packet: &ClientboundGamePacket,
|
||||
client: &Client,
|
||||
tx: &Sender<Event>,
|
||||
) -> Result<(), HandleError> {
|
||||
let packet = Arc::new(packet.clone());
|
||||
tx.send(Event::Packet(packet.clone())).await?;
|
||||
match &*packet {
|
||||
ClientboundGamePacket::Login(p) => {
|
||||
debug!("Got login packet");
|
||||
|
||||
{
|
||||
// // write p into login.txt
|
||||
// std::io::Write::write_all(
|
||||
// &mut std::fs::File::create("login.txt").unwrap(),
|
||||
// format!("{:#?}", p).as_bytes(),
|
||||
// )
|
||||
// .unwrap();
|
||||
|
||||
// TODO: have registry_holder be a struct because this sucks rn
|
||||
// best way would be to add serde support to azalea-nbt
|
||||
|
||||
let registry_holder = p
|
||||
.registry_holder
|
||||
.as_compound()
|
||||
.expect("Registry holder is not a compound")
|
||||
.get("")
|
||||
.expect("No \"\" tag")
|
||||
.as_compound()
|
||||
.expect("\"\" tag is not a compound");
|
||||
let dimension_types = registry_holder
|
||||
.get("minecraft:dimension_type")
|
||||
.expect("No dimension_type tag")
|
||||
.as_compound()
|
||||
.expect("dimension_type is not a compound")
|
||||
.get("value")
|
||||
.expect("No dimension_type value")
|
||||
.as_list()
|
||||
.expect("dimension_type value is not a list");
|
||||
let dimension_type = dimension_types
|
||||
.iter()
|
||||
.find(|t| {
|
||||
t.as_compound()
|
||||
.expect("dimension_type value is not a compound")
|
||||
.get("name")
|
||||
.expect("No name tag")
|
||||
.as_string()
|
||||
.expect("name is not a string")
|
||||
== p.dimension_type.to_string()
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
panic!("No dimension_type with name {}", p.dimension_type)
|
||||
})
|
||||
.as_compound()
|
||||
.unwrap()
|
||||
.get("element")
|
||||
.expect("No element tag")
|
||||
.as_compound()
|
||||
.expect("element is not a compound");
|
||||
let height = (*dimension_type
|
||||
.get("height")
|
||||
.expect("No height tag")
|
||||
.as_int()
|
||||
.expect("height tag is not an int"))
|
||||
.try_into()
|
||||
.expect("height is not a u32");
|
||||
let min_y = *dimension_type
|
||||
.get("min_y")
|
||||
.expect("No min_y tag")
|
||||
.as_int()
|
||||
.expect("min_y tag is not an int");
|
||||
|
||||
let world_name = p.dimension.clone();
|
||||
|
||||
*client.world_name.write() = Some(world_name.clone());
|
||||
// add this world to the world_container (or don't if it's already there)
|
||||
let weak_world = client
|
||||
.world_container
|
||||
.write()
|
||||
.insert(world_name, height, min_y);
|
||||
// set the loaded_world to an empty world
|
||||
// (when we add chunks or entities those will be in the world_container)
|
||||
let mut world_lock = client.world.write();
|
||||
*world_lock = PartialWorld::new(
|
||||
client.client_information.read().view_distance.into(),
|
||||
weak_world,
|
||||
Some(EntityId(p.player_id)),
|
||||
);
|
||||
|
||||
let player_bundle = entity::PlayerBundle {
|
||||
entity: entity::EntityBundle::new(
|
||||
client.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.client_information.read().clone();
|
||||
log::debug!(
|
||||
"Sending client information because login: {:?}",
|
||||
client_information_packet
|
||||
);
|
||||
client.write_packet(client_information_packet.get()).await?;
|
||||
|
||||
// brand
|
||||
client
|
||||
.write_packet(
|
||||
ServerboundCustomPayloadPacket {
|
||||
identifier: ResourceLocation::new("brand").unwrap(),
|
||||
// they don't have to know :)
|
||||
data: "vanilla".into(),
|
||||
}
|
||||
.get(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.send(Event::Login).await?;
|
||||
}
|
||||
ClientboundGamePacket::SetChunkCacheRadius(p) => {
|
||||
debug!("Got set chunk cache radius packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::CustomPayload(p) => {
|
||||
debug!("Got custom payload packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::ChangeDifficulty(p) => {
|
||||
debug!("Got difficulty packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::Commands(_p) => {
|
||||
debug!("Got declare commands packet");
|
||||
}
|
||||
ClientboundGamePacket::PlayerAbilities(p) => {
|
||||
debug!("Got player abilities packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::SetCarriedItem(p) => {
|
||||
debug!("Got set carried item packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::UpdateTags(_p) => {
|
||||
debug!("Got update tags packet");
|
||||
}
|
||||
ClientboundGamePacket::Disconnect(p) => {
|
||||
debug!("Got disconnect packet {:?}", p);
|
||||
client.disconnect().await?;
|
||||
}
|
||||
ClientboundGamePacket::UpdateRecipes(_p) => {
|
||||
debug!("Got update recipes packet");
|
||||
}
|
||||
ClientboundGamePacket::EntityEvent(_p) => {
|
||||
// debug!("Got entity event packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::Recipe(_p) => {
|
||||
debug!("Got recipe packet");
|
||||
}
|
||||
ClientboundGamePacket::PlayerPosition(p) => {
|
||||
// TODO: reply with teleport confirm
|
||||
debug!("Got player position packet {:?}", p);
|
||||
|
||||
let (new_pos, y_rot, x_rot) = {
|
||||
let player_entity_id = *client.entity();
|
||||
let world = client.world();
|
||||
// let mut player_entity = world.entity_mut(player_entity_id).unwrap();
|
||||
let mut entities = world.entities.write();
|
||||
let (mut physics, position) = entities
|
||||
.query_entity_mut::<(&mut entity::Physics, &mut entity::Position)>(
|
||||
player_entity_id,
|
||||
);
|
||||
|
||||
let delta_movement = physics.delta;
|
||||
|
||||
let is_x_relative = p.relative_arguments.x;
|
||||
let is_y_relative = p.relative_arguments.y;
|
||||
let is_z_relative = p.relative_arguments.z;
|
||||
|
||||
let (delta_x, new_pos_x) = if is_x_relative {
|
||||
physics.last_pos.x += p.x;
|
||||
(delta_movement.x, position.x + p.x)
|
||||
} else {
|
||||
physics.last_pos.x = p.x;
|
||||
(0.0, p.x)
|
||||
};
|
||||
let (delta_y, new_pos_y) = if is_y_relative {
|
||||
physics.last_pos.y += p.y;
|
||||
(delta_movement.y, position.y + p.y)
|
||||
} else {
|
||||
physics.last_pos.y = p.y;
|
||||
(0.0, p.y)
|
||||
};
|
||||
let (delta_z, new_pos_z) = if is_z_relative {
|
||||
physics.last_pos.z += p.z;
|
||||
(delta_movement.z, position.z + p.z)
|
||||
} else {
|
||||
physics.last_pos.z = p.z;
|
||||
(0.0, p.z)
|
||||
};
|
||||
|
||||
let mut y_rot = p.y_rot;
|
||||
let mut x_rot = p.x_rot;
|
||||
if p.relative_arguments.x_rot {
|
||||
x_rot += physics.x_rot;
|
||||
}
|
||||
if p.relative_arguments.y_rot {
|
||||
y_rot += physics.y_rot;
|
||||
}
|
||||
|
||||
physics.delta = Vec3 {
|
||||
x: delta_x,
|
||||
y: delta_y,
|
||||
z: delta_z,
|
||||
};
|
||||
entity::set_rotation(physics.deref_mut(), 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_from_refs(
|
||||
player_entity_id,
|
||||
new_pos,
|
||||
position.into_inner(),
|
||||
physics.into_inner(),
|
||||
)
|
||||
.expect("The player entity should always exist");
|
||||
|
||||
(new_pos, y_rot, x_rot)
|
||||
};
|
||||
|
||||
client
|
||||
.write_packet(ServerboundAcceptTeleportationPacket { id: p.id }.get())
|
||||
.await?;
|
||||
client
|
||||
.write_packet(
|
||||
ServerboundMovePlayerPosRotPacket {
|
||||
x: new_pos.x,
|
||||
y: new_pos.y,
|
||||
z: new_pos.z,
|
||||
y_rot,
|
||||
x_rot,
|
||||
// this is always false
|
||||
on_ground: false,
|
||||
}
|
||||
.get(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ClientboundGamePacket::PlayerInfoUpdate(p) => {
|
||||
debug!("Got player info packet {:?}", p);
|
||||
let mut events = Vec::new();
|
||||
{
|
||||
let mut players_lock = client.players.write();
|
||||
for updated_info in &p.entries {
|
||||
// add the new player maybe
|
||||
if p.actions.add_player {
|
||||
let player_info = PlayerInfo {
|
||||
profile: updated_info.profile.clone(),
|
||||
uuid: updated_info.profile.uuid,
|
||||
gamemode: updated_info.game_mode,
|
||||
latency: updated_info.latency,
|
||||
display_name: updated_info.display_name.clone(),
|
||||
};
|
||||
players_lock.insert(updated_info.profile.uuid, player_info.clone());
|
||||
events.push(Event::AddPlayer(player_info));
|
||||
} else if let Some(info) = players_lock.get_mut(&updated_info.profile.uuid)
|
||||
{
|
||||
// `else if` because the block for add_player above
|
||||
// already sets all the fields
|
||||
if p.actions.update_game_mode {
|
||||
info.gamemode = updated_info.game_mode;
|
||||
}
|
||||
if p.actions.update_latency {
|
||||
info.latency = updated_info.latency;
|
||||
}
|
||||
if p.actions.update_display_name {
|
||||
info.display_name = updated_info.display_name.clone();
|
||||
}
|
||||
events.push(Event::UpdatePlayer(info.clone()));
|
||||
} else {
|
||||
warn!(
|
||||
"Ignoring PlayerInfoUpdate for unknown player {}",
|
||||
updated_info.profile.uuid
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for event in events {
|
||||
tx.send(event).await?;
|
||||
}
|
||||
}
|
||||
ClientboundGamePacket::PlayerInfoRemove(p) => {
|
||||
let mut events = Vec::new();
|
||||
{
|
||||
let mut players_lock = client.players.write();
|
||||
for uuid in &p.profile_ids {
|
||||
if let Some(info) = players_lock.remove(uuid) {
|
||||
events.push(Event::RemovePlayer(info));
|
||||
}
|
||||
}
|
||||
}
|
||||
for event in events {
|
||||
tx.send(event).await?;
|
||||
}
|
||||
}
|
||||
ClientboundGamePacket::SetChunkCacheCenter(p) => {
|
||||
debug!("Got chunk cache center packet {:?}", p);
|
||||
client
|
||||
.world
|
||||
.write()
|
||||
.update_view_center(&ChunkPos::new(p.x, p.z));
|
||||
}
|
||||
ClientboundGamePacket::LevelChunkWithLight(p) => {
|
||||
// debug!("Got chunk with light packet {} {}", p.x, p.z);
|
||||
let pos = ChunkPos::new(p.x, p.z);
|
||||
|
||||
// OPTIMIZATION: if we already know about the chunk from the
|
||||
// shared world (and not ourselves), then we don't need to
|
||||
// parse it again. This is only used when we have a shared
|
||||
// world, since we check that the chunk isn't currently owned
|
||||
// by this client.
|
||||
let shared_has_chunk = client.world.read().get_chunk(&pos).is_some();
|
||||
let this_client_has_chunk = client.world.read().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(());
|
||||
}
|
||||
|
||||
// let chunk = Chunk::read_with_world_height(&mut p.chunk_data);
|
||||
// debug("chunk {:?}")
|
||||
if let Err(e) = client
|
||||
.world
|
||||
.write()
|
||||
.replace_with_packet_data(&pos, &mut Cursor::new(&p.chunk_data.data))
|
||||
{
|
||||
error!("Couldn't set chunk data: {}", e);
|
||||
}
|
||||
}
|
||||
ClientboundGamePacket::LightUpdate(_p) => {
|
||||
// debug!("Got light update packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::AddEntity(p) => {
|
||||
debug!("Got add entity packet {:?}", p);
|
||||
let 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.entities.shared.write();
|
||||
let mut entity = entities.ecs_entity_mut(EntityId(p.id)).unwrap();
|
||||
p.apply_metadata(&mut entity);
|
||||
}
|
||||
ClientboundGamePacket::SetEntityData(p) => {
|
||||
debug!("Got set entity data packet {:?}", p);
|
||||
let world = client.world.write();
|
||||
let mut entities = world.entities.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);
|
||||
}
|
||||
}
|
||||
ClientboundGamePacket::UpdateAttributes(_p) => {
|
||||
// debug!("Got update attributes packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::SetEntityMotion(_p) => {
|
||||
// debug!("Got entity velocity packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::SetEntityLink(p) => {
|
||||
debug!("Got set entity link packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::AddPlayer(p) => {
|
||||
debug!("Got add player packet {:?}", p);
|
||||
let 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
|
||||
}
|
||||
ClientboundGamePacket::InitializeBorder(p) => {
|
||||
debug!("Got initialize border packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::SetTime(p) => {
|
||||
debug!("Got set time packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::SetDefaultSpawnPosition(p) => {
|
||||
debug!("Got set default spawn position packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::ContainerSetContent(p) => {
|
||||
debug!("Got container set content packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::SetHealth(p) => {
|
||||
debug!("Got set health packet {:?}", p);
|
||||
if p.health == 0.0 {
|
||||
// we can't define a variable here with client.dead.lock()
|
||||
// because of https://github.com/rust-lang/rust/issues/57478
|
||||
if !*client.dead.lock() {
|
||||
*client.dead.lock() = true;
|
||||
tx.send(Event::Death(None)).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
ClientboundGamePacket::SetExperience(p) => {
|
||||
debug!("Got set experience packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::TeleportEntity(p) => {
|
||||
let mut world = client.world.write();
|
||||
let _ = world.set_entity_pos(
|
||||
EntityId(p.id),
|
||||
Vec3 {
|
||||
x: p.x,
|
||||
y: p.y,
|
||||
z: p.z,
|
||||
},
|
||||
);
|
||||
}
|
||||
ClientboundGamePacket::UpdateAdvancements(p) => {
|
||||
debug!("Got update advancements packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::RotateHead(_p) => {
|
||||
// debug!("Got rotate head packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::MoveEntityPos(p) => {
|
||||
let mut world = client.world.write();
|
||||
let _ = world.move_entity_with_delta(EntityId(p.entity_id), &p.delta);
|
||||
}
|
||||
ClientboundGamePacket::MoveEntityPosRot(p) => {
|
||||
let mut world = client.world.write();
|
||||
let _ = world.move_entity_with_delta(EntityId(p.entity_id), &p.delta);
|
||||
}
|
||||
ClientboundGamePacket::MoveEntityRot(_p) => {
|
||||
// debug!("Got move entity rot packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::KeepAlive(p) => {
|
||||
debug!("Got keep alive packet {:?}", p);
|
||||
client
|
||||
.write_packet(ServerboundKeepAlivePacket { id: p.id }.get())
|
||||
.await?;
|
||||
}
|
||||
ClientboundGamePacket::RemoveEntities(p) => {
|
||||
debug!("Got remove entities packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::PlayerChat(p) => {
|
||||
debug!("Got player chat packet {:?}", p);
|
||||
tx.send(Event::Chat(ChatPacket::Player(Arc::new(p.clone()))))
|
||||
.await?;
|
||||
}
|
||||
ClientboundGamePacket::SystemChat(p) => {
|
||||
debug!("Got system chat packet {:?}", p);
|
||||
tx.send(Event::Chat(ChatPacket::System(Arc::new(p.clone()))))
|
||||
.await?;
|
||||
}
|
||||
ClientboundGamePacket::Sound(_p) => {
|
||||
// debug!("Got sound packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::LevelEvent(p) => {
|
||||
debug!("Got level event packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::BlockUpdate(p) => {
|
||||
debug!("Got block update packet {:?}", p);
|
||||
let mut world = client.world.write();
|
||||
world.set_block_state(&p.pos, p.block_state);
|
||||
}
|
||||
ClientboundGamePacket::Animate(p) => {
|
||||
debug!("Got animate packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::SectionBlocksUpdate(p) => {
|
||||
debug!("Got section blocks update packet {:?}", p);
|
||||
let mut world = client.world.write();
|
||||
for state in &p.states {
|
||||
world.set_block_state(&(p.section_pos + state.pos.clone()), state.state);
|
||||
}
|
||||
}
|
||||
ClientboundGamePacket::GameEvent(p) => {
|
||||
debug!("Got game event packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::LevelParticles(p) => {
|
||||
debug!("Got level particles packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::ServerData(p) => {
|
||||
debug!("Got server data packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::SetEquipment(p) => {
|
||||
debug!("Got set equipment packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::UpdateMobEffect(p) => {
|
||||
debug!("Got update mob effect packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::AddExperienceOrb(_) => {}
|
||||
ClientboundGamePacket::AwardStats(_) => {}
|
||||
ClientboundGamePacket::BlockChangedAck(_) => {}
|
||||
ClientboundGamePacket::BlockDestruction(_) => {}
|
||||
ClientboundGamePacket::BlockEntityData(_) => {}
|
||||
ClientboundGamePacket::BlockEvent(_) => {}
|
||||
ClientboundGamePacket::BossEvent(_) => {}
|
||||
ClientboundGamePacket::CommandSuggestions(_) => {}
|
||||
ClientboundGamePacket::ContainerSetData(_) => {}
|
||||
ClientboundGamePacket::ContainerSetSlot(_) => {}
|
||||
ClientboundGamePacket::Cooldown(_) => {}
|
||||
ClientboundGamePacket::CustomChatCompletions(_) => {}
|
||||
ClientboundGamePacket::DeleteChat(_) => {}
|
||||
ClientboundGamePacket::Explode(_) => {}
|
||||
ClientboundGamePacket::ForgetLevelChunk(_) => {}
|
||||
ClientboundGamePacket::HorseScreenOpen(_) => {}
|
||||
ClientboundGamePacket::MapItemData(_) => {}
|
||||
ClientboundGamePacket::MerchantOffers(_) => {}
|
||||
ClientboundGamePacket::MoveVehicle(_) => {}
|
||||
ClientboundGamePacket::OpenBook(_) => {}
|
||||
ClientboundGamePacket::OpenScreen(_) => {}
|
||||
ClientboundGamePacket::OpenSignEditor(_) => {}
|
||||
ClientboundGamePacket::Ping(_) => {}
|
||||
ClientboundGamePacket::PlaceGhostRecipe(_) => {}
|
||||
ClientboundGamePacket::PlayerCombatEnd(_) => {}
|
||||
ClientboundGamePacket::PlayerCombatEnter(_) => {}
|
||||
ClientboundGamePacket::PlayerCombatKill(p) => {
|
||||
debug!("Got player kill packet {:?}", p);
|
||||
if client.entity() == EntityId(p.player_id) {
|
||||
// we can't define a variable here with client.dead.lock()
|
||||
// because of https://github.com/rust-lang/rust/issues/57478
|
||||
if !*client.dead.lock() {
|
||||
*client.dead.lock() = true;
|
||||
tx.send(Event::Death(Some(Arc::new(p.clone())))).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
ClientboundGamePacket::PlayerLookAt(_) => {}
|
||||
ClientboundGamePacket::RemoveMobEffect(_) => {}
|
||||
ClientboundGamePacket::ResourcePack(_) => {}
|
||||
ClientboundGamePacket::Respawn(p) => {
|
||||
debug!("Got respawn packet {:?}", p);
|
||||
// Sets clients dead state to false.
|
||||
let mut dead_lock = client.dead.lock();
|
||||
*dead_lock = false;
|
||||
}
|
||||
ClientboundGamePacket::SelectAdvancementsTab(_) => {}
|
||||
ClientboundGamePacket::SetActionBarText(_) => {}
|
||||
ClientboundGamePacket::SetBorderCenter(_) => {}
|
||||
ClientboundGamePacket::SetBorderLerpSize(_) => {}
|
||||
ClientboundGamePacket::SetBorderSize(_) => {}
|
||||
ClientboundGamePacket::SetBorderWarningDelay(_) => {}
|
||||
ClientboundGamePacket::SetBorderWarningDistance(_) => {}
|
||||
ClientboundGamePacket::SetCamera(_) => {}
|
||||
ClientboundGamePacket::SetDisplayObjective(_) => {}
|
||||
ClientboundGamePacket::SetObjective(_) => {}
|
||||
ClientboundGamePacket::SetPassengers(_) => {}
|
||||
ClientboundGamePacket::SetPlayerTeam(_) => {}
|
||||
ClientboundGamePacket::SetScore(_) => {}
|
||||
ClientboundGamePacket::SetSimulationDistance(_) => {}
|
||||
ClientboundGamePacket::SetSubtitleText(_) => {}
|
||||
ClientboundGamePacket::SetTitleText(_) => {}
|
||||
ClientboundGamePacket::SetTitlesAnimation(_) => {}
|
||||
ClientboundGamePacket::SoundEntity(_) => {}
|
||||
ClientboundGamePacket::StopSound(_) => {}
|
||||
ClientboundGamePacket::TabList(_) => {}
|
||||
ClientboundGamePacket::TagQuery(_) => {}
|
||||
ClientboundGamePacket::TakeItemEntity(_) => {}
|
||||
ClientboundGamePacket::DisguisedChat(_) => {}
|
||||
ClientboundGamePacket::UpdateEnabledFeatures(_) => {}
|
||||
ClientboundGamePacket::ContainerClose(_) => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs game_tick every 50 milliseconds.
|
||||
async fn game_tick_loop(mut client: Client, tx: Sender<Event>) {
|
||||
/// Start the game tick loop for every client in the shared world. This
|
||||
/// should only be run once per shared world!
|
||||
async fn game_tick_loop(ecs: Arc<Mutex<bevy_ecs::world::World>>) {
|
||||
let mut game_tick_interval = time::interval(time::Duration::from_millis(50));
|
||||
// TODO: Minecraft bursts up to 10 ticks and then skips, we should too
|
||||
game_tick_interval.set_missed_tick_behavior(time::MissedTickBehavior::Burst);
|
||||
|
||||
let mut schedule = Schedule::default();
|
||||
#[derive(StageLabel)]
|
||||
pub struct Tick;
|
||||
schedule.add_stage(
|
||||
Tick,
|
||||
SystemStage::single_threaded()
|
||||
.with_system(LocalPlayer::update_in_loaded_chunk)
|
||||
.with_system(LocalPlayer::send_position)
|
||||
.with_system(LocalPlayer::ai_step),
|
||||
);
|
||||
|
||||
loop {
|
||||
if !client.in_loaded_chunk() {
|
||||
continue;
|
||||
}
|
||||
game_tick_interval.tick().await;
|
||||
|
||||
tx.send(Event::Tick)
|
||||
.await
|
||||
.expect("Sending tick event should never fail");
|
||||
// schedule.run_once(&mut client.world().entities.read().ecs);
|
||||
let world = client.world();
|
||||
let mut ecs = &mut world.entities.write().ecs;
|
||||
client.send_position(
|
||||
SystemState::<
|
||||
Query<(
|
||||
&entity::Position,
|
||||
&mut entity::Physics,
|
||||
&entity::metadata::Sprinting,
|
||||
)>,
|
||||
>::new(&mut ecs)
|
||||
.get_mut(&mut ecs),
|
||||
);
|
||||
schedule.run(&mut ecs.lock());
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether our player is in a loaded chunk
|
||||
fn in_loaded_chunk(&self) -> bool {
|
||||
let world = self.world();
|
||||
let entities = world.entities.read();
|
||||
|
||||
let player_entity_id = self.entity();
|
||||
let position = entities.query_entity::<&entity::Position>(player_entity_id);
|
||||
|
||||
let player_chunk_pos = ChunkPos::from(position);
|
||||
world.get_chunk(&player_chunk_pos).is_some()
|
||||
}
|
||||
|
||||
/// Get a reference to our (potentially shared) world.
|
||||
///
|
||||
/// This gets the [`WeakWorld`] from our world container. If it's a normal
|
||||
|
@ -1083,12 +451,12 @@ impl Client {
|
|||
*self.entity_id.read()
|
||||
}
|
||||
|
||||
pub fn query<'w, 's, Param: SystemParam>(
|
||||
&'w self,
|
||||
) -> <Param::Fetch as bevy_ecs::system::SystemParamFetch<'w, 's>>::Item {
|
||||
let world = self.world();
|
||||
let mut ecs = &mut world.entities.write().ecs;
|
||||
SystemState::<Param>::new(ecs).get_mut(ecs)
|
||||
/// Query data of our player's entity.
|
||||
pub fn query<'w, 's, Q: WorldQuery>(&'w self) -> <Q as WorldQuery>::Item<'_> {
|
||||
let mut ecs = &mut self.world_container.write().ecs.lock();
|
||||
QueryState::<Q>::new(ecs)
|
||||
.get_mut(ecs, self.entity().into())
|
||||
.expect("Player entity should always exist when being queried")
|
||||
}
|
||||
|
||||
/// Returns whether we have a received the login packet yet.
|
||||
|
@ -1117,15 +485,11 @@ impl Client {
|
|||
client_information: ServerboundClientInformationPacket,
|
||||
) -> Result<(), std::io::Error> {
|
||||
{
|
||||
let mut client_information_lock = self.client_information.write();
|
||||
*client_information_lock = client_information;
|
||||
self.local_player_mut().client_information = client_information;
|
||||
}
|
||||
|
||||
if self.logged_in() {
|
||||
let client_information_packet = {
|
||||
let client_information = self.client_information.read();
|
||||
client_information.clone().get()
|
||||
};
|
||||
let client_information_packet = self.local_player().client_information.clone().get();
|
||||
log::debug!(
|
||||
"Sending client information (already logged in): {:?}",
|
||||
client_information_packet
|
||||
|
@ -1136,9 +500,3 @@ impl Client {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<std::sync::PoisonError<T>> for HandleError {
|
||||
fn from(e: std::sync::PoisonError<T>) -> Self {
|
||||
HandleError::Poison(e.to_string())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,13 +14,14 @@ mod account;
|
|||
mod chat;
|
||||
mod client;
|
||||
mod get_mc_dir;
|
||||
mod local_player;
|
||||
mod movement;
|
||||
pub mod ping;
|
||||
mod player;
|
||||
mod plugins;
|
||||
|
||||
pub use account::Account;
|
||||
pub use client::{ChatPacket, Client, ClientInformation, Event, JoinError, PhysicsState};
|
||||
pub use client::{ChatPacket, ClientInformation, Event, JoinError, LocalPlayer, PhysicsState};
|
||||
pub use movement::{SprintDirection, WalkDirection};
|
||||
pub use player::PlayerInfo;
|
||||
pub use plugins::{Plugin, PluginState, PluginStates, Plugins};
|
||||
|
|
751
azalea-client/src/local_player.rs
Normal file
751
azalea-client/src/local_player.rs
Normal file
|
@ -0,0 +1,751 @@
|
|||
use std::{collections::HashMap, io, sync::Arc};
|
||||
|
||||
use azalea_auth::game_profile::GameProfile;
|
||||
use azalea_core::{ChunkPos, ResourceLocation, Vec3};
|
||||
use azalea_protocol::{
|
||||
connect::{Connection, ReadConnection, WriteConnection},
|
||||
packets::game::{
|
||||
serverbound_keep_alive_packet::ServerboundKeepAlivePacket, ClientboundGamePacket,
|
||||
ServerboundGamePacket,
|
||||
},
|
||||
};
|
||||
use azalea_world::{
|
||||
entity::{self, metadata::PlayerMetadataBundle, EntityId},
|
||||
PartialWorld, WeakWorldContainer,
|
||||
};
|
||||
use bevy_ecs::{
|
||||
component::Component,
|
||||
event::EventReader,
|
||||
system::{Query, Res, ResMut},
|
||||
};
|
||||
use log::debug;
|
||||
use parking_lot::RwLock;
|
||||
use thiserror::Error;
|
||||
use tokio::sync::mpsc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{ChatPacket, ClientInformation, Event, PlayerInfo, WalkDirection};
|
||||
|
||||
/// A player that you control that is currently in a Minecraft server.
|
||||
#[derive(Component)]
|
||||
pub struct LocalPlayer {
|
||||
pub profile: GameProfile,
|
||||
// Arc<tokio::sync::Mutex<
|
||||
pub read_conn: ReadConnection<ClientboundGamePacket>,
|
||||
pub write_conn: WriteConnection<ServerboundGamePacket>,
|
||||
// pub world: Arc<RwLock<PartialWorld>>,
|
||||
pub physics_state: PhysicsState,
|
||||
pub client_information: ClientInformation,
|
||||
pub dead: bool,
|
||||
/// A map of player uuids to their information in the tab list
|
||||
pub players: HashMap<Uuid, PlayerInfo>,
|
||||
|
||||
pub world: Arc<RwLock<PartialWorld>>,
|
||||
pub world_name: Option<ResourceLocation>,
|
||||
|
||||
pub tx: mpsc::Sender<Event>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PhysicsState {
|
||||
/// Minecraft only sends a movement packet either after 20 ticks or if the
|
||||
/// player moved enough. This is that tick counter.
|
||||
pub position_remainder: u32,
|
||||
pub was_sprinting: bool,
|
||||
// Whether we're going to try to start sprinting this tick. Equivalent to
|
||||
// holding down ctrl for a tick.
|
||||
pub trying_to_sprint: bool,
|
||||
|
||||
pub move_direction: WalkDirection,
|
||||
pub forward_impulse: f32,
|
||||
pub left_impulse: f32,
|
||||
}
|
||||
|
||||
/// Marks a [`LocalPlayer`] that's in a loaded chunk. This is updated at the
|
||||
/// beginning of every tick.
|
||||
#[derive(Component)]
|
||||
pub struct LocalPlayerInLoadedChunk;
|
||||
|
||||
impl LocalPlayer {
|
||||
/// Create a new client from the given GameProfile, Connection, and World.
|
||||
/// You should only use this if you want to change these fields from the
|
||||
/// defaults, otherwise use [`Client::join`].
|
||||
pub fn new(
|
||||
profile: GameProfile,
|
||||
conn: Connection<ClientboundGamePacket, ServerboundGamePacket>,
|
||||
world: Arc<RwLock<PartialWorld>>,
|
||||
tx: mpsc::Sender<Event>,
|
||||
) -> Self {
|
||||
let (read_conn, write_conn) = conn.into_split();
|
||||
let (read_conn, write_conn) = (
|
||||
// Arc::new(tokio::sync::Mutex::new(read_conn)),
|
||||
// Arc::new(tokio::sync::Mutex::new(write_conn)),
|
||||
read_conn, write_conn,
|
||||
);
|
||||
|
||||
LocalPlayer {
|
||||
profile,
|
||||
read_conn,
|
||||
write_conn,
|
||||
physics_state: PhysicsState::default(),
|
||||
client_information: ClientInformation::default(),
|
||||
dead: false,
|
||||
players: HashMap::new(),
|
||||
world,
|
||||
tx,
|
||||
world_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a packet directly to the server.
|
||||
pub async fn write_packet_async(
|
||||
&mut self,
|
||||
packet: ServerboundGamePacket,
|
||||
) -> Result<(), std::io::Error> {
|
||||
self.write_conn.write(packet).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Spawn a task to write a packet directly to the server.
|
||||
pub fn write_packet(&mut self, packet: ServerboundGamePacket) {
|
||||
tokio::spawn(self.write_packet_async(packet));
|
||||
}
|
||||
|
||||
/// Update the [`LocalPlayerInLoadedChunk`] component for all
|
||||
/// [`LocalPlayer`]s.
|
||||
fn update_in_loaded_chunk(
|
||||
mut commands: bevy_ecs::system::Commands,
|
||||
query: Query<(entity::EcsEntityId, &LocalPlayer, &entity::Position)>,
|
||||
) {
|
||||
for (ecs_entity_id, local_player, position) in &query {
|
||||
let player_chunk_pos = ChunkPos::from(position);
|
||||
let in_loaded_chunk = local_player
|
||||
.world
|
||||
.read()
|
||||
.get_chunk(&player_chunk_pos)
|
||||
.is_some();
|
||||
if in_loaded_chunk {
|
||||
commands
|
||||
.entity(ecs_entity_id)
|
||||
.insert(LocalPlayerInLoadedChunk);
|
||||
} else {
|
||||
commands
|
||||
.entity(ecs_entity_id)
|
||||
.remove::<LocalPlayerInLoadedChunk>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn send_event(event: Event, tx: &mpsc::Sender<Event>) {
|
||||
tokio::spawn(tx.send(event));
|
||||
}
|
||||
|
||||
fn send_tick_event(query: Query<&LocalPlayer>) {
|
||||
for local_player in &query {
|
||||
let tx = local_player.tx.clone();
|
||||
Self::send_event(Event::Tick, &tx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_packet(
|
||||
mut event_reader: EventReader<(EntityId, ClientboundGamePacket)>,
|
||||
query: Query<(&mut LocalPlayer,)>,
|
||||
world_container: ResMut<WeakWorldContainer>,
|
||||
// client: &Client,
|
||||
// tx: &mpsc::Sender<Event>,
|
||||
) -> Result<(), HandlePacketError> {
|
||||
for (player_entity_id, packet) in event_reader.iter() {
|
||||
let (mut local_player,) = query.get_mut((*player_entity_id).into()).unwrap();
|
||||
|
||||
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()).await?;
|
||||
|
||||
// brand
|
||||
client
|
||||
.write_packet(
|
||||
ServerboundCustomPayloadPacket {
|
||||
identifier: ResourceLocation::new("brand").unwrap(),
|
||||
// they don't have to know :)
|
||||
data: "vanilla".into(),
|
||||
}
|
||||
.get(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.send(Event::Login).await?;
|
||||
}
|
||||
ClientboundGamePacket::SetChunkCacheRadius(p) => {
|
||||
debug!("Got set chunk cache radius packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::CustomPayload(p) => {
|
||||
debug!("Got custom payload packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::ChangeDifficulty(p) => {
|
||||
debug!("Got difficulty packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::Commands(_p) => {
|
||||
debug!("Got declare commands packet");
|
||||
}
|
||||
ClientboundGamePacket::PlayerAbilities(p) => {
|
||||
debug!("Got player abilities packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::SetCarriedItem(p) => {
|
||||
debug!("Got set carried item packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::UpdateTags(_p) => {
|
||||
debug!("Got update tags packet");
|
||||
}
|
||||
ClientboundGamePacket::Disconnect(p) => {
|
||||
debug!("Got disconnect packet {:?}", p);
|
||||
client.disconnect().await?;
|
||||
}
|
||||
ClientboundGamePacket::UpdateRecipes(_p) => {
|
||||
debug!("Got update recipes packet");
|
||||
}
|
||||
ClientboundGamePacket::EntityEvent(_p) => {
|
||||
// debug!("Got entity event packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::Recipe(_p) => {
|
||||
debug!("Got recipe packet");
|
||||
}
|
||||
ClientboundGamePacket::PlayerPosition(p) => {
|
||||
// TODO: reply with teleport confirm
|
||||
debug!("Got player position packet {:?}", p);
|
||||
|
||||
let (new_pos, y_rot, x_rot) = {
|
||||
let player_entity_id = *client.entity();
|
||||
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 delta_movement = physics.delta;
|
||||
|
||||
let is_x_relative = p.relative_arguments.x;
|
||||
let is_y_relative = p.relative_arguments.y;
|
||||
let is_z_relative = p.relative_arguments.z;
|
||||
|
||||
let (delta_x, new_pos_x) = if is_x_relative {
|
||||
physics.last_pos.x += p.x;
|
||||
(delta_movement.x, position.x + p.x)
|
||||
} else {
|
||||
physics.last_pos.x = p.x;
|
||||
(0.0, p.x)
|
||||
};
|
||||
let (delta_y, new_pos_y) = if is_y_relative {
|
||||
physics.last_pos.y += p.y;
|
||||
(delta_movement.y, position.y + p.y)
|
||||
} else {
|
||||
physics.last_pos.y = p.y;
|
||||
(0.0, p.y)
|
||||
};
|
||||
let (delta_z, new_pos_z) = if is_z_relative {
|
||||
physics.last_pos.z += p.z;
|
||||
(delta_movement.z, position.z + p.z)
|
||||
} else {
|
||||
physics.last_pos.z = p.z;
|
||||
(0.0, p.z)
|
||||
};
|
||||
|
||||
let mut y_rot = p.y_rot;
|
||||
let mut x_rot = p.x_rot;
|
||||
if p.relative_arguments.x_rot {
|
||||
x_rot += physics.x_rot;
|
||||
}
|
||||
if p.relative_arguments.y_rot {
|
||||
y_rot += physics.y_rot;
|
||||
}
|
||||
|
||||
physics.delta = Vec3 {
|
||||
x: delta_x,
|
||||
y: delta_y,
|
||||
z: delta_z,
|
||||
};
|
||||
entity::set_rotation(physics.into_inner(), y_rot, x_rot);
|
||||
// TODO: minecraft sets "xo", "yo", and "zo" here but idk what that means
|
||||
// so investigate that ig
|
||||
let new_pos = Vec3 {
|
||||
x: new_pos_x,
|
||||
y: new_pos_y,
|
||||
z: new_pos_z,
|
||||
};
|
||||
world
|
||||
.set_entity_pos(
|
||||
player_entity_id,
|
||||
new_pos,
|
||||
position.into_inner(),
|
||||
physics.into_inner(),
|
||||
)
|
||||
.expect("The player entity should always exist");
|
||||
|
||||
(new_pos, y_rot, x_rot)
|
||||
};
|
||||
|
||||
client
|
||||
.write_packet(ServerboundAcceptTeleportationPacket { id: p.id }.get())
|
||||
.await?;
|
||||
client
|
||||
.write_packet(
|
||||
ServerboundMovePlayerPosRotPacket {
|
||||
x: new_pos.x,
|
||||
y: new_pos.y,
|
||||
z: new_pos.z,
|
||||
y_rot,
|
||||
x_rot,
|
||||
// this is always false
|
||||
on_ground: false,
|
||||
}
|
||||
.get(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ClientboundGamePacket::PlayerInfoUpdate(p) => {
|
||||
debug!("Got player info packet {:?}", p);
|
||||
let mut events = Vec::new();
|
||||
{
|
||||
let mut players_lock = client.players.write();
|
||||
for updated_info in &p.entries {
|
||||
// add the new player maybe
|
||||
if p.actions.add_player {
|
||||
let player_info = PlayerInfo {
|
||||
profile: updated_info.profile.clone(),
|
||||
uuid: updated_info.profile.uuid,
|
||||
gamemode: updated_info.game_mode,
|
||||
latency: updated_info.latency,
|
||||
display_name: updated_info.display_name.clone(),
|
||||
};
|
||||
players_lock.insert(updated_info.profile.uuid, player_info.clone());
|
||||
events.push(Event::AddPlayer(player_info));
|
||||
} else if let Some(info) =
|
||||
players_lock.get_mut(&updated_info.profile.uuid)
|
||||
{
|
||||
// `else if` because the block for add_player above
|
||||
// already sets all the fields
|
||||
if p.actions.update_game_mode {
|
||||
info.gamemode = updated_info.game_mode;
|
||||
}
|
||||
if p.actions.update_latency {
|
||||
info.latency = updated_info.latency;
|
||||
}
|
||||
if p.actions.update_display_name {
|
||||
info.display_name = updated_info.display_name.clone();
|
||||
}
|
||||
events.push(Event::UpdatePlayer(info.clone()));
|
||||
} else {
|
||||
warn!(
|
||||
"Ignoring PlayerInfoUpdate for unknown player {}",
|
||||
updated_info.profile.uuid
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for event in events {
|
||||
tx.send(event).await?;
|
||||
}
|
||||
}
|
||||
ClientboundGamePacket::PlayerInfoRemove(p) => {
|
||||
let mut events = Vec::new();
|
||||
{
|
||||
let mut players_lock = client.players.write();
|
||||
for uuid in &p.profile_ids {
|
||||
if let Some(info) = players_lock.remove(uuid) {
|
||||
events.push(Event::RemovePlayer(info));
|
||||
}
|
||||
}
|
||||
}
|
||||
for event in events {
|
||||
tx.send(event).await?;
|
||||
}
|
||||
}
|
||||
ClientboundGamePacket::SetChunkCacheCenter(p) => {
|
||||
debug!("Got chunk cache center packet {:?}", p);
|
||||
client
|
||||
.world
|
||||
.write()
|
||||
.update_view_center(&ChunkPos::new(p.x, p.z));
|
||||
}
|
||||
ClientboundGamePacket::LevelChunkWithLight(p) => {
|
||||
// debug!("Got chunk with light packet {} {}", p.x, p.z);
|
||||
let pos = ChunkPos::new(p.x, p.z);
|
||||
|
||||
// OPTIMIZATION: if we already know about the chunk from the
|
||||
// shared world (and not ourselves), then we don't need to
|
||||
// parse it again. This is only used when we have a shared
|
||||
// world, since we check that the chunk isn't currently owned
|
||||
// by this client.
|
||||
let shared_has_chunk = client.world.read().get_chunk(&pos).is_some();
|
||||
let this_client_has_chunk =
|
||||
client.world.read().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(());
|
||||
}
|
||||
|
||||
// let chunk = Chunk::read_with_world_height(&mut p.chunk_data);
|
||||
// debug("chunk {:?}")
|
||||
if let Err(e) = client
|
||||
.world
|
||||
.write()
|
||||
.replace_with_packet_data(&pos, &mut Cursor::new(&p.chunk_data.data))
|
||||
{
|
||||
error!("Couldn't set chunk data: {}", e);
|
||||
}
|
||||
}
|
||||
ClientboundGamePacket::LightUpdate(_p) => {
|
||||
// debug!("Got light update packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::AddEntity(p) => {
|
||||
debug!("Got add entity packet {:?}", p);
|
||||
let 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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
ClientboundGamePacket::UpdateAttributes(_p) => {
|
||||
// debug!("Got update attributes packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::SetEntityMotion(_p) => {
|
||||
// debug!("Got entity velocity packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::SetEntityLink(p) => {
|
||||
debug!("Got set entity link packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::AddPlayer(p) => {
|
||||
debug!("Got add player packet {:?}", p);
|
||||
let 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
|
||||
}
|
||||
ClientboundGamePacket::InitializeBorder(p) => {
|
||||
debug!("Got initialize border packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::SetTime(p) => {
|
||||
debug!("Got set time packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::SetDefaultSpawnPosition(p) => {
|
||||
debug!("Got set default spawn position packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::ContainerSetContent(p) => {
|
||||
debug!("Got container set content packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::SetHealth(p) => {
|
||||
debug!("Got set health packet {:?}", p);
|
||||
if p.health == 0.0 {
|
||||
// we can't define a variable here with client.dead.lock()
|
||||
// because of https://github.com/rust-lang/rust/issues/57478
|
||||
if !*client.dead.lock() {
|
||||
*client.dead.lock() = true;
|
||||
tx.send(Event::Death(None)).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
ClientboundGamePacket::SetExperience(p) => {
|
||||
debug!("Got set experience packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::TeleportEntity(p) => {
|
||||
let mut world = 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,
|
||||
);
|
||||
}
|
||||
ClientboundGamePacket::UpdateAdvancements(p) => {
|
||||
debug!("Got update advancements packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::RotateHead(_p) => {
|
||||
// debug!("Got rotate head packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::MoveEntityPos(p) => {
|
||||
let mut world = client.world.write();
|
||||
let _ = world.move_entity_with_delta(EntityId(p.entity_id), &p.delta);
|
||||
}
|
||||
ClientboundGamePacket::MoveEntityPosRot(p) => {
|
||||
let mut world = client.world.write();
|
||||
let _ = world.move_entity_with_delta(EntityId(p.entity_id), &p.delta);
|
||||
}
|
||||
ClientboundGamePacket::MoveEntityRot(_p) => {
|
||||
// debug!("Got move entity rot packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::KeepAlive(p) => {
|
||||
debug!("Got keep alive packet {:?}", p);
|
||||
client
|
||||
.write_packet(ServerboundKeepAlivePacket { id: p.id }.get())
|
||||
.await?;
|
||||
}
|
||||
ClientboundGamePacket::RemoveEntities(p) => {
|
||||
debug!("Got remove entities packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::PlayerChat(p) => {
|
||||
debug!("Got player chat packet {:?}", p);
|
||||
tx.send(Event::Chat(ChatPacket::Player(Arc::new(p.clone()))))
|
||||
.await?;
|
||||
}
|
||||
ClientboundGamePacket::SystemChat(p) => {
|
||||
debug!("Got system chat packet {:?}", p);
|
||||
tx.send(Event::Chat(ChatPacket::System(Arc::new(p.clone()))))
|
||||
.await?;
|
||||
}
|
||||
ClientboundGamePacket::Sound(_p) => {
|
||||
// debug!("Got sound packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::LevelEvent(p) => {
|
||||
debug!("Got level event packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::BlockUpdate(p) => {
|
||||
debug!("Got block update packet {:?}", p);
|
||||
let mut world = client.world.write();
|
||||
world.set_block_state(&p.pos, p.block_state);
|
||||
}
|
||||
ClientboundGamePacket::Animate(p) => {
|
||||
debug!("Got animate packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::SectionBlocksUpdate(p) => {
|
||||
debug!("Got section blocks update packet {:?}", p);
|
||||
let mut world = client.world.write();
|
||||
for state in &p.states {
|
||||
world.set_block_state(&(p.section_pos + state.pos.clone()), state.state);
|
||||
}
|
||||
}
|
||||
ClientboundGamePacket::GameEvent(p) => {
|
||||
debug!("Got game event packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::LevelParticles(p) => {
|
||||
debug!("Got level particles packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::ServerData(p) => {
|
||||
debug!("Got server data packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::SetEquipment(p) => {
|
||||
debug!("Got set equipment packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::UpdateMobEffect(p) => {
|
||||
debug!("Got update mob effect packet {:?}", p);
|
||||
}
|
||||
ClientboundGamePacket::AddExperienceOrb(_) => {}
|
||||
ClientboundGamePacket::AwardStats(_) => {}
|
||||
ClientboundGamePacket::BlockChangedAck(_) => {}
|
||||
ClientboundGamePacket::BlockDestruction(_) => {}
|
||||
ClientboundGamePacket::BlockEntityData(_) => {}
|
||||
ClientboundGamePacket::BlockEvent(_) => {}
|
||||
ClientboundGamePacket::BossEvent(_) => {}
|
||||
ClientboundGamePacket::CommandSuggestions(_) => {}
|
||||
ClientboundGamePacket::ContainerSetData(_) => {}
|
||||
ClientboundGamePacket::ContainerSetSlot(_) => {}
|
||||
ClientboundGamePacket::Cooldown(_) => {}
|
||||
ClientboundGamePacket::CustomChatCompletions(_) => {}
|
||||
ClientboundGamePacket::DeleteChat(_) => {}
|
||||
ClientboundGamePacket::Explode(_) => {}
|
||||
ClientboundGamePacket::ForgetLevelChunk(_) => {}
|
||||
ClientboundGamePacket::HorseScreenOpen(_) => {}
|
||||
ClientboundGamePacket::MapItemData(_) => {}
|
||||
ClientboundGamePacket::MerchantOffers(_) => {}
|
||||
ClientboundGamePacket::MoveVehicle(_) => {}
|
||||
ClientboundGamePacket::OpenBook(_) => {}
|
||||
ClientboundGamePacket::OpenScreen(_) => {}
|
||||
ClientboundGamePacket::OpenSignEditor(_) => {}
|
||||
ClientboundGamePacket::Ping(_) => {}
|
||||
ClientboundGamePacket::PlaceGhostRecipe(_) => {}
|
||||
ClientboundGamePacket::PlayerCombatEnd(_) => {}
|
||||
ClientboundGamePacket::PlayerCombatEnter(_) => {}
|
||||
ClientboundGamePacket::PlayerCombatKill(p) => {
|
||||
debug!("Got player kill packet {:?}", p);
|
||||
if client.entity() == EntityId(p.player_id) {
|
||||
// we can't define a variable here with client.dead.lock()
|
||||
// because of https://github.com/rust-lang/rust/issues/57478
|
||||
if !*client.dead.lock() {
|
||||
*client.dead.lock() = true;
|
||||
tx.send(Event::Death(Some(Arc::new(p.clone())))).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
ClientboundGamePacket::PlayerLookAt(_) => {}
|
||||
ClientboundGamePacket::RemoveMobEffect(_) => {}
|
||||
ClientboundGamePacket::ResourcePack(_) => {}
|
||||
ClientboundGamePacket::Respawn(p) => {
|
||||
debug!("Got respawn packet {:?}", p);
|
||||
// Sets clients dead state to false.
|
||||
let mut dead_lock = client.dead.lock();
|
||||
*dead_lock = false;
|
||||
}
|
||||
ClientboundGamePacket::SelectAdvancementsTab(_) => {}
|
||||
ClientboundGamePacket::SetActionBarText(_) => {}
|
||||
ClientboundGamePacket::SetBorderCenter(_) => {}
|
||||
ClientboundGamePacket::SetBorderLerpSize(_) => {}
|
||||
ClientboundGamePacket::SetBorderSize(_) => {}
|
||||
ClientboundGamePacket::SetBorderWarningDelay(_) => {}
|
||||
ClientboundGamePacket::SetBorderWarningDistance(_) => {}
|
||||
ClientboundGamePacket::SetCamera(_) => {}
|
||||
ClientboundGamePacket::SetDisplayObjective(_) => {}
|
||||
ClientboundGamePacket::SetObjective(_) => {}
|
||||
ClientboundGamePacket::SetPassengers(_) => {}
|
||||
ClientboundGamePacket::SetPlayerTeam(_) => {}
|
||||
ClientboundGamePacket::SetScore(_) => {}
|
||||
ClientboundGamePacket::SetSimulationDistance(_) => {}
|
||||
ClientboundGamePacket::SetSubtitleText(_) => {}
|
||||
ClientboundGamePacket::SetTitleText(_) => {}
|
||||
ClientboundGamePacket::SetTitlesAnimation(_) => {}
|
||||
ClientboundGamePacket::SoundEntity(_) => {}
|
||||
ClientboundGamePacket::StopSound(_) => {}
|
||||
ClientboundGamePacket::TabList(_) => {}
|
||||
ClientboundGamePacket::TagQuery(_) => {}
|
||||
ClientboundGamePacket::TakeItemEntity(_) => {}
|
||||
ClientboundGamePacket::DisguisedChat(_) => {}
|
||||
ClientboundGamePacket::UpdateEnabledFeatures(_) => {}
|
||||
ClientboundGamePacket::ContainerClose(_) => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum HandlePacketError {
|
||||
#[error("{0}")]
|
||||
Poison(String),
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
#[error("{0}")]
|
||||
Send(#[from] mpsc::error::SendError<Event>),
|
||||
}
|
||||
|
||||
impl<T> From<std::sync::PoisonError<T>> for HandlePacketError {
|
||||
fn from(e: std::sync::PoisonError<T>) -> Self {
|
||||
HandlePacketError::Poison(e.to_string())
|
||||
}
|
||||
}
|
|
@ -1,8 +1,6 @@
|
|||
use std::backtrace::Backtrace;
|
||||
|
||||
use crate::{Client, PhysicsState};
|
||||
use crate::client::{Client, LocalPlayerInLoadedChunk};
|
||||
use crate::{LocalPlayer, PhysicsState};
|
||||
use azalea_core::Vec3;
|
||||
use azalea_physics::collision::{move_colliding, MoverType};
|
||||
use azalea_protocol::packets::game::serverbound_player_command_packet::ServerboundPlayerCommandPacket;
|
||||
use azalea_protocol::packets::game::{
|
||||
serverbound_move_player_pos_packet::ServerboundMovePlayerPosPacket,
|
||||
|
@ -10,8 +8,10 @@ use azalea_protocol::packets::game::{
|
|||
serverbound_move_player_rot_packet::ServerboundMovePlayerRotPacket,
|
||||
serverbound_move_player_status_only_packet::ServerboundMovePlayerStatusOnlyPacket,
|
||||
};
|
||||
use azalea_world::entity::EcsEntityId;
|
||||
use azalea_world::{entity, MoveEntityError};
|
||||
use bevy_ecs::system::Query;
|
||||
use std::backtrace::Backtrace;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
|
@ -33,124 +33,147 @@ impl From<MoveEntityError> for MovePlayerError {
|
|||
}
|
||||
|
||||
impl Client {
|
||||
/// Set whether we're jumping. This acts as if you held space in
|
||||
/// vanilla. If you want to jump once, use the `jump` function.
|
||||
///
|
||||
/// If you're making a realistic client, calling this function every tick is
|
||||
/// recommended.
|
||||
pub fn set_jumping(&mut self, jumping: bool) {
|
||||
let physics = self.query::<&mut entity::Physics>();
|
||||
physics.jumping = jumping;
|
||||
}
|
||||
|
||||
/// Returns whether the player will try to jump next tick.
|
||||
pub fn jumping(&self) -> bool {
|
||||
let physics = self.query::<&mut entity::Physics>();
|
||||
physics.jumping
|
||||
}
|
||||
|
||||
/// Sets your rotation. `y_rot` is yaw (looking to the side), `x_rot` is
|
||||
/// pitch (looking up and down). You can get these numbers from the vanilla
|
||||
/// f3 screen.
|
||||
/// `y_rot` goes from -180 to 180, and `x_rot` goes from -90 to 90.
|
||||
pub fn set_rotation(&mut self, y_rot: f32, x_rot: f32) {
|
||||
let mut physics = self.query::<&mut entity::Physics>();
|
||||
|
||||
entity::set_rotation(&mut physics, y_rot, x_rot);
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalPlayer {
|
||||
/// This gets called automatically every tick.
|
||||
pub(crate) fn send_position(
|
||||
&mut self,
|
||||
mut query: Query<(
|
||||
&entity::Position,
|
||||
&mut entity::Physics,
|
||||
&entity::metadata::Sprinting,
|
||||
)>,
|
||||
) -> Result<(), MovePlayerError> {
|
||||
let (player_pos, mut physics, sprinting) = query
|
||||
.get_mut((*self.entity_id.read()).into())
|
||||
.expect("Player should always be in world");
|
||||
mut query: Query<
|
||||
(
|
||||
EcsEntityId,
|
||||
&LocalPlayer,
|
||||
&entity::Position,
|
||||
&mut entity::Physics,
|
||||
&entity::metadata::Sprinting,
|
||||
),
|
||||
&LocalPlayerInLoadedChunk,
|
||||
>,
|
||||
) {
|
||||
for (entity, local_player, position, physics, sprinting) in &query {
|
||||
local_player.send_sprinting_if_needed(
|
||||
entity.into(),
|
||||
sprinting,
|
||||
&mut local_player.physics_state,
|
||||
);
|
||||
|
||||
let mut physics_state = self.physics_state.lock();
|
||||
let packet = {
|
||||
// TODO: the camera being able to be controlled by other entities isn't
|
||||
// implemented yet if !self.is_controlled_camera() { return };
|
||||
|
||||
self.send_sprinting_if_needed(sprinting, &mut physics_state)?;
|
||||
let old_position = physics.last_pos;
|
||||
|
||||
let packet = {
|
||||
// TODO: the camera being able to be controlled by other entities isn't
|
||||
// implemented yet if !self.is_controlled_camera() { return };
|
||||
let x_delta = position.x - old_position.x;
|
||||
let y_delta = position.y - old_position.y;
|
||||
let z_delta = position.z - old_position.z;
|
||||
let y_rot_delta = (physics.y_rot - physics.y_rot_last) as f64;
|
||||
let x_rot_delta = (physics.x_rot - physics.x_rot_last) as f64;
|
||||
|
||||
// i don't like this
|
||||
let entity_storage_lock = self.world().entities.clone();
|
||||
let mut entity_storage = entity_storage_lock.write();
|
||||
let (player_pos, mut physics) = entity_storage
|
||||
.query_entity_mut::<(&entity::Position, &mut entity::Physics)>(
|
||||
*self.entity_id.read(),
|
||||
);
|
||||
local_player.physics_state.position_remainder += 1;
|
||||
|
||||
let player_old_pos = physics.last_pos;
|
||||
// boolean sendingPosition = Mth.lengthSquared(xDelta, yDelta, zDelta) >
|
||||
// Mth.square(2.0E-4D) || this.positionReminder >= 20;
|
||||
let sending_position = ((x_delta.powi(2) + y_delta.powi(2) + z_delta.powi(2))
|
||||
> 2.0e-4f64.powi(2))
|
||||
|| local_player.physics_state.position_remainder >= 20;
|
||||
let sending_rotation = y_rot_delta != 0.0 || x_rot_delta != 0.0;
|
||||
|
||||
let x_delta = player_pos.x - player_old_pos.x;
|
||||
let y_delta = player_pos.y - player_old_pos.y;
|
||||
let z_delta = player_pos.z - player_old_pos.z;
|
||||
let y_rot_delta = (physics.y_rot - physics.y_rot_last) as f64;
|
||||
let x_rot_delta = (physics.x_rot - physics.x_rot_last) as f64;
|
||||
// if self.is_passenger() {
|
||||
// TODO: posrot packet for being a passenger
|
||||
// }
|
||||
let packet = if sending_position && sending_rotation {
|
||||
Some(
|
||||
ServerboundMovePlayerPosRotPacket {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
z: position.z,
|
||||
x_rot: physics.x_rot,
|
||||
y_rot: physics.y_rot,
|
||||
on_ground: physics.on_ground,
|
||||
}
|
||||
.get(),
|
||||
)
|
||||
} else if sending_position {
|
||||
Some(
|
||||
ServerboundMovePlayerPosPacket {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
z: position.z,
|
||||
on_ground: physics.on_ground,
|
||||
}
|
||||
.get(),
|
||||
)
|
||||
} else if sending_rotation {
|
||||
Some(
|
||||
ServerboundMovePlayerRotPacket {
|
||||
x_rot: physics.x_rot,
|
||||
y_rot: physics.y_rot,
|
||||
on_ground: physics.on_ground,
|
||||
}
|
||||
.get(),
|
||||
)
|
||||
} else if physics.last_on_ground != physics.on_ground {
|
||||
Some(
|
||||
ServerboundMovePlayerStatusOnlyPacket {
|
||||
on_ground: physics.on_ground,
|
||||
}
|
||||
.get(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
physics_state.position_remainder += 1;
|
||||
if sending_position {
|
||||
physics.last_pos = **position;
|
||||
local_player.physics_state.position_remainder = 0;
|
||||
}
|
||||
if sending_rotation {
|
||||
physics.y_rot_last = physics.y_rot;
|
||||
physics.x_rot_last = physics.x_rot;
|
||||
}
|
||||
|
||||
// boolean sendingPosition = Mth.lengthSquared(xDelta, yDelta, zDelta) >
|
||||
// Mth.square(2.0E-4D) || this.positionReminder >= 20;
|
||||
let sending_position = ((x_delta.powi(2) + y_delta.powi(2) + z_delta.powi(2))
|
||||
> 2.0e-4f64.powi(2))
|
||||
|| physics_state.position_remainder >= 20;
|
||||
let sending_rotation = y_rot_delta != 0.0 || x_rot_delta != 0.0;
|
||||
physics.last_on_ground = physics.on_ground;
|
||||
// minecraft checks for autojump here, but also autojump is bad so
|
||||
|
||||
// if self.is_passenger() {
|
||||
// TODO: posrot packet for being a passenger
|
||||
// }
|
||||
let packet = if sending_position && sending_rotation {
|
||||
Some(
|
||||
ServerboundMovePlayerPosRotPacket {
|
||||
x: player_pos.x,
|
||||
y: player_pos.y,
|
||||
z: player_pos.z,
|
||||
x_rot: physics.x_rot,
|
||||
y_rot: physics.y_rot,
|
||||
on_ground: physics.on_ground,
|
||||
}
|
||||
.get(),
|
||||
)
|
||||
} else if sending_position {
|
||||
Some(
|
||||
ServerboundMovePlayerPosPacket {
|
||||
x: player_pos.x,
|
||||
y: player_pos.y,
|
||||
z: player_pos.z,
|
||||
on_ground: physics.on_ground,
|
||||
}
|
||||
.get(),
|
||||
)
|
||||
} else if sending_rotation {
|
||||
Some(
|
||||
ServerboundMovePlayerRotPacket {
|
||||
x_rot: physics.x_rot,
|
||||
y_rot: physics.y_rot,
|
||||
on_ground: physics.on_ground,
|
||||
}
|
||||
.get(),
|
||||
)
|
||||
} else if physics.last_on_ground != physics.on_ground {
|
||||
Some(
|
||||
ServerboundMovePlayerStatusOnlyPacket {
|
||||
on_ground: physics.on_ground,
|
||||
}
|
||||
.get(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
packet
|
||||
};
|
||||
|
||||
if sending_position {
|
||||
physics.last_pos = **player_pos;
|
||||
physics_state.position_remainder = 0;
|
||||
if let Some(packet) = packet {
|
||||
tokio::spawn(local_player.write_packet(packet));
|
||||
}
|
||||
if sending_rotation {
|
||||
physics.y_rot_last = physics.y_rot;
|
||||
physics.x_rot_last = physics.x_rot;
|
||||
}
|
||||
|
||||
physics.last_on_ground = physics.on_ground;
|
||||
// minecraft checks for autojump here, but also autojump is bad so
|
||||
|
||||
packet
|
||||
};
|
||||
|
||||
if let Some(packet) = packet {
|
||||
tokio::spawn(self.write_packet(packet));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_sprinting_if_needed(
|
||||
&mut self,
|
||||
entity: entity::EntityId,
|
||||
sprinting: &entity::metadata::Sprinting,
|
||||
physics_state: &mut PhysicsState,
|
||||
) -> Result<(), MovePlayerError> {
|
||||
) {
|
||||
let was_sprinting = physics_state.was_sprinting;
|
||||
if **sprinting != was_sprinting {
|
||||
let sprinting_action = if **sprinting {
|
||||
|
@ -158,11 +181,10 @@ impl Client {
|
|||
} else {
|
||||
azalea_protocol::packets::game::serverbound_player_command_packet::Action::StopSprinting
|
||||
};
|
||||
let player_entity_id = *self.entity_id.read();
|
||||
tokio::spawn(
|
||||
self.write_packet(
|
||||
ServerboundPlayerCommandPacket {
|
||||
id: *player_entity_id,
|
||||
id: *entity,
|
||||
action: sprinting_action,
|
||||
data: 0,
|
||||
}
|
||||
|
@ -171,82 +193,77 @@ impl Client {
|
|||
);
|
||||
physics_state.was_sprinting = **sprinting;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Set our current position to the provided Vec3, potentially clipping through
|
||||
// blocks.
|
||||
pub async fn set_position(&mut self, new_pos: Vec3) -> Result<(), MovePlayerError> {
|
||||
let player_entity_id = *self.entity_id.read();
|
||||
let mut world_lock = self.world.write();
|
||||
|
||||
world_lock.set_entity_pos(player_entity_id, new_pos)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Makes the bot do one physics tick. Note that this is already handled
|
||||
/// automatically by the client.
|
||||
pub fn ai_step(
|
||||
&mut self,
|
||||
query: Query<(
|
||||
&mut entity::Physics,
|
||||
&mut entity::Position,
|
||||
&mut entity::metadata::Sprinting,
|
||||
&entity::Attributes,
|
||||
)>,
|
||||
query: Query<
|
||||
(
|
||||
EcsEntityId,
|
||||
&mut LocalPlayer,
|
||||
&mut entity::Physics,
|
||||
&mut entity::Position,
|
||||
&mut entity::metadata::Sprinting,
|
||||
&mut entity::Attributes,
|
||||
),
|
||||
&LocalPlayerInLoadedChunk,
|
||||
>,
|
||||
) {
|
||||
self.tick_controls(None);
|
||||
|
||||
let (mut physics, mut position, mut sprinting, attributes) =
|
||||
query.get_mut((*self.entity_id.read()).into()).unwrap();
|
||||
|
||||
// server ai step
|
||||
for (
|
||||
ecs_entity_id,
|
||||
mut local_player,
|
||||
mut physics,
|
||||
mut position,
|
||||
mut sprinting,
|
||||
mut attributes,
|
||||
) in &mut query
|
||||
{
|
||||
let physics_state = self.physics_state.lock();
|
||||
let physics_state = &mut local_player.physics_state;
|
||||
|
||||
Self::tick_controls(None, physics_state);
|
||||
|
||||
// server ai step
|
||||
physics.xxa = physics_state.left_impulse;
|
||||
physics.zza = physics_state.forward_impulse;
|
||||
}
|
||||
|
||||
// TODO: food data and abilities
|
||||
// let has_enough_food_to_sprint = self.food_data().food_level ||
|
||||
// self.abilities().may_fly;
|
||||
let has_enough_food_to_sprint = true;
|
||||
// TODO: food data and abilities
|
||||
// let has_enough_food_to_sprint = self.food_data().food_level ||
|
||||
// self.abilities().may_fly;
|
||||
let has_enough_food_to_sprint = true;
|
||||
|
||||
// TODO: double tapping w to sprint i think
|
||||
// TODO: double tapping w to sprint i think
|
||||
|
||||
let trying_to_sprint = self.physics_state.lock().trying_to_sprint;
|
||||
let trying_to_sprint = physics_state.trying_to_sprint;
|
||||
|
||||
if !self.sprinting()
|
||||
&& (
|
||||
// !self.is_in_water()
|
||||
// || self.is_underwater() &&
|
||||
self.has_enough_impulse_to_start_sprinting()
|
||||
if !**sprinting
|
||||
&& (
|
||||
// !self.is_in_water()
|
||||
// || self.is_underwater() &&
|
||||
Self::has_enough_impulse_to_start_sprinting(physics_state)
|
||||
&& has_enough_food_to_sprint
|
||||
// && !self.using_item()
|
||||
// && !self.has_effect(MobEffects.BLINDNESS)
|
||||
&& trying_to_sprint
|
||||
)
|
||||
{
|
||||
self.set_sprinting(true);
|
||||
}
|
||||
)
|
||||
{
|
||||
Self::set_sprinting(true, &mut sprinting, &mut attributes);
|
||||
}
|
||||
|
||||
azalea_physics::ai_step(
|
||||
*self.entity_id.read(),
|
||||
&self.world(),
|
||||
&mut physics,
|
||||
&mut position,
|
||||
&sprinting,
|
||||
attributes,
|
||||
);
|
||||
azalea_physics::ai_step(
|
||||
ecs_entity_id.into(),
|
||||
&local_player.world.read().shared,
|
||||
&mut physics,
|
||||
&mut position,
|
||||
&sprinting,
|
||||
&attributes,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the impulse from self.move_direction. The multipler is used for
|
||||
/// sneaking.
|
||||
pub(crate) fn tick_controls(&mut self, multiplier: Option<f32>) {
|
||||
let mut physics_state = self.physics_state.lock();
|
||||
|
||||
pub(crate) fn tick_controls(multiplier: Option<f32>, physics_state: &mut PhysicsState) {
|
||||
let mut forward_impulse: f32 = 0.;
|
||||
let mut left_impulse: f32 = 0.;
|
||||
let move_direction = physics_state.move_direction;
|
||||
|
@ -295,13 +312,15 @@ impl Client {
|
|||
/// bot.walk(WalkDirection::None);
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn walk(&mut self, direction: WalkDirection) {
|
||||
{
|
||||
let mut physics_state = self.physics_state.lock();
|
||||
physics_state.move_direction = direction;
|
||||
}
|
||||
pub fn walk(
|
||||
direction: WalkDirection,
|
||||
physics_state: &mut PhysicsState,
|
||||
sprinting: &mut entity::metadata::Sprinting,
|
||||
attributes: &mut entity::Attributes,
|
||||
) {
|
||||
physics_state.move_direction = direction;
|
||||
|
||||
self.set_sprinting(false);
|
||||
Self::set_sprinting(false, sprinting, attributes);
|
||||
}
|
||||
|
||||
/// Start sprinting in the given direction. To stop moving, call
|
||||
|
@ -319,70 +338,38 @@ impl Client {
|
|||
/// bot.walk(WalkDirection::None);
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn sprint(&mut self, direction: SprintDirection) {
|
||||
let mut physics_state = self.physics_state.lock();
|
||||
pub fn sprint(&mut self, direction: SprintDirection, physics_state: &mut PhysicsState) {
|
||||
physics_state.move_direction = WalkDirection::from(direction);
|
||||
physics_state.trying_to_sprint = true;
|
||||
}
|
||||
|
||||
// Whether we're currently sprinting.
|
||||
pub fn sprinting(&self) -> bool {
|
||||
self.entity().metadata.sprinting
|
||||
}
|
||||
|
||||
/// Change whether we're sprinting by adding an attribute modifier to the
|
||||
/// player. You should use the [`walk`] and [`sprint`] methods instead.
|
||||
/// Returns if the operation was successful.
|
||||
fn set_sprinting(&mut self, sprinting: bool) -> bool {
|
||||
let mut player_entity = self.entity();
|
||||
player_entity.metadata.sprinting = sprinting;
|
||||
fn set_sprinting(
|
||||
sprinting: bool,
|
||||
currently_sprinting: &mut entity::metadata::Sprinting,
|
||||
attributes: &mut entity::Attributes,
|
||||
) -> bool {
|
||||
**currently_sprinting = sprinting;
|
||||
if sprinting {
|
||||
player_entity
|
||||
.attributes
|
||||
attributes
|
||||
.speed
|
||||
.insert(azalea_world::entity::attributes::sprinting_modifier())
|
||||
.insert(entity::attributes::sprinting_modifier())
|
||||
.is_ok()
|
||||
} else {
|
||||
player_entity
|
||||
.attributes
|
||||
attributes
|
||||
.speed
|
||||
.remove(&azalea_world::entity::attributes::sprinting_modifier().uuid)
|
||||
.remove(&entity::attributes::sprinting_modifier().uuid)
|
||||
.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
/// Set whether we're jumping. This acts as if you held space in
|
||||
/// vanilla. If you want to jump once, use the `jump` function.
|
||||
///
|
||||
/// If you're making a realistic client, calling this function every tick is
|
||||
/// recommended.
|
||||
pub fn set_jumping(&mut self, jumping: bool) {
|
||||
let mut player_entity = self.entity();
|
||||
let physics = self.query::<Query<&entity::Physics>>();
|
||||
player_entity.jumping = jumping;
|
||||
}
|
||||
|
||||
/// Returns whether the player will try to jump next tick.
|
||||
pub fn jumping(&self) -> bool {
|
||||
let player_entity = self.entity();
|
||||
player_entity.jumping
|
||||
}
|
||||
|
||||
/// Sets your rotation. `y_rot` is yaw (looking to the side), `x_rot` is
|
||||
/// pitch (looking up and down). You can get these numbers from the vanilla
|
||||
/// f3 screen.
|
||||
/// `y_rot` goes from -180 to 180, and `x_rot` goes from -90 to 90.
|
||||
pub fn set_rotation(&mut self, y_rot: f32, x_rot: f32) {
|
||||
let mut player_entity = self.entity();
|
||||
player_entity.set_rotation(y_rot, x_rot);
|
||||
}
|
||||
|
||||
// Whether the player is moving fast enough to be able to start sprinting.
|
||||
fn has_enough_impulse_to_start_sprinting(&self) -> bool {
|
||||
fn has_enough_impulse_to_start_sprinting(physics_state: &PhysicsState) -> bool {
|
||||
// if self.underwater() {
|
||||
// self.has_forward_impulse()
|
||||
// } else {
|
||||
let physics_state = self.physics_state.lock();
|
||||
physics_state.forward_impulse > 0.8
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{Client, Event};
|
||||
use crate::{Event, LocalPlayer};
|
||||
use async_trait::async_trait;
|
||||
use nohash_hasher::NoHashHasher;
|
||||
use std::{
|
||||
|
@ -81,7 +81,7 @@ impl IntoIterator for PluginStates {
|
|||
/// fields must be atomic. Unique `PluginState`s are built from [`Plugin`]s.
|
||||
#[async_trait]
|
||||
pub trait PluginState: Send + Sync + PluginStateClone + Any + 'static {
|
||||
async fn handle(self: Box<Self>, event: Event, bot: Client);
|
||||
async fn handle(self: Box<Self>, event: Event, bot: LocalPlayer);
|
||||
}
|
||||
|
||||
/// Plugins can keep their own personal state, listen to [`Event`]s, and add
|
||||
|
|
|
@ -115,7 +115,7 @@ pub fn move_colliding(
|
|||
}
|
||||
};
|
||||
|
||||
world.set_entity_pos_from_refs(entity_id, new_pos, position, physics)?;
|
||||
world.set_entity_pos(entity_id, new_pos, position, physics);
|
||||
}
|
||||
|
||||
let x_collision = movement.x != collide_result.x;
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use crate::WeakWorld;
|
||||
use azalea_core::ResourceLocation;
|
||||
use bevy_ecs::system::Resource;
|
||||
use log::error;
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Weak},
|
||||
|
@ -8,15 +10,19 @@ use std::{
|
|||
|
||||
/// A container of [`WeakWorld`]s. Worlds are stored as a Weak pointer here, so
|
||||
/// if no clients are using a world it will be forgotten.
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Resource)]
|
||||
pub struct WeakWorldContainer {
|
||||
pub worlds: HashMap<ResourceLocation, Weak<WeakWorld>>,
|
||||
|
||||
/// The ECS world that contains all of the entities in all of the worlds.
|
||||
pub ecs: Arc<Mutex<bevy_ecs::world::World>>,
|
||||
}
|
||||
|
||||
impl WeakWorldContainer {
|
||||
pub fn new() -> Self {
|
||||
WeakWorldContainer {
|
||||
worlds: HashMap::new(),
|
||||
ecs: Arc::new(Mutex::new(bevy_ecs::world::World::new())),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,7 +52,7 @@ impl WeakWorldContainer {
|
|||
}
|
||||
existing
|
||||
} else {
|
||||
let world = Arc::new(WeakWorld::new(height, min_y));
|
||||
let world = Arc::new(WeakWorld::new(height, min_y, self.ecs.clone()));
|
||||
self.worlds.insert(name, Arc::downgrade(&world));
|
||||
world
|
||||
}
|
||||
|
|
|
@ -12,10 +12,7 @@ use bevy_ecs::{bundle::Bundle, component::Component, world::EntityMut};
|
|||
pub use data::*;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
pub use dimensions::EntityDimensions;
|
||||
use std::{
|
||||
fmt::{Debug, Display, Formatter},
|
||||
ops::Deref,
|
||||
};
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// An entity ID that's used by ECS library.
|
||||
|
@ -31,6 +28,11 @@ impl From<EntityId> for EcsEntityId {
|
|||
Self::from_raw(*id)
|
||||
}
|
||||
}
|
||||
impl From<EcsEntityId> for EntityId {
|
||||
fn from(id: EcsEntityId) -> Self {
|
||||
Self(id.index())
|
||||
}
|
||||
}
|
||||
impl Debug for EntityId {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "EntityId({})", **self)
|
||||
|
|
158
azalea-world/src/entity_storage.rs → azalea-world/src/entity_info.rs
Executable file → Normal file
158
azalea-world/src/entity_storage.rs → azalea-world/src/entity_info.rs
Executable file → Normal file
|
@ -1,5 +1,5 @@
|
|||
use crate::entity::{new_entity, EcsEntityId, EntityId, EntityUuid, Position};
|
||||
use azalea_core::ChunkPos;
|
||||
use crate::entity::{self, new_entity, EcsEntityId, EntityId, EntityUuid, Position};
|
||||
use azalea_core::{ChunkPos, Vec3};
|
||||
use bevy_ecs::{
|
||||
query::{QueryEntityError, QueryState, ReadOnlyWorldQuery, WorldQuery},
|
||||
world::{EntityMut, EntityRef},
|
||||
|
@ -39,8 +39,8 @@ use uuid::Uuid;
|
|||
///
|
||||
/// You can access the shared storage with `world.shared.read()`.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PartialEntityStorage {
|
||||
pub shared: Arc<RwLock<WeakEntityStorage>>,
|
||||
pub struct PartialEntityInfos {
|
||||
pub shared: Arc<RwLock<WeakEntityInfos>>,
|
||||
|
||||
/// The entity id of the player that owns this partial world. This will
|
||||
/// make [`PartialWorld::entity_mut`] pretend the entity doesn't exist so
|
||||
|
@ -55,30 +55,25 @@ pub struct PartialEntityStorage {
|
|||
/// update entities twice on accident.
|
||||
pub updates_received: IntMap<EntityId, u32>,
|
||||
/// A set of all the entity ids in render distance.
|
||||
loaded_entity_ids: IntSet<EntityId>,
|
||||
pub(crate) loaded_entity_ids: IntSet<EntityId>,
|
||||
}
|
||||
|
||||
/// Weakly store entities in a world. If the entities aren't being referenced
|
||||
/// by anything else (like an [`PartialEntityStorage`]), they'll be forgotten.
|
||||
#[derive(Default)]
|
||||
pub struct WeakEntityStorage {
|
||||
/// The ECS world that actually contains the entities.
|
||||
pub ecs: bevy_ecs::world::World,
|
||||
|
||||
pub struct WeakEntityInfos {
|
||||
/// The number of `PartialWorld`s that have this entity loaded.
|
||||
/// (this is reference counting)
|
||||
entity_reference_count: IntMap<EntityId, usize>,
|
||||
pub(crate) entity_reference_count: IntMap<EntityId, usize>,
|
||||
/// An index of all the entity ids we know are in a chunk
|
||||
ids_by_chunk: HashMap<ChunkPos, IntSet<EntityId>>,
|
||||
pub(crate) ids_by_chunk: HashMap<ChunkPos, IntSet<EntityId>>,
|
||||
/// An index of entity ids by their UUIDs
|
||||
id_by_uuid: HashMap<Uuid, EntityId>,
|
||||
pub(crate) id_by_uuid: HashMap<Uuid, EntityId>,
|
||||
|
||||
/// The canonical number of updates we've gotten for every entity.
|
||||
pub updates_received: IntMap<EntityId, u32>,
|
||||
}
|
||||
|
||||
impl PartialEntityStorage {
|
||||
pub fn new(shared: Arc<RwLock<WeakEntityStorage>>, owner_entity_id: Option<EntityId>) -> Self {
|
||||
impl PartialEntityInfos {
|
||||
pub fn new(shared: Arc<RwLock<WeakEntityInfos>>, owner_entity_id: Option<EntityId>) -> Self {
|
||||
if let Some(owner_entity_id) = owner_entity_id {
|
||||
shared.write().updates_received.insert(owner_entity_id, 0);
|
||||
}
|
||||
|
@ -90,68 +85,6 @@ impl PartialEntityStorage {
|
|||
}
|
||||
}
|
||||
|
||||
/// Add an entity to the storage.
|
||||
#[inline]
|
||||
pub fn insert(&mut self, id: EntityId, bundle: impl bevy_ecs::bundle::Bundle) {
|
||||
let mut shared = self.shared.write();
|
||||
// if the entity is already in the shared world, we don't need to do
|
||||
// anything
|
||||
if shared.contains_id(id) {
|
||||
return;
|
||||
}
|
||||
|
||||
new_entity(&mut shared.ecs, id, bundle);
|
||||
|
||||
let mut query = shared.ecs.query::<(&EntityUuid, &Position)>();
|
||||
let (&uuid, &pos) = query.get(&mut shared.ecs, id.into()).unwrap();
|
||||
|
||||
// add the entity to the indexes
|
||||
shared
|
||||
.ids_by_chunk
|
||||
.entry(ChunkPos::from(&pos))
|
||||
.or_default()
|
||||
.insert(id);
|
||||
shared.id_by_uuid.insert(*uuid, id);
|
||||
self.loaded_entity_ids.insert(id);
|
||||
// set our updates_received to the shared updates_received, unless it's
|
||||
// not there in which case set both to 1
|
||||
if let Some(&shared_updates_received) = shared.updates_received.get(&id) {
|
||||
// 0 means we're never tracking updates for this entity
|
||||
if shared_updates_received != 0 || Some(id) == self.owner_entity_id {
|
||||
self.updates_received.insert(id, 1);
|
||||
}
|
||||
} else {
|
||||
shared.updates_received.insert(id, 1);
|
||||
self.updates_received.insert(id, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove an entity from this storage by its id. It will only be removed
|
||||
/// from the shared storage if there are no other references to it.
|
||||
#[inline]
|
||||
pub fn remove_by_id(&mut self, id: EntityId) {
|
||||
if self.loaded_entity_ids.remove(&id) {
|
||||
let mut shared = self.shared.write();
|
||||
|
||||
let mut query = shared.query_to_state::<(&Position, &EntityUuid)>();
|
||||
let (pos, uuid) = query.get(&mut shared.ecs, id.into()).expect(
|
||||
"If the entity was being loaded by this storage, it must be in the shared
|
||||
storage.",
|
||||
);
|
||||
let chunk = ChunkPos::from(pos);
|
||||
let uuid = **uuid;
|
||||
|
||||
// TODO: is this line actually necessary? test if it doesn't immediately
|
||||
// panic/deadlock without this line
|
||||
drop(query);
|
||||
|
||||
self.updates_received.remove(&id);
|
||||
shared.remove_entity_if_unused(id, uuid, chunk);
|
||||
} else {
|
||||
warn!("Tried to remove entity with id {id} but it was not found.")
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the entity with the given id is being loaded by this storage.
|
||||
/// If you want to check whether the entity is in the shared storage, use
|
||||
/// [`WeakEntityStorage::contains_id`].
|
||||
|
@ -206,23 +139,6 @@ impl PartialEntityStorage {
|
|||
self.limited_get_by_id(entity_id.0)
|
||||
}
|
||||
|
||||
/// Clear all entities in a chunk. This will not clear them from the
|
||||
/// shared storage, unless there are no other references to them.
|
||||
pub fn clear_chunk(&mut self, chunk: &ChunkPos) {
|
||||
let mut shared = self.shared.write();
|
||||
let mut query = shared.query_to_state::<&EntityUuid>();
|
||||
|
||||
if let Some(entities) = shared.ids_by_chunk.get(chunk).cloned() {
|
||||
for &id in entities.iter() {
|
||||
if self.loaded_entity_ids.remove(&id) {
|
||||
let uuid = **query.get(&shared.ecs, id.into()).unwrap();
|
||||
// maybe remove it from the storage
|
||||
shared.remove_entity_if_unused(id, uuid, *chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Move an entity from its old chunk to a new chunk.
|
||||
#[inline]
|
||||
pub fn update_entity_chunk(
|
||||
|
@ -241,10 +157,9 @@ impl PartialEntityStorage {
|
|||
/// reference to nothing.
|
||||
static EMPTY_ENTITY_ID_INTSET: Lazy<IntSet<EntityId>> = Lazy::new(|| IntSet::default());
|
||||
|
||||
impl WeakEntityStorage {
|
||||
impl WeakEntityInfos {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
ecs: bevy_ecs::world::World::new(),
|
||||
entity_reference_count: IntMap::default(),
|
||||
ids_by_chunk: HashMap::default(),
|
||||
id_by_uuid: HashMap::default(),
|
||||
|
@ -296,16 +211,10 @@ impl WeakEntityStorage {
|
|||
}
|
||||
}
|
||||
|
||||
/// Query the ecs to get a [`QueryState`]. You should probably use
|
||||
/// [`Self::query_entity`] or [`Self::query_entity_mut`] instead.
|
||||
pub fn query_to_state<Q: WorldQuery>(&mut self) -> QueryState<Q, ()> {
|
||||
self.ecs.query::<Q>()
|
||||
}
|
||||
|
||||
/// Whether the entity with the given id is in the shared storage.
|
||||
#[inline]
|
||||
pub fn contains_id(&self, id: EntityId) -> bool {
|
||||
self.ecs.get_entity(id.into()).is_some()
|
||||
pub fn contains_entity(&self, id: EntityId) -> bool {
|
||||
self.entity_reference_count.contains_key(&id)
|
||||
}
|
||||
|
||||
/// Returns a set of entities in the given chunk.
|
||||
|
@ -336,31 +245,26 @@ impl WeakEntityStorage {
|
|||
.insert(entity_id);
|
||||
}
|
||||
|
||||
/// This can only be called for read-only queries, see
|
||||
/// [`Self::query_entity_mut`] for write-queries.
|
||||
pub fn query_entity<'w, Q: WorldQuery>(
|
||||
&'w mut self,
|
||||
/// Set an entity's position in the world when we already have references
|
||||
/// to the [`Position`] and [`Physics`] components.
|
||||
pub fn set_entity_pos(
|
||||
&mut self,
|
||||
entity_id: EntityId,
|
||||
) -> bevy_ecs::query::ROQueryItem<'w, Q> {
|
||||
let mut query = self.query_to_state::<Q>();
|
||||
query.get(&self.ecs, entity_id.into()).unwrap()
|
||||
}
|
||||
|
||||
pub fn query_entity_mut<'w, Q: WorldQuery>(&'w mut self, entity_id: EntityId) -> Q::Item<'w> {
|
||||
let mut query = self.query_to_state::<Q>();
|
||||
query.get_mut(&mut self.ecs, entity_id.into()).unwrap()
|
||||
}
|
||||
|
||||
/// Returns an [`EntityMut`] for the given entity ID.
|
||||
///
|
||||
/// You only need this if you're going to be adding new components to the
|
||||
/// entity. Otherwise, use [`Self::query_entity_mut`].
|
||||
pub fn ecs_entity_mut(&mut self, entity_id: EntityId) -> Option<EntityMut> {
|
||||
self.ecs.get_entity_mut(entity_id.into())
|
||||
new_pos: Vec3,
|
||||
pos: &mut entity::Position,
|
||||
physics: &mut entity::Physics,
|
||||
) {
|
||||
let old_chunk = ChunkPos::from(&*pos);
|
||||
let new_chunk = ChunkPos::from(&new_pos);
|
||||
// this is fine because we update the chunk below
|
||||
unsafe { entity::move_unchecked(pos, physics, new_pos) };
|
||||
if old_chunk != new_chunk {
|
||||
self.update_entity_chunk(entity_id, &old_chunk, &new_chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for WeakEntityStorage {
|
||||
impl Debug for WeakEntityInfos {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("WeakEntityStorage")
|
||||
// .field("ecs", &self.ecs)
|
||||
|
@ -381,7 +285,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_store_entity() {
|
||||
let mut storage = PartialEntityStorage::default();
|
||||
let mut storage = PartialEntityInfos::default();
|
||||
assert!(storage.limited_get_by_id(0).is_none());
|
||||
assert!(storage.shared.read().get_by_id(0).is_none());
|
||||
|
|
@ -6,7 +6,7 @@ mod bit_storage;
|
|||
mod chunk_storage;
|
||||
mod container;
|
||||
pub mod entity;
|
||||
mod entity_storage;
|
||||
mod entity_info;
|
||||
mod palette;
|
||||
mod world;
|
||||
|
||||
|
@ -15,7 +15,7 @@ use std::backtrace::Backtrace;
|
|||
pub use bit_storage::BitStorage;
|
||||
pub use chunk_storage::{Chunk, ChunkStorage, PartialChunkStorage, WeakChunkStorage};
|
||||
pub use container::*;
|
||||
pub use entity_storage::{PartialEntityStorage, WeakEntityStorage};
|
||||
pub use entity_info::{PartialEntityInfos, WeakEntityInfos};
|
||||
use thiserror::Error;
|
||||
pub use world::*;
|
||||
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
use crate::{
|
||||
entity::{move_unchecked, EntityId, Physics, Position},
|
||||
Chunk, MoveEntityError, PartialChunkStorage, PartialEntityStorage, WeakChunkStorage,
|
||||
WeakEntityStorage,
|
||||
entity::{self, move_unchecked, EntityId},
|
||||
Chunk, MoveEntityError, PartialChunkStorage, PartialEntityInfos, WeakChunkStorage,
|
||||
WeakEntityInfos,
|
||||
};
|
||||
use azalea_block::BlockState;
|
||||
use azalea_buf::BufReadError;
|
||||
use azalea_core::{BlockPos, ChunkPos, PositionDelta8, Vec3};
|
||||
use bevy_ecs::query::{QueryState, WorldQuery};
|
||||
use parking_lot::RwLock;
|
||||
use log::warn;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use std::{backtrace::Backtrace, fmt::Debug};
|
||||
use std::{fmt::Formatter, io::Cursor, sync::Arc};
|
||||
use uuid::Uuid;
|
||||
|
@ -26,15 +27,7 @@ pub struct PartialWorld {
|
|||
pub shared: Arc<WeakWorld>,
|
||||
|
||||
pub chunks: PartialChunkStorage,
|
||||
pub entities: PartialEntityStorage,
|
||||
}
|
||||
|
||||
/// A world where the chunks are stored as weak pointers. This is used for
|
||||
/// shared worlds.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct WeakWorld {
|
||||
pub chunks: Arc<RwLock<WeakChunkStorage>>,
|
||||
pub entities: Arc<RwLock<WeakEntityStorage>>,
|
||||
pub entity_infos: PartialEntityInfos,
|
||||
}
|
||||
|
||||
impl PartialWorld {
|
||||
|
@ -46,7 +39,7 @@ impl PartialWorld {
|
|||
PartialWorld {
|
||||
shared: shared.clone(),
|
||||
chunks: PartialChunkStorage::new(chunk_radius, shared.chunks.clone()),
|
||||
entities: PartialEntityStorage::new(shared.entities.clone(), owner_entity_id),
|
||||
entity_infos: PartialEntityInfos::new(shared.entity_infos.clone(), owner_entity_id),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,7 +75,7 @@ impl PartialWorld {
|
|||
/// cause the update tracker to get out of sync.
|
||||
fn maybe_update_entity(&mut self, id: EntityId) -> bool {
|
||||
// no entity for you (we're processing this entity somewhere else)
|
||||
if Some(id) != self.entities.owner_entity_id && !self.entities.maybe_update(id) {
|
||||
if Some(id) != self.entity_infos.owner_entity_id && !self.entity_infos.maybe_update(id) {
|
||||
false
|
||||
} else {
|
||||
true
|
||||
|
@ -101,17 +94,19 @@ impl PartialWorld {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn add_entity(&mut self, id: EntityId, bundle: impl bevy_ecs::prelude::Bundle) {
|
||||
self.entities.insert(id, bundle);
|
||||
}
|
||||
|
||||
pub fn set_entity_pos(
|
||||
&mut self,
|
||||
entity_id: EntityId,
|
||||
new_pos: Vec3,
|
||||
pos: &mut entity::Position,
|
||||
physics: &mut entity::Physics,
|
||||
) -> Result<(), MoveEntityError> {
|
||||
if self.maybe_update_entity(entity_id) {
|
||||
self.shared.set_entity_pos(entity_id, new_pos)
|
||||
self.shared
|
||||
.entity_infos
|
||||
.write()
|
||||
.set_entity_pos(entity_id, new_pos, pos, physics);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(MoveEntityError::EntityDoesNotExist(Backtrace::capture()))
|
||||
}
|
||||
|
@ -122,22 +117,125 @@ impl PartialWorld {
|
|||
entity_id: EntityId,
|
||||
delta: &PositionDelta8,
|
||||
) -> Result<(), MoveEntityError> {
|
||||
let mut query = self.shared.query_entities::<&Position>();
|
||||
let pos = **query
|
||||
.get(&self.shared.entities.read().ecs, entity_id.into())
|
||||
let global_ecs_lock = self.shared.global_ecs.clone();
|
||||
let mut ecs = global_ecs_lock.lock();
|
||||
let mut query = ecs.query::<(&mut entity::Position, &mut entity::Physics)>();
|
||||
let (mut pos, mut physics) = query
|
||||
.get_mut(&mut ecs, entity_id.into())
|
||||
.map_err(|_| MoveEntityError::EntityDoesNotExist(Backtrace::capture()))?;
|
||||
|
||||
let new_pos = pos.with_delta(delta);
|
||||
|
||||
self.set_entity_pos(entity_id, new_pos)
|
||||
self.set_entity_pos(entity_id, new_pos, &mut pos, &mut physics)
|
||||
}
|
||||
|
||||
/// Add an entity to the storage.
|
||||
#[inline]
|
||||
pub fn add_entity(&mut self, id: EntityId, bundle: impl bevy_ecs::bundle::Bundle) {
|
||||
// if the entity is already in the shared world, we don't need to do
|
||||
// anything
|
||||
if self.shared.contains_entity(id) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut ecs = self.shared.global_ecs.lock();
|
||||
entity::new_entity(&mut ecs, id, bundle);
|
||||
|
||||
let mut query = ecs.query::<(&entity::EntityUuid, &entity::Position)>();
|
||||
let (&uuid, &pos) = query.get(&mut ecs, id.into()).unwrap();
|
||||
|
||||
let partial_entity_infos = &mut self.entity_infos;
|
||||
let mut shared_entity_infos = self.shared.entity_infos.write();
|
||||
|
||||
// add the entity to the indexes
|
||||
shared_entity_infos
|
||||
.ids_by_chunk
|
||||
.entry(ChunkPos::from(&pos))
|
||||
.or_default()
|
||||
.insert(id);
|
||||
shared_entity_infos.id_by_uuid.insert(*uuid, id);
|
||||
partial_entity_infos.loaded_entity_ids.insert(id);
|
||||
// set our updates_received to the shared updates_received, unless it's
|
||||
// not there in which case set both to 1
|
||||
if let Some(&shared_updates_received) = shared_entity_infos.updates_received.get(&id) {
|
||||
// 0 means we're never tracking updates for this entity
|
||||
if shared_updates_received != 0 || Some(id) == partial_entity_infos.owner_entity_id {
|
||||
partial_entity_infos.updates_received.insert(id, 1);
|
||||
}
|
||||
} else {
|
||||
shared_entity_infos.updates_received.insert(id, 1);
|
||||
partial_entity_infos.updates_received.insert(id, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove an entity from this storage by its id. It will only be removed
|
||||
/// from the shared storage if there are no other references to it.
|
||||
#[inline]
|
||||
pub fn remove_entity_by_id(&mut self, id: EntityId) {
|
||||
let partial_entity_infos = &mut self.entity_infos;
|
||||
let mut shared_entity_infos = self.shared.entity_infos.write();
|
||||
|
||||
if partial_entity_infos.loaded_entity_ids.remove(&id) {
|
||||
let mut ecs = self.shared.global_ecs.lock();
|
||||
|
||||
let mut query = ecs.query::<(&entity::EntityUuid, &entity::Position)>();
|
||||
let (uuid, pos) = query.get(&mut ecs, id.into()).expect(
|
||||
"If the entity was being loaded by this storage, it must be in the shared
|
||||
storage.",
|
||||
);
|
||||
let chunk = ChunkPos::from(pos);
|
||||
let uuid = **uuid;
|
||||
|
||||
// TODO: is this line actually necessary? test if it doesn't immediately
|
||||
// panic/deadlock without this line
|
||||
drop(query);
|
||||
|
||||
partial_entity_infos.updates_received.remove(&id);
|
||||
shared_entity_infos.remove_entity_if_unused(id, uuid, chunk);
|
||||
} else {
|
||||
warn!("Tried to remove entity with id {id} but it was not found.")
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all entities in a chunk. This will not clear them from the
|
||||
/// shared storage, unless there are no other references to them.
|
||||
pub fn clear_entities_in_chunk(&mut self, chunk: &ChunkPos) {
|
||||
let partial_entity_infos = &mut self.entity_infos;
|
||||
let mut shared_entity_infos = self.shared.entity_infos.write();
|
||||
|
||||
let mut ecs = self.shared.global_ecs.lock();
|
||||
|
||||
let mut query = ecs.query::<&entity::EntityUuid>();
|
||||
if let Some(entities) = shared_entity_infos.ids_by_chunk.get(chunk).cloned() {
|
||||
for &id in &entities {
|
||||
if partial_entity_infos.loaded_entity_ids.remove(&id) {
|
||||
let uuid = **query.get(&ecs, id.into()).unwrap();
|
||||
// maybe remove it from the storage
|
||||
shared_entity_infos.remove_entity_if_unused(id, uuid, *chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A world where the chunks are stored as weak pointers. This is used for
|
||||
/// shared worlds.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct WeakWorld {
|
||||
pub chunks: Arc<RwLock<WeakChunkStorage>>,
|
||||
pub(crate) entity_infos: Arc<RwLock<WeakEntityInfos>>,
|
||||
|
||||
/// A reference to the ECS world that contains all of the entities in all of
|
||||
/// the worlds.
|
||||
pub(crate) global_ecs: Arc<Mutex<bevy_ecs::world::World>>,
|
||||
}
|
||||
|
||||
impl WeakWorld {
|
||||
pub fn new(height: u32, min_y: i32) -> Self {
|
||||
pub fn new(height: u32, min_y: i32, global_ecs: Arc<Mutex<bevy_ecs::world::World>>) -> Self {
|
||||
WeakWorld {
|
||||
chunks: Arc::new(RwLock::new(WeakChunkStorage::new(height, min_y))),
|
||||
entities: Arc::new(RwLock::new(WeakEntityStorage::new())),
|
||||
entity_infos: Arc::new(RwLock::new(WeakEntityInfos::new())),
|
||||
global_ecs,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,50 +250,12 @@ impl WeakWorld {
|
|||
self.chunks.read().min_y
|
||||
}
|
||||
|
||||
pub fn contains_entity(&self, id: EntityId) -> bool {
|
||||
self.entity_infos.read().contains_entity(id)
|
||||
}
|
||||
|
||||
pub fn id_by_uuid(&self, uuid: &Uuid) -> Option<EntityId> {
|
||||
self.entities.read().id_by_uuid(uuid).copied()
|
||||
}
|
||||
|
||||
pub fn query_entities<Q: WorldQuery>(&self) -> QueryState<Q, ()> {
|
||||
self.entities.write().query_to_state::<Q>()
|
||||
}
|
||||
|
||||
/// Set an entity's position in the world.
|
||||
///
|
||||
/// Note that this will access the [`Position`] and [`Physics`] components,
|
||||
/// so if you already references to those components, you should use
|
||||
/// [`Self::set_entity_pos_from_refs`] instead.
|
||||
pub fn set_entity_pos(
|
||||
&self,
|
||||
entity_id: EntityId,
|
||||
new_pos: Vec3,
|
||||
) -> Result<(), MoveEntityError> {
|
||||
let mut entity_storage = self.entities.write();
|
||||
let (pos, physics) =
|
||||
entity_storage.query_entity_mut::<(&mut Position, &mut Physics)>(entity_id);
|
||||
|
||||
self.set_entity_pos_from_refs(entity_id, new_pos, pos.into_inner(), physics.into_inner())
|
||||
}
|
||||
|
||||
/// Set an entity's position in the world when we already have references
|
||||
/// to the [`Position`] and [`Physics`] components.
|
||||
pub fn set_entity_pos_from_refs(
|
||||
&self,
|
||||
entity_id: EntityId,
|
||||
new_pos: Vec3,
|
||||
pos: &mut Position,
|
||||
physics: &mut Physics,
|
||||
) -> Result<(), MoveEntityError> {
|
||||
let old_chunk = ChunkPos::from(&*pos);
|
||||
let new_chunk = ChunkPos::from(&new_pos);
|
||||
// this is fine because we update the chunk below
|
||||
unsafe { move_unchecked(pos, physics, new_pos) };
|
||||
if old_chunk != new_chunk {
|
||||
self.entities
|
||||
.write()
|
||||
.update_entity_chunk(entity_id, &old_chunk, &new_chunk);
|
||||
}
|
||||
Ok(())
|
||||
self.entity_infos.read().id_by_uuid(uuid).copied()
|
||||
}
|
||||
|
||||
pub fn get_block_state(&self, pos: &BlockPos) -> Option<BlockState> {
|
||||
|
@ -205,29 +265,44 @@ impl WeakWorld {
|
|||
pub fn get_chunk(&self, pos: &ChunkPos) -> Option<Arc<RwLock<Chunk>>> {
|
||||
self.chunks.read().get(pos)
|
||||
}
|
||||
|
||||
pub fn set_entity_pos(
|
||||
&self,
|
||||
entity_id: EntityId,
|
||||
new_pos: Vec3,
|
||||
pos: &mut entity::Position,
|
||||
physics: &mut entity::Physics,
|
||||
) {
|
||||
self.entity_infos
|
||||
.write()
|
||||
.set_entity_pos(entity_id, new_pos, pos, physics);
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for PartialWorld {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("World")
|
||||
.field("chunk_storage", &self.chunks)
|
||||
.field("entity_storage", &self.entities)
|
||||
.field("entity_storage", &self.entity_infos)
|
||||
.field("shared", &self.shared)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PartialWorld {
|
||||
/// Creates a completely self-contained `PartialWorld`. This is only for
|
||||
/// testing and shouldn't be used in actual code!
|
||||
fn default() -> Self {
|
||||
let chunk_storage = PartialChunkStorage::default();
|
||||
let entity_storage = PartialEntityStorage::default();
|
||||
let entity_storage = PartialEntityInfos::default();
|
||||
Self {
|
||||
shared: Arc::new(WeakWorld {
|
||||
chunks: chunk_storage.shared.clone(),
|
||||
entities: entity_storage.shared.clone(),
|
||||
entity_infos: entity_storage.shared.clone(),
|
||||
global_ecs: Arc::new(Mutex::new(bevy_ecs::world::World::default())),
|
||||
}),
|
||||
chunks: chunk_storage,
|
||||
entities: entity_storage,
|
||||
entity_infos: entity_storage,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue