diff --git a/Cargo.lock b/Cargo.lock index a95ec186..8f36b6bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -278,7 +278,10 @@ dependencies = [ "azalea-protocol", "azalea-registry", "azalea-world", + "bevy_app", "bevy_ecs", + "futures", + "iyes_loopless", "log", "nohash-hasher", "once_cell", @@ -463,6 +466,7 @@ checksum = "536e4d0018347478545ed8b6cb6e57b9279ee984868e81b7c0e78e0fb3222e42" dependencies = [ "bevy_derive", "bevy_ecs", + "bevy_reflect", "bevy_utils", "downcast-rs", "wasm-bindgen", @@ -489,6 +493,7 @@ dependencies = [ "async-channel", "bevy_ecs_macros", "bevy_ptr", + "bevy_reflect", "bevy_tasks", "bevy_utils", "downcast-rs", @@ -522,12 +527,56 @@ dependencies = [ "toml", ] +[[package]] +name = "bevy_math" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d434c77ab766c806ed9062ef8a7285b3b02b47df51f188d4496199c3ac062eaf" +dependencies = [ + "glam", + "serde", +] + [[package]] name = "bevy_ptr" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ec44f7655039546bc5d34d98de877083473f3e9b2b81d560c528d6d74d3eff4" +[[package]] +name = "bevy_reflect" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6deae303a7f69dc243b2fa35b5e193cc920229f448942080c8eb2dbd9de6d37a" +dependencies = [ + "bevy_math", + "bevy_ptr", + "bevy_reflect_derive", + "bevy_utils", + "downcast-rs", + "erased-serde", + "glam", + "once_cell", + "parking_lot", + "serde", + "smallvec", + "thiserror", +] + +[[package]] +name = "bevy_reflect_derive" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bf4cb9cd5acb4193f890f36cb63679f1502e2de025e66a63b194b8b133d018" +dependencies = [ + "bevy_macro_utils", + "bit-set", + "proc-macro2", + "quote", + "syn", + "uuid", +] + [[package]] name = "bevy_tasks" version = "0.9.1" @@ -543,6 +592,19 @@ dependencies = [ "wasm-bindgen-futures", ] +[[package]] +name = "bevy_time" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c38a6d3ea929c7f81e6adf5a6c62cf7e8c40f5106c2174d6057e9d8ea624d" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_reflect", + "bevy_utils", + "crossbeam-channel", +] + [[package]] name = "bevy_utils" version = "0.9.1" @@ -557,6 +619,21 @@ dependencies = [ "uuid", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -604,6 +681,12 @@ version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" +[[package]] +name = "bytemuck" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" + [[package]] name = "byteorder" version = "1.4.3" @@ -924,6 +1007,15 @@ dependencies = [ "termcolor", ] +[[package]] +name = "erased-serde" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ca605381c017ec7a5fef5e548f1cfaa419ed0f6df6367339300db74c92aa7d" +dependencies = [ + "serde", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -1127,6 +1219,16 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" +[[package]] +name = "glam" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f597d56c1bd55a811a1be189459e8fad2bbc272616375602443bdfb37fa774" +dependencies = [ + "bytemuck", + "serde", +] + [[package]] name = "h2" version = "0.3.15" @@ -1333,6 +1435,18 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +[[package]] +name = "iyes_loopless" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c47fd2cbdb1d7f295c25e6bfccfd78a84b6eef3055bc9f01b34ae861721b01ee" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_time", + "bevy_utils", +] + [[package]] name = "js-sys" version = "0.3.60" @@ -2076,6 +2190,9 @@ name = "smallvec" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +dependencies = [ + "serde", +] [[package]] name = "socket2" diff --git a/Cargo.toml b/Cargo.toml index 8a984e81..9970c66e 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,19 +24,9 @@ debug = true # decoding packets takes forever if we don't do this [profile.dev.package.azalea-crypto] opt-level = 3 -[profile.dev.package.cipher] -opt-level = 3 [profile.dev.package.cfb8] opt-level = 3 [profile.dev.package.aes] opt-level = 3 -[profile.dev.package.crypto-common] -opt-level = 3 -[profile.dev.package.generic-array] -opt-level = 3 -[profile.dev.package.typenum] -opt-level = 3 -[profile.dev.package.inout] -opt-level = 3 [profile.dev.package.flate2] opt-level = 3 diff --git a/azalea-client/Cargo.toml b/azalea-client/Cargo.toml index 3357f430..d794a80d 100644 --- a/azalea-client/Cargo.toml +++ b/azalea-client/Cargo.toml @@ -20,7 +20,10 @@ azalea-physics = {path = "../azalea-physics", version = "0.5.0"} azalea-protocol = {path = "../azalea-protocol", version = "0.5.0"} azalea-registry = {path = "../azalea-registry", version = "0.5.0"} azalea-world = {path = "../azalea-world", version = "0.5.0"} +bevy_app = {version = "0.9.1", default-features = false} bevy_ecs = {version = "0.9.1", default-features = false} +futures = "0.3.25" +iyes_loopless = "0.9.1" log = "0.4.17" nohash-hasher = "0.2.0" once_cell = "1.16.0" diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index 61c121e5..90e8f640 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -1,7 +1,12 @@ pub use crate::chat::ChatPacket; use crate::{ - local_player::LocalPlayer, movement::WalkDirection, plugins::PluginStates, Account, PlayerInfo, + local_player::LocalPlayer, + movement::{send_position, WalkDirection}, + packet_handling, + plugins::PluginStates, + Account, PlayerInfo, }; + use azalea_auth::{game_profile::GameProfile, sessionserver::SessionServerError}; use azalea_core::{ChunkPos, ResourceLocation, Vec3}; use azalea_protocol::{ @@ -34,16 +39,19 @@ use azalea_world::{ entity::{ self, metadata::{self, PlayerMetadataBundle}, - EntityId, + Entity, }, - PartialChunkStorage, PartialWorld, WeakWorldContainer, World, + EntityInfos, PartialChunkStorage, PartialWorld, World, WorldContainer, }; +use bevy_app::App; use bevy_ecs::{ prelude::Component, query::{QueryState, WorldQuery}, - schedule::{Schedule, Stage, StageLabel, SystemStage}, + schedule::{IntoSystemDescriptor, Schedule, Stage, StageLabel, SystemStage}, system::{Query, SystemState}, }; +use futures::{stream::FuturesUnordered, StreamExt}; +use iyes_loopless::prelude::*; use log::{debug, error, info, trace, warn}; use parking_lot::{Mutex, RwLock}; use std::{ @@ -52,7 +60,9 @@ use std::{ collections::HashMap, fmt::Debug, io::{self, Cursor}, + ops::DerefMut, sync::Arc, + time::Duration, }; use thiserror::Error; use tokio::{ @@ -100,11 +110,12 @@ pub enum Event { /// Things that a player in the world will want to know are in [`LocalPlayer`]. pub struct Client { pub profile: GameProfile, - pub entity_id: Arc>, + /// The entity for this client in the ECS. + pub entity: Entity, /// A container of world names to worlds. If we're not using a shared world /// (i.e. not a swarm), then this will only contain data about the world /// we're currently in. - world_container: Arc>, + world_container: Arc>, /// The world that this client is in. pub world: Arc>, @@ -113,6 +124,11 @@ pub struct Client { /// the `azalea` crate. you can ignore this field. pub plugins: Arc, tasks: Arc>>>, + + /// The entity component system. You probably don't need to access this + /// directly. Note that if you're using a shared world (i.e. a swarm), this + /// will contain all entities in all worlds. + pub ecs: Arc>, } /// Whether we should ignore errors when decoding packets. @@ -143,19 +159,23 @@ impl Client { /// defaults, otherwise use [`Client::join`]. pub fn new( profile: GameProfile, - world_container: Option>>, + world_container: Option>>, + entity: Entity, + ecs: Arc>, ) -> Self { Self { profile, // default our id to 0, it'll be set later - entity_id: Arc::new(RwLock::new(EntityId(0))), + entity, world: Arc::new(RwLock::new(PartialWorld::default())), world_container: world_container - .unwrap_or_else(|| Arc::new(RwLock::new(WeakWorldContainer::new()))), + .unwrap_or_else(|| Arc::new(RwLock::new(WorldContainer::new()))), // The plugins can be modified by the user by replacing the plugins // field right after this. No Mutex so the user doesn't need to .lock(). plugins: Arc::new(PluginStates::default()), tasks: Arc::new(Mutex::new(Vec::new())), + + ecs, } } @@ -188,6 +208,7 @@ impl Client { let conn = Connection::new(&resolved_address).await?; let (conn, game_profile) = Self::handshake(conn, account, &address).await?; + let (read_conn, write_conn) = conn.into_split(); // The buffer has to be 1 to avoid a bug where if it lags events are // received a bit later instead of the instant they were fired. @@ -195,21 +216,43 @@ impl Client { 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.clone(), None); + // An event that causes the schedule to run. This is only used internally. + let (run_schedule_sender, run_schedule_receiver) = mpsc::unbounded_channel(); - let (read_conn, write_conn) = conn.into_split(); + let ecs_lock = start_ecs(run_schedule_receiver).await; + + let mut ecs = ecs_lock.lock(); + ecs.init_resource::(); + + let entity_mut = ecs.spawn_empty(); + let entity = entity_mut.id(); + + // we got the GameConnection, so the server is now connected :) + let client = Client::new(game_profile.clone(), None, entity, ecs_lock); + + let world = client.world(); let local_player = crate::local_player::LocalPlayer::new( + entity, game_profile, write_conn, - client.world.clone(), + world.clone(), + ecs.resource_mut::().deref_mut(), tx, ); - // just start up the game loop and we're ready! + // start receiving packets + let packet_receiver = packet_handling::PacketReceiver { + packets: Arc::new(Mutex::new(Vec::new())), + run_schedule_sender, + }; - client.start_tasks(read_conn); + tokio::spawn(packet_receiver.read_task(read_conn)); + + ecs.entity_mut(entity) + .insert((local_player, packet_receiver)); + + // just start up the game loop and we're ready! Ok((client, rx)) } @@ -359,688 +402,33 @@ impl Client { self.query::<&mut LocalPlayer>().into_inner() } - /// Start the protocol and game tick loop. - #[doc(hidden)] - pub fn start_tasks(&self, read_conn: ReadConnection) { - // if you get an error right here that means you're doing something with locks - // wrong read the error to see where the issue is - // you might be able to just drop the lock or put it in its own scope to fix - - let mut tasks = self.tasks.lock(); - tasks.push(tokio::spawn(self.protocol_loop(tx.clone(), read_conn))); - let ecs = self.world_container.clone().read().ecs; - tasks.push(tokio::spawn(Client::game_tick_loop(ecs.clone()))); - } - - async fn protocol_loop( - &self, - tx: Sender, - read_conn: ReadConnection, - ) { - loop { - let r = read_conn.lock().await.read().await; - match r { - Ok(packet) => LocalPlayer::send_event(Event::Packet(packet.clone()), &tx), - Err(e) => { - let e = *e; - if let ReadPacketError::ConnectionClosed = e { - info!("Connection closed"); - if let Err(e) = client.disconnect().await { - error!("Error shutting down connection: {:?}", e); - } - break; - } - let default_backtrace = Backtrace::capture(); - if IGNORE_ERRORS { - let backtrace = - any::request_ref::(&e).unwrap_or(&default_backtrace); - warn!("{e}\n{backtrace}"); - match e { - ReadPacketError::FrameSplitter { .. } => panic!("Error: {e:?}"), - _ => continue, - } - } else { - let backtrace = - any::request_ref::(&e).unwrap_or(&default_backtrace); - panic!("{e}\n{backtrace}") - } - } - } - } - } - - // handling packets is outside of the ecs schedule since it's not bound to - // ticks. - pub fn handle_packet( - client: &Client, - packet: ClientboundGamePacket, - ecs: &mut bevy_ecs::world::World, - tx: &mpsc::Sender, - ) -> Result<(), HandlePacketError> { - 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(()) - } - - /// 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>) { - 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 { - game_tick_interval.tick().await; - - schedule.run(&mut ecs.lock()); - } - } - /// Get a reference to our (potentially shared) world. /// /// This gets the [`WeakWorld`] from our world container. If it's a normal /// client, then it'll be the same as the world the client has loaded. /// If the client using a shared world, then the shared world will be a /// superset of the client's world. - pub fn world(&self) -> Arc { - self.world.read().shared.clone() - } - - pub fn entity(&self) -> EntityId { - *self.entity_id.read() + pub fn world(&self) -> Arc> { + let world_name = self + .local_player() + .world_name + .expect("World name must be known if we're doing Client::world"); + let world_container = self.world_container.read(); + world_container.get(&world_name).unwrap() } /// Query data of our player's entity. pub fn query<'w, 's, Q: WorldQuery>(&'w self) -> ::Item<'_> { - let mut ecs = &mut self.world_container.write().ecs.lock(); + let mut ecs = &mut self.ecs.lock(); QueryState::::new(ecs) - .get_mut(ecs, self.entity().into()) + .get_mut(ecs, self.entity) .expect("Player entity should always exist when being queried") } /// Returns whether we have a received the login packet yet. pub fn logged_in(&self) -> bool { // the login packet tells us the world name - self.world_name.read().is_some() + self.local_player().world_name.is_some() } /// Tell the server we changed our game options (i.e. render distance, main @@ -1078,3 +466,55 @@ impl Client { Ok(()) } } + +/// Start the protocol and game tick loop. +#[doc(hidden)] +pub async fn start_ecs( + run_schedule_receiver: mpsc::UnboundedReceiver<()>, +) -> Arc> { + // if you get an error right here that means you're doing something with locks + // wrong read the error to see where the issue is + // you might be able to just drop the lock or put it in its own scope to fix + + let mut app = App::new(); + + app.add_fixed_timestep(Duration::from_millis(50), "tick"); + + app.add_fixed_timestep_system("tick", 0, send_position); + // .add_system(LocalPlayer::update_in_loaded_chunk) + // .add_system(send_position) + // .add_system(LocalPlayer::ai_step); + + app.add_system(packet_handling::handle_packets.label("handle_packets")); + + // should happen last + app.add_system(packet_handling::clear_packets.after("handle_packets")); + + // all resources should have been added by now so we can take the ecs from the + // app + let ecs = Arc::new(Mutex::new(app.world)); + + 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); + + tokio::spawn(run_schedule_loop( + ecs.clone(), + app.schedule, + run_schedule_receiver, + )); + + ecs +} + +async fn run_schedule_loop( + ecs: Arc>, + mut schedule: Schedule, + mut run_schedule_receiver: mpsc::UnboundedReceiver<()>, +) { + loop { + // whenever we get an event from run_schedule_receiver, run the schedule + run_schedule_receiver.recv().await; + schedule.run(&mut ecs.lock()); + } +} diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs index 6a65cd83..25777bbe 100644 --- a/azalea-client/src/lib.rs +++ b/azalea-client/src/lib.rs @@ -16,6 +16,7 @@ mod client; mod get_mc_dir; mod local_player; mod movement; +pub mod packet_handling; pub mod ping; mod player; mod plugins; diff --git a/azalea-client/src/local_player.rs b/azalea-client/src/local_player.rs index 3b7050e7..86c4fdc9 100644 --- a/azalea-client/src/local_player.rs +++ b/azalea-client/src/local_player.rs @@ -10,8 +10,8 @@ use azalea_protocol::{ }, }; use azalea_world::{ - entity::{self, metadata::PlayerMetadataBundle, EntityId}, - PartialWorld, WeakWorldContainer, + entity::{self, metadata::PlayerMetadataBundle, Entity, EntityId}, + EntityInfos, PartialWorld, World, WorldContainer, }; use bevy_ecs::{ component::Component, @@ -32,6 +32,7 @@ pub struct LocalPlayer { pub profile: GameProfile, pub write_conn: WriteConnection, + // pub world: Arc>, pub physics_state: PhysicsState, pub client_information: ClientInformation, @@ -39,7 +40,8 @@ pub struct LocalPlayer { /// A map of player uuids to their information in the tab list pub players: HashMap, - pub world: Arc>, + pub partial_world: Arc>, + pub world: Arc>, pub world_name: Option, pub tx: mpsc::Sender, @@ -66,28 +68,39 @@ pub struct PhysicsState { pub struct LocalPlayerInLoadedChunk; impl LocalPlayer { - /// Create a new client from the given GameProfile, Connection, and World. + /// Create a new `LocalPlayer`. + /// /// You should only use this if you want to change these fields from the /// defaults, otherwise use [`Client::join`]. pub fn new( + entity: Entity, profile: GameProfile, write_conn: WriteConnection, - world: Arc>, + world: Arc>, + entity_infos: &mut EntityInfos, tx: mpsc::Sender, ) -> Self { + let client_information = ClientInformation::default(); + LocalPlayer { profile, + write_conn, + physics_state: PhysicsState::default(), client_information: ClientInformation::default(), dead: false, players: HashMap::new(), world, - world_name: Arc::new(RwLock::new(None)), + partial_world: Arc::new(RwLock::new(PartialWorld::new( + client_information.view_distance.into(), + Some(entity), + entity_infos, + ))), + world_name: None, tx, - world_name: None, } } diff --git a/azalea-client/src/movement.rs b/azalea-client/src/movement.rs index 2ea36e3e..b45affa5 100644 --- a/azalea-client/src/movement.rs +++ b/azalea-client/src/movement.rs @@ -1,5 +1,5 @@ -use crate::client::{Client, LocalPlayerInLoadedChunk}; -use crate::{LocalPlayer, PhysicsState}; +use crate::client::Client; +use crate::local_player::{LocalPlayer, LocalPlayerInLoadedChunk, PhysicsState}; use azalea_core::Vec3; use azalea_protocol::packets::game::serverbound_player_command_packet::ServerboundPlayerCommandPacket; use azalea_protocol::packets::game::{ @@ -8,7 +8,7 @@ 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::{Entity, MinecraftEntityId}; use azalea_world::{entity, MoveEntityError}; use bevy_ecs::system::Query; use std::backtrace::Backtrace; @@ -60,117 +60,118 @@ impl Client { } } -impl LocalPlayer { - /// This gets called automatically every tick. - pub(crate) fn send_position( - 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, - ); +pub(crate) fn send_position( + mut query: Query< + ( + Entity, + &MinecraftEntityId, + &LocalPlayer, + &entity::Position, + &entity::LastSentPosition, + &mut entity::Physics, + &entity::metadata::Sprinting, + ), + &LocalPlayerInLoadedChunk, + >, +) { + for (entity, id, local_player, position, last_sent_position, physics, sprinting) in &query { + local_player.send_sprinting_if_needed( + entity.into(), + id, + sprinting, + &mut local_player.physics_state, + ); - let packet = { - // TODO: the camera being able to be controlled by other entities isn't - // implemented yet if !self.is_controlled_camera() { return }; + let packet = { + // TODO: the camera being able to be controlled by other entities isn't + // implemented yet if !self.is_controlled_camera() { return }; - let old_position = physics.last_pos; + let x_delta = position.x - last_sent_position.x; + let y_delta = position.y - last_sent_position.y; + let z_delta = position.z - last_sent_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; - 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; + local_player.physics_state.position_remainder += 1; - local_player.physics_state.position_remainder += 1; + // 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; - // 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; - - // 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 - }; - - 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; - } - - physics.last_on_ground = physics.on_ground; - // minecraft checks for autojump here, but also autojump is bad so - - packet + // 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 }; - if let Some(packet) = packet { - tokio::spawn(local_player.write_packet(packet)); + if sending_position { + **last_sent_position = **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; + } + + physics.last_on_ground = physics.on_ground; + // minecraft checks for autojump here, but also autojump is bad so + + packet + }; + + if let Some(packet) = packet { + local_player.write_packet(packet); } } +} +impl LocalPlayer { fn send_sprinting_if_needed( &mut self, - entity: entity::EntityId, + entity: Entity, + id: &MinecraftEntityId, sprinting: &entity::metadata::Sprinting, physics_state: &mut PhysicsState, ) { @@ -181,15 +182,13 @@ impl LocalPlayer { } else { azalea_protocol::packets::game::serverbound_player_command_packet::Action::StopSprinting }; - tokio::spawn( - self.write_packet( - ServerboundPlayerCommandPacket { - id: *entity, - action: sprinting_action, - data: 0, - } - .get(), - ), + self.write_packet( + ServerboundPlayerCommandPacket { + id: **id, + action: sprinting_action, + data: 0, + } + .get(), ); physics_state.was_sprinting = **sprinting; } @@ -200,7 +199,7 @@ impl LocalPlayer { pub fn ai_step( query: Query< ( - EcsEntityId, + Entity, &mut LocalPlayer, &mut entity::Physics, &mut entity::Position, @@ -251,8 +250,7 @@ impl LocalPlayer { } azalea_physics::ai_step( - ecs_entity_id.into(), - &local_player.world.read().shared, + &local_player.world.read(), &mut physics, &mut position, &sprinting, diff --git a/azalea-client/src/packet_handling.rs b/azalea-client/src/packet_handling.rs new file mode 100644 index 00000000..e3c7d2f1 --- /dev/null +++ b/azalea-client/src/packet_handling.rs @@ -0,0 +1,616 @@ +use std::sync::Arc; + +use azalea_protocol::{connect::ReadConnection, packets::game::ClientboundGamePacket}; +use bevy_ecs::{component::Component, prelude::Entity, query::Changed, system::Query}; +use parking_lot::Mutex; +use tokio::sync::mpsc; + +/// Something that receives packets from the server. +#[derive(Component, Clone)] +pub struct PacketReceiver { + pub packets: Arc>>, + pub run_schedule_sender: mpsc::UnboundedSender<()>, +} + +pub fn handle_packets( + query: Query<(Entity, &PacketReceiver), Changed>, + // ecs: &mut bevy_ecs::world::World, +) { + for (entity, packet_events) in &query { + for packet in packet_events.packets.lock().iter() { + handle_packet(entity, packet); + } + } +} + +pub fn handle_packet(entity: Entity, packet: &ClientboundGamePacket) { + // match packet { + // ClientboundGamePacket::Login(p) => { + // debug!("Got login packet"); + + // { + // // // write p into login.txt + // // std::io::Write::write_all( + // // &mut std::fs::File::create("login.txt").unwrap(), + // // format!("{:#?}", p).as_bytes(), + // // ) + // // .unwrap(); + + // // TODO: have registry_holder be a struct because this sucks + // rn // best way would be to add serde support to + // azalea-nbt + + // let registry_holder = p + // .registry_holder + // .as_compound() + // .expect("Registry holder is not a compound") + // .get("") + // .expect("No \"\" tag") + // .as_compound() + // .expect("\"\" tag is not a compound"); + // let dimension_types = registry_holder + // .get("minecraft:dimension_type") + // .expect("No dimension_type tag") + // .as_compound() + // .expect("dimension_type is not a compound") + // .get("value") + // .expect("No dimension_type value") + // .as_list() + // .expect("dimension_type value is not a list"); + // let dimension_type = dimension_types + // .iter() + // .find(|t| { + // t.as_compound() + // .expect("dimension_type value is not a compound") + // .get("name") + // .expect("No name tag") + // .as_string() + // .expect("name is not a string") + // == p.dimension_type.to_string() + // }) + // .unwrap_or_else(|| panic!("No dimension_type with name + // {}", p.dimension_type)) .as_compound() + // .unwrap() + // .get("element") + // .expect("No element tag") + // .as_compound() + // .expect("element is not a compound"); + // let height = (*dimension_type + // .get("height") + // .expect("No height tag") + // .as_int() + // .expect("height tag is not an int")) + // .try_into() + // .expect("height is not a u32"); + // let min_y = *dimension_type + // .get("min_y") + // .expect("No min_y tag") + // .as_int() + // .expect("min_y tag is not an int"); + + // let world_name = p.dimension.clone(); + + // local_player.world_name = Some(world_name.clone()); + // // add this world to the world_container (or don't if it's + // already there) let weak_world = + // world_container.insert(world_name, height, min_y); // set + // the loaded_world to an empty world // (when we add chunks + // or entities those will be in the world_container) let mut + // world_lock = local_player.world.write(); *world_lock = + // PartialWorld::new( + // local_player.client_information.view_distance.into(), + // weak_world, Some(EntityId(p.player_id)), + // ); + + // let player_bundle = entity::PlayerBundle { + // entity: entity::EntityBundle::new( + // local_player.profile.uuid, + // Vec3::default(), + // azalea_registry::EntityKind::Player, + // ), + // metadata: PlayerMetadataBundle::default(), + // }; + // // let entity = EntityData::new( + // // client.profile.uuid, + // // Vec3::default(), + // // EntityMetadata::Player(metadata::Player::default()), + // // ); + // // the first argument makes it so other entities don't update + // this entity in // a shared world + // world_lock.add_entity(EntityId(p.player_id), player_bundle); + + // *client.entity_id.write() = EntityId(p.player_id); + // } + + // // send the client information that we have set + // let client_information_packet: ClientInformation = + // client.local_player().client_information.clone(); + // log::debug!( + // "Sending client information because login: {:?}", + // client_information_packet + // ); + // client.write_packet(client_information_packet.get()).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(_) => {} + // } +} + +/// A system that clears all packets in the clientbound packet events. +pub fn clear_packets(mut query: Query<&mut PacketReceiver>) { + for mut packets in query.iter_mut() { + packets.packets.lock().clear(); + } +} + +impl PacketReceiver { + /// Loop that reads from the connection and adds the packets to the queue + + /// runs the schedule. + pub async fn read_task(&self, mut read_conn: ReadConnection) { + while let Ok(packet) = read_conn.read().await { + self.packets.lock().push(packet); + // tell the client to run all the systems + self.run_schedule_sender.send(()); + } + } +} diff --git a/azalea-physics/src/collision/mod.rs b/azalea-physics/src/collision/mod.rs index a00cb4ac..64f24b95 100644 --- a/azalea-physics/src/collision/mod.rs +++ b/azalea-physics/src/collision/mod.rs @@ -6,13 +6,12 @@ mod world_collisions; use azalea_core::{Axis, Vec3, AABB, EPSILON}; use azalea_world::{ - entity::{self, EntityId}, + entity::{self}, MoveEntityError, World, }; pub use blocks::BlockWithShape; pub use discrete_voxel_shape::*; pub use shape::*; -use std::ops::Deref; use self::world_collisions::get_block_collisions; @@ -74,7 +73,6 @@ fn collide(movement: &Vec3, world: &World, physics: &entity::Physics) -> Vec3 { pub fn move_colliding( _mover_type: &MoverType, movement: &Vec3, - entity_id: EntityId, world: &World, position: &mut entity::Position, physics: &mut entity::Physics, @@ -100,7 +98,7 @@ pub fn move_colliding( // movement = this.maybeBackOffFromEdge(movement, moverType); - let collide_result = { collide(movement, world, physics) }; + let collide_result = collide(movement, world, physics); let move_distance = collide_result.length_sqr(); @@ -115,7 +113,7 @@ pub fn move_colliding( } }; - world.set_entity_pos(entity_id, new_pos, position, physics); + **position = new_pos; } let x_collision = movement.x != collide_result.x; @@ -127,7 +125,7 @@ pub fn move_colliding( // TODO: minecraft checks for a "minor" horizontal collision here - let _block_pos_below = entity::on_pos_legacy(world, position, physics); + let _block_pos_below = entity::on_pos_legacy(&world.chunks, position, physics); // let _block_state_below = self // .world // .get_block_state(&block_pos_below) diff --git a/azalea-physics/src/collision/world_collisions.rs b/azalea-physics/src/collision/world_collisions.rs index d4a552d6..d04b7cda 100644 --- a/azalea-physics/src/collision/world_collisions.rs +++ b/azalea-physics/src/collision/world_collisions.rs @@ -2,7 +2,7 @@ use super::Shapes; use crate::collision::{BlockWithShape, VoxelShape, AABB}; use azalea_block::BlockState; use azalea_core::{ChunkPos, ChunkSectionPos, Cursor3d, CursorIterationType, EPSILON}; -use azalea_world::{entity::EntityId, Chunk, World}; +use azalea_world::{Chunk, World}; use parking_lot::RwLock; use std::sync::Arc; @@ -57,7 +57,7 @@ impl<'a> BlockCollisions<'a> { // return var7; // } - self.world.get_chunk(&chunk_pos) + self.world.chunks.get(&chunk_pos) } } @@ -79,7 +79,7 @@ impl<'a> Iterator for BlockCollisions<'a> { let pos = item.pos; let block_state: BlockState = chunk .read() - .get(&(&pos).into(), self.world.min_y()) + .get(&(&pos).into(), self.world.chunks.min_y) .unwrap_or(BlockState::Air); // TODO: continue if self.only_suffocating_blocks and the block is not diff --git a/azalea-physics/src/lib.rs b/azalea-physics/src/lib.rs index b249232f..692e6947 100644 --- a/azalea-physics/src/lib.rs +++ b/azalea-physics/src/lib.rs @@ -5,16 +5,14 @@ pub mod collision; use azalea_block::{Block, BlockState}; use azalea_core::{BlockPos, Vec3}; use azalea_world::{ - entity::{self, EntityId}, + entity::{self}, World, }; use collision::{move_colliding, MoverType}; -use std::ops::Deref; /// Move the entity with the given acceleration while handling friction, /// gravity, collisions, and some other stuff. fn travel( - entity_id: EntityId, world: &World, physics: &mut entity::Physics, position: &mut entity::Position, @@ -38,6 +36,7 @@ fn travel( let block_pos_below = get_block_pos_below_that_affects_movement(position); let block_state_below = world + .chunks .get_block_state(&block_pos_below) .unwrap_or(BlockState::Air); let block_below: Box = block_state_below.into(); @@ -53,7 +52,6 @@ fn travel( let mut movement = handle_relative_friction_and_calculate_movement( acceleration, block_friction, - entity_id, world, physics, position, @@ -83,7 +81,6 @@ fn travel( /// applies air resistance, calls self.travel(), and some other random /// stuff. pub fn ai_step( - entity_id: EntityId, world: &World, physics: &mut entity::Physics, position: &mut entity::Position, @@ -115,7 +112,6 @@ pub fn ai_step( physics.zza *= 0.98; travel( - entity_id, world, physics, position, @@ -168,7 +164,6 @@ fn get_block_pos_below_that_affects_movement(position: &entity::Position) -> Blo fn handle_relative_friction_and_calculate_movement( acceleration: &Vec3, block_friction: f32, - entity_id: EntityId, world: &World, physics: &mut entity::Physics, position: &mut entity::Position, @@ -183,7 +178,6 @@ fn handle_relative_friction_and_calculate_movement( move_colliding( &MoverType::Own, &physics.delta.clone(), - entity_id, world, position, physics, @@ -221,8 +215,10 @@ fn get_friction_influenced_speed( /// Returns the what the entity's jump should be multiplied by based on the /// block they're standing on. fn block_jump_factor(world: &World, position: &entity::Position) -> f32 { - let block_at_pos = world.get_block_state(&position.into()); - let block_below = world.get_block_state(&get_block_pos_below_that_affects_movement(position)); + let block_at_pos = world.chunks.get_block_state(&position.into()); + let block_below = world + .chunks + .get_block_state(&get_block_pos_below_that_affects_movement(position)); let block_at_pos_jump_factor = if let Some(block) = block_at_pos { Box::::from(block).behavior().jump_factor diff --git a/azalea-protocol/src/packets/game/clientbound_add_entity_packet.rs b/azalea-protocol/src/packets/game/clientbound_add_entity_packet.rs index a84fe3b0..4069077e 100755 --- a/azalea-protocol/src/packets/game/clientbound_add_entity_packet.rs +++ b/azalea-protocol/src/packets/game/clientbound_add_entity_packet.rs @@ -1,5 +1,5 @@ use azalea_buf::McBuf; -use azalea_core::Vec3; +use azalea_core::{ResourceLocation, Vec3}; use azalea_protocol_macros::ClientboundGamePacket; use azalea_world::entity::{ metadata::{apply_default_metadata, PlayerMetadataBundle, UpdateMetadataError}, @@ -41,8 +41,11 @@ pub struct ClientboundAddEntityPacket { // } impl ClientboundAddEntityPacket { - pub fn as_entity_bundle(&self) -> EntityBundle { - EntityBundle::new(self.uuid, self.position, self.entity_type) + /// Make the entity into a bundle that can be inserted into the ECS. You + /// must apply the metadata after inserting the bundle with + /// [`Self::apply_metadata`]. + pub fn as_entity_bundle(&self, world_name: ResourceLocation) -> EntityBundle { + EntityBundle::new(self.uuid, self.position, self.entity_type, world_name) } pub fn apply_metadata(&self, entity: &mut bevy_ecs::world::EntityMut) { diff --git a/azalea-protocol/src/packets/game/clientbound_add_player_packet.rs b/azalea-protocol/src/packets/game/clientbound_add_player_packet.rs index 09b1e16c..4cbeb1b9 100755 --- a/azalea-protocol/src/packets/game/clientbound_add_player_packet.rs +++ b/azalea-protocol/src/packets/game/clientbound_add_player_packet.rs @@ -1,5 +1,5 @@ use azalea_buf::McBuf; -use azalea_core::Vec3; +use azalea_core::{ResourceLocation, Vec3}; use azalea_protocol_macros::ClientboundGamePacket; use azalea_registry::EntityKind; use azalea_world::entity::{metadata::PlayerMetadataBundle, EntityBundle, PlayerBundle}; @@ -18,9 +18,9 @@ pub struct ClientboundAddPlayerPacket { } impl ClientboundAddPlayerPacket { - pub fn as_player_bundle(&self) -> PlayerBundle { + pub fn as_player_bundle(&self, world_name: ResourceLocation) -> PlayerBundle { PlayerBundle { - entity: EntityBundle::new(self.uuid, self.position, EntityKind::Player), + entity: EntityBundle::new(self.uuid, self.position, EntityKind::Player, world_name), metadata: PlayerMetadataBundle::default(), } } diff --git a/azalea-world/src/chunk_storage.rs b/azalea-world/src/chunk_storage.rs index c0c5f43a..ae5ba75b 100755 --- a/azalea-world/src/chunk_storage.rs +++ b/azalea-world/src/chunk_storage.rs @@ -15,13 +15,8 @@ use std::{ const SECTION_HEIGHT: u32 = 16; /// An efficient storage of chunks for a client that has a limited render -/// distance. This has support for using a shared [`WeakChunkStorage`]. If you -/// have an infinite render distance (like a server), you should use -/// [`ChunkStorage`] instead. +/// distance. This has support for using a shared [`ChunkStorage`]. pub struct PartialChunkStorage { - /// Chunk storage that can be shared by clients. - pub shared: Arc>, - pub view_center: ChunkPos, chunk_radius: u32, view_range: u32, @@ -33,23 +28,16 @@ pub struct PartialChunkStorage { /// actively being used somewhere else they'll be forgotten. This is used for /// shared worlds. #[derive(Debug)] -pub struct WeakChunkStorage { +pub struct ChunkStorage { pub height: u32, pub min_y: i32, pub chunks: HashMap>>, } -/// A storage of potentially infinite chunks in a world. Chunks are stored as -/// an `Arc` so they can be shared across threads. -pub struct ChunkStorage { - pub height: u32, - pub min_y: i32, - pub chunks: HashMap>>, -} - /// A single chunk in a world (16*?*16 blocks). This only contains the blocks /// and biomes. You can derive the height of the chunk from the number of -/// sections, but you need a [`ChunkStorage`] to get the minimum Y coordinate. +/// sections, but you need a [`ChunkStorage`] to get the minimum Y +/// coordinate. #[derive(Debug)] pub struct Chunk { pub sections: Vec
, @@ -82,10 +70,9 @@ impl Default for Chunk { } impl PartialChunkStorage { - pub fn new(chunk_radius: u32, shared: Arc>) -> Self { + pub fn new(chunk_radius: u32) -> Self { let view_range = chunk_radius * 2 + 1; PartialChunkStorage { - shared, view_center: ChunkPos::new(0, 0), chunk_radius, view_range, @@ -93,13 +80,6 @@ impl PartialChunkStorage { } } - pub fn min_y(&self) -> i32 { - self.shared.read().min_y - } - pub fn height(&self) -> u32 { - self.shared.read().height - } - fn get_index(&self, chunk_pos: &ChunkPos) -> usize { (i32::rem_euclid(chunk_pos.x, self.view_range as i32) * (self.view_range as i32) + i32::rem_euclid(chunk_pos.z, self.view_range as i32)) as usize @@ -110,20 +90,28 @@ impl PartialChunkStorage { && (chunk_pos.z - self.view_center.z).unsigned_abs() <= self.chunk_radius } - pub fn set_block_state(&self, pos: &BlockPos, state: BlockState) -> Option { - if pos.y < self.min_y() || pos.y >= (self.min_y() + self.height() as i32) { + pub fn set_block_state( + &self, + pos: &BlockPos, + state: BlockState, + chunk_storage: &mut ChunkStorage, + ) -> Option { + if pos.y < chunk_storage.min_y + || pos.y >= (chunk_storage.min_y + chunk_storage.height as i32) + { return None; } let chunk_pos = ChunkPos::from(pos); - let chunk = self.get(&chunk_pos)?; - let mut chunk = chunk.write(); - Some(chunk.get_and_set(&ChunkBlockPos::from(pos), state, self.min_y())) + let chunk_lock = chunk_storage.get(&chunk_pos)?; + let mut chunk = chunk_lock.write(); + Some(chunk.get_and_set(&ChunkBlockPos::from(pos), state, chunk_storage.min_y)) } pub fn replace_with_packet_data( &mut self, pos: &ChunkPos, data: &mut Cursor<&[u8]>, + chunk_storage: &mut ChunkStorage, ) -> Result<(), BufReadError> { debug!("Replacing chunk at {:?}", pos); if !self.in_range(pos) { @@ -137,11 +125,11 @@ impl PartialChunkStorage { let chunk = Arc::new(RwLock::new(Chunk::read_with_dimension_height( data, - self.height(), + chunk_storage.height, )?)); trace!("Loaded chunk {:?}", pos); - self.set(pos, Some(chunk)); + self.set(pos, Some(chunk), chunk_storage); Ok(()) } @@ -172,26 +160,19 @@ impl PartialChunkStorage { Some(&mut self.chunks[index]) } - /// Get a chunk, - pub fn get(&self, pos: &ChunkPos) -> Option>> { - self.shared - .read() - .chunks - .get(pos) - .and_then(|chunk| chunk.upgrade()) - } - /// Set a chunk in the shared storage and reference it from the limited /// storage. /// /// # Panics /// If the chunk is not in the render distance. - pub fn set(&mut self, pos: &ChunkPos, chunk: Option>>) { + pub fn set( + &mut self, + pos: &ChunkPos, + chunk: Option>>, + chunk_storage: &mut ChunkStorage, + ) { if let Some(chunk) = &chunk { - self.shared - .write() - .chunks - .insert(*pos, Arc::downgrade(chunk)); + chunk_storage.chunks.insert(*pos, Arc::downgrade(chunk)); } else { // don't remove it from the shared storage, since it'll be removed // automatically if this was the last reference @@ -201,9 +182,9 @@ impl PartialChunkStorage { } } } -impl WeakChunkStorage { +impl ChunkStorage { pub fn new(height: u32, min_y: i32) -> Self { - WeakChunkStorage { + ChunkStorage { height, min_y, chunks: HashMap::new(), @@ -295,12 +276,10 @@ impl McBufWritable for Chunk { impl Debug for PartialChunkStorage { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ChunkStorage") + f.debug_struct("PartialChunkStorage") .field("view_center", &self.view_center) .field("chunk_radius", &self.chunk_radius) .field("view_range", &self.view_range) - .field("height", &self.height()) - .field("min_y", &self.min_y()) // .field("chunks", &self.chunks) .field("chunks", &format_args!("{} items", self.chunks.len())) .finish() @@ -373,10 +352,10 @@ impl Section { impl Default for PartialChunkStorage { fn default() -> Self { - Self::new(8, Arc::new(RwLock::new(WeakChunkStorage::default()))) + Self::new(8) } } -impl Default for WeakChunkStorage { +impl Default for ChunkStorage { fn default() -> Self { Self::new(384, -64) } @@ -408,34 +387,26 @@ mod tests { #[test] fn test_out_of_bounds_y() { - let mut chunk_storage = PartialChunkStorage::default(); - chunk_storage.set( + let mut chunk_storage = ChunkStorage::default(); + let mut partial_chunk_storage = PartialChunkStorage::default(); + partial_chunk_storage.set( &ChunkPos { x: 0, z: 0 }, Some(Arc::new(RwLock::new(Chunk::default()))), + &mut chunk_storage, ); assert!(chunk_storage - .shared - .read() .get_block_state(&BlockPos { x: 0, y: 319, z: 0 }) .is_some()); assert!(chunk_storage - .shared - .read() .get_block_state(&BlockPos { x: 0, y: 320, z: 0 }) .is_none()); assert!(chunk_storage - .shared - .read() .get_block_state(&BlockPos { x: 0, y: 338, z: 0 }) .is_none()); assert!(chunk_storage - .shared - .read() .get_block_state(&BlockPos { x: 0, y: -64, z: 0 }) .is_some()); assert!(chunk_storage - .shared - .read() .get_block_state(&BlockPos { x: 0, y: -65, z: 0 }) .is_none()); } diff --git a/azalea-world/src/container.rs b/azalea-world/src/container.rs index 97939c6c..966e2c3b 100644 --- a/azalea-world/src/container.rs +++ b/azalea-world/src/container.rs @@ -1,18 +1,18 @@ use azalea_core::ResourceLocation; use bevy_ecs::system::Resource; use log::error; -use parking_lot::Mutex; +use parking_lot::RwLock; use std::{ collections::HashMap, sync::{Arc, Weak}, }; -use crate::World; +use crate::{ChunkStorage, World}; -/// A container of [`WeakWorld`]s. Worlds are stored as a Weak pointer here, so +/// A container of [`World`]s. Worlds are stored as a Weak pointer here, so /// if no clients are using a world it will be forgotten. #[derive(Default, Resource)] -pub struct WeakWorldContainer { +pub struct WorldContainer { // We just refer to the chunks here and don't include entities because there's not that many // cases where we'd want to get every entity in the world (just getting the entities in chunks // should work fine). @@ -23,49 +23,52 @@ pub struct WeakWorldContainer { // If it looks like we're relying on the server giving us unique world names, that's because we // are. An evil server could give us two worlds with the same name and then we'd have no way of // telling them apart. We hope most servers are nice and don't do that though. It's only an - // issue when there's multiple clients with the same WorldContainer in different worlds + // issue when there's multiple clients with the same WeakWorldContainer in different worlds // anyways. - pub worlds: HashMap, - - /// The ECS world that contains all of the entities in all of the worlds. - pub ecs: Arc>, + pub worlds: HashMap>>, } -impl WeakWorldContainer { +impl WorldContainer { pub fn new() -> Self { - WeakWorldContainer { + WorldContainer { worlds: HashMap::new(), - ecs: Arc::new(Mutex::new(bevy_ecs::world::World::new())), } } /// Get a world from the container. - pub fn get(&self, name: &ResourceLocation) -> Option> { + pub fn get(&self, name: &ResourceLocation) -> Option>> { self.worlds.get(name).and_then(|world| world.upgrade()) } /// Add an empty world to the container (or not if it already exists) and /// returns a strong reference to the world. #[must_use = "the world will be immediately forgotten if unused"] - pub fn insert(&mut self, name: ResourceLocation, height: u32, min_y: i32) -> Arc { - if let Some(existing) = self.worlds.get(&name).and_then(|world| world.upgrade()) { - if existing.height() != height { + pub fn insert( + &mut self, + name: ResourceLocation, + height: u32, + min_y: i32, + ) -> Arc> { + if let Some(existing_lock) = self.worlds.get(&name).and_then(|world| world.upgrade()) { + let existing = existing_lock.read(); + if existing.chunks.height != height { error!( "Shared dimension height mismatch: {} != {}", - existing.height(), - height, + existing.chunks.height, height, ); } - if existing.min_y() != min_y { + if existing.chunks.min_y != min_y { error!( "Shared world min_y mismatch: {} != {}", - existing.min_y(), - min_y, + existing.chunks.min_y, min_y, ); } - existing + existing_lock.clone() } else { - let world = Arc::new(World::new(height, min_y, self.ecs.clone())); + let world = Arc::new(RwLock::new(World { + chunks: ChunkStorage::new(height, min_y), + entities_by_chunk: HashMap::new(), + })); self.worlds.insert(name, Arc::downgrade(&world)); world } diff --git a/azalea-world/src/entity/mod.rs b/azalea-world/src/entity/mod.rs index e201aaf0..21b247a9 100644 --- a/azalea-world/src/entity/mod.rs +++ b/azalea-world/src/entity/mod.rs @@ -3,17 +3,17 @@ mod data; mod dimensions; pub mod metadata; +use crate::ChunkStorage; + use self::{attributes::AttributeInstance, metadata::UpdateMetadataError}; pub use attributes::Attributes; use azalea_block::BlockState; -use azalea_core::{BlockPos, ChunkPos, Vec3, AABB}; -use bevy_ecs::{ - bundle::Bundle, component::Component, query::Changed, system::Query, world::EntityMut, -}; +use azalea_core::{BlockPos, ChunkPos, ResourceLocation, Vec3, AABB}; +use bevy_ecs::{bundle::Bundle, component::Component, query::Changed, system::Query}; pub use data::*; use derive_more::{Deref, DerefMut}; pub use dimensions::EntityDimensions; -use std::fmt::{Debug, Display, Formatter}; +use std::fmt::Debug; use uuid::Uuid; /// A lightweight identifier of an entity. @@ -41,8 +41,8 @@ impl nohash_hasher::IsEnabled for MinecraftEntityId {} /// /// # Safety /// Cached position in the world must be updated. -pub fn update_bounding_box(query: Query<(&mut Position, &Physics), Changed>) { - for (mut position, physics) in query.iter_mut() { +pub fn update_bounding_box(mut query: Query<(&Position, &mut Physics), Changed>) { + for (position, mut physics) in query.iter_mut() { let bounding_box = physics.dimensions.make_bounding_box(&position); physics.bounding_box = bounding_box; } @@ -95,8 +95,12 @@ pub fn make_bounding_box(pos: &Position, physics: &Physics) -> AABB { } /// Get the position of the block below the entity, but a little lower. -pub fn on_pos_legacy(world: &World, position: &Position, physics: &Physics) -> BlockPos { - on_pos(world, position, physics, 0.2) +pub fn on_pos_legacy( + chunk_storage: &ChunkStorage, + position: &Position, + physics: &Physics, +) -> BlockPos { + on_pos(0.2, chunk_storage, position, physics) } // int x = Mth.floor(this.position.x); @@ -111,7 +115,12 @@ pub fn on_pos_legacy(world: &World, position: &Position, physics: &Physics) -> B // } // } // return var5; -pub fn on_pos(world: &World, pos: &Position, physics: &Physics, offset: f32) -> BlockPos { +pub fn on_pos( + offset: f32, + chunk_storage: &ChunkStorage, + pos: &Position, + physics: &Physics, +) -> BlockPos { let x = pos.x.floor() as i32; let y = (pos.y - offset as f64).floor() as i32; let z = pos.z.floor() as i32; @@ -119,10 +128,10 @@ pub fn on_pos(world: &World, pos: &Position, physics: &Physics, offset: f32) -> // TODO: check if block below is a fence, wall, or fence gate let block_pos = pos.down(1); - let block_state = world.get_block_state(&block_pos); + let block_state = chunk_storage.get_block_state(&block_pos); if block_state == Some(BlockState::Air) { let block_pos_below = block_pos.down(1); - let block_state_below = world.get_block_state(&block_pos_below); + let block_state_below = chunk_storage.get_block_state(&block_pos_below); if let Some(_block_state_below) = block_state_below { // if block_state_below.is_fence() // || block_state_below.is_wall() @@ -154,30 +163,45 @@ impl From for BlockPos { BlockPos::from(&value.0) } } +impl From<&Position> for ChunkPos { + fn from(value: &Position) -> Self { + ChunkPos::from(value.0) + } +} +impl From<&Position> for BlockPos { + fn from(value: &Position) -> Self { + BlockPos::from(value.0) + } +} -/// The position of the entity last tick. +/// The last position of the entity that was sent to the network. #[derive(Component, Clone, Copy, Debug, Default, PartialEq, Deref, DerefMut)] -pub struct LastPosition(Vec3); -impl From for ChunkPos { - fn from(value: LastPosition) -> Self { +pub struct LastSentPosition(Vec3); +impl From for ChunkPos { + fn from(value: LastSentPosition) -> Self { ChunkPos::from(&value.0) } } -impl From for BlockPos { - fn from(value: LastPosition) -> Self { +impl From for BlockPos { + fn from(value: LastSentPosition) -> Self { BlockPos::from(&value.0) } } - -/// Set the [`LastPosition`] component to the current [`Position`] component. -/// This should happen at the end of every tick. -pub fn update_last_position( - mut query: Query<(&mut Position, &mut LastPosition), Changed>, -) { - for (mut position, mut last_position) in query.iter_mut() { - *last_position = LastPosition(**position); +impl From<&LastSentPosition> for ChunkPos { + fn from(value: &LastSentPosition) -> Self { + ChunkPos::from(value.0) } } +impl From<&LastSentPosition> for BlockPos { + fn from(value: &LastSentPosition) -> Self { + BlockPos::from(value.0) + } +} + +/// The name of the world the entity is in. If two entities share the same world +/// name, we assume they're in the same world. +#[derive(Component, Clone, Debug, PartialEq, Deref, DerefMut)] +pub struct WorldName(ResourceLocation); /// The physics data relating to the entity, such as position, velocity, and /// bounding box. @@ -227,14 +251,20 @@ pub struct EntityKind(azalea_registry::EntityKind); pub struct EntityBundle { pub kind: EntityKind, pub uuid: EntityUuid, + pub world_name: WorldName, pub position: Position, - pub last_position: LastPosition, + pub last_sent_position: LastSentPosition, pub physics: Physics, pub attributes: Attributes, } impl EntityBundle { - pub fn new(uuid: Uuid, pos: Vec3, kind: azalea_registry::EntityKind) -> Self { + pub fn new( + uuid: Uuid, + pos: Vec3, + kind: azalea_registry::EntityKind, + world_name: ResourceLocation, + ) -> Self { let dimensions = EntityDimensions { width: 0.6, height: 1.8, @@ -243,8 +273,9 @@ impl EntityBundle { Self { kind: EntityKind(kind), uuid: EntityUuid(uuid), + world_name: WorldName(world_name), position: Position(pos), - last_position: Position(pos), + last_sent_position: LastSentPosition(pos), physics: Physics { delta: Vec3::default(), diff --git a/azalea-world/src/entity_info.rs b/azalea-world/src/entity_info.rs index ab168a58..ebc5fc1a 100644 --- a/azalea-world/src/entity_info.rs +++ b/azalea-world/src/entity_info.rs @@ -1,20 +1,21 @@ -use crate::entity::{self, Entity, EntityUuid, MinecraftEntityId, Position}; -use azalea_core::{ChunkPos, ResourceLocation, Vec3}; +use crate::{ + entity::{self, update_bounding_box, Entity, MinecraftEntityId}, + MaybeRemovedEntity, World, WorldContainer, +}; +use azalea_core::ChunkPos; use bevy_app::{App, CoreStage, Plugin}; use bevy_ecs::{ - query::{Changed, QueryEntityError, QueryState, ReadOnlyWorldQuery, WorldQuery}, + query::Changed, schedule::SystemSet, - system::{Query, ResMut, Resource}, - world::{EntityMut, EntityRef}, + system::{Query, Res, ResMut, Resource}, }; use log::warn; use nohash_hasher::{IntMap, IntSet}; use once_cell::sync::Lazy; -use parking_lot::{Mutex, RwLock}; use std::{ collections::{HashMap, HashSet}, fmt::Debug, - sync::{Arc, Weak}, + ops::DerefMut, }; use uuid::Uuid; @@ -23,8 +24,11 @@ pub struct EntityPlugin; impl Plugin for EntityPlugin { fn build(&self, app: &mut App) { app.add_system_set_to_stage( - CoreStage::Last, - SystemSet::new().with_system(update_entity_chunk_positions), + CoreStage::PostUpdate, + SystemSet::new() + .with_system(update_entity_chunk_positions) + .with_system(remove_despawned_entities_from_indexes) + .with_system(update_bounding_box), ); } } @@ -115,21 +119,25 @@ impl PartialEntityInfos { /// we WILL update it if it's true. Don't call this unless you actually /// got an entity update that all other clients within render distance /// will get too. - pub fn maybe_update(&mut self, id: MinecraftEntityId, entity_infos: &mut EntityInfos) -> bool { - let this_client_updates_received = self.updates_received.get(&id).copied(); + pub fn maybe_update( + &mut self, + entity: Entity, + id: &MinecraftEntityId, + entity_infos: &mut EntityInfos, + ) -> bool { + if Some(entity) == self.owner_entity { + // the owner of the entity is always allowed to update it + return true; + }; - let entity = entity_infos - .minecraft_entity_ids_to_azalea_entities - .get(&id) - .expect("entity should be in minecraft_entity_ids_to_azalea_entities") - .clone(); + let this_client_updates_received = self.updates_received.get(&id).copied(); let shared_updates_received = entity_infos.updates_received.get(&entity).copied(); let can_update = this_client_updates_received == shared_updates_received; if can_update { let new_updates_received = this_client_updates_received.unwrap_or(0) + 1; - self.updates_received.insert(id, new_updates_received); + self.updates_received.insert(*id, new_updates_received); entity_infos .updates_received .insert(entity, new_updates_received); @@ -140,10 +148,6 @@ impl PartialEntityInfos { } } -/// This is useful so functions that return an IntSet of entity ids can return a -/// reference to nothing. -static EMPTY_ENTITY_ID_INTSET: Lazy> = Lazy::new(|| HashSet::default()); - // TODO: optimization: switch out the `HashMap`s for `IntMap`s /// Things that are shared between all the partial worlds. @@ -154,9 +158,6 @@ pub struct EntityInfos { /// The number of `PartialWorld`s that have this entity loaded. /// (this is reference counting) pub(crate) entity_reference_count: HashMap, - /// An index of all the entities we know are in a chunk - pub(crate) entities_by_world_name_and_chunk: - HashMap>>, /// An index of entities by their UUIDs pub(crate) entity_by_uuid: HashMap, @@ -171,7 +172,6 @@ impl EntityInfos { pub fn new() -> Self { Self { entity_reference_count: HashMap::default(), - entities_by_world_name_and_chunk: HashMap::default(), entity_by_uuid: HashMap::default(), updates_received: HashMap::default(), @@ -185,7 +185,13 @@ impl EntityInfos { /// storage if there's no more references to it. /// /// Returns whether the entity was removed. - pub fn remove_entity_if_unused(&mut self, entity: Entity, uuid: Uuid, chunk: ChunkPos) -> bool { + pub fn remove_entity_if_unused( + &mut self, + entity: Entity, + uuid: Uuid, + chunk: ChunkPos, + world: &mut World, + ) -> bool { if let Some(count) = self.entity_reference_count.get_mut(&entity) { *count -= 1; if *count == 0 { @@ -196,7 +202,7 @@ impl EntityInfos { warn!("Tried to remove entity but it was not found."); return false; } - if self.entities_by_chunk.remove(&chunk).is_none() { + if world.entities_by_chunk.remove(&chunk).is_none() { warn!("Tried to remove entity from chunk {chunk:?} but it was not found."); } if self.entity_by_uuid.remove(&uuid).is_none() { @@ -210,16 +216,6 @@ impl EntityInfos { true } - /// Remove a chunk from the storage if the entities in it have no strong - /// references left. - pub fn remove_chunk_if_unused(&mut self, chunk: &ChunkPos) { - if let Some(entities) = self.entities_by_chunk.get(chunk) { - if entities.is_empty() { - self.entities_by_chunk.remove(chunk); - } - } - } - /// Whether the entity is in the shared storage. To check if a Minecraft /// entity ID is in the storage, you'll have to use /// [`PartialEntityInfo::limited_contains_id`]. @@ -228,13 +224,6 @@ impl EntityInfos { self.entity_reference_count.contains_key(&id) } - /// Returns a set of entities in the given chunk. - pub fn entities_in_chunk(&self, chunk: &ChunkPos) -> &HashSet { - self.entities_by_chunk - .get(chunk) - .unwrap_or(&EMPTY_ENTITY_ID_INTSET) - } - pub fn entity_by_uuid(&self, uuid: &Uuid) -> Option<&Entity> { self.entity_by_uuid.get(uuid) } @@ -242,27 +231,30 @@ impl EntityInfos { /// Update the chunk position indexes in [`EntityInfos`]. fn update_entity_chunk_positions( - query: Query< + mut query: Query< ( Entity, &entity::Position, - &mut entity::LastPosition, - &mut entity::Physics, + &mut entity::LastSentPosition, + &entity::WorldName, ), Changed, >, - entity_infos: ResMut, + world_container: Res, ) { - for (entity, pos, last_pos, mut physics) in query.iter_mut() { + for (entity, pos, last_pos, world_name) in query.iter_mut() { + let world_lock = world_container.get(&**world_name).unwrap(); + let mut world = world_lock.write(); + let old_chunk = ChunkPos::from(*last_pos); let new_chunk = ChunkPos::from(*pos); if old_chunk != new_chunk { // move the entity from the old chunk to the new one - if let Some(entities) = entity_infos.entities_by_chunk.get_mut(&old_chunk) { + if let Some(entities) = world.entities_by_chunk.get_mut(&old_chunk) { entities.remove(&entity); } - entity_infos + world .entities_by_chunk .entry(new_chunk) .or_default() @@ -271,6 +263,40 @@ fn update_entity_chunk_positions( } } +pub fn remove_despawned_entities_from_indexes( + mut entity_infos: ResMut, + world_container: Res, + query: Query< + ( + Entity, + &entity::EntityUuid, + &entity::Position, + &entity::WorldName, + ), + &MaybeRemovedEntity, + >, +) { + for (entity, uuid, position, world_name) in &query { + let world = world_container.get(world_name).unwrap(); + entity_infos.remove_entity_if_unused( + entity, + **uuid, + (*position).into(), + world.write().deref_mut(), + ); + } +} + +/// Remove a chunk from the storage if the entities in it have no strong +/// references left. +pub fn remove_chunk_if_unused(world: &mut World, chunk: &ChunkPos) { + if let Some(entities) = world.entities_by_chunk.get(chunk) { + if entities.is_empty() { + world.entities_by_chunk.remove(chunk); + } + } +} + impl Debug for EntityInfos { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("EntityInfos").finish() diff --git a/azalea-world/src/lib.rs b/azalea-world/src/lib.rs index 1dcff1c0..a97cb0e8 100644 --- a/azalea-world/src/lib.rs +++ b/azalea-world/src/lib.rs @@ -13,7 +13,7 @@ mod world; use std::backtrace::Backtrace; pub use bit_storage::BitStorage; -pub use chunk_storage::{Chunk, ChunkStorage, PartialChunkStorage, WeakChunkStorage}; +pub use chunk_storage::{Chunk, ChunkStorage, PartialChunkStorage}; pub use container::*; pub use entity_info::{EntityInfos, PartialEntityInfos}; use thiserror::Error; diff --git a/azalea-world/src/world.rs b/azalea-world/src/world.rs index aecdc430..ae67510e 100644 --- a/azalea-world/src/world.rs +++ b/azalea-world/src/world.rs @@ -1,15 +1,18 @@ use crate::{ - entity::{self, Entity, MinecraftEntityId}, - Chunk, EntityInfos, MoveEntityError, PartialChunkStorage, PartialEntityInfos, WeakChunkStorage, + entity::{self, Entity, MinecraftEntityId, WorldName}, + Chunk, ChunkStorage, EntityInfos, MoveEntityError, PartialChunkStorage, PartialEntityInfos, + WorldContainer, +}; +use azalea_core::{ChunkPos, PositionDelta8}; +use bevy_ecs::{ + component::Component, + system::{Commands, Query}, +}; +use std::fmt::Formatter; +use std::{ + collections::{HashMap, HashSet}, + fmt::Debug, }; -use azalea_block::BlockState; -use azalea_buf::BufReadError; -use azalea_core::{BlockPos, ChunkPos, PositionDelta8, Vec3}; -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; /// PartialWorlds are usually owned by clients, and hold strong references to /// chunks and entities in [`WeakWorld`]s. @@ -20,265 +23,127 @@ use uuid::Uuid; /// /// This is primarily useful for having multiple clients in the same world. pub struct PartialWorld { - // we just need to keep a strong reference to `shared` so it doesn't get - // dropped, we don't need to do anything with it - pub shared: Arc, - pub chunks: PartialChunkStorage, + /// Some metadata about entities, like what entities are in certain chunks. + /// This does not contain the entity data itself, that's in the ECS. pub entity_infos: PartialEntityInfos, } impl PartialWorld { pub fn new( chunk_radius: u32, - shared: Arc, owner_entity: Option, entity_infos: &mut EntityInfos, ) -> Self { PartialWorld { - shared: shared.clone(), - chunks: PartialChunkStorage::new(chunk_radius, shared.chunks.clone()), + chunks: PartialChunkStorage::new(chunk_radius), entity_infos: PartialEntityInfos::new(owner_entity, entity_infos), } } - pub fn replace_with_packet_data( - &mut self, - pos: &ChunkPos, - data: &mut Cursor<&[u8]>, - ) -> Result<(), BufReadError> { - self.chunks.replace_with_packet_data(pos, data) - } - - pub fn get_chunk(&self, pos: &ChunkPos) -> Option>> { - self.chunks.get(pos) - } - - pub fn set_chunk(&mut self, pos: &ChunkPos, chunk: Option) -> Result<(), BufReadError> { - self.chunks - .set(pos, chunk.map(|c| Arc::new(RwLock::new(c)))); - Ok(()) - } - - pub fn update_view_center(&mut self, pos: &ChunkPos) { - self.chunks.view_center = *pos; - } - - pub fn set_block_state(&mut self, pos: &BlockPos, state: BlockState) -> Option { - self.chunks.set_block_state(pos, state) - } - - /// Whether we're allowed to update the entity with the given ID. - /// - /// Only call this if you're actually updating the entity, otherwise it'll - /// cause the update tracker to get out of sync. - fn maybe_update_entity(&mut self, entity: Entity, entity_infos: &mut EntityInfos) -> bool { - // no entity for you (we're processing this entity somewhere else) - if Some(entity) != self.entity_infos.owner_entity - && !self.entity_infos.maybe_update(id, entity_infos) - { - false - } else { - true - } - } - - /// Makes sure we can modify this EntityId, and returns it if we can. - /// - /// Only call this if you're actually updating the entity, otherwise it'll - /// cause the update tracker to get out of sync. - pub fn entity_mut(&mut self, id: EntityId) -> Option { - if self.maybe_update_entity(id) { - Some(id) - } else { - None - } - } - - 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 - .entity_infos - .write() - .set_entity_pos(entity_id, new_pos, pos, physics); - Ok(()) - } else { - Err(MoveEntityError::EntityDoesNotExist(Backtrace::capture())) - } - } - - pub fn move_entity_with_delta( - &mut self, - entity_id: EntityId, - delta: &PositionDelta8, - ) -> Result<(), MoveEntityError> { - 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, &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; + pub fn add_entity( + &mut self, + commands: &mut Commands, + bundle: impl bevy_ecs::bundle::Bundle, + entity_infos: &mut EntityInfos, + world: &mut World, + query: Query<(&entity::Position, &MinecraftEntityId, &entity::EntityUuid)>, + id_query: Query<&MinecraftEntityId>, + ) { + let mut entity_commands = commands.spawn(bundle); + let entity = entity_commands.id(); + let (position, &id, uuid) = query.get(entity).unwrap(); + let chunk_pos = ChunkPos::from(*position); + + // check every entity in this entitys chunk to make sure it doesn't already + // exist there + if let Some(entities_in_chunk) = world.entities_by_chunk.get(&chunk_pos) { + for entity in entities_in_chunk { + if id_query.get(*entity).unwrap() == &id { + // the entity is already in the world, so remove that extra entity we just made + entity_commands.despawn(); + 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(); + partial_entity_infos.loaded_entity_ids.insert(id); // add the entity to the indexes - shared_entity_infos - .ids_by_chunk - .entry(ChunkPos::from(&pos)) + world + .entities_by_chunk + .entry(chunk_pos) .or_default() - .insert(id); - shared_entity_infos.id_by_uuid.insert(*uuid, id); - partial_entity_infos.loaded_entity_ids.insert(id); + .insert(entity); + entity_infos.entity_by_uuid.insert(**uuid, entity); // 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) { + if let Some(&shared_updates_received) = entity_infos.updates_received.get(&entity) { // 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); + if shared_updates_received != 0 || Some(entity) == partial_entity_infos.owner_entity { + partial_entity_infos + .updates_received + .insert(id, shared_updates_received); } } else { - shared_entity_infos.updates_received.insert(id, 1); + entity_infos.updates_received.insert(entity, 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(); +pub unsafe fn move_entity_with_delta(delta: &PositionDelta8, position: &mut entity::Position) { + let new_pos = position.with_delta(delta); + **position = new_pos; +} - if partial_entity_infos.loaded_entity_ids.remove(&id) { - let mut ecs = self.shared.global_ecs.lock(); +/// A component marker signifying that the entity may have been removed from the +/// world, but we're not entirely sure. +#[derive(Component)] +pub struct MaybeRemovedEntity; - 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; +/// 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( + commands: &mut Commands, + partial_entity_infos: &mut PartialEntityInfos, + chunk: &ChunkPos, + world_container: &WorldContainer, + world_name: &WorldName, + query: Query<&MinecraftEntityId>, +) { + let world_lock = world_container.get(world_name).unwrap(); + let world = world_lock.read(); - // 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); - } + if let Some(entities) = world.entities_by_chunk.get(chunk).cloned() { + for &entity in &entities { + let id = query.get(entity).unwrap(); + if partial_entity_infos.loaded_entity_ids.remove(&id) { + // maybe remove it from the storage + commands.entity(entity).insert(MaybeRemovedEntity); } } } } -// /// A world where the chunks are stored as weak pointers. This is used for -// /// shared worlds. -// #[derive(Default, Debug)] -// pub struct World { -// pub chunks: Arc>, -// } +/// A world where the chunks are stored as weak pointers. This is used for +/// shared worlds. +#[derive(Default, Debug)] +pub struct World { + pub chunks: ChunkStorage, -// impl World { -// pub fn new(height: u32, min_y: i32) -> Self { -// World { -// chunks: Arc::new(RwLock::new(WeakChunkStorage::new(height, -// min_y))), } -// } - -// /// Read the total height of the world. You can add this to -// [`Self::min_y`] /// to get the highest possible y coordinate a block can -// be placed at. pub fn height(&self) -> u32 { -// self.chunks.read().height -// } - -// /// Get the lowest possible y coordinate a block can be placed at. -// pub fn min_y(&self) -> i32 { -// 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 { -// self.entity_infos.read().id_by_uuid(uuid).copied() -// } - -// pub fn get_block_state(&self, pos: &BlockPos) -> Option { -// self.chunks.read().get_block_state(pos) -// } - -// pub fn get_chunk(&self, pos: &ChunkPos) -> Option>> { -// 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); -// } -// } + /// An index of all the entities we know are in the chunks of the world + pub entities_by_chunk: HashMap>, +} 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.entity_infos) - .field("shared", &self.shared) .finish() } } @@ -290,11 +155,6 @@ impl Default for PartialWorld { let chunk_storage = PartialChunkStorage::default(); let entity_storage = PartialEntityInfos::default(); Self { - shared: Arc::new(World { - chunks: chunk_storage.shared.clone(), - entity_infos: entity_storage.shared.clone(), - global_ecs: Arc::new(Mutex::new(bevy_ecs::world::World::default())), - }), chunks: chunk_storage, entity_infos: entity_storage, } diff --git a/azalea/src/swarm/mod.rs b/azalea/src/swarm/mod.rs index 37a95321..a2c92599 100644 --- a/azalea/src/swarm/mod.rs +++ b/azalea/src/swarm/mod.rs @@ -10,7 +10,7 @@ use azalea_protocol::{ resolver::{self, ResolverError}, ServerAddress, }; -use azalea_world::WeakWorldContainer; +use azalea_world::WorldContainer; use futures::future::join_all; use log::error; use parking_lot::{Mutex, RwLock}; @@ -52,7 +52,7 @@ pub struct Swarm { resolved_address: SocketAddr, address: ServerAddress, - pub worlds: Arc>, + pub worlds: Arc>, /// Plugins that are set for new bots plugins: Plugins, @@ -228,7 +228,7 @@ pub async fn start_swarm< // resolve the address let resolved_address = resolver::resolve_address(&address).await?; - let world_container = Arc::new(RwLock::new(WeakWorldContainer::default())); + let world_container = Arc::new(RwLock::new(WorldContainer::default())); let mut plugins = options.plugins; let swarm_plugins = options.swarm_plugins;