From 78238251e90ea011d217d3afb6b1821bfaec877b Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 3 Jul 2023 08:33:41 -0700 Subject: [PATCH 01/47] message buffer --- crates/valence_instance/src/instance.rs | 2 +- crates/valence_instance/src/lib.rs | 1 + crates/valence_instance/src/message.rs | 205 ++++++++++++++++++++++++ 3 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 crates/valence_instance/src/message.rs diff --git a/crates/valence_instance/src/instance.rs b/crates/valence_instance/src/instance.rs index e05bc0adb..e71c812c3 100644 --- a/crates/valence_instance/src/instance.rs +++ b/crates/valence_instance/src/instance.rs @@ -4,7 +4,7 @@ use std::borrow::Cow; use std::collections::hash_map::{Entry, OccupiedEntry, VacantEntry}; use bevy_ecs::prelude::*; -use glam::{DVec3, Vec3}; +use glam::{DVec3, Vec3, u32}; use num_integer::div_ceil; use rustc_hash::FxHashMap; use valence_biome::BiomeRegistry; diff --git a/crates/valence_instance/src/lib.rs b/crates/valence_instance/src/lib.rs index d9f9fb34e..84b35c9b3 100644 --- a/crates/valence_instance/src/lib.rs +++ b/crates/valence_instance/src/lib.rs @@ -43,6 +43,7 @@ use valence_entity::{ pub mod chunk; mod instance; pub mod packet; +pub mod message; pub use chunk::{Block, BlockRef}; pub use instance::*; diff --git a/crates/valence_instance/src/message.rs b/crates/valence_instance/src/message.rs new file mode 100644 index 000000000..7fac188ba --- /dev/null +++ b/crates/valence_instance/src/message.rs @@ -0,0 +1,205 @@ +use bevy_ecs::entity::Entity; +use glam::DVec3; +use valence_core::{ + chunk_pos::ChunkPos, + protocol::{ + encode::{PacketWriter, WritePacket}, + Encode, Packet, + }, +}; + +#[derive(Clone, Debug)] +pub struct MessageBuffer { + messages: Vec, + bytes: Vec, + compression_threshold: Option, +} + +impl MessageBuffer { + pub fn new(compression_threshold: Option) -> Self { + Self { + messages: vec![], + bytes: vec![], + compression_threshold, + } + } + + pub fn messages(&self) -> &[Message] { + &self.messages + } + + pub fn bytes(&self) -> &[u8] { + &self.bytes + } + + pub fn append_packet

(&mut self, cond: MessageCondition, pkt: &P) + where + P: Packet + Encode, + { + let threshold = self.compression_threshold; + + self.append(cond, |bytes| { + PacketWriter::new(bytes, threshold).write_packet(pkt) + }) + } + + pub fn append_packet_bytes(&mut self, cond: MessageCondition, bytes: &[u8]) { + if !bytes.is_empty() { + self.append(cond, |b| b.extend_from_slice(bytes)); + } + } + + fn append(&mut self, cond: MessageCondition, append_data: impl FnOnce(&mut Vec)) { + const LOOKBACK_BYTE_LIMIT: usize = 512; + const LOOKBACK_MSG_LIMIT: usize = 64; + + // Look for a message with an identical condition to ours. If we find one, move it to the front and merge our message with it. + + let mut acc = 0; + + // Special case for the most recent message. + if let Some(msg) = self.messages.last_mut() { + if msg.cond == cond { + let old_len = self.bytes.len(); + append_data(&mut self.bytes); + let new_len = self.bytes.len(); + + msg.len += new_len - old_len; + + return; + } + + acc += msg.len; + } + + for (i, msg) in self + .messages + .iter() + .enumerate() + .rev() + .take(LOOKBACK_MSG_LIMIT) + .skip(1) + { + acc += msg.len; + + if acc > LOOKBACK_BYTE_LIMIT { + break; + } + + if msg.cond == cond { + let mut msg = self.messages.remove(i); + + let start = self.bytes.len() - acc; + let range = start..start + msg.len; + + // Copy to the back and remove. + self.bytes.extend_from_within(range.clone()); + self.bytes.drain(range); + + let old_len = self.bytes.len(); + append_data(&mut self.bytes); + let new_len = self.bytes.len(); + + msg.len += new_len - old_len; + + self.messages.push(msg); + + return; + } + } + + // Didn't find a compatible message, so append a new one to the end. + + let old_len = self.bytes.len(); + append_data(&mut self.bytes); + let new_len = self.bytes.len(); + + self.messages.push(Message { + cond, + len: new_len - old_len, + }); + } + + pub fn clear(&mut self) { + self.messages.clear(); + self.bytes.clear(); + } +} + +#[derive(Copy, Clone, PartialEq, Debug)] +pub struct Message { + pub cond: MessageCondition, + /// Length of this message in bytes. + pub len: usize, +} + +impl Message { + pub const fn new(cond: MessageCondition, len: usize) -> Self { + Self { cond, len } + } +} + +/// A condition that must be met in order for a client to receive packet data. +#[derive(PartialEq, Copy, Clone, Default, Debug)] +pub enum MessageCondition { + /// Data will be received unconditionally. + #[default] + All, + /// All clients excluding this specific client. + Except { + client: Entity, + }, + /// In view of this chunk position. + View { + pos: ChunkPos, + }, + ViewExcept { + pos: ChunkPos, + except: Entity, + }, + /// In view of `viewed` and not in view of `unviewed`. + TransitionView { + viewed: ChunkPos, + unviewed: ChunkPos, + }, + /// Client's position must be contained in this sphere. + Sphere { + center: DVec3, + radius: f64, + }, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn message_buffer_append() { + let cond1 = MessageCondition::All; + let cond2 = MessageCondition::Except { + client: Entity::PLACEHOLDER, + }; + let cond3 = MessageCondition::Sphere { + center: DVec3::ZERO, + radius: 10.0, + }; + + let mut buf = MessageBuffer::new(None); + + let bytes = &[1, 2, 3, 4, 5]; + + buf.append_packet_bytes(cond1, bytes); + buf.append_packet_bytes(cond2, bytes); + buf.append_packet_bytes(cond3, bytes); + + buf.append_packet_bytes(cond2, bytes); + buf.append_packet_bytes(cond3, bytes); + buf.append_packet_bytes(cond1, bytes); + + let msgs = buf.messages(); + + assert_eq!(msgs[0], Message::new(cond2, bytes.len() * 2)); + assert_eq!(msgs[1], Message::new(cond3, bytes.len() * 2)); + assert_eq!(msgs[2], Message::new(cond1, bytes.len() * 2)); + } +} From 505cc95751f7479a05ca68e3f6638014f8133b8e Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 4 Jul 2023 07:31:30 -0700 Subject: [PATCH 02/47] new `valence_instance`, reorganize `valence_client` --- benches/idle.rs | 12 +- benches/many_players.rs | 29 +- crates/valence_client/src/keepalive.rs | 6 +- crates/valence_client/src/lib.rs | 65 +---- crates/valence_client/src/packet.rs | 10 - crates/valence_core/build/chunk_pos.rs | 2 +- crates/valence_entity/build.rs | 4 +- crates/valence_entity/src/flags.rs | 144 ++++++++++ crates/valence_entity/src/layer.rs | 32 +++ crates/valence_entity/src/lib.rs | 282 +------------------- crates/valence_entity/src/packet.rs | 10 + crates/valence_entity/src/query.rs | 193 ++++++++++++++ crates/valence_entity/src/tracked_data.rs | 136 ++++++++++ crates/valence_instance/Cargo.toml | 1 + crates/valence_instance/src/chunk.rs | 12 +- crates/valence_instance/src/chunk/loaded.rs | 276 +++++++------------ crates/valence_instance/src/instance.rs | 95 ++----- crates/valence_instance/src/lib.rs | 258 +++++++----------- crates/valence_instance/src/message.rs | 87 +++--- 19 files changed, 827 insertions(+), 827 deletions(-) create mode 100644 crates/valence_entity/src/flags.rs create mode 100644 crates/valence_entity/src/layer.rs create mode 100644 crates/valence_entity/src/query.rs create mode 100644 crates/valence_entity/src/tracked_data.rs diff --git a/benches/idle.rs b/benches/idle.rs index cbe5f47eb..7634b8555 100644 --- a/benches/idle.rs +++ b/benches/idle.rs @@ -4,15 +4,15 @@ use valence::prelude::*; /// Benches the performance of a single server tick while nothing much is /// happening. pub fn idle_update(c: &mut Criterion) { - let mut app = App::new(); + c.bench_function("idle_update", |b| { + let mut app = App::new(); - app.add_plugins(DefaultPlugins); - app.add_systems(Startup, setup); + app.add_plugins(DefaultPlugins); + app.add_systems(Startup, setup); - // Run startup schedule. - app.update(); + // Run startup schedule. + app.update(); - c.bench_function("idle_update", |b| { b.iter(|| { app.update(); }); diff --git a/benches/many_players.rs b/benches/many_players.rs index f31c5b67e..81ab1d6eb 100644 --- a/benches/many_players.rs +++ b/benches/many_players.rs @@ -18,11 +18,18 @@ use valence_instance::chunk::UnloadedChunk; use valence_instance::Instance; use valence_network::NetworkPlugin; -const CLIENT_COUNT: usize = 3000; -const VIEW_DIST: u8 = 20; -const INST_SIZE: i32 = 16; - pub fn many_players(c: &mut Criterion) { + run_many_players(c, "many_players", 3000, 20, 16); + run_many_players(c, "many_players_spread_out", 3000, 8, 200); +} + +fn run_many_players( + c: &mut Criterion, + func_name: &str, + client_count: usize, + view_dist: u8, + inst_size: i32, +) { let mut app = App::new(); app.insert_resource(CoreSettings { @@ -45,8 +52,8 @@ pub fn many_players(c: &mut Criterion) { app.world.resource::(), ); - for z in -INST_SIZE..INST_SIZE { - for x in -INST_SIZE..INST_SIZE { + for z in -inst_size..inst_size { + for x in -inst_size..inst_size { inst.insert_chunk(ChunkPos::new(x, z), UnloadedChunk::new()); } } @@ -56,15 +63,15 @@ pub fn many_players(c: &mut Criterion) { let mut clients = vec![]; // Spawn a bunch of clients in at random initial positions in the instance. - for i in 0..CLIENT_COUNT { + for i in 0..client_count { let (mut bundle, helper) = create_mock_client(format!("client_{i}")); bundle.player.location.0 = inst_ent; - bundle.view_distance.set(VIEW_DIST); + bundle.view_distance.set(view_dist); let mut rng = rand::thread_rng(); - let x = rng.gen_range(-INST_SIZE as f64 * 16.0..=INST_SIZE as f64 * 16.0); - let z = rng.gen_range(-INST_SIZE as f64 * 16.0..=INST_SIZE as f64 * 16.0); + let x = rng.gen_range(-inst_size as f64 * 16.0..=inst_size as f64 * 16.0); + let z = rng.gen_range(-inst_size as f64 * 16.0..=inst_size as f64 * 16.0); bundle.player.position.set(DVec3::new(x, 64.0, z)); @@ -83,7 +90,7 @@ pub fn many_players(c: &mut Criterion) { app.update(); - c.bench_function("many_players", |b| { + c.bench_function(func_name, |b| { b.iter(|| { let mut rng = rand::thread_rng(); diff --git a/crates/valence_client/src/keepalive.rs b/crates/valence_client/src/keepalive.rs index bc8b5eaad..f92b97464 100644 --- a/crates/valence_client/src/keepalive.rs +++ b/crates/valence_client/src/keepalive.rs @@ -66,7 +66,7 @@ fn send_keepalive( } else { let millis = settings.period.as_millis(); warn!("Client {entity:?} timed out: no keepalive response after {millis}ms"); - commands.entity(entity).remove::(); + commands.entity(entity).insert(Despawned); } } } @@ -82,13 +82,13 @@ fn handle_keepalive_response( if let Ok((client, mut state, mut ping)) = clients.get_mut(packet.client) { if state.got_keepalive { warn!("unexpected keepalive from client {client:?}"); - commands.entity(client).remove::(); + commands.entity(entity).insert(Despawned); } else if pkt.id != state.last_keepalive_id { warn!( "keepalive IDs don't match for client {client:?} (expected {}, got {})", state.last_keepalive_id, pkt.id, ); - commands.entity(client).remove::(); + commands.entity(entity).insert(Despawned); } else { state.got_keepalive = true; ping.0 = state.last_send.elapsed().as_millis() as i32; diff --git a/crates/valence_client/src/lib.rs b/crates/valence_client/src/lib.rs index 72197e73a..8be6050af 100644 --- a/crates/valence_client/src/lib.rs +++ b/crates/valence_client/src/lib.rs @@ -431,7 +431,7 @@ impl Command for DisconnectClient { reason: self.reason.into(), }); - entity.remove::(); + entity.remove::(); // TODO ? } } } @@ -808,69 +808,6 @@ fn update_chunk_load_dist( } } -#[derive(WorldQuery)] -struct EntityInitQuery { - entity_id: &'static EntityId, - uuid: &'static UniqueId, - kind: &'static EntityKind, - look: &'static Look, - head_yaw: &'static HeadYaw, - on_ground: &'static OnGround, - object_data: &'static ObjectData, - velocity: &'static Velocity, - tracked_data: &'static TrackedData, -} - -impl EntityInitQueryItem<'_> { - /// Writes the appropriate packets to initialize an entity. This will spawn - /// the entity and initialize tracked data. - fn write_init_packets(&self, pos: DVec3, mut writer: impl WritePacket) { - match *self.kind { - EntityKind::MARKER => {} - EntityKind::EXPERIENCE_ORB => { - writer.write_packet(&ExperienceOrbSpawnS2c { - entity_id: self.entity_id.get().into(), - position: pos, - count: self.object_data.0 as i16, - }); - } - EntityKind::PLAYER => { - writer.write_packet(&PlayerSpawnS2c { - entity_id: self.entity_id.get().into(), - player_uuid: self.uuid.0, - position: pos, - yaw: ByteAngle::from_degrees(self.look.yaw), - pitch: ByteAngle::from_degrees(self.look.pitch), - }); - - // Player spawn packet doesn't include head yaw for some reason. - writer.write_packet(&EntitySetHeadYawS2c { - entity_id: self.entity_id.get().into(), - head_yaw: ByteAngle::from_degrees(self.head_yaw.0), - }); - } - _ => writer.write_packet(&EntitySpawnS2c { - entity_id: self.entity_id.get().into(), - object_uuid: self.uuid.0, - kind: self.kind.get().into(), - position: pos, - pitch: ByteAngle::from_degrees(self.look.pitch), - yaw: ByteAngle::from_degrees(self.look.yaw), - head_yaw: ByteAngle::from_degrees(self.head_yaw.0), - data: self.object_data.0.into(), - velocity: self.velocity.to_packet_units(), - }), - } - - if let Some(init_data) = self.tracked_data.init_data() { - writer.write_packet(&EntityTrackerUpdateS2c { - entity_id: self.entity_id.get().into(), - metadata: init_data.into(), - }); - } - } -} - fn read_data_in_old_view( mut clients: Query<( Entity, diff --git a/crates/valence_client/src/packet.rs b/crates/valence_client/src/packet.rs index 989826f1a..c549b2456 100644 --- a/crates/valence_client/src/packet.rs +++ b/crates/valence_client/src/packet.rs @@ -397,16 +397,6 @@ pub struct PlayerSpawnPositionS2c { pub angle: f32, } -#[derive(Copy, Clone, Debug, Encode, Decode, Packet)] -#[packet(id = packet_id::PLAYER_SPAWN_S2C)] -pub struct PlayerSpawnS2c { - pub entity_id: VarInt, - pub player_uuid: Uuid, - pub position: DVec3, - pub yaw: ByteAngle, - pub pitch: ByteAngle, -} - #[derive(Clone, Debug, Encode, Decode, Packet)] #[packet(id = packet_id::SERVER_METADATA_S2C)] pub struct ServerMetadataS2c<'a> { diff --git a/crates/valence_core/build/chunk_pos.rs b/crates/valence_core/build/chunk_pos.rs index bba9b3355..82f75dff6 100644 --- a/crates/valence_core/build/chunk_pos.rs +++ b/crates/valence_core/build/chunk_pos.rs @@ -33,7 +33,7 @@ pub fn build() -> TokenStream { /// The maximum view distance for a [`ChunkView`]. pub const MAX_VIEW_DIST: u8 = #MAX_VIEW_DIST; - const EXTRA_VIEW_RADIUS: i32 = 2; + const EXTRA_VIEW_RADIUS: i32 = #EXTRA_VIEW_RADIUS; static CHUNK_VIEW_LUT: [&[(i8, i8)]; #array_len] = [ #(#entries),* ]; } diff --git a/crates/valence_entity/build.rs b/crates/valence_entity/build.rs index 50912f3fd..23565086b 100644 --- a/crates/valence_entity/build.rs +++ b/crates/valence_entity/build.rs @@ -401,7 +401,7 @@ fn build() -> anyhow::Result { pub statuses: super::EntityStatuses, pub animations: super::EntityAnimations, pub object_data: super::ObjectData, - pub tracked_data: super::TrackedData, + pub tracked_data: super::tracked_data::TrackedData, }]); bundle_init_fields.extend([quote! { @@ -477,7 +477,7 @@ fn build() -> anyhow::Result { systems.extend([quote! { #[allow(clippy::needless_borrow)] fn #system_name_ident( - mut query: Query<(&#component_path, &mut TrackedData), Changed<#component_path>> + mut query: Query<(&#component_path, &mut tracked_data::TrackedData), Changed<#component_path>> ) { for (value, mut tracked_data) in &mut query { if *value == Default::default() { diff --git a/crates/valence_entity/src/flags.rs b/crates/valence_entity/src/flags.rs new file mode 100644 index 000000000..cb09e9ed5 --- /dev/null +++ b/crates/valence_entity/src/flags.rs @@ -0,0 +1,144 @@ +use super::*; + +// TODO: should `set_if_neq` behavior be the default behavior for setters? +macro_rules! flags { + ( + $( + $component:path { + $($flag:ident: $offset:literal),* $(,)? + } + )* + + ) => { + $( + impl $component { + $( + #[doc = "Gets the bit at offset "] + #[doc = stringify!($offset)] + #[doc = "."] + #[inline] + pub const fn $flag(&self) -> bool { + (self.0 >> $offset) & 1 == 1 + } + + paste! { + #[doc = "Sets the bit at offset "] + #[doc = stringify!($offset)] + #[doc = "."] + #[inline] + pub fn [< set_$flag >] (&mut self, $flag: bool) { + self.0 = (self.0 & !(1 << $offset)) | (($flag as i8) << $offset); + } + } + )* + } + )* + } +} + +flags! { + entity::Flags { + on_fire: 0, + sneaking: 1, + sprinting: 3, + swimming: 4, + invisible: 5, + glowing: 6, + fall_flying: 7, + } + persistent_projectile::ProjectileFlags { + critical: 0, + no_clip: 1, + } + living::LivingFlags { + using_item: 0, + off_hand_active: 1, + using_riptide: 2, + } + player::PlayerModelParts { + cape: 0, + jacket: 1, + left_sleeve: 2, + right_sleeve: 3, + left_pants_leg: 4, + right_pants_leg: 5, + hat: 6, + } + player::MainArm { + right: 0, + } + armor_stand::ArmorStandFlags { + small: 0, + show_arms: 1, + hide_base_plate: 2, + marker: 3, + } + mob::MobFlags { + ai_disabled: 0, + left_handed: 1, + attacking: 2, + } + bat::BatFlags { + hanging: 0, + } + abstract_horse::HorseFlags { + tamed: 1, + saddled: 2, + bred: 3, + eating_grass: 4, + angry: 5, + eating: 6, + } + fox::FoxFlags { + sitting: 0, + crouching: 2, + rolling_head: 3, + chasing: 4, + sleeping: 5, + walking: 6, + aggressive: 7, + } + panda::PandaFlags { + sneezing: 1, + playing: 2, + sitting: 3, + lying_on_back: 4, + } + tameable::TameableFlags { + sitting_pose: 0, + tamed: 2, + } + iron_golem::IronGolemFlags { + player_created: 0, + } + snow_golem::SnowGolemFlags { + has_pumpkin: 4, + } + blaze::BlazeFlags { + fire_active: 0, + } + vex::VexFlags { + charging: 0, + } + spider::SpiderFlags { + climbing_wall: 0, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn get_set_flags() { + let mut flags = entity::Flags(0); + + flags.set_on_fire(true); + let before = flags.clone(); + assert_ne!(flags.0, 0); + flags.set_on_fire(true); + assert_eq!(before, flags); + flags.set_on_fire(false); + assert_eq!(flags.0, 0); + } +} diff --git a/crates/valence_entity/src/layer.rs b/crates/valence_entity/src/layer.rs new file mode 100644 index 000000000..edd9f02eb --- /dev/null +++ b/crates/valence_entity/src/layer.rs @@ -0,0 +1,32 @@ +use std::sync::atomic::{AtomicU32, Ordering}; + +use bevy_ecs::prelude::*; +use tracing::warn; + +#[derive(Component, Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct Layer(u32); + +impl Layer { + pub const DEFAULT: Self = Self(0); + + pub fn new() -> Self { + static NEXT: AtomicU32 = AtomicU32::new(1); // Skip default layer. + + let val = NEXT.fetch_add(1, Ordering::Relaxed); + + if val == 0 { + warn!("layer counter overflowed!"); + } + + Self(val) + } +} + +#[derive(Component, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct OldLayer(Layer); + +impl OldLayer { + pub fn get(&self) -> Layer { + self.0 + } +} diff --git a/crates/valence_entity/src/lib.rs b/crates/valence_entity/src/lib.rs index 3d9ef9f16..2169c0bcf 100644 --- a/crates/valence_entity/src/lib.rs +++ b/crates/valence_entity/src/lib.rs @@ -17,11 +17,14 @@ clippy::dbg_macro )] +mod flags; pub mod hitbox; +pub mod layer; pub mod packet; +pub mod query; +pub mod tracked_data; use std::num::Wrapping; -use std::ops::Range; use bevy_app::prelude::*; use bevy_ecs::prelude::*; @@ -29,6 +32,7 @@ use glam::{DVec3, Vec3}; use paste::paste; use rustc_hash::FxHashMap; use tracing::warn; +use tracked_data::TrackedData; use uuid::Uuid; use valence_core::chunk_pos::ChunkPos; use valence_core::despawn::Despawned; @@ -406,116 +410,6 @@ impl EntityAnimations { #[derive(Component, Default, Debug)] pub struct ObjectData(pub i32); -/// The range of packet bytes for this entity within the chunk the entity is -/// located in. For internal use only. -#[derive(Component, Default, Debug)] -pub struct PacketByteRange(pub Range); - -/// Cache for all the tracked data of an entity. Used for the -/// [`EntityTrackerUpdateS2c`][packet] packet. -/// -/// [packet]: crate::packet::EntityTrackerUpdateS2c -#[derive(Component, Default, Debug)] -pub struct TrackedData { - init_data: Vec, - /// A map of tracked data indices to the byte length of the entry in - /// `init_data`. - init_entries: Vec<(u8, u32)>, - update_data: Vec, -} - -impl TrackedData { - /// Returns initial tracked data for the entity, ready to be sent in the - /// [`EntityTrackerUpdateS2c`][packet] packet. This is used when the entity - /// enters the view of a client. - /// - /// [packet]: crate::packet::EntityTrackerUpdateS2c - pub fn init_data(&self) -> Option<&[u8]> { - if self.init_data.len() > 1 { - Some(&self.init_data) - } else { - None - } - } - - /// Contains updated tracked data for the entity, ready to be sent in the - /// [`EntityTrackerUpdateS2c`][packet] packet. This is used when tracked - /// data is changed and the client is already in view of the entity. - /// - /// [packet]: crate::packet::EntityTrackerUpdateS2c - pub fn update_data(&self) -> Option<&[u8]> { - if self.update_data.len() > 1 { - Some(&self.update_data) - } else { - None - } - } - - pub fn insert_init_value(&mut self, index: u8, type_id: u8, value: impl Encode) { - debug_assert!( - index != 0xff, - "index of 0xff is reserved for the terminator" - ); - - self.remove_init_value(index); - - self.init_data.pop(); // Remove terminator. - - // Append the new value to the end. - let len_before = self.init_data.len(); - - self.init_data.extend_from_slice(&[index, type_id]); - if let Err(e) = value.encode(&mut self.init_data) { - warn!("failed to encode initial tracked data: {e:#}"); - } - - let len = self.init_data.len() - len_before; - - self.init_entries.push((index, len as u32)); - - self.init_data.push(0xff); // Add terminator. - } - - pub fn remove_init_value(&mut self, index: u8) -> bool { - let mut start = 0; - - for (pos, &(idx, len)) in self.init_entries.iter().enumerate() { - if idx == index { - let end = start + len as usize; - - self.init_data.drain(start..end); - self.init_entries.remove(pos); - - return true; - } - - start += len as usize; - } - - false - } - - pub fn append_update_value(&mut self, index: u8, type_id: u8, value: impl Encode) { - debug_assert!( - index != 0xff, - "index of 0xff is reserved for the terminator" - ); - - self.update_data.pop(); // Remove terminator. - - self.update_data.extend_from_slice(&[index, type_id]); - if let Err(e) = value.encode(&mut self.update_data) { - warn!("failed to encode updated tracked data: {e:#}"); - } - - self.update_data.push(0xff); // Add terminator. - } - - pub fn clear_update_values(&mut self) { - self.update_data.clear(); - } -} - #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Encode, Decode)] pub struct VillagerData { pub kind: VillagerKind, @@ -753,169 +647,3 @@ impl EntityManager { self.uuid_to_entity.get(&uuid).cloned() } } - -// TODO: should `set_if_neq` behavior be the default behavior for setters? -macro_rules! flags { - ( - $( - $component:path { - $($flag:ident: $offset:literal),* $(,)? - } - )* - - ) => { - $( - impl $component { - $( - #[doc = "Gets the bit at offset "] - #[doc = stringify!($offset)] - #[doc = "."] - #[inline] - pub const fn $flag(&self) -> bool { - (self.0 >> $offset) & 1 == 1 - } - - paste! { - #[doc = "Sets the bit at offset "] - #[doc = stringify!($offset)] - #[doc = "."] - #[inline] - pub fn [< set_$flag >] (&mut self, $flag: bool) { - self.0 = (self.0 & !(1 << $offset)) | (($flag as i8) << $offset); - } - } - )* - } - )* - } -} - -flags! { - entity::Flags { - on_fire: 0, - sneaking: 1, - sprinting: 3, - swimming: 4, - invisible: 5, - glowing: 6, - fall_flying: 7, - } - persistent_projectile::ProjectileFlags { - critical: 0, - no_clip: 1, - } - living::LivingFlags { - using_item: 0, - off_hand_active: 1, - using_riptide: 2, - } - player::PlayerModelParts { - cape: 0, - jacket: 1, - left_sleeve: 2, - right_sleeve: 3, - left_pants_leg: 4, - right_pants_leg: 5, - hat: 6, - } - player::MainArm { - right: 0, - } - armor_stand::ArmorStandFlags { - small: 0, - show_arms: 1, - hide_base_plate: 2, - marker: 3, - } - mob::MobFlags { - ai_disabled: 0, - left_handed: 1, - attacking: 2, - } - bat::BatFlags { - hanging: 0, - } - abstract_horse::HorseFlags { - tamed: 1, - saddled: 2, - bred: 3, - eating_grass: 4, - angry: 5, - eating: 6, - } - fox::FoxFlags { - sitting: 0, - crouching: 2, - rolling_head: 3, - chasing: 4, - sleeping: 5, - walking: 6, - aggressive: 7, - } - panda::PandaFlags { - sneezing: 1, - playing: 2, - sitting: 3, - lying_on_back: 4, - } - tameable::TameableFlags { - sitting_pose: 0, - tamed: 2, - } - iron_golem::IronGolemFlags { - player_created: 0, - } - snow_golem::SnowGolemFlags { - has_pumpkin: 4, - } - blaze::BlazeFlags { - fire_active: 0, - } - vex::VexFlags { - charging: 0, - } - spider::SpiderFlags { - climbing_wall: 0, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn insert_remove_init_tracked_data() { - let mut td = TrackedData::default(); - - td.insert_init_value(0, 3, "foo"); - td.insert_init_value(10, 6, "bar"); - td.insert_init_value(5, 9, "baz"); - - assert!(td.remove_init_value(10)); - assert!(!td.remove_init_value(10)); - - // Insertion overwrites value at index 0. - td.insert_init_value(0, 64, "quux"); - - assert!(td.remove_init_value(0)); - assert!(td.remove_init_value(5)); - - assert!(td.init_data.as_slice().is_empty() || td.init_data.as_slice() == [0xff]); - assert!(td.init_data().is_none()); - - assert!(td.update_data.is_empty()); - } - - #[test] - fn get_set_flags() { - let mut flags = entity::Flags(0); - - flags.set_on_fire(true); - let before = flags.clone(); - assert_ne!(flags.0, 0); - flags.set_on_fire(true); - assert_eq!(before, flags); - flags.set_on_fire(false); - assert_eq!(flags.0, 0); - } -} diff --git a/crates/valence_entity/src/packet.rs b/crates/valence_entity/src/packet.rs index ab6a3d4be..71c59745b 100644 --- a/crates/valence_entity/src/packet.rs +++ b/crates/valence_entity/src/packet.rs @@ -197,6 +197,16 @@ pub struct EntitySpawnS2c { pub velocity: [i16; 3], } +#[derive(Copy, Clone, Debug, Encode, Decode, Packet)] +#[packet(id = packet_id::PLAYER_SPAWN_S2C)] +pub struct PlayerSpawnS2c { + pub entity_id: VarInt, + pub player_uuid: Uuid, + pub position: DVec3, + pub yaw: ByteAngle, + pub pitch: ByteAngle, +} + #[derive(Clone, Debug, Encode, Decode, Packet)] #[packet(id = packet_id::ENTITY_STATUS_EFFECT_S2C)] pub struct EntityStatusEffectS2c { diff --git a/crates/valence_entity/src/query.rs b/crates/valence_entity/src/query.rs new file mode 100644 index 000000000..5a1017618 --- /dev/null +++ b/crates/valence_entity/src/query.rs @@ -0,0 +1,193 @@ +use std::mem; + +use bevy_ecs::prelude::DetectChanges; +use bevy_ecs::query::WorldQuery; +use bevy_ecs::world::Ref; +use glam::DVec3; +use valence_core::protocol::byte_angle::ByteAngle; +use valence_core::protocol::encode::WritePacket; +use valence_core::protocol::var_int::VarInt; +use valence_core::uuid::UniqueId; + +use crate::packet::{ + EntityAnimationS2c, EntityPositionS2c, EntitySetHeadYawS2c, EntitySpawnS2c, EntityStatusS2c, + EntityTrackerUpdateS2c, EntityVelocityUpdateS2c, ExperienceOrbSpawnS2c, MoveRelativeS2c, + PlayerSpawnS2c, RotateAndMoveRelativeS2c, RotateS2c, +}; +use crate::tracked_data::TrackedData; +use crate::{ + EntityAnimations, EntityId, EntityKind, EntityStatuses, HeadYaw, Location, Look, ObjectData, + OldLocation, OldPosition, OnGround, Position, Velocity, +}; + +#[derive(WorldQuery)] +pub struct EntityInitQuery { + entity_id: &'static EntityId, + uuid: &'static UniqueId, + kind: &'static EntityKind, + look: &'static Look, + head_yaw: &'static HeadYaw, + on_ground: &'static OnGround, + object_data: &'static ObjectData, + velocity: &'static Velocity, + tracked_data: &'static TrackedData, +} + +impl EntityInitQueryItem<'_> { + /// Writes the appropriate packets to initialize an entity. This will spawn + /// the entity and initialize tracked data. `pos` is the initial position of + /// the entity. + pub fn write_init_packets(&self, pos: DVec3, mut writer: impl WritePacket) { + match *self.kind { + EntityKind::MARKER => {} + EntityKind::EXPERIENCE_ORB => { + writer.write_packet(&ExperienceOrbSpawnS2c { + entity_id: self.entity_id.get().into(), + position: pos, + count: self.object_data.0 as i16, + }); + } + EntityKind::PLAYER => { + writer.write_packet(&PlayerSpawnS2c { + entity_id: self.entity_id.get().into(), + player_uuid: self.uuid.0, + position: pos, + yaw: ByteAngle::from_degrees(self.look.yaw), + pitch: ByteAngle::from_degrees(self.look.pitch), + }); + + // Player spawn packet doesn't include head yaw for some reason. + writer.write_packet(&EntitySetHeadYawS2c { + entity_id: self.entity_id.get().into(), + head_yaw: ByteAngle::from_degrees(self.head_yaw.0), + }); + } + _ => writer.write_packet(&EntitySpawnS2c { + entity_id: self.entity_id.get().into(), + object_uuid: self.uuid.0, + kind: self.kind.get().into(), + position: pos, + pitch: ByteAngle::from_degrees(self.look.pitch), + yaw: ByteAngle::from_degrees(self.look.yaw), + head_yaw: ByteAngle::from_degrees(self.head_yaw.0), + data: self.object_data.0.into(), + velocity: self.velocity.to_packet_units(), + }), + } + + if let Some(init_data) = self.tracked_data.init_data() { + writer.write_packet(&EntityTrackerUpdateS2c { + entity_id: self.entity_id.get().into(), + metadata: init_data.into(), + }); + } + } +} + +#[derive(WorldQuery)] +pub struct UpdateEntityQuery { + id: &'static EntityId, + pos: &'static Position, + old_pos: &'static OldPosition, + loc: &'static Location, + old_loc: &'static OldLocation, + look: Ref<'static, Look>, + head_yaw: Ref<'static, HeadYaw>, + on_ground: &'static OnGround, + velocity: Ref<'static, Velocity>, + tracked_data: &'static TrackedData, + statuses: &'static EntityStatuses, + animations: &'static EntityAnimations, +} + +impl UpdateEntityQueryItem<'_> { + pub fn write_update_packets(&self, mut writer: impl WritePacket) { + // TODO: @RJ I saw you're using UpdateEntityPosition and UpdateEntityRotation sometimes. These two packets are actually broken on the client and will erase previous position/rotation https://bugs.mojang.com/browse/MC-255263 -Moulberry + + let entity_id = VarInt(self.id.get()); + + let position_delta = self.pos.0 - self.old_pos.get(); + let needs_teleport = position_delta.abs().max_element() >= 8.0; + let changed_position = self.pos.0 != self.old_pos.get(); + + if changed_position && !needs_teleport && self.look.is_changed() { + writer.write_packet(&RotateAndMoveRelativeS2c { + entity_id, + delta: (position_delta * 4096.0).to_array().map(|v| v as i16), + yaw: ByteAngle::from_degrees(self.look.yaw), + pitch: ByteAngle::from_degrees(self.look.pitch), + on_ground: self.on_ground.0, + }); + } else { + if changed_position && !needs_teleport { + writer.write_packet(&MoveRelativeS2c { + entity_id, + delta: (position_delta * 4096.0).to_array().map(|v| v as i16), + on_ground: self.on_ground.0, + }); + } + + if self.look.is_changed() { + writer.write_packet(&RotateS2c { + entity_id, + yaw: ByteAngle::from_degrees(self.look.yaw), + pitch: ByteAngle::from_degrees(self.look.pitch), + on_ground: self.on_ground.0, + }); + } + } + + if needs_teleport { + writer.write_packet(&EntityPositionS2c { + entity_id, + position: self.pos.0, + yaw: ByteAngle::from_degrees(self.look.yaw), + pitch: ByteAngle::from_degrees(self.look.pitch), + on_ground: self.on_ground.0, + }); + } + + if self.velocity.is_changed() { + writer.write_packet(&EntityVelocityUpdateS2c { + entity_id, + velocity: self.velocity.to_packet_units(), + }); + } + + if self.head_yaw.is_changed() { + writer.write_packet(&EntitySetHeadYawS2c { + entity_id, + head_yaw: ByteAngle::from_degrees(self.head_yaw.0), + }); + } + + if let Some(update_data) = self.tracked_data.update_data() { + writer.write_packet(&EntityTrackerUpdateS2c { + entity_id, + metadata: update_data.into(), + }); + } + + if self.statuses.0 != 0 { + for i in 0..mem::size_of_val(self.statuses) { + if (self.statuses.0 >> i) & 1 == 1 { + writer.write_packet(&EntityStatusS2c { + entity_id: entity_id.0, + entity_status: i as u8, + }); + } + } + } + + if self.animations.0 != 0 { + for i in 0..mem::size_of_val(self.animations) { + if (self.animations.0 >> i) & 1 == 1 { + writer.write_packet(&EntityAnimationS2c { + entity_id, + animation: i as u8, + }); + } + } + } + } +} diff --git a/crates/valence_entity/src/tracked_data.rs b/crates/valence_entity/src/tracked_data.rs new file mode 100644 index 000000000..a81a040c6 --- /dev/null +++ b/crates/valence_entity/src/tracked_data.rs @@ -0,0 +1,136 @@ +use bevy_ecs::prelude::*; +use tracing::warn; +use valence_core::protocol::Encode; + +/// Cache for all the tracked data of an entity. Used for the +/// [`EntityTrackerUpdateS2c`][packet] packet. +/// +/// [packet]: crate::packet::EntityTrackerUpdateS2c +#[derive(Component, Default, Debug)] +pub struct TrackedData { + init_data: Vec, + /// A map of tracked data indices to the byte length of the entry in + /// `init_data`. + init_entries: Vec<(u8, u32)>, + update_data: Vec, +} + +impl TrackedData { + /// Returns initial tracked data for the entity, ready to be sent in the + /// [`EntityTrackerUpdateS2c`][packet] packet. This is used when the entity + /// enters the view of a client. + /// + /// [packet]: crate::packet::EntityTrackerUpdateS2c + pub fn init_data(&self) -> Option<&[u8]> { + if self.init_data.len() > 1 { + Some(&self.init_data) + } else { + None + } + } + + /// Contains updated tracked data for the entity, ready to be sent in the + /// [`EntityTrackerUpdateS2c`][packet] packet. This is used when tracked + /// data is changed and the client is already in view of the entity. + /// + /// [packet]: crate::packet::EntityTrackerUpdateS2c + pub fn update_data(&self) -> Option<&[u8]> { + if self.update_data.len() > 1 { + Some(&self.update_data) + } else { + None + } + } + + pub fn insert_init_value(&mut self, index: u8, type_id: u8, value: impl Encode) { + debug_assert!( + index != 0xff, + "index of 0xff is reserved for the terminator" + ); + + self.remove_init_value(index); + + self.init_data.pop(); // Remove terminator. + + // Append the new value to the end. + let len_before = self.init_data.len(); + + self.init_data.extend_from_slice(&[index, type_id]); + if let Err(e) = value.encode(&mut self.init_data) { + warn!("failed to encode initial tracked data: {e:#}"); + } + + let len = self.init_data.len() - len_before; + + self.init_entries.push((index, len as u32)); + + self.init_data.push(0xff); // Add terminator. + } + + pub fn remove_init_value(&mut self, index: u8) -> bool { + let mut start = 0; + + for (pos, &(idx, len)) in self.init_entries.iter().enumerate() { + if idx == index { + let end = start + len as usize; + + self.init_data.drain(start..end); + self.init_entries.remove(pos); + + return true; + } + + start += len as usize; + } + + false + } + + pub fn append_update_value(&mut self, index: u8, type_id: u8, value: impl Encode) { + debug_assert!( + index != 0xff, + "index of 0xff is reserved for the terminator" + ); + + self.update_data.pop(); // Remove terminator. + + self.update_data.extend_from_slice(&[index, type_id]); + if let Err(e) = value.encode(&mut self.update_data) { + warn!("failed to encode updated tracked data: {e:#}"); + } + + self.update_data.push(0xff); // Add terminator. + } + + pub fn clear_update_values(&mut self) { + self.update_data.clear(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn insert_remove_init_tracked_data() { + let mut td = TrackedData::default(); + + td.insert_init_value(0, 3, "foo"); + td.insert_init_value(10, 6, "bar"); + td.insert_init_value(5, 9, "baz"); + + assert!(td.remove_init_value(10)); + assert!(!td.remove_init_value(10)); + + // Insertion overwrites value at index 0. + td.insert_init_value(0, 64, "quux"); + + assert!(td.remove_init_value(0)); + assert!(td.remove_init_value(5)); + + assert!(td.init_data.as_slice().is_empty() || td.init_data.as_slice() == [0xff]); + assert!(td.init_data().is_none()); + + assert!(td.update_data.is_empty()); + } +} diff --git a/crates/valence_instance/Cargo.toml b/crates/valence_instance/Cargo.toml index 6f9266bb3..f540eb7c3 100644 --- a/crates/valence_instance/Cargo.toml +++ b/crates/valence_instance/Cargo.toml @@ -20,3 +20,4 @@ valence_dimension.workspace = true valence_entity.workspace = true valence_nbt.workspace = true valence_registry.workspace = true +tracing.workspace = true diff --git a/crates/valence_instance/src/chunk.rs b/crates/valence_instance/src/chunk.rs index dac85b66f..c8f39ab51 100644 --- a/crates/valence_instance/src/chunk.rs +++ b/crates/valence_instance/src/chunk.rs @@ -338,7 +338,7 @@ mod tests { } let unloaded = UnloadedChunk::with_height(512); - let loaded = LoadedChunk::new(512, None); + let loaded = LoadedChunk::new(512); check(unloaded); check(loaded); @@ -356,7 +356,7 @@ mod tests { #[test] #[should_panic] fn chunk_debug_oob_1() { - let mut chunk = LoadedChunk::new(512, None); + let mut chunk = LoadedChunk::new(512); chunk.set_block_state(0, 0, 16, BlockState::AIR); } @@ -372,7 +372,7 @@ mod tests { #[test] #[should_panic] fn chunk_debug_oob_3() { - let mut chunk = LoadedChunk::new(512, None); + let mut chunk = LoadedChunk::new(512); chunk.set_block_entity(0, 0, 16, None); } @@ -388,7 +388,7 @@ mod tests { #[test] #[should_panic] fn chunk_debug_oob_5() { - let mut chunk = LoadedChunk::new(512, None); + let mut chunk = LoadedChunk::new(512); chunk.set_biome(0, 0, 4, BiomeId::DEFAULT); } @@ -404,7 +404,7 @@ mod tests { #[test] #[should_panic] fn chunk_debug_oob_7() { - let mut chunk = LoadedChunk::new(512, None); + let mut chunk = LoadedChunk::new(512); chunk.fill_block_state_section(chunk.height() / 16, BlockState::AIR); } @@ -420,7 +420,7 @@ mod tests { #[test] #[should_panic] fn chunk_debug_oob_9() { - let mut chunk = LoadedChunk::new(512, None); + let mut chunk = LoadedChunk::new(512); chunk.fill_biome_section(chunk.height() / 16, BiomeId::DEFAULT); } } diff --git a/crates/valence_instance/src/chunk/loaded.rs b/crates/valence_instance/src/chunk/loaded.rs index 7ced7e1aa..018991d83 100644 --- a/crates/valence_instance/src/chunk/loaded.rs +++ b/crates/valence_instance/src/chunk/loaded.rs @@ -14,7 +14,7 @@ use valence_core::despawn::Despawned; use valence_core::protocol::encode::{PacketWriter, WritePacket}; use valence_core::protocol::var_int::VarInt; use valence_core::protocol::var_long::VarLong; -use valence_core::protocol::{Encode, Packet}; +use valence_core::protocol::Encode; use valence_entity::EntityKind; use valence_nbt::{compound, Compound}; use valence_registry::RegistryIdx; @@ -24,11 +24,11 @@ use super::{ bit_width, check_biome_oob, check_block_oob, check_section_oob, unloaded, BiomeContainer, BlockStateContainer, Chunk, UnloadedChunk, SECTION_BLOCK_COUNT, }; +use crate::message::{MessageBuf, MessageCondition}; use crate::packet::{ - BlockEntityUpdateS2c, BlockUpdateS2c, ChunkBiome, ChunkBiomeDataS2c, ChunkDataBlockEntity, - ChunkDataS2c, ChunkDeltaUpdateS2c, + BlockEntityUpdateS2c, BlockUpdateS2c, ChunkDataBlockEntity, ChunkDataS2c, ChunkDeltaUpdateS2c, }; -use crate::{InstanceInfo, UpdateEntityQuery}; +use crate::{BiomeChange, InstanceInfo, UpdateEntityQuery}; #[derive(Debug)] pub struct LoadedChunk { @@ -48,30 +48,12 @@ pub struct LoadedChunk { changed_block_entities: BTreeSet, /// If any biomes in this chunk have been modified this tick. changed_biomes: bool, - /// The global compression threshold. - compression_threshold: Option, - /// A buffer of packets to send to all clients currently in view of this - /// chunk at the end of the tick. Clients entering the view of this - /// chunk this tick should _not_ receive this data. - /// - /// Cleared at the end of the tick. - packet_buf: Vec, /// Cached bytes of the chunk initialization packet. The cache is considered /// invalidated if empty. This should be cleared whenever the chunk is /// modified in an observable way, even if the chunk is not viewed. cached_init_packets: Mutex>, - /// Minecraft entities in this chunk. + /// The unique set of Minecraft entities with positions in this chunk. pub(crate) entities: BTreeSet, - /// Minecraft entities that have entered the chunk this tick, paired with - /// the chunk position in this instance they came from. If the position is - /// `None`, then the entity either came from an unloaded chunk, a different - /// instance, or is newly spawned. - pub(crate) incoming_entities: Vec<(Entity, Option)>, - /// Minecraft entities that have left the chunk this tick, paired with the - /// chunk position in this instance they arrived at. If the position is - /// `None`, then the entity either moved to an unloaded chunk, different - /// instance, or despawned. - pub(crate) outgoing_entities: Vec<(Entity, Option)>, } /// Describes the current state of a loaded chunk. @@ -131,7 +113,7 @@ impl Section { } impl LoadedChunk { - pub(crate) fn new(height: u32, compression_threshold: Option) -> Self { + pub(crate) fn new(height: u32) -> Self { Self { state: ChunkState::Added, is_viewed: AtomicBool::new(false), @@ -139,12 +121,8 @@ impl LoadedChunk { block_entities: BTreeMap::new(), changed_block_entities: BTreeSet::new(), changed_biomes: false, - compression_threshold, - packet_buf: vec![], cached_init_packets: Mutex::new(vec![]), entities: BTreeSet::new(), - incoming_entities: vec![], - outgoing_entities: vec![], } } @@ -183,7 +161,6 @@ impl LoadedChunk { let old_block_entities = mem::replace(&mut self.block_entities, chunk.block_entities); self.changed_block_entities.clear(); self.changed_biomes = false; - self.packet_buf.clear(); self.cached_init_packets.get_mut().clear(); self.assert_no_changes(); @@ -219,7 +196,6 @@ impl LoadedChunk { let old_block_entities = mem::take(&mut self.block_entities); self.changed_block_entities.clear(); self.changed_biomes = false; - self.packet_buf.clear(); self.cached_init_packets.get_mut().clear(); self.assert_no_changes(); @@ -259,22 +235,6 @@ impl LoadedChunk { self.is_viewed.store(true, Ordering::Relaxed); } - /// An immutable view into this chunk's packet buffer. - #[doc(hidden)] - pub fn packet_buf(&self) -> &[u8] { - &self.packet_buf - } - - #[doc(hidden)] - pub fn incoming_entities(&self) -> &[(Entity, Option)] { - &self.incoming_entities - } - - #[doc(hidden)] - pub fn outgoing_entities(&self) -> &[(Entity, Option)] { - &self.outgoing_entities - } - /// Performs the changes necessary to prepare this chunk for client updates. /// Notably: /// - Chunk and entity update packets are written to this chunk's packet @@ -286,10 +246,12 @@ impl LoadedChunk { &mut self, pos: ChunkPos, info: &InstanceInfo, + message_buf: &mut MessageBuf, + biome_changes: &mut Vec, entity_query: &mut Query, Without)>, ) { if !*self.is_viewed.get_mut() { - // Nobody is viewing the chunk, so no need to write to the packet buf. There + // Nobody is viewing the chunk, so no need to send any messages. There // also shouldn't be any changes that need to be cleared. self.assert_no_changes(); @@ -304,128 +266,109 @@ impl LoadedChunk { "other chunk states should be unviewed" ); - let mut writer = PacketWriter::new(&mut self.packet_buf, info.compression_threshold); - - // Block states - for (sect_y, sect) in self.sections.iter_mut().enumerate() { - match sect.section_updates.len() { - 0 => {} - 1 => { - let packed = sect.section_updates[0].0 as u64; - let offset_y = packed & 0b1111; - let offset_z = (packed >> 4) & 0b1111; - let offset_x = (packed >> 8) & 0b1111; - let block = packed >> 12; - - let global_x = pos.x * 16 + offset_x as i32; - let global_y = info.min_y + sect_y as i32 * 16 + offset_y as i32; - let global_z = pos.z * 16 + offset_z as i32; - - writer.write_packet(&BlockUpdateS2c { - position: BlockPos::new(global_x, global_y, global_z), - block_id: VarInt(block as i32), - }) - } - _ => { - let chunk_section_position = (pos.x as i64) << 42 - | (pos.z as i64 & 0x3fffff) << 20 - | (sect_y as i64 + info.min_y.div_euclid(16) as i64) & 0xfffff; - - writer.write_packet(&ChunkDeltaUpdateS2c { - chunk_section_position, - blocks: Cow::Borrowed(§.section_updates), - }); + message_buf.send(MessageCondition::View { pos }, |mut writer| { + // Block states + for (sect_y, sect) in self.sections.iter_mut().enumerate() { + match sect.section_updates.len() { + 0 => {} + 1 => { + let packed = sect.section_updates[0].0 as u64; + let offset_y = packed & 0b1111; + let offset_z = (packed >> 4) & 0b1111; + let offset_x = (packed >> 8) & 0b1111; + let block = packed >> 12; + + let global_x = pos.x * 16 + offset_x as i32; + let global_y = info.min_y + sect_y as i32 * 16 + offset_y as i32; + let global_z = pos.z * 16 + offset_z as i32; + + writer.write_packet(&BlockUpdateS2c { + position: BlockPos::new(global_x, global_y, global_z), + block_id: VarInt(block as i32), + }); + } + _ => { + let chunk_section_position = (pos.x as i64) << 42 + | (pos.z as i64 & 0x3fffff) << 20 + | (sect_y as i64 + info.min_y.div_euclid(16) as i64) & 0xfffff; + + writer.write_packet(&ChunkDeltaUpdateS2c { + chunk_section_position, + blocks: Cow::Borrowed(§.section_updates), + }); + } } - } - - sect.section_updates.clear(); - } - - // Block entities - for &idx in &self.changed_block_entities { - let Some(nbt) = self.block_entities.get(&idx) else { - continue; - }; - - let x = idx % 16; - let z = (idx / 16) % 16; - let y = idx / 16 / 16; - - let state = self.sections[y as usize / 16] - .block_states - .get(idx as usize % SECTION_BLOCK_COUNT); - let Some(kind) = state.block_entity_kind() else { - continue; - }; - - let global_x = pos.x * 16 + x as i32; - let global_y = info.min_y + y as i32; - let global_z = pos.z * 16 + z as i32; + sect.section_updates.clear(); + } - writer.write_packet(&BlockEntityUpdateS2c { - position: BlockPos::new(global_x, global_y, global_z), - kind: VarInt(kind as i32), - data: Cow::Borrowed(nbt), - }); - } + // Block entities + for &idx in &self.changed_block_entities { + let Some(nbt) = self.block_entities.get(&idx) else { + continue; + }; + + let x = idx % 16; + let z = (idx / 16) % 16; + let y = idx / 16 / 16; + + let state = self.sections[y as usize / 16] + .block_states + .get(idx as usize % SECTION_BLOCK_COUNT); + + let Some(kind) = state.block_entity_kind() else { + continue; + }; + + let global_x = pos.x * 16 + x as i32; + let global_y = info.min_y + y as i32; + let global_z = pos.z * 16 + z as i32; + + writer.write_packet(&BlockEntityUpdateS2c { + position: BlockPos::new(global_x, global_y, global_z), + kind: VarInt(kind as i32), + data: Cow::Borrowed(nbt), + }); + } - self.changed_block_entities.clear(); + self.changed_block_entities.clear(); - // Biomes - if self.changed_biomes { - self.changed_biomes = false; + // Biomes + if self.changed_biomes { + self.changed_biomes = false; - // TODO: ChunkBiomeData packet supports updating multiple chunks in a single - // packet, so it would make more sense to cache the biome data and send it later - // during client updates. + let mut data: Vec = vec![]; - let mut biomes: Vec = vec![]; + for sect in self.sections.iter() { + sect.biomes + .encode_mc_format( + &mut data, + |b| b.to_index() as _, + 0, + 3, + bit_width(info.biome_registry_len - 1), + ) + .expect("paletted container encode should always succeed"); + } - for sect in self.sections.iter() { - sect.biomes - .encode_mc_format( - &mut biomes, - |b| b.to_index() as _, - 0, - 3, - bit_width(info.biome_registry_len - 1), - ) - .expect("paletted container encode should always succeed"); + biome_changes.push(BiomeChange { pos, data }); } - self.write_packet(&ChunkBiomeDataS2c { - chunks: Cow::Borrowed(&[ChunkBiome { pos, data: &biomes }]), - }); - } - - // Entities - for &entity in &self.entities { - let entity = entity_query - .get_mut(entity) - .expect("entity in chunk's list of entities should exist"); - - let start = self.packet_buf.len(); - - let writer = PacketWriter::new(&mut self.packet_buf, self.compression_threshold); - entity.write_update_packets(writer); + // Entities + for &entity in &self.entities { + let entity = entity_query + .get_mut(entity) + .expect("entity in chunk's list of entities should exist"); - let end = self.packet_buf.len(); - - if let Some(mut range) = entity.packet_byte_range { - range.0 = start..end; + entity.write_update_packets(&mut writer); } - } + }); // All changes should be cleared. self.assert_no_changes(); } pub(crate) fn update_post_client(&mut self) { - self.packet_buf.clear(); - self.incoming_entities.clear(); - self.outgoing_entities.clear(); - self.state = match self.state { ChunkState::Added => ChunkState::Normal, ChunkState::AddedRemoved => unreachable!(), @@ -434,7 +377,7 @@ impl LoadedChunk { ChunkState::Normal => ChunkState::Normal, }; - // Changes were already cleared in `write_updates_to_packet_buf`. + // Changes were already cleared in `update_pre_client`. self.assert_no_changes(); } @@ -738,10 +681,7 @@ impl Chunk for LoadedChunk { } fn optimize(&mut self) { - self.packet_buf.shrink_to_fit(); self.cached_init_packets.get_mut().shrink_to_fit(); - self.incoming_entities.shrink_to_fit(); - self.outgoing_entities.shrink_to_fit(); for sect in self.sections.iter_mut() { sect.block_states.optimize(); @@ -751,28 +691,6 @@ impl Chunk for LoadedChunk { } } -/// Packets written to chunks will be sent to clients in view of the chunk at -/// the end of the tick. -impl WritePacket for LoadedChunk { - fn write_packet_fallible

(&mut self, packet: &P) -> anyhow::Result<()> - where - P: Packet + Encode, - { - if *self.is_viewed.get_mut() { - PacketWriter::new(&mut self.packet_buf, self.compression_threshold) - .write_packet_fallible(packet)?; - } - - Ok(()) - } - - fn write_packet_bytes(&mut self, bytes: &[u8]) { - if *self.is_viewed.get_mut() { - self.packet_buf.extend_from_slice(bytes); - } - } -} - #[cfg(test)] mod tests { use valence_core::ident; @@ -783,7 +701,7 @@ mod tests { #[test] fn loaded_chunk_unviewed_no_changes() { - let mut chunk = LoadedChunk::new(512, THRESHOLD); + let mut chunk = LoadedChunk::new(512); chunk.set_block(0, 10, 0, BlockState::MAGMA_BLOCK); chunk.assert_no_changes(); @@ -830,7 +748,7 @@ mod tests { assert!(!chunk.cached_init_packets.get_mut().is_empty()); } - let mut chunk = LoadedChunk::new(512, THRESHOLD); + let mut chunk = LoadedChunk::new(512); check(&mut chunk, |c| { c.set_block_state(0, 4, 0, BlockState::ACACIA_WOOD) diff --git a/crates/valence_instance/src/instance.rs b/crates/valence_instance/src/instance.rs index e71c812c3..03e30987d 100644 --- a/crates/valence_instance/src/instance.rs +++ b/crates/valence_instance/src/instance.rs @@ -1,26 +1,22 @@ //! Contains the [`Instance`] component and methods. -use std::borrow::Cow; use std::collections::hash_map::{Entry, OccupiedEntry, VacantEntry}; use bevy_ecs::prelude::*; -use glam::{DVec3, Vec3, u32}; use num_integer::div_ceil; use rustc_hash::FxHashMap; use valence_biome::BiomeRegistry; use valence_core::block_pos::BlockPos; use valence_core::chunk_pos::ChunkPos; use valence_core::ident::Ident; -use valence_core::particle::{Particle, ParticleS2c}; use valence_core::protocol::array::LengthPrefixedArray; -use valence_core::protocol::encode::{PacketWriter, WritePacket}; -use valence_core::protocol::packet::sound::{PlaySoundS2c, Sound, SoundCategory}; -use valence_core::protocol::{Encode, Packet}; use valence_core::Server; use valence_dimension::DimensionTypeRegistry; +use valence_entity::EntityId; use valence_nbt::Compound; use crate::chunk::{Block, BlockRef, Chunk, IntoBlock, LoadedChunk, UnloadedChunk, MAX_HEIGHT}; +use crate::message::{MessageBuf, MessageCondition}; /// An Instance represents a Minecraft world, which consist of [`Chunk`]s. /// It manages updating clients when chunks change, and caches chunk and entity @@ -29,9 +25,9 @@ use crate::chunk::{Block, BlockRef, Chunk, IntoBlock, LoadedChunk, UnloadedChunk pub struct Instance { pub(super) chunks: FxHashMap, pub(super) info: InstanceInfo, - /// Packet data to send to all clients in this instance at the end of the - /// tick. - pub(super) packet_buf: Vec, + pub(super) message_buf: MessageBuf, + pub(super) entity_removals: Vec, + pub(super) biome_changes: Vec, } #[doc(hidden)] @@ -46,6 +42,19 @@ pub struct InstanceInfo { pub(super) sky_light_arrays: Box<[LengthPrefixedArray]>, } +#[doc(hidden)] +pub struct EntityRemoval { + pub pos: ChunkPos, + pub id: EntityId, +} + +#[doc(hidden)] +pub struct BiomeChange { + pub pos: ChunkPos, + /// Data for the ChunkBiomeDataS2c packet. + pub data: Vec, +} + impl Instance { #[track_caller] pub fn new( @@ -84,7 +93,9 @@ impl Instance { sky_light_arrays: vec![LengthPrefixedArray([0xff; 2048]); light_section_count] .into(), }, - packet_buf: vec![], + message_buf: MessageBuf::new(server.compression_threshold()), + entity_removals: vec![], + biome_changes: vec![], } } @@ -157,7 +168,6 @@ impl Instance { Entry::Occupied(oe) => ChunkEntry::Occupied(OccupiedChunkEntry { entry: oe }), Entry::Vacant(ve) => ChunkEntry::Vacant(VacantChunkEntry { height: self.info.height, - compression_threshold: self.info.compression_threshold, entry: ve, }), } @@ -182,7 +192,7 @@ impl Instance { } self.chunks.shrink_to_fit(); - self.packet_buf.shrink_to_fit(); + self.message_buf.shrink_to_fit(); } pub fn block(&self, pos: impl Into) -> Option { @@ -243,43 +253,17 @@ impl Instance { Some((chunk, x, y, z)) } - /// Writes a packet to all clients in view of `pos` in this instance. Has no - /// effect if there is no chunk at `pos`. - /// - /// This is more efficient than sending the packet to each client - /// individually. - pub fn write_packet_at

(&mut self, pkt: &P, pos: impl Into) - where - P: Packet + Encode, - { - if let Some(chunk) = self.chunks.get_mut(&pos.into()) { - chunk.write_packet(pkt); - } - } - - /// Writes arbitrary packet data to all clients in view of `pos` in this - /// instance. Has no effect if there is no chunk at `pos`. - /// - /// The packet data must be properly compressed for the current compression - /// threshold but never encrypted. Don't use this function unless you know - /// what you're doing. Consider using [`Self::write_packet`] instead. - pub fn write_packet_bytes_at(&mut self, bytes: &[u8], pos: impl Into) { - if let Some(chunk) = self.chunks.get_mut(&pos.into()) { - chunk.write_packet_bytes(bytes); - } - } - - /// An immutable view into this instance's packet buffer. #[doc(hidden)] - pub fn packet_buf(&self) -> &[u8] { - &self.packet_buf + pub fn info(&self) -> &InstanceInfo { + &self.info } #[doc(hidden)] - pub fn info(&self) -> &InstanceInfo { - &self.info + pub fn message_buf(&self) -> &MessageBuf { + &self.message_buf } + /* // TODO: move to `valence_particle`. /// Puts a particle effect at the given position in the world. The particle /// effect is visible to all players in the instance with the @@ -333,27 +317,7 @@ impl Instance { }, ChunkPos::from_dvec3(position), ); - } -} - -/// Writing packets to the instance writes to the instance's global packet -/// buffer. All clients in the instance will receive the packet at the end of -/// the tick. -/// -/// This is generally more efficient than sending the packet to each client -/// individually. -impl WritePacket for Instance { - fn write_packet_fallible

(&mut self, packet: &P) -> anyhow::Result<()> - where - P: Packet + Encode, - { - PacketWriter::new(&mut self.packet_buf, self.info.compression_threshold) - .write_packet_fallible(packet) - } - - fn write_packet_bytes(&mut self, bytes: &[u8]) { - self.packet_buf.extend_from_slice(bytes) - } + }*/ } #[derive(Debug)] @@ -412,13 +376,12 @@ impl<'a> OccupiedChunkEntry<'a> { #[derive(Debug)] pub struct VacantChunkEntry<'a> { height: u32, - compression_threshold: Option, entry: VacantEntry<'a, ChunkPos, LoadedChunk>, } impl<'a> VacantChunkEntry<'a> { pub fn insert(self, chunk: UnloadedChunk) -> &'a mut LoadedChunk { - let mut loaded = LoadedChunk::new(self.height, self.compression_threshold); + let mut loaded = LoadedChunk::new(self.height); loaded.insert(chunk); self.entry.insert(loaded) diff --git a/crates/valence_instance/src/lib.rs b/crates/valence_instance/src/lib.rs index 84b35c9b3..30a3b75d3 100644 --- a/crates/valence_instance/src/lib.rs +++ b/crates/valence_instance/src/lib.rs @@ -18,41 +18,35 @@ )] #![allow(clippy::type_complexity)] -use std::mem; +use std::borrow::Cow; use bevy_app::prelude::*; use bevy_ecs::prelude::*; -use bevy_ecs::query::{Has, WorldQuery}; +use bevy_ecs::query::Has; use chunk::loaded::ChunkState; +use message::MessageCondition; use valence_core::chunk_pos::ChunkPos; use valence_core::despawn::Despawned; -use valence_core::protocol::byte_angle::ByteAngle; -use valence_core::protocol::encode::WritePacket; -use valence_core::protocol::var_int::VarInt; -use valence_entity::packet::{ - EntityAnimationS2c, EntityPositionS2c, EntitySetHeadYawS2c, EntityStatusS2c, - EntityTrackerUpdateS2c, EntityVelocityUpdateS2c, MoveRelativeS2c, RotateAndMoveRelativeS2c, - RotateS2c, -}; +use valence_entity::packet::EntitiesDestroyS2c; +use valence_entity::query::{EntityInitQuery, UpdateEntityQuery}; use valence_entity::{ - EntityAnimations, EntityId, EntityKind, EntityStatuses, HeadYaw, InitEntitiesSet, Location, - Look, OldLocation, OldPosition, OnGround, PacketByteRange, Position, TrackedData, - UpdateTrackedDataSet, Velocity, + EntityId, EntityKind, InitEntitiesSet, Location, OldLocation, OldPosition, Position, + UpdateTrackedDataSet, }; pub mod chunk; mod instance; -pub mod packet; pub mod message; +pub mod packet; pub use chunk::{Block, BlockRef}; pub use instance::*; pub struct InstancePlugin; -/// When Minecraft entity changes are written to the packet buffers of chunks. -/// Systems that modify entites should run _before_ this. Systems that read from -/// the packet buffer of chunks should run _after_ this. +/// When Minecraft entity changes are written to the packet buffers of +/// instances. Systems that modify entites should run _before_ this. Systems +/// that read from the packet buffer of instances should run _after_ this. /// /// This set lives in [`PostUpdate`]. #[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] @@ -86,7 +80,7 @@ impl Plugin for InstancePlugin { ) .add_systems( PostUpdate, - write_update_packets_to_chunks + write_update_packets_per_chunk .after(update_entity_chunk_positions) .in_set(WriteUpdatePacketsToInstancesSet), ) @@ -103,17 +97,19 @@ struct Orphaned; /// Attempts to add orphaned entities to the chunk they're positioned in. fn add_orphaned_entities( - entities: Query<(Entity, &Position, &Location), With>, + entities: Query<(Entity, &Position, &OldPosition, &Location, EntityInitQuery), With>, mut instances: Query<&mut Instance>, mut commands: Commands, ) { - for (entity, pos, loc) in &entities { + for (entity, pos, old_pos, loc, init_item) in &entities { if let Ok(mut inst) = instances.get_mut(loc.0) { let pos = ChunkPos::at(pos.0.x, pos.0.z); if let Some(chunk) = inst.chunk_mut(pos) { - if chunk.entities.insert(entity) { - chunk.incoming_entities.push((entity, None)); + if chunk.entities.insert(entity) && chunk.is_viewed_mut() { + inst.message_buf.send(MessageCondition::View { pos }, |w| { + init_item.write_init_packets(old_pos.get(), w) + }); } // Entity is no longer orphaned. @@ -132,73 +128,113 @@ fn update_entity_chunk_positions( &OldPosition, &Location, &OldLocation, + EntityInitQuery, Has, ), - ( - With, - Or<(Changed, Changed, With)>, - ), + (Or<(Changed, Changed, With)>,), >, + entity_ids: Query<&EntityId>, mut instances: Query<&mut Instance>, mut commands: Commands, ) { - for (entity, pos, old_pos, loc, old_loc, despawned) in &entities { - let pos = ChunkPos::at(pos.0.x, pos.0.z); - let old_pos = ChunkPos::at(old_pos.get().x, old_pos.get().z); + for (entity, pos, old_pos, loc, old_loc, init_item, despawned) in &entities { + let chunk_pos = ChunkPos::at(pos.0.x, pos.0.z); + let old_chunk_pos = ChunkPos::at(old_pos.get().x, old_pos.get().z); if despawned { // Entity was deleted. Remove it from the chunk it was in. if let Ok(mut old_inst) = instances.get_mut(old_loc.get()) { - if let Some(old_chunk) = old_inst.chunk_mut(old_pos) { + if let Some(old_chunk) = old_inst.chunk_mut(old_chunk_pos) { if old_chunk.entities.remove(&entity) { - old_chunk.outgoing_entities.push((entity, None)); + let id = *entity_ids.get(entity).unwrap(); + + old_inst.entity_removals.push(EntityRemoval { + pos: old_chunk_pos, + id, + }); } } } } else if old_loc.get() != loc.0 { - // Entity changed the instance it is in. Remove it from old chunk and + // Entity changed the instance it's in. Remove it from old chunk and // insert it in the new chunk. if let Ok(mut old_inst) = instances.get_mut(old_loc.get()) { - if let Some(old_chunk) = old_inst.chunk_mut(old_pos) { - if old_chunk.entities.remove(&entity) { - old_chunk.outgoing_entities.push((entity, None)); + if let Some(old_chunk) = old_inst.chunk_mut(old_chunk_pos) { + if old_chunk.entities.remove(&entity) && old_chunk.is_viewed_mut() { + let id = *entity_ids.get(entity).unwrap(); + + old_inst.entity_removals.push(EntityRemoval { + pos: old_chunk_pos, + id, + }); } } } if let Ok(mut inst) = instances.get_mut(loc.0) { - if let Some(chunk) = inst.chunk_mut(pos) { - if chunk.entities.insert(entity) { - chunk.incoming_entities.push((entity, None)); + if let Some(chunk) = inst.chunk_mut(chunk_pos) { + if chunk.entities.insert(entity) && chunk.is_viewed_mut() { + inst.message_buf + .send(MessageCondition::View { pos: chunk_pos }, |w| { + init_item.write_init_packets(old_pos.get(), w) + }); } } else { // Entity is now orphaned. commands.entity(entity).insert(Orphaned); } } - } else if pos != old_pos { + } else if chunk_pos != old_chunk_pos { // Entity changed its chunk position without changing instances. Remove it from // the old chunk and insert it in the new chunk. if let Ok(mut inst) = instances.get_mut(loc.0) { - let in_new_chunk = inst.chunk(pos).is_some(); + // TODO: extra hashmap lookup that isn't strictly necessary. + let in_new_chunk = inst.chunk(chunk_pos).is_some(); let mut in_old_chunk = true; - if let Some(old_chunk) = inst.chunk_mut(old_pos) { - if old_chunk.entities.remove(&entity) { - let to = if in_new_chunk { Some(pos) } else { None }; - old_chunk.outgoing_entities.push((entity, to)); + if let Some(old_chunk) = inst.chunk_mut(old_chunk_pos) { + if old_chunk.entities.remove(&entity) && old_chunk.is_viewed_mut() { + let id = *entity_ids.get(entity).unwrap(); + + if in_new_chunk { + inst.message_buf.send_packet( + MessageCondition::TransitionView { + viewed: old_chunk_pos, + unviewed: chunk_pos, + }, + &EntitiesDestroyS2c { + entity_ids: Cow::Borrowed(&[id.get().into()]), + }, + ); + } else { + inst.entity_removals.push(EntityRemoval { + pos: old_chunk_pos, + id, + }) + } } else { in_old_chunk = false; } } - if let Some(chunk) = inst.chunk_mut(pos) { - let from = if in_old_chunk { Some(old_pos) } else { None }; - - if chunk.entities.insert(entity) { - chunk.incoming_entities.push((entity, from)); + if let Some(chunk) = inst.chunk_mut(chunk_pos) { + if chunk.entities.insert(entity) && chunk.is_viewed_mut() { + if in_old_chunk { + inst.message_buf.send( + MessageCondition::TransitionView { + viewed: chunk_pos, + unviewed: old_chunk_pos, + }, + |w| init_item.write_init_packets(old_pos.get(), w), + ); + } else { + inst.message_buf + .send(MessageCondition::View { pos: chunk_pos }, |w| { + init_item.write_init_packets(old_pos.get(), w) + }); + }; } } else { // Entity is now orphaned. @@ -214,7 +250,7 @@ fn update_entity_chunk_positions( /// Writes update packets from entities and chunks into each chunk's packet /// buffer. -fn write_update_packets_to_chunks( +fn write_update_packets_per_chunk( mut instances: Query<&mut Instance>, mut entities: Query, Without)>, ) { @@ -222,117 +258,13 @@ fn write_update_packets_to_chunks( let inst = inst.into_inner(); for (&pos, chunk) in &mut inst.chunks { - chunk.update_pre_client(pos, &inst.info, &mut entities); - } - } -} - -#[derive(WorldQuery)] -#[world_query(mutable)] -struct UpdateEntityQuery { - id: &'static EntityId, - pos: &'static Position, - old_pos: &'static OldPosition, - loc: &'static Location, - old_loc: &'static OldLocation, - look: Ref<'static, Look>, - head_yaw: Ref<'static, HeadYaw>, - on_ground: &'static OnGround, - velocity: Ref<'static, Velocity>, - tracked_data: &'static TrackedData, - statuses: &'static EntityStatuses, - animations: &'static EntityAnimations, - packet_byte_range: Option<&'static mut PacketByteRange>, -} - -impl UpdateEntityQueryItem<'_> { - fn write_update_packets(&self, mut writer: impl WritePacket) { - // TODO: @RJ I saw you're using UpdateEntityPosition and UpdateEntityRotation sometimes. These two packets are actually broken on the client and will erase previous position/rotation https://bugs.mojang.com/browse/MC-255263 -Moulberry - - let entity_id = VarInt(self.id.get()); - - let position_delta = self.pos.0 - self.old_pos.get(); - let needs_teleport = position_delta.abs().max_element() >= 8.0; - let changed_position = self.pos.0 != self.old_pos.get(); - - if changed_position && !needs_teleport && self.look.is_changed() { - writer.write_packet(&RotateAndMoveRelativeS2c { - entity_id, - delta: (position_delta * 4096.0).to_array().map(|v| v as i16), - yaw: ByteAngle::from_degrees(self.look.yaw), - pitch: ByteAngle::from_degrees(self.look.pitch), - on_ground: self.on_ground.0, - }); - } else { - if changed_position && !needs_teleport { - writer.write_packet(&MoveRelativeS2c { - entity_id, - delta: (position_delta * 4096.0).to_array().map(|v| v as i16), - on_ground: self.on_ground.0, - }); - } - - if self.look.is_changed() { - writer.write_packet(&RotateS2c { - entity_id, - yaw: ByteAngle::from_degrees(self.look.yaw), - pitch: ByteAngle::from_degrees(self.look.pitch), - on_ground: self.on_ground.0, - }); - } - } - - if needs_teleport { - writer.write_packet(&EntityPositionS2c { - entity_id, - position: self.pos.0, - yaw: ByteAngle::from_degrees(self.look.yaw), - pitch: ByteAngle::from_degrees(self.look.pitch), - on_ground: self.on_ground.0, - }); - } - - if self.velocity.is_changed() { - writer.write_packet(&EntityVelocityUpdateS2c { - entity_id, - velocity: self.velocity.to_packet_units(), - }); - } - - if self.head_yaw.is_changed() { - writer.write_packet(&EntitySetHeadYawS2c { - entity_id, - head_yaw: ByteAngle::from_degrees(self.head_yaw.0), - }); - } - - if let Some(update_data) = self.tracked_data.update_data() { - writer.write_packet(&EntityTrackerUpdateS2c { - entity_id, - metadata: update_data.into(), - }); - } - - if self.statuses.0 != 0 { - for i in 0..mem::size_of_val(self.statuses) { - if (self.statuses.0 >> i) & 1 == 1 { - writer.write_packet(&EntityStatusS2c { - entity_id: entity_id.0, - entity_status: i as u8, - }); - } - } - } - - if self.animations.0 != 0 { - for i in 0..mem::size_of_val(self.animations) { - if (self.animations.0 >> i) & 1 == 1 { - writer.write_packet(&EntityAnimationS2c { - entity_id, - animation: i as u8, - }); - } - } + chunk.update_pre_client( + pos, + &inst.info, + &mut inst.message_buf, + &mut inst.biome_changes, + &mut entities, + ); } } } @@ -356,6 +288,8 @@ fn update_post_client(mut instances: Query<&mut Instance>, mut commands: Command } }); - inst.packet_buf.clear(); + inst.message_buf.clear(); + inst.entity_removals.clear(); + inst.biome_changes.clear(); } } diff --git a/crates/valence_instance/src/message.rs b/crates/valence_instance/src/message.rs index 7fac188ba..823c8a25d 100644 --- a/crates/valence_instance/src/message.rs +++ b/crates/valence_instance/src/message.rs @@ -1,22 +1,17 @@ use bevy_ecs::entity::Entity; -use glam::DVec3; -use valence_core::{ - chunk_pos::ChunkPos, - protocol::{ - encode::{PacketWriter, WritePacket}, - Encode, Packet, - }, -}; +use valence_core::chunk_pos::ChunkPos; +use valence_core::protocol::encode::{PacketWriter, WritePacket}; +use valence_core::protocol::{Encode, Packet}; #[derive(Clone, Debug)] -pub struct MessageBuffer { - messages: Vec, +pub struct MessageBuf { + messages: Vec>, bytes: Vec, compression_threshold: Option, } -impl MessageBuffer { - pub fn new(compression_threshold: Option) -> Self { +impl MessageBuf { + pub(super) fn new(compression_threshold: Option) -> Self { Self { messages: vec![], bytes: vec![], @@ -24,7 +19,7 @@ impl MessageBuffer { } } - pub fn messages(&self) -> &[Message] { + pub fn messages(&self) -> &[Message] { &self.messages } @@ -32,28 +27,25 @@ impl MessageBuffer { &self.bytes } - pub fn append_packet

(&mut self, cond: MessageCondition, pkt: &P) + pub fn send_packet

(&mut self, cond: C, pkt: &P) where P: Packet + Encode, { - let threshold = self.compression_threshold; - - self.append(cond, |bytes| { - PacketWriter::new(bytes, threshold).write_packet(pkt) - }) + self.send(cond, |mut w| w.write_packet(pkt)) } - pub fn append_packet_bytes(&mut self, cond: MessageCondition, bytes: &[u8]) { + pub fn send_packet_bytes(&mut self, cond: C, bytes: &[u8]) { if !bytes.is_empty() { - self.append(cond, |b| b.extend_from_slice(bytes)); + self.send(cond, |mut w| w.write_packet_bytes(bytes)); } } - fn append(&mut self, cond: MessageCondition, append_data: impl FnOnce(&mut Vec)) { + pub fn send(&mut self, cond: C, append_data: impl FnOnce(PacketWriter)) { const LOOKBACK_BYTE_LIMIT: usize = 512; const LOOKBACK_MSG_LIMIT: usize = 64; - // Look for a message with an identical condition to ours. If we find one, move it to the front and merge our message with it. + // Look for a message with an identical condition to ours. If we find one, move + // it to the front and merge our message with it. let mut acc = 0; @@ -61,7 +53,10 @@ impl MessageBuffer { if let Some(msg) = self.messages.last_mut() { if msg.cond == cond { let old_len = self.bytes.len(); - append_data(&mut self.bytes); + append_data(PacketWriter::new( + &mut self.bytes, + self.compression_threshold, + )); let new_len = self.bytes.len(); msg.len += new_len - old_len; @@ -97,7 +92,10 @@ impl MessageBuffer { self.bytes.drain(range); let old_len = self.bytes.len(); - append_data(&mut self.bytes); + append_data(PacketWriter::new( + &mut self.bytes, + self.compression_threshold, + )); let new_len = self.bytes.len(); msg.len += new_len - old_len; @@ -111,7 +109,10 @@ impl MessageBuffer { // Didn't find a compatible message, so append a new one to the end. let old_len = self.bytes.len(); - append_data(&mut self.bytes); + append_data(PacketWriter::new( + &mut self.bytes, + self.compression_threshold, + )); let new_len = self.bytes.len(); self.messages.push(Message { @@ -124,17 +125,22 @@ impl MessageBuffer { self.messages.clear(); self.bytes.clear(); } + + pub fn shrink_to_fit(&mut self) { + self.messages.shrink_to_fit(); + self.bytes.shrink_to_fit(); + } } #[derive(Copy, Clone, PartialEq, Debug)] -pub struct Message { - pub cond: MessageCondition, +pub struct Message { + pub cond: C, /// Length of this message in bytes. pub len: usize, } -impl Message { - pub const fn new(cond: MessageCondition, len: usize) -> Self { +impl Message { + pub const fn new(cond: C, len: usize) -> Self { Self { cond, len } } } @@ -162,11 +168,13 @@ pub enum MessageCondition { viewed: ChunkPos, unviewed: ChunkPos, }, + /* /// Client's position must be contained in this sphere. Sphere { center: DVec3, radius: f64, }, + */ } #[cfg(test)] @@ -179,22 +187,21 @@ mod tests { let cond2 = MessageCondition::Except { client: Entity::PLACEHOLDER, }; - let cond3 = MessageCondition::Sphere { - center: DVec3::ZERO, - radius: 10.0, + let cond3 = MessageCondition::View { + pos: ChunkPos::new(5, 6), }; - let mut buf = MessageBuffer::new(None); + let mut buf = MessageBuf::new(None); let bytes = &[1, 2, 3, 4, 5]; - buf.append_packet_bytes(cond1, bytes); - buf.append_packet_bytes(cond2, bytes); - buf.append_packet_bytes(cond3, bytes); + buf.send_packet_bytes(cond1, bytes); + buf.send_packet_bytes(cond2, bytes); + buf.send_packet_bytes(cond3, bytes); - buf.append_packet_bytes(cond2, bytes); - buf.append_packet_bytes(cond3, bytes); - buf.append_packet_bytes(cond1, bytes); + buf.send_packet_bytes(cond2, bytes); + buf.send_packet_bytes(cond3, bytes); + buf.send_packet_bytes(cond1, bytes); let msgs = buf.messages(); From 3e7fc00a54bf5c2beffca2e66cafd26562a7d81a Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 5 Jul 2023 23:37:09 -0700 Subject: [PATCH 03/47] chunk BVH --- crates/valence_core/src/chunk_pos.rs | 24 ++ crates/valence_instance/src/chunk_bvh.rs | 320 +++++++++++++++++++++++ crates/valence_instance/src/lib.rs | 2 + 3 files changed, 346 insertions(+) create mode 100644 crates/valence_instance/src/chunk_bvh.rs diff --git a/crates/valence_core/src/chunk_pos.rs b/crates/valence_core/src/chunk_pos.rs index ea0462273..14a47a8f0 100644 --- a/crates/valence_core/src/chunk_pos.rs +++ b/crates/valence_core/src/chunk_pos.rs @@ -123,6 +123,17 @@ impl ChunkView { pub fn diff(self, other: Self) -> impl DoubleEndedIterator + Clone { self.iter().filter(move |&p| !other.contains(p)) } + + /// Returns a `(min, max)` tuple of a tight bounding box containing this + /// view. + pub fn bounding_box(self) -> (ChunkPos, ChunkPos) { + let r = self.dist as i32 + EXTRA_VIEW_RADIUS; + + ( + ChunkPos::new(self.pos.x - r, self.pos.z - r), + ChunkPos::new(self.pos.x + r, self.pos.x + r), + ) + } } #[cfg(test)] @@ -151,4 +162,17 @@ mod tests { assert_eq!(ChunkPos::from(<(i32, i32)>::from(p)), p); assert_eq!(ChunkPos::from(<[i32; 2]>::from(p)), p); } + + #[test] + fn view_bounding_box() { + let view = ChunkView::new(ChunkPos::new(5, -4), 32); + + let (min, max) = view.bounding_box(); + + for z in min.z..=max.z { + for x in min.x..=max.x { + assert!(view.contains(ChunkPos::new(x, z))); + } + } + } } diff --git a/crates/valence_instance/src/chunk_bvh.rs b/crates/valence_instance/src/chunk_bvh.rs new file mode 100644 index 000000000..87c44adf0 --- /dev/null +++ b/crates/valence_instance/src/chunk_bvh.rs @@ -0,0 +1,320 @@ +use std::mem; +use std::ops::Range; + +use valence_core::chunk_pos::{ChunkPos, ChunkView}; + +#[derive(Clone, Debug)] +pub struct ChunkBvh { + nodes: Vec, + values: Vec, +} + +#[derive(Clone, Debug)] +enum Node { + Internal { + bounds: Aabb, + left: NodeIdx, + right: NodeIdx, + }, + Leaf { + bounds: Aabb, + /// Range of values in the values array. + values: Range, + }, +} + +#[cfg(test)] +impl Node { + fn bounds(&self) -> Aabb { + match self { + Node::Internal { bounds, .. } => *bounds, + Node::Leaf { bounds, .. } => *bounds, + } + } +} + +type NodeIdx = u32; + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +struct Aabb { + min: ChunkPos, + max: ChunkPos, +} + +impl Aabb { + fn point(pos: ChunkPos) -> Self { + Self { min: pos, max: pos } + } + + /// Sum of side lengths. + fn surface_area(self) -> i32 { + (self.length_x() + self.length_z()) * 2 + } + + /// Returns the smallest AABB containing `self` and `other`. + fn union(self, other: Self) -> Self { + Self { + min: ChunkPos::new(self.min.x.min(other.min.x), self.min.z.min(other.min.z)), + max: ChunkPos::new(self.max.x.max(other.max.x), self.max.z.max(other.max.z)), + } + } + + fn length_x(self) -> i32 { + self.max.x - self.min.x + } + + fn length_z(self) -> i32 { + self.max.z - self.min.z + } + + fn intersects(self, other: Self) -> bool { + self.min.x <= other.max.x + && self.max.x >= other.min.x + && self.min.z <= other.max.z + && self.max.z >= other.min.z + } +} + +pub trait GetChunkPos { + fn chunk_pos(&self) -> ChunkPos; +} + +impl GetChunkPos for ChunkPos { + fn chunk_pos(&self) -> ChunkPos { + *self + } +} + +/// When to stop subdividing nodes. +const MAX_SURFACE_AREA: i32 = 8 * 4; + +impl ChunkBvh { + pub fn new() -> Self { + Self { + nodes: vec![], + values: vec![], + } + } + + pub fn build(&mut self, items: impl IntoIterator) { + self.nodes.clear(); + self.values.clear(); + + self.values.extend(items); + + if let Some(bounds) = value_bounds(&self.values) { + self.build_rec(bounds, 0..self.values.len()); + } + } + + fn build_rec(&mut self, bounds: Aabb, value_range: Range) { + if bounds.surface_area() <= MAX_SURFACE_AREA { + self.nodes.push(Node::Leaf { + bounds, + values: value_range.start as u32..value_range.end as u32, + }); + + return; + } + + let values = &mut self.values[value_range.clone()]; + + // Determine splitting axis based on the side that's longer. Then split along + // the spatial midpoint. We could use a more advanced heuristic like SAH, + // but it probably doesn't matter here. + + let point = if bounds.length_x() >= bounds.length_z() { + // Split on Z axis. + + let mid = middle(bounds.min.x, bounds.max.x); + partition(values, |v| v.chunk_pos().x >= mid) + } else { + // Split on X axis. + + let mid = middle(bounds.min.z, bounds.max.z); + partition(values, |v| v.chunk_pos().z >= mid) + }; + + let left_range = value_range.start..value_range.start + point; + let right_range = left_range.end..value_range.end; + + let left_bounds = + value_bounds(&self.values[left_range.clone()]).expect("left half should be nonempty"); + + let right_bounds = + value_bounds(&self.values[right_range.clone()]).expect("right half should be nonempty"); + + self.build_rec(left_bounds, left_range); + let left_idx = (self.nodes.len() - 1) as NodeIdx; + + self.build_rec(right_bounds, right_range); + let right_idx = (self.nodes.len() - 1) as NodeIdx; + + self.nodes.push(Node::Internal { + bounds, + left: left_idx, + right: right_idx, + }); + } + + pub fn traverse(&self, view: ChunkView, mut f: impl FnMut(&T)) { + if let Some(root) = self.nodes.last() { + let (min, max) = view.bounding_box(); + self.traverse_rec(root, view, Aabb { min, max }, &mut f); + } + } + + fn traverse_rec(&self, node: &Node, view: ChunkView, view_aabb: Aabb, f: &mut impl FnMut(&T)) { + match node { + Node::Internal { + bounds, + left, + right, + } => { + if bounds.intersects(view_aabb) { + self.traverse_rec(&self.nodes[*left as usize], view, view_aabb, f); + self.traverse_rec(&self.nodes[*right as usize], view, view_aabb, f); + } + } + Node::Leaf { bounds, values } => { + if bounds.intersects(view_aabb) { + for val in &self.values[values.start as usize..values.end as usize] { + if view.contains(val.chunk_pos()) { + f(val) + } + } + } + } + } + } + + #[cfg(test)] + fn check_invariants(&self) { + if let Some(root) = self.nodes.last() { + self.check_invariants_rec(root); + } + } + + #[cfg(test)] + fn check_invariants_rec(&self, node: &Node) { + match node { + Node::Internal { + bounds, + left, + right, + } => { + let left = &self.nodes[*left as usize]; + let right = &self.nodes[*right as usize]; + + assert_eq!(left.bounds().union(right.bounds()), *bounds); + + self.check_invariants_rec(left); + self.check_invariants_rec(right); + } + Node::Leaf { bounds: leaf_bounds, values } => { + let bounds = value_bounds(&self.values[values.start as usize..values.end as usize]) + .expect("leaf should be nonempty"); + + assert_eq!(*leaf_bounds, bounds); + } + } + } +} + +fn value_bounds(values: &[T]) -> Option { + values + .iter() + .map(|v| Aabb::point(v.chunk_pos())) + .reduce(Aabb::union) +} + +fn middle(min: i32, max: i32) -> i32 { + // Cast to i64 to avoid intermediate overflow. + ((min as i64 + max as i64) / 2) as i32 +} + +/// Partitions the slice in place and returns the partition point. Why this +/// isn't in Rust's stdlib I don't know. +fn partition(s: &mut [T], mut pred: impl FnMut(&T) -> bool) -> usize { + let mut it = s.iter_mut(); + let mut true_count = 0; + + while let Some(head) = it.find(|x| { + if pred(x) { + true_count += 1; + false + } else { + true + } + }) { + if let Some(tail) = it.rfind(|x| pred(x)) { + mem::swap(head, tail); + true_count += 1; + } else { + break; + } + } + true_count +} + +#[cfg(test)] +mod tests { + use rand::Rng; + + use super::*; + + #[test] + fn partition_middle() { + let mut arr = [2, 3, 4, 5]; + let mid = middle(arr[0], arr[arr.len() - 1]); + + let point = partition(&mut arr, |&x| mid >= x); + + assert_eq!(point, 2); + assert_eq!(&arr[..point], &[2, 3]); + assert_eq!(&arr[point..], &[4, 5]); + } + + #[test] + fn traversal_visits_correct_nodes() { + let mut bvh = ChunkBvh::::new(); + + let mut positions = vec![]; + + let size = 500; + let mut rng = rand::thread_rng(); + + // Create a bunch of positions in a large area. + for _ in 0..100_000 { + positions.push(ChunkPos { + x: rng.gen_range(-size / 2..size / 2), + z: rng.gen_range(-size / 2..size / 2), + }); + } + + // Put the view in the center of that area. + let view = ChunkView::new(ChunkPos::default(), 32); + + let mut viewed_positions = vec![]; + + // Create a list of positions the view contains. + for &pos in &positions { + if view.contains(pos) { + viewed_positions.push(pos); + } + } + + bvh.build(positions); + + bvh.check_invariants(); + + // Check that we traverse exactly the positions that we know the view can see. + + bvh.traverse(view, |pos| { + let idx = viewed_positions.iter().position(|p| p == pos).expect("😔"); + viewed_positions.remove(idx); + }); + + assert!(viewed_positions.is_empty()); + } +} diff --git a/crates/valence_instance/src/lib.rs b/crates/valence_instance/src/lib.rs index 30a3b75d3..6310c56a6 100644 --- a/crates/valence_instance/src/lib.rs +++ b/crates/valence_instance/src/lib.rs @@ -35,6 +35,8 @@ use valence_entity::{ }; pub mod chunk; +#[doc(hidden)] +pub mod chunk_bvh; mod instance; pub mod message; pub mod packet; From 1266a6b16d8e9efa707edc787f5871a751961e53 Mon Sep 17 00:00:00 2001 From: Ryan Date: Sat, 8 Jul 2023 00:35:57 -0700 Subject: [PATCH 04/47] replace `valence_instance` with `valence_layer` --- crates/valence_anvil/src/lib.rs | 4 +- crates/valence_client/src/lib.rs | 25 +- crates/valence_client/src/weather.rs | 2 +- crates/valence_entity/build.rs | 8 +- crates/valence_entity/src/layer.rs | 32 -- crates/valence_entity/src/lib.rs | 87 +---- crates/valence_entity/src/manager.rs | 53 ++++ crates/valence_entity/src/query.rs | 46 +-- crates/valence_instance/README.md | 3 - crates/valence_instance/src/lib.rs | 297 ------------------ crates/valence_instance/src/message.rs | 212 ------------- .../Cargo.toml | 2 +- crates/valence_layer/README.md | 1 + .../chunk_bvh.rs => valence_layer/src/bvh.rs} | 41 ++- .../src/chunk.rs} | 160 +++++++--- .../src => valence_layer/src/chunk}/chunk.rs | 29 +- .../src/chunk/loaded.rs | 179 +++++------ .../src/chunk/paletted_container.rs | 6 +- .../src/chunk/unloaded.rs | 8 +- crates/valence_layer/src/entity.rs | 278 ++++++++++++++++ crates/valence_layer/src/lib.rs | 112 +++++++ crates/valence_layer/src/message.rs | 233 ++++++++++++++ .../src/packet.rs | 0 crates/valence_world_border/src/lib.rs | 4 +- examples/advancement.rs | 2 +- examples/anvil_loading.rs | 10 +- examples/bench_players.rs | 2 +- examples/biomes.rs | 2 +- examples/block_entities.rs | 5 +- examples/boss_bar.rs | 2 +- examples/building.rs | 10 +- examples/chest.rs | 2 +- examples/combat.rs | 2 +- examples/cow_sphere.rs | 4 +- examples/death.rs | 4 +- examples/entity_hitbox.rs | 12 +- examples/game_of_life.rs | 2 +- examples/parkour.rs | 2 +- examples/particles.rs | 2 +- examples/player_list.rs | 10 +- examples/resource_pack.rs | 4 +- examples/terrain.rs | 10 +- examples/text.rs | 10 +- examples/world_border.rs | 6 +- src/lib.rs | 4 +- src/tests/client.rs | 4 +- src/tests/world_border.rs | 4 +- 47 files changed, 1077 insertions(+), 860 deletions(-) delete mode 100644 crates/valence_entity/src/layer.rs create mode 100644 crates/valence_entity/src/manager.rs delete mode 100644 crates/valence_instance/README.md delete mode 100644 crates/valence_instance/src/lib.rs delete mode 100644 crates/valence_instance/src/message.rs rename crates/{valence_instance => valence_layer}/Cargo.toml (95%) create mode 100644 crates/valence_layer/README.md rename crates/{valence_instance/src/chunk_bvh.rs => valence_layer/src/bvh.rs} (87%) rename crates/{valence_instance/src/instance.rs => valence_layer/src/chunk.rs} (75%) rename crates/{valence_instance/src => valence_layer/src/chunk}/chunk.rs (94%) rename crates/{valence_instance => valence_layer}/src/chunk/loaded.rs (84%) rename crates/{valence_instance => valence_layer}/src/chunk/paletted_container.rs (98%) rename crates/{valence_instance => valence_layer}/src/chunk/unloaded.rs (97%) create mode 100644 crates/valence_layer/src/entity.rs create mode 100644 crates/valence_layer/src/lib.rs create mode 100644 crates/valence_layer/src/message.rs rename crates/{valence_instance => valence_layer}/src/packet.rs (100%) diff --git a/crates/valence_anvil/src/lib.rs b/crates/valence_anvil/src/lib.rs index 7695c166d..cb9f52136 100644 --- a/crates/valence_anvil/src/lib.rs +++ b/crates/valence_anvil/src/lib.rs @@ -37,7 +37,7 @@ use valence_biome::{BiomeId, BiomeRegistry}; use valence_client::{Client, OldView, UpdateClientsSet, View}; use valence_core::chunk_pos::ChunkPos; use valence_core::ident::Ident; -use valence_entity::{Location, OldLocation}; +use valence_entity::{EntityLayerId, OldEntityLayerId}; use valence_instance::chunk::UnloadedChunk; use valence_instance::Instance; use valence_nbt::Compound; @@ -317,7 +317,7 @@ fn remove_unviewed_chunks( } fn update_client_views( - clients: Query<(&Location, Ref, View, OldView), With>, + clients: Query<(&EntityLayerId, Ref, View, OldView), With>, mut instances: Query<(&Instance, &mut AnvilLevel)>, ) { for (loc, old_loc, view, old_view) in &clients { diff --git a/crates/valence_client/src/lib.rs b/crates/valence_client/src/lib.rs index 8be6050af..1b37ea66f 100644 --- a/crates/valence_client/src/lib.rs +++ b/crates/valence_client/src/lib.rs @@ -58,8 +58,9 @@ use valence_entity::packet::{ }; use valence_entity::player::PlayerEntityBundle; use valence_entity::{ - ClearEntityChangesSet, EntityId, EntityKind, EntityStatus, HeadYaw, Location, Look, ObjectData, - OldLocation, OldPosition, OnGround, PacketByteRange, Position, TrackedData, Velocity, + ClearEntityChangesSet, EntityId, EntityKind, EntityLayerId, EntityStatus, HeadYaw, Look, + ObjectData, OldEntityLayerId, OldPosition, OnGround, PacketByteRange, Position, TrackedData, + Velocity, }; use valence_instance::chunk::loaded::ChunkState; use valence_instance::packet::{ @@ -665,7 +666,7 @@ pub fn despawn_disconnected_clients( struct ClientJoinQuery { entity: Entity, client: &'static mut Client, - loc: &'static Location, + loc: &'static EntityLayerId, pos: &'static Position, is_hardcore: &'static IsHardcore, game_mode: &'static GameMode, @@ -744,7 +745,7 @@ fn respawn( mut clients: Query< ( &mut Client, - &Location, + &EntityLayerId, &DeathLocation, &HashedSeed, &GameMode, @@ -752,7 +753,7 @@ fn respawn( &IsDebug, &IsFlat, ), - Changed, + Changed, >, instances: Query<&Instance>, ) { @@ -813,8 +814,8 @@ fn read_data_in_old_view( Entity, &mut Client, &mut EntityRemoveBuf, - &Location, - &OldLocation, + &EntityLayerId, + &OldEntityLayerId, &Position, &OldPosition, &OldViewDistance, @@ -939,14 +940,18 @@ fn update_view( Entity, &mut Client, &mut EntityRemoveBuf, - &Location, - &OldLocation, + &EntityLayerId, + &OldEntityLayerId, &Position, &OldPosition, &ViewDistance, &OldViewDistance, ), - Or<(Changed, Changed, Changed)>, + Or<( + Changed, + Changed, + Changed, + )>, >, instances: Query<&Instance>, entities: Query<(EntityInitQuery, &Position)>, diff --git a/crates/valence_client/src/weather.rs b/crates/valence_client/src/weather.rs index 2f0ce9788..b77564fb9 100644 --- a/crates/valence_client/src/weather.rs +++ b/crates/valence_client/src/weather.rs @@ -73,7 +73,7 @@ pub struct Rain(pub f32); pub struct Thunder(pub f32); fn handle_weather_for_joined_player( - mut clients: Query<(&mut Client, &Location), Added>, + mut clients: Query<(&mut Client, &EntityLayerId), Added>, weathers: Query<(Option<&Rain>, Option<&Thunder>), With>, ) { for (mut client, loc) in &mut clients { diff --git a/crates/valence_entity/build.rs b/crates/valence_entity/build.rs index 23565086b..4de3539ad 100644 --- a/crates/valence_entity/build.rs +++ b/crates/valence_entity/build.rs @@ -390,8 +390,8 @@ fn build() -> anyhow::Result { pub kind: super::EntityKind, pub id: super::EntityId, pub uuid: super::UniqueId, - pub location: super::Location, - pub old_location: super::OldLocation, + pub layer: super::EntityLayerId, + pub old_layer: super::EntityLayerId, pub position: super::Position, pub old_position: super::OldPosition, pub look: super::Look, @@ -408,8 +408,8 @@ fn build() -> anyhow::Result { kind: super::EntityKind::#stripped_shouty_entity_name_ident, id: Default::default(), uuid: Default::default(), - location: Default::default(), - old_location: Default::default(), + layer: Default::default(), + old_layer: Default::default(), position: Default::default(), old_position: Default::default(), look: Default::default(), diff --git a/crates/valence_entity/src/layer.rs b/crates/valence_entity/src/layer.rs deleted file mode 100644 index edd9f02eb..000000000 --- a/crates/valence_entity/src/layer.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::sync::atomic::{AtomicU32, Ordering}; - -use bevy_ecs::prelude::*; -use tracing::warn; - -#[derive(Component, Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] -pub struct Layer(u32); - -impl Layer { - pub const DEFAULT: Self = Self(0); - - pub fn new() -> Self { - static NEXT: AtomicU32 = AtomicU32::new(1); // Skip default layer. - - let val = NEXT.fetch_add(1, Ordering::Relaxed); - - if val == 0 { - warn!("layer counter overflowed!"); - } - - Self(val) - } -} - -#[derive(Component, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] -pub struct OldLayer(Layer); - -impl OldLayer { - pub fn get(&self) -> Layer { - self.0 - } -} diff --git a/crates/valence_entity/src/lib.rs b/crates/valence_entity/src/lib.rs index 2169c0bcf..07c87636a 100644 --- a/crates/valence_entity/src/lib.rs +++ b/crates/valence_entity/src/lib.rs @@ -16,24 +16,22 @@ unreachable_pub, clippy::dbg_macro )] +#![allow(clippy::type_complexity)] mod flags; pub mod hitbox; -pub mod layer; +pub mod manager; pub mod packet; pub mod query; pub mod tracked_data; -use std::num::Wrapping; - use bevy_app::prelude::*; use bevy_ecs::prelude::*; use glam::{DVec3, Vec3}; +pub use manager::EntityManager; use paste::paste; -use rustc_hash::FxHashMap; use tracing::warn; use tracked_data::TrackedData; -use uuid::Uuid; use valence_core::chunk_pos::ChunkPos; use valence_core::despawn::Despawned; use valence_core::protocol::var_int::VarInt; @@ -111,7 +109,7 @@ fn update_old_position(mut query: Query<(&Position, &mut OldPosition)>) { } } -fn update_old_location(mut query: Query<(&Location, &mut OldLocation)>) { +fn update_old_location(mut query: Query<(&EntityLayerId, &mut OldEntityLayerId)>) { for (loc, mut old_loc) in &mut query { old_loc.0 = loc.0; } @@ -153,7 +151,6 @@ fn init_entities( } } -#[allow(clippy::type_complexity)] fn remove_despawned_from_manager( entities: Query<(&EntityId, &UniqueId), (With, With)>, mut manager: ResMut, @@ -184,47 +181,40 @@ fn clear_tracked_data_changes(mut tracked_data: Query<&mut TrackedData, Changed< } } -/// Contains the `Instance` an entity is located in. For the coordinates -/// within the instance, see [`Position`]. +/// Contains the entity layer an entity is on. #[derive(Component, Copy, Clone, PartialEq, Eq, Debug)] -pub struct Location(pub Entity); +pub struct EntityLayerId(pub Entity); -impl Default for Location { +impl Default for EntityLayerId { fn default() -> Self { Self(Entity::PLACEHOLDER) } } -impl PartialEq for Location { - fn eq(&self, other: &OldLocation) -> bool { +impl PartialEq for EntityLayerId { + fn eq(&self, other: &OldEntityLayerId) -> bool { self.0 == other.0 } } -/// The value of [`Location`] from the end of the previous tick. -/// -/// **NOTE**: You should not modify this component after the entity is spawned. +/// The value of [`EntityLayerId`] from the end of the previous tick. #[derive(Component, Copy, Clone, PartialEq, Eq, Debug)] -pub struct OldLocation(Entity); - -impl OldLocation { - pub fn new(instance: Entity) -> Self { - Self(instance) - } +pub struct OldEntityLayerId(Entity); +impl OldEntityLayerId { pub fn get(&self) -> Entity { self.0 } } -impl Default for OldLocation { +impl Default for OldEntityLayerId { fn default() -> Self { Self(Entity::PLACEHOLDER) } } -impl PartialEq for OldLocation { - fn eq(&self, other: &Location) -> bool { +impl PartialEq for OldEntityLayerId { + fn eq(&self, other: &EntityLayerId) -> bool { self.0 == other.0 } } @@ -363,6 +353,8 @@ impl Velocity { } } +// TODO: don't make statuses and animations components. + #[derive(Component, Copy, Clone, Default, Debug)] pub struct EntityStatuses(pub u64); @@ -602,48 +594,3 @@ impl Decode<'_> for OptionalInt { })) } } - -/// A [`Resource`] which maintains information about all spawned Minecraft -/// entities. -#[derive(Resource, Debug)] -pub struct EntityManager { - /// Maps protocol IDs to ECS entities. - id_to_entity: FxHashMap, - uuid_to_entity: FxHashMap, - next_id: Wrapping, -} - -impl EntityManager { - fn new() -> Self { - Self { - id_to_entity: FxHashMap::default(), - uuid_to_entity: FxHashMap::default(), - next_id: Wrapping(1), // Skip 0. - } - } - - /// Returns the next unique entity ID and increments the counter. - pub fn next_id(&mut self) -> EntityId { - if self.next_id.0 == 0 { - warn!("entity ID overflow!"); - // ID 0 is reserved for clients, so skip over it. - self.next_id.0 = 1; - } - - let id = EntityId(self.next_id.0); - - self.next_id += 1; - - id - } - - /// Gets the entity with the given entity ID. - pub fn get_by_id(&self, entity_id: i32) -> Option { - self.id_to_entity.get(&entity_id).cloned() - } - - /// Gets the entity with the given UUID. - pub fn get_by_uuid(&self, uuid: Uuid) -> Option { - self.uuid_to_entity.get(&uuid).cloned() - } -} diff --git a/crates/valence_entity/src/manager.rs b/crates/valence_entity/src/manager.rs new file mode 100644 index 000000000..390902718 --- /dev/null +++ b/crates/valence_entity/src/manager.rs @@ -0,0 +1,53 @@ +use std::num::Wrapping; + +use bevy_ecs::prelude::*; +use rustc_hash::FxHashMap; +use tracing::warn; +use uuid::Uuid; + +use super::EntityId; + +/// A [`Resource`] which maintains information about all spawned Minecraft +/// entities. +#[derive(Resource, Debug)] +pub struct EntityManager { + /// Maps protocol IDs to ECS entities. + pub(super) id_to_entity: FxHashMap, + pub(super) uuid_to_entity: FxHashMap, + next_id: Wrapping, +} + +impl EntityManager { + pub(super) fn new() -> Self { + Self { + id_to_entity: FxHashMap::default(), + uuid_to_entity: FxHashMap::default(), + next_id: Wrapping(1), // Skip 0. + } + } + + /// Returns the next unique entity ID and increments the counter. + pub fn next_id(&mut self) -> EntityId { + if self.next_id.0 == 0 { + warn!("entity ID overflow!"); + // ID 0 is reserved for clients, so skip over it. + self.next_id.0 = 1; + } + + let id = EntityId(self.next_id.0); + + self.next_id += 1; + + id + } + + /// Gets the entity with the given entity ID. + pub fn get_by_id(&self, entity_id: i32) -> Option { + self.id_to_entity.get(&entity_id).cloned() + } + + /// Gets the entity with the given UUID. + pub fn get_by_uuid(&self, uuid: Uuid) -> Option { + self.uuid_to_entity.get(&uuid).cloned() + } +} diff --git a/crates/valence_entity/src/query.rs b/crates/valence_entity/src/query.rs index 5a1017618..f31c77016 100644 --- a/crates/valence_entity/src/query.rs +++ b/crates/valence_entity/src/query.rs @@ -16,21 +16,21 @@ use crate::packet::{ }; use crate::tracked_data::TrackedData; use crate::{ - EntityAnimations, EntityId, EntityKind, EntityStatuses, HeadYaw, Location, Look, ObjectData, - OldLocation, OldPosition, OnGround, Position, Velocity, + EntityAnimations, EntityId, EntityKind, EntityLayerId, EntityStatuses, HeadYaw, Look, + ObjectData, OldEntityLayerId, OldPosition, OnGround, Position, Velocity, }; #[derive(WorldQuery)] pub struct EntityInitQuery { - entity_id: &'static EntityId, - uuid: &'static UniqueId, - kind: &'static EntityKind, - look: &'static Look, - head_yaw: &'static HeadYaw, - on_ground: &'static OnGround, - object_data: &'static ObjectData, - velocity: &'static Velocity, - tracked_data: &'static TrackedData, + pub entity_id: &'static EntityId, + pub uuid: &'static UniqueId, + pub kind: &'static EntityKind, + pub look: &'static Look, + pub head_yaw: &'static HeadYaw, + pub on_ground: &'static OnGround, + pub object_data: &'static ObjectData, + pub velocity: &'static Velocity, + pub tracked_data: &'static TrackedData, } impl EntityInitQueryItem<'_> { @@ -86,18 +86,18 @@ impl EntityInitQueryItem<'_> { #[derive(WorldQuery)] pub struct UpdateEntityQuery { - id: &'static EntityId, - pos: &'static Position, - old_pos: &'static OldPosition, - loc: &'static Location, - old_loc: &'static OldLocation, - look: Ref<'static, Look>, - head_yaw: Ref<'static, HeadYaw>, - on_ground: &'static OnGround, - velocity: Ref<'static, Velocity>, - tracked_data: &'static TrackedData, - statuses: &'static EntityStatuses, - animations: &'static EntityAnimations, + pub id: &'static EntityId, + pub pos: &'static Position, + pub old_pos: &'static OldPosition, + pub loc: &'static EntityLayerId, + pub old_loc: &'static OldEntityLayerId, + pub look: Ref<'static, Look>, + pub head_yaw: Ref<'static, HeadYaw>, + pub on_ground: &'static OnGround, + pub velocity: Ref<'static, Velocity>, + pub tracked_data: &'static TrackedData, + pub statuses: &'static EntityStatuses, + pub animations: &'static EntityAnimations, } impl UpdateEntityQueryItem<'_> { diff --git a/crates/valence_instance/README.md b/crates/valence_instance/README.md deleted file mode 100644 index 8affa990e..000000000 --- a/crates/valence_instance/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# valence_instance - -Containers for chunks and entities. Instances are analogous to "levels" or "worlds" in Minecraft's parlance. diff --git a/crates/valence_instance/src/lib.rs b/crates/valence_instance/src/lib.rs deleted file mode 100644 index 6310c56a6..000000000 --- a/crates/valence_instance/src/lib.rs +++ /dev/null @@ -1,297 +0,0 @@ -#![doc = include_str!("../README.md")] -#![deny( - rustdoc::broken_intra_doc_links, - rustdoc::private_intra_doc_links, - rustdoc::missing_crate_level_docs, - rustdoc::invalid_codeblock_attributes, - rustdoc::invalid_rust_codeblocks, - rustdoc::bare_urls, - rustdoc::invalid_html_tags -)] -#![warn( - trivial_casts, - trivial_numeric_casts, - unused_lifetimes, - unused_import_braces, - unreachable_pub, - clippy::dbg_macro -)] -#![allow(clippy::type_complexity)] - -use std::borrow::Cow; - -use bevy_app::prelude::*; -use bevy_ecs::prelude::*; -use bevy_ecs::query::Has; -use chunk::loaded::ChunkState; -use message::MessageCondition; -use valence_core::chunk_pos::ChunkPos; -use valence_core::despawn::Despawned; -use valence_entity::packet::EntitiesDestroyS2c; -use valence_entity::query::{EntityInitQuery, UpdateEntityQuery}; -use valence_entity::{ - EntityId, EntityKind, InitEntitiesSet, Location, OldLocation, OldPosition, Position, - UpdateTrackedDataSet, -}; - -pub mod chunk; -#[doc(hidden)] -pub mod chunk_bvh; -mod instance; -pub mod message; -pub mod packet; - -pub use chunk::{Block, BlockRef}; -pub use instance::*; - -pub struct InstancePlugin; - -/// When Minecraft entity changes are written to the packet buffers of -/// instances. Systems that modify entites should run _before_ this. Systems -/// that read from the packet buffer of instances should run _after_ this. -/// -/// This set lives in [`PostUpdate`]. -#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct WriteUpdatePacketsToInstancesSet; - -/// When instances are updated and changes from the current tick are cleared. -/// Systems that read changes from instances should run _before_ this. -/// -/// This set lives in [`PostUpdate`]. -#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct ClearInstanceChangesSet; - -impl Plugin for InstancePlugin { - fn build(&self, app: &mut App) { - app.configure_sets( - PostUpdate, - ( - WriteUpdatePacketsToInstancesSet - .after(InitEntitiesSet) - .after(UpdateTrackedDataSet), - ClearInstanceChangesSet.after(WriteUpdatePacketsToInstancesSet), - ), - ) - .add_systems( - PostUpdate, - // This can run at the same time as entity init because we're only looking at position - // + location. - (add_orphaned_entities, update_entity_chunk_positions) - .chain() - .before(WriteUpdatePacketsToInstancesSet), - ) - .add_systems( - PostUpdate, - write_update_packets_per_chunk - .after(update_entity_chunk_positions) - .in_set(WriteUpdatePacketsToInstancesSet), - ) - .add_systems( - PostUpdate, - update_post_client.in_set(ClearInstanceChangesSet), - ); - } -} - -/// Marker component for entities that are not contained in a chunk. -#[derive(Component, Debug)] -struct Orphaned; - -/// Attempts to add orphaned entities to the chunk they're positioned in. -fn add_orphaned_entities( - entities: Query<(Entity, &Position, &OldPosition, &Location, EntityInitQuery), With>, - mut instances: Query<&mut Instance>, - mut commands: Commands, -) { - for (entity, pos, old_pos, loc, init_item) in &entities { - if let Ok(mut inst) = instances.get_mut(loc.0) { - let pos = ChunkPos::at(pos.0.x, pos.0.z); - - if let Some(chunk) = inst.chunk_mut(pos) { - if chunk.entities.insert(entity) && chunk.is_viewed_mut() { - inst.message_buf.send(MessageCondition::View { pos }, |w| { - init_item.write_init_packets(old_pos.get(), w) - }); - } - - // Entity is no longer orphaned. - commands.entity(entity).remove::(); - } - } - } -} - -/// Handles entities moving from one chunk to another. -fn update_entity_chunk_positions( - entities: Query< - ( - Entity, - &Position, - &OldPosition, - &Location, - &OldLocation, - EntityInitQuery, - Has, - ), - (Or<(Changed, Changed, With)>,), - >, - entity_ids: Query<&EntityId>, - mut instances: Query<&mut Instance>, - mut commands: Commands, -) { - for (entity, pos, old_pos, loc, old_loc, init_item, despawned) in &entities { - let chunk_pos = ChunkPos::at(pos.0.x, pos.0.z); - let old_chunk_pos = ChunkPos::at(old_pos.get().x, old_pos.get().z); - - if despawned { - // Entity was deleted. Remove it from the chunk it was in. - if let Ok(mut old_inst) = instances.get_mut(old_loc.get()) { - if let Some(old_chunk) = old_inst.chunk_mut(old_chunk_pos) { - if old_chunk.entities.remove(&entity) { - let id = *entity_ids.get(entity).unwrap(); - - old_inst.entity_removals.push(EntityRemoval { - pos: old_chunk_pos, - id, - }); - } - } - } - } else if old_loc.get() != loc.0 { - // Entity changed the instance it's in. Remove it from old chunk and - // insert it in the new chunk. - - if let Ok(mut old_inst) = instances.get_mut(old_loc.get()) { - if let Some(old_chunk) = old_inst.chunk_mut(old_chunk_pos) { - if old_chunk.entities.remove(&entity) && old_chunk.is_viewed_mut() { - let id = *entity_ids.get(entity).unwrap(); - - old_inst.entity_removals.push(EntityRemoval { - pos: old_chunk_pos, - id, - }); - } - } - } - - if let Ok(mut inst) = instances.get_mut(loc.0) { - if let Some(chunk) = inst.chunk_mut(chunk_pos) { - if chunk.entities.insert(entity) && chunk.is_viewed_mut() { - inst.message_buf - .send(MessageCondition::View { pos: chunk_pos }, |w| { - init_item.write_init_packets(old_pos.get(), w) - }); - } - } else { - // Entity is now orphaned. - commands.entity(entity).insert(Orphaned); - } - } - } else if chunk_pos != old_chunk_pos { - // Entity changed its chunk position without changing instances. Remove it from - // the old chunk and insert it in the new chunk. - - if let Ok(mut inst) = instances.get_mut(loc.0) { - // TODO: extra hashmap lookup that isn't strictly necessary. - let in_new_chunk = inst.chunk(chunk_pos).is_some(); - let mut in_old_chunk = true; - - if let Some(old_chunk) = inst.chunk_mut(old_chunk_pos) { - if old_chunk.entities.remove(&entity) && old_chunk.is_viewed_mut() { - let id = *entity_ids.get(entity).unwrap(); - - if in_new_chunk { - inst.message_buf.send_packet( - MessageCondition::TransitionView { - viewed: old_chunk_pos, - unviewed: chunk_pos, - }, - &EntitiesDestroyS2c { - entity_ids: Cow::Borrowed(&[id.get().into()]), - }, - ); - } else { - inst.entity_removals.push(EntityRemoval { - pos: old_chunk_pos, - id, - }) - } - } else { - in_old_chunk = false; - } - } - - if let Some(chunk) = inst.chunk_mut(chunk_pos) { - if chunk.entities.insert(entity) && chunk.is_viewed_mut() { - if in_old_chunk { - inst.message_buf.send( - MessageCondition::TransitionView { - viewed: chunk_pos, - unviewed: old_chunk_pos, - }, - |w| init_item.write_init_packets(old_pos.get(), w), - ); - } else { - inst.message_buf - .send(MessageCondition::View { pos: chunk_pos }, |w| { - init_item.write_init_packets(old_pos.get(), w) - }); - }; - } - } else { - // Entity is now orphaned. - commands.entity(entity).insert(Orphaned); - } - } - } else { - // The entity didn't change its chunk position, so there's nothing - // we need to do. - } - } -} - -/// Writes update packets from entities and chunks into each chunk's packet -/// buffer. -fn write_update_packets_per_chunk( - mut instances: Query<&mut Instance>, - mut entities: Query, Without)>, -) { - for inst in &mut instances { - let inst = inst.into_inner(); - - for (&pos, chunk) in &mut inst.chunks { - chunk.update_pre_client( - pos, - &inst.info, - &mut inst.message_buf, - &mut inst.biome_changes, - &mut entities, - ); - } - } -} - -/// Clears changes made to instances and removes removed chunks. -fn update_post_client(mut instances: Query<&mut Instance>, mut commands: Commands) { - for mut inst in &mut instances { - inst.retain_chunks(|_, chunk| match chunk.state() { - ChunkState::Removed | ChunkState::AddedRemoved => { - // Any entities still in this chunk are now orphaned. - for &entity in &chunk.entities { - if let Some(mut commands) = commands.get_entity(entity) { - commands.insert(Orphaned); - } - } - false - } - ChunkState::Added | ChunkState::Overwrite | ChunkState::Normal => { - chunk.update_post_client(); - true - } - }); - - inst.message_buf.clear(); - inst.entity_removals.clear(); - inst.biome_changes.clear(); - } -} diff --git a/crates/valence_instance/src/message.rs b/crates/valence_instance/src/message.rs deleted file mode 100644 index 823c8a25d..000000000 --- a/crates/valence_instance/src/message.rs +++ /dev/null @@ -1,212 +0,0 @@ -use bevy_ecs::entity::Entity; -use valence_core::chunk_pos::ChunkPos; -use valence_core::protocol::encode::{PacketWriter, WritePacket}; -use valence_core::protocol::{Encode, Packet}; - -#[derive(Clone, Debug)] -pub struct MessageBuf { - messages: Vec>, - bytes: Vec, - compression_threshold: Option, -} - -impl MessageBuf { - pub(super) fn new(compression_threshold: Option) -> Self { - Self { - messages: vec![], - bytes: vec![], - compression_threshold, - } - } - - pub fn messages(&self) -> &[Message] { - &self.messages - } - - pub fn bytes(&self) -> &[u8] { - &self.bytes - } - - pub fn send_packet

(&mut self, cond: C, pkt: &P) - where - P: Packet + Encode, - { - self.send(cond, |mut w| w.write_packet(pkt)) - } - - pub fn send_packet_bytes(&mut self, cond: C, bytes: &[u8]) { - if !bytes.is_empty() { - self.send(cond, |mut w| w.write_packet_bytes(bytes)); - } - } - - pub fn send(&mut self, cond: C, append_data: impl FnOnce(PacketWriter)) { - const LOOKBACK_BYTE_LIMIT: usize = 512; - const LOOKBACK_MSG_LIMIT: usize = 64; - - // Look for a message with an identical condition to ours. If we find one, move - // it to the front and merge our message with it. - - let mut acc = 0; - - // Special case for the most recent message. - if let Some(msg) = self.messages.last_mut() { - if msg.cond == cond { - let old_len = self.bytes.len(); - append_data(PacketWriter::new( - &mut self.bytes, - self.compression_threshold, - )); - let new_len = self.bytes.len(); - - msg.len += new_len - old_len; - - return; - } - - acc += msg.len; - } - - for (i, msg) in self - .messages - .iter() - .enumerate() - .rev() - .take(LOOKBACK_MSG_LIMIT) - .skip(1) - { - acc += msg.len; - - if acc > LOOKBACK_BYTE_LIMIT { - break; - } - - if msg.cond == cond { - let mut msg = self.messages.remove(i); - - let start = self.bytes.len() - acc; - let range = start..start + msg.len; - - // Copy to the back and remove. - self.bytes.extend_from_within(range.clone()); - self.bytes.drain(range); - - let old_len = self.bytes.len(); - append_data(PacketWriter::new( - &mut self.bytes, - self.compression_threshold, - )); - let new_len = self.bytes.len(); - - msg.len += new_len - old_len; - - self.messages.push(msg); - - return; - } - } - - // Didn't find a compatible message, so append a new one to the end. - - let old_len = self.bytes.len(); - append_data(PacketWriter::new( - &mut self.bytes, - self.compression_threshold, - )); - let new_len = self.bytes.len(); - - self.messages.push(Message { - cond, - len: new_len - old_len, - }); - } - - pub fn clear(&mut self) { - self.messages.clear(); - self.bytes.clear(); - } - - pub fn shrink_to_fit(&mut self) { - self.messages.shrink_to_fit(); - self.bytes.shrink_to_fit(); - } -} - -#[derive(Copy, Clone, PartialEq, Debug)] -pub struct Message { - pub cond: C, - /// Length of this message in bytes. - pub len: usize, -} - -impl Message { - pub const fn new(cond: C, len: usize) -> Self { - Self { cond, len } - } -} - -/// A condition that must be met in order for a client to receive packet data. -#[derive(PartialEq, Copy, Clone, Default, Debug)] -pub enum MessageCondition { - /// Data will be received unconditionally. - #[default] - All, - /// All clients excluding this specific client. - Except { - client: Entity, - }, - /// In view of this chunk position. - View { - pos: ChunkPos, - }, - ViewExcept { - pos: ChunkPos, - except: Entity, - }, - /// In view of `viewed` and not in view of `unviewed`. - TransitionView { - viewed: ChunkPos, - unviewed: ChunkPos, - }, - /* - /// Client's position must be contained in this sphere. - Sphere { - center: DVec3, - radius: f64, - }, - */ -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn message_buffer_append() { - let cond1 = MessageCondition::All; - let cond2 = MessageCondition::Except { - client: Entity::PLACEHOLDER, - }; - let cond3 = MessageCondition::View { - pos: ChunkPos::new(5, 6), - }; - - let mut buf = MessageBuf::new(None); - - let bytes = &[1, 2, 3, 4, 5]; - - buf.send_packet_bytes(cond1, bytes); - buf.send_packet_bytes(cond2, bytes); - buf.send_packet_bytes(cond3, bytes); - - buf.send_packet_bytes(cond2, bytes); - buf.send_packet_bytes(cond3, bytes); - buf.send_packet_bytes(cond1, bytes); - - let msgs = buf.messages(); - - assert_eq!(msgs[0], Message::new(cond2, bytes.len() * 2)); - assert_eq!(msgs[1], Message::new(cond3, bytes.len() * 2)); - assert_eq!(msgs[2], Message::new(cond1, bytes.len() * 2)); - } -} diff --git a/crates/valence_instance/Cargo.toml b/crates/valence_layer/Cargo.toml similarity index 95% rename from crates/valence_instance/Cargo.toml rename to crates/valence_layer/Cargo.toml index f540eb7c3..f7fae352a 100644 --- a/crates/valence_instance/Cargo.toml +++ b/crates/valence_layer/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "valence_instance" +name = "valence_layer" version.workspace = true edition.workspace = true diff --git a/crates/valence_layer/README.md b/crates/valence_layer/README.md new file mode 100644 index 000000000..f87f5c14c --- /dev/null +++ b/crates/valence_layer/README.md @@ -0,0 +1 @@ +# TODO \ No newline at end of file diff --git a/crates/valence_instance/src/chunk_bvh.rs b/crates/valence_layer/src/bvh.rs similarity index 87% rename from crates/valence_instance/src/chunk_bvh.rs rename to crates/valence_layer/src/bvh.rs index 87c44adf0..0b38086c2 100644 --- a/crates/valence_instance/src/chunk_bvh.rs +++ b/crates/valence_layer/src/bvh.rs @@ -4,11 +4,17 @@ use std::ops::Range; use valence_core::chunk_pos::{ChunkPos, ChunkView}; #[derive(Clone, Debug)] -pub struct ChunkBvh { +pub struct ChunkBvh { nodes: Vec, values: Vec, } +impl Default for ChunkBvh { + fn default() -> Self { + Self::new() + } +} + #[derive(Clone, Debug)] enum Node { Internal { @@ -85,17 +91,18 @@ impl GetChunkPos for ChunkPos { } } -/// When to stop subdividing nodes. -const MAX_SURFACE_AREA: i32 = 8 * 4; - -impl ChunkBvh { +impl ChunkBvh { pub fn new() -> Self { + assert!(MAX_SURFACE_AREA > 0); + Self { nodes: vec![], values: vec![], } } +} +impl ChunkBvh { pub fn build(&mut self, items: impl IntoIterator) { self.nodes.clear(); self.values.clear(); @@ -157,14 +164,14 @@ impl ChunkBvh { }); } - pub fn traverse(&self, view: ChunkView, mut f: impl FnMut(&T)) { + pub fn query(&self, view: ChunkView, mut f: impl FnMut(&T)) { if let Some(root) = self.nodes.last() { let (min, max) = view.bounding_box(); - self.traverse_rec(root, view, Aabb { min, max }, &mut f); + self.query_rec(root, view, Aabb { min, max }, &mut f); } } - fn traverse_rec(&self, node: &Node, view: ChunkView, view_aabb: Aabb, f: &mut impl FnMut(&T)) { + fn query_rec(&self, node: &Node, view: ChunkView, view_aabb: Aabb, f: &mut impl FnMut(&T)) { match node { Node::Internal { bounds, @@ -172,8 +179,8 @@ impl ChunkBvh { right, } => { if bounds.intersects(view_aabb) { - self.traverse_rec(&self.nodes[*left as usize], view, view_aabb, f); - self.traverse_rec(&self.nodes[*right as usize], view, view_aabb, f); + self.query_rec(&self.nodes[*left as usize], view, view_aabb, f); + self.query_rec(&self.nodes[*right as usize], view, view_aabb, f); } } Node::Leaf { bounds, values } => { @@ -188,6 +195,11 @@ impl ChunkBvh { } } + pub fn shrink_to_fit(&mut self) { + self.nodes.shrink_to_fit(); + self.values.shrink_to_fit(); + } + #[cfg(test)] fn check_invariants(&self) { if let Some(root) = self.nodes.last() { @@ -211,7 +223,10 @@ impl ChunkBvh { self.check_invariants_rec(left); self.check_invariants_rec(right); } - Node::Leaf { bounds: leaf_bounds, values } => { + Node::Leaf { + bounds: leaf_bounds, + values, + } => { let bounds = value_bounds(&self.values[values.start as usize..values.end as usize]) .expect("leaf should be nonempty"); @@ -276,7 +291,7 @@ mod tests { } #[test] - fn traversal_visits_correct_nodes() { + fn query_visits_correct_nodes() { let mut bvh = ChunkBvh::::new(); let mut positions = vec![]; @@ -310,7 +325,7 @@ mod tests { // Check that we traverse exactly the positions that we know the view can see. - bvh.traverse(view, |pos| { + bvh.query(view, |pos| { let idx = viewed_positions.iter().position(|p| p == pos).expect("😔"); viewed_positions.remove(idx); }); diff --git a/crates/valence_instance/src/instance.rs b/crates/valence_layer/src/chunk.rs similarity index 75% rename from crates/valence_instance/src/instance.rs rename to crates/valence_layer/src/chunk.rs index 03e30987d..400baad4f 100644 --- a/crates/valence_instance/src/instance.rs +++ b/crates/valence_layer/src/chunk.rs @@ -1,7 +1,13 @@ //! Contains the [`Instance`] component and methods. +mod chunk; +pub mod loaded; +mod paletted_container; +pub mod unloaded; + use std::collections::hash_map::{Entry, OccupiedEntry, VacantEntry}; +use bevy_app::prelude::*; use bevy_ecs::prelude::*; use num_integer::div_ceil; use rustc_hash::FxHashMap; @@ -12,50 +18,86 @@ use valence_core::ident::Ident; use valence_core::protocol::array::LengthPrefixedArray; use valence_core::Server; use valence_dimension::DimensionTypeRegistry; -use valence_entity::EntityId; use valence_nbt::Compound; -use crate::chunk::{Block, BlockRef, Chunk, IntoBlock, LoadedChunk, UnloadedChunk, MAX_HEIGHT}; -use crate::message::{MessageBuf, MessageCondition}; - -/// An Instance represents a Minecraft world, which consist of [`Chunk`]s. -/// It manages updating clients when chunks change, and caches chunk and entity -/// update packets on a per-chunk basis. -#[derive(Component)] -pub struct Instance { - pub(super) chunks: FxHashMap, - pub(super) info: InstanceInfo, - pub(super) message_buf: MessageBuf, - pub(super) entity_removals: Vec, - pub(super) biome_changes: Vec, +use crate::bvh::GetChunkPos; +use crate::chunk::chunk::MAX_HEIGHT; +use crate::message::Messages; +use crate::{Layer, UpdateLayersPreClient}; + +pub use self::chunk::*; +pub use self::loaded::LoadedChunk; +pub use self::unloaded::UnloadedChunk; + +#[derive(Component, Debug)] +pub struct ChunkLayer { + messages: ChunkLayerMessages, + chunks: FxHashMap, + info: ChunkLayerInfo, } #[doc(hidden)] -pub struct InstanceInfo { - pub(super) dimension_type_name: Ident, - pub(super) height: u32, - pub(super) min_y: i32, - pub(super) biome_registry_len: usize, - pub(super) compression_threshold: Option, +#[derive(Debug)] +pub struct ChunkLayerInfo { + dimension_type_name: Ident, + height: u32, + min_y: i32, + biome_registry_len: usize, + compression_threshold: Option, // We don't have a proper lighting engine yet, so we just fill chunks with full brightness. - pub(super) sky_light_mask: Box<[u64]>, - pub(super) sky_light_arrays: Box<[LengthPrefixedArray]>, + sky_light_mask: Box<[u64]>, + sky_light_arrays: Box<[LengthPrefixedArray]>, } -#[doc(hidden)] -pub struct EntityRemoval { - pub pos: ChunkPos, - pub id: EntityId, +type ChunkLayerMessages = Messages; + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub enum GlobalMsg { + /// Send packet data to all clients viewing the layer. + Packet, + /// Send packet data to all clients viewing layer, except the client identified by `except`. + PacketExcept, } -#[doc(hidden)] -pub struct BiomeChange { - pub pos: ChunkPos, - /// Data for the ChunkBiomeDataS2c packet. - pub data: Vec, +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub enum LocalMsg { + /// Send packet data to all clients viewing the layer in view of `pos`. + PacketAt { + pos: ChunkPos, + }, + ChangeBiome { + pos: ChunkPos, + }, +} + +impl GetChunkPos for LocalMsg { + fn chunk_pos(&self) -> ChunkPos { + match *self { + LocalMsg::PacketAt { pos } => pos, + LocalMsg::ChangeBiome { pos } => pos, + } + } } -impl Instance { +impl Layer for ChunkLayer { + type Global = GlobalMsg; + + type Local = LocalMsg; + + fn send_global(&mut self, msg: Self::Global, f: impl FnOnce(&mut Vec)) { + self.messages.send_global(msg, f) + } + + fn send_local(&mut self, msg: Self::Local, f: impl FnOnce(&mut Vec)) { + self.messages.send_local(msg, f) + } + + fn compression_threshold(&self) -> Option { + self.info.compression_threshold + } +} + +impl ChunkLayer { #[track_caller] pub fn new( dimension_type_name: impl Into>, @@ -82,8 +124,9 @@ impl Instance { } Self { + messages: Messages::new(), chunks: Default::default(), - info: InstanceInfo { + info: ChunkLayerInfo { dimension_type_name, height: dim.height as u32, min_y: dim.min_y, @@ -93,9 +136,6 @@ impl Instance { sky_light_arrays: vec![LengthPrefixedArray([0xff; 2048]); light_section_count] .into(), }, - message_buf: MessageBuf::new(server.compression_threshold()), - entity_removals: vec![], - biome_changes: vec![], } } @@ -159,7 +199,11 @@ impl Instance { where F: FnMut(ChunkPos, &mut LoadedChunk) -> bool, { - self.chunks.retain(|pos, chunk| f(*pos, chunk)); + for (pos, chunk) in &mut self.chunks { + if !f(*pos, chunk) { + chunk.remove(); + } + } } /// Get a [`ChunkEntry`] for the given position. @@ -188,11 +232,11 @@ impl Instance { /// Optimizes the memory usage of the instance. pub fn optimize(&mut self) { for (_, chunk) in self.chunks_mut() { - chunk.optimize(); + chunk.shrink_to_fit(); } self.chunks.shrink_to_fit(); - self.message_buf.shrink_to_fit(); + self.messages.shrink_to_fit(); } pub fn block(&self, pos: impl Into) -> Option { @@ -254,13 +298,13 @@ impl Instance { } #[doc(hidden)] - pub fn info(&self) -> &InstanceInfo { + pub fn info(&self) -> &ChunkLayerInfo { &self.info } #[doc(hidden)] - pub fn message_buf(&self) -> &MessageBuf { - &self.message_buf + pub fn messages(&self) -> &ChunkLayerMessages { + &self.messages } /* @@ -395,3 +439,35 @@ impl<'a> VacantChunkEntry<'a> { self.entry.key() } } + +pub(super) fn build(app: &mut App) { + app.add_systems( + PostUpdate, + ( + update_chunks_pre_client.in_set(UpdateLayersPreClient), + update_chunks_post_client.in_set(UpdateLayersPreClient), + ), + ); +} + +fn update_chunks_pre_client(mut layers: Query<&mut ChunkLayer>) { + for layer in &mut layers { + let layer = layer.into_inner(); + + for (&pos, chunk) in &mut layer.chunks { + chunk.update_pre_client(pos, &layer.info, &mut layer.messages); + } + + layer.messages.ready(); + } +} + +fn update_chunks_post_client(mut layers: Query<&mut ChunkLayer>) { + for mut layer in &mut layers { + layer + .chunks + .retain(|&pos, chunk| chunk.update_post_client(pos)); + + layer.messages.unready(); + } +} diff --git a/crates/valence_instance/src/chunk.rs b/crates/valence_layer/src/chunk/chunk.rs similarity index 94% rename from crates/valence_instance/src/chunk.rs rename to crates/valence_layer/src/chunk/chunk.rs index c8f39ab51..8eb71daf6 100644 --- a/crates/valence_instance/src/chunk.rs +++ b/crates/valence_layer/src/chunk/chunk.rs @@ -1,14 +1,8 @@ -pub mod loaded; -mod paletted_container; -pub mod unloaded; - -pub use loaded::LoadedChunk; -pub use unloaded::UnloadedChunk; use valence_biome::BiomeId; use valence_block::BlockState; use valence_nbt::Compound; -use self::paletted_container::PalettedContainer; +use super::paletted_container::PalettedContainer; /// Common operations on chunks. Notable implementors are [`LoadedChunk`] and /// [`UnloadedChunk`]. @@ -213,7 +207,7 @@ pub trait Chunk { /// /// This method must not alter the semantics of the chunk in any observable /// way. - fn optimize(&mut self); + fn shrink_to_fit(&mut self); } /// Represents a complete block, which is a pair of block state and optional NBT @@ -274,20 +268,21 @@ impl IntoBlock for BlockState { } } -const SECTION_BLOCK_COUNT: usize = 16 * 16 * 16; -const SECTION_BIOME_COUNT: usize = 4 * 4 * 4; +pub(super) const SECTION_BLOCK_COUNT: usize = 16 * 16 * 16; +pub(super) const SECTION_BIOME_COUNT: usize = 4 * 4 * 4; /// The maximum height of a chunk. pub const MAX_HEIGHT: u32 = 4096; -type BlockStateContainer = +pub(super) type BlockStateContainer = PalettedContainer; -type BiomeContainer = PalettedContainer; +pub(super) type BiomeContainer = + PalettedContainer; #[inline] #[track_caller] -fn check_block_oob(chunk: &impl Chunk, x: u32, y: u32, z: u32) { +pub(super) fn check_block_oob(chunk: &impl Chunk, x: u32, y: u32, z: u32) { assert!( x < 16 && y < chunk.height() && z < 16, "chunk block offsets of ({x}, {y}, {z}) are out of bounds" @@ -296,7 +291,7 @@ fn check_block_oob(chunk: &impl Chunk, x: u32, y: u32, z: u32) { #[inline] #[track_caller] -fn check_biome_oob(chunk: &impl Chunk, x: u32, y: u32, z: u32) { +pub(super) fn check_biome_oob(chunk: &impl Chunk, x: u32, y: u32, z: u32) { assert!( x < 4 && y < chunk.height() / 4 && z < 4, "chunk biome offsets of ({x}, {y}, {z}) are out of bounds" @@ -305,7 +300,7 @@ fn check_biome_oob(chunk: &impl Chunk, x: u32, y: u32, z: u32) { #[inline] #[track_caller] -fn check_section_oob(chunk: &impl Chunk, sect_y: u32) { +pub(super) fn check_section_oob(chunk: &impl Chunk, sect_y: u32) { assert!( sect_y < chunk.height() / 16, "chunk section offset of {sect_y} is out of bounds" @@ -313,12 +308,14 @@ fn check_section_oob(chunk: &impl Chunk, sect_y: u32) { } /// Returns the minimum number of bits needed to represent the integer `n`. -const fn bit_width(n: usize) -> usize { +pub(super) const fn bit_width(n: usize) -> usize { (usize::BITS - n.leading_zeros()) as _ } #[cfg(test)] mod tests { + use crate::chunk::{loaded::LoadedChunk, unloaded::UnloadedChunk}; + use super::*; #[test] diff --git a/crates/valence_instance/src/chunk/loaded.rs b/crates/valence_layer/src/chunk/loaded.rs similarity index 84% rename from crates/valence_instance/src/chunk/loaded.rs rename to crates/valence_layer/src/chunk/loaded.rs index 018991d83..4abf09859 100644 --- a/crates/valence_instance/src/chunk/loaded.rs +++ b/crates/valence_layer/src/chunk/loaded.rs @@ -1,45 +1,37 @@ use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet}; use std::mem; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicU32, Ordering}; -use bevy_ecs::entity::Entity; -use bevy_ecs::prelude::*; use parking_lot::Mutex; // Using nonstandard mutex to avoid poisoning API. use valence_biome::BiomeId; use valence_block::BlockState; use valence_core::block_pos::BlockPos; use valence_core::chunk_pos::ChunkPos; -use valence_core::despawn::Despawned; use valence_core::protocol::encode::{PacketWriter, WritePacket}; use valence_core::protocol::var_int::VarInt; use valence_core::protocol::var_long::VarLong; use valence_core::protocol::Encode; -use valence_entity::EntityKind; use valence_nbt::{compound, Compound}; use valence_registry::RegistryIdx; -use super::paletted_container::PalettedContainer; -use super::{ - bit_width, check_biome_oob, check_block_oob, check_section_oob, unloaded, BiomeContainer, - BlockStateContainer, Chunk, UnloadedChunk, SECTION_BLOCK_COUNT, +use super::chunk::{ + bit_width, check_biome_oob, check_block_oob, check_section_oob, BiomeContainer, + BlockStateContainer, Chunk, SECTION_BLOCK_COUNT, }; -use crate::message::{MessageBuf, MessageCondition}; +use super::paletted_container::PalettedContainer; +use super::unloaded::{self, UnloadedChunk}; +use super::{ChunkLayerInfo, ChunkLayerMessages, LocalMsg}; use crate::packet::{ BlockEntityUpdateS2c, BlockUpdateS2c, ChunkDataBlockEntity, ChunkDataS2c, ChunkDeltaUpdateS2c, + UnloadChunkS2c, }; -use crate::{BiomeChange, InstanceInfo, UpdateEntityQuery}; #[derive(Debug)] pub struct LoadedChunk { state: ChunkState, - /// If this chunk is potentially in view of any clients this tick. Useful - /// for knowing if it's necessary to record changes, since no client - /// would be in view to receive the changes if this were false. - /// - /// Invariant: `is_viewed` is always `false` when this chunk's state not - /// [`ChunkState::Normal`]. - is_viewed: AtomicBool, + /// A count of the clients viewing this chunk. Useful for knowing if it's necessary to record changes, since no client would be in view to receive the changes if this were nonzero. + viewer_count: AtomicU32, /// Block and biome data for the chunk. sections: Box<[Section]>, /// The block entities in this chunk. @@ -52,8 +44,6 @@ pub struct LoadedChunk { /// invalidated if empty. This should be cleared whenever the chunk is /// modified in an observable way, even if the chunk is not viewed. cached_init_packets: Mutex>, - /// The unique set of Minecraft entities with positions in this chunk. - pub(crate) entities: BTreeSet, } /// Describes the current state of a loaded chunk. @@ -116,13 +106,12 @@ impl LoadedChunk { pub(crate) fn new(height: u32) -> Self { Self { state: ChunkState::Added, - is_viewed: AtomicBool::new(false), + viewer_count: AtomicU32::new(0), sections: vec![Section::default(); height as usize / 16].into(), block_entities: BTreeMap::new(), changed_block_entities: BTreeSet::new(), changed_biomes: false, cached_init_packets: Mutex::new(vec![]), - entities: BTreeSet::new(), } } @@ -144,7 +133,6 @@ impl LoadedChunk { ChunkState::Normal => ChunkState::Overwrite, }; - *self.is_viewed.get_mut() = false; let old_sections = self .sections .iter_mut() @@ -180,7 +168,6 @@ impl LoadedChunk { ChunkState::Normal => ChunkState::Removed, }; - *self.is_viewed.get_mut() = false; let old_sections = self .sections .iter_mut() @@ -210,63 +197,73 @@ impl LoadedChunk { self.state } - /// All the entities positioned in this chunk. - pub fn entities(&self) -> impl Iterator + '_ { - self.entities.iter().copied() + /// Returns the number of clients in view of this chunk. + pub fn viewer_count(&self) -> u32 { + self.viewer_count.load(Ordering::Relaxed) } - /// If this chunk is potentially in view of any clients. - pub fn is_viewed(&self) -> bool { - self.is_viewed.load(Ordering::Relaxed) + /// Like [`Self::viewer_count`], but avoids an atomic operation. + pub fn viewer_count_mut(&mut self) -> u32 { + *self.viewer_count.get_mut() } - /// Same as [`Self::is_viewed`], but avoids atomic operations. Useful if - /// mutable access to the chunk was needed anyway. + /// For internal use only. #[doc(hidden)] - pub fn is_viewed_mut(&mut self) -> bool { - *self.is_viewed.get_mut() + pub fn inc_viewer_count(&self) { + self.viewer_count.fetch_add(1, Ordering::Relaxed); } - /// Marks this chunk as being viewed. Not intended for use outside of - /// `valence_client`. Calling this function at the wrong time might break - /// things. + /// For internal use only. #[doc(hidden)] - pub fn set_viewed(&self) { - self.is_viewed.store(true, Ordering::Relaxed); + pub fn dec_viewer_count(&self) { + let old = self.viewer_count.fetch_sub(1, Ordering::Relaxed); + debug_assert_ne!(old, 0, "viewer count underflow!"); } /// Performs the changes necessary to prepare this chunk for client updates. /// Notably: - /// - Chunk and entity update packets are written to this chunk's packet + /// - Message is sent to spawn or despawn the chunk. + /// - Chunk update packets are written to this chunk's packet /// buffer. /// - Recorded changes are cleared. - /// - Marks this chunk as unviewed so that clients can mark it as viewed - /// during client updates. pub(crate) fn update_pre_client( &mut self, pos: ChunkPos, - info: &InstanceInfo, - message_buf: &mut MessageBuf, - biome_changes: &mut Vec, - entity_query: &mut Query, Without)>, + info: &ChunkLayerInfo, + messages: &mut ChunkLayerMessages, ) { - if !*self.is_viewed.get_mut() { - // Nobody is viewing the chunk, so no need to send any messages. There + match self.state { + ChunkState::Added | ChunkState::Overwrite => { + // Load the chunk for any viewers. + messages.send_local(LocalMsg::PacketAt { pos }, |buf| { + self.write_init_packets( + PacketWriter::new(buf, info.compression_threshold), + pos, + info, + ) + }); + } + ChunkState::Removed | ChunkState::AddedRemoved => { + // Unload the chunk. + messages.send_local(LocalMsg::PacketAt { pos }, |buf| { + PacketWriter::new(buf, info.compression_threshold) + .write_packet(&UnloadChunkS2c { pos }) + }); + } + ChunkState::Normal => {} + } + + if !is_viewed(&mut self.viewer_count, self.state) { + // Nobody is viewing the chunk, so no need to send any update packets. There // also shouldn't be any changes that need to be cleared. self.assert_no_changes(); return; } - *self.is_viewed.get_mut() = false; + messages.send_local(LocalMsg::PacketAt { pos }, |buf| { + let mut writer = PacketWriter::new(buf, info.compression_threshold); - debug_assert_eq!( - self.state, - ChunkState::Normal, - "other chunk states should be unviewed" - ); - - message_buf.send(MessageCondition::View { pos }, |mut writer| { // Block states for (sect_y, sect) in self.sections.iter_mut().enumerate() { match sect.section_updates.len() { @@ -332,17 +329,17 @@ impl LoadedChunk { } self.changed_block_entities.clear(); + }); + messages.send_local(LocalMsg::ChangeBiome { pos }, |buf| { // Biomes if self.changed_biomes { self.changed_biomes = false; - let mut data: Vec = vec![]; - for sect in self.sections.iter() { sect.biomes .encode_mc_format( - &mut data, + &mut *buf, |b| b.to_index() as _, 0, 3, @@ -350,17 +347,6 @@ impl LoadedChunk { ) .expect("paletted container encode should always succeed"); } - - biome_changes.push(BiomeChange { pos, data }); - } - - // Entities - for &entity in &self.entities { - let entity = entity_query - .get_mut(entity) - .expect("entity in chunk's list of entities should exist"); - - entity.write_update_packets(&mut writer); } }); @@ -368,17 +354,18 @@ impl LoadedChunk { self.assert_no_changes(); } - pub(crate) fn update_post_client(&mut self) { - self.state = match self.state { - ChunkState::Added => ChunkState::Normal, - ChunkState::AddedRemoved => unreachable!(), - ChunkState::Removed => unreachable!(), - ChunkState::Overwrite => ChunkState::Normal, - ChunkState::Normal => ChunkState::Normal, - }; - + /// Returns if the chunk should be retained. + pub(crate) fn update_post_client(&mut self, pos: ChunkPos) -> bool { // Changes were already cleared in `update_pre_client`. self.assert_no_changes(); + + match self.state { + ChunkState::Added | ChunkState::Overwrite | ChunkState::Normal => { + self.state = ChunkState::Normal; + true + } + ChunkState::Removed | ChunkState::AddedRemoved => false, + } } /// Writes the packet data needed to initialize this chunk. @@ -387,7 +374,7 @@ impl LoadedChunk { &self, mut writer: impl WritePacket, pos: ChunkPos, - info: &InstanceInfo, + info: &ChunkLayerInfo, ) { debug_assert!( self.state != ChunkState::Removed && self.state != ChunkState::AddedRemoved, @@ -511,7 +498,7 @@ impl Chunk for LoadedChunk { if block != old_block { self.cached_init_packets.get_mut().clear(); - if *self.is_viewed.get_mut() { + if is_viewed(&mut self.viewer_count, self.state) { let compact = (block.to_raw() as i64) << 12 | (x << 8 | z << 4 | (y % 16)) as i64; sect.section_updates.push(VarLong(compact)); } @@ -529,7 +516,7 @@ impl Chunk for LoadedChunk { if *b != block { self.cached_init_packets.get_mut().clear(); - if *self.is_viewed.get_mut() { + if is_viewed(&mut self.viewer_count, self.state) { // The whole section is being modified, so any previous modifications would // be overwritten. sect.section_updates.clear(); @@ -553,7 +540,7 @@ impl Chunk for LoadedChunk { if block != sect.block_states.get(idx as usize) { self.cached_init_packets.get_mut().clear(); - if *self.is_viewed.get_mut() { + if is_viewed(&mut self.viewer_count, self.state) { let packed = block_bits | (x << 8 | z << 4 | sect_y) as i64; sect.section_updates.push(VarLong(packed)); } @@ -578,7 +565,7 @@ impl Chunk for LoadedChunk { let idx = x + z * 16 + y * 16 * 16; if let Some(be) = self.block_entities.get_mut(&idx) { - if *self.is_viewed.get_mut() { + if is_viewed(&mut self.viewer_count, self.state) { self.changed_block_entities.insert(idx); } self.cached_init_packets.get_mut().clear(); @@ -602,7 +589,7 @@ impl Chunk for LoadedChunk { match block_entity { Some(nbt) => { - if *self.is_viewed.get_mut() { + if is_viewed(&mut self.viewer_count, self.state) { self.changed_block_entities.insert(idx); } self.cached_init_packets.get_mut().clear(); @@ -628,7 +615,7 @@ impl Chunk for LoadedChunk { self.cached_init_packets.get_mut().clear(); - if *self.is_viewed.get_mut() { + if is_viewed(&mut self.viewer_count, self.state) { self.changed_block_entities .extend(mem::take(&mut self.block_entities).into_keys()); } else { @@ -654,7 +641,7 @@ impl Chunk for LoadedChunk { if biome != old_biome { self.cached_init_packets.get_mut().clear(); - if *self.is_viewed.get_mut() { + if is_viewed(&mut self.viewer_count, self.state) { self.changed_biomes = true; } } @@ -670,27 +657,33 @@ impl Chunk for LoadedChunk { if let PalettedContainer::Single(b) = §.biomes { if *b != biome { self.cached_init_packets.get_mut().clear(); - self.changed_biomes = *self.is_viewed.get_mut(); + self.changed_biomes = is_viewed(&mut self.viewer_count, self.state); } } else { self.cached_init_packets.get_mut().clear(); - self.changed_biomes = *self.is_viewed.get_mut(); + self.changed_biomes = is_viewed(&mut self.viewer_count, self.state); } sect.biomes.fill(biome); } - fn optimize(&mut self) { + fn shrink_to_fit(&mut self) { self.cached_init_packets.get_mut().shrink_to_fit(); for sect in self.sections.iter_mut() { - sect.block_states.optimize(); - sect.biomes.optimize(); + sect.block_states.shrink_to_fit(); + sect.biomes.shrink_to_fit(); sect.section_updates.shrink_to_fit(); } } } +/// If there are potentially clients viewing this chunk. +#[inline] +fn is_viewed(viewer_count: &mut AtomicU32, state: ChunkState) -> bool { + state == ChunkState::Normal && *viewer_count.get_mut() > 0 +} + #[cfg(test)] mod tests { use valence_core::ident; @@ -720,7 +713,7 @@ mod tests { fn loaded_chunk_changes_clear_packet_cache() { #[track_caller] fn check(chunk: &mut LoadedChunk, change: impl FnOnce(&mut LoadedChunk) -> T) { - let info = InstanceInfo { + let info = ChunkLayerInfo { dimension_type_name: ident!("whatever").into(), height: 512, min_y: -16, diff --git a/crates/valence_instance/src/chunk/paletted_container.rs b/crates/valence_layer/src/chunk/paletted_container.rs similarity index 98% rename from crates/valence_instance/src/chunk/paletted_container.rs rename to crates/valence_layer/src/chunk/paletted_container.rs index 65f624832..ced9967c9 100644 --- a/crates/valence_instance/src/chunk/paletted_container.rs +++ b/crates/valence_layer/src/chunk/paletted_container.rs @@ -6,7 +6,7 @@ use num_integer::div_ceil; use valence_core::protocol::var_int::VarInt; use valence_core::protocol::Encode; -use crate::chunk::bit_width; +use super::chunk::bit_width; /// `HALF_LEN` must be equal to `ceil(LEN / 2)`. #[derive(Clone, Debug)] @@ -89,7 +89,7 @@ impl } } - pub(super) fn optimize(&mut self) { + pub(super) fn shrink_to_fit(&mut self) { match self { Self::Single(_) => {} Self::Indirect(ind) => { @@ -332,7 +332,7 @@ mod tests { assert_eq!(val, p.get(idx)); a[idx] = val; - p.optimize(); + p.shrink_to_fit(); assert!(check(&p, &a)); } diff --git a/crates/valence_instance/src/chunk/unloaded.rs b/crates/valence_layer/src/chunk/unloaded.rs similarity index 97% rename from crates/valence_instance/src/chunk/unloaded.rs rename to crates/valence_layer/src/chunk/unloaded.rs index 1f91a6803..9bc2ab4bc 100644 --- a/crates/valence_instance/src/chunk/unloaded.rs +++ b/crates/valence_layer/src/chunk/unloaded.rs @@ -5,7 +5,7 @@ use valence_biome::BiomeId; use valence_block::BlockState; use valence_nbt::Compound; -use super::{ +use super::chunk::{ check_biome_oob, check_block_oob, check_section_oob, BiomeContainer, BlockStateContainer, Chunk, MAX_HEIGHT, SECTION_BLOCK_COUNT, }; @@ -149,10 +149,10 @@ impl Chunk for UnloadedChunk { self.sections[sect_y as usize].biomes.fill(biome); } - fn optimize(&mut self) { + fn shrink_to_fit(&mut self) { for sect in &mut self.sections { - sect.block_states.optimize(); - sect.biomes.optimize(); + sect.block_states.shrink_to_fit(); + sect.biomes.shrink_to_fit(); } } } diff --git a/crates/valence_layer/src/entity.rs b/crates/valence_layer/src/entity.rs new file mode 100644 index 000000000..dc7effdd0 --- /dev/null +++ b/crates/valence_layer/src/entity.rs @@ -0,0 +1,278 @@ +use std::collections::hash_map::Entry; +use std::collections::BTreeSet; + +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; +use bevy_ecs::query::Has; +use rustc_hash::FxHashMap; +use valence_core::chunk_pos::ChunkPos; +use valence_core::despawn::Despawned; +use valence_core::protocol::encode::PacketWriter; +use valence_core::Server; +use valence_entity::query::UpdateEntityQuery; +use valence_entity::{EntityId, EntityLayerId, OldEntityLayerId, OldPosition, Position}; + +use crate::bvh::GetChunkPos; +use crate::message::Messages; +use crate::{Layer, UpdateLayersPostClient, UpdateLayersPreClient}; + +#[derive(Component, Debug)] +pub struct EntityLayer { + messages: EntityLayerMessages, + entities: FxHashMap>, + compression_threshold: Option, +} + +#[doc(hidden)] +pub type EntityLayerMessages = Messages; + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub enum GlobalMsg { + /// Send packet data to all clients viewing the layer. + Packet, + /// Send packet data to all clients viewing layer, except the client identified by `except`. + PacketExcept, +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub enum LocalMsg { + /// Send packet data to all clients viewing the layer in view of `pos`. + PacketAt { pos: ChunkPos }, + /// Send packet data to all clients viewing the layer in view of `pos`, except the client identified by `except`. + PacketAtExcept { pos: ChunkPos, except: Entity }, + /// Spawn entities if the client is not already viewing `src_layer`. Message data is the serialized form of [`Entity`]. + SpawnEntity { pos: ChunkPos, src_layer: Entity }, + /// Spawn entities if the client is not in view of `src_pos`. Message data is the serialized form of [`Entity`]. + SpawnEntityTransition { pos: ChunkPos, src_pos: ChunkPos }, + /// Despawn entities if the client is not already viewing `dest_layer`. Message data is the serialized form of `EntityId`. + DespawnEntity { pos: ChunkPos, dest_layer: Entity }, + /// Despawn entities if the client is not in view of `dest_pos`. Message data is the serialized form of `EntityId`. + DespawnEntityTransition { pos: ChunkPos, dest_pos: ChunkPos }, +} + +impl GetChunkPos for LocalMsg { + fn chunk_pos(&self) -> ChunkPos { + match *self { + LocalMsg::PacketAt { pos } => pos, + LocalMsg::PacketAtExcept { pos, .. } => pos, + LocalMsg::SpawnEntity { pos, .. } => pos, + LocalMsg::SpawnEntityTransition { pos, .. } => pos, + LocalMsg::DespawnEntity { pos, .. } => pos, + LocalMsg::DespawnEntityTransition { pos, .. } => pos, + } + } +} + +impl EntityLayer { + pub fn new(server: &Server) -> Self { + Self { + messages: Messages::new(), + entities: Default::default(), + compression_threshold: server.compression_threshold(), + } + } + + /// Returns a list of entities with positions within the provided chunk + /// position on this layer. + pub fn entities_at( + &self, + pos: impl Into, + ) -> impl Iterator + Clone + '_ { + self.entities + .get(&pos.into()) + .into_iter() + .flat_map(|entities| entities.iter().copied()) + } +} + +impl Layer for EntityLayer { + type Global = GlobalMsg; + + type Local = LocalMsg; + + fn send_global(&mut self, msg: Self::Global, f: impl FnOnce(&mut Vec)) { + self.messages.send_global(msg, f); + } + + fn send_local(&mut self, msg: Self::Local, f: impl FnOnce(&mut Vec)) { + self.messages.send_local(msg, f); + } + + fn compression_threshold(&self) -> Option { + self.compression_threshold + } +} + +pub(super) fn build(app: &mut App) { + app.add_systems( + PostUpdate, + ( + ( + change_entity_positions, + send_entity_updates::, + ready_entity_layers, + ) + .chain() + .in_set(UpdateLayersPreClient), + unready_entity_layers.in_set(UpdateLayersPostClient), + ), + ); +} + +pub fn change_entity_positions( + entities: Query< + ( + Entity, + &EntityId, + &Position, + &OldPosition, + &EntityLayerId, + &OldEntityLayerId, + Has, + ), + Or<(Changed, Changed, With)>, + >, + mut layers: Query<&mut EntityLayer>, +) { + for (entity, entity_id, pos, old_pos, layer_id, old_layer_id, despawned) in &entities { + let chunk_pos = pos.chunk_pos(); + let old_chunk_pos = old_pos.chunk_pos(); + + if despawned { + // Entity was deleted. Remove it from the layer. + + if let Ok(old_layer) = layers.get_mut(layer_id.0) { + let old_layer = old_layer.into_inner(); + + if let Entry::Occupied(mut old_cell) = old_layer.entities.entry(old_chunk_pos) { + if old_cell.get_mut().remove(&entity) { + old_layer.messages.send_local( + LocalMsg::DespawnEntity { + pos: old_chunk_pos, + dest_layer: Entity::PLACEHOLDER, + }, + |b| b.extend_from_slice(&entity_id.get().to_ne_bytes()), + ); + + if old_cell.get().is_empty() { + old_cell.remove(); + } + } + } + } + } else if old_layer_id != layer_id { + // Entity changed their layer. Remove it from old layer and insert it in the new + // layer. + + if let Ok(old_layer) = layers.get_mut(old_layer_id.get()) { + let old_layer = old_layer.into_inner(); + + if let Entry::Occupied(mut old_cell) = old_layer.entities.entry(old_chunk_pos) { + if old_cell.get_mut().remove(&entity) { + old_layer.messages.send_local( + LocalMsg::DespawnEntity { + pos: old_chunk_pos, + dest_layer: layer_id.0, + }, + |b| b.extend_from_slice(&entity_id.get().to_ne_bytes()), + ); + + if old_cell.get().is_empty() { + old_cell.remove(); + } + } + } + } + + if let Ok(mut layer) = layers.get_mut(layer_id.0) { + if layer.entities.entry(chunk_pos).or_default().insert(entity) { + layer.messages.send_local( + LocalMsg::SpawnEntity { + pos: chunk_pos, + src_layer: old_layer_id.get(), + }, + |b| b.extend_from_slice(&entity.to_bits().to_ne_bytes()), + ); + } + } + } else if chunk_pos != old_chunk_pos { + // Entity changed their chunk position without changing layers. Remove it from + // old cell and insert it in the new cell. + + if let Ok(mut layer) = layers.get_mut(layer_id.0) { + if let Entry::Occupied(mut old_cell) = layer.entities.entry(old_chunk_pos) { + if old_cell.get_mut().remove(&entity) { + layer.messages.send_local( + LocalMsg::DespawnEntityTransition { + pos: old_chunk_pos, + dest_pos: chunk_pos, + }, + |b| b.extend_from_slice(&entity_id.get().to_ne_bytes()), + ); + } + } + + if layer.entities.entry(chunk_pos).or_default().insert(entity) { + layer.messages.send_local( + LocalMsg::SpawnEntityTransition { + pos: chunk_pos, + src_pos: old_chunk_pos, + }, + |b| b.extend_from_slice(&entity.to_bits().to_ne_bytes()), + ); + } + } + } + } +} + +pub fn send_entity_updates( + entities: Query<(Entity, UpdateEntityQuery, Has), Without>, + mut layers: Query<&mut EntityLayer>, +) { + for layer in layers.iter_mut() { + let layer = layer.into_inner(); + + for cell in layer.entities.values_mut() { + for &entity in cell.iter() { + if let Ok((entity, update, is_client)) = entities.get(entity) { + let chunk_pos = update.pos.chunk_pos(); + + // Send the update packets to all viewers. If the entity being updated is a + // client, then we need to be careful to exclude the client itself from + // receiving the update packets. + let msg = if is_client { + LocalMsg::PacketAtExcept { + pos: chunk_pos, + except: entity, + } + } else { + LocalMsg::PacketAt { pos: chunk_pos } + }; + + layer.messages.send_local(msg, |b| { + update + .write_update_packets(PacketWriter::new(b, layer.compression_threshold)) + }); + } else { + panic!( + "Entity {entity:?} was not properly removed from entity layer. Did you \ + forget to use the `Despawned` component?" + ); + } + } + } + } +} + +pub fn ready_entity_layers(mut layers: Query<&mut EntityLayer>) { + for mut layer in &mut layers { + layer.messages.ready(); + } +} + +pub fn unready_entity_layers(mut layers: Query<&mut EntityLayer>) { + for mut layer in &mut layers { + layer.messages.unready(); + } +} diff --git a/crates/valence_layer/src/lib.rs b/crates/valence_layer/src/lib.rs new file mode 100644 index 000000000..a52e6ad1d --- /dev/null +++ b/crates/valence_layer/src/lib.rs @@ -0,0 +1,112 @@ +#![doc = include_str!("../README.md")] +#![deny( + rustdoc::broken_intra_doc_links, + rustdoc::private_intra_doc_links, + rustdoc::missing_crate_level_docs, + rustdoc::invalid_codeblock_attributes, + rustdoc::invalid_rust_codeblocks, + rustdoc::bare_urls, + rustdoc::invalid_html_tags +)] +#![warn( + trivial_casts, + trivial_numeric_casts, + unused_lifetimes, + unused_import_braces, + unreachable_pub, + clippy::dbg_macro +)] +#![allow(clippy::type_complexity)] + +pub mod bvh; +pub mod chunk; +pub mod entity; +pub mod message; +pub mod packet; + +use std::marker::PhantomData; + +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; +pub use chunk::ChunkLayer; +pub use entity::EntityLayer; +use valence_core::protocol::encode::{PacketWriter, WritePacket}; +use valence_core::protocol::{Encode, Packet}; +use valence_entity::{InitEntitiesSet, UpdateTrackedDataSet}; + +// Plugin is generic over the client type for hacky reasons. +pub struct LayerPlugin(PhantomData); + +impl LayerPlugin { + pub fn new() -> Self { + Self(PhantomData) + } +} + +impl Default for LayerPlugin { + fn default() -> Self { + Self::new() + } +} + +/// When entity and chunk changes are written to layers. Systems that modify chunks and entities should run _before_ this. Systems that need to read layer messages should run _after_ this. +#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct UpdateLayersPreClient; + +/// When layers are cleared and messages from this tick are lost. Systems that read layer messages should run _before_ this. +#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct UpdateLayersPostClient; + +impl Plugin for LayerPlugin { + fn build(&self, app: &mut App) { + app.configure_sets( + PostUpdate, + ( + UpdateLayersPreClient + .after(InitEntitiesSet) + .after(UpdateTrackedDataSet), + UpdateLayersPostClient.after(UpdateLayersPreClient), + ), + ); + + chunk::build(app); + entity::build::(app); + } +} + +pub trait Layer { + type Global; + type Local; + + fn send_global(&mut self, msg: Self::Global, f: impl FnOnce(&mut Vec)); + + fn send_local(&mut self, msg: Self::Local, f: impl FnOnce(&mut Vec)); + + fn compression_threshold(&self) -> Option; + + fn send_global_bytes(&mut self, msg: Self::Global, bytes: &[u8]) { + self.send_global(msg, |b| b.extend_from_slice(bytes)); + } + + fn send_local_bytes(&mut self, msg: Self::Local, bytes: &[u8]) { + self.send_local(msg, |b| b.extend_from_slice(bytes)); + } + + fn send_global_packet

(&mut self, msg: Self::Global, pkt: &P) + where + P: Encode + Packet, + { + let threshold = self.compression_threshold(); + + self.send_global(msg, |b| PacketWriter::new(b, threshold).write_packet(pkt)); + } + + fn send_local_packet

(&mut self, msg: Self::Local, pkt: &P) + where + P: Encode + Packet, + { + let threshold = self.compression_threshold(); + + self.send_local(msg, |b| PacketWriter::new(b, threshold).write_packet(pkt)); + } +} diff --git a/crates/valence_layer/src/message.rs b/crates/valence_layer/src/message.rs new file mode 100644 index 000000000..6725bb0f2 --- /dev/null +++ b/crates/valence_layer/src/message.rs @@ -0,0 +1,233 @@ +use core::fmt; +use std::ops::Range; + +use valence_core::chunk_pos::ChunkPos; + +use crate::bvh::{ChunkBvh, GetChunkPos}; + +pub struct Messages { + global: Vec<(G, Range)>, + local: Vec<(L, Range)>, + bvh: ChunkBvh>, + staging: Vec, + ready: Vec, + is_ready: bool, +} + +impl Messages +where + G: Copy + Ord, + L: Copy + Ord + GetChunkPos, +{ + pub(crate) fn new() -> Self { + Self::default() + } + + pub(crate) fn send_global(&mut self, msg: G, f: impl FnOnce(&mut Vec)) { + debug_assert!(!self.is_ready); + + let start = self.staging.len(); + f(&mut self.staging); + let end = self.staging.len(); + + if start != end { + self.global.push((msg, start as u32..end as u32)); + } + } + + pub(crate) fn send_local(&mut self, msg: L, f: impl FnOnce(&mut Vec)) { + debug_assert!(!self.is_ready); + + let start = self.staging.len(); + f(&mut self.staging); + let end = self.staging.len(); + + if start != end { + self.local.push((msg, start as u32..end as u32)); + } + } + + /// Readies messages to be read by clients. + pub(crate) fn ready(&mut self) { + debug_assert!(!self.is_ready); + self.is_ready = true; + + debug_assert!(self.ready.is_empty()); + + self.ready.reserve_exact(self.staging.len()); + + fn sort_and_merge( + msgs: &mut Vec<(M, Range)>, + staging: &[u8], + ready: &mut Vec, + ) { + // Sort is deliberately stable. + msgs.sort_by_key(|(msg, _)| *msg); + + // Make sure the first element is already copied to "ready". + if let Some((_, range)) = msgs.first_mut() { + let start = ready.len(); + ready.extend_from_slice(&staging[range.start as usize..range.end as usize]); + let end = ready.len(); + + *range = start as u32..end as u32; + } + + msgs.dedup_by(|(right_msg, right_range), (left_msg, left_range)| { + if *left_msg == *right_msg { + // Extend the left element with the right element. Then delete the right + // element. + + let right_bytes = + &staging[right_range.start as usize..right_range.end as usize]; + + ready.extend_from_slice(right_bytes); + + left_range.end += right_bytes.len() as u32; + + true + } else { + // Copy right element to "ready". + + let right_bytes = + &staging[right_range.start as usize..right_range.end as usize]; + + let start = ready.len(); + ready.extend_from_slice(right_bytes); + let end = ready.len(); + + *right_range = start as u32..end as u32; + + false + } + }); + } + + sort_and_merge(&mut self.global, &self.staging, &mut self.ready); + sort_and_merge(&mut self.local, &self.staging, &mut self.ready); + + self.bvh.build( + self.local + .iter() + .cloned() + .map(|(msg, range)| MessagePair { msg, range }), + ); + } + + pub(crate) fn unready(&mut self) { + assert!(self.is_ready); + self.is_ready = false; + + self.local.clear(); + self.global.clear(); + self.staging.clear(); + self.ready.clear(); + } + + pub(crate) fn shrink_to_fit(&mut self) { + self.global.shrink_to_fit(); + self.local.shrink_to_fit(); + self.bvh.shrink_to_fit(); + self.staging.shrink_to_fit(); + self.ready.shrink_to_fit(); + } + + pub fn bytes(&self) -> &[u8] { + debug_assert!(self.is_ready); + + &self.ready + } + + pub fn iter_global(&self) -> impl Iterator)> + '_ { + debug_assert!(self.is_ready); + + self.global.iter().map(|(m, r)| (*m, r.start as usize..r.end as usize)) + } + + pub fn query_local(&self) { + debug_assert!(self.is_ready); + + todo!() + } +} + +impl Default for Messages { + fn default() -> Self { + Self { + global: Default::default(), + local: Default::default(), + bvh: Default::default(), + staging: Default::default(), + ready: Default::default(), + is_ready: Default::default(), + } + } +} + +impl fmt::Debug for Messages +where + G: fmt::Debug, + L: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Messages") + .field("global", &self.global) + .field("local", &self.local) + .field("is_ready", &self.is_ready) + .finish() + } +} + +#[derive(Debug)] +struct MessagePair { + pub msg: M, + pub range: Range, +} + +impl GetChunkPos for MessagePair { + fn chunk_pos(&self) -> ChunkPos { + self.msg.chunk_pos() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] + struct DummyLocal; + + impl GetChunkPos for DummyLocal { + fn chunk_pos(&self) -> ChunkPos { + unimplemented!() + } + } + + #[test] + fn send_global_message() { + #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] + enum TestMsg { + Foo, + Bar, + } + + let mut messages = Messages::::new(); + + messages.send_global(TestMsg::Foo, |w| w.extend_from_slice(&[1, 2, 3])); + messages.send_global(TestMsg::Bar, |b| b.extend_from_slice(&[4, 5, 6])); + messages.send_global(TestMsg::Foo, |b| b.extend_from_slice(&[7, 8, 9])); + + messages.ready(); + + let bytes = messages.bytes(); + + for (msg, range) in messages.iter_global() { + match msg { + TestMsg::Foo => assert_eq!(&bytes[range.clone()], &[1, 2, 3, 7, 8, 9]), + TestMsg::Bar => assert_eq!(&bytes[range.clone()], &[4, 5, 6]), + } + } + + messages.unready(); + } +} diff --git a/crates/valence_instance/src/packet.rs b/crates/valence_layer/src/packet.rs similarity index 100% rename from crates/valence_instance/src/packet.rs rename to crates/valence_layer/src/packet.rs diff --git a/crates/valence_world_border/src/lib.rs b/crates/valence_world_border/src/lib.rs index 480f93eba..67e753b2d 100644 --- a/crates/valence_world_border/src/lib.rs +++ b/crates/valence_world_border/src/lib.rs @@ -77,7 +77,7 @@ use valence_client::{Client, FlushPacketsSet}; use valence_core::protocol::encode::WritePacket; use valence_core::protocol::var_int::VarInt; use valence_core::protocol::var_long::VarLong; -use valence_entity::Location; +use valence_entity::EntityLayerId; use valence_instance::{Instance, WriteUpdatePacketsToInstancesSet}; use valence_registry::*; @@ -269,7 +269,7 @@ fn wb_size_change( } fn border_for_player( - mut clients: Query<(&mut Client, &Location), Changed>, + mut clients: Query<(&mut Client, &EntityLayerId), Changed>, wbs: Query< ( &WorldBorderCenter, diff --git a/examples/advancement.rs b/examples/advancement.rs index e0da009e7..c0cdacb1a 100644 --- a/examples/advancement.rs +++ b/examples/advancement.rs @@ -165,7 +165,7 @@ fn setup( } fn init_clients( - mut clients: Query<(&mut Location, &mut Position, &mut GameMode), Added>, + mut clients: Query<(&mut EntityLayerId, &mut Position, &mut GameMode), Added>, instances: Query>, ) { for (mut loc, mut pos, mut game_mode) in &mut clients { diff --git a/examples/anvil_loading.rs b/examples/anvil_loading.rs index 84acc1d59..064361e2c 100644 --- a/examples/anvil_loading.rs +++ b/examples/anvil_loading.rs @@ -71,7 +71,15 @@ fn setup( } fn init_clients( - mut clients: Query<(&mut Location, &mut Position, &mut GameMode, &mut IsFlat), Added>, + mut clients: Query< + ( + &mut EntityLayerId, + &mut Position, + &mut GameMode, + &mut IsFlat, + ), + Added, + >, instances: Query>, ) { for (mut loc, mut pos, mut game_mode, mut is_flat) in &mut clients { diff --git a/examples/bench_players.rs b/examples/bench_players.rs index d60766817..9b7c8bddf 100644 --- a/examples/bench_players.rs +++ b/examples/bench_players.rs @@ -72,7 +72,7 @@ fn setup( } fn init_clients( - mut clients: Query<(&mut Location, &mut Position, &mut GameMode), Added>, + mut clients: Query<(&mut EntityLayerId, &mut Position, &mut GameMode), Added>, instances: Query>, ) { for (mut loc, mut pos, mut game_mode) in &mut clients { diff --git a/examples/biomes.rs b/examples/biomes.rs index c8b9f4472..475743a95 100644 --- a/examples/biomes.rs +++ b/examples/biomes.rs @@ -53,7 +53,7 @@ fn setup( } fn init_clients( - mut clients: Query<(&mut Position, &mut Location, &mut GameMode), Added>, + mut clients: Query<(&mut Position, &mut EntityLayerId, &mut GameMode), Added>, instances: Query>, ) { for (mut pos, mut loc, mut game_mode) in &mut clients { diff --git a/examples/block_entities.rs b/examples/block_entities.rs index 87d5e71ec..3a0f1f33d 100644 --- a/examples/block_entities.rs +++ b/examples/block_entities.rs @@ -64,7 +64,10 @@ fn setup( } fn init_clients( - mut clients: Query<(&mut Location, &mut Position, &mut Look, &mut GameMode), Added>, + mut clients: Query< + (&mut EntityLayerId, &mut Position, &mut Look, &mut GameMode), + Added, + >, instances: Query>, ) { for (mut loc, mut pos, mut look, mut game_mode) in &mut clients { diff --git a/examples/boss_bar.rs b/examples/boss_bar.rs index ac73c457a..8d72fdd2e 100644 --- a/examples/boss_bar.rs +++ b/examples/boss_bar.rs @@ -54,7 +54,7 @@ fn init_clients( ( Entity, &mut Client, - &mut Location, + &mut EntityLayerId, &mut Position, &mut GameMode, ), diff --git a/examples/building.rs b/examples/building.rs index 94c670557..1c0ab212b 100644 --- a/examples/building.rs +++ b/examples/building.rs @@ -49,7 +49,15 @@ fn setup( } fn init_clients( - mut clients: Query<(&mut Client, &mut Location, &mut Position, &mut GameMode), Added>, + mut clients: Query< + ( + &mut Client, + &mut EntityLayerId, + &mut Position, + &mut GameMode, + ), + Added, + >, instances: Query>, ) { for (mut client, mut loc, mut pos, mut game_mode) in &mut clients { diff --git a/examples/chest.rs b/examples/chest.rs index 77c6b7ccc..afb862fcc 100644 --- a/examples/chest.rs +++ b/examples/chest.rs @@ -53,7 +53,7 @@ fn setup( } fn init_clients( - mut clients: Query<(&mut Location, &mut Position, &mut GameMode), Added>, + mut clients: Query<(&mut EntityLayerId, &mut Position, &mut GameMode), Added>, instances: Query>, ) { for (mut loc, mut pos, mut game_mode) in &mut clients { diff --git a/examples/combat.rs b/examples/combat.rs index 9c4e271a1..b806bb176 100644 --- a/examples/combat.rs +++ b/examples/combat.rs @@ -71,7 +71,7 @@ fn setup( } fn init_clients( - mut clients: Query<(Entity, &mut Location, &mut Position), Added>, + mut clients: Query<(Entity, &mut EntityLayerId, &mut Position), Added>, instances: Query>, mut commands: Commands, ) { diff --git a/examples/cow_sphere.rs b/examples/cow_sphere.rs index cd00a0969..abad213c6 100644 --- a/examples/cow_sphere.rs +++ b/examples/cow_sphere.rs @@ -51,7 +51,7 @@ fn setup( commands.spawn_batch([0; SPHERE_AMOUNT].map(|_| { ( SpherePartBundle { - location: Location(instance_id), + location: EntityLayerId(instance_id), ..Default::default() }, SpherePart, @@ -60,7 +60,7 @@ fn setup( } fn init_clients( - mut clients: Query<(&mut Location, &mut Position, &mut GameMode), Added>, + mut clients: Query<(&mut EntityLayerId, &mut Position, &mut GameMode), Added>, instances: Query>, ) { for (mut loc, mut pos, mut game_mode) in &mut clients { diff --git a/examples/death.rs b/examples/death.rs index c64016a72..04648fbcb 100644 --- a/examples/death.rs +++ b/examples/death.rs @@ -52,7 +52,7 @@ fn setup( } fn init_clients( - mut clients: Query<(&mut Client, &mut Location, &mut Position), Added>, + mut clients: Query<(&mut Client, &mut EntityLayerId, &mut Position), Added>, instances: Query>, ) { for (mut client, mut loc, mut pos) in &mut clients { @@ -76,7 +76,7 @@ fn squat_and_die(mut clients: Query<&mut Client>, mut events: EventReader, + mut clients: Query<(&mut EntityLayerId, &mut RespawnPosition)>, mut events: EventReader, instances: Query>, ) { diff --git a/examples/entity_hitbox.rs b/examples/entity_hitbox.rs index d6c044334..575c69a69 100644 --- a/examples/entity_hitbox.rs +++ b/examples/entity_hitbox.rs @@ -46,7 +46,15 @@ fn setup( } fn init_clients( - mut clients: Query<(&mut Location, &mut Position, &mut GameMode, &mut Client), Added>, + mut clients: Query< + ( + &mut EntityLayerId, + &mut Position, + &mut GameMode, + &mut Client, + ), + Added, + >, instances: Query>, ) { for (mut loc, mut pos, mut game_mode, mut client) in &mut clients { @@ -60,7 +68,7 @@ fn init_clients( fn spawn_entity( mut commands: Commands, mut sneaking: EventReader, - client_query: Query<(&Position, &Location)>, + client_query: Query<(&Position, &EntityLayerId)>, ) { for sneaking in sneaking.iter() { if sneaking.state == SneakState::Start { diff --git a/examples/game_of_life.rs b/examples/game_of_life.rs index e2cabc2fb..81f952dea 100644 --- a/examples/game_of_life.rs +++ b/examples/game_of_life.rs @@ -72,7 +72,7 @@ fn setup( } fn init_clients( - mut clients: Query<(&mut Client, &mut Location, &mut Position), Added>, + mut clients: Query<(&mut Client, &mut EntityLayerId, &mut Position), Added>, instances: Query>, ) { for (mut client, mut loc, mut pos) in &mut clients { diff --git a/examples/parkour.rs b/examples/parkour.rs index aa079f798..4a758fbdd 100644 --- a/examples/parkour.rs +++ b/examples/parkour.rs @@ -52,7 +52,7 @@ fn init_clients( ( Entity, &mut Client, - &mut Location, + &mut EntityLayerId, &mut IsFlat, &mut GameMode, ), diff --git a/examples/particles.rs b/examples/particles.rs index 9f62246c1..5d3e5869f 100644 --- a/examples/particles.rs +++ b/examples/particles.rs @@ -42,7 +42,7 @@ fn setup( } fn init_clients( - mut clients: Query<(&mut Location, &mut Position, &mut GameMode), Added>, + mut clients: Query<(&mut EntityLayerId, &mut Position, &mut GameMode), Added>, instances: Query>, ) { for (mut loc, mut pos, mut game_mode) in &mut clients { diff --git a/examples/player_list.rs b/examples/player_list.rs index 55407cb2d..abb6861d3 100644 --- a/examples/player_list.rs +++ b/examples/player_list.rs @@ -56,7 +56,15 @@ fn setup( } fn init_clients( - mut clients: Query<(&mut Client, &mut Position, &mut Location, &mut GameMode), Added>, + mut clients: Query< + ( + &mut Client, + &mut Position, + &mut EntityLayerId, + &mut GameMode, + ), + Added, + >, instances: Query>, ) { for (mut client, mut pos, mut loc, mut game_mode) in &mut clients { diff --git a/examples/resource_pack.rs b/examples/resource_pack.rs index 8d3512d53..50fa983e2 100644 --- a/examples/resource_pack.rs +++ b/examples/resource_pack.rs @@ -47,7 +47,7 @@ fn setup( let instance_ent = commands.spawn(instance).id(); commands.spawn(SheepEntityBundle { - location: Location(instance_ent), + location: EntityLayerId(instance_ent), position: Position::new([0.0, SPAWN_Y as f64 + 1.0, 2.0]), look: Look::new(180.0, 0.0), head_yaw: HeadYaw(180.0), @@ -66,7 +66,7 @@ fn init_clients( client.send_chat_message("Hit the sheep to prompt for the resource pack.".italic()); commands.entity(entity).insert(PlayerEntityBundle { - location: Location(instances.single()), + location: EntityLayerId(instances.single()), position: Position::new([0.0, SPAWN_Y as f64 + 1.0, 0.0]), uuid: *uuid, ..Default::default() diff --git a/examples/terrain.rs b/examples/terrain.rs index c3be1812b..1999a3e2c 100644 --- a/examples/terrain.rs +++ b/examples/terrain.rs @@ -110,7 +110,15 @@ fn setup( } fn init_clients( - mut clients: Query<(&mut Location, &mut Position, &mut IsFlat, &mut GameMode), Added>, + mut clients: Query< + ( + &mut EntityLayerId, + &mut Position, + &mut IsFlat, + &mut GameMode, + ), + Added, + >, instances: Query>, ) { for (mut loc, mut pos, mut is_flat, mut game_mode) in &mut clients { diff --git a/examples/text.rs b/examples/text.rs index d4edcc6bc..7533b663f 100644 --- a/examples/text.rs +++ b/examples/text.rs @@ -37,7 +37,15 @@ fn setup( } fn init_clients( - mut clients: Query<(&mut Client, &mut Position, &mut Location, &mut GameMode), Added>, + mut clients: Query< + ( + &mut Client, + &mut Position, + &mut EntityLayerId, + &mut GameMode, + ), + Added, + >, instances: Query>, ) { for (mut client, mut pos, mut loc, mut game_mode) in &mut clients { diff --git a/examples/world_border.rs b/examples/world_border.rs index d09fbd590..491f64bbc 100644 --- a/examples/world_border.rs +++ b/examples/world_border.rs @@ -56,7 +56,7 @@ fn init_clients( mut clients: Query< ( &mut Client, - &mut Location, + &mut EntityLayerId, &mut Position, &mut Inventory, &HeldItem, @@ -75,7 +75,7 @@ fn init_clients( } fn border_center_avg( - clients: Query<(&Location, &Position)>, + clients: Query<(&EntityLayerId, &Position)>, mut instances: Query<(Entity, &mut WorldBorderCenter), With>, ) { for (entity, mut center) in instances.iter_mut() { @@ -99,7 +99,7 @@ fn border_center_avg( fn border_expand( mut events: EventReader, - clients: Query<&Location, With>, + clients: Query<&EntityLayerId, With>, wbs: Query<&WorldBorderDiameter, With>, mut event_writer: EventWriter, ) { diff --git a/src/lib.rs b/src/lib.rs index 579b5e97b..884a94868 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -105,8 +105,8 @@ pub mod prelude { pub use valence_dimension::{DimensionType, DimensionTypeRegistry}; pub use valence_entity::hitbox::{Hitbox, HitboxShape}; pub use valence_entity::{ - EntityAnimation, EntityKind, EntityManager, EntityStatus, HeadYaw, Location, Look, - OldLocation, OldPosition, Position, + EntityAnimation, EntityKind, EntityLayerId, EntityManager, EntityStatus, HeadYaw, Look, + OldEntityLayerId, OldPosition, Position, }; pub use valence_instance::chunk::{Chunk, LoadedChunk, UnloadedChunk}; pub use valence_instance::{Block, BlockRef, Instance}; diff --git a/src/tests/client.rs b/src/tests/client.rs index df14b65c9..c8c29c02b 100644 --- a/src/tests/client.rs +++ b/src/tests/client.rs @@ -11,7 +11,7 @@ use valence_core::chunk_pos::{ChunkPos, ChunkView}; use valence_core::protocol::Packet; use valence_entity::cow::CowEntityBundle; use valence_entity::packet::{EntitiesDestroyS2c, EntitySpawnS2c, MoveRelativeS2c}; -use valence_entity::{Location, Position}; +use valence_entity::{EntityLayerId, Position}; use valence_instance::chunk::UnloadedChunk; use valence_instance::packet::{ChunkDataS2c, UnloadChunkS2c}; use valence_instance::Instance; @@ -118,7 +118,7 @@ fn entity_chunk_spawn_despawn() { .world .spawn(CowEntityBundle { position: Position::new([8.0, 0.0, 8.0]), - location: Location(inst_ent), + location: EntityLayerId(inst_ent), ..Default::default() }) .id(); diff --git a/src/tests/world_border.rs b/src/tests/world_border.rs index cedf9fc28..d7a3ea569 100644 --- a/src/tests/world_border.rs +++ b/src/tests/world_border.rs @@ -1,7 +1,7 @@ use std::time::Duration; use bevy_app::App; -use valence_entity::Location; +use valence_entity::EntityLayerId; use valence_instance::Instance; use valence_registry::{Entity, Mut}; use valence_world_border::packet::*; @@ -17,7 +17,7 @@ fn test_intialize_on_join() { let (client, mut client_helper) = create_mock_client("test"); let client_ent = app.world.spawn(client).id(); - app.world.get_mut::(client_ent).unwrap().0 = instance_ent; + app.world.get_mut::(client_ent).unwrap().0 = instance_ent; app.update(); client_helper From b82b43b7d2f9925de1643db88bfabf612e0e90f1 Mon Sep 17 00:00:00 2001 From: Ryan Date: Sat, 8 Jul 2023 08:14:35 -0700 Subject: [PATCH 05/47] `valence_client` changes --- Cargo.toml | 4 +- crates/valence_anvil/Cargo.toml | 2 +- crates/valence_client/Cargo.toml | 3 +- crates/valence_client/src/keepalive.rs | 12 +- crates/valence_client/src/lib.rs | 1012 ++++++++++++++-------- crates/valence_client/src/packet.rs | 1 - crates/valence_client/src/spawn.rs | 206 +++++ crates/valence_client/src/teleport.rs | 3 +- crates/valence_client/src/weather.rs | 2 + crates/valence_layer/src/chunk.rs | 17 +- crates/valence_layer/src/chunk/chunk.rs | 4 +- crates/valence_layer/src/chunk/loaded.rs | 19 +- crates/valence_layer/src/entity.rs | 32 +- crates/valence_layer/src/lib.rs | 7 +- crates/valence_layer/src/message.rs | 16 +- crates/valence_player_list/Cargo.toml | 2 +- crates/valence_world_border/Cargo.toml | 2 +- 17 files changed, 909 insertions(+), 435 deletions(-) create mode 100644 crates/valence_client/src/spawn.rs diff --git a/Cargo.toml b/Cargo.toml index 5a12b1af0..be47d3f18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ valence_client.workspace = true valence_core.workspace = true valence_dimension.workspace = true valence_entity.workspace = true -valence_instance.workspace = true +valence_layer.workspace = true valence_inventory = { workspace = true, optional = true } valence_nbt.workspace = true valence_network = { workspace = true, optional = true } @@ -170,7 +170,7 @@ valence_core_macros.path = "crates/valence_core_macros" valence_core.path = "crates/valence_core" valence_dimension.path = "crates/valence_dimension" valence_entity.path = "crates/valence_entity" -valence_instance.path = "crates/valence_instance" +valence_layer.path = "crates/valence_layer" valence_inventory.path = "crates/valence_inventory" valence_nbt = { path = "crates/valence_nbt", features = ["uuid"] } valence_network.path = "crates/valence_network" diff --git a/crates/valence_anvil/Cargo.toml b/crates/valence_anvil/Cargo.toml index 4031233bd..b8db1e3ef 100644 --- a/crates/valence_anvil/Cargo.toml +++ b/crates/valence_anvil/Cargo.toml @@ -24,5 +24,5 @@ valence_block.workspace = true valence_client.workspace = true valence_core.workspace = true valence_entity.workspace = true -valence_instance.workspace = true +valence_layer.workspace = true valence_nbt.workspace = true diff --git a/crates/valence_client/Cargo.toml b/crates/valence_client/Cargo.toml index b7df47732..bc0ec0d18 100644 --- a/crates/valence_client/Cargo.toml +++ b/crates/valence_client/Cargo.toml @@ -14,10 +14,11 @@ glam.workspace = true rand.workspace = true tracing.workspace = true uuid.workspace = true +byteorder.workspace = true valence_biome.workspace = true valence_core.workspace = true valence_dimension.workspace = true valence_entity.workspace = true -valence_instance.workspace = true +valence_layer.workspace = true valence_nbt.workspace = true valence_registry.workspace = true diff --git a/crates/valence_client/src/keepalive.rs b/crates/valence_client/src/keepalive.rs index f92b97464..2eaf96d7f 100644 --- a/crates/valence_client/src/keepalive.rs +++ b/crates/valence_client/src/keepalive.rs @@ -66,7 +66,7 @@ fn send_keepalive( } else { let millis = settings.period.as_millis(); warn!("Client {entity:?} timed out: no keepalive response after {millis}ms"); - commands.entity(entity).insert(Despawned); + commands.entity(entity).remove::(); } } } @@ -79,16 +79,16 @@ fn handle_keepalive_response( ) { for packet in packets.iter() { if let Some(pkt) = packet.decode::() { - if let Ok((client, mut state, mut ping)) = clients.get_mut(packet.client) { + if let Ok((entity, mut state, mut ping)) = clients.get_mut(packet.client) { if state.got_keepalive { - warn!("unexpected keepalive from client {client:?}"); - commands.entity(entity).insert(Despawned); + warn!("unexpected keepalive from client {entity:?}"); + commands.entity(entity).remove::(); } else if pkt.id != state.last_keepalive_id { warn!( - "keepalive IDs don't match for client {client:?} (expected {}, got {})", + "keepalive IDs don't match for client {entity:?} (expected {}, got {})", state.last_keepalive_id, pkt.id, ); - commands.entity(entity).insert(Despawned); + commands.entity(entity).remove::(); } else { state.got_keepalive = true; ping.0 = state.last_send.elapsed().as_millis() as i32; diff --git a/crates/valence_client/src/lib.rs b/crates/valence_client/src/lib.rs index 1b37ea66f..7775ffd7f 100644 --- a/crates/valence_client/src/lib.rs +++ b/crates/valence_client/src/lib.rs @@ -19,6 +19,7 @@ #![allow(clippy::type_complexity)] use std::borrow::Cow; +use std::collections::BTreeSet; use std::fmt; use std::net::IpAddr; use std::ops::Deref; @@ -28,13 +29,15 @@ use bevy_app::prelude::*; use bevy_ecs::prelude::*; use bevy_ecs::query::WorldQuery; use bevy_ecs::system::Command; +use byteorder::{NativeEndian, ReadBytesExt}; use bytes::{Bytes, BytesMut}; use glam::{DVec3, Vec3}; use packet::{ DeathMessageS2c, DisconnectS2c, GameEventKind, GameJoinS2c, GameStateChangeS2c, - PlayerRespawnS2c, PlayerSpawnPositionS2c, PlayerSpawnS2c, + PlayerRespawnS2c, PlayerSpawnPositionS2c, }; -use tracing::{debug, warn}; +use spawn::PortalCooldown; +use tracing::warn; use uuid::Uuid; use valence_biome::BiomeRegistry; use valence_core::block_pos::BlockPos; @@ -44,7 +47,6 @@ use valence_core::game_mode::GameMode; use valence_core::ident::Ident; use valence_core::particle::{Particle, ParticleS2c}; use valence_core::property::Property; -use valence_core::protocol::byte_angle::ByteAngle; use valence_core::protocol::encode::{PacketEncoder, WritePacket}; use valence_core::protocol::global_pos::GlobalPos; use valence_core::protocol::packet::sound::{PlaySoundS2c, Sound, SoundCategory}; @@ -53,22 +55,21 @@ use valence_core::protocol::{Encode, Packet}; use valence_core::text::Text; use valence_core::uuid::UniqueId; use valence_entity::packet::{ - EntitiesDestroyS2c, EntitySetHeadYawS2c, EntitySpawnS2c, EntityStatusS2c, - EntityTrackerUpdateS2c, EntityVelocityUpdateS2c, ExperienceOrbSpawnS2c, + EntitiesDestroyS2c, EntityStatusS2c, EntityTrackerUpdateS2c, EntityVelocityUpdateS2c, }; use valence_entity::player::PlayerEntityBundle; +use valence_entity::query::EntityInitQuery; +use valence_entity::tracked_data::TrackedData; use valence_entity::{ - ClearEntityChangesSet, EntityId, EntityKind, EntityLayerId, EntityStatus, HeadYaw, Look, - ObjectData, OldEntityLayerId, OldPosition, OnGround, PacketByteRange, Position, TrackedData, + ClearEntityChangesSet, EntityId, EntityLayerId, EntityStatus, Look, OldPosition, Position, Velocity, }; -use valence_instance::chunk::loaded::ChunkState; -use valence_instance::packet::{ - ChunkLoadDistanceS2c, ChunkRenderDistanceCenterS2c, UnloadChunkS2c, +use valence_layer::chunk::loaded::ChunkState; +use valence_layer::packet::{ + ChunkBiome, ChunkBiomeDataS2c, ChunkLoadDistanceS2c, ChunkRenderDistanceCenterS2c, + UnloadChunkS2c, }; -use valence_instance::{ClearInstanceChangesSet, Instance, WriteUpdatePacketsToInstancesSet}; -use valence_registry::codec::RegistryCodec; -use valence_registry::tags::TagsRegistry; +use valence_layer::{ChunkLayer, EntityLayer, UpdateLayersPostClient, UpdateLayersPreClient}; use valence_registry::RegistrySet; pub mod action; @@ -86,6 +87,7 @@ pub mod op_level; pub mod packet; pub mod resource_pack; pub mod settings; +pub mod spawn; pub mod status; pub mod teleport; pub mod title; @@ -115,19 +117,21 @@ impl Plugin for ClientPlugin { app.add_systems( PostUpdate, ( - initial_join.after(RegistrySet), + spawn::initial_join.after(RegistrySet), update_chunk_load_dist, - read_data_in_old_view - .after(WriteUpdatePacketsToInstancesSet) + handle_layer_messages + .after(UpdateLayersPreClient) .after(update_chunk_load_dist), - update_view.after(initial_join).after(read_data_in_old_view), - update_respawn_position.after(update_view), - respawn.after(update_respawn_position), - remove_entities.after(update_view), - update_old_view_dist.after(update_view), + update_view_and_layers + .after(spawn::initial_join) + .after(handle_layer_messages), + spawn::update_respawn_position.after(update_view_and_layers), + spawn::respawn.after(spawn::update_respawn_position), + remove_entities.after(update_view_and_layers), + update_old_view_dist.after(update_view_and_layers), update_game_mode, - update_tracked_data.after(WriteUpdatePacketsToInstancesSet), - init_tracked_data.after(WriteUpdatePacketsToInstancesSet), + update_tracked_data.after(UpdateLayersPreClient), + init_tracked_data.after(UpdateLayersPreClient), ) .in_set(UpdateClientsSet), ) @@ -139,7 +143,7 @@ impl Plugin for ClientPlugin { UpdateClientsSet.before(FlushPacketsSet), ClearEntityChangesSet.after(UpdateClientsSet), FlushPacketsSet, - ClearInstanceChangesSet.after(FlushPacketsSet), + UpdateLayersPostClient.after(FlushPacketsSet), ), ); @@ -151,7 +155,7 @@ impl Plugin for ClientPlugin { settings::build(app); action::build(app); teleport::build(app); - weather::build(app); + // weather::build(app); message::build(app); custom_payload::build(app); hand_swing::build(app); @@ -174,23 +178,27 @@ pub struct ClientBundle { pub ip: Ip, pub properties: Properties, pub respawn_pos: RespawnPosition, - pub game_mode: GameMode, pub op_level: op_level::OpLevel, pub action_sequence: action::ActionSequence, pub view_distance: ViewDistance, pub old_view_distance: OldViewDistance, - pub death_location: DeathLocation, + pub visible_chunk_layer: VisibleChunkLayer, + pub old_visible_chunk_layer: OldVisibleChunkLayer, + pub visible_entity_layers: VisibleEntityLayers, + pub old_visible_entity_layers: OldVisibleEntityLayers, pub keepalive_state: keepalive::KeepaliveState, pub ping: Ping, - pub is_hardcore: IsHardcore, - pub prev_game_mode: PrevGameMode, - pub hashed_seed: HashedSeed, - pub reduced_debug_info: ReducedDebugInfo, - pub has_respawn_screen: HasRespawnScreen, - pub is_debug: IsDebug, - pub is_flat: IsFlat, pub teleport_state: teleport::TeleportState, - pub packet_byte_range: PacketByteRange, + pub game_mode: GameMode, + pub prev_game_mode: spawn::PrevGameMode, + pub death_location: spawn::DeathLocation, + pub is_hardcore: spawn::IsHardcore, + pub hashed_seed: spawn::HashedSeed, + pub reduced_debug_info: spawn::ReducedDebugInfo, + pub has_respawn_screen: spawn::HasRespawnScreen, + pub is_debug: spawn::IsDebug, + pub is_flat: spawn::IsFlat, + pub portal_cooldown: PortalCooldown, pub player: PlayerEntityBundle, } @@ -207,23 +215,27 @@ impl ClientBundle { ip: Ip(args.ip), properties: Properties(args.properties), respawn_pos: RespawnPosition::default(), - game_mode: GameMode::default(), op_level: op_level::OpLevel::default(), action_sequence: action::ActionSequence::default(), view_distance: ViewDistance::default(), old_view_distance: OldViewDistance(2), - death_location: DeathLocation::default(), + visible_chunk_layer: VisibleChunkLayer::default(), + old_visible_chunk_layer: OldVisibleChunkLayer(Entity::PLACEHOLDER), + visible_entity_layers: VisibleEntityLayers::default(), + old_visible_entity_layers: OldVisibleEntityLayers(BTreeSet::new()), keepalive_state: keepalive::KeepaliveState::new(), ping: Ping::default(), teleport_state: teleport::TeleportState::new(), - is_hardcore: IsHardcore::default(), - is_flat: IsFlat::default(), - has_respawn_screen: HasRespawnScreen::default(), - prev_game_mode: PrevGameMode::default(), - hashed_seed: HashedSeed::default(), - reduced_debug_info: ReducedDebugInfo::default(), - is_debug: IsDebug::default(), - packet_byte_range: PacketByteRange::default(), + game_mode: GameMode::default(), + prev_game_mode: spawn::PrevGameMode::default(), + death_location: spawn::DeathLocation::default(), + is_hardcore: spawn::IsHardcore::default(), + is_flat: spawn::IsFlat::default(), + has_respawn_screen: spawn::HasRespawnScreen::default(), + hashed_seed: spawn::HashedSeed::default(), + reduced_debug_info: spawn::ReducedDebugInfo::default(), + is_debug: spawn::IsDebug::default(), + portal_cooldown: PortalCooldown::default(), player: PlayerEntityBundle { uuid: UniqueId(args.uuid), ..Default::default() @@ -605,9 +617,6 @@ impl OldViewItem<'_> { } } -#[derive(Component, Clone, PartialEq, Eq, Default, Debug)] -pub struct DeathLocation(pub Option<(Ident, BlockPos)>); - /// Delay measured in milliseconds. Negative values indicate absence. #[derive(Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub struct Ping(pub i32); @@ -618,35 +627,35 @@ impl Default for Ping { } } -#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)] -pub struct IsHardcore(pub bool); - -/// The initial previous gamemode. Used for the F3+F4 gamemode switcher. -#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)] -pub struct PrevGameMode(pub Option); - -/// Hashed world seed used for biome noise. -#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)] -pub struct HashedSeed(pub u64); - -#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)] -pub struct ReducedDebugInfo(pub bool); - #[derive(Component, Copy, Clone, PartialEq, Eq, Debug)] -pub struct HasRespawnScreen(pub bool); +pub struct VisibleChunkLayer(pub Entity); -impl Default for HasRespawnScreen { +impl Default for VisibleChunkLayer { fn default() -> Self { - Self(true) + Self(Entity::PLACEHOLDER) } } -#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)] -pub struct IsDebug(pub bool); +#[derive(Component, PartialEq, Eq, Debug)] +pub struct OldVisibleChunkLayer(Entity); -/// Changes the perceived horizon line (used for superflat worlds). -#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)] -pub struct IsFlat(pub bool); +impl OldVisibleChunkLayer { + pub fn get(&self) -> Entity { + self.0 + } +} + +#[derive(Component, Default, Debug)] +pub struct VisibleEntityLayers(pub BTreeSet); + +#[derive(Component, Default, Debug)] +pub struct OldVisibleEntityLayers(BTreeSet); + +impl OldVisibleEntityLayers { + pub fn get(&self) -> &BTreeSet { + &self.0 + } +} /// A system for adding [`Despawned`] components to disconnected clients. This /// works by listening for removed [`Client`] components. @@ -661,137 +670,6 @@ pub fn despawn_disconnected_clients( } } -#[derive(WorldQuery)] -#[world_query(mutable)] -struct ClientJoinQuery { - entity: Entity, - client: &'static mut Client, - loc: &'static EntityLayerId, - pos: &'static Position, - is_hardcore: &'static IsHardcore, - game_mode: &'static GameMode, - prev_game_mode: &'static PrevGameMode, - hashed_seed: &'static HashedSeed, - view_distance: &'static ViewDistance, - reduced_debug_info: &'static ReducedDebugInfo, - has_respawn_screen: &'static HasRespawnScreen, - is_debug: &'static IsDebug, - is_flat: &'static IsFlat, - death_loc: &'static DeathLocation, -} - -fn initial_join( - codec: Res, - tags: Res, - mut clients: Query>, - instances: Query<&Instance>, - mut commands: Commands, -) { - for mut q in &mut clients { - let Ok(instance) = instances.get(q.loc.0) else { - warn!("client {:?} joined nonexistent instance {:?}", q.entity, q.loc.0); - commands.entity(q.entity).remove::(); - continue - }; - - let dimension_names: Vec>> = codec - .registry(BiomeRegistry::KEY) - .iter() - .map(|value| value.name.as_str_ident().into()) - .collect(); - - let dimension_name: Ident> = instance.dimension_type_name().into(); - - let last_death_location = q.death_loc.0.as_ref().map(|(id, pos)| GlobalPos { - dimension_name: id.as_str_ident().into(), - position: *pos, - }); - - // The login packet is prepended so that it's sent before all the other packets. - // Some packets don't work corectly when sent before the game join packet. - _ = q.client.enc.prepend_packet(&GameJoinS2c { - entity_id: 0, // We reserve ID 0 for clients. - is_hardcore: q.is_hardcore.0, - game_mode: *q.game_mode, - previous_game_mode: q.prev_game_mode.0.map(|g| g as i8).unwrap_or(-1), - dimension_names, - registry_codec: Cow::Borrowed(codec.cached_codec()), - dimension_type_name: dimension_name.clone(), - dimension_name, - hashed_seed: q.hashed_seed.0 as i64, - max_players: VarInt(0), // Ignored by clients. - view_distance: VarInt(q.view_distance.0 as i32), - simulation_distance: VarInt(16), // TODO. - reduced_debug_info: q.reduced_debug_info.0, - enable_respawn_screen: q.has_respawn_screen.0, - is_debug: q.is_debug.0, - is_flat: q.is_flat.0, - last_death_location, - portal_cooldown: VarInt(0), // TODO. - }); - - q.client.enc.append_bytes(tags.sync_tags_packet()); - - /* - // TODO: enable all the features? - q.client.write_packet(&FeatureFlags { - features: vec![Ident::new("vanilla").unwrap()], - })?; - */ - } -} - -fn respawn( - mut clients: Query< - ( - &mut Client, - &EntityLayerId, - &DeathLocation, - &HashedSeed, - &GameMode, - &PrevGameMode, - &IsDebug, - &IsFlat, - ), - Changed, - >, - instances: Query<&Instance>, -) { - for (mut client, loc, death_loc, hashed_seed, game_mode, prev_game_mode, is_debug, is_flat) in - &mut clients - { - if client.is_added() { - // No need to respawn since we are sending the game join packet this tick. - continue; - } - - let Ok(instance) = instances.get(loc.0) else { - warn!("Client respawned in nonexistent instance."); - continue - }; - - let dimension_name = instance.dimension_type_name(); - - let last_death_location = death_loc.0.as_ref().map(|(id, pos)| GlobalPos { - dimension_name: id.as_str_ident().into(), - position: *pos, - }); - - client.write_packet(&PlayerRespawnS2c { - dimension_type_name: dimension_name.into(), - dimension_name: dimension_name.into(), - hashed_seed: hashed_seed.0, - game_mode: *game_mode, - previous_game_mode: prev_game_mode.0.map(|g| g as i8).unwrap_or(-1), - is_debug: is_debug.0, - is_flat: is_flat.0, - copy_metadata: true, - last_death_location, - portal_cooldown: VarInt(0), // TODO - }); - } -} - fn update_chunk_load_dist( mut clients: Query<(&mut Client, &ViewDistance, &OldViewDistance), Changed>, ) { @@ -802,6 +680,7 @@ fn update_chunk_load_dist( } if dist.0 != old_dist.0 { + // Note: This packet is just aesthetic. client.write_packet(&ChunkLoadDistanceS2c { view_distance: VarInt(dist.0.into()), }); @@ -809,161 +688,313 @@ fn update_chunk_load_dist( } } -fn read_data_in_old_view( +fn handle_layer_messages( mut clients: Query<( Entity, + &EntityId, &mut Client, &mut EntityRemoveBuf, - &EntityLayerId, - &OldEntityLayerId, - &Position, - &OldPosition, - &OldViewDistance, - &PacketByteRange, + OldView, + &OldVisibleChunkLayer, + &OldVisibleEntityLayers, )>, - instances: Query<&Instance>, + chunk_layers: Query<&ChunkLayer>, + entity_layers: Query<&EntityLayer>, entities: Query<(EntityInitQuery, &OldPosition)>, - entity_ids: Query<&EntityId>, ) { clients.par_iter_mut().for_each_mut( |( self_entity, + self_entity_id, mut client, mut remove_buf, - loc, - old_loc, - pos, - old_pos, - old_view_dist, - byte_range, + old_view, + old_chunk_layer, + old_entity_layers, )| { - let Ok(inst) = instances.get(old_loc.get()) else { - return; - }; - - // Send instance-wide packet data. - client.write_packet_bytes(inst.packet_buf()); - - // TODO: cache the chunk position? - let old_chunk_pos = old_pos.chunk_pos(); - let new_chunk_pos = pos.chunk_pos(); - - let view = ChunkView::new(old_chunk_pos, old_view_dist.0); - - // Iterate over all visible chunks from the previous tick. - for pos in view.iter() { - if let Some(chunk) = inst.chunk(pos) { - // Mark this chunk as being in view of a client. - chunk.set_viewed(); - - // Send entity spawn packets for entities entering the client's view. - for &(entity, src_pos) in chunk.incoming_entities() { - if src_pos.map_or(true, |p| !view.contains(p)) { - // The incoming entity originated from outside the view distance, so it - // must be spawned. - if let Ok((entity, old_pos)) = entities.get(entity) { - // Notice we are spawning the entity at its old position rather than - // the current position. This is because the client could also - // receive update packets for this entity this tick, which may - // include a relative entity movement. - entity.write_init_packets(old_pos.get(), &mut client.enc); + let old_view = old_view.get(); + + if let Ok(chunk_layer) = chunk_layers.get(old_chunk_layer.get()) { + let messages = chunk_layer.messages(); + let bytes = messages.bytes(); + + for (msg, range) in messages.iter_global() { + match msg { + valence_layer::chunk::GlobalMsg::Packet => { + client.write_packet_bytes(&bytes[range]); + } + valence_layer::chunk::GlobalMsg::PacketExcept { except } => { + if self_entity != except { + client.write_packet_bytes(&bytes[range]); } } } + } + + let mut chunk_biome_buf = vec![]; + + messages.query_local(old_view, |msg, range| match msg { + valence_layer::chunk::LocalMsg::PacketAt { .. } => { + client.write_packet_bytes(&bytes[range]); + } + valence_layer::chunk::LocalMsg::ChangeBiome { pos } => { + chunk_biome_buf.push(ChunkBiome { + pos, + data: &bytes[range], + }); + } + }); + + if !chunk_biome_buf.is_empty() { + client.write_packet(&ChunkBiomeDataS2c { + chunks: chunk_biome_buf.into(), + }); + } + } + + for &layer_id in &old_entity_layers.0 { + if let Ok(layer) = entity_layers.get(layer_id) { + let messages = layer.messages(); + let bytes = messages.bytes(); - // Send entity despawn packets for entities exiting the client's view. - for &(entity, dest_pos) in chunk.outgoing_entities() { - if dest_pos.map_or(true, |p| !view.contains(p)) { - // The outgoing entity moved outside the view distance, so it must be - // despawned. - if let Ok(id) = entity_ids.get(entity) { - remove_buf.push(id.get()); + for (msg, range) in messages.iter_global() { + match msg { + valence_layer::entity::GlobalMsg::Packet => { + client.write_packet_bytes(&bytes[range]); + } + valence_layer::entity::GlobalMsg::PacketExcept { except } => { + if self_entity != except { + client.write_packet_bytes(&bytes[range]); + } } } } - match chunk.state() { - ChunkState::Added | ChunkState::Overwrite => { - // Chunk was added or overwritten this tick. Send the packet to - // initialize the chunk. - chunk.write_init_packets(&mut *client, pos, inst.info()); + messages.query_local(old_view, |msg, range| match msg { + valence_layer::entity::LocalMsg::PacketAt { pos: _ } => { + client.write_packet_bytes(&bytes[range]); } - ChunkState::AddedRemoved => { - // Chunk was added and removed this tick, so there's - // nothing that needs to be sent. + valence_layer::entity::LocalMsg::PacketAtExcept { pos: _, except } => { + if self_entity != except { + client.write_packet_bytes(&bytes[range]); + } } - ChunkState::Removed => { - // Chunk was removed this tick, so send the packet to deinitialize the - // chunk and despawn all the contained entities. - client.write_packet(&UnloadChunkS2c { pos }); - - for entity in chunk.entities() { - // Skip the client's own entity. - if entity != self_entity { - if let Ok(id) = entity_ids.get(entity) { - remove_buf.push(id.get()); + valence_layer::entity::LocalMsg::SpawnEntity { pos: _, src_layer } => { + if !old_entity_layers.0.contains(&src_layer) { + let mut bytes = &bytes[range]; + + while let Ok(u64) = bytes.read_u64::() { + let entity = Entity::from_bits(u64); + + if self_entity != entity { + if let Ok((init, old_pos)) = entities.get(entity) { + // Spawn at the entity's old position since we may get a + // relative movement packet for this entity in a later + // iteration of the loop. + init.write_init_packets(old_pos.get(), &mut *client); + } + } + } + } + } + valence_layer::entity::LocalMsg::SpawnEntityTransition { + pos: _, + src_pos, + } => { + if !old_view.contains(src_pos) { + let mut bytes = &bytes[range]; + + while let Ok(u64) = bytes.read_u64::() { + let entity = Entity::from_bits(u64); + + if self_entity != entity { + if let Ok((init, old_pos)) = entities.get(entity) { + // Spawn at the entity's old position since we may get a + // relative movement packet for this entity in a later + // iteration of the loop. + init.write_init_packets(old_pos.get(), &mut *client); + } } } } } - ChunkState::Normal => { - // Send the data to update this chunk as normal. - - // Send all data in the chunk's packet buffer to this client. This will - // update entities in the chunk, update the - // chunk itself, and send any other packet - // data that was added in the buffer by users. - if pos == new_chunk_pos && loc == old_loc { - // Skip range of bytes for the client's own entity. - client - .write_packet_bytes(&chunk.packet_buf()[..byte_range.0.start]); - client.write_packet_bytes(&chunk.packet_buf()[byte_range.0.end..]); - } else { - client.write_packet_bytes(chunk.packet_buf()); + valence_layer::entity::LocalMsg::DespawnEntity { pos: _, dest_layer } => { + if !old_entity_layers.0.contains(&dest_layer) { + let mut bytes = &bytes[range]; + + while let Ok(id) = bytes.read_i32::() { + if self_entity_id.get() != id { + remove_buf.push(id); + } + } } } - } + valence_layer::entity::LocalMsg::DespawnEntityTransition { + pos: _, + dest_pos, + } => { + if !old_view.contains(dest_pos) { + let mut bytes = &bytes[range]; + + while let Ok(id) = bytes.read_i32::() { + if self_entity_id.get() != id { + remove_buf.push(id); + } + } + } + } + }); } } }, ); } -/// Updates the clients' view, i.e. the set of chunks that are visible from the -/// client's chunk position. -/// -/// This handles the situation when a client changes instances or chunk -/// position. It must run after [`read_data_in_old_view`]. -fn update_view( +// fn read_data_in_old_view( +// mut clients: Query<( +// Entity, +// &mut Client, +// &mut EntityRemoveBuf, +// &EntityLayerId, +// &OldEntityLayerId, +// &Position, +// &OldPosition, +// &OldViewDistance, +// &PacketByteRange, +// )>, +// instances: Query<&Instance>, +// entities: Query<(EntityInitQuery, &OldPosition)>, +// entity_ids: Query<&EntityId>, +// ) { clients.par_iter_mut().for_each_mut( |( self_entity, mut client, mut +// remove_buf, loc, old_loc, pos, old_pos, old_view_dist, byte_range, )| { let +// Ok(inst) = instances.get(old_loc.get()) else { return; }; + +// // Send instance-wide packet data. +// client.write_packet_bytes(inst.packet_buf()); + +// // TODO: cache the chunk position? +// let old_chunk_pos = old_pos.chunk_pos(); +// let new_chunk_pos = pos.chunk_pos(); + +// let view = ChunkView::new(old_chunk_pos, old_view_dist.0); + +// // Iterate over all visible chunks from the previous tick. +// for pos in view.iter() { +// if let Some(chunk) = inst.chunk(pos) { +// // Mark this chunk as being in view of a client. +// chunk.set_viewed(); + +// // Send entity spawn packets for entities entering the +// client's view. for &(entity, src_pos) in +// chunk.incoming_entities() { if src_pos.map_or(true, +// |p| !view.contains(p)) { // The incoming entity +// originated from outside the view distance, so it +// // must be spawned. if let Ok((entity, old_pos)) +// = entities.get(entity) { // Notice we are +// spawning the entity at its old position rather than +// // the current position. This is because the client could also +// // receive update packets for this entity this tick, which may +// // include a relative entity movement. +// entity.write_init_packets(old_pos.get(), &mut client.enc); +// } } +// } + +// // Send entity despawn packets for entities exiting the +// client's view. for &(entity, dest_pos) in +// chunk.outgoing_entities() { if dest_pos.map_or(true, +// |p| !view.contains(p)) { // The outgoing entity +// moved outside the view distance, so it must be // +// despawned. if let Ok(id) = entity_ids.get(entity) +// { remove_buf.push(id.get()); +// } +// } +// } + +// match chunk.state() { +// ChunkState::Added | ChunkState::Overwrite => { +// // Chunk was added or overwritten this tick. Send +// the packet to // initialize the chunk. +// chunk.write_init_packets(&mut *client, pos, +// inst.info()); } +// ChunkState::AddedRemoved => { +// // Chunk was added and removed this tick, so +// there's // nothing that needs to be sent. +// } +// ChunkState::Removed => { +// // Chunk was removed this tick, so send the +// packet to deinitialize the // chunk and despawn +// all the contained entities. +// client.write_packet(&UnloadChunkS2c { pos }); + +// for entity in chunk.entities() { +// // Skip the client's own entity. +// if entity != self_entity { +// if let Ok(id) = entity_ids.get(entity) { +// remove_buf.push(id.get()); +// } +// } +// } +// } +// ChunkState::Normal => { +// // Send the data to update this chunk as normal. + +// // Send all data in the chunk's packet buffer to +// this client. This will // update entities in the +// chunk, update the // chunk itself, and send any +// other packet // data that was added in the buffer +// by users. if pos == new_chunk_pos && loc == +// old_loc { // Skip range of bytes for the +// client's own entity. client +// +// .write_packet_bytes(&chunk.packet_buf()[..byte_range.0.start]); +// client.write_packet_bytes(&chunk.packet_buf()[byte_range.0.end..]); +// } else { +// +// client.write_packet_bytes(chunk.packet_buf()); } +// } +// } +// } +// } +// }, +// ); +// } + +fn update_view_and_layers( mut clients: Query< ( Entity, &mut Client, &mut EntityRemoveBuf, - &EntityLayerId, - &OldEntityLayerId, + &VisibleChunkLayer, + &mut OldVisibleChunkLayer, + &VisibleEntityLayers, + &mut OldVisibleEntityLayers, &Position, &OldPosition, &ViewDistance, &OldViewDistance, ), Or<( - Changed, + Changed, + Changed, Changed, Changed, )>, >, - instances: Query<&Instance>, - entities: Query<(EntityInitQuery, &Position)>, + chunk_layers: Query<&ChunkLayer>, + entity_layers: Query<&EntityLayer>, entity_ids: Query<&EntityId>, + entity_init: Query<(EntityInitQuery, &Position)>, ) { clients.par_iter_mut().for_each_mut( |( self_entity, mut client, mut remove_buf, - loc, - old_loc, + chunk_layer, + mut old_chunk_layer, + visible_entity_layers, + mut old_visible_entity_layers, pos, old_pos, view_dist, @@ -981,30 +1012,47 @@ fn update_view( }); } - // Was the client's instance changed? - if loc.0 != old_loc.get() { - if let Ok(old_inst) = instances.get(old_loc.get()) { - // TODO: only send unload packets when old dimension == new dimension, since the - // client will do the unloading for us in that case? - - // Unload all chunks and entities in the old view. + // Was the client's chunk layer changed? + if old_chunk_layer.0 != chunk_layer.0 { + // Unload all chunks in the old view. + if let Ok(layer) = chunk_layers.get(old_chunk_layer.0) { for pos in old_view.iter() { - if let Some(chunk) = old_inst.chunk(pos) { + if let Some(chunk) = layer.chunk(pos) { // Unload the chunk if its state is not "removed", since we already - // unloaded "removed" chunks earlier. + // unloaded "removed" chunks while we were handling layer messages. if chunk.state() != ChunkState::Removed && chunk.state() != ChunkState::AddedRemoved { - // Unload the chunk. client.write_packet(&UnloadChunkS2c { pos }); + } - // Unload all the entities in the chunk. - for entity in chunk.entities() { - // Skip the client's own entity. - if entity != self_entity { - if let Ok(entity_id) = entity_ids.get(entity) { - remove_buf.push(entity_id.get()); - } + chunk.dec_viewer_count(); + } + } + } + + // Load all chunks in the new view. + if let Ok(layer) = chunk_layers.get(chunk_layer.0) { + for pos in view.iter() { + if let Some(chunk) = layer.chunk(pos) { + if chunk.state() != ChunkState::Removed + && chunk.state() != ChunkState::AddedRemoved + { + chunk.write_init_packets(&mut *client, pos, layer.info()); + } + } + } + } + + // Unload all entities from the old view in all old visible entity layers. + // TODO: can we skip this step if old dimension != new dimension? + for &layer in &old_visible_entity_layers.0 { + if let Ok(layer) = entity_layers.get(layer) { + for pos in old_view.iter() { + for entity in layer.entities_at(pos) { + if self_entity != entity { + if let Ok(id) = entity_ids.get(entity) { + remove_buf.push(id.get()); } } } @@ -1012,53 +1060,112 @@ fn update_view( } } - if let Ok(inst) = instances.get(loc.0) { - // Load all chunks and entities in new view. - for pos in view.iter() { - if let Some(chunk) = inst.chunk(pos) { - // Mark this chunk as being in view of a client. - chunk.set_viewed(); - - // Load the chunk if it's not already removed. - chunk.write_init_packets(&mut *client, pos, inst.info()); - - // Load all the entities in this chunk. - for entity in chunk.entities() { - // Skip client's own entity. - if entity != self_entity { - if let Ok((entity, pos)) = entities.get(entity) { - entity.write_init_packets(pos.get(), &mut *client); + // Load all entities in the new view from all new visible entity layers. + for &layer in &visible_entity_layers.0 { + if let Ok(layer) = entity_layers.get(layer) { + for pos in view.iter() { + for entity in layer.entities_at(pos) { + if self_entity != entity { + if let Ok((init, pos)) = entity_init.get(entity) { + init.write_init_packets(pos.get(), &mut *client); + } + } + } + } + } + } + + // Update the old chunk layer. + old_chunk_layer.0 = chunk_layer.0; + } else { + // Update the client's visible entity layers. + if old_visible_entity_layers.0 != visible_entity_layers.0 { + // Unload all entity layers that are no longer visible in the old view. + for &layer in old_visible_entity_layers + .0 + .difference(&visible_entity_layers.0) + { + if let Ok(layer) = entity_layers.get(layer) { + for pos in old_view.iter() { + for entity in layer.entities_at(pos) { + if self_entity != entity { + if let Ok(id) = entity_ids.get(entity) { + remove_buf.push(id.get()); + } + } + } + } + } + } + + // Load all entity layers that are newly visible in the old view. + for &layer in visible_entity_layers + .0 + .difference(&old_visible_entity_layers.0) + { + if let Ok(layer) = entity_layers.get(layer) { + for pos in old_view.iter() { + for entity in layer.entities_at(pos) { + if self_entity != entity { + if let Ok((init, pos)) = entity_init.get(entity) { + init.write_init_packets(pos.get(), &mut *client); + } } } } } } - } else { - debug!("Client entered nonexistent instance ({loc:?})."); + + // old := new + old_visible_entity_layers + .0 + .clone_from(&visible_entity_layers.0); } - } else if old_view != view { - // Client changed their view without changing the instance. - if let Ok(inst) = instances.get(loc.0) { + // Update the client's view (chunk position and view distance) + if old_view != view { // Unload chunks and entities in the old view and load chunks and entities in // the new view. We don't need to do any work where the old and new view // overlap. - for pos in old_view.diff(view) { - if let Some(chunk) = inst.chunk(pos) { - // Unload the chunk if its state is not "removed", since we already - // unloaded "removed" chunks earlier. - if chunk.state() != ChunkState::Removed - && chunk.state() != ChunkState::AddedRemoved - { - // Unload the chunk. - client.write_packet(&UnloadChunkS2c { pos }); - // Unload all the entities in the chunk. - for entity in chunk.entities() { - // Skip client's own entity. - if entity != self_entity { - if let Ok(entity_id) = entity_ids.get(entity) { - remove_buf.push(entity_id.get()); + // Unload chunks in the old view. + if let Ok(layer) = chunk_layers.get(chunk_layer.0) { + for pos in old_view.diff(view) { + if let Some(chunk) = layer.chunk(pos) { + // Unload the chunk if its state is not "removed", since we already + // unloaded "removed" chunks while we were handling layer messages. + if chunk.state() != ChunkState::Removed + && chunk.state() != ChunkState::AddedRemoved + { + client.write_packet(&UnloadChunkS2c { pos }); + } + + chunk.dec_viewer_count(); + } + } + } + + // Load chunks in the new view. + if let Ok(layer) = chunk_layers.get(chunk_layer.0) { + for pos in view.diff(old_view) { + if let Some(chunk) = layer.chunk(pos) { + if chunk.state() != ChunkState::Removed + && chunk.state() != ChunkState::AddedRemoved + { + chunk.write_init_packets(&mut *client, pos, layer.info()); + } + } + } + } + + // Unload entities from the new visible layers (since we updated it above). + for &layer in &visible_entity_layers.0 { + if let Ok(layer) = entity_layers.get(layer) { + for pos in old_view.diff(view) { + for entity in layer.entities_at(pos) { + if self_entity != entity { + if let Ok(id) = entity_ids.get(entity) { + remove_buf.push(id.get()); } } } @@ -1066,24 +1173,14 @@ fn update_view( } } - for pos in view.diff(old_view) { - if let Some(chunk) = inst.chunk(pos) { - // Load the chunk unless it's already unloaded. - if chunk.state() != ChunkState::Removed - && chunk.state() != ChunkState::AddedRemoved - { - // Mark this chunk as being in view of a client. - chunk.set_viewed(); - - // Load the chunk. - chunk.write_init_packets(&mut *client, pos, inst.info()); - - // Load all the entities in this chunk. - for entity in chunk.entities() { - // Skip client's own entity. - if entity != self_entity { - if let Ok((entity, pos)) = entities.get(entity) { - entity.write_init_packets(pos.get(), &mut *client); + // Load entities from the new visible layers. + for &layer in &visible_entity_layers.0 { + if let Ok(layer) = entity_layers.get(layer) { + for pos in view.diff(old_view) { + for entity in layer.entities_at(pos) { + if self_entity != entity { + if let Ok((init, pos)) = entity_init.get(entity) { + init.write_init_packets(pos.get(), &mut *client); } } } @@ -1096,6 +1193,166 @@ fn update_view( ); } +// /// Updates the clients' view, i.e. the set of chunks that are visible from +// the /// client's chunk position. +// /// +// /// This handles the situation when a client changes instances or chunk +// /// position. It must run after [`read_data_in_old_view`]. +// fn update_view( +// mut clients: Query< +// ( +// Entity, +// &mut Client, +// &mut EntityRemoveBuf, +// &EntityLayerId, +// &OldEntityLayerId, +// &Position, +// &OldPosition, +// &ViewDistance, +// &OldViewDistance, +// ), +// Or<( +// Changed, +// Changed, +// Changed, +// )>, +// >, +// instances: Query<&Instance>, +// entities: Query<(EntityInitQuery, &Position)>, +// entity_ids: Query<&EntityId>, +// ) { clients.par_iter_mut().for_each_mut( |( self_entity, mut client, mut +// remove_buf, loc, old_loc, pos, old_pos, view_dist, old_view_dist, )| { let +// view = ChunkView::new(ChunkPos::from_dvec3(pos.0), view_dist.0); let +// old_view = ChunkView::new(ChunkPos::from_dvec3(old_pos.get()), +// old_view_dist.0); + +// // Make sure the center chunk is set before loading chunks! +// Otherwise the client // may ignore the chunk. +// if old_view.pos != view.pos { +// client.write_packet(&ChunkRenderDistanceCenterS2c { +// chunk_x: VarInt(view.pos.x), +// chunk_z: VarInt(view.pos.z), +// }); +// } + +// // Was the client's instance changed? +// if loc.0 != old_loc.get() { +// if let Ok(old_inst) = instances.get(old_loc.get()) { +// // TODO: only send unload packets when old dimension == +// new dimension, since the // client will do the +// unloading for us in that case? + +// // Unload all chunks and entities in the old view. +// for pos in old_view.iter() { +// if let Some(chunk) = old_inst.chunk(pos) { +// // Unload the chunk if its state is not +// "removed", since we already // unloaded "removed" +// chunks earlier. if chunk.state() != +// ChunkState::Removed && chunk.state() != +// ChunkState::AddedRemoved { +// // Unload the chunk. +// client.write_packet(&UnloadChunkS2c { pos }); + +// // Unload all the entities in the chunk. +// for entity in chunk.entities() { +// // Skip the client's own entity. +// if entity != self_entity { +// if let Ok(entity_id) = +// entity_ids.get(entity) { +// remove_buf.push(entity_id.get()); } +// } +// } +// } +// } +// } +// } + +// if let Ok(inst) = instances.get(loc.0) { +// // Load all chunks and entities in new view. +// for pos in view.iter() { +// if let Some(chunk) = inst.chunk(pos) { +// // Mark this chunk as being in view of a client. +// chunk.set_viewed(); + +// // Load the chunk if it's not already removed. +// chunk.write_init_packets(&mut *client, pos, +// inst.info()); + +// // Load all the entities in this chunk. +// for entity in chunk.entities() { +// // Skip client's own entity. +// if entity != self_entity { +// if let Ok((entity, pos)) = +// entities.get(entity) { +// entity.write_init_packets(pos.get(), &mut *client); +// } } +// } +// } +// } +// } else { +// debug!("Client entered nonexistent instance ({loc:?})."); +// } +// } else if old_view != view { +// // Client changed their view without changing the instance. + +// if let Ok(inst) = instances.get(loc.0) { +// // Unload chunks and entities in the old view and load +// chunks and entities in // the new view. We don't need to +// do any work where the old and new view // overlap. +// for pos in old_view.diff(view) { +// if let Some(chunk) = inst.chunk(pos) { +// // Unload the chunk if its state is not +// "removed", since we already // unloaded "removed" +// chunks earlier. if chunk.state() != +// ChunkState::Removed && chunk.state() != +// ChunkState::AddedRemoved { +// // Unload the chunk. +// client.write_packet(&UnloadChunkS2c { pos }); + +// // Unload all the entities in the chunk. +// for entity in chunk.entities() { +// // Skip client's own entity. +// if entity != self_entity { +// if let Ok(entity_id) = +// entity_ids.get(entity) { +// remove_buf.push(entity_id.get()); } +// } +// } +// } +// } +// } + +// for pos in view.diff(old_view) { +// if let Some(chunk) = inst.chunk(pos) { +// // Load the chunk unless it's already unloaded. +// if chunk.state() != ChunkState::Removed +// && chunk.state() != ChunkState::AddedRemoved +// { +// // Mark this chunk as being in view of a +// client. chunk.set_viewed(); + +// // Load the chunk. +// chunk.write_init_packets(&mut *client, pos, +// inst.info()); + +// // Load all the entities in this chunk. +// for entity in chunk.entities() { +// // Skip client's own entity. +// if entity != self_entity { +// if let Ok((entity, pos)) = +// entities.get(entity) { +// entity.write_init_packets(pos.get(), &mut *client); +// } } +// } +// } +// } +// } +// } +// } +// }, +// ); +// } + /// Removes all the entities that are queued to be removed for each client. fn remove_entities( mut clients: Query<(&mut Client, &mut EntityRemoveBuf), Changed>, @@ -1133,21 +1390,6 @@ fn update_old_view_dist( } } -/// Sets the client's respawn and compass position. -/// -/// This also closes the "downloading terrain" screen when first joining, so -/// it should happen after the initial chunks are written. -fn update_respawn_position( - mut clients: Query<(&mut Client, &RespawnPosition), Changed>, -) { - for (mut client, respawn_pos) in &mut clients { - client.write_packet(&PlayerSpawnPositionS2c { - position: respawn_pos.pos, - angle: respawn_pos.yaw, - }); - } -} - fn flush_packets( mut clients: Query<(Entity, &mut Client), Changed>, mut commands: Commands, diff --git a/crates/valence_client/src/packet.rs b/crates/valence_client/src/packet.rs index c549b2456..ee97e4b42 100644 --- a/crates/valence_client/src/packet.rs +++ b/crates/valence_client/src/packet.rs @@ -11,7 +11,6 @@ use valence_core::direction::Direction; use valence_core::game_mode::GameMode; use valence_core::hand::Hand; use valence_core::ident::Ident; -use valence_core::protocol::byte_angle::ByteAngle; use valence_core::protocol::global_pos::GlobalPos; use valence_core::protocol::var_int::VarInt; use valence_core::protocol::var_long::VarLong; diff --git a/crates/valence_client/src/spawn.rs b/crates/valence_client/src/spawn.rs new file mode 100644 index 000000000..ca798ff15 --- /dev/null +++ b/crates/valence_client/src/spawn.rs @@ -0,0 +1,206 @@ +//! Handles spawning and respawning the client. + +use valence_registry::codec::RegistryCodec; +use valence_registry::tags::TagsRegistry; + +use super::*; + +// Components for the join game and respawn packet. + +#[derive(Component, Clone, PartialEq, Eq, Default, Debug)] +pub struct DeathLocation(pub Option<(Ident, BlockPos)>); + +#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)] +pub struct IsHardcore(pub bool); + +/// Hashed world seed used for biome noise. +#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)] +pub struct HashedSeed(pub u64); + +#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)] +pub struct ReducedDebugInfo(pub bool); + +#[derive(Component, Copy, Clone, PartialEq, Eq, Debug)] +pub struct HasRespawnScreen(pub bool); + +/// If the client is spawning into a debug world. +#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)] +pub struct IsDebug(pub bool); + +/// Changes the perceived horizon line (used for superflat worlds). +#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)] +pub struct IsFlat(pub bool); + +#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)] +pub struct PortalCooldown(pub i32); + +/// The initial previous gamemode. Used for the F3+F4 gamemode switcher. +#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)] +pub struct PrevGameMode(pub Option); + +impl Default for HasRespawnScreen { + fn default() -> Self { + Self(true) + } +} + +/// A convenient [`WorldQuery`] for obtaining client spawn components. Also see +/// [`ClientSpawnQueryReadOnly`]. +#[derive(WorldQuery)] +#[world_query(mutable)] +pub struct ClientSpawnQuery { + pub is_hardcore: &'static mut IsHardcore, + pub game_mode: &'static mut GameMode, + pub prev_game_mode: &'static mut PrevGameMode, + pub hashed_seed: &'static mut HashedSeed, + pub view_distance: &'static mut ViewDistance, + pub reduced_debug_info: &'static mut ReducedDebugInfo, + pub has_respawn_screen: &'static mut HasRespawnScreen, + pub is_debug: &'static mut IsDebug, + pub is_flat: &'static mut IsFlat, + pub death_loc: &'static mut DeathLocation, + pub portal_cooldown: &'static mut PortalCooldown, +} + +// #[derive(WorldQuery)] +// #[world_query(mutable)] +// struct ClientJoinQuery { +// entity: Entity, +// client: &'static mut Client, +// visible_chunk_layer: &'static VisibleChunkLayer, +// pos: &'static Position, +// is_hardcore: &'static IsHardcore, +// game_mode: &'static GameMode, +// prev_game_mode: &'static PrevGameMode, +// hashed_seed: &'static HashedSeed, +// view_distance: &'static ViewDistance, +// reduced_debug_info: &'static ReducedDebugInfo, +// has_respawn_screen: &'static HasRespawnScreen, +// is_debug: &'static IsDebug, +// is_flat: &'static IsFlat, +// death_loc: &'static DeathLocation, +// portal_cooldown: &'static PortalCooldown, +// } + +pub(super) fn initial_join( + codec: Res, + tags: Res, + mut clients: Query<(&mut Client, &VisibleChunkLayer, ClientSpawnQueryReadOnly), Added>, + chunk_layers: Query<&ChunkLayer>, +) { + for (mut client, visible_chunk_layer, spawn) in &mut clients { + let Ok(chunk_layer) = chunk_layers.get(visible_chunk_layer.0) else { + continue + }; + + let dimension_names: Vec>> = codec + .registry(BiomeRegistry::KEY) + .iter() + .map(|value| value.name.as_str_ident().into()) + .collect(); + + let dimension_name: Ident> = chunk_layer.dimension_type_name().into(); + + let last_death_location = spawn.death_loc.0.as_ref().map(|(id, pos)| GlobalPos { + dimension_name: id.as_str_ident().into(), + position: *pos, + }); + + // The login packet is prepended so that it's sent before all the other packets. + // Some packets don't work corectly when sent before the game join packet. + _ = client.enc.prepend_packet(&GameJoinS2c { + entity_id: 0, // We reserve ID 0 for clients. + is_hardcore: spawn.is_hardcore.0, + game_mode: *spawn.game_mode, + previous_game_mode: spawn.prev_game_mode.0.map(|g| g as i8).unwrap_or(-1), + dimension_names, + registry_codec: Cow::Borrowed(codec.cached_codec()), + dimension_type_name: dimension_name.clone(), + dimension_name, + hashed_seed: spawn.hashed_seed.0 as i64, + max_players: VarInt(0), // Ignored by clients. + view_distance: VarInt(spawn.view_distance.0 as i32), + simulation_distance: VarInt(16), // TODO. + reduced_debug_info: spawn.reduced_debug_info.0, + enable_respawn_screen: spawn.has_respawn_screen.0, + is_debug: spawn.is_debug.0, + is_flat: spawn.is_flat.0, + last_death_location, + portal_cooldown: VarInt(spawn.portal_cooldown.0), + }); + + client.enc.append_bytes(tags.sync_tags_packet()); + + /* + // TODO: enable all the features? + q.client.write_packet(&FeatureFlags { + features: vec![Ident::new("vanilla").unwrap()], + })?; + */ + } +} + +pub(super) fn respawn( + mut clients: Query< + ( + &mut Client, + &EntityLayerId, + &DeathLocation, + &HashedSeed, + &GameMode, + &PrevGameMode, + &IsDebug, + &IsFlat, + ), + Changed, + >, + chunk_layers: Query<&ChunkLayer>, +) { + for (mut client, loc, death_loc, hashed_seed, game_mode, prev_game_mode, is_debug, is_flat) in + &mut clients + { + if client.is_added() { + // No need to respawn since we are sending the game join packet this tick. + continue; + } + + let Ok(chunk_layer) = chunk_layers.get(loc.0) else { + continue + }; + + let dimension_name = chunk_layer.dimension_type_name(); + + let last_death_location = death_loc.0.as_ref().map(|(id, pos)| GlobalPos { + dimension_name: id.as_str_ident().into(), + position: *pos, + }); + + client.write_packet(&PlayerRespawnS2c { + dimension_type_name: dimension_name.into(), + dimension_name: dimension_name.into(), + hashed_seed: hashed_seed.0, + game_mode: *game_mode, + previous_game_mode: prev_game_mode.0.map(|g| g as i8).unwrap_or(-1), + is_debug: is_debug.0, + is_flat: is_flat.0, + copy_metadata: true, + last_death_location, + portal_cooldown: VarInt(0), // TODO + }); + } +} + +/// Sets the client's respawn and compass position. +/// +/// This also closes the "downloading terrain" screen when first joining, so +/// it should happen after the initial chunks are written. +pub(super) fn update_respawn_position( + mut clients: Query<(&mut Client, &RespawnPosition), Changed>, +) { + for (mut client, respawn_pos) in &mut clients { + client.write_packet(&PlayerSpawnPositionS2c { + position: respawn_pos.pos, + angle: respawn_pos.yaw, + }); + } +} diff --git a/crates/valence_client/src/teleport.rs b/crates/valence_client/src/teleport.rs index 1c18848b2..81bf54cf6 100644 --- a/crates/valence_client/src/teleport.rs +++ b/crates/valence_client/src/teleport.rs @@ -5,12 +5,13 @@ use valence_core::protocol::{packet_id, Decode, Encode, Packet}; use super::*; use crate::event_loop::{EventLoopPreUpdate, PacketEvent}; +use crate::spawn::update_respawn_position; pub(super) fn build(app: &mut App) { app.add_systems( PostUpdate, teleport - .after(update_view) + .after(update_view_and_layers) .before(update_respawn_position) .in_set(UpdateClientsSet), ) diff --git a/crates/valence_client/src/weather.rs b/crates/valence_client/src/weather.rs index b77564fb9..a7a8e5ff3 100644 --- a/crates/valence_client/src/weather.rs +++ b/crates/valence_client/src/weather.rs @@ -15,6 +15,7 @@ //! New joined players are handled, so that they are get weather events from //! the instance. +/* use super::*; use crate::packet::{GameEventKind, GameStateChangeS2c}; @@ -212,3 +213,4 @@ fn thunder_end_per_client( } } } +*/ diff --git a/crates/valence_layer/src/chunk.rs b/crates/valence_layer/src/chunk.rs index 400baad4f..1874299ee 100644 --- a/crates/valence_layer/src/chunk.rs +++ b/crates/valence_layer/src/chunk.rs @@ -20,15 +20,13 @@ use valence_core::Server; use valence_dimension::DimensionTypeRegistry; use valence_nbt::Compound; +pub use self::chunk::{MAX_HEIGHT, *}; +pub use self::loaded::LoadedChunk; +pub use self::unloaded::UnloadedChunk; use crate::bvh::GetChunkPos; -use crate::chunk::chunk::MAX_HEIGHT; use crate::message::Messages; use crate::{Layer, UpdateLayersPreClient}; -pub use self::chunk::*; -pub use self::loaded::LoadedChunk; -pub use self::unloaded::UnloadedChunk; - #[derive(Component, Debug)] pub struct ChunkLayer { messages: ChunkLayerMessages, @@ -55,8 +53,9 @@ type ChunkLayerMessages = Messages; pub enum GlobalMsg { /// Send packet data to all clients viewing the layer. Packet, - /// Send packet data to all clients viewing layer, except the client identified by `except`. - PacketExcept, + /// Send packet data to all clients viewing the layer, except the client + /// identified by `except`. + PacketExcept { except: Entity }, } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] @@ -464,9 +463,7 @@ fn update_chunks_pre_client(mut layers: Query<&mut ChunkLayer>) { fn update_chunks_post_client(mut layers: Query<&mut ChunkLayer>) { for mut layer in &mut layers { - layer - .chunks - .retain(|&pos, chunk| chunk.update_post_client(pos)); + layer.chunks.retain(|_, chunk| chunk.update_post_client()); layer.messages.unready(); } diff --git a/crates/valence_layer/src/chunk/chunk.rs b/crates/valence_layer/src/chunk/chunk.rs index 8eb71daf6..4e39f6f59 100644 --- a/crates/valence_layer/src/chunk/chunk.rs +++ b/crates/valence_layer/src/chunk/chunk.rs @@ -314,9 +314,9 @@ pub(super) const fn bit_width(n: usize) -> usize { #[cfg(test)] mod tests { - use crate::chunk::{loaded::LoadedChunk, unloaded::UnloadedChunk}; - use super::*; + use crate::chunk::loaded::LoadedChunk; + use crate::chunk::unloaded::UnloadedChunk; #[test] fn chunk_get_set() { diff --git a/crates/valence_layer/src/chunk/loaded.rs b/crates/valence_layer/src/chunk/loaded.rs index 4abf09859..4f4c3bbf9 100644 --- a/crates/valence_layer/src/chunk/loaded.rs +++ b/crates/valence_layer/src/chunk/loaded.rs @@ -30,7 +30,9 @@ use crate::packet::{ #[derive(Debug)] pub struct LoadedChunk { state: ChunkState, - /// A count of the clients viewing this chunk. Useful for knowing if it's necessary to record changes, since no client would be in view to receive the changes if this were nonzero. + /// A count of the clients viewing this chunk. Useful for knowing if it's + /// necessary to record changes, since no client would be in view to receive + /// the changes if this were nonzero. viewer_count: AtomicU32, /// Block and biome data for the chunk. sections: Box<[Section]>, @@ -223,8 +225,7 @@ impl LoadedChunk { /// Performs the changes necessary to prepare this chunk for client updates. /// Notably: /// - Message is sent to spawn or despawn the chunk. - /// - Chunk update packets are written to this chunk's packet - /// buffer. + /// - Chunk update packets are written to this chunk's packet buffer. /// - Recorded changes are cleared. pub(crate) fn update_pre_client( &mut self, @@ -355,7 +356,7 @@ impl LoadedChunk { } /// Returns if the chunk should be retained. - pub(crate) fn update_post_client(&mut self, pos: ChunkPos) -> bool { + pub(crate) fn update_post_client(&mut self) -> bool { // Changes were already cleared in `update_pre_client`. self.assert_no_changes(); @@ -364,7 +365,15 @@ impl LoadedChunk { self.state = ChunkState::Normal; true } - ChunkState::Removed | ChunkState::AddedRemoved => false, + ChunkState::Removed | ChunkState::AddedRemoved => { + debug_assert_eq!( + *self.viewer_count.get_mut(), + 0, + "chunk viewer count should be zero by the time it's removed" + ); + + false + } } } diff --git a/crates/valence_layer/src/entity.rs b/crates/valence_layer/src/entity.rs index dc7effdd0..23858a19c 100644 --- a/crates/valence_layer/src/entity.rs +++ b/crates/valence_layer/src/entity.rs @@ -23,30 +23,35 @@ pub struct EntityLayer { compression_threshold: Option, } -#[doc(hidden)] -pub type EntityLayerMessages = Messages; +type EntityLayerMessages = Messages; #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub enum GlobalMsg { /// Send packet data to all clients viewing the layer. Packet, - /// Send packet data to all clients viewing layer, except the client identified by `except`. - PacketExcept, + /// Send packet data to all clients viewing layer, except the client + /// identified by `except`. + PacketExcept { except: Entity }, } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub enum LocalMsg { + /// Spawn entities if the client is not already viewing `src_layer`. Message + /// data is the serialized form of [`Entity`]. + SpawnEntity { pos: ChunkPos, src_layer: Entity }, + /// Spawn entities if the client is not in view of `src_pos`. Message data + /// is the serialized form of [`Entity`]. + SpawnEntityTransition { pos: ChunkPos, src_pos: ChunkPos }, /// Send packet data to all clients viewing the layer in view of `pos`. PacketAt { pos: ChunkPos }, - /// Send packet data to all clients viewing the layer in view of `pos`, except the client identified by `except`. + /// Send packet data to all clients viewing the layer in view of `pos`, + /// except the client identified by `except`. PacketAtExcept { pos: ChunkPos, except: Entity }, - /// Spawn entities if the client is not already viewing `src_layer`. Message data is the serialized form of [`Entity`]. - SpawnEntity { pos: ChunkPos, src_layer: Entity }, - /// Spawn entities if the client is not in view of `src_pos`. Message data is the serialized form of [`Entity`]. - SpawnEntityTransition { pos: ChunkPos, src_pos: ChunkPos }, - /// Despawn entities if the client is not already viewing `dest_layer`. Message data is the serialized form of `EntityId`. + /// Despawn entities if the client is not already viewing `dest_layer`. + /// Message data is the serialized form of `EntityId`. DespawnEntity { pos: ChunkPos, dest_layer: Entity }, - /// Despawn entities if the client is not in view of `dest_pos`. Message data is the serialized form of `EntityId`. + /// Despawn entities if the client is not in view of `dest_pos`. Message + /// data is the serialized form of `EntityId`. DespawnEntityTransition { pos: ChunkPos, dest_pos: ChunkPos }, } @@ -83,6 +88,11 @@ impl EntityLayer { .into_iter() .flat_map(|entities| entities.iter().copied()) } + + #[doc(hidden)] + pub fn messages(&self) -> &EntityLayerMessages { + &self.messages + } } impl Layer for EntityLayer { diff --git a/crates/valence_layer/src/lib.rs b/crates/valence_layer/src/lib.rs index a52e6ad1d..e86d6c1cd 100644 --- a/crates/valence_layer/src/lib.rs +++ b/crates/valence_layer/src/lib.rs @@ -49,11 +49,14 @@ impl Default for LayerPlugin { } } -/// When entity and chunk changes are written to layers. Systems that modify chunks and entities should run _before_ this. Systems that need to read layer messages should run _after_ this. +/// When entity and chunk changes are written to layers. Systems that modify +/// chunks and entities should run _before_ this. Systems that need to read +/// layer messages should run _after_ this. #[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] pub struct UpdateLayersPreClient; -/// When layers are cleared and messages from this tick are lost. Systems that read layer messages should run _before_ this. +/// When layers are cleared and messages from this tick are lost. Systems that +/// read layer messages should run _before_ this. #[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] pub struct UpdateLayersPostClient; diff --git a/crates/valence_layer/src/message.rs b/crates/valence_layer/src/message.rs index 6725bb0f2..6940af5eb 100644 --- a/crates/valence_layer/src/message.rs +++ b/crates/valence_layer/src/message.rs @@ -1,7 +1,7 @@ use core::fmt; use std::ops::Range; -use valence_core::chunk_pos::ChunkPos; +use valence_core::chunk_pos::{ChunkPos, ChunkView}; use crate::bvh::{ChunkBvh, GetChunkPos}; @@ -141,13 +141,17 @@ where pub fn iter_global(&self) -> impl Iterator)> + '_ { debug_assert!(self.is_ready); - self.global.iter().map(|(m, r)| (*m, r.start as usize..r.end as usize)) + self.global + .iter() + .map(|(m, r)| (*m, r.start as usize..r.end as usize)) } - pub fn query_local(&self) { + pub fn query_local(&self, view: ChunkView, mut f: impl FnMut(L, Range)) { debug_assert!(self.is_ready); - todo!() + self.bvh.query(view, |pair| { + f(pair.msg, pair.range.start as usize..pair.range.end as usize) + }); } } @@ -180,8 +184,8 @@ where #[derive(Debug)] struct MessagePair { - pub msg: M, - pub range: Range, + msg: M, + range: Range, } impl GetChunkPos for MessagePair { diff --git a/crates/valence_player_list/Cargo.toml b/crates/valence_player_list/Cargo.toml index 21aef41c6..baf933fb1 100644 --- a/crates/valence_player_list/Cargo.toml +++ b/crates/valence_player_list/Cargo.toml @@ -10,5 +10,5 @@ bevy_ecs.workspace = true bitfield-struct.workspace = true valence_core.workspace = true valence_client.workspace = true -valence_instance.workspace = true +valence_layer.workspace = true uuid.workspace = true diff --git a/crates/valence_world_border/Cargo.toml b/crates/valence_world_border/Cargo.toml index c2e5e2736..6179212a9 100644 --- a/crates/valence_world_border/Cargo.toml +++ b/crates/valence_world_border/Cargo.toml @@ -10,5 +10,5 @@ glam.workspace = true valence_client.workspace = true valence_core.workspace = true valence_entity.workspace = true -valence_instance.workspace = true +valence_layer.workspace = true valence_registry.workspace = true From 34cf30ae34cfd84b978ed1356697fd59d9375676 Mon Sep 17 00:00:00 2001 From: Ryan Date: Sun, 9 Jul 2023 07:25:10 -0700 Subject: [PATCH 06/47] get `bench_players` working at least --- crates/valence_anvil/src/lib.rs | 34 +-- crates/valence_anvil/src/parse_chunk.rs | 2 +- crates/valence_client/src/lib.rs | 360 ++++------------------- crates/valence_entity/build.rs | 2 +- crates/valence_entity/src/lib.rs | 4 +- crates/valence_layer/src/chunk.rs | 23 +- crates/valence_layer/src/chunk/loaded.rs | 118 ++++---- crates/valence_layer/src/entity.rs | 6 +- crates/valence_layer/src/lib.rs | 8 +- crates/valence_layer/src/message.rs | 20 +- crates/valence_player_list/src/lib.rs | 4 +- crates/valence_world_border/src/lib.rs | 8 +- src/lib.rs | 25 +- src/testing.rs | 26 +- 14 files changed, 224 insertions(+), 416 deletions(-) diff --git a/crates/valence_anvil/src/lib.rs b/crates/valence_anvil/src/lib.rs index cb9f52136..ee9eb1b7e 100644 --- a/crates/valence_anvil/src/lib.rs +++ b/crates/valence_anvil/src/lib.rs @@ -38,8 +38,8 @@ use valence_client::{Client, OldView, UpdateClientsSet, View}; use valence_core::chunk_pos::ChunkPos; use valence_core::ident::Ident; use valence_entity::{EntityLayerId, OldEntityLayerId}; -use valence_instance::chunk::UnloadedChunk; -use valence_instance::Instance; +use valence_layer::chunk::UnloadedChunk; +use valence_layer::ChunkLayer; use valence_nbt::Compound; mod parse_chunk; @@ -285,7 +285,7 @@ impl Plugin for AnvilPlugin { } } -fn init_anvil(mut query: Query<&mut AnvilLevel, (Added, With)>) { +fn init_anvil(mut query: Query<&mut AnvilLevel, (Added, With)>) { for mut level in &mut query { if let Some(state) = level.worker_state.take() { thread::spawn(move || anvil_worker(state)); @@ -298,16 +298,16 @@ fn init_anvil(mut query: Query<&mut AnvilLevel, (Added, With, + mut chunk_layers: Query<(Entity, &mut ChunkLayer, &AnvilLevel)>, mut unload_events: EventWriter, ) { - for (entity, mut inst, anvil) in &mut instances { - inst.retain_chunks(|pos, chunk| { - if chunk.is_viewed_mut() || anvil.ignored_chunks.contains(&pos) { + for (entity, mut layer, anvil) in &mut chunk_layers { + layer.retain_chunks(|pos, chunk| { + if chunk.viewer_count_mut() > 0 || anvil.ignored_chunks.contains(&pos) { true } else { unload_events.send(ChunkUnloadEvent { - instance: entity, + chunk_layer: entity, pos, }); false @@ -318,19 +318,19 @@ fn remove_unviewed_chunks( fn update_client_views( clients: Query<(&EntityLayerId, Ref, View, OldView), With>, - mut instances: Query<(&Instance, &mut AnvilLevel)>, + mut chunk_layers: Query<(&ChunkLayer, &mut AnvilLevel)>, ) { for (loc, old_loc, view, old_view) in &clients { let view = view.get(); let old_view = old_view.get(); if loc != &*old_loc || view != old_view || old_loc.is_added() { - let Ok((inst, mut anvil)) = instances.get_mut(loc.0) else { + let Ok((layer, mut anvil)) = chunk_layers.get_mut(loc.0) else { continue }; let queue_pos = |pos| { - if !anvil.ignored_chunks.contains(&pos) && inst.chunk(pos).is_none() { + if !anvil.ignored_chunks.contains(&pos) && layer.chunk(pos).is_none() { // Chunks closer to clients are prioritized. match anvil.pending.entry(pos) { Entry::Occupied(mut oe) => { @@ -358,21 +358,21 @@ fn update_client_views( } fn send_recv_chunks( - mut instances: Query<(Entity, &mut Instance, &mut AnvilLevel)>, + mut instances: Query<(Entity, &mut ChunkLayer, &mut AnvilLevel)>, mut to_send: Local>, mut load_events: EventWriter, ) { - for (entity, mut inst, anvil) in &mut instances { + for (entity, mut layer, anvil) in &mut instances { let anvil = anvil.into_inner(); - // Insert the chunks that are finished loading into the instance and send load - // events. + // Insert the chunks that are finished loading into the chunk layer and send + // load events. for (pos, res) in anvil.receiver.drain() { anvil.pending.remove(&pos); let status = match res { Ok(Some((chunk, timestamp))) => { - inst.insert_chunk(pos, chunk); + layer.insert_chunk(pos, chunk); ChunkLoadStatus::Success { timestamp } } Ok(None) => ChunkLoadStatus::Empty, @@ -450,7 +450,7 @@ pub enum ChunkLoadStatus { #[derive(Event, Debug)] pub struct ChunkUnloadEvent { /// The [`Instance`] where the chunk was unloaded. - pub instance: Entity, + pub chunk_layer: Entity, /// The position of the chunk that was unloaded. pub pos: ChunkPos, } diff --git a/crates/valence_anvil/src/parse_chunk.rs b/crates/valence_anvil/src/parse_chunk.rs index 9b7c1e35b..71aa547dc 100644 --- a/crates/valence_anvil/src/parse_chunk.rs +++ b/crates/valence_anvil/src/parse_chunk.rs @@ -6,7 +6,7 @@ use thiserror::Error; use valence_biome::BiomeId; use valence_block::{BlockKind, PropName, PropValue}; use valence_core::ident::Ident; -use valence_instance::chunk::{Chunk, UnloadedChunk}; +use valence_layer::chunk::{Chunk, UnloadedChunk}; use valence_nbt::{Compound, List, Value}; #[derive(Clone, Debug, Error)] diff --git a/crates/valence_client/src/lib.rs b/crates/valence_client/src/lib.rs index 7775ffd7f..a573a8412 100644 --- a/crates/valence_client/src/lib.rs +++ b/crates/valence_client/src/lib.rs @@ -69,7 +69,7 @@ use valence_layer::packet::{ ChunkBiome, ChunkBiomeDataS2c, ChunkLoadDistanceS2c, ChunkRenderDistanceCenterS2c, UnloadChunkS2c, }; -use valence_layer::{ChunkLayer, EntityLayer, UpdateLayersPostClient, UpdateLayersPreClient}; +use valence_layer::{ChunkLayer, EntityLayer, UpdateLayersPostClientSet, UpdateLayersPreClientSet}; use valence_registry::RegistrySet; pub mod action; @@ -117,33 +117,37 @@ impl Plugin for ClientPlugin { app.add_systems( PostUpdate, ( - spawn::initial_join.after(RegistrySet), - update_chunk_load_dist, - handle_layer_messages - .after(UpdateLayersPreClient) - .after(update_chunk_load_dist), - update_view_and_layers - .after(spawn::initial_join) - .after(handle_layer_messages), - spawn::update_respawn_position.after(update_view_and_layers), - spawn::respawn.after(spawn::update_respawn_position), - remove_entities.after(update_view_and_layers), - update_old_view_dist.after(update_view_and_layers), - update_game_mode, - update_tracked_data.after(UpdateLayersPreClient), - init_tracked_data.after(UpdateLayersPreClient), - ) - .in_set(UpdateClientsSet), + ( + spawn::initial_join.after(RegistrySet), + update_chunk_load_dist, + handle_layer_messages.after(update_chunk_load_dist), + update_view_and_layers + .after(spawn::initial_join) + .after(handle_layer_messages), + spawn::update_respawn_position.after(update_view_and_layers), + spawn::respawn.after(spawn::update_respawn_position), + remove_entities.after(update_view_and_layers), + update_old_view_dist.after(update_view_and_layers), + update_game_mode, + update_tracked_data, + init_tracked_data, + ) + .in_set(UpdateClientsSet), + flush_packets.in_set(FlushPacketsSet), + cleanup_chunks_after_client_despawn.after(UpdateClientsSet), + ), ) - .add_systems(PostUpdate, flush_packets.in_set(FlushPacketsSet)) .configure_set(PreUpdate, SpawnClientsSet) .configure_sets( PostUpdate, ( - UpdateClientsSet.before(FlushPacketsSet), + UpdateClientsSet + .after(UpdateLayersPreClientSet) + .before(UpdateLayersPostClientSet) + .before(FlushPacketsSet), ClearEntityChangesSet.after(UpdateClientsSet), FlushPacketsSet, - UpdateLayersPostClient.after(FlushPacketsSet), + UpdateLayersPostClientSet.after(FlushPacketsSet), ), ); @@ -171,6 +175,7 @@ impl Plugin for ClientPlugin { /// required unless otherwise stated. #[derive(Bundle)] pub struct ClientBundle { + pub marker: ClientMarker, pub client: Client, pub settings: settings::ClientSettings, pub entity_remove_buf: EntityRemoveBuf, @@ -205,6 +210,7 @@ pub struct ClientBundle { impl ClientBundle { pub fn new(args: ClientBundleArgs) -> Self { Self { + marker: ClientMarker, client: Client { conn: args.conn, enc: args.enc, @@ -256,6 +262,9 @@ pub struct ClientBundleArgs { pub enc: PacketEncoder, } +#[derive(Component, Copy, Clone)] +pub struct ClientMarker; + /// The main client component. Contains the underlying network connection and /// packet buffer. /// @@ -743,6 +752,18 @@ fn handle_layer_messages( data: &bytes[range], }); } + valence_layer::chunk::LocalMsg::LoadChunk { pos } => { + if let Some(chunk) = chunk_layer.chunk(pos) { + chunk.write_init_packets(&mut *client, pos, chunk_layer.info()); + chunk.inc_viewer_count(); + } + } + valence_layer::chunk::LocalMsg::UnloadChunk { pos } => { + if let Some(chunk) = chunk_layer.chunk(pos) { + client.write_packet(&UnloadChunkS2c { pos }); + chunk.dec_viewer_count(); + } + } }); if !chunk_biome_buf.is_empty() { @@ -850,115 +871,6 @@ fn handle_layer_messages( ); } -// fn read_data_in_old_view( -// mut clients: Query<( -// Entity, -// &mut Client, -// &mut EntityRemoveBuf, -// &EntityLayerId, -// &OldEntityLayerId, -// &Position, -// &OldPosition, -// &OldViewDistance, -// &PacketByteRange, -// )>, -// instances: Query<&Instance>, -// entities: Query<(EntityInitQuery, &OldPosition)>, -// entity_ids: Query<&EntityId>, -// ) { clients.par_iter_mut().for_each_mut( |( self_entity, mut client, mut -// remove_buf, loc, old_loc, pos, old_pos, old_view_dist, byte_range, )| { let -// Ok(inst) = instances.get(old_loc.get()) else { return; }; - -// // Send instance-wide packet data. -// client.write_packet_bytes(inst.packet_buf()); - -// // TODO: cache the chunk position? -// let old_chunk_pos = old_pos.chunk_pos(); -// let new_chunk_pos = pos.chunk_pos(); - -// let view = ChunkView::new(old_chunk_pos, old_view_dist.0); - -// // Iterate over all visible chunks from the previous tick. -// for pos in view.iter() { -// if let Some(chunk) = inst.chunk(pos) { -// // Mark this chunk as being in view of a client. -// chunk.set_viewed(); - -// // Send entity spawn packets for entities entering the -// client's view. for &(entity, src_pos) in -// chunk.incoming_entities() { if src_pos.map_or(true, -// |p| !view.contains(p)) { // The incoming entity -// originated from outside the view distance, so it -// // must be spawned. if let Ok((entity, old_pos)) -// = entities.get(entity) { // Notice we are -// spawning the entity at its old position rather than -// // the current position. This is because the client could also -// // receive update packets for this entity this tick, which may -// // include a relative entity movement. -// entity.write_init_packets(old_pos.get(), &mut client.enc); -// } } -// } - -// // Send entity despawn packets for entities exiting the -// client's view. for &(entity, dest_pos) in -// chunk.outgoing_entities() { if dest_pos.map_or(true, -// |p| !view.contains(p)) { // The outgoing entity -// moved outside the view distance, so it must be // -// despawned. if let Ok(id) = entity_ids.get(entity) -// { remove_buf.push(id.get()); -// } -// } -// } - -// match chunk.state() { -// ChunkState::Added | ChunkState::Overwrite => { -// // Chunk was added or overwritten this tick. Send -// the packet to // initialize the chunk. -// chunk.write_init_packets(&mut *client, pos, -// inst.info()); } -// ChunkState::AddedRemoved => { -// // Chunk was added and removed this tick, so -// there's // nothing that needs to be sent. -// } -// ChunkState::Removed => { -// // Chunk was removed this tick, so send the -// packet to deinitialize the // chunk and despawn -// all the contained entities. -// client.write_packet(&UnloadChunkS2c { pos }); - -// for entity in chunk.entities() { -// // Skip the client's own entity. -// if entity != self_entity { -// if let Ok(id) = entity_ids.get(entity) { -// remove_buf.push(id.get()); -// } -// } -// } -// } -// ChunkState::Normal => { -// // Send the data to update this chunk as normal. - -// // Send all data in the chunk's packet buffer to -// this client. This will // update entities in the -// chunk, update the // chunk itself, and send any -// other packet // data that was added in the buffer -// by users. if pos == new_chunk_pos && loc == -// old_loc { // Skip range of bytes for the -// client's own entity. client -// -// .write_packet_bytes(&chunk.packet_buf()[..byte_range.0.start]); -// client.write_packet_bytes(&chunk.packet_buf()[byte_range.0.end..]); -// } else { -// -// client.write_packet_bytes(chunk.packet_buf()); } -// } -// } -// } -// } -// }, -// ); -// } - fn update_view_and_layers( mut clients: Query< ( @@ -967,7 +879,7 @@ fn update_view_and_layers( &mut EntityRemoveBuf, &VisibleChunkLayer, &mut OldVisibleChunkLayer, - &VisibleEntityLayers, + Ref, &mut OldVisibleEntityLayers, &Position, &OldPosition, @@ -1024,9 +936,8 @@ fn update_view_and_layers( && chunk.state() != ChunkState::AddedRemoved { client.write_packet(&UnloadChunkS2c { pos }); + chunk.dec_viewer_count(); } - - chunk.dec_viewer_count(); } } } @@ -1039,6 +950,7 @@ fn update_view_and_layers( && chunk.state() != ChunkState::AddedRemoved { chunk.write_init_packets(&mut *client, pos, layer.info()); + chunk.inc_viewer_count(); } } } @@ -1079,7 +991,7 @@ fn update_view_and_layers( old_chunk_layer.0 = chunk_layer.0; } else { // Update the client's visible entity layers. - if old_visible_entity_layers.0 != visible_entity_layers.0 { + if visible_entity_layers.is_changed() { // Unload all entity layers that are no longer visible in the old view. for &layer in old_visible_entity_layers .0 @@ -1138,9 +1050,8 @@ fn update_view_and_layers( && chunk.state() != ChunkState::AddedRemoved { client.write_packet(&UnloadChunkS2c { pos }); + chunk.dec_viewer_count(); } - - chunk.dec_viewer_count(); } } } @@ -1153,6 +1064,7 @@ fn update_view_and_layers( && chunk.state() != ChunkState::AddedRemoved { chunk.write_init_packets(&mut *client, pos, layer.info()); + chunk.inc_viewer_count(); } } } @@ -1193,166 +1105,6 @@ fn update_view_and_layers( ); } -// /// Updates the clients' view, i.e. the set of chunks that are visible from -// the /// client's chunk position. -// /// -// /// This handles the situation when a client changes instances or chunk -// /// position. It must run after [`read_data_in_old_view`]. -// fn update_view( -// mut clients: Query< -// ( -// Entity, -// &mut Client, -// &mut EntityRemoveBuf, -// &EntityLayerId, -// &OldEntityLayerId, -// &Position, -// &OldPosition, -// &ViewDistance, -// &OldViewDistance, -// ), -// Or<( -// Changed, -// Changed, -// Changed, -// )>, -// >, -// instances: Query<&Instance>, -// entities: Query<(EntityInitQuery, &Position)>, -// entity_ids: Query<&EntityId>, -// ) { clients.par_iter_mut().for_each_mut( |( self_entity, mut client, mut -// remove_buf, loc, old_loc, pos, old_pos, view_dist, old_view_dist, )| { let -// view = ChunkView::new(ChunkPos::from_dvec3(pos.0), view_dist.0); let -// old_view = ChunkView::new(ChunkPos::from_dvec3(old_pos.get()), -// old_view_dist.0); - -// // Make sure the center chunk is set before loading chunks! -// Otherwise the client // may ignore the chunk. -// if old_view.pos != view.pos { -// client.write_packet(&ChunkRenderDistanceCenterS2c { -// chunk_x: VarInt(view.pos.x), -// chunk_z: VarInt(view.pos.z), -// }); -// } - -// // Was the client's instance changed? -// if loc.0 != old_loc.get() { -// if let Ok(old_inst) = instances.get(old_loc.get()) { -// // TODO: only send unload packets when old dimension == -// new dimension, since the // client will do the -// unloading for us in that case? - -// // Unload all chunks and entities in the old view. -// for pos in old_view.iter() { -// if let Some(chunk) = old_inst.chunk(pos) { -// // Unload the chunk if its state is not -// "removed", since we already // unloaded "removed" -// chunks earlier. if chunk.state() != -// ChunkState::Removed && chunk.state() != -// ChunkState::AddedRemoved { -// // Unload the chunk. -// client.write_packet(&UnloadChunkS2c { pos }); - -// // Unload all the entities in the chunk. -// for entity in chunk.entities() { -// // Skip the client's own entity. -// if entity != self_entity { -// if let Ok(entity_id) = -// entity_ids.get(entity) { -// remove_buf.push(entity_id.get()); } -// } -// } -// } -// } -// } -// } - -// if let Ok(inst) = instances.get(loc.0) { -// // Load all chunks and entities in new view. -// for pos in view.iter() { -// if let Some(chunk) = inst.chunk(pos) { -// // Mark this chunk as being in view of a client. -// chunk.set_viewed(); - -// // Load the chunk if it's not already removed. -// chunk.write_init_packets(&mut *client, pos, -// inst.info()); - -// // Load all the entities in this chunk. -// for entity in chunk.entities() { -// // Skip client's own entity. -// if entity != self_entity { -// if let Ok((entity, pos)) = -// entities.get(entity) { -// entity.write_init_packets(pos.get(), &mut *client); -// } } -// } -// } -// } -// } else { -// debug!("Client entered nonexistent instance ({loc:?})."); -// } -// } else if old_view != view { -// // Client changed their view without changing the instance. - -// if let Ok(inst) = instances.get(loc.0) { -// // Unload chunks and entities in the old view and load -// chunks and entities in // the new view. We don't need to -// do any work where the old and new view // overlap. -// for pos in old_view.diff(view) { -// if let Some(chunk) = inst.chunk(pos) { -// // Unload the chunk if its state is not -// "removed", since we already // unloaded "removed" -// chunks earlier. if chunk.state() != -// ChunkState::Removed && chunk.state() != -// ChunkState::AddedRemoved { -// // Unload the chunk. -// client.write_packet(&UnloadChunkS2c { pos }); - -// // Unload all the entities in the chunk. -// for entity in chunk.entities() { -// // Skip client's own entity. -// if entity != self_entity { -// if let Ok(entity_id) = -// entity_ids.get(entity) { -// remove_buf.push(entity_id.get()); } -// } -// } -// } -// } -// } - -// for pos in view.diff(old_view) { -// if let Some(chunk) = inst.chunk(pos) { -// // Load the chunk unless it's already unloaded. -// if chunk.state() != ChunkState::Removed -// && chunk.state() != ChunkState::AddedRemoved -// { -// // Mark this chunk as being in view of a -// client. chunk.set_viewed(); - -// // Load the chunk. -// chunk.write_init_packets(&mut *client, pos, -// inst.info()); - -// // Load all the entities in this chunk. -// for entity in chunk.entities() { -// // Skip client's own entity. -// if entity != self_entity { -// if let Ok((entity, pos)) = -// entities.get(entity) { -// entity.write_init_packets(pos.get(), &mut *client); -// } } -// } -// } -// } -// } -// } -// } -// }, -// ); -// } - /// Removes all the entities that are queued to be removed for each client. fn remove_entities( mut clients: Query<(&mut Client, &mut EntityRemoveBuf), Changed>, @@ -1423,3 +1175,19 @@ fn update_tracked_data(mut clients: Query<(&mut Client, &TrackedData)>) { } } } + +/// Decrement viewer count of chunks when the client is despawned. +fn cleanup_chunks_after_client_despawn( + mut clients: Query<(View, &VisibleChunkLayer), (With, With)>, + chunk_layers: Query<&ChunkLayer>, +) { + for (view, layer) in &mut clients { + if let Ok(layer) = chunk_layers.get(layer.0) { + for pos in view.get().iter() { + if let Some(chunk) = layer.chunk(pos) { + chunk.dec_viewer_count(); + } + } + } + } +} diff --git a/crates/valence_entity/build.rs b/crates/valence_entity/build.rs index 4de3539ad..02f4e5399 100644 --- a/crates/valence_entity/build.rs +++ b/crates/valence_entity/build.rs @@ -391,7 +391,7 @@ fn build() -> anyhow::Result { pub id: super::EntityId, pub uuid: super::UniqueId, pub layer: super::EntityLayerId, - pub old_layer: super::EntityLayerId, + pub old_layer: super::OldEntityLayerId, pub position: super::Position, pub old_position: super::OldPosition, pub look: super::Look, diff --git a/crates/valence_entity/src/lib.rs b/crates/valence_entity/src/lib.rs index 07c87636a..5b31fd6e3 100644 --- a/crates/valence_entity/src/lib.rs +++ b/crates/valence_entity/src/lib.rs @@ -94,7 +94,7 @@ impl Plugin for EntityPlugin { clear_animation_changes, clear_tracked_data_changes, update_old_position, - update_old_location, + update_old_layer_id, ) .in_set(ClearEntityChangesSet), ); @@ -109,7 +109,7 @@ fn update_old_position(mut query: Query<(&Position, &mut OldPosition)>) { } } -fn update_old_location(mut query: Query<(&EntityLayerId, &mut OldEntityLayerId)>) { +fn update_old_layer_id(mut query: Query<(&EntityLayerId, &mut OldEntityLayerId)>) { for (loc, mut old_loc) in &mut query { old_loc.0 = loc.0; } diff --git a/crates/valence_layer/src/chunk.rs b/crates/valence_layer/src/chunk.rs index 1874299ee..98794db0c 100644 --- a/crates/valence_layer/src/chunk.rs +++ b/crates/valence_layer/src/chunk.rs @@ -25,7 +25,7 @@ pub use self::loaded::LoadedChunk; pub use self::unloaded::UnloadedChunk; use crate::bvh::GetChunkPos; use crate::message::Messages; -use crate::{Layer, UpdateLayersPreClient}; +use crate::{Layer, UpdateLayersPostClientSet, UpdateLayersPreClientSet}; #[derive(Component, Debug)] pub struct ChunkLayer { @@ -61,12 +61,15 @@ pub enum GlobalMsg { #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub enum LocalMsg { /// Send packet data to all clients viewing the layer in view of `pos`. - PacketAt { - pos: ChunkPos, - }, - ChangeBiome { - pos: ChunkPos, - }, + PacketAt { pos: ChunkPos }, + /// Instruct clients to load the chunk at `pos`. Message content is + /// empty/ignored. + LoadChunk { pos: ChunkPos }, + /// Instruct clients to load the chunk at `pos`. Message content is + UnloadChunk { pos: ChunkPos }, + /// Message content is the data for a single biome in the "change biomes" + /// packet. + ChangeBiome { pos: ChunkPos }, } impl GetChunkPos for LocalMsg { @@ -74,6 +77,8 @@ impl GetChunkPos for LocalMsg { match *self { LocalMsg::PacketAt { pos } => pos, LocalMsg::ChangeBiome { pos } => pos, + LocalMsg::LoadChunk { pos } => pos, + LocalMsg::UnloadChunk { pos } => pos, } } } @@ -443,8 +448,8 @@ pub(super) fn build(app: &mut App) { app.add_systems( PostUpdate, ( - update_chunks_pre_client.in_set(UpdateLayersPreClient), - update_chunks_post_client.in_set(UpdateLayersPreClient), + update_chunks_pre_client.in_set(UpdateLayersPreClientSet), + update_chunks_post_client.in_set(UpdateLayersPostClientSet), ), ); } diff --git a/crates/valence_layer/src/chunk/loaded.rs b/crates/valence_layer/src/chunk/loaded.rs index 4f4c3bbf9..6d3674615 100644 --- a/crates/valence_layer/src/chunk/loaded.rs +++ b/crates/valence_layer/src/chunk/loaded.rs @@ -235,17 +235,13 @@ impl LoadedChunk { ) { match self.state { ChunkState::Added | ChunkState::Overwrite => { - // Load the chunk for any viewers. - messages.send_local(LocalMsg::PacketAt { pos }, |buf| { - self.write_init_packets( - PacketWriter::new(buf, info.compression_threshold), - pos, - info, - ) - }); + // Load the chunk. + messages.send_local(LocalMsg::LoadChunk { pos }, |_| {}); } ChunkState::Removed | ChunkState::AddedRemoved => { // Unload the chunk. + messages.send_local(LocalMsg::UnloadChunk { pos }, |_| {}); + messages.send_local(LocalMsg::PacketAt { pos }, |buf| { PacketWriter::new(buf, info.compression_threshold) .write_packet(&UnloadChunkS2c { pos }) @@ -262,81 +258,89 @@ impl LoadedChunk { return; } - messages.send_local(LocalMsg::PacketAt { pos }, |buf| { - let mut writer = PacketWriter::new(buf, info.compression_threshold); + // Block states + for (sect_y, sect) in self.sections.iter_mut().enumerate() { + match sect.section_updates.len() { + 0 => {} + 1 => { + let packed = sect.section_updates[0].0 as u64; + let offset_y = packed & 0b1111; + let offset_z = (packed >> 4) & 0b1111; + let offset_x = (packed >> 8) & 0b1111; + let block = packed >> 12; - // Block states - for (sect_y, sect) in self.sections.iter_mut().enumerate() { - match sect.section_updates.len() { - 0 => {} - 1 => { - let packed = sect.section_updates[0].0 as u64; - let offset_y = packed & 0b1111; - let offset_z = (packed >> 4) & 0b1111; - let offset_x = (packed >> 8) & 0b1111; - let block = packed >> 12; + let global_x = pos.x * 16 + offset_x as i32; + let global_y = info.min_y + sect_y as i32 * 16 + offset_y as i32; + let global_z = pos.z * 16 + offset_z as i32; - let global_x = pos.x * 16 + offset_x as i32; - let global_y = info.min_y + sect_y as i32 * 16 + offset_y as i32; - let global_z = pos.z * 16 + offset_z as i32; + messages.send_local(LocalMsg::PacketAt { pos }, |buf| { + let mut writer = PacketWriter::new(buf, info.compression_threshold); writer.write_packet(&BlockUpdateS2c { position: BlockPos::new(global_x, global_y, global_z), block_id: VarInt(block as i32), }); - } - _ => { - let chunk_section_position = (pos.x as i64) << 42 - | (pos.z as i64 & 0x3fffff) << 20 - | (sect_y as i64 + info.min_y.div_euclid(16) as i64) & 0xfffff; + }); + } + _ => { + let chunk_section_position = (pos.x as i64) << 42 + | (pos.z as i64 & 0x3fffff) << 20 + | (sect_y as i64 + info.min_y.div_euclid(16) as i64) & 0xfffff; + + messages.send_local(LocalMsg::PacketAt { pos }, |buf| { + let mut writer = PacketWriter::new(buf, info.compression_threshold); writer.write_packet(&ChunkDeltaUpdateS2c { chunk_section_position, blocks: Cow::Borrowed(§.section_updates), }); - } + }); } - - sect.section_updates.clear(); } - // Block entities - for &idx in &self.changed_block_entities { - let Some(nbt) = self.block_entities.get(&idx) else { - continue; - }; + sect.section_updates.clear(); + } + + // Block entities + for &idx in &self.changed_block_entities { + let Some(nbt) = self.block_entities.get(&idx) else { + continue; + }; + + let x = idx % 16; + let z = (idx / 16) % 16; + let y = idx / 16 / 16; - let x = idx % 16; - let z = (idx / 16) % 16; - let y = idx / 16 / 16; + let state = self.sections[y as usize / 16] + .block_states + .get(idx as usize % SECTION_BLOCK_COUNT); - let state = self.sections[y as usize / 16] - .block_states - .get(idx as usize % SECTION_BLOCK_COUNT); + let Some(kind) = state.block_entity_kind() else { + continue; + }; - let Some(kind) = state.block_entity_kind() else { - continue; - }; + let global_x = pos.x * 16 + x as i32; + let global_y = info.min_y + y as i32; + let global_z = pos.z * 16 + z as i32; - let global_x = pos.x * 16 + x as i32; - let global_y = info.min_y + y as i32; - let global_z = pos.z * 16 + z as i32; + messages.send_local(LocalMsg::PacketAt { pos }, |buf| { + let mut writer = PacketWriter::new(buf, info.compression_threshold); writer.write_packet(&BlockEntityUpdateS2c { position: BlockPos::new(global_x, global_y, global_z), kind: VarInt(kind as i32), data: Cow::Borrowed(nbt), }); - } + }); + } - self.changed_block_entities.clear(); - }); + self.changed_block_entities.clear(); - messages.send_local(LocalMsg::ChangeBiome { pos }, |buf| { - // Biomes - if self.changed_biomes { - self.changed_biomes = false; + // Biomes + if self.changed_biomes { + self.changed_biomes = false; + messages.send_local(LocalMsg::ChangeBiome { pos }, |buf| { for sect in self.sections.iter() { sect.biomes .encode_mc_format( @@ -348,8 +352,8 @@ impl LoadedChunk { ) .expect("paletted container encode should always succeed"); } - } - }); + }); + } // All changes should be cleared. self.assert_no_changes(); diff --git a/crates/valence_layer/src/entity.rs b/crates/valence_layer/src/entity.rs index 23858a19c..00658eeee 100644 --- a/crates/valence_layer/src/entity.rs +++ b/crates/valence_layer/src/entity.rs @@ -14,7 +14,7 @@ use valence_entity::{EntityId, EntityLayerId, OldEntityLayerId, OldPosition, Pos use crate::bvh::GetChunkPos; use crate::message::Messages; -use crate::{Layer, UpdateLayersPostClient, UpdateLayersPreClient}; +use crate::{Layer, UpdateLayersPostClientSet, UpdateLayersPreClientSet}; #[derive(Component, Debug)] pub struct EntityLayer { @@ -123,8 +123,8 @@ pub(super) fn build(app: &mut App) { ready_entity_layers, ) .chain() - .in_set(UpdateLayersPreClient), - unready_entity_layers.in_set(UpdateLayersPostClient), + .in_set(UpdateLayersPreClientSet), + unready_entity_layers.in_set(UpdateLayersPostClientSet), ), ); } diff --git a/crates/valence_layer/src/lib.rs b/crates/valence_layer/src/lib.rs index e86d6c1cd..21e960239 100644 --- a/crates/valence_layer/src/lib.rs +++ b/crates/valence_layer/src/lib.rs @@ -53,22 +53,22 @@ impl Default for LayerPlugin { /// chunks and entities should run _before_ this. Systems that need to read /// layer messages should run _after_ this. #[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct UpdateLayersPreClient; +pub struct UpdateLayersPreClientSet; /// When layers are cleared and messages from this tick are lost. Systems that /// read layer messages should run _before_ this. #[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct UpdateLayersPostClient; +pub struct UpdateLayersPostClientSet; impl Plugin for LayerPlugin { fn build(&self, app: &mut App) { app.configure_sets( PostUpdate, ( - UpdateLayersPreClient + UpdateLayersPreClientSet .after(InitEntitiesSet) .after(UpdateTrackedDataSet), - UpdateLayersPostClient.after(UpdateLayersPreClient), + UpdateLayersPostClientSet.after(UpdateLayersPreClientSet), ), ); diff --git a/crates/valence_layer/src/message.rs b/crates/valence_layer/src/message.rs index 6940af5eb..851b47b43 100644 --- a/crates/valence_layer/src/message.rs +++ b/crates/valence_layer/src/message.rs @@ -30,9 +30,15 @@ where f(&mut self.staging); let end = self.staging.len(); - if start != end { - self.global.push((msg, start as u32..end as u32)); + if let Some((m, range)) = self.global.last_mut() { + if msg == *m { + // Extend the existing message. + range.end = end as u32; + return; + } } + + self.global.push((msg, start as u32..end as u32)); } pub(crate) fn send_local(&mut self, msg: L, f: impl FnOnce(&mut Vec)) { @@ -42,9 +48,15 @@ where f(&mut self.staging); let end = self.staging.len(); - if start != end { - self.local.push((msg, start as u32..end as u32)); + if let Some((m, range)) = self.local.last_mut() { + if msg == *m { + // Extend the existing message. + range.end = end as u32; + return; + } } + + self.local.push((msg, start as u32..end as u32)); } /// Readies messages to be read by clients. diff --git a/crates/valence_player_list/src/lib.rs b/crates/valence_player_list/src/lib.rs index de1db6b3d..e91fcaa7d 100644 --- a/crates/valence_player_list/src/lib.rs +++ b/crates/valence_player_list/src/lib.rs @@ -33,7 +33,7 @@ use valence_core::protocol::encode::{PacketWriter, WritePacket}; use valence_core::text::Text; use valence_core::uuid::UniqueId; use valence_core::Server; -use valence_instance::WriteUpdatePacketsToInstancesSet; +use valence_layer::UpdateLayersPreClientSet; use crate::packet::PlayerRemoveS2c; @@ -50,7 +50,7 @@ impl Plugin for PlayerListPlugin { PostUpdate, // Needs to happen before player entities are initialized. Otherwise, they will // appear invisible. - PlayerListSet.before(WriteUpdatePacketsToInstancesSet), + PlayerListSet.before(UpdateLayersPreClientSet), ) .add_systems( PostUpdate, diff --git a/crates/valence_world_border/src/lib.rs b/crates/valence_world_border/src/lib.rs index 67e753b2d..5e0a9b378 100644 --- a/crates/valence_world_border/src/lib.rs +++ b/crates/valence_world_border/src/lib.rs @@ -66,6 +66,9 @@ clippy::dbg_macro )] +// TODO: fix. + +/* pub mod packet; use std::time::{Duration, Instant}; @@ -78,7 +81,7 @@ use valence_core::protocol::encode::WritePacket; use valence_core::protocol::var_int::VarInt; use valence_core::protocol::var_long::VarLong; use valence_entity::EntityLayerId; -use valence_instance::{Instance, WriteUpdatePacketsToInstancesSet}; +use valence_layer::UpdateLayersPreClientSet; use valence_registry::*; // https://minecraft.fandom.com/wiki/World_border @@ -100,7 +103,7 @@ impl Plugin for WorldBorderPlugin { app.configure_sets( PostUpdate, ( - UpdateWorldBorderPerInstanceSet.before(WriteUpdatePacketsToInstancesSet), + UpdateWorldBorderPerInstanceSet.before(UpdateLayersPreClientSet), UpdateWorldBorderPerClientSet.before(FlushPacketsSet), ), ) @@ -396,3 +399,4 @@ fn portal_teleport_bounary_change( fn lerp(start: f64, end: f64, t: f64) -> f64 { start + (end - start) * t } +*/ diff --git a/src/lib.rs b/src/lib.rs index 884a94868..1214b3bd3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,7 +47,7 @@ pub use valence_world_border as world_border; pub use { bevy_app as app, bevy_ecs as ecs, glam, valence_biome as biome, valence_block as block, valence_client as client, valence_dimension as dimension, valence_entity as entity, - valence_instance as instance, valence_nbt as nbt, valence_registry as registry, + valence_layer as layer, valence_nbt as nbt, valence_registry as registry, }; /// Contains the most frequently used items in Valence projects. @@ -84,11 +84,11 @@ pub mod prelude { EventLoopPostUpdate, EventLoopPreUpdate, EventLoopUpdate, }; pub use valence_client::interact_entity::{EntityInteraction, InteractEntityEvent}; + pub use valence_client::spawn::{ClientSpawnQuery, ClientSpawnQueryReadOnly}; pub use valence_client::title::SetTitle as _; pub use valence_client::{ - despawn_disconnected_clients, Client, DeathLocation, HasRespawnScreen, HashedSeed, Ip, - IsDebug, IsFlat, IsHardcore, OldView, OldViewDistance, PrevGameMode, Properties, - ReducedDebugInfo, RespawnPosition, Username, View, ViewDistance, + despawn_disconnected_clients, Client, Ip, OldView, OldViewDistance, Properties, + RespawnPosition, Username, View, ViewDistance, }; pub use valence_core::block_pos::BlockPos; pub use valence_core::chunk_pos::{ChunkPos, ChunkView}; @@ -108,12 +108,14 @@ pub mod prelude { EntityAnimation, EntityKind, EntityLayerId, EntityManager, EntityStatus, HeadYaw, Look, OldEntityLayerId, OldPosition, Position, }; - pub use valence_instance::chunk::{Chunk, LoadedChunk, UnloadedChunk}; - pub use valence_instance::{Block, BlockRef, Instance}; #[cfg(feature = "inventory")] pub use valence_inventory::{ CursorItem, Inventory, InventoryKind, InventoryWindow, InventoryWindowMut, OpenInventory, }; + pub use valence_layer::chunk::{ + Block, BlockRef, Chunk, ChunkLayer, LoadedChunk, UnloadedChunk, + }; + pub use valence_layer::entity::EntityLayer; pub use valence_nbt::Compound; #[cfg(feature = "network")] pub use valence_network::{ @@ -144,7 +146,7 @@ impl PluginGroup for DefaultPlugins { .add(valence_dimension::DimensionPlugin) .add(valence_entity::EntityPlugin) .add(valence_entity::hitbox::HitboxPlugin) - .add(valence_instance::InstancePlugin) + .add(valence_layer::LayerPlugin::::new()) .add(valence_client::ClientPlugin); #[cfg(feature = "log")] @@ -177,10 +179,11 @@ impl PluginGroup for DefaultPlugins { group = group.add(valence_advancement::AdvancementPlugin) } - #[cfg(feature = "world_border")] - { - group = group.add(valence_world_border::WorldBorderPlugin); - } + // TODO + // #[cfg(feature = "world_border")] + // { + // group = group.add(valence_world_border::WorldBorderPlugin); + // } #[cfg(feature = "boss_bar")] { diff --git a/src/testing.rs b/src/testing.rs index fe8507f34..e1c4aaf71 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -17,17 +17,17 @@ use valence_core::protocol::var_int::VarInt; use valence_core::protocol::{Decode, Encode, Packet}; use valence_core::{ident, CoreSettings, Server}; use valence_dimension::DimensionTypeRegistry; +use valence_layer::{ChunkLayer, EntityLayer}; use valence_network::NetworkPlugin; use crate::client::{ClientBundle, ClientConnection, ReceivedPacket}; -use crate::instance::Instance; use crate::DefaultPlugins; /// Sets up valence with a single mock client. Returns the Entity of the client /// and the corresponding MockClientHelper. /// /// Reduces boilerplate in unit tests. -pub fn scenario_single_client(app: &mut App) -> (Entity, MockClientHelper) { +pub fn scenario_single_client(app: &mut App) -> ScenarioSingleClient { app.insert_resource(CoreSettings { compression_threshold: None, ..Default::default() @@ -41,20 +41,32 @@ pub fn scenario_single_client(app: &mut App) -> (Entity, MockClientHelper) { app.update(); // Initialize plugins. - let instance = Instance::new( + let chunk_layer = ChunkLayer::new( ident!("overworld"), app.world.resource::(), app.world.resource::(), app.world.resource::(), ); - - let instance_ent = app.world.spawn(instance).id(); + let entity_layer = EntityLayer::new(app.world.resource::()); + let layer_ent = app.world.spawn((chunk_layer, entity_layer)).id(); let (mut client, client_helper) = create_mock_client("test"); - client.player.location.0 = instance_ent; + client.player.layer.0 = layer_ent; + client.visible_chunk_layer.0 = layer_ent; + client.visible_entity_layers.0.insert(layer_ent); let client_ent = app.world.spawn(client).id(); - (client_ent, client_helper) + ScenarioSingleClient { + client: client_ent, + helper: client_helper, + layer: layer_ent, + } +} + +pub struct ScenarioSingleClient { + pub client: Entity, + pub helper: MockClientHelper, + pub layer: Entity, } /// Creates a mock client bundle that can be used for unit testing. From aa6e3e4f67246b64c50d68468ebec4a36b17b5f1 Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 13 Jul 2023 01:11:03 -0700 Subject: [PATCH 07/47] remove `ChunkState` --- crates/valence_client/src/lib.rs | 62 +++++------- crates/valence_layer/src/bvh.rs | 4 +- crates/valence_layer/src/chunk.rs | 85 ++++++++++++---- crates/valence_layer/src/chunk/loaded.rs | 122 +++-------------------- 4 files changed, 105 insertions(+), 168 deletions(-) diff --git a/crates/valence_client/src/lib.rs b/crates/valence_client/src/lib.rs index a573a8412..7e970d223 100644 --- a/crates/valence_client/src/lib.rs +++ b/crates/valence_client/src/lib.rs @@ -64,7 +64,6 @@ use valence_entity::{ ClearEntityChangesSet, EntityId, EntityLayerId, EntityStatus, Look, OldPosition, Position, Velocity, }; -use valence_layer::chunk::loaded::ChunkState; use valence_layer::packet::{ ChunkBiome, ChunkBiomeDataS2c, ChunkLoadDistanceS2c, ChunkRenderDistanceCenterS2c, UnloadChunkS2c, @@ -752,16 +751,20 @@ fn handle_layer_messages( data: &bytes[range], }); } - valence_layer::chunk::LocalMsg::LoadChunk { pos } => { - if let Some(chunk) = chunk_layer.chunk(pos) { - chunk.write_init_packets(&mut *client, pos, chunk_layer.info()); - chunk.inc_viewer_count(); - } - } - valence_layer::chunk::LocalMsg::UnloadChunk { pos } => { - if let Some(chunk) = chunk_layer.chunk(pos) { - client.write_packet(&UnloadChunkS2c { pos }); - chunk.dec_viewer_count(); + valence_layer::chunk::LocalMsg::LoadOrUnloadChunk { pos } => { + match bytes[range].last() { + Some(&1) => { + // Load chunk. + if let Some(chunk) = chunk_layer.chunk(pos) { + chunk.write_init_packets(&mut *client, pos, chunk_layer.info()); + chunk.inc_viewer_count(); + } + } + Some(&0) => { + // Unload chunk. + client.write_packet(&UnloadChunkS2c { pos }); + } + _ => panic!("invalid message data"), } } }); @@ -927,17 +930,12 @@ fn update_view_and_layers( // Was the client's chunk layer changed? if old_chunk_layer.0 != chunk_layer.0 { // Unload all chunks in the old view. + // TODO: can we skip this step if old dimension != new dimension? if let Ok(layer) = chunk_layers.get(old_chunk_layer.0) { for pos in old_view.iter() { if let Some(chunk) = layer.chunk(pos) { - // Unload the chunk if its state is not "removed", since we already - // unloaded "removed" chunks while we were handling layer messages. - if chunk.state() != ChunkState::Removed - && chunk.state() != ChunkState::AddedRemoved - { - client.write_packet(&UnloadChunkS2c { pos }); - chunk.dec_viewer_count(); - } + client.write_packet(&UnloadChunkS2c { pos }); + chunk.dec_viewer_count(); } } } @@ -946,12 +944,8 @@ fn update_view_and_layers( if let Ok(layer) = chunk_layers.get(chunk_layer.0) { for pos in view.iter() { if let Some(chunk) = layer.chunk(pos) { - if chunk.state() != ChunkState::Removed - && chunk.state() != ChunkState::AddedRemoved - { - chunk.write_init_packets(&mut *client, pos, layer.info()); - chunk.inc_viewer_count(); - } + chunk.write_init_packets(&mut *client, pos, layer.info()); + chunk.inc_viewer_count(); } } } @@ -1044,14 +1038,8 @@ fn update_view_and_layers( if let Ok(layer) = chunk_layers.get(chunk_layer.0) { for pos in old_view.diff(view) { if let Some(chunk) = layer.chunk(pos) { - // Unload the chunk if its state is not "removed", since we already - // unloaded "removed" chunks while we were handling layer messages. - if chunk.state() != ChunkState::Removed - && chunk.state() != ChunkState::AddedRemoved - { - client.write_packet(&UnloadChunkS2c { pos }); - chunk.dec_viewer_count(); - } + client.write_packet(&UnloadChunkS2c { pos }); + chunk.dec_viewer_count(); } } } @@ -1060,12 +1048,8 @@ fn update_view_and_layers( if let Ok(layer) = chunk_layers.get(chunk_layer.0) { for pos in view.diff(old_view) { if let Some(chunk) = layer.chunk(pos) { - if chunk.state() != ChunkState::Removed - && chunk.state() != ChunkState::AddedRemoved - { - chunk.write_init_packets(&mut *client, pos, layer.info()); - chunk.inc_viewer_count(); - } + chunk.write_init_packets(&mut *client, pos, layer.info()); + chunk.inc_viewer_count(); } } } diff --git a/crates/valence_layer/src/bvh.rs b/crates/valence_layer/src/bvh.rs index 0b38086c2..e007d9120 100644 --- a/crates/valence_layer/src/bvh.rs +++ b/crates/valence_layer/src/bvh.rs @@ -128,7 +128,7 @@ impl ChunkBvh // Determine splitting axis based on the side that's longer. Then split along // the spatial midpoint. We could use a more advanced heuristic like SAH, - // but it probably doesn't matter here. + // but it's probably not worth it. let point = if bounds.length_x() >= bounds.length_z() { // Split on Z axis. @@ -323,7 +323,7 @@ mod tests { bvh.check_invariants(); - // Check that we traverse exactly the positions that we know the view can see. + // Check that we query exactly the positions that we know the view can see. bvh.query(view, |pos| { let idx = viewed_positions.iter().position(|p| p == pos).expect("😔"); diff --git a/crates/valence_layer/src/chunk.rs b/crates/valence_layer/src/chunk.rs index 98794db0c..bad21efa4 100644 --- a/crates/valence_layer/src/chunk.rs +++ b/crates/valence_layer/src/chunk.rs @@ -62,11 +62,12 @@ pub enum GlobalMsg { pub enum LocalMsg { /// Send packet data to all clients viewing the layer in view of `pos`. PacketAt { pos: ChunkPos }, - /// Instruct clients to load the chunk at `pos`. Message content is - /// empty/ignored. - LoadChunk { pos: ChunkPos }, - /// Instruct clients to load the chunk at `pos`. Message content is - UnloadChunk { pos: ChunkPos }, + /// Instruct clients to load or unload the chunk at `pos`. Loading and + /// unloading are combined into a single message so that load/unload order + /// is not lost when messages are sorted. + /// + /// Message content is a single byte indicating load (1) or unload (0). + LoadOrUnloadChunk { pos: ChunkPos }, /// Message content is the data for a single biome in the "change biomes" /// packet. ChangeBiome { pos: ChunkPos }, @@ -77,8 +78,7 @@ impl GetChunkPos for LocalMsg { match *self { LocalMsg::PacketAt { pos } => pos, LocalMsg::ChangeBiome { pos } => pos, - LocalMsg::LoadChunk { pos } => pos, - LocalMsg::UnloadChunk { pos } => pos, + LocalMsg::LoadOrUnloadChunk { pos } => pos, } } } @@ -203,19 +203,28 @@ impl ChunkLayer { where F: FnMut(ChunkPos, &mut LoadedChunk) -> bool, { - for (pos, chunk) in &mut self.chunks { + self.chunks.retain(|pos, chunk| { if !f(*pos, chunk) { - chunk.remove(); + self.messages + .send_local(LocalMsg::LoadOrUnloadChunk { pos: *pos }, |b| b.push(0)); + + false + } else { + true } - } + }); } /// Get a [`ChunkEntry`] for the given position. pub fn chunk_entry(&mut self, pos: impl Into) -> ChunkEntry { match self.chunks.entry(pos.into()) { - Entry::Occupied(oe) => ChunkEntry::Occupied(OccupiedChunkEntry { entry: oe }), + Entry::Occupied(oe) => ChunkEntry::Occupied(OccupiedChunkEntry { + messages: &mut self.messages, + entry: oe, + }), Entry::Vacant(ve) => ChunkEntry::Vacant(VacantChunkEntry { height: self.info.height, + messages: &mut self.messages, entry: ve, }), } @@ -234,7 +243,7 @@ impl ChunkLayer { } /// Optimizes the memory usage of the instance. - pub fn optimize(&mut self) { + pub fn shrink_to_fit(&mut self) { for (_, chunk) in self.chunks_mut() { chunk.shrink_to_fit(); } @@ -260,7 +269,11 @@ impl ChunkLayer { #[inline] fn chunk_and_offsets(&self, pos: BlockPos) -> Option<(&LoadedChunk, u32, u32, u32)> { - let Some(y) = pos.y.checked_sub(self.info.min_y).and_then(|y| y.try_into().ok()) else { + let Some(y) = pos + .y + .checked_sub(self.info.min_y) + .and_then(|y| y.try_into().ok()) + else { return None; }; @@ -283,7 +296,11 @@ impl ChunkLayer { &mut self, pos: BlockPos, ) -> Option<(&mut LoadedChunk, u32, u32, u32)> { - let Some(y) = pos.y.checked_sub(self.info.min_y).and_then(|y| y.try_into().ok()) else { + let Some(y) = pos + .y + .checked_sub(self.info.min_y) + .and_then(|y| y.try_into().ok()) + else { return None; }; @@ -385,6 +402,7 @@ impl<'a> ChunkEntry<'a> { #[derive(Debug)] pub struct OccupiedChunkEntry<'a> { + messages: &'a mut ChunkLayerMessages, entry: OccupiedEntry<'a, ChunkPos, LoadedChunk>, } @@ -398,6 +416,13 @@ impl<'a> OccupiedChunkEntry<'a> { } pub fn insert(&mut self, chunk: UnloadedChunk) -> UnloadedChunk { + self.messages.send_local( + LocalMsg::LoadOrUnloadChunk { + pos: *self.entry.key(), + }, + |b| b.push(1), + ); + self.entry.get_mut().insert(chunk) } @@ -410,6 +435,13 @@ impl<'a> OccupiedChunkEntry<'a> { } pub fn remove(mut self) -> UnloadedChunk { + self.messages.send_local( + LocalMsg::LoadOrUnloadChunk { + pos: *self.entry.key(), + }, + |b| b.push(0), + ); + self.entry.get_mut().remove() } @@ -417,6 +449,13 @@ impl<'a> OccupiedChunkEntry<'a> { let pos = *self.entry.key(); let chunk = self.entry.get_mut().remove(); + self.messages.send_local( + LocalMsg::LoadOrUnloadChunk { + pos: *self.entry.key(), + }, + |b| b.push(0), + ); + (pos, chunk) } } @@ -424,6 +463,7 @@ impl<'a> OccupiedChunkEntry<'a> { #[derive(Debug)] pub struct VacantChunkEntry<'a> { height: u32, + messages: &'a mut ChunkLayerMessages, entry: VacantEntry<'a, ChunkPos, LoadedChunk>, } @@ -432,6 +472,13 @@ impl<'a> VacantChunkEntry<'a> { let mut loaded = LoadedChunk::new(self.height); loaded.insert(chunk); + self.messages.send_local( + LocalMsg::LoadOrUnloadChunk { + pos: *self.entry.key(), + }, + |b| b.push(1), + ); + self.entry.insert(loaded) } @@ -448,13 +495,13 @@ pub(super) fn build(app: &mut App) { app.add_systems( PostUpdate, ( - update_chunks_pre_client.in_set(UpdateLayersPreClientSet), - update_chunks_post_client.in_set(UpdateLayersPostClientSet), + update_chunk_layers_pre_client.in_set(UpdateLayersPreClientSet), + update_chunk_layers_post_client.in_set(UpdateLayersPostClientSet), ), ); } -fn update_chunks_pre_client(mut layers: Query<&mut ChunkLayer>) { +fn update_chunk_layers_pre_client(mut layers: Query<&mut ChunkLayer>) { for layer in &mut layers { let layer = layer.into_inner(); @@ -466,10 +513,8 @@ fn update_chunks_pre_client(mut layers: Query<&mut ChunkLayer>) { } } -fn update_chunks_post_client(mut layers: Query<&mut ChunkLayer>) { +fn update_chunk_layers_post_client(mut layers: Query<&mut ChunkLayer>) { for mut layer in &mut layers { - layer.chunks.retain(|_, chunk| chunk.update_post_client()); - layer.messages.unready(); } } diff --git a/crates/valence_layer/src/chunk/loaded.rs b/crates/valence_layer/src/chunk/loaded.rs index 6d3674615..c0fa7dbfd 100644 --- a/crates/valence_layer/src/chunk/loaded.rs +++ b/crates/valence_layer/src/chunk/loaded.rs @@ -24,12 +24,10 @@ use super::unloaded::{self, UnloadedChunk}; use super::{ChunkLayerInfo, ChunkLayerMessages, LocalMsg}; use crate::packet::{ BlockEntityUpdateS2c, BlockUpdateS2c, ChunkDataBlockEntity, ChunkDataS2c, ChunkDeltaUpdateS2c, - UnloadChunkS2c, }; #[derive(Debug)] pub struct LoadedChunk { - state: ChunkState, /// A count of the clients viewing this chunk. Useful for knowing if it's /// necessary to record changes, since no client would be in view to receive /// the changes if this were nonzero. @@ -48,23 +46,6 @@ pub struct LoadedChunk { cached_init_packets: Mutex>, } -/// Describes the current state of a loaded chunk. -#[derive(Copy, Clone, PartialEq, Eq, Debug)] - -pub enum ChunkState { - /// The chunk is newly inserted this tick. - Added, - /// The chunk was `Added` this tick, but then was later removed this tick. - AddedRemoved, - /// The chunk was `Normal` in the last tick and has been removed this tick. - Removed, - /// The chunk was `Normal` in the last tick and has been overwritten with a - /// new chunk this tick. - Overwrite, - /// The chunk is in none of the other states. This is the common case. - Normal, -} - #[derive(Clone, Default, Debug)] struct Section { block_states: BlockStateContainer, @@ -107,7 +88,6 @@ impl Section { impl LoadedChunk { pub(crate) fn new(height: u32) -> Self { Self { - state: ChunkState::Added, viewer_count: AtomicU32::new(0), sections: vec![Section::default(); height as usize / 16].into(), block_entities: BTreeMap::new(), @@ -124,17 +104,9 @@ impl LoadedChunk { /// The previous chunk data is returned. /// /// [resized]: UnloadedChunk::set_height - pub fn insert(&mut self, mut chunk: UnloadedChunk) -> UnloadedChunk { + pub(crate) fn insert(&mut self, mut chunk: UnloadedChunk) -> UnloadedChunk { chunk.set_height(self.height()); - self.state = match self.state { - ChunkState::Added => ChunkState::Added, - ChunkState::AddedRemoved => ChunkState::Added, - ChunkState::Removed => ChunkState::Overwrite, - ChunkState::Overwrite => ChunkState::Overwrite, - ChunkState::Normal => ChunkState::Overwrite, - }; - let old_sections = self .sections .iter_mut() @@ -161,15 +133,7 @@ impl LoadedChunk { } } - pub fn remove(&mut self) -> UnloadedChunk { - self.state = match self.state { - ChunkState::Added => ChunkState::AddedRemoved, - ChunkState::AddedRemoved => ChunkState::AddedRemoved, - ChunkState::Removed => ChunkState::Removed, - ChunkState::Overwrite => ChunkState::Removed, - ChunkState::Normal => ChunkState::Removed, - }; - + pub(crate) fn remove(&mut self) -> UnloadedChunk { let old_sections = self .sections .iter_mut() @@ -195,10 +159,6 @@ impl LoadedChunk { } } - pub fn state(&self) -> ChunkState { - self.state - } - /// Returns the number of clients in view of this chunk. pub fn viewer_count(&self) -> u32 { self.viewer_count.load(Ordering::Relaxed) @@ -209,13 +169,13 @@ impl LoadedChunk { *self.viewer_count.get_mut() } - /// For internal use only. + /// Increments the viewer count. For internal use only. #[doc(hidden)] pub fn inc_viewer_count(&self) { self.viewer_count.fetch_add(1, Ordering::Relaxed); } - /// For internal use only. + /// Decrements the viewer count. For internal use only. #[doc(hidden)] pub fn dec_viewer_count(&self) { let old = self.viewer_count.fetch_sub(1, Ordering::Relaxed); @@ -223,9 +183,7 @@ impl LoadedChunk { } /// Performs the changes necessary to prepare this chunk for client updates. - /// Notably: - /// - Message is sent to spawn or despawn the chunk. - /// - Chunk update packets are written to this chunk's packet buffer. + /// - Chunk change messages are written to the layer. /// - Recorded changes are cleared. pub(crate) fn update_pre_client( &mut self, @@ -233,24 +191,7 @@ impl LoadedChunk { info: &ChunkLayerInfo, messages: &mut ChunkLayerMessages, ) { - match self.state { - ChunkState::Added | ChunkState::Overwrite => { - // Load the chunk. - messages.send_local(LocalMsg::LoadChunk { pos }, |_| {}); - } - ChunkState::Removed | ChunkState::AddedRemoved => { - // Unload the chunk. - messages.send_local(LocalMsg::UnloadChunk { pos }, |_| {}); - - messages.send_local(LocalMsg::PacketAt { pos }, |buf| { - PacketWriter::new(buf, info.compression_threshold) - .write_packet(&UnloadChunkS2c { pos }) - }); - } - ChunkState::Normal => {} - } - - if !is_viewed(&mut self.viewer_count, self.state) { + if *self.viewer_count.get_mut() == 0 { // Nobody is viewing the chunk, so no need to send any update packets. There // also shouldn't be any changes that need to be cleared. self.assert_no_changes(); @@ -359,28 +300,6 @@ impl LoadedChunk { self.assert_no_changes(); } - /// Returns if the chunk should be retained. - pub(crate) fn update_post_client(&mut self) -> bool { - // Changes were already cleared in `update_pre_client`. - self.assert_no_changes(); - - match self.state { - ChunkState::Added | ChunkState::Overwrite | ChunkState::Normal => { - self.state = ChunkState::Normal; - true - } - ChunkState::Removed | ChunkState::AddedRemoved => { - debug_assert_eq!( - *self.viewer_count.get_mut(), - 0, - "chunk viewer count should be zero by the time it's removed" - ); - - false - } - } - } - /// Writes the packet data needed to initialize this chunk. #[doc(hidden)] pub fn write_init_packets( @@ -389,11 +308,6 @@ impl LoadedChunk { pos: ChunkPos, info: &ChunkLayerInfo, ) { - debug_assert!( - self.state != ChunkState::Removed && self.state != ChunkState::AddedRemoved, - "attempt to initialize removed chunk" - ); - let mut init_packets = self.cached_init_packets.lock(); if init_packets.is_empty() { @@ -511,7 +425,7 @@ impl Chunk for LoadedChunk { if block != old_block { self.cached_init_packets.get_mut().clear(); - if is_viewed(&mut self.viewer_count, self.state) { + if *self.viewer_count.get_mut() > 0 { let compact = (block.to_raw() as i64) << 12 | (x << 8 | z << 4 | (y % 16)) as i64; sect.section_updates.push(VarLong(compact)); } @@ -529,7 +443,7 @@ impl Chunk for LoadedChunk { if *b != block { self.cached_init_packets.get_mut().clear(); - if is_viewed(&mut self.viewer_count, self.state) { + if *self.viewer_count.get_mut() > 0 { // The whole section is being modified, so any previous modifications would // be overwritten. sect.section_updates.clear(); @@ -553,7 +467,7 @@ impl Chunk for LoadedChunk { if block != sect.block_states.get(idx as usize) { self.cached_init_packets.get_mut().clear(); - if is_viewed(&mut self.viewer_count, self.state) { + if *self.viewer_count.get_mut() > 0 { let packed = block_bits | (x << 8 | z << 4 | sect_y) as i64; sect.section_updates.push(VarLong(packed)); } @@ -578,7 +492,7 @@ impl Chunk for LoadedChunk { let idx = x + z * 16 + y * 16 * 16; if let Some(be) = self.block_entities.get_mut(&idx) { - if is_viewed(&mut self.viewer_count, self.state) { + if *self.viewer_count.get_mut() > 0 { self.changed_block_entities.insert(idx); } self.cached_init_packets.get_mut().clear(); @@ -602,7 +516,7 @@ impl Chunk for LoadedChunk { match block_entity { Some(nbt) => { - if is_viewed(&mut self.viewer_count, self.state) { + if *self.viewer_count.get_mut() > 0 { self.changed_block_entities.insert(idx); } self.cached_init_packets.get_mut().clear(); @@ -628,7 +542,7 @@ impl Chunk for LoadedChunk { self.cached_init_packets.get_mut().clear(); - if is_viewed(&mut self.viewer_count, self.state) { + if *self.viewer_count.get_mut() > 0 { self.changed_block_entities .extend(mem::take(&mut self.block_entities).into_keys()); } else { @@ -654,7 +568,7 @@ impl Chunk for LoadedChunk { if biome != old_biome { self.cached_init_packets.get_mut().clear(); - if is_viewed(&mut self.viewer_count, self.state) { + if *self.viewer_count.get_mut() > 0 { self.changed_biomes = true; } } @@ -670,11 +584,11 @@ impl Chunk for LoadedChunk { if let PalettedContainer::Single(b) = §.biomes { if *b != biome { self.cached_init_packets.get_mut().clear(); - self.changed_biomes = is_viewed(&mut self.viewer_count, self.state); + self.changed_biomes = *self.viewer_count.get_mut() > 0; } } else { self.cached_init_packets.get_mut().clear(); - self.changed_biomes = is_viewed(&mut self.viewer_count, self.state); + self.changed_biomes = *self.viewer_count.get_mut() > 0; } sect.biomes.fill(biome); @@ -691,12 +605,6 @@ impl Chunk for LoadedChunk { } } -/// If there are potentially clients viewing this chunk. -#[inline] -fn is_viewed(viewer_count: &mut AtomicU32, state: ChunkState) -> bool { - state == ChunkState::Normal && *viewer_count.get_mut() > 0 -} - #[cfg(test)] mod tests { use valence_core::ident; From 89c61f0bfebb83826b85a9a8a9ae8ca968916968 Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 13 Jul 2023 02:44:03 -0700 Subject: [PATCH 08/47] despawn entities in despawned entity layers --- crates/valence_client/src/lib.rs | 33 +++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/crates/valence_client/src/lib.rs b/crates/valence_client/src/lib.rs index 7e970d223..4ca4ef171 100644 --- a/crates/valence_client/src/lib.rs +++ b/crates/valence_client/src/lib.rs @@ -120,9 +120,10 @@ impl Plugin for ClientPlugin { spawn::initial_join.after(RegistrySet), update_chunk_load_dist, handle_layer_messages.after(update_chunk_load_dist), + despawn_entities_in_despawned_layers.after(handle_layer_messages), update_view_and_layers .after(spawn::initial_join) - .after(handle_layer_messages), + .after(despawn_entities_in_despawned_layers), spawn::update_respawn_position.after(update_view_and_layers), spawn::respawn.after(spawn::update_respawn_position), remove_entities.after(update_view_and_layers), @@ -452,7 +453,7 @@ impl Command for DisconnectClient { reason: self.reason.into(), }); - entity.remove::(); // TODO ? + entity.remove::(); } } } @@ -874,6 +875,32 @@ fn handle_layer_messages( ); } +/// Despawn all entities in despawned entity layers. +/// +/// TODO: this could be faster with an entity layer -> client mapping. (wait +/// until relations land?) +fn despawn_entities_in_despawned_layers( + mut clients: Query<(&mut VisibleEntityLayers, OldView, &mut EntityRemoveBuf), With>, + entity_layers: Query<(Entity, &EntityLayer), With>, + entities: Query<&EntityId>, +) { + if entity_layers.iter().len() > 0 { + for (mut visible_entity_layers, old_view, mut remove_buf) in &mut clients { + for (layer_entity, layer) in &entity_layers { + if visible_entity_layers.0.remove(&layer_entity) { + for pos in old_view.get().iter() { + for entity in layer.entities_at(pos) { + if let Ok(&id) = entities.get(entity) { + remove_buf.push(id.get()); + } + } + } + } + } + } + } +} + fn update_view_and_layers( mut clients: Query< ( @@ -897,7 +924,7 @@ fn update_view_and_layers( )>, >, chunk_layers: Query<&ChunkLayer>, - entity_layers: Query<&EntityLayer>, + entity_layers: Query<&EntityLayer, Without>, entity_ids: Query<&EntityId>, entity_init: Query<(EntityInitQuery, &Position)>, ) { From 5c04cbd62882a51925764077f4d9f95507b3bf21 Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 13 Jul 2023 02:48:18 -0700 Subject: [PATCH 09/47] layer bundle --- crates/valence_layer/src/lib.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/crates/valence_layer/src/lib.rs b/crates/valence_layer/src/lib.rs index 21e960239..0b47190d8 100644 --- a/crates/valence_layer/src/lib.rs +++ b/crates/valence_layer/src/lib.rs @@ -30,8 +30,12 @@ use bevy_app::prelude::*; use bevy_ecs::prelude::*; pub use chunk::ChunkLayer; pub use entity::EntityLayer; +use valence_biome::BiomeRegistry; +use valence_core::Server; +use valence_core::ident::Ident; use valence_core::protocol::encode::{PacketWriter, WritePacket}; use valence_core::protocol::{Encode, Packet}; +use valence_dimension::DimensionTypeRegistry; use valence_entity::{InitEntitiesSet, UpdateTrackedDataSet}; // Plugin is generic over the client type for hacky reasons. @@ -113,3 +117,24 @@ pub trait Layer { self.send_local(msg, |b| PacketWriter::new(b, threshold).write_packet(pkt)); } } + +/// Convenience [`Bundle`] for spawning a layer entity with both [`ChunkLayer`] and [`EntityLayer`]. +#[derive(Bundle)] +pub struct LayerBundle { + pub chunk: ChunkLayer, + pub entity: EntityLayer, +} + +impl LayerBundle { + pub fn new( + dimension_type_name: impl Into>, + dimensions: &DimensionTypeRegistry, + biomes: &BiomeRegistry, + server: &Server, + ) -> Self { + Self { + chunk: ChunkLayer::new(dimension_type_name, dimensions, biomes, server), + entity: EntityLayer::new(server), + } + } +} \ No newline at end of file From 0b98e7bdcb836a97dbcb7bb9d238c441e3a5552f Mon Sep 17 00:00:00 2001 From: Ryan Date: Sun, 16 Jul 2023 01:04:41 -0700 Subject: [PATCH 10/47] better entity layer despawn --- crates/valence_client/src/lib.rs | 69 ++++++++++-------------- crates/valence_layer/src/chunk.rs | 12 ++++- crates/valence_layer/src/chunk/loaded.rs | 1 + crates/valence_layer/src/entity.rs | 27 +++++++--- crates/valence_layer/src/lib.rs | 7 +-- 5 files changed, 65 insertions(+), 51 deletions(-) diff --git a/crates/valence_client/src/lib.rs b/crates/valence_client/src/lib.rs index 4ca4ef171..fe421d4b6 100644 --- a/crates/valence_client/src/lib.rs +++ b/crates/valence_client/src/lib.rs @@ -36,7 +36,6 @@ use packet::{ DeathMessageS2c, DisconnectS2c, GameEventKind, GameJoinS2c, GameStateChangeS2c, PlayerRespawnS2c, PlayerSpawnPositionS2c, }; -use spawn::PortalCooldown; use tracing::warn; use uuid::Uuid; use valence_biome::BiomeRegistry; @@ -120,10 +119,9 @@ impl Plugin for ClientPlugin { spawn::initial_join.after(RegistrySet), update_chunk_load_dist, handle_layer_messages.after(update_chunk_load_dist), - despawn_entities_in_despawned_layers.after(handle_layer_messages), update_view_and_layers .after(spawn::initial_join) - .after(despawn_entities_in_despawned_layers), + .after(handle_layer_messages), spawn::update_respawn_position.after(update_view_and_layers), spawn::respawn.after(spawn::update_respawn_position), remove_entities.after(update_view_and_layers), @@ -203,7 +201,7 @@ pub struct ClientBundle { pub has_respawn_screen: spawn::HasRespawnScreen, pub is_debug: spawn::IsDebug, pub is_flat: spawn::IsFlat, - pub portal_cooldown: PortalCooldown, + pub portal_cooldown: spawn::PortalCooldown, pub player: PlayerEntityBundle, } @@ -241,7 +239,7 @@ impl ClientBundle { hashed_seed: spawn::HashedSeed::default(), reduced_debug_info: spawn::ReducedDebugInfo::default(), is_debug: spawn::IsDebug::default(), - portal_cooldown: PortalCooldown::default(), + portal_cooldown: spawn::PortalCooldown::default(), player: PlayerEntityBundle { uuid: UniqueId(args.uuid), ..Default::default() @@ -705,6 +703,7 @@ fn handle_layer_messages( &mut EntityRemoveBuf, OldView, &OldVisibleChunkLayer, + &mut VisibleEntityLayers, &OldVisibleEntityLayers, )>, chunk_layers: Query<&ChunkLayer>, @@ -718,15 +717,18 @@ fn handle_layer_messages( mut client, mut remove_buf, old_view, - old_chunk_layer, - old_entity_layers, + old_visible_chunk_layer, + mut visible_entity_layers, + old_visible_entity_layers, )| { let old_view = old_view.get(); - if let Ok(chunk_layer) = chunk_layers.get(old_chunk_layer.get()) { + // Chunk layer messages + if let Ok(chunk_layer) = chunk_layers.get(old_visible_chunk_layer.get()) { let messages = chunk_layer.messages(); let bytes = messages.bytes(); + // Global messages for (msg, range) in messages.iter_global() { match msg { valence_layer::chunk::GlobalMsg::Packet => { @@ -742,6 +744,7 @@ fn handle_layer_messages( let mut chunk_biome_buf = vec![]; + // Local messages messages.query_local(old_view, |msg, range| match msg { valence_layer::chunk::LocalMsg::PacketAt { .. } => { client.write_packet_bytes(&bytes[range]); @@ -762,8 +765,11 @@ fn handle_layer_messages( } } Some(&0) => { - // Unload chunk. - client.write_packet(&UnloadChunkS2c { pos }); + // Unload chunk. If the chunk doesn't exist, then it shouldn't be + // loaded on the client and no packet needs to be sent. + if chunk_layer.chunk(pos).is_some() { + client.write_packet(&UnloadChunkS2c { pos }); + } } _ => panic!("invalid message data"), } @@ -777,11 +783,13 @@ fn handle_layer_messages( } } - for &layer_id in &old_entity_layers.0 { + // Entity layer messages + for &layer_id in &old_visible_entity_layers.0 { if let Ok(layer) = entity_layers.get(layer_id) { let messages = layer.messages(); let bytes = messages.bytes(); + // Global messages for (msg, range) in messages.iter_global() { match msg { valence_layer::entity::GlobalMsg::Packet => { @@ -792,9 +800,16 @@ fn handle_layer_messages( client.write_packet_bytes(&bytes[range]); } } + valence_layer::entity::GlobalMsg::DespawnLayer => { + // Remove this entity layer. The changes to the visible entity layer + // set will be detected by the `update_view_and_layers` system and + // despawning of entities will happen there. + visible_entity_layers.0.remove(&layer_id); + } } } + // Local messages messages.query_local(old_view, |msg, range| match msg { valence_layer::entity::LocalMsg::PacketAt { pos: _ } => { client.write_packet_bytes(&bytes[range]); @@ -805,7 +820,7 @@ fn handle_layer_messages( } } valence_layer::entity::LocalMsg::SpawnEntity { pos: _, src_layer } => { - if !old_entity_layers.0.contains(&src_layer) { + if !old_visible_entity_layers.0.contains(&src_layer) { let mut bytes = &bytes[range]; while let Ok(u64) = bytes.read_u64::() { @@ -844,7 +859,7 @@ fn handle_layer_messages( } } valence_layer::entity::LocalMsg::DespawnEntity { pos: _, dest_layer } => { - if !old_entity_layers.0.contains(&dest_layer) { + if !old_visible_entity_layers.0.contains(&dest_layer) { let mut bytes = &bytes[range]; while let Ok(id) = bytes.read_i32::() { @@ -875,32 +890,6 @@ fn handle_layer_messages( ); } -/// Despawn all entities in despawned entity layers. -/// -/// TODO: this could be faster with an entity layer -> client mapping. (wait -/// until relations land?) -fn despawn_entities_in_despawned_layers( - mut clients: Query<(&mut VisibleEntityLayers, OldView, &mut EntityRemoveBuf), With>, - entity_layers: Query<(Entity, &EntityLayer), With>, - entities: Query<&EntityId>, -) { - if entity_layers.iter().len() > 0 { - for (mut visible_entity_layers, old_view, mut remove_buf) in &mut clients { - for (layer_entity, layer) in &entity_layers { - if visible_entity_layers.0.remove(&layer_entity) { - for pos in old_view.get().iter() { - for entity in layer.entities_at(pos) { - if let Ok(&id) = entities.get(entity) { - remove_buf.push(id.get()); - } - } - } - } - } - } - } -} - fn update_view_and_layers( mut clients: Query< ( @@ -924,7 +913,7 @@ fn update_view_and_layers( )>, >, chunk_layers: Query<&ChunkLayer>, - entity_layers: Query<&EntityLayer, Without>, + entity_layers: Query<&EntityLayer>, entity_ids: Query<&EntityId>, entity_init: Query<(EntityInitQuery, &Position)>, ) { diff --git a/crates/valence_layer/src/chunk.rs b/crates/valence_layer/src/chunk.rs index bad21efa4..812be4751 100644 --- a/crates/valence_layer/src/chunk.rs +++ b/crates/valence_layer/src/chunk.rs @@ -11,7 +11,7 @@ use bevy_app::prelude::*; use bevy_ecs::prelude::*; use num_integer::div_ceil; use rustc_hash::FxHashMap; -use valence_biome::BiomeRegistry; +use valence_biome::{BiomeId, BiomeRegistry}; use valence_core::block_pos::BlockPos; use valence_core::chunk_pos::ChunkPos; use valence_core::ident::Ident; @@ -267,6 +267,16 @@ impl ChunkLayer { chunk.block_entity_mut(x, y, z) } + pub fn biome(&self, pos: impl Into) -> Option { + let (chunk, x, y, z) = self.chunk_and_offsets(pos.into())?; + Some(chunk.biome(x / 4, y / 4, z / 4)) + } + + pub fn set_biome(&mut self, pos: impl Into, biome: BiomeId) -> Option { + let (chunk, x, y, z) = self.chunk_and_offsets_mut(pos.into())?; + Some(chunk.set_biome(x / 4, y / 4, z / 4, biome)) + } + #[inline] fn chunk_and_offsets(&self, pos: BlockPos) -> Option<(&LoadedChunk, u32, u32, u32)> { let Some(y) = pos diff --git a/crates/valence_layer/src/chunk/loaded.rs b/crates/valence_layer/src/chunk/loaded.rs index c0fa7dbfd..1315bac73 100644 --- a/crates/valence_layer/src/chunk/loaded.rs +++ b/crates/valence_layer/src/chunk/loaded.rs @@ -177,6 +177,7 @@ impl LoadedChunk { /// Decrements the viewer count. For internal use only. #[doc(hidden)] + #[track_caller] pub fn dec_viewer_count(&self) { let old = self.viewer_count.fetch_sub(1, Ordering::Relaxed); debug_assert_ne!(old, 0, "viewer count underflow!"); diff --git a/crates/valence_layer/src/entity.rs b/crates/valence_layer/src/entity.rs index 00658eeee..bcfec9255 100644 --- a/crates/valence_layer/src/entity.rs +++ b/crates/valence_layer/src/entity.rs @@ -27,11 +27,15 @@ type EntityLayerMessages = Messages; #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub enum GlobalMsg { - /// Send packet data to all clients viewing the layer. + /// Send packet data to all clients viewing the layer. Message data is + /// serialized packet data. Packet, /// Send packet data to all clients viewing layer, except the client /// identified by `except`. PacketExcept { except: Entity }, + /// This layer was despawned and should be removed from the set of visible + /// entity layers. Message data is empty. + DespawnLayer, } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] @@ -43,9 +47,11 @@ pub enum LocalMsg { /// is the serialized form of [`Entity`]. SpawnEntityTransition { pos: ChunkPos, src_pos: ChunkPos }, /// Send packet data to all clients viewing the layer in view of `pos`. + /// Message data is serialized packet data. PacketAt { pos: ChunkPos }, /// Send packet data to all clients viewing the layer in view of `pos`, - /// except the client identified by `except`. + /// except the client identified by `except`. Message data is serialized + /// packet data. PacketAtExcept { pos: ChunkPos, except: Entity }, /// Despawn entities if the client is not already viewing `dest_layer`. /// Message data is the serialized form of `EntityId`. @@ -119,7 +125,8 @@ pub(super) fn build(app: &mut App) { ( ( change_entity_positions, - send_entity_updates::, + send_entity_update_messages::, + send_layer_despawn_messages, ready_entity_layers, ) .chain() @@ -129,7 +136,7 @@ pub(super) fn build(app: &mut App) { ); } -pub fn change_entity_positions( +fn change_entity_positions( entities: Query< ( Entity, @@ -236,7 +243,7 @@ pub fn change_entity_positions( } } -pub fn send_entity_updates( +fn send_entity_update_messages( entities: Query<(Entity, UpdateEntityQuery, Has), Without>, mut layers: Query<&mut EntityLayer>, ) { @@ -275,13 +282,19 @@ pub fn send_entity_updates( } } -pub fn ready_entity_layers(mut layers: Query<&mut EntityLayer>) { +fn send_layer_despawn_messages(mut layers: Query<&mut EntityLayer, With>) { + for mut layer in &mut layers { + layer.send_global(GlobalMsg::DespawnLayer, |_| {}); + } +} + +fn ready_entity_layers(mut layers: Query<&mut EntityLayer>) { for mut layer in &mut layers { layer.messages.ready(); } } -pub fn unready_entity_layers(mut layers: Query<&mut EntityLayer>) { +fn unready_entity_layers(mut layers: Query<&mut EntityLayer>) { for mut layer in &mut layers { layer.messages.unready(); } diff --git a/crates/valence_layer/src/lib.rs b/crates/valence_layer/src/lib.rs index 0b47190d8..0ecaadaf4 100644 --- a/crates/valence_layer/src/lib.rs +++ b/crates/valence_layer/src/lib.rs @@ -31,10 +31,10 @@ use bevy_ecs::prelude::*; pub use chunk::ChunkLayer; pub use entity::EntityLayer; use valence_biome::BiomeRegistry; -use valence_core::Server; use valence_core::ident::Ident; use valence_core::protocol::encode::{PacketWriter, WritePacket}; use valence_core::protocol::{Encode, Packet}; +use valence_core::Server; use valence_dimension::DimensionTypeRegistry; use valence_entity::{InitEntitiesSet, UpdateTrackedDataSet}; @@ -118,7 +118,8 @@ pub trait Layer { } } -/// Convenience [`Bundle`] for spawning a layer entity with both [`ChunkLayer`] and [`EntityLayer`]. +/// Convenience [`Bundle`] for spawning a layer entity with both [`ChunkLayer`] +/// and [`EntityLayer`] components. #[derive(Bundle)] pub struct LayerBundle { pub chunk: ChunkLayer, @@ -137,4 +138,4 @@ impl LayerBundle { entity: EntityLayer::new(server), } } -} \ No newline at end of file +} From e74330b56e1b2f6a22b2590f0f68a455eca1fd4c Mon Sep 17 00:00:00 2001 From: Ryan Date: Sun, 16 Jul 2023 03:08:08 -0700 Subject: [PATCH 11/47] fix most examples --- benches/many_players.rs | 24 ++-- crates/valence_advancement/src/lib.rs | 4 +- crates/valence_anvil/src/lib.rs | 2 +- crates/valence_anvil/src/parse_chunk.rs | 32 +++--- crates/valence_boss_bar/src/components.rs | 34 ++---- crates/valence_client/src/spawn.rs | 4 +- crates/valence_core/src/protocol/encode.rs | 32 +++++- crates/valence_core/src/text.rs | 2 +- crates/valence_core_macros/src/packet.rs | 5 +- crates/valence_inventory/src/lib.rs | 18 ++- crates/valence_inventory/src/validate.rs | 7 +- crates/valence_layer/src/chunk.rs | 37 +++++- crates/valence_layer/src/entity.rs | 19 +++- crates/valence_nbt/src/snbt.rs | 4 +- crates/valence_registry/src/codec.rs | 6 +- crates/valence_registry/src/lib.rs | 4 + crates/valence_world_border/src/lib.rs | 17 +-- examples/advancement.rs | 48 ++++++-- examples/anvil_loading.rs | 29 +++-- examples/bench_players.rs | 39 +++++-- examples/biomes.rs | 124 +++++++++++++++------ examples/block_entities.rs | 56 ++++++---- examples/boss_bar.rs | 49 +++++--- examples/building.rs | 54 +++++---- examples/chest.rs | 44 ++++++-- examples/combat.rs | 55 ++++++--- examples/cow_sphere.rs | 39 +++++-- examples/death.rs | 70 +++++++++--- examples/entity_hitbox.rs | 56 ++++++---- examples/game_of_life.rs | 49 +++++--- examples/parkour.rs | 45 ++++---- examples/particles.rs | 46 +++++--- examples/player_list.rs | 32 ++++-- examples/resource_pack.rs | 49 +++++--- examples/terrain.rs | 43 ++++--- examples/text.rs | 30 +++-- examples/world_border.rs | 14 ++- src/lib.rs | 4 +- src/tests/boss_bar.rs | 12 +- src/tests/client.rs | 18 +-- src/tests/world_border.rs | 2 + 41 files changed, 861 insertions(+), 397 deletions(-) diff --git a/benches/many_players.rs b/benches/many_players.rs index 81ab1d6eb..b5d59e034 100644 --- a/benches/many_players.rs +++ b/benches/many_players.rs @@ -16,6 +16,7 @@ use valence_dimension::DimensionTypeRegistry; use valence_entity::Position; use valence_instance::chunk::UnloadedChunk; use valence_instance::Instance; +use valence_layer::{ChunkLayer, EntityLayer}; use valence_network::NetworkPlugin; pub fn many_players(c: &mut Criterion) { @@ -28,7 +29,7 @@ fn run_many_players( func_name: &str, client_count: usize, view_dist: u8, - inst_size: i32, + world_size: i32, ) { let mut app = App::new(); @@ -45,20 +46,23 @@ fn run_many_players( app.update(); // Initialize plugins. - let mut inst = Instance::new( + let mut chunks = ChunkLayer::new( ident!("overworld"), app.world.resource::(), app.world.resource::(), app.world.resource::(), ); - for z in -inst_size..inst_size { - for x in -inst_size..inst_size { - inst.insert_chunk(ChunkPos::new(x, z), UnloadedChunk::new()); + for z in -world_size..world_size { + for x in -world_size..world_size { + chunks.insert_chunk(ChunkPos::new(x, z), UnloadedChunk::new()); } } - let inst_ent = app.world.spawn(inst).id(); + let layer = app + .world + .spawn((chunks, EntityLayer::new(app.world.resource::()))) + .id(); let mut clients = vec![]; @@ -66,12 +70,14 @@ fn run_many_players( for i in 0..client_count { let (mut bundle, helper) = create_mock_client(format!("client_{i}")); - bundle.player.location.0 = inst_ent; + bundle.visible_chunk_layer.0 = layer; + bundle.visible_entity_layers.0.insert(layer); + bundle.player.layer.0 = layer; bundle.view_distance.set(view_dist); let mut rng = rand::thread_rng(); - let x = rng.gen_range(-inst_size as f64 * 16.0..=inst_size as f64 * 16.0); - let z = rng.gen_range(-inst_size as f64 * 16.0..=inst_size as f64 * 16.0); + let x = rng.gen_range(-world_size as f64 * 16.0..=world_size as f64 * 16.0); + let z = rng.gen_range(-world_size as f64 * 16.0..=world_size as f64 * 16.0); bundle.player.position.set(DVec3::new(x, 64.0, z)); diff --git a/crates/valence_advancement/src/lib.rs b/crates/valence_advancement/src/lib.rs index 844f29b48..925f8ff8e 100644 --- a/crates/valence_advancement/src/lib.rs +++ b/crates/valence_advancement/src/lib.rs @@ -132,7 +132,9 @@ impl<'w, 's> UpdateAdvancementCachedBytesQuery<'w, 's> { if let Some(a_children) = a_children { for a_child in a_children.iter() { - let Ok(c_identifier) = criteria_query.get(*a_child) else { continue; }; + let Ok(c_identifier) = criteria_query.get(*a_child) else { + continue; + }; pkt.criteria.push((c_identifier.0.borrowed(), ())); } } diff --git a/crates/valence_anvil/src/lib.rs b/crates/valence_anvil/src/lib.rs index ee9eb1b7e..74fc4a8a8 100644 --- a/crates/valence_anvil/src/lib.rs +++ b/crates/valence_anvil/src/lib.rs @@ -326,7 +326,7 @@ fn update_client_views( if loc != &*old_loc || view != old_view || old_loc.is_added() { let Ok((layer, mut anvil)) = chunk_layers.get_mut(loc.0) else { - continue + continue; }; let queue_pos = |pos| { diff --git a/crates/valence_anvil/src/parse_chunk.rs b/crates/valence_anvil/src/parse_chunk.rs index 71aa547dc..616209408 100644 --- a/crates/valence_anvil/src/parse_chunk.rs +++ b/crates/valence_anvil/src/parse_chunk.rs @@ -69,7 +69,7 @@ pub(crate) fn parse_chunk( biome_map: &BTreeMap, BiomeId>, // TODO: replace with biome registry arg. ) -> Result { let Some(Value::List(List::Compound(sections))) = nbt.remove("sections") else { - return Err(ParseChunkError::MissingSections) + return Err(ParseChunkError::MissingSections); }; if sections.is_empty() { @@ -96,7 +96,7 @@ pub(crate) fn parse_chunk( for mut section in sections { let Some(Value::Byte(sect_y)) = section.remove("Y") else { - return Err(ParseChunkError::MissingSectionY) + return Err(ParseChunkError::MissingSectionY); }; let sect_y = (sect_y as i32 - min_sect_y) as u32; @@ -106,11 +106,11 @@ pub(crate) fn parse_chunk( } let Some(Value::Compound(mut block_states)) = section.remove("block_states") else { - return Err(ParseChunkError::MissingBlockStates) + return Err(ParseChunkError::MissingBlockStates); }; let Some(Value::List(List::Compound(palette))) = block_states.remove("palette") else { - return Err(ParseChunkError::MissingBlockPalette) + return Err(ParseChunkError::MissingBlockPalette); }; if !(1..BLOCKS_PER_SECTION).contains(&palette.len()) { @@ -121,11 +121,11 @@ pub(crate) fn parse_chunk( for mut block in palette { let Some(Value::String(name)) = block.remove("Name") else { - return Err(ParseChunkError::MissingBlockName) + return Err(ParseChunkError::MissingBlockName); }; let Some(block_kind) = BlockKind::from_str(ident_path(&name)) else { - return Err(ParseChunkError::UnknownBlockName(name)) + return Err(ParseChunkError::UnknownBlockName(name)); }; let mut state = block_kind.to_state(); @@ -133,15 +133,15 @@ pub(crate) fn parse_chunk( if let Some(Value::Compound(properties)) = block.remove("Properties") { for (key, value) in properties { let Value::String(value) = value else { - return Err(ParseChunkError::BadPropValueType) + return Err(ParseChunkError::BadPropValueType); }; let Some(prop_name) = PropName::from_str(&key) else { - return Err(ParseChunkError::UnknownPropName(key)) + return Err(ParseChunkError::UnknownPropName(key)); }; let Some(prop_value) = PropValue::from_str(&value) else { - return Err(ParseChunkError::UnknownPropValue(value)) + return Err(ParseChunkError::UnknownPropValue(value)); }; state = state.set(prop_name, prop_value); @@ -157,7 +157,7 @@ pub(crate) fn parse_chunk( debug_assert!(converted_block_palette.len() > 1); let Some(Value::LongArray(data)) = block_states.remove("data") else { - return Err(ParseChunkError::MissingBlockStateData) + return Err(ParseChunkError::MissingBlockStateData); }; let bits_per_idx = bit_width(converted_block_palette.len() - 1).max(4); @@ -181,7 +181,7 @@ pub(crate) fn parse_chunk( let idx = (u64 >> (bits_per_idx * j)) & mask; let Some(block) = converted_block_palette.get(idx as usize).cloned() else { - return Err(ParseChunkError::BadBlockPaletteIndex) + return Err(ParseChunkError::BadBlockPaletteIndex); }; let x = i % 16; @@ -196,11 +196,11 @@ pub(crate) fn parse_chunk( } let Some(Value::Compound(biomes)) = section.get("biomes") else { - return Err(ParseChunkError::MissingBiomes) + return Err(ParseChunkError::MissingBiomes); }; let Some(Value::List(List::String(palette))) = biomes.get("palette") else { - return Err(ParseChunkError::MissingBiomePalette) + return Err(ParseChunkError::MissingBiomePalette); }; if !(1..BIOMES_PER_SECTION).contains(&palette.len()) { @@ -211,7 +211,7 @@ pub(crate) fn parse_chunk( for biome_name in palette { let Ok(ident) = Ident::>::new(biome_name) else { - return Err(ParseChunkError::BadBiomeName) + return Err(ParseChunkError::BadBiomeName); }; converted_biome_palette @@ -224,7 +224,7 @@ pub(crate) fn parse_chunk( debug_assert!(converted_biome_palette.len() > 1); let Some(Value::LongArray(data)) = biomes.get("data") else { - return Err(ParseChunkError::MissingBiomeData) + return Err(ParseChunkError::MissingBiomeData); }; let bits_per_idx = bit_width(converted_biome_palette.len() - 1); @@ -248,7 +248,7 @@ pub(crate) fn parse_chunk( let idx = (u64 >> (bits_per_idx * j)) & mask; let Some(biome) = converted_biome_palette.get(idx as usize).cloned() else { - return Err(ParseChunkError::BadBiomePaletteIndex) + return Err(ParseChunkError::BadBiomePaletteIndex); }; let x = i % 4; diff --git a/crates/valence_boss_bar/src/components.rs b/crates/valence_boss_bar/src/components.rs index 86f422e45..385b62925 100644 --- a/crates/valence_boss_bar/src/components.rs +++ b/crates/valence_boss_bar/src/components.rs @@ -7,7 +7,7 @@ use valence_core::text::Text; use valence_core::uuid::UniqueId; /// The bundle of components that make up a boss bar. -#[derive(Bundle)] +#[derive(Bundle, Default)] pub struct BossBarBundle { pub id: UniqueId, pub title: BossBarTitle, @@ -17,43 +17,26 @@ pub struct BossBarBundle { pub viewers: BossBarViewers, } -impl BossBarBundle { - pub fn new( - title: Text, - color: BossBarColor, - division: BossBarDivision, - flags: BossBarFlags, - ) -> BossBarBundle { - BossBarBundle { - id: UniqueId::default(), - title: BossBarTitle(title), - health: BossBarHealth(1.0), - style: BossBarStyle { color, division }, - flags, - viewers: BossBarViewers::default(), - } - } -} - /// The title of a boss bar. -#[derive(Component, Clone)] +#[derive(Component, Clone, Default)] pub struct BossBarTitle(pub Text); /// The health of a boss bar. -#[derive(Component)] +#[derive(Component, Default)] pub struct BossBarHealth(pub f32); /// The style of a boss bar. This includes the color and division of the boss /// bar. -#[derive(Component)] +#[derive(Component, Default)] pub struct BossBarStyle { pub color: BossBarColor, pub division: BossBarDivision, } /// The color of a boss bar. -#[derive(Component, Copy, Clone, PartialEq, Eq, Debug, Encode, Decode)] +#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug, Encode, Decode)] pub enum BossBarColor { + #[default] Pink, Blue, Red, @@ -64,8 +47,9 @@ pub enum BossBarColor { } /// The division of a boss bar. -#[derive(Component, Copy, Clone, PartialEq, Eq, Debug, Encode, Decode)] +#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug, Encode, Decode)] pub enum BossBarDivision { + #[default] NoDivision, SixNotches, TenNotches, @@ -75,7 +59,7 @@ pub enum BossBarDivision { /// The flags of a boss bar (darken sky, dragon bar, create fog). #[bitfield(u8)] -#[derive(Component, PartialEq, Eq, Encode, Decode)] +#[derive(Component, PartialEq, Eq, Default, Encode, Decode)] pub struct BossBarFlags { pub darken_sky: bool, pub dragon_bar: bool, diff --git a/crates/valence_client/src/spawn.rs b/crates/valence_client/src/spawn.rs index ca798ff15..5914d224a 100644 --- a/crates/valence_client/src/spawn.rs +++ b/crates/valence_client/src/spawn.rs @@ -90,7 +90,7 @@ pub(super) fn initial_join( ) { for (mut client, visible_chunk_layer, spawn) in &mut clients { let Ok(chunk_layer) = chunk_layers.get(visible_chunk_layer.0) else { - continue + continue; }; let dimension_names: Vec>> = codec @@ -165,7 +165,7 @@ pub(super) fn respawn( } let Ok(chunk_layer) = chunk_layers.get(loc.0) else { - continue + continue; }; let dimension_name = chunk_layer.dimension_type_name(); diff --git a/crates/valence_core/src/protocol/encode.rs b/crates/valence_core/src/protocol/encode.rs index f540d48a1..5e21a2380 100644 --- a/crates/valence_core/src/protocol/encode.rs +++ b/crates/valence_core/src/protocol/encode.rs @@ -229,7 +229,11 @@ impl WritePacket for Mut<'_, T> { } } -/// An implementor of [`WritePacket`] backed by a `Vec` reference. +/// An implementor of [`WritePacket`] backed by a `Vec` mutable reference. +/// +/// Packets are written by appending to the contained vec. If an error occurs +/// while writing, the written bytes are truncated away. +#[derive(Debug)] pub struct PacketWriter<'a> { pub buf: &'a mut Vec, pub threshold: Option, @@ -242,19 +246,35 @@ impl<'a> PacketWriter<'a> { } impl WritePacket for PacketWriter<'_> { + #[cfg_attr(not(feature = "compression"), track_caller)] fn write_packet_fallible

(&mut self, pkt: &P) -> anyhow::Result<()> where P: Packet + Encode, { - #[cfg(feature = "compression")] + let start = self.buf.len(); + + let res; + if let Some(threshold) = self.threshold { - encode_packet_compressed(self.buf, pkt, threshold) + #[cfg(feature = "compression")] + { + res = encode_packet_compressed(self.buf, pkt, threshold); + } + + #[cfg(not(feature = "compression"))] + { + let _ = threshold; + panic!("\"compression\" feature must be enabled to write compressed packets"); + } } else { - encode_packet(self.buf, pkt) + res = encode_packet(self.buf, pkt) + }; + + if res.is_err() { + self.buf.truncate(start); } - #[cfg(not(feature = "compression"))] - encode_packet(self.buf, pkt) + res } fn write_packet_bytes(&mut self, bytes: &[u8]) { diff --git a/crates/valence_core/src/text.rs b/crates/valence_core/src/text.rs index eb13f87d6..550ee6735 100644 --- a/crates/valence_core/src/text.rs +++ b/crates/valence_core/src/text.rs @@ -84,7 +84,7 @@ impl<'de> Deserialize<'de> for Text { fn visit_seq>(self, mut seq: A) -> Result { let Some(mut res) = seq.next_element()? else { - return Ok(Text::default()) + return Ok(Text::default()); }; while let Some(child) = seq.next_element::()? { diff --git a/crates/valence_core_macros/src/packet.rs b/crates/valence_core_macros/src/packet.rs index 9e9a1e942..f1eda6a18 100644 --- a/crates/valence_core_macros/src/packet.rs +++ b/crates/valence_core_macros/src/packet.rs @@ -13,7 +13,10 @@ pub(super) fn derive_packet(item: TokenStream) -> Result { }; let Some(packet_id) = packet_attr.id else { - return Err(Error::new(packet_attr.span, "missing `id = ...` value from packet attribute")); + return Err(Error::new( + packet_attr.span, + "missing `id = ...` value from packet attribute", + )); }; add_trait_bounds(&mut input.generics, quote!(::std::fmt::Debug)); diff --git a/crates/valence_inventory/src/lib.rs b/crates/valence_inventory/src/lib.rs index 4f274f6f3..2668081f1 100644 --- a/crates/valence_inventory/src/lib.rs +++ b/crates/valence_inventory/src/lib.rs @@ -777,16 +777,12 @@ fn handle_click_slot( for packet in packets.iter() { let Some(pkt) = packet.decode::() else { // Not the packet we're looking for. - continue + continue; }; - let Ok(( - mut client, - mut client_inv, - mut inv_state, - open_inventory, - mut cursor_item - )) = clients.get_mut(packet.client) else { + let Ok((mut client, mut client_inv, mut inv_state, open_inventory, mut cursor_item)) = + clients.get_mut(packet.client) + else { // The client does not exist, ignore. continue; }; @@ -1117,8 +1113,10 @@ fn handle_creative_inventory_action( ) { for packet in packets.iter() { if let Some(pkt) = packet.decode::() { - let Ok((mut client, mut inventory, mut inv_state, game_mode)) = clients.get_mut(packet.client) else { - continue + let Ok((mut client, mut inventory, mut inv_state, game_mode)) = + clients.get_mut(packet.client) + else { + continue; }; if *game_mode != GameMode::Creative { diff --git a/crates/valence_inventory/src/validate.rs b/crates/valence_inventory/src/validate.rs index ee14d3bc9..d07f17b26 100644 --- a/crates/valence_inventory/src/validate.rs +++ b/crates/valence_inventory/src/validate.rs @@ -206,9 +206,10 @@ pub(super) fn validate_click_slot_packet( .iter() .filter_map(|s| s.item.as_ref()) .next() - .map(|s| s.item) else { - bail!("shift click must move an item"); - }; + .map(|s| s.item) + else { + bail!("shift click must move an item"); + }; let Some(old_slot_kind) = window.slot(packet.slot_idx as u16).map(|s| s.item) else { bail!("shift click must move an item"); diff --git a/crates/valence_layer/src/chunk.rs b/crates/valence_layer/src/chunk.rs index 812be4751..ef7562266 100644 --- a/crates/valence_layer/src/chunk.rs +++ b/crates/valence_layer/src/chunk.rs @@ -5,17 +5,23 @@ pub mod loaded; mod paletted_container; pub mod unloaded; +use std::borrow::Cow; use std::collections::hash_map::{Entry, OccupiedEntry, VacantEntry}; use bevy_app::prelude::*; use bevy_ecs::prelude::*; +use glam::{DVec3, Vec3}; use num_integer::div_ceil; use rustc_hash::FxHashMap; use valence_biome::{BiomeId, BiomeRegistry}; use valence_core::block_pos::BlockPos; use valence_core::chunk_pos::ChunkPos; use valence_core::ident::Ident; +use valence_core::particle::{Particle, ParticleS2c}; use valence_core::protocol::array::LengthPrefixedArray; +use valence_core::protocol::encode::WritePacket; +use valence_core::protocol::packet::sound::{PlaySoundS2c, Sound, SoundCategory}; +use valence_core::protocol::{Encode, Packet}; use valence_core::Server; use valence_dimension::DimensionTypeRegistry; use valence_nbt::Compound; @@ -338,7 +344,6 @@ impl ChunkLayer { &self.messages } - /* // TODO: move to `valence_particle`. /// Puts a particle effect at the given position in the world. The particle /// effect is visible to all players in the instance with the @@ -354,7 +359,10 @@ impl ChunkLayer { ) { let position = position.into(); - self.write_packet_at( + self.send_local_packet( + LocalMsg::PacketAt { + pos: ChunkPos::from_dvec3(position), + }, &ParticleS2c { particle: Cow::Borrowed(particle), long_distance, @@ -363,7 +371,6 @@ impl ChunkLayer { max_speed, count, }, - ChunkPos::from_dvec3(position), ); } @@ -381,7 +388,10 @@ impl ChunkLayer { ) { let position = position.into(); - self.write_packet_at( + self.send_local_packet( + LocalMsg::PacketAt { + pos: ChunkPos::from_dvec3(position), + }, &PlaySoundS2c { id: sound.to_id(), category, @@ -390,9 +400,8 @@ impl ChunkLayer { pitch, seed: rand::random(), }, - ChunkPos::from_dvec3(position), ); - }*/ + } } #[derive(Debug)] @@ -501,6 +510,22 @@ impl<'a> VacantChunkEntry<'a> { } } +impl WritePacket for ChunkLayer { + fn write_packet_fallible

(&mut self, packet: &P) -> anyhow::Result<()> + where + P: Packet + Encode, + { + self.send_global_packet(GlobalMsg::Packet, packet); + + // TODO: propagate error up. + Ok(()) + } + + fn write_packet_bytes(&mut self, bytes: &[u8]) { + self.send_global_bytes(GlobalMsg::Packet, bytes) + } +} + pub(super) fn build(app: &mut App) { app.add_systems( PostUpdate, diff --git a/crates/valence_layer/src/entity.rs b/crates/valence_layer/src/entity.rs index bcfec9255..dda44623d 100644 --- a/crates/valence_layer/src/entity.rs +++ b/crates/valence_layer/src/entity.rs @@ -7,7 +7,8 @@ use bevy_ecs::query::Has; use rustc_hash::FxHashMap; use valence_core::chunk_pos::ChunkPos; use valence_core::despawn::Despawned; -use valence_core::protocol::encode::PacketWriter; +use valence_core::protocol::encode::{PacketWriter, WritePacket}; +use valence_core::protocol::{Encode, Packet}; use valence_core::Server; use valence_entity::query::UpdateEntityQuery; use valence_entity::{EntityId, EntityLayerId, OldEntityLayerId, OldPosition, Position}; @@ -119,6 +120,22 @@ impl Layer for EntityLayer { } } +impl WritePacket for EntityLayer { + fn write_packet_fallible

(&mut self, packet: &P) -> anyhow::Result<()> + where + P: Packet + Encode, + { + self.send_global_packet(GlobalMsg::Packet, packet); + + // TODO: propagate error up. + Ok(()) + } + + fn write_packet_bytes(&mut self, bytes: &[u8]) { + self.send_global_bytes(GlobalMsg::Packet, bytes) + } +} + pub(super) fn build(app: &mut App) { app.add_systems( PostUpdate, diff --git a/crates/valence_nbt/src/snbt.rs b/crates/valence_nbt/src/snbt.rs index e66449983..d92125ec1 100644 --- a/crates/valence_nbt/src/snbt.rs +++ b/crates/valence_nbt/src/snbt.rs @@ -642,7 +642,9 @@ mod tests { *more.get("larr").unwrap().as_long_array().unwrap(), vec![1, 2, 3] ); - let List::String(list) = cpd.get("empty").unwrap().as_list().unwrap() else { panic!() }; + let List::String(list) = cpd.get("empty").unwrap().as_list().unwrap() else { + panic!() + }; assert_eq!(list[0], "Bibabo"); assert_eq!( from_snbt_str("\"\\n\"").unwrap_err().error_type, diff --git a/crates/valence_registry/src/codec.rs b/crates/valence_registry/src/codec.rs index c53da1e4b..2c570bd94 100644 --- a/crates/valence_registry/src/codec.rs +++ b/crates/valence_registry/src/codec.rs @@ -64,7 +64,7 @@ impl Default for RegistryCodec { let Value::Compound(mut outer) = v else { error!("registry {reg_name} is not a compound"); - continue + continue; }; let values = match outer.remove("value") { @@ -79,7 +79,7 @@ impl Default for RegistryCodec { for mut value in values { let Some(Value::String(name)) = value.remove("name") else { error!("missing \"name\" string in value for {reg_name}"); - continue + continue; }; let name = match Ident::new(name) { @@ -92,7 +92,7 @@ impl Default for RegistryCodec { let Some(Value::Compound(element)) = value.remove("element") else { error!("missing \"element\" compound in value for {reg_name}"); - continue + continue; }; reg_values.push(RegistryValue { name, element }); diff --git a/crates/valence_registry/src/lib.rs b/crates/valence_registry/src/lib.rs index bff49bbea..0984f60c4 100644 --- a/crates/valence_registry/src/lib.rs +++ b/crates/valence_registry/src/lib.rs @@ -91,6 +91,10 @@ impl Registry { self.items.shift_remove(name.as_str()) } + pub fn clear(&mut self) { + self.items.clear(); + } + pub fn get(&self, name: Ident<&str>) -> Option<&V> { self.items.get(name.as_str()) } diff --git a/crates/valence_world_border/src/lib.rs b/crates/valence_world_border/src/lib.rs index 5e0a9b378..020dfaa2d 100644 --- a/crates/valence_world_border/src/lib.rs +++ b/crates/valence_world_border/src/lib.rs @@ -117,7 +117,7 @@ impl Plugin for WorldBorderPlugin { center_change, warn_time_change, warn_blocks_change, - portal_teleport_bounary_change, + portal_teleport_boundary_change, ) .in_set(UpdateWorldBorderPerInstanceSet), ) @@ -223,6 +223,7 @@ impl MovingWorldBorder { /// An event for controlling world border diameter. /// Setting duration to 0 will move the border to `new_diameter` immediately, /// otherwise it will interpolate to `new_diameter` over `duration` time. +/// /// ``` /// fn change_diameter( /// event_writer: EventWriter, @@ -230,17 +231,17 @@ impl MovingWorldBorder { /// duration: Duration, /// ) { /// event_writer.send(SetWorldBorderSizeEvent { -/// instance: entity, +/// entity_layer: entity, /// new_diameter: diameter, /// duration, -/// }) +/// }); /// } /// ``` #[derive(Event, Clone, Debug)] pub struct SetWorldBorderSizeEvent { - /// The instance to change border size. Note that this instance must contain - /// the [`WorldBorderBundle`] bundle - pub instance: Entity, + /// The [`EntityLayer`] to change border size. Note that this entity layer must contain + /// the [`WorldBorderBundle`] bundle. + pub entity_layer: Entity, /// The new diameter of the world border pub new_diameter: f64, /// How long the border takes to reach it new_diameter in millisecond. Set @@ -253,7 +254,7 @@ fn wb_size_change( mut instances: Query<(&WorldBorderDiameter, Option<&mut MovingWorldBorder>)>, ) { for SetWorldBorderSizeEvent { - instance, + entity_layer: instance, new_diameter, duration, } in events.iter() @@ -362,7 +363,7 @@ fn warn_blocks_change( } } -fn portal_teleport_bounary_change( +fn portal_teleport_boundary_change( mut wbs: Query< ( &mut Instance, diff --git a/examples/advancement.rs b/examples/advancement.rs index c0cdacb1a..5d0e56daf 100644 --- a/examples/advancement.rs +++ b/examples/advancement.rs @@ -47,21 +47,21 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - instance.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); } } for z in -25..25 { for x in -25..25 { - instance.set_block([x, 64, z], BlockState::GRASS_BLOCK); + layer.chunk.set_block([x, 64, z], BlockState::GRASS_BLOCK); } } - commands.spawn(instance); + commands.spawn(layer); let root_criteria = commands .spawn(( @@ -165,11 +165,31 @@ fn setup( } fn init_clients( - mut clients: Query<(&mut EntityLayerId, &mut Position, &mut GameMode), Added>, - instances: Query>, + mut clients: Query< + ( + &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut Position, + &mut GameMode, + ), + Added, + >, + layers: Query>, ) { - for (mut loc, mut pos, mut game_mode) in &mut clients { - loc.0 = instances.single(); + for ( + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut pos, + mut game_mode, + ) in &mut clients + { + let layer = layers.single(); + + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); pos.set([0.5, 65.0, 0.5]); *game_mode = GameMode::Creative; } @@ -237,7 +257,11 @@ fn sneak( if sneaking.state == SneakState::Stop { continue; } - let Ok((mut advancement_client_update, mut root_criteria_done)) = client.get_mut(sneaking.client) else { continue; }; + let Ok((mut advancement_client_update, mut root_criteria_done)) = + client.get_mut(sneaking.client) + else { + continue; + }; root_criteria_done.0 = !root_criteria_done.0; match root_criteria_done.0 { true => advancement_client_update.criteria_done(root_criteria), @@ -262,7 +286,11 @@ fn tab_change( let root2_criteria = root2_criteria.single(); let root = root.single(); for tab_change in tab_change.iter() { - let Ok((mut advancement_client_update, mut tab_change_count)) = client.get_mut(tab_change.client) else { continue; }; + let Ok((mut advancement_client_update, mut tab_change_count)) = + client.get_mut(tab_change.client) + else { + continue; + }; if let Some(ref opened) = tab_change.opened_tab { if opened.as_str() == "custom:root2" { tab_change_count.0 += 1; diff --git a/examples/anvil_loading.rs b/examples/anvil_loading.rs index 064361e2c..2f9ccb5a6 100644 --- a/examples/anvil_loading.rs +++ b/examples/anvil_loading.rs @@ -55,9 +55,9 @@ fn setup( let instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); let mut level = AnvilLevel::new(&cli.path, &biomes); - // Force a 16x16 area of chunks around the origin to be loaded at all times, - // similar to spawn chunks in vanilla. This isn't necessary, but it is done to - // demonstrate that it is possible. + // Force a 16x16 area of chunks around the origin to be loaded at all times. + // This is similar to "spawn chunks" in vanilla. This isn't necessary for the + // example to function, but it's done to demonstrate that it's possible. for z in -8..8 { for x in -8..8 { let pos = ChunkPos::new(x, z); @@ -74,19 +74,30 @@ fn init_clients( mut clients: Query< ( &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, &mut Position, &mut GameMode, - &mut IsFlat, ), Added, >, - instances: Query>, + layers: Query>, ) { - for (mut loc, mut pos, mut game_mode, mut is_flat) in &mut clients { - loc.0 = instances.single(); - pos.set(SPAWN_POS); + for ( + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut pos, + mut game_mode, + ) in &mut clients + { + let layer = layers.single(); + + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); + pos.set([0.5, 65.0, 0.5]); *game_mode = GameMode::Creative; - is_flat.0 = true; } } diff --git a/examples/bench_players.rs b/examples/bench_players.rs index 9b7c8bddf..4f147e702 100644 --- a/examples/bench_players.rs +++ b/examples/bench_players.rs @@ -3,6 +3,7 @@ use std::time::Instant; use valence::prelude::*; +use valence_client::{VisibleChunkLayer, VisibleEntityLayers}; const SPAWN_Y: i32 = 64; @@ -54,29 +55,51 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - instance.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); } } for z in -50..50 { for x in -50..50 { - instance.set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); + layer + .chunk + .set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); } } - commands.spawn(instance); + commands.spawn(layer); } fn init_clients( - mut clients: Query<(&mut EntityLayerId, &mut Position, &mut GameMode), Added>, - instances: Query>, + mut clients: Query< + ( + &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut Position, + &mut GameMode, + ), + Added, + >, + layers: Query, With)>, ) { - for (mut loc, mut pos, mut game_mode) in &mut clients { - loc.0 = instances.single(); + for ( + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut pos, + mut game_mode, + ) in &mut clients + { + let layer = layers.single(); + + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); *game_mode = GameMode::Creative; } diff --git a/examples/biomes.rs b/examples/biomes.rs index 475743a95..2f2d8f159 100644 --- a/examples/biomes.rs +++ b/examples/biomes.rs @@ -1,64 +1,118 @@ #![allow(clippy::type_complexity)] +use rand::seq::IteratorRandom; +use rand::Rng; use valence::prelude::*; +use valence_biome::BiomeEffects; const SPAWN_Y: i32 = 0; +const SIZE: i32 = 5; pub fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) - .add_systems(Update, (init_clients, despawn_disconnected_clients)) + .add_systems( + Update, + (init_clients, despawn_disconnected_clients, set_biomes), + ) .run(); } fn setup( mut commands: Commands, dimensions: Res, - biomes: Res, - biome_reg: Res, + mut biomes: ResMut, server: Res, ) { - let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); - - let biome_count = biome_reg.iter().count() as u32; - - for z in -5..5 { - for x in -5..5 { - let mut chunk = UnloadedChunk::with_height(64); - // Set chunk blocks - for z in 0..16 { - for x in 0..16 { - chunk.set_block_state(x, 63, z, BlockState::GRASS_BLOCK); - } - } - - // Set the biomes of the chunk to a 4x4x4 grid of biomes - for bz in 0..4 { - for bx in 0..4 { - for by in 0..chunk.height() / 4 { - let nth = (bx + bz * 4 + by * 4 * 4) % biome_count; - - let biome_id = biome_reg.iter().nth(nth as usize).unwrap().0; - - chunk.set_biome(bx, by, bz, biome_id); - } - } - } - instance.insert_chunk([x, z], chunk); + let colors = [ + 0xeb4034, 0xffffff, 0xe3d810, 0x1fdbde, 0x1121d1, 0xe60ed7, 0xe68f0e, 0x840ee6, 0x0ee640, + ]; + + biomes.clear(); + + // Client will be sad if you don't have a "plains" biome. + biomes.insert(ident!("plains"), Biome::default()); + + for color in colors { + let name = Ident::new(format!("biome_{color:x}")).unwrap(); + + let biome = Biome { + effects: BiomeEffects { + grass_color: Some(color), + ..Default::default() + }, + ..Default::default() + }; + + biomes.insert(name, biome); + } + + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + + for z in -SIZE..SIZE { + for x in -SIZE..SIZE { + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); } } - commands.spawn(instance); + for x in -SIZE * 16..SIZE * 16 { + for z in -SIZE * 16..SIZE * 16 { + layer + .chunk + .set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); + } + } + + commands.spawn(layer); +} + +fn set_biomes(mut layers: Query<&mut ChunkLayer>, biomes: Res) { + let mut layer = layers.single_mut(); + + let mut rng = rand::thread_rng(); + + for _ in 0..10 { + let x = rng.gen_range(-SIZE * 16..SIZE * 16); + let z = rng.gen_range(-SIZE * 16..SIZE * 16); + + let biome = biomes + .iter() + .choose(&mut rng) + .map(|(biome, _, _)| biome) + .unwrap_or_default(); + + layer.set_biome([x, SPAWN_Y, z], biome); + } } fn init_clients( - mut clients: Query<(&mut Position, &mut EntityLayerId, &mut GameMode), Added>, - instances: Query>, + mut clients: Query< + ( + &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut Position, + &mut GameMode, + ), + Added, + >, + layers: Query, With)>, ) { - for (mut pos, mut loc, mut game_mode) in &mut clients { + for ( + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut pos, + mut game_mode, + ) in &mut clients + { + let layer = layers.single(); + + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); - loc.0 = instances.single(); *game_mode = GameMode::Creative; } } diff --git a/examples/block_entities.rs b/examples/block_entities.rs index 3a0f1f33d..d891e2118 100644 --- a/examples/block_entities.rs +++ b/examples/block_entities.rs @@ -26,26 +26,28 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - instance.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); } } for z in 0..16 { for x in 0..8 { - instance.set_block([x, FLOOR_Y, z], BlockState::WHITE_CONCRETE); + layer + .chunk + .set_block([x, FLOOR_Y, z], BlockState::WHITE_CONCRETE); } } - instance.set_block( + layer.chunk.set_block( [3, FLOOR_Y + 1, 1], BlockState::CHEST.set(PropName::Facing, PropValue::West), ); - instance.set_block( + layer.chunk.set_block( SIGN_POS, Block { state: BlockState::OAK_SIGN.set(PropName::Rotation, PropValue::_4), @@ -55,26 +57,41 @@ fn setup( }, ); - instance.set_block( + layer.chunk.set_block( SKULL_POS, BlockState::PLAYER_HEAD.set(PropName::Rotation, PropValue::_12), ); - commands.spawn(instance); + commands.spawn(layer); } fn init_clients( mut clients: Query< - (&mut EntityLayerId, &mut Position, &mut Look, &mut GameMode), + ( + &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut Position, + &mut GameMode, + ), Added, >, - instances: Query>, + layers: Query, With)>, ) { - for (mut loc, mut pos, mut look, mut game_mode) in &mut clients { - loc.0 = instances.single(); - pos.set([1.5, FLOOR_Y as f64 + 1.0, 1.5]); - *look = Look::new(-90.0, 0.0); + for ( + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut pos, + mut game_mode, + ) in &mut clients + { + let layer = layers.single(); + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); + pos.set([0.0, FLOOR_Y as f64 + 1.0, 0.0]); *game_mode = GameMode::Creative; } } @@ -83,18 +100,19 @@ fn event_handler( clients: Query<(&Username, &Properties, &UniqueId)>, mut messages: EventReader, mut block_interacts: EventReader, - mut instances: Query<&mut Instance>, + mut layers: Query<&mut ChunkLayer>, ) { - let mut instance = instances.single_mut(); + let mut layer = layers.single_mut(); + for ChatMessageEvent { client, message, .. } in messages.iter() { let Ok((username, _, _)) = clients.get(*client) else { - continue + continue; }; - let nbt = instance.block_entity_mut(SIGN_POS).unwrap(); + let nbt = layer.block_entity_mut(SIGN_POS).unwrap(); nbt.insert("Text2", message.to_string().color(Color::DARK_GREEN)); nbt.insert("Text3", format!("~{username}").italic()); @@ -109,14 +127,14 @@ fn event_handler( { if *hand == Hand::Main && *position == SKULL_POS { let Ok((_, properties, uuid)) = clients.get(*client) else { - continue + continue; }; let Some(textures) = properties.textures() else { continue; }; - *instance.block_entity_mut(SKULL_POS).unwrap() = compound! { + *layer.block_entity_mut(SKULL_POS).unwrap() = compound! { "SkullOwner" => compound! { "Id" => uuid.0, "Properties" => compound! { diff --git a/examples/boss_bar.rs b/examples/boss_bar.rs index 8d72fdd2e..db4c950dd 100644 --- a/examples/boss_bar.rs +++ b/examples/boss_bar.rs @@ -25,28 +25,33 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - instance.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); } } for z in -25..25 { for x in -25..25 { - instance.set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); + layer + .chunk + .set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); } } - commands.spawn(BossBarBundle::new( - Text::text("Boss bar"), - BossBarColor::Blue, - BossBarDivision::TenNotches, - BossBarFlags::new(), - )); - - commands.spawn(instance); + commands.spawn(layer); + + commands.spawn(BossBarBundle { + title: BossBarTitle("Boss Bar".into_text()), + health: BossBarHealth(1.0), + style: BossBarStyle { + color: BossBarColor::Blue, + division: BossBarDivision::TenNotches, + }, + ..Default::default() + }); } fn init_clients( @@ -55,17 +60,33 @@ fn init_clients( Entity, &mut Client, &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, &mut Position, &mut GameMode, ), Added, >, mut boss_bar_viewers: Query<&mut BossBarViewers>, - instances: Query>, + layers: Query>, ) { let mut boss_bar_viewers = boss_bar_viewers.single_mut(); - for (entity, mut client, mut loc, mut pos, mut game_mode) in &mut clients { - loc.0 = instances.single(); + + for ( + entity, + mut client, + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut pos, + mut game_mode, + ) in &mut clients + { + let layer = layers.single(); + + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); pos.set([0.5, SPAWN_Y as f64 + 1.0, 0.5]); *game_mode = GameMode::Creative; diff --git a/examples/building.rs b/examples/building.rs index 1c0ab212b..39b3761db 100644 --- a/examples/building.rs +++ b/examples/building.rs @@ -31,21 +31,23 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - instance.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); } } - for z in -25..25 { - for x in -25..25 { - instance.set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); + for z in -50..50 { + for x in -50..50 { + layer + .chunk + .set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); } } - commands.spawn(instance); + commands.spawn(layer); } fn init_clients( @@ -53,17 +55,31 @@ fn init_clients( ( &mut Client, &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, &mut Position, &mut GameMode, ), Added, >, - instances: Query>, + layers: Query, With)>, ) { - for (mut client, mut loc, mut pos, mut game_mode) in &mut clients { - *game_mode = GameMode::Creative; - loc.0 = instances.single(); + for ( + mut client, + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut pos, + mut game_mode, + ) in &mut clients + { + let layer = layers.single(); + + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); + *game_mode = GameMode::Creative; client.send_chat_message("Welcome to Valence! Build something cool.".italic()); } @@ -89,44 +105,44 @@ fn toggle_gamemode_on_sneak( fn digging_creative_mode( clients: Query<&GameMode>, - mut instances: Query<&mut Instance>, + mut layers: Query<&mut ChunkLayer>, mut events: EventReader, ) { - let mut instance = instances.single_mut(); + let mut layer = layers.single_mut(); for event in events.iter() { let Ok(game_mode) = clients.get(event.client) else { continue; }; if *game_mode == GameMode::Creative && event.state == DiggingState::Start { - instance.set_block(event.position, BlockState::AIR); + layer.set_block(event.position, BlockState::AIR); } } } fn digging_survival_mode( clients: Query<&GameMode>, - mut instances: Query<&mut Instance>, + mut layers: Query<&mut ChunkLayer>, mut events: EventReader, ) { - let mut instance = instances.single_mut(); + let mut layer = layers.single_mut(); for event in events.iter() { let Ok(game_mode) = clients.get(event.client) else { continue; }; if *game_mode == GameMode::Survival && event.state == DiggingState::Stop { - instance.set_block(event.position, BlockState::AIR); + layer.set_block(event.position, BlockState::AIR); } } } fn place_blocks( mut clients: Query<(&mut Inventory, &GameMode, &HeldItem)>, - mut instances: Query<&mut Instance>, + mut layers: Query<&mut ChunkLayer>, mut events: EventReader, ) { - let mut instance = instances.single_mut(); + let mut layer = layers.single_mut(); for event in events.iter() { let Ok((mut inventory, game_mode, held)) = clients.get_mut(event.client) else { @@ -159,6 +175,6 @@ fn place_blocks( } } let real_pos = event.position.get_in_direction(event.face); - instance.set_block(real_pos, block_kind.to_state()); + layer.set_block(real_pos, block_kind.to_state()); } } diff --git a/examples/chest.rs b/examples/chest.rs index afb862fcc..2aaab908f 100644 --- a/examples/chest.rs +++ b/examples/chest.rs @@ -28,37 +28,61 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - instance.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); } } for z in -25..25 { for x in -25..25 { - instance.set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); + layer + .chunk + .set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); } } - instance.set_block(CHEST_POS, BlockState::CHEST); - commands.spawn(instance); + layer.chunk.set_block(CHEST_POS, BlockState::CHEST); + + commands.spawn(layer); let inventory = Inventory::with_title( InventoryKind::Generic9x3, "Extra".italic() + " Chesty".not_italic().bold().color(Color::RED) + " Chest".not_italic(), ); + commands.spawn(inventory); } fn init_clients( - mut clients: Query<(&mut EntityLayerId, &mut Position, &mut GameMode), Added>, - instances: Query>, + mut clients: Query< + ( + &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut Position, + &mut GameMode, + ), + Added, + >, + layers: Query, With)>, ) { - for (mut loc, mut pos, mut game_mode) in &mut clients { - loc.0 = instances.single(); - pos.set([0.5, SPAWN_Y as f64 + 1.0, 0.5]); + for ( + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut pos, + mut game_mode, + ) in &mut clients + { + let layer = layers.single(); + + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); + pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); *game_mode = GameMode::Creative; } } diff --git a/examples/combat.rs b/examples/combat.rs index b806bb176..4803fb892 100644 --- a/examples/combat.rs +++ b/examples/combat.rs @@ -2,6 +2,7 @@ use bevy_ecs::query::WorldQuery; use glam::Vec3Swizzles; +use rand::Rng; use valence::entity::EntityStatuses; use valence::prelude::*; @@ -38,14 +39,16 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - instance.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); } } + let mut rng = rand::thread_rng(); + // Create circular arena. for z in -ARENA_RADIUS..ARENA_RADIUS { for x in -ARENA_RADIUS..ARENA_RADIUS { @@ -55,34 +58,49 @@ fn setup( continue; } - let block = if rand::random::() < dist { + let block = if rng.gen::() < dist { BlockState::STONE } else { BlockState::DEEPSLATE }; for y in 0..SPAWN_Y { - instance.set_block([x, y, z], block); + layer.chunk.set_block([x, y, z], block); } } } - commands.spawn(instance); + commands.spawn(layer); } fn init_clients( - mut clients: Query<(Entity, &mut EntityLayerId, &mut Position), Added>, - instances: Query>, - mut commands: Commands, + mut clients: Query< + ( + &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut Position, + &mut GameMode, + ), + Added, + >, + layers: Query, With)>, ) { - for (entity, mut loc, mut pos) in &mut clients { - loc.0 = instances.single(); - pos.set([0.5, SPAWN_Y as f64, 0.5]); - - commands.entity(entity).insert((CombatState { - last_attacked_tick: 0, - has_bonus_knockback: false, - },)); + for ( + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut pos, + mut game_mode, + ) in &mut clients + { + let layer = layers.single(); + + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); + pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); + *game_mode = GameMode::Creative; } } @@ -113,9 +131,10 @@ fn handle_combat_events( .. } in interact_entity.iter() { - let Ok([mut attacker, mut victim]) = clients.get_many_mut([attacker_client, victim_client]) else { + let Ok([mut attacker, mut victim]) = clients.get_many_mut([attacker_client, victim_client]) + else { // Victim or attacker does not exist, or the attacker is attacking itself. - continue + continue; }; if server.current_tick() - victim.state.last_attacked_tick < 10 { diff --git a/examples/cow_sphere.rs b/examples/cow_sphere.rs index abad213c6..90e9e1707 100644 --- a/examples/cow_sphere.rs +++ b/examples/cow_sphere.rs @@ -36,22 +36,22 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - instance.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); } } - instance.set_block(SPAWN_POS, BlockState::BEDROCK); + layer.chunk.set_block(SPAWN_POS, BlockState::BEDROCK); - let instance_id = commands.spawn(instance).id(); + let layer_id = commands.spawn(layer).id(); commands.spawn_batch([0; SPHERE_AMOUNT].map(|_| { ( SpherePartBundle { - location: EntityLayerId(instance_id), + layer: EntityLayerId(layer_id), ..Default::default() }, SpherePart, @@ -60,17 +60,36 @@ fn setup( } fn init_clients( - mut clients: Query<(&mut EntityLayerId, &mut Position, &mut GameMode), Added>, - instances: Query>, + mut clients: Query< + ( + &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut Position, + &mut GameMode, + ), + Added, + >, + layers: Query, With)>, ) { - for (mut loc, mut pos, mut game_mode) in &mut clients { - loc.0 = instances.single(); + for ( + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut pos, + mut game_mode, + ) in &mut clients + { + let layer = layers.single(); + + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); pos.set([ SPAWN_POS.x as f64 + 0.5, SPAWN_POS.y as f64 + 1.0, SPAWN_POS.z as f64 + 0.5, ]); - *game_mode = GameMode::Creative; } } diff --git a/examples/death.rs b/examples/death.rs index 04648fbcb..dbe7a3c5d 100644 --- a/examples/death.rs +++ b/examples/death.rs @@ -33,31 +33,54 @@ fn setup( BlockState::DEEPSLATE, BlockState::MAGMA_BLOCK, ] { - let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - instance.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); } } for z in -25..25 { for x in -25..25 { - instance.set_block([x, SPAWN_Y, z], block); + layer.chunk.set_block([x, SPAWN_Y, z], block); } } - commands.spawn(instance); + commands.spawn(layer); } } fn init_clients( - mut clients: Query<(&mut Client, &mut EntityLayerId, &mut Position), Added>, - instances: Query>, + mut clients: Query< + ( + &mut Client, + &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut Position, + &mut GameMode, + ), + Added, + >, + layers: Query, With)>, ) { - for (mut client, mut loc, mut pos) in &mut clients { - loc.0 = instances.iter().next().unwrap(); + for ( + mut client, + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut pos, + mut game_mode, + ) in &mut clients + { + let layer = layers.iter().next().unwrap(); + + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); + *game_mode = GameMode::Creative; client.send_chat_message( "Welcome to Valence! Sneak to die in the game (but not in real life).".italic(), @@ -76,20 +99,35 @@ fn squat_and_die(mut clients: Query<&mut Client>, mut events: EventReader, + mut clients: Query<( + &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut RespawnPosition, + )>, mut events: EventReader, - instances: Query>, + layers: Query, With)>, ) { for event in events.iter() { - if let Ok((mut loc, mut spawn_pos)) = clients.get_mut(event.client) { - spawn_pos.pos = BlockPos::new(0, SPAWN_Y, 0); + if let Ok(( + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut respawn_pos, + )) = clients.get_mut(event.client) + { + respawn_pos.pos = BlockPos::new(0, SPAWN_Y, 0); - // make the client respawn in another instance - let idx = instances.iter().position(|i| i == loc.0).unwrap(); + // make the client respawn in another chunk layer. - let count = instances.iter().len(); + let idx = layers.iter().position(|l| l == layer_id.0).unwrap(); + let count = layers.iter().len(); + let layer = layers.into_iter().nth((idx + 1) % count).unwrap(); - loc.0 = instances.into_iter().nth((idx + 1) % count).unwrap(); + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.clear(); + visible_entity_layers.0.insert(layer); } } } diff --git a/examples/entity_hitbox.rs b/examples/entity_hitbox.rs index 575c69a69..7be49ab94 100644 --- a/examples/entity_hitbox.rs +++ b/examples/entity_hitbox.rs @@ -28,39 +28,54 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - instance.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); } } for z in -25..25 { for x in -25..25 { - instance.set_block([x, 64, z], BlockState::GRASS_BLOCK); + layer.chunk.set_block([x, 64, z], BlockState::GRASS_BLOCK); } } - commands.spawn(instance); + commands.spawn(layer); } fn init_clients( mut clients: Query< ( + &mut Client, &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, &mut Position, &mut GameMode, - &mut Client, ), Added, >, - instances: Query>, + layers: Query, With)>, ) { - for (mut loc, mut pos, mut game_mode, mut client) in &mut clients { - loc.0 = instances.single(); - pos.set([0.5, 65.0, 0.5]); + for ( + mut client, + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut pos, + mut game_mode, + ) in &mut clients + { + let layer = layers.single(); + + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); + pos.set([0.0, 65.0, 0.0]); *game_mode = GameMode::Creative; + client.send_chat_message("To spawn an entity, press shift. F3 + B to activate hitboxes"); } } @@ -75,54 +90,53 @@ fn spawn_entity( continue; } - let (position, location) = client_query.get(sneaking.client).unwrap(); + let (position, layer) = client_query.get(sneaking.client).unwrap(); let position = *position; - let location = *location; + let layer = *layer; match rand::thread_rng().gen_range(0..7) { 0 => commands.spawn(SheepEntityBundle { position, - location, + layer, entity_name_visible: NameVisible(true), ..Default::default() }), 1 => commands.spawn(PigEntityBundle { position, - location, + layer, entity_name_visible: NameVisible(true), ..Default::default() }), 2 => commands.spawn(ZombieEntityBundle { position, - location, + layer, entity_name_visible: NameVisible(true), ..Default::default() }), 3 => commands.spawn(ZombieHorseEntityBundle { position, - location, + layer, entity_name_visible: NameVisible(true), ..Default::default() }), 4 => commands.spawn(WardenEntityBundle { position, - location, + layer, entity_name_visible: NameVisible(true), entity_pose: entity::Pose(Pose::Digging), ..Default::default() }), 5 => commands.spawn(WardenEntityBundle { position, - location, + layer, entity_name_visible: NameVisible(true), ..Default::default() }), 6 => commands.spawn(HoglinEntityBundle { position, - location, + layer, entity_name_visible: NameVisible(true), - ..Default::default() }), _ => unreachable!(), @@ -148,7 +162,9 @@ fn intersections(query: Query<(Entity, &Hitbox)>, mut name_query: Query<&mut ent } for (entity, value) in intersections { - let Ok(mut name) = name_query.get_mut(entity) else { continue; }; + let Ok(mut name) = name_query.get_mut(entity) else { + continue; + }; name.0 = Some(format!("{value}").into()); } } diff --git a/examples/game_of_life.rs b/examples/game_of_life.rs index 81f952dea..ab8b426b2 100644 --- a/examples/game_of_life.rs +++ b/examples/game_of_life.rs @@ -48,21 +48,21 @@ fn setup( biome.effects.grass_color = Some(0x00ff00); } - let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); for z in -10..10 { for x in -10..10 { - instance.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); } } for z in BOARD_MIN_Z..=BOARD_MAX_Z { for x in BOARD_MIN_X..=BOARD_MAX_X { - instance.set_block([x, BOARD_Y, z], BlockState::DIRT); + layer.chunk.set_block([x, BOARD_Y, z], BlockState::DIRT); } } - commands.spawn(instance); + commands.spawn(layer); commands.insert_resource(LifeBoard { paused: true, @@ -72,19 +72,42 @@ fn setup( } fn init_clients( - mut clients: Query<(&mut Client, &mut EntityLayerId, &mut Position), Added>, - instances: Query>, + mut clients: Query< + ( + &mut Client, + &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut Position, + &mut GameMode, + ), + Added, + >, + layers: Query, With)>, ) { - for (mut client, mut loc, mut pos) in &mut clients { + for ( + mut client, + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut pos, + mut game_mode, + ) in &mut clients + { + let layer = layers.single(); + + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); + pos.set([0.0, 65.0, 0.0]); + *game_mode = GameMode::Survival; + client.send_chat_message("Welcome to Conway's game of life in Minecraft!".italic()); client.send_chat_message( "Sneak to toggle running the simulation and the left mouse button to bring blocks to \ life." .italic(), ); - - loc.0 = instances.single(); - pos.set(SPAWN_POS); } } @@ -163,14 +186,14 @@ fn toggle_cell_on_dig(mut events: EventReader, mut board: ResMut, - mut instances: Query<&mut Instance>, + mut layers: Query<&mut ChunkLayer>, server: Res, ) { if !board.paused && server.current_tick() % 2 == 0 { board.update(); } - let mut instance = instances.single_mut(); + let mut layer = layers.single_mut(); for z in BOARD_MIN_Z..=BOARD_MAX_Z { for x in BOARD_MIN_X..=BOARD_MAX_X { @@ -180,7 +203,7 @@ fn update_board( BlockState::DIRT }; - instance.set_block([x, BOARD_Y, z], block); + layer.set_block([x, BOARD_Y, z], block); } } } diff --git a/examples/parkour.rs b/examples/parkour.rs index 4a758fbdd..d5a5b94c0 100644 --- a/examples/parkour.rs +++ b/examples/parkour.rs @@ -8,6 +8,7 @@ use rand::Rng; use valence::prelude::*; use valence::protocol::packet::sound::{Sound, SoundCategory}; use valence_client::message::SendMessage; +use valence_client::spawn::IsFlat; const START_POS: BlockPos = BlockPos::new(0, 100, 0); const VIEW_DIST: u8 = 10; @@ -52,7 +53,7 @@ fn init_clients( ( Entity, &mut Client, - &mut EntityLayerId, + &mut VisibleChunkLayer, &mut IsFlat, &mut GameMode, ), @@ -63,8 +64,10 @@ fn init_clients( biomes: Res, mut commands: Commands, ) { - for (entity, mut client, mut loc, mut is_flat, mut game_mode) in clients.iter_mut() { - loc.0 = entity; + for (entity, mut client, mut visible_chunk_layer, mut is_flat, mut game_mode) in + clients.iter_mut() + { + visible_chunk_layer.0 = entity; is_flat.0 = true; *game_mode = GameMode::Adventure; @@ -78,9 +81,9 @@ fn init_clients( last_block_timestamp: 0, }; - let instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + let layer = ChunkLayer::new(ident!("overworld"), &dimensions, &biomes, &server); - commands.entity(entity).insert((state, instance)); + commands.entity(entity).insert((state, layer)); } } @@ -90,10 +93,10 @@ fn reset_clients( &mut Position, &mut Look, &mut GameState, - &mut Instance, + &mut ChunkLayer, )>, ) { - for (mut client, mut pos, mut look, mut state, mut instance) in clients.iter_mut() { + for (mut client, mut pos, mut look, mut state, mut layer) in clients.iter_mut() { let out_of_bounds = (pos.0.y as i32) < START_POS.y - 32; if out_of_bounds || state.is_added() { @@ -111,21 +114,21 @@ fn reset_clients( // Init chunks. for pos in ChunkView::new(ChunkPos::from_block_pos(START_POS), VIEW_DIST).iter() { - instance.insert_chunk(pos, UnloadedChunk::new()); + layer.insert_chunk(pos, UnloadedChunk::new()); } state.score = 0; state.combo = 0; for block in &state.blocks { - instance.set_block(*block, BlockState::AIR); + layer.set_block(*block, BlockState::AIR); } state.blocks.clear(); state.blocks.push_back(START_POS); - instance.set_block(START_POS, BlockState::STONE); + layer.set_block(START_POS, BlockState::STONE); for _ in 0..10 { - generate_next_block(&mut state, &mut instance, false); + generate_next_block(&mut state, &mut layer, false); } pos.set([ @@ -139,8 +142,8 @@ fn reset_clients( } } -fn manage_blocks(mut clients: Query<(&mut Client, &Position, &mut GameState, &mut Instance)>) { - for (mut client, pos, mut state, mut instance) in clients.iter_mut() { +fn manage_blocks(mut clients: Query<(&mut Client, &Position, &mut GameState, &mut ChunkLayer)>) { + for (mut client, pos, mut state, mut layer) in clients.iter_mut() { let pos_under_player = BlockPos::new( (pos.0.x - 0.5).round() as i32, pos.0.y as i32 - 1, @@ -168,7 +171,7 @@ fn manage_blocks(mut clients: Query<(&mut Client, &Position, &mut GameState, &mu } for _ in 0..index { - generate_next_block(&mut state, &mut instance, true) + generate_next_block(&mut state, &mut layer, true) } let pitch = 0.9 + ((state.combo as f32) - 1.0) * 0.05; @@ -187,27 +190,27 @@ fn manage_blocks(mut clients: Query<(&mut Client, &Position, &mut GameState, &mu } } -fn manage_chunks(mut clients: Query<(&Position, &OldPosition, &mut Instance), With>) { - for (pos, old_pos, mut instance) in &mut clients { +fn manage_chunks(mut clients: Query<(&Position, &OldPosition, &mut ChunkLayer), With>) { + for (pos, old_pos, mut layer) in &mut clients { let old_view = ChunkView::new(old_pos.chunk_pos(), VIEW_DIST); let view = ChunkView::new(pos.chunk_pos(), VIEW_DIST); if old_view != view { for pos in old_view.diff(view) { - instance.remove_chunk(pos); + layer.remove_chunk(pos); } for pos in view.diff(old_view) { - instance.chunk_entry(pos).or_default(); + layer.chunk_entry(pos).or_default(); } } } } -fn generate_next_block(state: &mut GameState, instance: &mut Instance, in_game: bool) { +fn generate_next_block(state: &mut GameState, layer: &mut ChunkLayer, in_game: bool) { if in_game { let removed_block = state.blocks.pop_front().unwrap(); - instance.set_block(removed_block, BlockState::AIR); + layer.set_block(removed_block, BlockState::AIR); state.score += 1 } @@ -223,7 +226,7 @@ fn generate_next_block(state: &mut GameState, instance: &mut Instance, in_game: let mut rng = rand::thread_rng(); - instance.set_block(block_pos, *BLOCK_TYPES.choose(&mut rng).unwrap()); + layer.set_block(block_pos, *BLOCK_TYPES.choose(&mut rng).unwrap()); state.blocks.push_back(block_pos); // Combo System diff --git a/examples/particles.rs b/examples/particles.rs index 5d3e5869f..f67488c06 100644 --- a/examples/particles.rs +++ b/examples/particles.rs @@ -26,28 +26,48 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - instance.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); } } - instance.set_block([0, SPAWN_Y, 0], BlockState::BEDROCK); + layer.chunk.set_block([0, SPAWN_Y, 0], BlockState::BEDROCK); - commands.spawn(instance); + commands.spawn(layer); commands.insert_resource(ParticleVec(create_particle_vec())); } fn init_clients( - mut clients: Query<(&mut EntityLayerId, &mut Position, &mut GameMode), Added>, - instances: Query>, + mut clients: Query< + ( + &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut Position, + &mut GameMode, + ), + Added, + >, + layers: Query, With)>, ) { - for (mut loc, mut pos, mut game_mode) in &mut clients { - loc.0 = instances.single(); - pos.set([0.5, SPAWN_Y as f64 + 1.0, 0.5]); + for ( + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut pos, + mut game_mode, + ) in &mut clients + { + let layer = layers.single(); + + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); + pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); *game_mode = GameMode::Creative; } } @@ -55,7 +75,7 @@ fn init_clients( fn manage_particles( particles: Res, server: Res, - mut instances: Query<&mut Instance>, + mut layers: Query<&mut ChunkLayer>, mut particle_idx: Local, ) { if server.current_tick() % 10 != 0 { @@ -71,10 +91,10 @@ fn manage_particles( let pos = [0.5, SPAWN_Y as f64 + 2.0, 5.0]; let offset = [0.5, 0.5, 0.5]; - let mut instance = instances.single_mut(); + let mut layer = layers.single_mut(); - instance.play_particle(particle, true, pos, offset, 0.1, 100); - instance.set_action_bar(name.bold()); + layer.play_particle(particle, true, pos, offset, 0.1, 100); + layer.set_action_bar(name.bold()); } fn dbg_name(dbg: &impl fmt::Debug) -> String { diff --git a/examples/player_list.rs b/examples/player_list.rs index abb6861d3..b8f7aa926 100644 --- a/examples/player_list.rs +++ b/examples/player_list.rs @@ -32,21 +32,23 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - instance.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); } } for z in -25..25 { for x in -25..25 { - instance.set_block([x, SPAWN_Y, z], BlockState::LIGHT_GRAY_WOOL); + layer + .chunk + .set_block([x, SPAWN_Y, z], BlockState::LIGHT_GRAY_WOOL); } } - commands.spawn(instance); + commands.spawn(layer); commands.spawn(PlayerListEntryBundle { uuid: UniqueId(PLAYER_UUID_1), @@ -59,17 +61,31 @@ fn init_clients( mut clients: Query< ( &mut Client, - &mut Position, &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut Position, &mut GameMode, ), Added, >, - instances: Query>, + layers: Query, With)>, ) { - for (mut client, mut pos, mut loc, mut game_mode) in &mut clients { + for ( + mut client, + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut pos, + mut game_mode, + ) in &mut clients + { + let layer = layers.single(); + + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); - loc.0 = instances.single(); *game_mode = GameMode::Creative; client.send_chat_message( diff --git a/examples/resource_pack.rs b/examples/resource_pack.rs index 50fa983e2..a85519633 100644 --- a/examples/resource_pack.rs +++ b/examples/resource_pack.rs @@ -1,6 +1,5 @@ #![allow(clippy::type_complexity)] -use valence::entity::player::PlayerEntityBundle; use valence::entity::sheep::SheepEntityBundle; use valence::prelude::*; use valence_client::message::SendMessage; @@ -30,24 +29,24 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - instance.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); } } for z in -25..25 { for x in -25..25 { - instance.set_block([x, SPAWN_Y, z], BlockState::BEDROCK); + layer.chunk.set_block([x, SPAWN_Y, z], BlockState::BEDROCK); } } - let instance_ent = commands.spawn(instance).id(); + let layer_ent = commands.spawn(layer).id(); commands.spawn(SheepEntityBundle { - location: EntityLayerId(instance_ent), + layer: EntityLayerId(layer_ent), position: Position::new([0.0, SPAWN_Y as f64 + 1.0, 2.0]), look: Look::new(180.0, 0.0), head_yaw: HeadYaw(180.0), @@ -56,21 +55,37 @@ fn setup( } fn init_clients( - mut clients: Query<(Entity, &UniqueId, &mut Client, &mut GameMode), Added>, - instances: Query>, - mut commands: Commands, + mut clients: Query< + ( + &mut Client, + &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut Position, + &mut GameMode, + ), + Added, + >, + layers: Query, With)>, ) { - for (entity, uuid, mut client, mut game_mode) in &mut clients { + for ( + mut client, + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut pos, + mut game_mode, + ) in &mut clients + { + let layer = layers.single(); + + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); + pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); *game_mode = GameMode::Creative; client.send_chat_message("Hit the sheep to prompt for the resource pack.".italic()); - - commands.entity(entity).insert(PlayerEntityBundle { - location: EntityLayerId(instances.single()), - position: Position::new([0.0, SPAWN_Y as f64 + 1.0, 0.0]), - uuid: *uuid, - ..Default::default() - }); } } diff --git a/examples/terrain.rs b/examples/terrain.rs index 1999a3e2c..20629ac2e 100644 --- a/examples/terrain.rs +++ b/examples/terrain.rs @@ -104,48 +104,59 @@ fn setup( receiver: finished_receiver, }); - let instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + let layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); - commands.spawn(instance); + commands.spawn(layer); } fn init_clients( mut clients: Query< ( &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, &mut Position, - &mut IsFlat, &mut GameMode, ), Added, >, - instances: Query>, + layers: Query, With)>, ) { - for (mut loc, mut pos, mut is_flat, mut game_mode) in &mut clients { - loc.0 = instances.single(); + for ( + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut pos, + mut game_mode, + ) in &mut clients + { + let layer = layers.single(); + + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); pos.set(SPAWN_POS); - is_flat.0 = true; *game_mode = GameMode::Creative; } } -fn remove_unviewed_chunks(mut instances: Query<&mut Instance>) { - instances +fn remove_unviewed_chunks(mut layers: Query<&mut ChunkLayer>) { + layers .single_mut() - .retain_chunks(|_, chunk| chunk.is_viewed_mut()); + .retain_chunks(|_, chunk| chunk.viewer_count_mut() > 0); } fn update_client_views( - mut instances: Query<&mut Instance>, + mut layers: Query<&mut ChunkLayer>, mut clients: Query<(&mut Client, View, OldView)>, mut state: ResMut, ) { - let instance = instances.single_mut(); + let layer = layers.single_mut(); for (client, view, old_view) in &mut clients { let view = view.get(); let queue_pos = |pos| { - if instance.chunk(pos).is_none() { + if layer.chunk(pos).is_none() { match state.pending.entry(pos) { Entry::Occupied(mut oe) => { if let Some(priority) = oe.get_mut() { @@ -173,13 +184,13 @@ fn update_client_views( } } -fn send_recv_chunks(mut instances: Query<&mut Instance>, state: ResMut) { - let mut instance = instances.single_mut(); +fn send_recv_chunks(mut layers: Query<&mut ChunkLayer>, state: ResMut) { + let mut layer = layers.single_mut(); let state = state.into_inner(); // Insert the chunks that are finished generating into the instance. for (pos, chunk) in state.receiver.drain() { - instance.insert_chunk(pos, chunk); + layer.insert_chunk(pos, chunk); assert!(state.pending.remove(&pos).is_some()); } diff --git a/examples/text.rs b/examples/text.rs index 7533b663f..6839d5240 100644 --- a/examples/text.rs +++ b/examples/text.rs @@ -19,21 +19,23 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - instance.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); } } for z in -25..25 { for x in -25..25 { - instance.set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); + layer + .chunk + .set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); } } - commands.spawn(instance); + commands.spawn(layer); } fn init_clients( @@ -42,15 +44,29 @@ fn init_clients( &mut Client, &mut Position, &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, &mut GameMode, ), Added, >, - instances: Query>, + layers: Query, With)>, ) { - for (mut client, mut pos, mut loc, mut game_mode) in &mut clients { + for ( + mut client, + mut pos, + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut game_mode, + ) in &mut clients + { + let layer = layers.single(); + pos.0 = [0.0, SPAWN_Y as f64 + 1.0, 0.0].into(); - loc.0 = instances.single(); + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); *game_mode = GameMode::Creative; client.send_chat_message("Welcome to the text example.".bold()); diff --git a/examples/world_border.rs b/examples/world_border.rs index 491f64bbc..f04afc138 100644 --- a/examples/world_border.rs +++ b/examples/world_border.rs @@ -33,22 +33,24 @@ fn setup( biomes: Res, dimensions: Res, ) { - let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - instance.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); } } for z in -25..25 { for x in -25..25 { - instance.set_block([x, SPAWN_Y, z], BlockState::MOSSY_COBBLESTONE); + layer + .chunk + .set_block([x, SPAWN_Y, z], BlockState::MOSSY_COBBLESTONE); } } commands - .spawn(instance) + .spawn(layer) .insert(WorldBorderBundle::new([0.0, 0.0], 1.0)); } @@ -113,7 +115,7 @@ fn border_expand( }; event_writer.send(SetWorldBorderSizeEvent { - instance: loc.0, + entity_layer: loc.0, new_diameter: size.get() + 1.0, duration: Duration::from_secs(1), }); @@ -143,7 +145,7 @@ fn border_controls( }; event_writer.send(SetWorldBorderSizeEvent { - instance: entity, + entity_layer: entity, new_diameter: diameter.get() + value, duration: Duration::from_millis(speed as u64), }) diff --git a/src/lib.rs b/src/lib.rs index 1214b3bd3..4b03a10a2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,7 +88,7 @@ pub mod prelude { pub use valence_client::title::SetTitle as _; pub use valence_client::{ despawn_disconnected_clients, Client, Ip, OldView, OldViewDistance, Properties, - RespawnPosition, Username, View, ViewDistance, + RespawnPosition, Username, View, ViewDistance, VisibleChunkLayer, VisibleEntityLayers, }; pub use valence_core::block_pos::BlockPos; pub use valence_core::chunk_pos::{ChunkPos, ChunkView}; @@ -115,7 +115,7 @@ pub mod prelude { pub use valence_layer::chunk::{ Block, BlockRef, Chunk, ChunkLayer, LoadedChunk, UnloadedChunk, }; - pub use valence_layer::entity::EntityLayer; + pub use valence_layer::{EntityLayer, LayerBundle}; pub use valence_nbt::Compound; #[cfg(feature = "network")] pub use valence_network::{ diff --git a/src/tests/boss_bar.rs b/src/tests/boss_bar.rs index 317f10730..ff215731a 100644 --- a/src/tests/boss_bar.rs +++ b/src/tests/boss_bar.rs @@ -8,7 +8,7 @@ use valence_boss_bar::{ use valence_core::despawn::Despawned; use valence_core::text::Text; -use crate::testing::{scenario_single_client, MockClientHelper}; +use crate::testing::{scenario_single_client, MockClientHelper, ScenarioSingleClient}; #[test] fn test_intialize_on_join() { @@ -182,7 +182,11 @@ fn test_client_disconnection() { } fn prepare(app: &mut App) -> (Entity, MockClientHelper, Entity) { - let (client_ent, mut client_helper) = scenario_single_client(app); + let ScenarioSingleClient { + client, + helper, + layer, + } = scenario_single_client(app); // Process a tick to get past the "on join" logic. app.update(); @@ -203,6 +207,6 @@ fn prepare(app: &mut App) -> (Entity, MockClientHelper, Entity) { app.update(); } - client_helper.clear_received(); - (client_ent, client_helper, boss_bar) + helper.clear_received(); + (client, helper, boss_bar) } diff --git a/src/tests/client.rs b/src/tests/client.rs index c8c29c02b..8e5913d00 100644 --- a/src/tests/client.rs +++ b/src/tests/client.rs @@ -15,8 +15,9 @@ use valence_entity::{EntityLayerId, Position}; use valence_instance::chunk::UnloadedChunk; use valence_instance::packet::{ChunkDataS2c, UnloadChunkS2c}; use valence_instance::Instance; +use valence_layer::ChunkLayer; -use crate::testing::{create_mock_client, scenario_single_client}; +use crate::testing::{create_mock_client, scenario_single_client, ScenarioSingleClient}; #[test] fn client_chunk_view_change() { @@ -103,22 +104,23 @@ fn client_chunk_view_change() { fn entity_chunk_spawn_despawn() { let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let ScenarioSingleClient { + client: client_ent, + helper, + layer, + } = scenario_single_client(&mut app); - let (inst_ent, mut inst) = app - .world - .query::<(Entity, &mut Instance)>() - .single_mut(&mut app.world); + let mut chunks = app.world.entity_mut::(layer); // Insert an empty chunk at (0, 0). - inst.insert_chunk([0, 0], UnloadedChunk::new()); + chunks.insert_chunk([0, 0], UnloadedChunk::new()); // Put an entity in the new chunk. let cow_ent = app .world .spawn(CowEntityBundle { position: Position::new([8.0, 0.0, 8.0]), - location: EntityLayerId(inst_ent), + layer: EntityLayerId(inst_ent), ..Default::default() }) .id(); diff --git a/src/tests/world_border.rs b/src/tests/world_border.rs index d7a3ea569..733c8e527 100644 --- a/src/tests/world_border.rs +++ b/src/tests/world_border.rs @@ -9,6 +9,7 @@ use valence_world_border::*; use crate::testing::{create_mock_client, scenario_single_client, MockClientHelper}; +/* #[test] fn test_intialize_on_join() { let mut app = App::new(); @@ -131,3 +132,4 @@ fn prepare(app: &mut App) -> (MockClientHelper, Entity) { client_helper.clear_received(); (client_helper, instance_ent) } +*/ From 99ada623c3ccdf8b717c81f736ad9697abbafd81 Mon Sep 17 00:00:00 2001 From: Ryan Date: Sun, 16 Jul 2023 07:03:46 -0700 Subject: [PATCH 12/47] better layer interface --- crates/valence_layer/src/chunk.rs | 138 +++++++++++++---------- crates/valence_layer/src/chunk/loaded.rs | 23 ++-- crates/valence_layer/src/entity.rs | 86 +++++++++----- crates/valence_layer/src/lib.rs | 67 ++++++----- crates/valence_layer/src/message.rs | 35 ++++-- 5 files changed, 217 insertions(+), 132 deletions(-) diff --git a/crates/valence_layer/src/chunk.rs b/crates/valence_layer/src/chunk.rs index ef7562266..0abef8568 100644 --- a/crates/valence_layer/src/chunk.rs +++ b/crates/valence_layer/src/chunk.rs @@ -7,6 +7,7 @@ pub mod unloaded; use std::borrow::Cow; use std::collections::hash_map::{Entry, OccupiedEntry, VacantEntry}; +use std::convert::Infallible; use bevy_app::prelude::*; use bevy_ecs::prelude::*; @@ -19,7 +20,7 @@ use valence_core::chunk_pos::ChunkPos; use valence_core::ident::Ident; use valence_core::particle::{Particle, ParticleS2c}; use valence_core::protocol::array::LengthPrefixedArray; -use valence_core::protocol::encode::WritePacket; +use valence_core::protocol::encode::{PacketWriter, WritePacket}; use valence_core::protocol::packet::sound::{PlaySoundS2c, Sound, SoundCategory}; use valence_core::protocol::{Encode, Packet}; use valence_core::Server; @@ -55,6 +56,7 @@ pub struct ChunkLayerInfo { type ChunkLayerMessages = Messages; +#[doc(hidden)] #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub enum GlobalMsg { /// Send packet data to all clients viewing the layer. @@ -64,6 +66,7 @@ pub enum GlobalMsg { PacketExcept { except: Entity }, } +#[doc(hidden)] #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub enum LocalMsg { /// Send packet data to all clients viewing the layer in view of `pos`. @@ -89,24 +92,6 @@ impl GetChunkPos for LocalMsg { } } -impl Layer for ChunkLayer { - type Global = GlobalMsg; - - type Local = LocalMsg; - - fn send_global(&mut self, msg: Self::Global, f: impl FnOnce(&mut Vec)) { - self.messages.send_global(msg, f) - } - - fn send_local(&mut self, msg: Self::Local, f: impl FnOnce(&mut Vec)) { - self.messages.send_local(msg, f) - } - - fn compression_threshold(&self) -> Option { - self.info.compression_threshold - } -} - impl ChunkLayer { #[track_caller] pub fn new( @@ -211,8 +196,11 @@ impl ChunkLayer { { self.chunks.retain(|pos, chunk| { if !f(*pos, chunk) { - self.messages - .send_local(LocalMsg::LoadOrUnloadChunk { pos: *pos }, |b| b.push(0)); + let _ = self + .messages + .send_local::(LocalMsg::LoadOrUnloadChunk { pos: *pos }, |b| { + Ok(b.push(0)) + }); false } else { @@ -359,19 +347,15 @@ impl ChunkLayer { ) { let position = position.into(); - self.send_local_packet( - LocalMsg::PacketAt { - pos: ChunkPos::from_dvec3(position), - }, - &ParticleS2c { + self.chunk_writer(ChunkPos::from_dvec3(position)) + .write_packet(&ParticleS2c { particle: Cow::Borrowed(particle), long_distance, position, offset: offset.into(), max_speed, count, - }, - ); + }); } // TODO: move to `valence_sound`. @@ -388,19 +372,71 @@ impl ChunkLayer { ) { let position = position.into(); - self.send_local_packet( - LocalMsg::PacketAt { - pos: ChunkPos::from_dvec3(position), - }, - &PlaySoundS2c { + self.chunk_writer(ChunkPos::from_dvec3(position)) + .write_packet(&PlaySoundS2c { id: sound.to_id(), category, position: (position * 8.0).as_ivec3(), volume, pitch, seed: rand::random(), - }, - ); + }); + } +} + +impl Layer for ChunkLayer { + type ChunkWriter<'a> = ChunkWriter<'a>; + + fn chunk_writer(&mut self, pos: impl Into) -> Self::ChunkWriter<'_> { + ChunkWriter { + layer: self, + pos: pos.into(), + } + } +} + +impl WritePacket for ChunkLayer { + fn write_packet_fallible

(&mut self, packet: &P) -> anyhow::Result<()> + where + P: Packet + Encode, + { + self.messages.send_global(GlobalMsg::Packet, |b| { + PacketWriter::new(b, self.info.compression_threshold).write_packet_fallible(packet) + }) + } + + fn write_packet_bytes(&mut self, bytes: &[u8]) { + let _ = self + .messages + .send_global::(GlobalMsg::Packet, |b| Ok(b.extend_from_slice(bytes))); + } +} + +pub struct ChunkWriter<'a> { + layer: &'a mut ChunkLayer, + pos: ChunkPos, +} + +impl<'a> WritePacket for ChunkWriter<'a> { + fn write_packet_fallible

(&mut self, packet: &P) -> anyhow::Result<()> + where + P: Packet + Encode, + { + self.layer + .messages + .send_local(LocalMsg::PacketAt { pos: self.pos }, |b| { + PacketWriter::new(b, self.layer.info.compression_threshold) + .write_packet_fallible(packet) + }) + } + + fn write_packet_bytes(&mut self, bytes: &[u8]) { + let _ = self + .layer + .messages + .send_local::(LocalMsg::PacketAt { pos: self.pos }, |b| { + Ok(b.extend_from_slice(bytes)) + }); } } @@ -435,11 +471,11 @@ impl<'a> OccupiedChunkEntry<'a> { } pub fn insert(&mut self, chunk: UnloadedChunk) -> UnloadedChunk { - self.messages.send_local( + let _ = self.messages.send_local::( LocalMsg::LoadOrUnloadChunk { pos: *self.entry.key(), }, - |b| b.push(1), + |b| Ok(b.push(1)), ); self.entry.get_mut().insert(chunk) @@ -454,11 +490,11 @@ impl<'a> OccupiedChunkEntry<'a> { } pub fn remove(mut self) -> UnloadedChunk { - self.messages.send_local( + let _ = self.messages.send_local::( LocalMsg::LoadOrUnloadChunk { pos: *self.entry.key(), }, - |b| b.push(0), + |b| Ok(b.push(0)), ); self.entry.get_mut().remove() @@ -468,11 +504,11 @@ impl<'a> OccupiedChunkEntry<'a> { let pos = *self.entry.key(); let chunk = self.entry.get_mut().remove(); - self.messages.send_local( + let _ = self.messages.send_local::( LocalMsg::LoadOrUnloadChunk { pos: *self.entry.key(), }, - |b| b.push(0), + |b| Ok(b.push(0)), ); (pos, chunk) @@ -491,11 +527,11 @@ impl<'a> VacantChunkEntry<'a> { let mut loaded = LoadedChunk::new(self.height); loaded.insert(chunk); - self.messages.send_local( + let _ = self.messages.send_local::( LocalMsg::LoadOrUnloadChunk { pos: *self.entry.key(), }, - |b| b.push(1), + |b| Ok(b.push(1)), ); self.entry.insert(loaded) @@ -510,22 +546,6 @@ impl<'a> VacantChunkEntry<'a> { } } -impl WritePacket for ChunkLayer { - fn write_packet_fallible

(&mut self, packet: &P) -> anyhow::Result<()> - where - P: Packet + Encode, - { - self.send_global_packet(GlobalMsg::Packet, packet); - - // TODO: propagate error up. - Ok(()) - } - - fn write_packet_bytes(&mut self, bytes: &[u8]) { - self.send_global_bytes(GlobalMsg::Packet, bytes) - } -} - pub(super) fn build(app: &mut App) { app.add_systems( PostUpdate, diff --git a/crates/valence_layer/src/chunk/loaded.rs b/crates/valence_layer/src/chunk/loaded.rs index 1315bac73..6372ffd1b 100644 --- a/crates/valence_layer/src/chunk/loaded.rs +++ b/crates/valence_layer/src/chunk/loaded.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet}; +use std::convert::Infallible; use std::mem; use std::sync::atomic::{AtomicU32, Ordering}; @@ -215,13 +216,15 @@ impl LoadedChunk { let global_y = info.min_y + sect_y as i32 * 16 + offset_y as i32; let global_z = pos.z * 16 + offset_z as i32; - messages.send_local(LocalMsg::PacketAt { pos }, |buf| { + let _ = messages.send_local::(LocalMsg::PacketAt { pos }, |buf| { let mut writer = PacketWriter::new(buf, info.compression_threshold); writer.write_packet(&BlockUpdateS2c { position: BlockPos::new(global_x, global_y, global_z), block_id: VarInt(block as i32), }); + + Ok(()) }); } _ => { @@ -229,13 +232,15 @@ impl LoadedChunk { | (pos.z as i64 & 0x3fffff) << 20 | (sect_y as i64 + info.min_y.div_euclid(16) as i64) & 0xfffff; - messages.send_local(LocalMsg::PacketAt { pos }, |buf| { + let _ = messages.send_local::(LocalMsg::PacketAt { pos }, |buf| { let mut writer = PacketWriter::new(buf, info.compression_threshold); writer.write_packet(&ChunkDeltaUpdateS2c { chunk_section_position, blocks: Cow::Borrowed(§.section_updates), }); + + Ok(()) }); } } @@ -265,7 +270,7 @@ impl LoadedChunk { let global_y = info.min_y + y as i32; let global_z = pos.z * 16 + z as i32; - messages.send_local(LocalMsg::PacketAt { pos }, |buf| { + let _ = messages.send_local::(LocalMsg::PacketAt { pos }, |buf| { let mut writer = PacketWriter::new(buf, info.compression_threshold); writer.write_packet(&BlockEntityUpdateS2c { @@ -273,6 +278,8 @@ impl LoadedChunk { kind: VarInt(kind as i32), data: Cow::Borrowed(nbt), }); + + Ok(()) }); } @@ -282,7 +289,7 @@ impl LoadedChunk { if self.changed_biomes { self.changed_biomes = false; - messages.send_local(LocalMsg::ChangeBiome { pos }, |buf| { + let _ = messages.send_local::(LocalMsg::ChangeBiome { pos }, |buf| { for sect in self.sections.iter() { sect.biomes .encode_mc_format( @@ -294,6 +301,8 @@ impl LoadedChunk { ) .expect("paletted container encode should always succeed"); } + + Ok(()) }); } @@ -612,8 +621,6 @@ mod tests { use super::*; - const THRESHOLD: Option = Some(256); - #[test] fn loaded_chunk_unviewed_no_changes() { let mut chunk = LoadedChunk::new(512); @@ -640,13 +647,13 @@ mod tests { height: 512, min_y: -16, biome_registry_len: 200, - compression_threshold: THRESHOLD, + compression_threshold: None, sky_light_mask: vec![].into(), sky_light_arrays: vec![].into(), }; let mut buf = vec![]; - let mut writer = PacketWriter::new(&mut buf, THRESHOLD); + let mut writer = PacketWriter::new(&mut buf, None); // Rebuild cache. chunk.write_init_packets(&mut writer, ChunkPos::new(3, 4), &info); diff --git a/crates/valence_layer/src/entity.rs b/crates/valence_layer/src/entity.rs index dda44623d..811359781 100644 --- a/crates/valence_layer/src/entity.rs +++ b/crates/valence_layer/src/entity.rs @@ -1,5 +1,6 @@ use std::collections::hash_map::Entry; use std::collections::BTreeSet; +use std::convert::Infallible; use bevy_app::prelude::*; use bevy_ecs::prelude::*; @@ -26,6 +27,7 @@ pub struct EntityLayer { type EntityLayerMessages = Messages; +#[doc(hidden)] #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub enum GlobalMsg { /// Send packet data to all clients viewing the layer. Message data is @@ -39,6 +41,7 @@ pub enum GlobalMsg { DespawnLayer, } +#[doc(hidden)] #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub enum LocalMsg { /// Spawn entities if the client is not already viewing `src_layer`. Message @@ -103,36 +106,57 @@ impl EntityLayer { } impl Layer for EntityLayer { - type Global = GlobalMsg; + type ChunkWriter<'a> = ChunkWriter<'a>; - type Local = LocalMsg; - - fn send_global(&mut self, msg: Self::Global, f: impl FnOnce(&mut Vec)) { - self.messages.send_global(msg, f); + fn chunk_writer(&mut self, pos: impl Into) -> Self::ChunkWriter<'_> { + ChunkWriter { + layer: self, + pos: pos.into(), + } } +} - fn send_local(&mut self, msg: Self::Local, f: impl FnOnce(&mut Vec)) { - self.messages.send_local(msg, f); +impl WritePacket for EntityLayer { + fn write_packet_fallible

(&mut self, packet: &P) -> anyhow::Result<()> + where + P: Packet + Encode, + { + self.messages.send_global(GlobalMsg::Packet, |b| { + PacketWriter::new(b, self.compression_threshold).write_packet_fallible(packet) + }) } - fn compression_threshold(&self) -> Option { - self.compression_threshold + fn write_packet_bytes(&mut self, bytes: &[u8]) { + let _ = self + .messages + .send_global::(GlobalMsg::Packet, |b| Ok(b.extend_from_slice(bytes))); } } -impl WritePacket for EntityLayer { +pub struct ChunkWriter<'a> { + layer: &'a mut EntityLayer, + pos: ChunkPos, +} + +impl<'a> WritePacket for ChunkWriter<'a> { fn write_packet_fallible

(&mut self, packet: &P) -> anyhow::Result<()> where P: Packet + Encode, { - self.send_global_packet(GlobalMsg::Packet, packet); - - // TODO: propagate error up. - Ok(()) + self.layer + .messages + .send_local(LocalMsg::PacketAt { pos: self.pos }, |b| { + PacketWriter::new(b, self.layer.compression_threshold).write_packet_fallible(packet) + }) } fn write_packet_bytes(&mut self, bytes: &[u8]) { - self.send_global_bytes(GlobalMsg::Packet, bytes) + let _ = self + .layer + .messages + .send_local::(LocalMsg::PacketAt { pos: self.pos }, |b| { + Ok(b.extend_from_slice(bytes)) + }); } } @@ -180,12 +204,12 @@ fn change_entity_positions( if let Entry::Occupied(mut old_cell) = old_layer.entities.entry(old_chunk_pos) { if old_cell.get_mut().remove(&entity) { - old_layer.messages.send_local( + let _ = old_layer.messages.send_local::( LocalMsg::DespawnEntity { pos: old_chunk_pos, dest_layer: Entity::PLACEHOLDER, }, - |b| b.extend_from_slice(&entity_id.get().to_ne_bytes()), + |b| Ok(b.extend_from_slice(&entity_id.get().to_ne_bytes())), ); if old_cell.get().is_empty() { @@ -203,12 +227,12 @@ fn change_entity_positions( if let Entry::Occupied(mut old_cell) = old_layer.entities.entry(old_chunk_pos) { if old_cell.get_mut().remove(&entity) { - old_layer.messages.send_local( + let _ = old_layer.messages.send_local::( LocalMsg::DespawnEntity { pos: old_chunk_pos, dest_layer: layer_id.0, }, - |b| b.extend_from_slice(&entity_id.get().to_ne_bytes()), + |b| Ok(b.extend_from_slice(&entity_id.get().to_ne_bytes())), ); if old_cell.get().is_empty() { @@ -220,12 +244,12 @@ fn change_entity_positions( if let Ok(mut layer) = layers.get_mut(layer_id.0) { if layer.entities.entry(chunk_pos).or_default().insert(entity) { - layer.messages.send_local( + let _ = layer.messages.send_local::( LocalMsg::SpawnEntity { pos: chunk_pos, src_layer: old_layer_id.get(), }, - |b| b.extend_from_slice(&entity.to_bits().to_ne_bytes()), + |b| Ok(b.extend_from_slice(&entity.to_bits().to_ne_bytes())), ); } } @@ -236,23 +260,23 @@ fn change_entity_positions( if let Ok(mut layer) = layers.get_mut(layer_id.0) { if let Entry::Occupied(mut old_cell) = layer.entities.entry(old_chunk_pos) { if old_cell.get_mut().remove(&entity) { - layer.messages.send_local( + let _ = layer.messages.send_local::( LocalMsg::DespawnEntityTransition { pos: old_chunk_pos, dest_pos: chunk_pos, }, - |b| b.extend_from_slice(&entity_id.get().to_ne_bytes()), + |b| Ok(b.extend_from_slice(&entity_id.get().to_ne_bytes())), ); } } if layer.entities.entry(chunk_pos).or_default().insert(entity) { - layer.messages.send_local( + let _ = layer.messages.send_local::( LocalMsg::SpawnEntityTransition { pos: chunk_pos, src_pos: old_chunk_pos, }, - |b| b.extend_from_slice(&entity.to_bits().to_ne_bytes()), + |b| Ok(b.extend_from_slice(&entity.to_bits().to_ne_bytes())), ); } } @@ -284,9 +308,11 @@ fn send_entity_update_messages( LocalMsg::PacketAt { pos: chunk_pos } }; - layer.messages.send_local(msg, |b| { - update - .write_update_packets(PacketWriter::new(b, layer.compression_threshold)) + let _ = layer.messages.send_local::(msg, |b| { + Ok(update.write_update_packets(PacketWriter::new( + b, + layer.compression_threshold, + ))) }); } else { panic!( @@ -301,7 +327,9 @@ fn send_entity_update_messages( fn send_layer_despawn_messages(mut layers: Query<&mut EntityLayer, With>) { for mut layer in &mut layers { - layer.send_global(GlobalMsg::DespawnLayer, |_| {}); + let _ = layer + .messages + .send_global::(GlobalMsg::DespawnLayer, |_| Ok(())); } } diff --git a/crates/valence_layer/src/lib.rs b/crates/valence_layer/src/lib.rs index 0ecaadaf4..c81f65755 100644 --- a/crates/valence_layer/src/lib.rs +++ b/crates/valence_layer/src/lib.rs @@ -31,9 +31,9 @@ use bevy_ecs::prelude::*; pub use chunk::ChunkLayer; pub use entity::EntityLayer; use valence_biome::BiomeRegistry; +use valence_core::chunk_pos::ChunkPos; use valence_core::ident::Ident; -use valence_core::protocol::encode::{PacketWriter, WritePacket}; -use valence_core::protocol::{Encode, Packet}; +use valence_core::protocol::encode::WritePacket; use valence_core::Server; use valence_dimension::DimensionTypeRegistry; use valence_entity::{InitEntitiesSet, UpdateTrackedDataSet}; @@ -81,41 +81,54 @@ impl Plugin for LayerPlugin { } } -pub trait Layer { - type Global; - type Local; +// pub trait Layer { +// type Global; +// type Local; - fn send_global(&mut self, msg: Self::Global, f: impl FnOnce(&mut Vec)); +// fn send_global(&mut self, msg: Self::Global, f: impl FnOnce(&mut +// Vec)); - fn send_local(&mut self, msg: Self::Local, f: impl FnOnce(&mut Vec)); +// fn send_local(&mut self, msg: Self::Local, f: impl FnOnce(&mut Vec)); - fn compression_threshold(&self) -> Option; +// fn compression_threshold(&self) -> Option; - fn send_global_bytes(&mut self, msg: Self::Global, bytes: &[u8]) { - self.send_global(msg, |b| b.extend_from_slice(bytes)); - } +// fn send_global_bytes(&mut self, msg: Self::Global, bytes: &[u8]) { +// self.send_global(msg, |b| b.extend_from_slice(bytes)); +// } - fn send_local_bytes(&mut self, msg: Self::Local, bytes: &[u8]) { - self.send_local(msg, |b| b.extend_from_slice(bytes)); - } +// fn send_local_bytes(&mut self, msg: Self::Local, bytes: &[u8]) { +// self.send_local(msg, |b| b.extend_from_slice(bytes)); +// } - fn send_global_packet

(&mut self, msg: Self::Global, pkt: &P) - where - P: Encode + Packet, - { - let threshold = self.compression_threshold(); +// fn send_global_packet

(&mut self, msg: Self::Global, pkt: &P) +// where +// P: Encode + Packet, +// { +// let threshold = self.compression_threshold(); - self.send_global(msg, |b| PacketWriter::new(b, threshold).write_packet(pkt)); - } +// self.send_global(msg, |b| PacketWriter::new(b, +// threshold).write_packet(pkt)); } - fn send_local_packet

(&mut self, msg: Self::Local, pkt: &P) +// fn send_local_packet

(&mut self, msg: Self::Local, pkt: &P) +// where +// P: Encode + Packet, +// { +// let threshold = self.compression_threshold(); + +// self.send_local(msg, |b| PacketWriter::new(b, +// threshold).write_packet(pkt)); } +// } + +pub trait Layer: WritePacket { + type ChunkWriter<'a>: WritePacket where - P: Encode + Packet, - { - let threshold = self.compression_threshold(); + Self: 'a; - self.send_local(msg, |b| PacketWriter::new(b, threshold).write_packet(pkt)); - } + /// Returns a [`WritePacket`] implementor for a chunk position. + /// + /// When writing packets to the chunk writer, only clients in view of `pos` + /// will receive the packet. + fn chunk_writer(&mut self, pos: impl Into) -> Self::ChunkWriter<'_>; } /// Convenience [`Bundle`] for spawning a layer entity with both [`ChunkLayer`] diff --git a/crates/valence_layer/src/message.rs b/crates/valence_layer/src/message.rs index 851b47b43..a41c6e99d 100644 --- a/crates/valence_layer/src/message.rs +++ b/crates/valence_layer/src/message.rs @@ -23,40 +23,52 @@ where Self::default() } - pub(crate) fn send_global(&mut self, msg: G, f: impl FnOnce(&mut Vec)) { + pub(crate) fn send_global( + &mut self, + msg: G, + f: impl FnOnce(&mut Vec) -> Result<(), E>, + ) -> Result<(), E> { debug_assert!(!self.is_ready); let start = self.staging.len(); - f(&mut self.staging); + f(&mut self.staging)?; let end = self.staging.len(); if let Some((m, range)) = self.global.last_mut() { if msg == *m { // Extend the existing message. range.end = end as u32; - return; + return Ok(()); } } self.global.push((msg, start as u32..end as u32)); + + Ok(()) } - pub(crate) fn send_local(&mut self, msg: L, f: impl FnOnce(&mut Vec)) { + pub(crate) fn send_local( + &mut self, + msg: L, + f: impl FnOnce(&mut Vec) -> Result<(), E>, + ) -> Result<(), E> { debug_assert!(!self.is_ready); let start = self.staging.len(); - f(&mut self.staging); + f(&mut self.staging)?; let end = self.staging.len(); if let Some((m, range)) = self.local.last_mut() { if msg == *m { // Extend the existing message. range.end = end as u32; - return; + return Ok(()); } } self.local.push((msg, start as u32..end as u32)); + + Ok(()) } /// Readies messages to be read by clients. @@ -208,6 +220,8 @@ impl GetChunkPos for MessagePair { #[cfg(test)] mod tests { + use std::convert::Infallible; + use super::*; #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] @@ -229,9 +243,12 @@ mod tests { let mut messages = Messages::::new(); - messages.send_global(TestMsg::Foo, |w| w.extend_from_slice(&[1, 2, 3])); - messages.send_global(TestMsg::Bar, |b| b.extend_from_slice(&[4, 5, 6])); - messages.send_global(TestMsg::Foo, |b| b.extend_from_slice(&[7, 8, 9])); + let _ = messages + .send_global::(TestMsg::Foo, |b| Ok(b.extend_from_slice(&[1, 2, 3]))); + let _ = messages + .send_global::(TestMsg::Bar, |b| Ok(b.extend_from_slice(&[4, 5, 6]))); + let _ = messages + .send_global::(TestMsg::Foo, |b| Ok(b.extend_from_slice(&[7, 8, 9]))); messages.ready(); From 3166b87cfe3a9b14e49a3bc3dab77d42dc268add Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 17 Jul 2023 01:15:36 -0700 Subject: [PATCH 13/47] get tests to compile --- crates/valence_layer/src/chunk.rs | 14 +- crates/valence_layer/src/entity.rs | 10 +- crates/valence_layer/src/lib.rs | 4 +- examples/anvil_loading.rs | 7 +- examples/building.rs | 2 +- examples/world_border.rs | 6 + src/testing.rs | 96 ++++--- src/tests.rs | 2 +- src/tests/boss_bar.rs | 156 ++++++----- src/tests/client.rs | 90 +++--- src/tests/example.rs | 32 ++- src/tests/instance.rs | 57 ---- src/tests/inventory.rs | 426 ++++++++++++++++++----------- src/tests/layer.rs | 55 ++++ src/tests/player_list.rs | 54 +--- src/tests/weather.rs | 64 ++--- src/tests/world_border.rs | 6 +- 17 files changed, 598 insertions(+), 483 deletions(-) delete mode 100644 src/tests/instance.rs create mode 100644 src/tests/layer.rs diff --git a/crates/valence_layer/src/chunk.rs b/crates/valence_layer/src/chunk.rs index 0abef8568..6ecc920b8 100644 --- a/crates/valence_layer/src/chunk.rs +++ b/crates/valence_layer/src/chunk.rs @@ -347,7 +347,7 @@ impl ChunkLayer { ) { let position = position.into(); - self.chunk_writer(ChunkPos::from_dvec3(position)) + self.view_writer(ChunkPos::from_dvec3(position)) .write_packet(&ParticleS2c { particle: Cow::Borrowed(particle), long_distance, @@ -372,7 +372,7 @@ impl ChunkLayer { ) { let position = position.into(); - self.chunk_writer(ChunkPos::from_dvec3(position)) + self.view_writer(ChunkPos::from_dvec3(position)) .write_packet(&PlaySoundS2c { id: sound.to_id(), category, @@ -385,10 +385,10 @@ impl ChunkLayer { } impl Layer for ChunkLayer { - type ChunkWriter<'a> = ChunkWriter<'a>; + type ViewWriter<'a> = ViewWriter<'a>; - fn chunk_writer(&mut self, pos: impl Into) -> Self::ChunkWriter<'_> { - ChunkWriter { + fn view_writer(&mut self, pos: impl Into) -> Self::ViewWriter<'_> { + ViewWriter { layer: self, pos: pos.into(), } @@ -412,12 +412,12 @@ impl WritePacket for ChunkLayer { } } -pub struct ChunkWriter<'a> { +pub struct ViewWriter<'a> { layer: &'a mut ChunkLayer, pos: ChunkPos, } -impl<'a> WritePacket for ChunkWriter<'a> { +impl<'a> WritePacket for ViewWriter<'a> { fn write_packet_fallible

(&mut self, packet: &P) -> anyhow::Result<()> where P: Packet + Encode, diff --git a/crates/valence_layer/src/entity.rs b/crates/valence_layer/src/entity.rs index 811359781..505d0d131 100644 --- a/crates/valence_layer/src/entity.rs +++ b/crates/valence_layer/src/entity.rs @@ -106,10 +106,10 @@ impl EntityLayer { } impl Layer for EntityLayer { - type ChunkWriter<'a> = ChunkWriter<'a>; + type ViewWriter<'a> = ViewWriter<'a>; - fn chunk_writer(&mut self, pos: impl Into) -> Self::ChunkWriter<'_> { - ChunkWriter { + fn view_writer(&mut self, pos: impl Into) -> Self::ViewWriter<'_> { + ViewWriter { layer: self, pos: pos.into(), } @@ -133,12 +133,12 @@ impl WritePacket for EntityLayer { } } -pub struct ChunkWriter<'a> { +pub struct ViewWriter<'a> { layer: &'a mut EntityLayer, pos: ChunkPos, } -impl<'a> WritePacket for ChunkWriter<'a> { +impl<'a> WritePacket for ViewWriter<'a> { fn write_packet_fallible

(&mut self, packet: &P) -> anyhow::Result<()> where P: Packet + Encode, diff --git a/crates/valence_layer/src/lib.rs b/crates/valence_layer/src/lib.rs index c81f65755..2621a62c8 100644 --- a/crates/valence_layer/src/lib.rs +++ b/crates/valence_layer/src/lib.rs @@ -120,7 +120,7 @@ impl Plugin for LayerPlugin { // } pub trait Layer: WritePacket { - type ChunkWriter<'a>: WritePacket + type ViewWriter<'a>: WritePacket where Self: 'a; @@ -128,7 +128,7 @@ pub trait Layer: WritePacket { /// /// When writing packets to the chunk writer, only clients in view of `pos` /// will receive the packet. - fn chunk_writer(&mut self, pos: impl Into) -> Self::ChunkWriter<'_>; + fn view_writer(&mut self, pos: impl Into) -> Self::ViewWriter<'_>; } /// Convenience [`Bundle`] for spawning a layer entity with both [`ChunkLayer`] diff --git a/examples/anvil_loading.rs b/examples/anvil_loading.rs index 2f9ccb5a6..c6c3e6bf6 100644 --- a/examples/anvil_loading.rs +++ b/examples/anvil_loading.rs @@ -1,5 +1,9 @@ -use std::path::PathBuf; +// TODO + +fn main() {} +/* +use std::path::PathBuf; use clap::Parser; use valence::prelude::*; use valence_anvil::{AnvilLevel, ChunkLoadEvent, ChunkLoadStatus}; @@ -146,3 +150,4 @@ fn display_loaded_chunk_count(mut instances: Query<&mut Instance>, mut last_coun ); } } +*/ diff --git a/examples/building.rs b/examples/building.rs index 9e408170c..f57ef2522 100644 --- a/examples/building.rs +++ b/examples/building.rs @@ -113,7 +113,7 @@ fn digging( let Ok(game_mode) = clients.get(event.client) else { continue; }; - + if (*game_mode == GameMode::Creative && event.state == DiggingState::Start) || (*game_mode == GameMode::Survival && event.state == DiggingState::Stop) { diff --git a/examples/world_border.rs b/examples/world_border.rs index f04afc138..52e7d9b40 100644 --- a/examples/world_border.rs +++ b/examples/world_border.rs @@ -1,3 +1,8 @@ +// TODO: fix + +fn main() {} + +/* use std::time::Duration; use bevy_app::App; @@ -165,3 +170,4 @@ fn border_controls( } } } +*/ diff --git a/src/testing.rs b/src/testing.rs index e1c4aaf71..1beca58be 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -23,52 +23,62 @@ use valence_network::NetworkPlugin; use crate::client::{ClientBundle, ClientConnection, ReceivedPacket}; use crate::DefaultPlugins; -/// Sets up valence with a single mock client. Returns the Entity of the client -/// and the corresponding MockClientHelper. -/// -/// Reduces boilerplate in unit tests. -pub fn scenario_single_client(app: &mut App) -> ScenarioSingleClient { - app.insert_resource(CoreSettings { - compression_threshold: None, - ..Default::default() - }); - - app.insert_resource(KeepaliveSettings { - period: Duration::MAX, - }); - - app.add_plugins(DefaultPlugins.build().disable::()); - - app.update(); // Initialize plugins. - - let chunk_layer = ChunkLayer::new( - ident!("overworld"), - app.world.resource::(), - app.world.resource::(), - app.world.resource::(), - ); - let entity_layer = EntityLayer::new(app.world.resource::()); - let layer_ent = app.world.spawn((chunk_layer, entity_layer)).id(); - - let (mut client, client_helper) = create_mock_client("test"); - client.player.layer.0 = layer_ent; - client.visible_chunk_layer.0 = layer_ent; - client.visible_entity_layers.0.insert(layer_ent); - let client_ent = app.world.spawn(client).id(); - - ScenarioSingleClient { - client: client_ent, - helper: client_helper, - layer: layer_ent, - } -} - pub struct ScenarioSingleClient { + /// The new bevy application. + pub app: App, + /// Entity handle of the single [`Client`]. pub client: Entity, + /// Helper for sending and receiving packets from the mock client. pub helper: MockClientHelper, + /// Entity with [`ChunkLayer`] and [`EntityLayer`] components. pub layer: Entity, } +impl ScenarioSingleClient { + /// Sets up Valence with a single mock client and entity+chunk layer. The + /// client is configured to be placed within the layer. + /// + /// Reduces boilerplate in unit tests. + pub fn new() -> Self { + let mut app = App::new(); + + app.insert_resource(CoreSettings { + compression_threshold: None, + ..Default::default() + }); + + app.insert_resource(KeepaliveSettings { + period: Duration::MAX, + }); + + app.add_plugins(DefaultPlugins.build().disable::()); + + app.update(); // Initialize plugins. + + let chunk_layer = ChunkLayer::new( + ident!("overworld"), + app.world.resource::(), + app.world.resource::(), + app.world.resource::(), + ); + let entity_layer = EntityLayer::new(app.world.resource::()); + let layer = app.world.spawn((chunk_layer, entity_layer)).id(); + + let (mut client, helper) = create_mock_client("test"); + client.player.layer.0 = layer; + client.visible_chunk_layer.0 = layer; + client.visible_entity_layers.0.insert(layer); + let client = app.world.spawn(client).id(); + + ScenarioSingleClient { + app, + client, + helper, + layer, + } + } +} + /// Creates a mock client bundle that can be used for unit testing. /// /// Returns the client, and a helper to inject packets as if the client sent @@ -117,7 +127,7 @@ impl MockClientConnection { } /// Injects a (Packet ID + data) frame to be received by the server. - fn inject_send(&mut self, mut bytes: BytesMut) { + fn inject_send(&self, mut bytes: BytesMut) { let id = VarInt::decode_partial((&mut bytes).reader()).expect("failed to decode packet ID"); self.inner @@ -131,11 +141,11 @@ impl MockClientConnection { }); } - fn take_received(&mut self) -> BytesMut { + fn take_received(&self) -> BytesMut { self.inner.lock().unwrap().send_buf.split() } - fn clear_received(&mut self) { + fn clear_received(&self) { self.inner.lock().unwrap().send_buf.clear(); } } diff --git a/src/tests.rs b/src/tests.rs index d393c8137..3641539b0 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,8 +1,8 @@ mod boss_bar; mod client; mod example; -mod instance; mod inventory; +mod layer; mod player_list; mod weather; mod world_border; diff --git a/src/tests/boss_bar.rs b/src/tests/boss_bar.rs index ff215731a..5539ed179 100644 --- a/src/tests/boss_bar.rs +++ b/src/tests/boss_bar.rs @@ -1,5 +1,3 @@ -use bevy_app::App; -use bevy_ecs::entity::Entity; use valence_boss_bar::packet::BossBarS2c; use valence_boss_bar::{ BossBarBundle, BossBarColor, BossBarDivision, BossBarFlags, BossBarHealth, BossBarStyle, @@ -8,112 +6,130 @@ use valence_boss_bar::{ use valence_core::despawn::Despawned; use valence_core::text::Text; -use crate::testing::{scenario_single_client, MockClientHelper, ScenarioSingleClient}; +use crate::testing::ScenarioSingleClient; #[test] fn test_intialize_on_join() { - let mut app = App::new(); - let (client_ent, mut client_helper, instance_ent) = prepare(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer, + } = prepare(); // Fetch the boss bar component - let mut boss_bar = app.world.get_mut::(instance_ent).unwrap(); + let mut boss_bar = app.world.get_mut::(layer).unwrap(); // Add our mock client to the viewers list - assert!(boss_bar.viewers.insert(client_ent)); + assert!(boss_bar.viewers.insert(client)); app.update(); // Check if a boss bar packet was sent - let frames = client_helper.collect_received(); + let frames = helper.collect_received(); frames.assert_count::(1); } #[test] fn test_despawn() { - let mut app = App::new(); - let (client_ent, mut client_helper, instance_ent) = prepare(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer, + } = prepare(); // Fetch the boss bar component - let mut boss_bar = app.world.get_mut::(instance_ent).unwrap(); + let mut boss_bar = app.world.get_mut::(layer).unwrap(); // Add our mock client to the viewers list - assert!(boss_bar.viewers.insert(client_ent)); + assert!(boss_bar.viewers.insert(client)); app.update(); // Despawn the boss bar - app.world.entity_mut(instance_ent).insert(Despawned); + app.world.entity_mut(layer).insert(Despawned); app.update(); // Check if a boss bar packet was sent in addition to the ADD packet, which // should be a Remove packet - let frames = client_helper.collect_received(); + let frames = helper.collect_received(); frames.assert_count::(2); } #[test] fn test_title_update() { - let mut app = App::new(); - let (client_ent, mut client_helper, instance_ent) = prepare(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer, + } = prepare(); // Fetch the boss bar component - let mut boss_bar = app.world.get_mut::(instance_ent).unwrap(); + let mut boss_bar = app.world.get_mut::(layer).unwrap(); // Add our mock client to the viewers list - assert!(boss_bar.viewers.insert(client_ent)); + assert!(boss_bar.viewers.insert(client)); app.update(); // Update the title app.world - .entity_mut(instance_ent) + .entity_mut(layer) .insert(BossBarTitle(Text::text("Test 2"))); app.update(); // Check if a boss bar packet was sent in addition to the ADD packet, which // should be an UpdateTitle packet - let frames = client_helper.collect_received(); + let frames = helper.collect_received(); frames.assert_count::(2); } #[test] fn test_health_update() { - let mut app = App::new(); - let (client_ent, mut client_helper, instance_ent) = prepare(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer, + } = prepare(); // Fetch the boss bar component - let mut boss_bar = app.world.get_mut::(instance_ent).unwrap(); + let mut boss_bar = app.world.get_mut::(layer).unwrap(); // Add our mock client to the viewers list - assert!(boss_bar.viewers.insert(client_ent)); + assert!(boss_bar.viewers.insert(client)); app.update(); // Update the health - app.world - .entity_mut(instance_ent) - .insert(BossBarHealth(0.5)); + app.world.entity_mut(layer).insert(BossBarHealth(0.5)); app.update(); // Check if a boss bar packet was sent in addition to the ADD packet, which // should be an UpdateHealth packet - let frames = client_helper.collect_received(); + let frames = helper.collect_received(); frames.assert_count::(2); } #[test] fn test_style_update() { - let mut app = App::new(); - let (client_ent, mut client_helper, instance_ent) = prepare(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer, + } = prepare(); // Fetch the boss bar component - let mut boss_bar = app.world.get_mut::(instance_ent).unwrap(); + let mut boss_bar = app.world.get_mut::(layer).unwrap(); // Add our mock client to the viewers list - assert!(boss_bar.viewers.insert(client_ent)); + assert!(boss_bar.viewers.insert(client)); app.update(); // Update the style - app.world.entity_mut(instance_ent).insert(BossBarStyle { + app.world.entity_mut(layer).insert(BossBarStyle { color: BossBarColor::Red, division: BossBarDivision::TenNotches, }); @@ -122,91 +138,97 @@ fn test_style_update() { // Check if a boss bar packet was sent in addition to the ADD packet, which // should be an UpdateStyle packet - let frames = client_helper.collect_received(); + let frames = helper.collect_received(); frames.assert_count::(2); } #[test] fn test_flags_update() { - let mut app = App::new(); - let (client_ent, mut client_helper, instance_ent) = prepare(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer, + } = prepare(); // Fetch the boss bar component - let mut boss_bar = app.world.get_mut::(instance_ent).unwrap(); + let mut boss_bar = app.world.get_mut::(layer).unwrap(); // Add our mock client to the viewers list - assert!(boss_bar.viewers.insert(client_ent)); + assert!(boss_bar.viewers.insert(client)); app.update(); // Update the flags let mut new_flags = BossBarFlags::new(); new_flags.set_create_fog(true); - app.world.entity_mut(instance_ent).insert(new_flags); + app.world.entity_mut(layer).insert(new_flags); app.update(); // Check if a boss bar packet was sent in addition to the ADD packet, which // should be an UpdateFlags packet - let frames = client_helper.collect_received(); + let frames = helper.collect_received(); frames.assert_count::(2); } #[test] fn test_client_disconnection() { - let mut app = App::new(); - let (client_ent, mut client_helper, instance_ent) = prepare(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer, + } = prepare(); // Fetch the boss bar component - let mut boss_bar = app.world.get_mut::(instance_ent).unwrap(); + let mut boss_bar = app.world.get_mut::(layer).unwrap(); // Add our mock client to the viewers list - assert!(boss_bar.viewers.insert(client_ent)); + assert!(boss_bar.viewers.insert(client)); app.update(); // Remove the client from the world - app.world.entity_mut(client_ent).insert(Despawned); + app.world.entity_mut(client).insert(Despawned); app.update(); assert!(app .world - .get_mut::(instance_ent) + .get_mut::(layer) .unwrap() .viewers .is_empty()); // Check if a boss bar packet was sent in addition to the ADD packet, which // should be a Remove packet - let frames = client_helper.collect_received(); + let frames = helper.collect_received(); frames.assert_count::(2); } -fn prepare(app: &mut App) -> (Entity, MockClientHelper, Entity) { - let ScenarioSingleClient { - client, - helper, - layer, - } = scenario_single_client(app); +fn prepare() -> ScenarioSingleClient { + let mut s = ScenarioSingleClient::new(); // Process a tick to get past the "on join" logic. - app.update(); - client_helper.clear_received(); + s.app.update(); + s.helper.clear_received(); // Insert a boss bar into the world - let boss_bar = app - .world - .spawn(BossBarBundle::new( - Text::text("Test"), - BossBarColor::Blue, - BossBarDivision::SixNotches, - BossBarFlags::new(), - )) - .id(); + + // Attach the new boss bar to the layer for convenience. + s.app.world.entity_mut(s.layer).insert(BossBarBundle { + title: BossBarTitle(Text::text("Test")), + style: BossBarStyle { + color: BossBarColor::Blue, + division: BossBarDivision::SixNotches, + }, + flags: BossBarFlags::new(), + ..Default::default() + }); for _ in 0..2 { - app.update(); + s.app.update(); } - helper.clear_received(); - (client, helper, boss_bar) + s.helper.clear_received(); + s } diff --git a/src/tests/client.rs b/src/tests/client.rs index 8e5913d00..70ae801fa 100644 --- a/src/tests/client.rs +++ b/src/tests/client.rs @@ -1,7 +1,5 @@ use std::collections::BTreeSet; -use bevy_app::prelude::*; -use bevy_ecs::prelude::*; use bevy_ecs::world::EntityMut; use glam::DVec3; use valence_client::movement::FullC2s; @@ -9,15 +7,13 @@ use valence_client::teleport::{PlayerPositionLookS2c, TeleportConfirmC2s}; use valence_client::ViewDistance; use valence_core::chunk_pos::{ChunkPos, ChunkView}; use valence_core::protocol::Packet; -use valence_entity::cow::CowEntityBundle; -use valence_entity::packet::{EntitiesDestroyS2c, EntitySpawnS2c, MoveRelativeS2c}; -use valence_entity::{EntityLayerId, Position}; -use valence_instance::chunk::UnloadedChunk; -use valence_instance::packet::{ChunkDataS2c, UnloadChunkS2c}; -use valence_instance::Instance; +use valence_entity::packet::MoveRelativeS2c; +use valence_entity::Position; +use valence_layer::chunk::UnloadedChunk; +use valence_layer::packet::{ChunkDataS2c, UnloadChunkS2c}; use valence_layer::ChunkLayer; -use crate::testing::{create_mock_client, scenario_single_client, ScenarioSingleClient}; +use crate::testing::{create_mock_client, ScenarioSingleClient}; #[test] fn client_chunk_view_change() { @@ -28,18 +24,18 @@ fn client_chunk_view_change() { ChunkView::new(chunk_pos, view_dist) } - let mut app = App::new(); + let ScenarioSingleClient { + mut app, + client: client_ent, + mut helper, + layer: layer_ent, + } = ScenarioSingleClient::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); - - let mut instance = app - .world - .query::<&mut Instance>() - .single_mut(&mut app.world); + let mut layer = app.world.get_mut::(layer_ent).unwrap(); for z in -30..30 { for x in -30..30 { - instance.insert_chunk([x, z], UnloadedChunk::new()); + layer.insert_chunk([x, z], UnloadedChunk::new()); } } @@ -55,7 +51,7 @@ fn client_chunk_view_change() { let mut loaded_chunks = BTreeSet::new(); // Collect all chunks received on join. - for f in client_helper.collect_received().0 { + for f in helper.collect_received().0 { if f.id == ChunkDataS2c::ID { let ChunkDataS2c { pos, .. } = f.decode::().unwrap(); // Newly received chunk was not previously loaded. @@ -78,7 +74,7 @@ fn client_chunk_view_change() { let client = app.world.entity_mut(client_ent); // For all chunks received this tick... - for f in client_helper.collect_received().0 { + for f in helper.collect_received().0 { match f.id { ChunkDataS2c::ID => { let ChunkDataS2c { pos, .. } = f.decode().unwrap(); @@ -100,17 +96,17 @@ fn client_chunk_view_change() { } } +/* #[test] fn entity_chunk_spawn_despawn() { - let mut app = App::new(); - let ScenarioSingleClient { + mut app, client: client_ent, - helper, - layer, - } = scenario_single_client(&mut app); + mut helper, + layer: layer_ent, + } = ScenarioSingleClient::new(); - let mut chunks = app.world.entity_mut::(layer); + let mut chunks = app.world.get_mut::(layer_ent).unwrap(); // Insert an empty chunk at (0, 0). chunks.insert_chunk([0, 0], UnloadedChunk::new()); @@ -120,7 +116,7 @@ fn entity_chunk_spawn_despawn() { .world .spawn(CowEntityBundle { position: Position::new([8.0, 0.0, 8.0]), - layer: EntityLayerId(inst_ent), + layer: EntityLayerId(layer_ent), ..Default::default() }) .id(); @@ -130,7 +126,7 @@ fn entity_chunk_spawn_despawn() { // Client is in view of the chunk, so they should receive exactly one chunk // spawn packet and entity spawn packet. { - let recvd = client_helper.collect_received(); + let recvd = helper.collect_received(); recvd.assert_count::(1); recvd.assert_count::(1); @@ -143,19 +139,19 @@ fn entity_chunk_spawn_despawn() { app.update(); - client_helper + helper .collect_received() .assert_count::(1); // Despawning the chunk should delete the chunk and the entity contained within. - let mut inst = app.world.get_mut::(inst_ent).unwrap(); + let mut inst = app.world.get_mut::(layer_ent).unwrap(); inst.remove_chunk([0, 0]).unwrap(); app.update(); { - let recvd = client_helper.collect_received(); + let recvd = helper.collect_received(); recvd.assert_count::(1); recvd.assert_count::(1); @@ -165,14 +161,14 @@ fn entity_chunk_spawn_despawn() { // Placing the chunk back should respawn the orphaned entity. - let mut inst = app.world.get_mut::(inst_ent).unwrap(); + let mut inst = app.world.get_mut::(layer_ent).unwrap(); assert!(inst.insert_chunk([0, 0], UnloadedChunk::new()).is_none()); app.update(); { - let recvd = client_helper.collect_received(); + let recvd = helper.collect_received(); recvd.assert_count::(1); recvd.assert_count::(1); @@ -188,7 +184,7 @@ fn entity_chunk_spawn_despawn() { app.update(); { - let recvd = client_helper.collect_received(); + let recvd = helper.collect_received(); recvd.assert_count::(1); recvd.assert_count::(1); @@ -210,7 +206,7 @@ fn entity_chunk_spawn_despawn() { app.update(); { - let recvd = client_helper.collect_received(); + let recvd = helper.collect_received(); recvd.assert_count::(1); recvd.assert_count::(1); @@ -223,7 +219,7 @@ fn entity_chunk_spawn_despawn() { // once. app.world - .get_mut::(inst_ent) + .get_mut::(layer_ent) .unwrap() .chunk_entry([0, 1]) .or_default() @@ -237,7 +233,7 @@ fn entity_chunk_spawn_despawn() { app.update(); { - let recvd = client_helper.collect_received(); + let recvd = helper.collect_received(); recvd.assert_count::(0); recvd.assert_count::(0); @@ -255,28 +251,30 @@ fn entity_chunk_spawn_despawn() { } } } -} +}*/ #[test] fn client_teleport_and_move() { - let mut app = App::new(); - - let (_, mut helper_1) = scenario_single_client(&mut app); + let ScenarioSingleClient { + mut app, + client: _, + helper: mut helper_1, + layer: layer_ent, + } = ScenarioSingleClient::new(); - let (inst_ent, mut inst) = app - .world - .query::<(Entity, &mut Instance)>() - .single_mut(&mut app.world); + let mut layer = app.world.get_mut::(layer_ent).unwrap(); for z in -10..10 { for x in -10..10 { - inst.insert_chunk(ChunkPos::new(x, z), UnloadedChunk::new()); + layer.insert_chunk(ChunkPos::new(x, z), UnloadedChunk::new()); } } let (mut bundle, mut helper_2) = create_mock_client("other"); - bundle.player.location.0 = inst_ent; + bundle.player.layer.0 = layer_ent; + bundle.visible_chunk_layer.0 = layer_ent; + bundle.visible_entity_layers.0.insert(layer_ent); app.world.spawn(bundle); diff --git a/src/tests/example.rs b/src/tests/example.rs index 413d8a087..25405cea4 100644 --- a/src/tests/example.rs +++ b/src/tests/example.rs @@ -14,7 +14,7 @@ use valence_entity::Position; use valence_inventory::packet::{InventoryS2c, OpenScreenS2c}; use valence_inventory::{Inventory, InventoryKind, OpenInventory}; -use crate::testing::scenario_single_client; +use crate::testing::ScenarioSingleClient; use crate::DefaultPlugins; /// The server's tick should increment every update. @@ -36,41 +36,49 @@ fn example_test_server_tick_increment() { /// packet to the server. #[test] fn example_test_client_position() { - let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); // Send a packet as the client to the server. let packet = PositionAndOnGroundC2s { position: DVec3::new(12.0, 64.0, 0.0), on_ground: true, }; - client_helper.send(&packet); + helper.send(&packet); // Process the packet. app.update(); // Make assertions - let pos = app.world.get::(client_ent).unwrap(); + let pos = app.world.get::(client).unwrap(); assert_eq!(pos.0, DVec3::new(12.0, 64.0, 0.0)); } /// A unit test where we want to test what packets are sent to the client. #[test] fn example_test_open_inventory() { - let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); let inventory = Inventory::new(InventoryKind::Generic3x3); let inventory_ent = app.world.spawn(inventory).id(); // Process a tick to get past the "on join" logic. app.update(); - client_helper.clear_received(); + helper.clear_received(); // Open the inventory. let open_inventory = OpenInventory::new(inventory_ent); app.world - .get_entity_mut(client_ent) + .get_entity_mut(client) .expect("could not find client") .insert(open_inventory); @@ -78,11 +86,9 @@ fn example_test_open_inventory() { app.update(); // Make assertions - app.world - .get::(client_ent) - .expect("client not found"); + app.world.get::(client).expect("client not found"); - let sent_packets = client_helper.collect_received(); + let sent_packets = helper.collect_received(); sent_packets.assert_count::(1); sent_packets.assert_count::(1); diff --git a/src/tests/instance.rs b/src/tests/instance.rs deleted file mode 100644 index e13d7abdb..000000000 --- a/src/tests/instance.rs +++ /dev/null @@ -1,57 +0,0 @@ -use bevy_app::prelude::*; -use bevy_ecs::prelude::*; -use valence_block::BlockState; -use valence_instance::chunk::UnloadedChunk; -use valence_instance::packet::{BlockEntityUpdateS2c, ChunkDeltaUpdateS2c}; -use valence_instance::Instance; - -use crate::testing::scenario_single_client; - -#[test] -fn block_create_destroy() { - let mut app = App::new(); - - let (_client_ent, mut client_helper) = scenario_single_client(&mut app); - - let (inst_ent, mut inst) = app - .world - .query::<(Entity, &mut Instance)>() - .single_mut(&mut app.world); - - // Insert an empty chunk at (0, 0). - inst.insert_chunk([0, 0], UnloadedChunk::new()); - - // Wait until the next tick to start sending changes. - app.update(); - - let mut inst = app.world.get_mut::(inst_ent).unwrap(); - - // Set some blocks. - inst.set_block([1, 1, 1], BlockState::CHEST); - inst.set_block([1, 2, 1], BlockState::PLAYER_HEAD); - inst.set_block([1, 3, 1], BlockState::OAK_SIGN); - - app.update(); - - { - let recvd = client_helper.collect_received(); - - recvd.assert_count::(1); - recvd.assert_count::(3); - } - - let mut inst = app.world.get_mut::(inst_ent).unwrap(); - - inst.set_block([1, 1, 1], BlockState::AIR); - inst.set_block([1, 2, 1], BlockState::AIR); - inst.set_block([1, 3, 1], BlockState::AIR); - - app.update(); - - { - let recvd = client_helper.collect_received(); - - recvd.assert_count::(1); - recvd.assert_count::(0); - } -} diff --git a/src/tests/inventory.rs b/src/tests/inventory.rs index ab6fd042e..34d6b41a9 100644 --- a/src/tests/inventory.rs +++ b/src/tests/inventory.rs @@ -12,31 +12,35 @@ use valence_inventory::{ Inventory, InventoryKind, OpenInventory, }; -use crate::testing::scenario_single_client; +use crate::testing::ScenarioSingleClient; #[test] fn test_should_open_inventory() { - let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); let inventory = Inventory::new(InventoryKind::Generic3x3); let inventory_ent = app.world.spawn(inventory).id(); // Process a tick to get past the "on join" logic. app.update(); - client_helper.clear_received(); + helper.clear_received(); // Open the inventory. let open_inventory = OpenInventory::new(inventory_ent); app.world - .get_entity_mut(client_ent) + .get_entity_mut(client) .expect("could not find client") .insert(open_inventory); app.update(); // Make assertions - let sent_packets = client_helper.collect_received(); + let sent_packets = helper.collect_received(); sent_packets.assert_count::(1); sent_packets.assert_count::(1); @@ -45,61 +49,69 @@ fn test_should_open_inventory() { #[test] fn test_should_close_inventory() { - let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); let inventory = Inventory::new(InventoryKind::Generic3x3); let inventory_ent = app.world.spawn(inventory).id(); // Process a tick to get past the "on join" logic. app.update(); - client_helper.clear_received(); + helper.clear_received(); // Open the inventory. let open_inventory = OpenInventory::new(inventory_ent); app.world - .get_entity_mut(client_ent) + .get_entity_mut(client) .expect("could not find client") .insert(open_inventory); app.update(); - client_helper.clear_received(); + helper.clear_received(); // Close the inventory. app.world - .get_entity_mut(client_ent) + .get_entity_mut(client) .expect("could not find client") .remove::(); app.update(); // Make assertions - let sent_packets = client_helper.collect_received(); + let sent_packets = helper.collect_received(); sent_packets.assert_count::(1); } #[test] fn test_should_remove_invalid_open_inventory() { - let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); let inventory = Inventory::new(InventoryKind::Generic3x3); let inventory_ent = app.world.spawn(inventory).id(); // Process a tick to get past the "on join" logic. app.update(); - client_helper.clear_received(); + helper.clear_received(); // Open the inventory. let open_inventory = OpenInventory::new(inventory_ent); app.world - .get_entity_mut(client_ent) + .get_entity_mut(client) .expect("could not find client") .insert(open_inventory); app.update(); - client_helper.clear_received(); + helper.clear_received(); // Remove the inventory. app.world.despawn(inventory_ent); @@ -107,35 +119,39 @@ fn test_should_remove_invalid_open_inventory() { app.update(); // Make assertions - assert!(app.world.get::(client_ent).is_none()); + assert!(app.world.get::(client).is_none()); - let sent_packets = client_helper.collect_received(); + let sent_packets = helper.collect_received(); sent_packets.assert_count::(1); } #[test] fn test_should_modify_player_inventory_click_slot() { - let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); // Process a tick to get past the "on join" logic. app.update(); - client_helper.clear_received(); + helper.clear_received(); let mut inventory = app .world - .get_mut::(client_ent) + .get_mut::(client) .expect("could not find inventory for client"); inventory.set_slot(20, ItemStack::new(ItemKind::Diamond, 2, None)); // Make the client click the slot and pick up the item. let state_id = app .world - .get::(client_ent) + .get::(client) .unwrap() .state_id(); - client_helper.send(&ClickSlotC2s { + helper.send(&ClickSlotC2s { window_id: 0, button: 0, mode: ClickMode::Click, @@ -151,7 +167,7 @@ fn test_should_modify_player_inventory_click_slot() { app.update(); // Make assertions - let sent_packets = client_helper.collect_received(); + let sent_packets = helper.collect_received(); // because the inventory was changed as a result of the client's click, the // server should not send any packets to the client because the client @@ -161,14 +177,14 @@ fn test_should_modify_player_inventory_click_slot() { let inventory = app .world - .get::(client_ent) + .get::(client) .expect("could not find inventory for client"); assert_eq!(inventory.slot(20), None); let cursor_item = app .world - .get::(client_ent) + .get::(client) .expect("could not find client"); assert_eq!( @@ -179,32 +195,36 @@ fn test_should_modify_player_inventory_click_slot() { #[test] fn test_should_modify_player_inventory_server_side() { - let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); // Process a tick to get past the "on join" logic. app.update(); let mut inventory = app .world - .get_mut::(client_ent) + .get_mut::(client) .expect("could not find inventory for client"); inventory.set_slot(20, ItemStack::new(ItemKind::Diamond, 2, None)); app.update(); - client_helper.clear_received(); + helper.clear_received(); // Modify the inventory. let mut inventory = app .world - .get_mut::(client_ent) + .get_mut::(client) .expect("could not find inventory for client"); inventory.set_slot(21, ItemStack::new(ItemKind::IronIngot, 1, None)); app.update(); // Make assertions - let sent_packets = client_helper.collect_received(); + let sent_packets = helper.collect_received(); // because the inventory was modified server side, the client needs to be // updated with the change. sent_packets.assert_count::(1); @@ -212,23 +232,27 @@ fn test_should_modify_player_inventory_server_side() { #[test] fn test_should_sync_entire_player_inventory() { - let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); // Process a tick to get past the "on join" logic. app.update(); - client_helper.clear_received(); + helper.clear_received(); let mut inventory = app .world - .get_mut::(client_ent) + .get_mut::(client) .expect("could not find inventory for client"); inventory.changed = u64::MAX; app.update(); // Make assertions - let sent_packets = client_helper.collect_received(); + let sent_packets = helper.collect_received(); sent_packets.assert_count::(1); } @@ -248,24 +272,31 @@ fn set_up_open_inventory(app: &mut App, client_ent: Entity) -> Entity { #[test] fn test_should_modify_open_inventory_click_slot() { - let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); - let inventory_ent = set_up_open_inventory(&mut app, client_ent); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); + + let inventory_ent = set_up_open_inventory(&mut app, client); + let mut inventory = app .world .get_mut::(inventory_ent) .expect("could not find inventory for client"); + inventory.set_slot(20, ItemStack::new(ItemKind::Diamond, 2, None)); // Process a tick to get past the "on join" logic. app.update(); - client_helper.clear_received(); + helper.clear_received(); // Make the client click the slot and pick up the item. - let inv_state = app.world.get::(client_ent).unwrap(); + let inv_state = app.world.get::(client).unwrap(); let state_id = inv_state.state_id(); let window_id = inv_state.window_id(); - client_helper.send(&ClickSlotC2s { + helper.send(&ClickSlotC2s { window_id, state_id: VarInt(state_id.0), slot_idx: 20, @@ -281,7 +312,7 @@ fn test_should_modify_open_inventory_click_slot() { app.update(); // Make assertions - let sent_packets = client_helper.collect_received(); + let sent_packets = helper.collect_received(); // because the inventory was modified as a result of the client's click, the // server should not send any packets to the client because the client @@ -296,7 +327,7 @@ fn test_should_modify_open_inventory_click_slot() { assert_eq!(inventory.slot(20), None); let cursor_item = app .world - .get::(client_ent) + .get::(client) .expect("could not find client"); assert_eq!( cursor_item.0, @@ -306,13 +337,18 @@ fn test_should_modify_open_inventory_click_slot() { #[test] fn test_should_modify_open_inventory_server_side() { - let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); - let inventory_ent = set_up_open_inventory(&mut app, client_ent); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); + + let inventory_ent = set_up_open_inventory(&mut app, client); // Process a tick to get past the "on join" logic. app.update(); - client_helper.clear_received(); + helper.clear_received(); // Modify the inventory. let mut inventory = app @@ -324,7 +360,7 @@ fn test_should_modify_open_inventory_server_side() { app.update(); // Make assertions - let sent_packets = client_helper.collect_received(); + let sent_packets = helper.collect_received(); // because the inventory was modified server side, the client needs to be // updated with the change. @@ -343,13 +379,18 @@ fn test_should_modify_open_inventory_server_side() { #[test] fn test_should_sync_entire_open_inventory() { - let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); - let inventory_ent = set_up_open_inventory(&mut app, client_ent); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); + + let inventory_ent = set_up_open_inventory(&mut app, client); // Process a tick to get past the "on join" logic. app.update(); - client_helper.clear_received(); + helper.clear_received(); let mut inventory = app .world @@ -360,25 +401,30 @@ fn test_should_sync_entire_open_inventory() { app.update(); // Make assertions - let sent_packets = client_helper.collect_received(); + let sent_packets = helper.collect_received(); sent_packets.assert_count::(1); } #[test] fn test_set_creative_mode_slot_handling() { - let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); + let mut game_mode = app .world - .get_mut::(client_ent) + .get_mut::(client) .expect("could not find client"); *game_mode.as_mut() = GameMode::Creative; // Process a tick to get past the "on join" logic. app.update(); - client_helper.clear_received(); + helper.clear_received(); - client_helper.send(&CreativeInventoryActionC2s { + helper.send(&CreativeInventoryActionC2s { slot: 36, clicked_item: Some(ItemStack::new(ItemKind::Diamond, 2, None)), }); @@ -388,7 +434,7 @@ fn test_set_creative_mode_slot_handling() { // Make assertions let inventory = app .world - .get::(client_ent) + .get::(client) .expect("could not find inventory for client"); assert_eq!( @@ -399,19 +445,24 @@ fn test_set_creative_mode_slot_handling() { #[test] fn test_ignore_set_creative_mode_slot_if_not_creative() { - let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); + let mut game_mode = app .world - .get_mut::(client_ent) + .get_mut::(client) .expect("could not find client"); *game_mode.as_mut() = GameMode::Survival; // Process a tick to get past the "on join" logic. app.update(); - client_helper.clear_received(); + helper.clear_received(); - client_helper.send(&CreativeInventoryActionC2s { + helper.send(&CreativeInventoryActionC2s { slot: 36, clicked_item: Some(ItemStack::new(ItemKind::Diamond, 2, None)), }); @@ -421,33 +472,38 @@ fn test_ignore_set_creative_mode_slot_if_not_creative() { // Make assertions let inventory = app .world - .get::(client_ent) + .get::(client) .expect("could not find inventory for client"); assert_eq!(inventory.slot(36), None); } #[test] fn test_window_id_increments() { - let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); + let inventory = Inventory::new(InventoryKind::Generic9x3); let inventory_ent = app.world.spawn(inventory).id(); // Process a tick to get past the "on join" logic. app.update(); - client_helper.clear_received(); + helper.clear_received(); for _ in 0..3 { let open_inventory = OpenInventory::new(inventory_ent); app.world - .get_entity_mut(client_ent) + .get_entity_mut(client) .expect("could not find client") .insert(open_inventory); app.update(); app.world - .get_entity_mut(client_ent) + .get_entity_mut(client) .expect("could not find client") .remove::(); @@ -457,28 +513,32 @@ fn test_window_id_increments() { // Make assertions let inv_state = app .world - .get::(client_ent) + .get::(client) .expect("could not find client"); assert_eq!(inv_state.window_id(), 3); } #[test] fn test_should_handle_set_held_item() { - let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); // Process a tick to get past the "on join" logic. app.update(); - client_helper.clear_received(); + helper.clear_received(); - client_helper.send(&UpdateSelectedSlotC2s { slot: 4 }); + helper.send(&UpdateSelectedSlotC2s { slot: 4 }); app.update(); // Make assertions let held = app .world - .get::(client_ent) + .get::(client) .expect("could not find client"); assert_eq!(held.slot(), 40); @@ -486,20 +546,24 @@ fn test_should_handle_set_held_item() { #[test] fn should_not_increment_state_id_on_cursor_item_change() { - let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); // Process a tick to get past the "on join" logic. app.update(); - client_helper.clear_received(); + helper.clear_received(); let inv_state = app .world - .get::(client_ent) + .get::(client) .expect("could not find client"); let expected_state_id = inv_state.state_id().0; - let mut cursor_item = app.world.get_mut::(client_ent).unwrap(); + let mut cursor_item = app.world.get_mut::(client).unwrap(); cursor_item.0 = Some(ItemStack::new(ItemKind::Diamond, 2, None)); app.update(); @@ -507,7 +571,7 @@ fn should_not_increment_state_id_on_cursor_item_change() { // Make assertions let inv_state = app .world - .get::(client_ent) + .get::(client) .expect("could not find client"); assert_eq!( inv_state.state_id().0, @@ -526,20 +590,24 @@ mod dropping_items { #[test] fn should_drop_item_player_action() { - let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); // Process a tick to get past the "on join" logic. app.update(); - client_helper.clear_received(); + helper.clear_received(); let mut inventory = app .world - .get_mut::(client_ent) + .get_mut::(client) .expect("could not find inventory"); inventory.set_slot(36, ItemStack::new(ItemKind::IronIngot, 3, None)); - client_helper.send(&PlayerActionC2s { + helper.send(&PlayerActionC2s { action: PlayerAction::DropItem, position: BlockPos::new(0, 0, 0), direction: Direction::Down, @@ -551,7 +619,7 @@ mod dropping_items { // Make assertions let inventory = app .world - .get::(client_ent) + .get::(client) .expect("could not find client"); assert_eq!( @@ -567,34 +635,38 @@ mod dropping_items { let events = events.iter_current_update_events().collect::>(); assert_eq!(events.len(), 1); - assert_eq!(events[0].client, client_ent); + assert_eq!(events[0].client, client); assert_eq!(events[0].from_slot, Some(36)); assert_eq!( events[0].stack, ItemStack::new(ItemKind::IronIngot, 1, None) ); - let sent_packets = client_helper.collect_received(); + let sent_packets = helper.collect_received(); sent_packets.assert_count::(0); } #[test] fn should_drop_item_stack_player_action() { - let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); // Process a tick to get past the "on join" logic. app.update(); - client_helper.clear_received(); + helper.clear_received(); let mut inventory = app .world - .get_mut::(client_ent) + .get_mut::(client) .expect("could not find inventory"); inventory.set_slot(36, ItemStack::new(ItemKind::IronIngot, 32, None)); - client_helper.send(&PlayerActionC2s { + helper.send(&PlayerActionC2s { action: PlayerAction::DropAllItems, position: BlockPos::new(0, 0, 0), direction: Direction::Down, @@ -606,12 +678,12 @@ mod dropping_items { // Make assertions let held = app .world - .get::(client_ent) + .get::(client) .expect("could not find client"); assert_eq!(held.slot(), 36); let inventory = app .world - .get::(client_ent) + .get::(client) .expect("could not find inventory"); assert_eq!(inventory.slot(36), None); let events = app @@ -620,7 +692,7 @@ mod dropping_items { .expect("expected drop item stack events"); let events = events.iter_current_update_events().collect::>(); assert_eq!(events.len(), 1); - assert_eq!(events[0].client, client_ent); + assert_eq!(events[0].client, client); assert_eq!(events[0].from_slot, Some(36)); assert_eq!( events[0].stack, @@ -630,16 +702,20 @@ mod dropping_items { #[test] fn should_drop_item_stack_set_creative_mode_slot() { - let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); // Process a tick to get past the "on join" logic. app.update(); - client_helper.clear_received(); + helper.clear_received(); - app.world.entity_mut(client_ent).insert(GameMode::Creative); + app.world.entity_mut(client).insert(GameMode::Creative); - client_helper.send(&CreativeInventoryActionC2s { + helper.send(&CreativeInventoryActionC2s { slot: -1, clicked_item: Some(ItemStack::new(ItemKind::IronIngot, 32, None)), }); @@ -655,7 +731,7 @@ mod dropping_items { .collect::>(); assert_eq!(events.len(), 1); - assert_eq!(events[0].client, client_ent); + assert_eq!(events[0].client, client); assert_eq!(events[0].from_slot, None); assert_eq!( events[0].stack, @@ -665,25 +741,29 @@ mod dropping_items { #[test] fn should_drop_item_stack_click_container_outside() { - let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); // Process a tick to get past the "on join" logic. app.update(); - client_helper.clear_received(); + helper.clear_received(); let mut cursor_item = app .world - .get_mut::(client_ent) + .get_mut::(client) .expect("could not find client"); cursor_item.0 = Some(ItemStack::new(ItemKind::IronIngot, 32, None)); let inv_state = app .world - .get_mut::(client_ent) + .get_mut::(client) .expect("could not find client"); let state_id = inv_state.state_id().0; - client_helper.send(&ClickSlotC2s { + helper.send(&ClickSlotC2s { window_id: 0, state_id: VarInt(state_id), slot_idx: -999, @@ -698,7 +778,7 @@ mod dropping_items { // Make assertions let cursor_item = app .world - .get::(client_ent) + .get::(client) .expect("could not find client"); assert_eq!(cursor_item.0, None); @@ -711,7 +791,7 @@ mod dropping_items { let events = events.iter_current_update_events().collect::>(); assert_eq!(events.len(), 1); - assert_eq!(events[0].client, client_ent); + assert_eq!(events[0].client, client); assert_eq!(events[0].from_slot, None); assert_eq!( events[0].stack, @@ -721,28 +801,32 @@ mod dropping_items { #[test] fn should_drop_item_click_container_with_dropkey_single() { - let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); // Process a tick to get past the "on join" logic. app.update(); - client_helper.clear_received(); + helper.clear_received(); let inv_state = app .world - .get_mut::(client_ent) + .get_mut::(client) .expect("could not find client"); let state_id = inv_state.state_id().0; let mut inventory = app .world - .get_mut::(client_ent) + .get_mut::(client) .expect("could not find inventory"); inventory.set_slot(40, ItemStack::new(ItemKind::IronIngot, 32, None)); - client_helper.send(&ClickSlotC2s { + helper.send(&ClickSlotC2s { window_id: 0, slot_idx: 40, button: 0, @@ -766,7 +850,7 @@ mod dropping_items { let events = events.iter_current_update_events().collect::>(); assert_eq!(events.len(), 1); - assert_eq!(events[0].client, client_ent); + assert_eq!(events[0].client, client); assert_eq!(events[0].from_slot, Some(40)); assert_eq!( events[0].stack, @@ -776,28 +860,32 @@ mod dropping_items { #[test] fn should_drop_item_stack_click_container_with_dropkey() { - let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); // Process a tick to get past the "on join" logic. app.update(); - client_helper.clear_received(); + helper.clear_received(); let inv_state = app .world - .get_mut::(client_ent) + .get_mut::(client) .expect("could not find client"); let state_id = inv_state.state_id().0; let mut inventory = app .world - .get_mut::(client_ent) + .get_mut::(client) .expect("could not find inventory"); inventory.set_slot(40, ItemStack::new(ItemKind::IronIngot, 32, None)); - client_helper.send(&ClickSlotC2s { + helper.send(&ClickSlotC2s { window_id: 0, slot_idx: 40, button: 1, // pressing control @@ -821,7 +909,7 @@ mod dropping_items { let events = events.iter_current_update_events().collect::>(); assert_eq!(events.len(), 1); - assert_eq!(events[0].client, client_ent); + assert_eq!(events[0].client, client); assert_eq!(events[0].from_slot, Some(40)); assert_eq!( events[0].stack, @@ -829,20 +917,23 @@ mod dropping_items { ); } + /// The item should be dropped successfully, if the player has an inventory + /// open and the slot id points to his inventory. #[test] fn should_drop_item_player_open_inventory_with_dropkey() { - // The item should be dropped successfully, if the player has an inventory open - // and the slot id points to his inventory. - - let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); // Process a tick to get past the "on join" logic. app.update(); let mut inventory = app .world - .get_mut::(client_ent) + .get_mut::(client) .expect("could not find inventory"); inventory.set_slot( @@ -850,21 +941,21 @@ mod dropping_items { ItemStack::new(ItemKind::IronIngot, 32, None), ); - let _inventory_ent = set_up_open_inventory(&mut app, client_ent); + let _inventory_ent = set_up_open_inventory(&mut app, client); app.update(); - client_helper.clear_received(); + helper.clear_received(); let inv_state = app .world - .get_mut::(client_ent) + .get_mut::(client) .expect("could not find client"); let state_id = inv_state.state_id().0; let window_id = inv_state.window_id(); - client_helper.send(&ClickSlotC2s { + helper.send(&ClickSlotC2s { window_id, state_id: VarInt(state_id), slot_idx: 50, // not pressing control @@ -887,13 +978,13 @@ mod dropping_items { let player_inventory = app .world - .get::(client_ent) + .get::(client) .expect("could not find inventory"); let events = events.iter_current_update_events().collect::>(); assert_eq!(events.len(), 1); - assert_eq!(events[0].client, client_ent); + assert_eq!(events[0].client, client); assert_eq!( events[0].from_slot, Some(convert_to_player_slot_id(InventoryKind::Generic9x3, 50)) @@ -913,20 +1004,23 @@ mod dropping_items { } } +/// The item stack should be dropped successfully, if the player has an +/// inventory open and the slot id points to his inventory. #[test] fn should_drop_item_stack_player_open_inventory_with_dropkey() { - // The item stack should be dropped successfully, if the player has an inventory - // open and the slot id points to his inventory. - - let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); // Process a tick to get past the "on join" logic. app.update(); let mut inventory = app .world - .get_mut::(client_ent) + .get_mut::(client) .expect("could not find inventory"); inventory.set_slot( @@ -934,20 +1028,20 @@ fn should_drop_item_stack_player_open_inventory_with_dropkey() { ItemStack::new(ItemKind::IronIngot, 32, None), ); - let _inventory_ent = set_up_open_inventory(&mut app, client_ent); + let _inventory_ent = set_up_open_inventory(&mut app, client); app.update(); - client_helper.clear_received(); + helper.clear_received(); let inv_state = app .world - .get_mut::(client_ent) + .get_mut::(client) .expect("could not find client"); let state_id = inv_state.state_id().0; let window_id = inv_state.window_id(); - client_helper.send(&ClickSlotC2s { + helper.send(&ClickSlotC2s { window_id, state_id: VarInt(state_id), slot_idx: 50, // pressing control, the whole stack is dropped @@ -970,13 +1064,13 @@ fn should_drop_item_stack_player_open_inventory_with_dropkey() { let player_inventory = app .world - .get::(client_ent) + .get::(client) .expect("could not find inventory"); let events = events.iter_current_update_events().collect::>(); assert_eq!(events.len(), 1); - assert_eq!(events[0].client, client_ent); + assert_eq!(events[0].client, client); assert_eq!( events[0].from_slot, Some(convert_to_player_slot_id(InventoryKind::Generic9x3, 50)) @@ -993,17 +1087,21 @@ fn should_drop_item_stack_player_open_inventory_with_dropkey() { #[test] fn dragging_items() { - let mut app = App::new(); - let (client_ent, mut client_helper) = scenario_single_client(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); // Process a tick to get past the "on join" logic. app.update(); - client_helper.clear_received(); + helper.clear_received(); - app.world.get_mut::(client_ent).unwrap().0 = + app.world.get_mut::(client).unwrap().0 = Some(ItemStack::new(ItemKind::Diamond, 64, None)); - let inv_state = app.world.get::(client_ent).unwrap(); + let inv_state = app.world.get::(client).unwrap(); let window_id = inv_state.window_id(); let state_id = inv_state.state_id().0; @@ -1029,15 +1127,15 @@ fn dragging_items() { ], carried_item: Some(ItemStack::new(ItemKind::Diamond, 1, None)), }; - client_helper.send(&drag_packet); + helper.send(&drag_packet); app.update(); - let sent_packets = client_helper.collect_received(); + let sent_packets = helper.collect_received(); assert_eq!(sent_packets.0.len(), 0); let cursor_item = app .world - .get::(client_ent) + .get::(client) .expect("could not find client"); assert_eq!( @@ -1047,7 +1145,7 @@ fn dragging_items() { let inventory = app .world - .get::(client_ent) + .get::(client) .expect("could not find inventory"); for i in 9..12 { diff --git a/src/tests/layer.rs b/src/tests/layer.rs new file mode 100644 index 000000000..2ad71384e --- /dev/null +++ b/src/tests/layer.rs @@ -0,0 +1,55 @@ +use valence_block::BlockState; +use valence_layer::chunk::UnloadedChunk; +use valence_layer::packet::{BlockEntityUpdateS2c, ChunkDeltaUpdateS2c}; +use valence_layer::ChunkLayer; + +use crate::testing::ScenarioSingleClient; + +#[test] +fn block_create_destroy() { + let ScenarioSingleClient { + mut app, + client: _, + mut helper, + layer: layer_ent, + } = ScenarioSingleClient::new(); + + let mut layer = app.world.get_mut::(layer_ent).unwrap(); + + // Insert an empty chunk at (0, 0). + layer.insert_chunk([0, 0], UnloadedChunk::new()); + + // Wait until the next tick to start sending changes. + app.update(); + + let mut layer = app.world.get_mut::(layer_ent).unwrap(); + + // Set some blocks. + layer.set_block([1, 1, 1], BlockState::CHEST); + layer.set_block([1, 2, 1], BlockState::PLAYER_HEAD); + layer.set_block([1, 3, 1], BlockState::OAK_SIGN); + + app.update(); + + { + let recvd = helper.collect_received(); + + recvd.assert_count::(1); + recvd.assert_count::(3); + } + + let mut layer = app.world.get_mut::(layer_ent).unwrap(); + + layer.set_block([1, 1, 1], BlockState::AIR); + layer.set_block([1, 2, 1], BlockState::AIR); + layer.set_block([1, 3, 1], BlockState::AIR); + + app.update(); + + { + let recvd = helper.collect_received(); + + recvd.assert_count::(1); + recvd.assert_count::(0); + } +} diff --git a/src/tests/player_list.rs b/src/tests/player_list.rs index d5b1936f3..650c8aad8 100644 --- a/src/tests/player_list.rs +++ b/src/tests/player_list.rs @@ -1,27 +1,24 @@ -use bevy_app::prelude::*; -use bevy_ecs::prelude::*; -use valence_client::packet::PlayerSpawnS2c; -use valence_instance::chunk::UnloadedChunk; -use valence_instance::Instance; +use valence_entity::packet::PlayerSpawnS2c; +use valence_layer::chunk::UnloadedChunk; +use valence_layer::ChunkLayer; use valence_player_list::packet::PlayerListS2c; -use crate::testing::{create_mock_client, scenario_single_client}; +use crate::testing::{create_mock_client, ScenarioSingleClient}; #[test] fn player_list_arrives_before_player_spawn() { - let mut app = App::new(); + let ScenarioSingleClient { + mut app, + client: _, + helper: mut client_helper_1, + layer: layer_ent, + } = ScenarioSingleClient::new(); - let (_client_ent_1, mut client_helper_1) = scenario_single_client(&mut app); - - let (inst_ent, mut inst) = app - .world - .query::<(Entity, &mut Instance)>() - .get_single_mut(&mut app.world) - .unwrap(); + let mut layer = app.world.get_mut::(layer_ent).unwrap(); for z in -5..5 { for x in -5..5 { - inst.insert_chunk([x, z], UnloadedChunk::new()); + layer.insert_chunk([x, z], UnloadedChunk::new()); } } @@ -39,7 +36,9 @@ fn player_list_arrives_before_player_spawn() { } let (mut client_2, mut client_helper_2) = create_mock_client("test_2"); - client_2.player.location.0 = inst_ent; + client_2.player.layer.0 = layer_ent; + client_2.visible_chunk_layer.0 = layer_ent; + client_2.visible_entity_layers.0.insert(layer_ent); app.world.spawn(client_2); @@ -66,27 +65,4 @@ fn player_list_arrives_before_player_spawn() { assert!(pkt.actions.add_player()); assert_eq!(pkt.entries.len(), 2); } - - /* - { - let recvd = client_helper_1.collect_received(); - recvd.assert_count::(1); - recvd.assert_count::(1); - recvd.assert_order::<(PlayerListS2c, PlayerSpawnS2c)>(); - - let pkt = recvd.first::(); - assert!(pkt.actions.add_player()); - assert_eq!(pkt.entries.len(), 2); - } - - { - let recvd = client_helper_2.collect_received(); - recvd.assert_count::(1); - recvd.assert_count::(1); - recvd.assert_order::<(PlayerListS2c, PlayerSpawnS2c)>(); - - let pkt = recvd.first::(); - assert!(pkt.actions.add_player()); - assert_eq!(pkt.entries.len(), 2); - }*/ } diff --git a/src/tests/weather.rs b/src/tests/weather.rs index ada7fdce6..aa0b8f052 100644 --- a/src/tests/weather.rs +++ b/src/tests/weather.rs @@ -1,101 +1,94 @@ +/* use bevy_app::App; use valence_client::packet::GameStateChangeS2c; use valence_client::weather::{Rain, Thunder}; use valence_client::Client; use valence_instance::Instance; -use crate::testing::{scenario_single_client, PacketFrames}; +use crate::testing::{PacketFrames, ScenarioSingleClient}; #[test] fn test_weather_instance() { - let mut app = App::new(); - let (_, mut client_helper) = scenario_single_client(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer, + } = ScenarioSingleClient::new(); // Process a tick to get past the "on join" logic. app.update(); - client_helper.clear_received(); - - // Get the instance entity. - let instance_ent = app - .world - .iter_entities() - .find(|e| e.contains::()) - .expect("could not find instance") - .id(); + helper.clear_received(); // Insert a rain component to the instance. - app.world.entity_mut(instance_ent).insert(Rain(0.5)); + app.world.entity_mut(layer).insert(Rain(0.5)); for _ in 0..2 { app.update(); } // Alter a rain component of the instance. - app.world.entity_mut(instance_ent).insert(Rain(1.0)); + app.world.entity_mut(layer).insert(Rain(1.0)); app.update(); // Insert a thunder component to the instance. - app.world.entity_mut(instance_ent).insert(Thunder(0.5)); + app.world.entity_mut(layer).insert(Thunder(0.5)); app.update(); // Alter a thunder component of the instance. - app.world.entity_mut(instance_ent).insert(Thunder(1.0)); + app.world.entity_mut(layer).insert(Thunder(1.0)); app.update(); // Remove the rain component from the instance. - app.world.entity_mut(instance_ent).remove::(); + app.world.entity_mut(layer).remove::(); for _ in 0..2 { app.update(); } // Make assertions. - let sent_packets = client_helper.collect_received(); + let sent_packets = helper.collect_received(); assert_weather_packets(sent_packets); } #[test] fn test_weather_client() { - let mut app = App::new(); - let (_, mut client_helper) = scenario_single_client(&mut app); + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer, + } = ScenarioSingleClient::new(); // Process a tick to get past the "on join" logic. app.update(); - client_helper.clear_received(); - - // Get the client entity. - let client_ent = app - .world - .iter_entities() - .find(|e| e.contains::()) - .expect("could not find client") - .id(); + helper.clear_received(); // Insert a rain component to the client. - app.world.entity_mut(client_ent).insert(Rain(0.5)); + app.world.entity_mut(client).insert(Rain(0.5)); for _ in 0..2 { app.update(); } // Alter a rain component of the client. - app.world.entity_mut(client_ent).insert(Rain(1.0)); + app.world.entity_mut(client).insert(Rain(1.0)); app.update(); // Insert a thunder component to the client. - app.world.entity_mut(client_ent).insert(Thunder(0.5)); + app.world.entity_mut(client).insert(Thunder(0.5)); app.update(); // Alter a thunder component of the client. - app.world.entity_mut(client_ent).insert(Thunder(1.0)); + app.world.entity_mut(client).insert(Thunder(1.0)); app.update(); // Remove the rain component from the client. - app.world.entity_mut(client_ent).remove::(); + app.world.entity_mut(client).remove::(); for _ in 0..2 { app.update(); } // Make assertions. - let sent_packets = client_helper.collect_received(); + let sent_packets = helper.collect_received(); assert_weather_packets(sent_packets); } @@ -104,3 +97,4 @@ fn test_weather_client() { fn assert_weather_packets(sent_packets: PacketFrames) { sent_packets.assert_count::(6); } +*/ diff --git a/src/tests/world_border.rs b/src/tests/world_border.rs index 733c8e527..ac8ea58fc 100644 --- a/src/tests/world_border.rs +++ b/src/tests/world_border.rs @@ -1,3 +1,6 @@ +// TODO: fix + +/* use std::time::Duration; use bevy_app::App; @@ -7,9 +10,8 @@ use valence_registry::{Entity, Mut}; use valence_world_border::packet::*; use valence_world_border::*; -use crate::testing::{create_mock_client, scenario_single_client, MockClientHelper}; +use crate::testing::{create_mock_client, MockClientHelper}; -/* #[test] fn test_intialize_on_join() { let mut app = App::new(); From aaa9c58f2ebf6a059a3d63d5354d604aa6171deb Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 17 Jul 2023 01:23:59 -0700 Subject: [PATCH 14/47] fix packet_inspector --- crates/valence_world_border/src/lib.rs | 4 ++-- tools/packet_inspector/src/app/text_viewer.rs | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/crates/valence_world_border/src/lib.rs b/crates/valence_world_border/src/lib.rs index dddd3bd60..14b4ac991 100644 --- a/crates/valence_world_border/src/lib.rs +++ b/crates/valence_world_border/src/lib.rs @@ -73,11 +73,11 @@ clippy::dbg_macro )] +pub mod packet; + // TODO: fix. /* -pub mod packet; - use std::time::{Duration, Instant}; use bevy_app::prelude::*; diff --git a/tools/packet_inspector/src/app/text_viewer.rs b/tools/packet_inspector/src/app/text_viewer.rs index 6da07d594..ec95b18ca 100644 --- a/tools/packet_inspector/src/app/text_viewer.rs +++ b/tools/packet_inspector/src/app/text_viewer.rs @@ -1,10 +1,7 @@ use super::{SharedState, Tab, View}; mod utils { - use packet_inspector::Packet as ProxyPacket; - use packet_inspector::{PacketSide, PacketState}; - use valence::protocol::{Decode, Packet}; - + use packet_inspector::{Packet as ProxyPacket, PacketSide, PacketState}; use valence::advancement::packet::*; use valence::boss_bar::packet::*; use valence::client::action::*; @@ -24,9 +21,9 @@ mod utils { use valence::client::teleport::*; use valence::client::title::*; use valence::entity::packet::*; - use valence::instance::packet::*; use valence::inventory::packet::synchronize_recipes::*; use valence::inventory::packet::*; + use valence::layer::packet::*; use valence::network::packet::*; use valence::particle::*; use valence::player_list::packet::*; @@ -35,6 +32,7 @@ mod utils { use valence::protocol::packet::map::*; use valence::protocol::packet::scoreboard::*; use valence::protocol::packet::sound::*; + use valence::protocol::{Decode, Packet}; use valence::registry::tags::*; use valence::world_border::packet::*; From 885c3a377076ff66362d9e7f84edc9000c18bea8 Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 17 Jul 2023 01:37:54 -0700 Subject: [PATCH 15/47] compile benches --- benches/idle.rs | 8 ++++---- benches/many_players.rs | 18 ++++++++---------- benches/packet.rs | 2 +- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/benches/idle.rs b/benches/idle.rs index 7634b8555..5e320ac9d 100644 --- a/benches/idle.rs +++ b/benches/idle.rs @@ -25,19 +25,19 @@ fn setup( biomes: Res, server: Res, ) { - let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - instance.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); } } for z in -50..50 { for x in -50..50 { - instance.set_block([x, 64, z], BlockState::GRASS_BLOCK); + layer.chunk.set_block([x, 64, z], BlockState::GRASS_BLOCK); } } - commands.spawn(instance); + commands.spawn(layer); } diff --git a/benches/many_players.rs b/benches/many_players.rs index b5d59e034..0b980514b 100644 --- a/benches/many_players.rs +++ b/benches/many_players.rs @@ -14,9 +14,8 @@ use valence_core::chunk_pos::ChunkPos; use valence_core::{ident, CoreSettings, Server}; use valence_dimension::DimensionTypeRegistry; use valence_entity::Position; -use valence_instance::chunk::UnloadedChunk; -use valence_instance::Instance; -use valence_layer::{ChunkLayer, EntityLayer}; +use valence_layer::chunk::UnloadedChunk; +use valence_layer::LayerBundle; use valence_network::NetworkPlugin; pub fn many_players(c: &mut Criterion) { @@ -34,7 +33,7 @@ fn run_many_players( let mut app = App::new(); app.insert_resource(CoreSettings { - compression_threshold: Some(256), + compression_threshold: None, ..Default::default() }); @@ -46,7 +45,7 @@ fn run_many_players( app.update(); // Initialize plugins. - let mut chunks = ChunkLayer::new( + let mut layer = LayerBundle::new( ident!("overworld"), app.world.resource::(), app.world.resource::(), @@ -55,14 +54,13 @@ fn run_many_players( for z in -world_size..world_size { for x in -world_size..world_size { - chunks.insert_chunk(ChunkPos::new(x, z), UnloadedChunk::new()); + layer + .chunk + .insert_chunk(ChunkPos::new(x, z), UnloadedChunk::new()); } } - let layer = app - .world - .spawn((chunks, EntityLayer::new(app.world.resource::()))) - .id(); + let layer = app.world.spawn(layer).id(); let mut clients = vec![]; diff --git a/benches/packet.rs b/benches/packet.rs index 690d76d75..0bb8dc905 100644 --- a/benches/packet.rs +++ b/benches/packet.rs @@ -12,7 +12,7 @@ use valence::protocol::var_int::VarInt; use valence::text::TextFormat; use valence_core::protocol::encode::{PacketWriter, WritePacket}; use valence_entity::packet::EntitySpawnS2c; -use valence_instance::packet::ChunkDataS2c; +use valence_layer::packet::ChunkDataS2c; use valence_player_list::packet::PlayerListHeaderS2c; pub fn packet(c: &mut Criterion) { From 85379f1d0908cedccedafeeb04cb4bbb16043812 Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 17 Jul 2023 02:42:14 -0700 Subject: [PATCH 16/47] update old visible layers correctly --- crates/valence_client/src/lib.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/crates/valence_client/src/lib.rs b/crates/valence_client/src/lib.rs index fe421d4b6..f2388ce7f 100644 --- a/crates/valence_client/src/lib.rs +++ b/crates/valence_client/src/lib.rs @@ -996,9 +996,6 @@ fn update_view_and_layers( } } } - - // Update the old chunk layer. - old_chunk_layer.0 = chunk_layer.0; } else { // Update the client's visible entity layers. if visible_entity_layers.is_changed() { @@ -1037,11 +1034,6 @@ fn update_view_and_layers( } } } - - // old := new - old_visible_entity_layers - .0 - .clone_from(&visible_entity_layers.0); } // Update the client's view (chunk position and view distance) @@ -1101,6 +1093,16 @@ fn update_view_and_layers( } } } + + // Update the old layers. + + old_chunk_layer.0 = chunk_layer.0; + + if visible_entity_layers.is_changed() { + old_visible_entity_layers + .0 + .clone_from(&visible_entity_layers.0); + } }, ); } From 6ebc00494a5e483a59f7000ac826245f0b3d3306 Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 17 Jul 2023 03:21:39 -0700 Subject: [PATCH 17/47] update playground --- tools/playground/src/playground.template.rs | 41 ++++++++++++++++----- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/tools/playground/src/playground.template.rs b/tools/playground/src/playground.template.rs index 3b490c483..aa17ada01 100644 --- a/tools/playground/src/playground.template.rs +++ b/tools/playground/src/playground.template.rs @@ -26,30 +26,53 @@ fn setup( biomes: Res, dimensions: Res, ) { - let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - instance.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); } } for z in -25..25 { for x in -25..25 { - instance.set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); + layer + .chunk + .set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); } } - commands.spawn(instance); + commands.spawn(layer); } fn init_clients( - mut clients: Query<(&mut Location, &mut Position), Added>, - instances: Query>, + mut clients: Query< + ( + &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut Position, + &mut GameMode, + ), + Added, + >, + layers: Query, With)>, ) { - for (mut loc, mut pos) in &mut clients { - loc.0 = instances.single(); - pos.set([0.5, SPAWN_Y as f64 + 1.0, 0.5]); + for ( + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut pos, + mut game_mode, + ) in &mut clients + { + let layer = layers.single(); + + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); + pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); + *game_mode = GameMode::Creative; } } From 550f1110a5cc85f44a7461db015ea0dfde147b01 Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 17 Jul 2023 04:49:12 -0700 Subject: [PATCH 18/47] send_*_infallible, fix chunk load bugs --- crates/valence_client/src/lib.rs | 25 ++- crates/valence_layer/src/chunk.rs | 74 ++++--- crates/valence_layer/src/chunk/loaded.rs | 17 +- crates/valence_layer/src/entity.rs | 45 ++--- crates/valence_layer/src/message.rs | 20 +- src/tests/client.rs | 247 +---------------------- src/tests/layer.rs | 235 ++++++++++++++++++++- 7 files changed, 330 insertions(+), 333 deletions(-) diff --git a/crates/valence_client/src/lib.rs b/crates/valence_client/src/lib.rs index f2388ce7f..d88535b31 100644 --- a/crates/valence_client/src/lib.rs +++ b/crates/valence_client/src/lib.rs @@ -122,6 +122,7 @@ impl Plugin for ClientPlugin { update_view_and_layers .after(spawn::initial_join) .after(handle_layer_messages), + cleanup_chunks_after_client_despawn.after(update_view_and_layers), spawn::update_respawn_position.after(update_view_and_layers), spawn::respawn.after(spawn::update_respawn_position), remove_entities.after(update_view_and_layers), @@ -132,7 +133,6 @@ impl Plugin for ClientPlugin { ) .in_set(UpdateClientsSet), flush_packets.in_set(FlushPacketsSet), - cleanup_chunks_after_client_despawn.after(UpdateClientsSet), ), ) .configure_set(PreUpdate, SpawnClientsSet) @@ -260,6 +260,8 @@ pub struct ClientBundleArgs { pub enc: PacketEncoder, } +/// Marker [`Component`] for client entities. This component should exist even +/// if the client is disconnected. #[derive(Component, Copy, Clone)] pub struct ClientMarker; @@ -755,23 +757,24 @@ fn handle_layer_messages( data: &bytes[range], }); } - valence_layer::chunk::LocalMsg::LoadOrUnloadChunk { pos } => { - match bytes[range].last() { - Some(&1) => { + valence_layer::chunk::LocalMsg::ChangeChunkState { pos } => { + match &bytes[range] { + [ChunkLayer::LOAD, .., ChunkLayer::UNLOAD] => { + // Chunk is being loaded and unloaded on the + // same tick, so there's no need to do anything. + } + [.., ChunkLayer::LOAD | ChunkLayer::OVERWRITE] => { // Load chunk. if let Some(chunk) = chunk_layer.chunk(pos) { chunk.write_init_packets(&mut *client, pos, chunk_layer.info()); chunk.inc_viewer_count(); } } - Some(&0) => { - // Unload chunk. If the chunk doesn't exist, then it shouldn't be - // loaded on the client and no packet needs to be sent. - if chunk_layer.chunk(pos).is_some() { - client.write_packet(&UnloadChunkS2c { pos }); - } + [.., ChunkLayer::UNLOAD] => { + // Unload chunk. + client.write_packet(&UnloadChunkS2c { pos }); } - _ => panic!("invalid message data"), + _ => unreachable!("invalid message data while changing chunk state"), } } }); diff --git a/crates/valence_layer/src/chunk.rs b/crates/valence_layer/src/chunk.rs index 6ecc920b8..11a033500 100644 --- a/crates/valence_layer/src/chunk.rs +++ b/crates/valence_layer/src/chunk.rs @@ -7,7 +7,7 @@ pub mod unloaded; use std::borrow::Cow; use std::collections::hash_map::{Entry, OccupiedEntry, VacantEntry}; -use std::convert::Infallible; +use std::fmt; use bevy_app::prelude::*; use bevy_ecs::prelude::*; @@ -42,7 +42,6 @@ pub struct ChunkLayer { } #[doc(hidden)] -#[derive(Debug)] pub struct ChunkLayerInfo { dimension_type_name: Ident, height: u32, @@ -54,6 +53,19 @@ pub struct ChunkLayerInfo { sky_light_arrays: Box<[LengthPrefixedArray]>, } +impl fmt::Debug for ChunkLayerInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ChunkLayerInfo") + .field("dimension_type_name", &self.dimension_type_name) + .field("height", &self.height) + .field("min_y", &self.min_y) + .field("biome_registry_len", &self.biome_registry_len) + .field("compression_threshold", &self.compression_threshold) + // Ignore sky light mask and array. + .finish() + } +} + type ChunkLayerMessages = Messages; #[doc(hidden)] @@ -76,7 +88,7 @@ pub enum LocalMsg { /// is not lost when messages are sorted. /// /// Message content is a single byte indicating load (1) or unload (0). - LoadOrUnloadChunk { pos: ChunkPos }, + ChangeChunkState { pos: ChunkPos }, /// Message content is the data for a single biome in the "change biomes" /// packet. ChangeBiome { pos: ChunkPos }, @@ -87,12 +99,19 @@ impl GetChunkPos for LocalMsg { match *self { LocalMsg::PacketAt { pos } => pos, LocalMsg::ChangeBiome { pos } => pos, - LocalMsg::LoadOrUnloadChunk { pos } => pos, + LocalMsg::ChangeChunkState { pos } => pos, } } } impl ChunkLayer { + #[doc(hidden)] + pub const LOAD: u8 = 0; + #[doc(hidden)] + pub const UNLOAD: u8 = 1; + #[doc(hidden)] + pub const OVERWRITE: u8 = 2; + #[track_caller] pub fn new( dimension_type_name: impl Into>, @@ -196,10 +215,9 @@ impl ChunkLayer { { self.chunks.retain(|pos, chunk| { if !f(*pos, chunk) { - let _ = self - .messages - .send_local::(LocalMsg::LoadOrUnloadChunk { pos: *pos }, |b| { - Ok(b.push(0)) + self.messages + .send_local_infallible(LocalMsg::ChangeChunkState { pos: *pos }, |b| { + b.push(Self::UNLOAD) }); false @@ -406,9 +424,8 @@ impl WritePacket for ChunkLayer { } fn write_packet_bytes(&mut self, bytes: &[u8]) { - let _ = self - .messages - .send_global::(GlobalMsg::Packet, |b| Ok(b.extend_from_slice(bytes))); + self.messages + .send_global_infallible(GlobalMsg::Packet, |b| b.extend_from_slice(bytes)); } } @@ -431,11 +448,10 @@ impl<'a> WritePacket for ViewWriter<'a> { } fn write_packet_bytes(&mut self, bytes: &[u8]) { - let _ = self - .layer + self.layer .messages - .send_local::(LocalMsg::PacketAt { pos: self.pos }, |b| { - Ok(b.extend_from_slice(bytes)) + .send_local_infallible(LocalMsg::PacketAt { pos: self.pos }, |b| { + b.extend_from_slice(bytes) }); } } @@ -471,11 +487,11 @@ impl<'a> OccupiedChunkEntry<'a> { } pub fn insert(&mut self, chunk: UnloadedChunk) -> UnloadedChunk { - let _ = self.messages.send_local::( - LocalMsg::LoadOrUnloadChunk { + self.messages.send_local_infallible( + LocalMsg::ChangeChunkState { pos: *self.entry.key(), }, - |b| Ok(b.push(1)), + |b| b.push(ChunkLayer::OVERWRITE), ); self.entry.get_mut().insert(chunk) @@ -489,26 +505,26 @@ impl<'a> OccupiedChunkEntry<'a> { self.entry.key() } - pub fn remove(mut self) -> UnloadedChunk { - let _ = self.messages.send_local::( - LocalMsg::LoadOrUnloadChunk { + pub fn remove(self) -> UnloadedChunk { + self.messages.send_local_infallible( + LocalMsg::ChangeChunkState { pos: *self.entry.key(), }, - |b| Ok(b.push(0)), + |b| b.push(ChunkLayer::UNLOAD), ); - self.entry.get_mut().remove() + self.entry.remove().remove() } pub fn remove_entry(mut self) -> (ChunkPos, UnloadedChunk) { let pos = *self.entry.key(); let chunk = self.entry.get_mut().remove(); - let _ = self.messages.send_local::( - LocalMsg::LoadOrUnloadChunk { + self.messages.send_local_infallible( + LocalMsg::ChangeChunkState { pos: *self.entry.key(), }, - |b| Ok(b.push(0)), + |b| b.push(ChunkLayer::UNLOAD), ); (pos, chunk) @@ -527,11 +543,11 @@ impl<'a> VacantChunkEntry<'a> { let mut loaded = LoadedChunk::new(self.height); loaded.insert(chunk); - let _ = self.messages.send_local::( - LocalMsg::LoadOrUnloadChunk { + self.messages.send_local_infallible( + LocalMsg::ChangeChunkState { pos: *self.entry.key(), }, - |b| Ok(b.push(1)), + |b| b.push(ChunkLayer::LOAD), ); self.entry.insert(loaded) diff --git a/crates/valence_layer/src/chunk/loaded.rs b/crates/valence_layer/src/chunk/loaded.rs index 6372ffd1b..aefbedf06 100644 --- a/crates/valence_layer/src/chunk/loaded.rs +++ b/crates/valence_layer/src/chunk/loaded.rs @@ -1,6 +1,5 @@ use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet}; -use std::convert::Infallible; use std::mem; use std::sync::atomic::{AtomicU32, Ordering}; @@ -216,15 +215,13 @@ impl LoadedChunk { let global_y = info.min_y + sect_y as i32 * 16 + offset_y as i32; let global_z = pos.z * 16 + offset_z as i32; - let _ = messages.send_local::(LocalMsg::PacketAt { pos }, |buf| { + messages.send_local_infallible(LocalMsg::PacketAt { pos }, |buf| { let mut writer = PacketWriter::new(buf, info.compression_threshold); writer.write_packet(&BlockUpdateS2c { position: BlockPos::new(global_x, global_y, global_z), block_id: VarInt(block as i32), }); - - Ok(()) }); } _ => { @@ -232,15 +229,13 @@ impl LoadedChunk { | (pos.z as i64 & 0x3fffff) << 20 | (sect_y as i64 + info.min_y.div_euclid(16) as i64) & 0xfffff; - let _ = messages.send_local::(LocalMsg::PacketAt { pos }, |buf| { + messages.send_local_infallible(LocalMsg::PacketAt { pos }, |buf| { let mut writer = PacketWriter::new(buf, info.compression_threshold); writer.write_packet(&ChunkDeltaUpdateS2c { chunk_section_position, blocks: Cow::Borrowed(§.section_updates), }); - - Ok(()) }); } } @@ -270,7 +265,7 @@ impl LoadedChunk { let global_y = info.min_y + y as i32; let global_z = pos.z * 16 + z as i32; - let _ = messages.send_local::(LocalMsg::PacketAt { pos }, |buf| { + messages.send_local_infallible(LocalMsg::PacketAt { pos }, |buf| { let mut writer = PacketWriter::new(buf, info.compression_threshold); writer.write_packet(&BlockEntityUpdateS2c { @@ -278,8 +273,6 @@ impl LoadedChunk { kind: VarInt(kind as i32), data: Cow::Borrowed(nbt), }); - - Ok(()) }); } @@ -289,7 +282,7 @@ impl LoadedChunk { if self.changed_biomes { self.changed_biomes = false; - let _ = messages.send_local::(LocalMsg::ChangeBiome { pos }, |buf| { + messages.send_local_infallible(LocalMsg::ChangeBiome { pos }, |buf| { for sect in self.sections.iter() { sect.biomes .encode_mc_format( @@ -301,8 +294,6 @@ impl LoadedChunk { ) .expect("paletted container encode should always succeed"); } - - Ok(()) }); } diff --git a/crates/valence_layer/src/entity.rs b/crates/valence_layer/src/entity.rs index 505d0d131..1daf54049 100644 --- a/crates/valence_layer/src/entity.rs +++ b/crates/valence_layer/src/entity.rs @@ -1,6 +1,5 @@ use std::collections::hash_map::Entry; use std::collections::BTreeSet; -use std::convert::Infallible; use bevy_app::prelude::*; use bevy_ecs::prelude::*; @@ -127,9 +126,8 @@ impl WritePacket for EntityLayer { } fn write_packet_bytes(&mut self, bytes: &[u8]) { - let _ = self - .messages - .send_global::(GlobalMsg::Packet, |b| Ok(b.extend_from_slice(bytes))); + self.messages + .send_global_infallible(GlobalMsg::Packet, |b| b.extend_from_slice(bytes)); } } @@ -151,11 +149,10 @@ impl<'a> WritePacket for ViewWriter<'a> { } fn write_packet_bytes(&mut self, bytes: &[u8]) { - let _ = self - .layer + self.layer .messages - .send_local::(LocalMsg::PacketAt { pos: self.pos }, |b| { - Ok(b.extend_from_slice(bytes)) + .send_local_infallible(LocalMsg::PacketAt { pos: self.pos }, |b| { + b.extend_from_slice(bytes) }); } } @@ -204,12 +201,12 @@ fn change_entity_positions( if let Entry::Occupied(mut old_cell) = old_layer.entities.entry(old_chunk_pos) { if old_cell.get_mut().remove(&entity) { - let _ = old_layer.messages.send_local::( + old_layer.messages.send_local_infallible( LocalMsg::DespawnEntity { pos: old_chunk_pos, dest_layer: Entity::PLACEHOLDER, }, - |b| Ok(b.extend_from_slice(&entity_id.get().to_ne_bytes())), + |b| b.extend_from_slice(&entity_id.get().to_ne_bytes()), ); if old_cell.get().is_empty() { @@ -227,12 +224,12 @@ fn change_entity_positions( if let Entry::Occupied(mut old_cell) = old_layer.entities.entry(old_chunk_pos) { if old_cell.get_mut().remove(&entity) { - let _ = old_layer.messages.send_local::( + old_layer.messages.send_local_infallible( LocalMsg::DespawnEntity { pos: old_chunk_pos, dest_layer: layer_id.0, }, - |b| Ok(b.extend_from_slice(&entity_id.get().to_ne_bytes())), + |b| b.extend_from_slice(&entity_id.get().to_ne_bytes()), ); if old_cell.get().is_empty() { @@ -244,12 +241,12 @@ fn change_entity_positions( if let Ok(mut layer) = layers.get_mut(layer_id.0) { if layer.entities.entry(chunk_pos).or_default().insert(entity) { - let _ = layer.messages.send_local::( + layer.messages.send_local_infallible( LocalMsg::SpawnEntity { pos: chunk_pos, src_layer: old_layer_id.get(), }, - |b| Ok(b.extend_from_slice(&entity.to_bits().to_ne_bytes())), + |b| b.extend_from_slice(&entity.to_bits().to_ne_bytes()), ); } } @@ -260,23 +257,23 @@ fn change_entity_positions( if let Ok(mut layer) = layers.get_mut(layer_id.0) { if let Entry::Occupied(mut old_cell) = layer.entities.entry(old_chunk_pos) { if old_cell.get_mut().remove(&entity) { - let _ = layer.messages.send_local::( + layer.messages.send_local_infallible( LocalMsg::DespawnEntityTransition { pos: old_chunk_pos, dest_pos: chunk_pos, }, - |b| Ok(b.extend_from_slice(&entity_id.get().to_ne_bytes())), + |b| b.extend_from_slice(&entity_id.get().to_ne_bytes()), ); } } if layer.entities.entry(chunk_pos).or_default().insert(entity) { - let _ = layer.messages.send_local::( + layer.messages.send_local_infallible( LocalMsg::SpawnEntityTransition { pos: chunk_pos, src_pos: old_chunk_pos, }, - |b| Ok(b.extend_from_slice(&entity.to_bits().to_ne_bytes())), + |b| b.extend_from_slice(&entity.to_bits().to_ne_bytes()), ); } } @@ -308,11 +305,9 @@ fn send_entity_update_messages( LocalMsg::PacketAt { pos: chunk_pos } }; - let _ = layer.messages.send_local::(msg, |b| { - Ok(update.write_update_packets(PacketWriter::new( - b, - layer.compression_threshold, - ))) + layer.messages.send_local_infallible(msg, |b| { + update + .write_update_packets(PacketWriter::new(b, layer.compression_threshold)) }); } else { panic!( @@ -327,9 +322,9 @@ fn send_entity_update_messages( fn send_layer_despawn_messages(mut layers: Query<&mut EntityLayer, With>) { for mut layer in &mut layers { - let _ = layer + layer .messages - .send_global::(GlobalMsg::DespawnLayer, |_| Ok(())); + .send_global_infallible(GlobalMsg::DespawnLayer, |_| {}); } } diff --git a/crates/valence_layer/src/message.rs b/crates/valence_layer/src/message.rs index a41c6e99d..de25de2f5 100644 --- a/crates/valence_layer/src/message.rs +++ b/crates/valence_layer/src/message.rs @@ -1,4 +1,5 @@ use core::fmt; +use std::convert::Infallible; use std::ops::Range; use valence_core::chunk_pos::{ChunkPos, ChunkView}; @@ -71,6 +72,14 @@ where Ok(()) } + pub(crate) fn send_global_infallible(&mut self, msg: G, f: impl FnOnce(&mut Vec)) { + let _ = self.send_global::(msg, |b| Ok(f(b))); + } + + pub(crate) fn send_local_infallible(&mut self, msg: L, f: impl FnOnce(&mut Vec)) { + let _ = self.send_local::(msg, |b| Ok(f(b))); + } + /// Readies messages to be read by clients. pub(crate) fn ready(&mut self) { debug_assert!(!self.is_ready); @@ -220,8 +229,6 @@ impl GetChunkPos for MessagePair { #[cfg(test)] mod tests { - use std::convert::Infallible; - use super::*; #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] @@ -243,12 +250,9 @@ mod tests { let mut messages = Messages::::new(); - let _ = messages - .send_global::(TestMsg::Foo, |b| Ok(b.extend_from_slice(&[1, 2, 3]))); - let _ = messages - .send_global::(TestMsg::Bar, |b| Ok(b.extend_from_slice(&[4, 5, 6]))); - let _ = messages - .send_global::(TestMsg::Foo, |b| Ok(b.extend_from_slice(&[7, 8, 9]))); + messages.send_global_infallible(TestMsg::Foo, |b| b.extend_from_slice(&[1, 2, 3])); + messages.send_global_infallible(TestMsg::Bar, |b| b.extend_from_slice(&[4, 5, 6])); + messages.send_global_infallible(TestMsg::Foo, |b| b.extend_from_slice(&[7, 8, 9])); messages.ready(); diff --git a/src/tests/client.rs b/src/tests/client.rs index 70ae801fa..84d9cef1a 100644 --- a/src/tests/client.rs +++ b/src/tests/client.rs @@ -1,258 +1,13 @@ -use std::collections::BTreeSet; - -use bevy_ecs::world::EntityMut; use glam::DVec3; use valence_client::movement::FullC2s; use valence_client::teleport::{PlayerPositionLookS2c, TeleportConfirmC2s}; -use valence_client::ViewDistance; -use valence_core::chunk_pos::{ChunkPos, ChunkView}; -use valence_core::protocol::Packet; +use valence_core::chunk_pos::ChunkPos; use valence_entity::packet::MoveRelativeS2c; -use valence_entity::Position; use valence_layer::chunk::UnloadedChunk; -use valence_layer::packet::{ChunkDataS2c, UnloadChunkS2c}; use valence_layer::ChunkLayer; use crate::testing::{create_mock_client, ScenarioSingleClient}; -#[test] -fn client_chunk_view_change() { - fn view(client: &EntityMut) -> ChunkView { - let chunk_pos = client.get::().unwrap().chunk_pos(); - let view_dist = client.get::().unwrap().get(); - - ChunkView::new(chunk_pos, view_dist) - } - - let ScenarioSingleClient { - mut app, - client: client_ent, - mut helper, - layer: layer_ent, - } = ScenarioSingleClient::new(); - - let mut layer = app.world.get_mut::(layer_ent).unwrap(); - - for z in -30..30 { - for x in -30..30 { - layer.insert_chunk([x, z], UnloadedChunk::new()); - } - } - - let mut client = app.world.entity_mut(client_ent); - - client.get_mut::().unwrap().set([8.0, 0.0, 8.0]); - client.get_mut::().unwrap().set(6); - - // Tick - app.update(); - let mut client = app.world.entity_mut(client_ent); - - let mut loaded_chunks = BTreeSet::new(); - - // Collect all chunks received on join. - for f in helper.collect_received().0 { - if f.id == ChunkDataS2c::ID { - let ChunkDataS2c { pos, .. } = f.decode::().unwrap(); - // Newly received chunk was not previously loaded. - assert!(loaded_chunks.insert(pos), "({pos:?})"); - } - } - - // Check that all the received chunks are in the client's view. - for pos in view(&client).iter() { - assert!(loaded_chunks.contains(&pos), "{pos:?}"); - } - - assert!(!loaded_chunks.is_empty()); - - // Move the client to the adjacent chunk. - client.get_mut::().unwrap().set([24.0, 0.0, 24.0]); - - // Tick - app.update(); - let client = app.world.entity_mut(client_ent); - - // For all chunks received this tick... - for f in helper.collect_received().0 { - match f.id { - ChunkDataS2c::ID => { - let ChunkDataS2c { pos, .. } = f.decode().unwrap(); - // Newly received chunk was not previously loaded. - assert!(loaded_chunks.insert(pos), "({pos:?})"); - } - UnloadChunkS2c::ID => { - let UnloadChunkS2c { pos } = f.decode().unwrap(); - // Newly removed chunk was previously loaded. - assert!(loaded_chunks.remove(&pos), "({pos:?})"); - } - _ => {} - } - } - - // Check that all chunks loaded now are within the client's view. - for pos in view(&client).iter() { - assert!(loaded_chunks.contains(&pos), "{pos:?}"); - } -} - -/* -#[test] -fn entity_chunk_spawn_despawn() { - let ScenarioSingleClient { - mut app, - client: client_ent, - mut helper, - layer: layer_ent, - } = ScenarioSingleClient::new(); - - let mut chunks = app.world.get_mut::(layer_ent).unwrap(); - - // Insert an empty chunk at (0, 0). - chunks.insert_chunk([0, 0], UnloadedChunk::new()); - - // Put an entity in the new chunk. - let cow_ent = app - .world - .spawn(CowEntityBundle { - position: Position::new([8.0, 0.0, 8.0]), - layer: EntityLayerId(layer_ent), - ..Default::default() - }) - .id(); - - app.update(); - - // Client is in view of the chunk, so they should receive exactly one chunk - // spawn packet and entity spawn packet. - { - let recvd = helper.collect_received(); - - recvd.assert_count::(1); - recvd.assert_count::(1); - recvd.assert_count::(0); - recvd.assert_count::(0); - } - - // Move the entity. Client should receive entity move packet. - app.world.get_mut::(cow_ent).unwrap().0.x += 0.1; - - app.update(); - - helper - .collect_received() - .assert_count::(1); - - // Despawning the chunk should delete the chunk and the entity contained within. - let mut inst = app.world.get_mut::(layer_ent).unwrap(); - - inst.remove_chunk([0, 0]).unwrap(); - - app.update(); - - { - let recvd = helper.collect_received(); - - recvd.assert_count::(1); - recvd.assert_count::(1); - recvd.assert_count::(0); - recvd.assert_count::(0); - } - - // Placing the chunk back should respawn the orphaned entity. - - let mut inst = app.world.get_mut::(layer_ent).unwrap(); - - assert!(inst.insert_chunk([0, 0], UnloadedChunk::new()).is_none()); - - app.update(); - - { - let recvd = helper.collect_received(); - - recvd.assert_count::(1); - recvd.assert_count::(1); - recvd.assert_count::(0); - recvd.assert_count::(0); - } - - // Move player and entity away from the chunk on the same tick. - - app.world.get_mut::(client_ent).unwrap().0.x = 1000.0; - app.world.get_mut::(cow_ent).unwrap().0.x = 1000.0; - - app.update(); - - { - let recvd = helper.collect_received(); - - recvd.assert_count::(1); - recvd.assert_count::(1); - recvd.assert_count::(0); - recvd.assert_count::(0); - } - - // Put the client and entity back on the same tick. - - app.world - .get_mut::(client_ent) - .unwrap() - .set([8.0, 0.0, 8.0]); - app.world - .get_mut::(cow_ent) - .unwrap() - .set([8.0, 0.0, 8.0]); - - app.update(); - - { - let recvd = helper.collect_received(); - - recvd.assert_count::(1); - recvd.assert_count::(1); - recvd.assert_count::(0); - recvd.assert_count::(0); - } - - // Adding and removing a chunk on the same tick should should have no effect on - // the client. Moving the entity to the removed chunk should despawn the entity - // once. - - app.world - .get_mut::(layer_ent) - .unwrap() - .chunk_entry([0, 1]) - .or_default() - .remove(); - - app.world - .get_mut::(cow_ent) - .unwrap() - .set([24.0, 0.0, 24.0]); - - app.update(); - - { - let recvd = helper.collect_received(); - - recvd.assert_count::(0); - recvd.assert_count::(0); - recvd.assert_count::(0); - recvd.assert_count::(1); - - for pkt in recvd.0 { - if pkt.id == EntitiesDestroyS2c::ID { - let destroy = pkt.decode::().unwrap(); - - assert!( - destroy.entity_ids.len() == 1, - "entity should be listed as despawned only once" - ); - } - } - } -}*/ - #[test] fn client_teleport_and_move() { let ScenarioSingleClient { diff --git a/src/tests/layer.rs b/src/tests/layer.rs index 2ad71384e..8443af2fe 100644 --- a/src/tests/layer.rs +++ b/src/tests/layer.rs @@ -1,6 +1,17 @@ +use std::collections::BTreeSet; + +use bevy_ecs::world::EntityMut; use valence_block::BlockState; +use valence_client::ViewDistance; +use valence_core::chunk_pos::ChunkView; +use valence_core::protocol::Packet; +use valence_entity::cow::CowEntityBundle; +use valence_entity::packet::{EntitiesDestroyS2c, EntitySpawnS2c, MoveRelativeS2c}; +use valence_entity::{EntityLayerId, Position}; use valence_layer::chunk::UnloadedChunk; -use valence_layer::packet::{BlockEntityUpdateS2c, ChunkDeltaUpdateS2c}; +use valence_layer::packet::{ + BlockEntityUpdateS2c, ChunkDataS2c, ChunkDeltaUpdateS2c, UnloadChunkS2c, +}; use valence_layer::ChunkLayer; use crate::testing::ScenarioSingleClient; @@ -53,3 +64,225 @@ fn block_create_destroy() { recvd.assert_count::(0); } } + +#[test] +fn layer_chunk_view_change() { + fn view(client: &EntityMut) -> ChunkView { + let chunk_pos = client.get::().unwrap().chunk_pos(); + let view_dist = client.get::().unwrap().get(); + + ChunkView::new(chunk_pos, view_dist) + } + + let ScenarioSingleClient { + mut app, + client: client_ent, + mut helper, + layer: layer_ent, + } = ScenarioSingleClient::new(); + + let mut layer = app.world.get_mut::(layer_ent).unwrap(); + + for z in -30..30 { + for x in -30..30 { + layer.insert_chunk([x, z], UnloadedChunk::new()); + } + } + + let mut client = app.world.entity_mut(client_ent); + + client.get_mut::().unwrap().set([8.0, 0.0, 8.0]); + client.get_mut::().unwrap().set(6); + + // Tick + app.update(); + let mut client = app.world.entity_mut(client_ent); + + let mut loaded_chunks = BTreeSet::new(); + + // Collect all chunks received on join. + for f in helper.collect_received().0 { + if f.id == ChunkDataS2c::ID { + let ChunkDataS2c { pos, .. } = f.decode::().unwrap(); + // Newly received chunk was not previously loaded. + assert!(loaded_chunks.insert(pos), "({pos:?})"); + } + } + + // Check that all the received chunks are in the client's view. + for pos in view(&client).iter() { + assert!(loaded_chunks.contains(&pos), "{pos:?}"); + } + + assert!(!loaded_chunks.is_empty()); + + // Move the client to the adjacent chunk. + client.get_mut::().unwrap().set([24.0, 0.0, 24.0]); + + // Tick + app.update(); + let client = app.world.entity_mut(client_ent); + + // For all chunks received this tick... + for f in helper.collect_received().0 { + match f.id { + ChunkDataS2c::ID => { + let ChunkDataS2c { pos, .. } = f.decode().unwrap(); + // Newly received chunk was not previously loaded. + assert!(loaded_chunks.insert(pos), "({pos:?})"); + } + UnloadChunkS2c::ID => { + let UnloadChunkS2c { pos } = f.decode().unwrap(); + // Newly removed chunk was previously loaded. + assert!(loaded_chunks.remove(&pos), "({pos:?})"); + } + _ => {} + } + } + + // Check that all chunks loaded now are within the client's view. + for pos in view(&client).iter() { + assert!(loaded_chunks.contains(&pos), "{pos:?}"); + } +} + +#[test] +fn chunk_entity_spawn_despawn() { + let ScenarioSingleClient { + mut app, + client: client_ent, + mut helper, + layer: layer_ent, + } = ScenarioSingleClient::new(); + + let mut chunks = app.world.get_mut::(layer_ent).unwrap(); + + // Insert an empty chunk at (0, 0). + chunks.insert_chunk([0, 0], UnloadedChunk::new()); + + // Put an entity in the new chunk. + let cow_ent = app + .world + .spawn(CowEntityBundle { + position: Position::new([8.0, 0.0, 8.0]), + layer: EntityLayerId(layer_ent), + ..Default::default() + }) + .id(); + + app.update(); + + // Client is in view of the chunk, so they should receive exactly one chunk + // spawn packet and entity spawn packet. + { + let recvd = helper.collect_received(); + + recvd.assert_count::(1); + recvd.assert_count::(1); + recvd.assert_count::(0); + recvd.assert_count::(0); + } + + // Move the entity. Client should receive entity move packet. + app.world.get_mut::(cow_ent).unwrap().0.x += 0.1; + + app.update(); + + helper.collect_received().assert_count::(1); + + // Despawning the chunk should delete the chunk and not the entity contained + // within. + let mut layer = app.world.get_mut::(layer_ent).unwrap(); + + layer.remove_chunk([0, 0]).unwrap(); + + app.update(); + + { + let recvd = helper.collect_received(); + + recvd.assert_count::(1); + recvd.assert_count::(0); + recvd.assert_count::(0); + recvd.assert_count::(0); + } + + // Placing the chunk back should respawn the chunk and not the entity. + + let mut layer = app.world.get_mut::(layer_ent).unwrap(); + + assert!(layer.insert_chunk([0, 0], UnloadedChunk::new()).is_none()); + + app.update(); + + { + let recvd = helper.collect_received(); + + recvd.assert_count::(1); + recvd.assert_count::(0); + recvd.assert_count::(0); + recvd.assert_count::(0); + } + + // Move player and entity away from the chunk on the same tick. + + app.world.get_mut::(client_ent).unwrap().0.x = 1000.0; + app.world.get_mut::(cow_ent).unwrap().0.x = -1000.0; + + app.update(); + + { + let recvd = helper.collect_received(); + + recvd.assert_count::(1); + recvd.assert_count::(1); + recvd.assert_count::(0); + recvd.assert_count::(0); + } + + // Put the client and entity back on the same tick. + + app.world + .get_mut::(client_ent) + .unwrap() + .set([8.0, 0.0, 8.0]); + app.world + .get_mut::(cow_ent) + .unwrap() + .set([8.0, 0.0, 8.0]); + + app.update(); + + { + let recvd = helper.collect_received(); + + recvd.assert_count::(1); + recvd.assert_count::(1); + recvd.assert_count::(0); + recvd.assert_count::(0); + } + + // Adding and removing a chunk on the same tick should have no effect on + // the client. + + let mut layer = app.world.get_mut::(layer_ent).unwrap(); + + layer.insert_chunk([0, 1], UnloadedChunk::new()); + layer.remove_chunk([0, 1]).unwrap(); + + app.world + .get_mut::(cow_ent) + .unwrap() + .set([24.0, 0.0, 24.0]); + + app.update(); + + { + let recvd = helper.collect_received(); + + recvd.assert_count::(0); + recvd.assert_count::(0); + recvd.assert_count::(0); + recvd.assert_count::(0); + } +} From 6facf74ac45f9befe7b105acbee9bf6e53ba7be0 Mon Sep 17 00:00:00 2001 From: Ryan Date: Sat, 22 Jul 2023 18:55:24 -0700 Subject: [PATCH 19/47] tweak idle_update --- benches/idle.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/benches/idle.rs b/benches/idle.rs index 5e320ac9d..d773cc2c7 100644 --- a/benches/idle.rs +++ b/benches/idle.rs @@ -4,15 +4,15 @@ use valence::prelude::*; /// Benches the performance of a single server tick while nothing much is /// happening. pub fn idle_update(c: &mut Criterion) { - c.bench_function("idle_update", |b| { - let mut app = App::new(); + let mut app = App::new(); - app.add_plugins(DefaultPlugins); - app.add_systems(Startup, setup); + app.add_plugins(DefaultPlugins); + app.add_systems(Startup, setup); - // Run startup schedule. - app.update(); + // Run startup schedule. + app.update(); + c.bench_function("idle_update", |b| { b.iter(|| { app.update(); }); From 379fc4b42fb71db4b0aad798f060595094c62d33 Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 25 Jul 2023 01:58:59 -0700 Subject: [PATCH 20/47] Fix mysterious excessive memory usage --- benches/many_players.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benches/many_players.rs b/benches/many_players.rs index 0b980514b..9c900dfed 100644 --- a/benches/many_players.rs +++ b/benches/many_players.rs @@ -33,7 +33,7 @@ fn run_many_players( let mut app = App::new(); app.insert_resource(CoreSettings { - compression_threshold: None, + compression_threshold: Some(256), ..Default::default() }); From a5e8dc8d7899de19e0154c1d4bcf3bbfe004204e Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 25 Jul 2023 01:59:16 -0700 Subject: [PATCH 21/47] tweak many_players bench --- benches/many_players.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benches/many_players.rs b/benches/many_players.rs index 9c900dfed..fa9fd653e 100644 --- a/benches/many_players.rs +++ b/benches/many_players.rs @@ -103,7 +103,7 @@ fn run_many_players( for (id, helper) in &mut clients { let pos = query.get(&app.world, *id).unwrap().get(); - let offset = DVec3::new(rng.gen_range(-2.0..=2.0), 0.0, rng.gen_range(-2.0..=2.0)); + let offset = DVec3::new(rng.gen_range(-1.0..=1.0), 0.0, rng.gen_range(-1.0..=1.0)); helper.send(&FullC2s { position: pos + offset, From d56a975b9eae7bc3d99a1ae9663d2634b5d2009c Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 25 Jul 2023 01:59:41 -0700 Subject: [PATCH 22/47] tweak comment in `valence_layer` --- crates/valence_layer/src/message.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/valence_layer/src/message.rs b/crates/valence_layer/src/message.rs index de25de2f5..f70c8293d 100644 --- a/crates/valence_layer/src/message.rs +++ b/crates/valence_layer/src/message.rs @@ -94,7 +94,7 @@ where staging: &[u8], ready: &mut Vec, ) { - // Sort is deliberately stable. + // Sort must be stable. msgs.sort_by_key(|(msg, _)| *msg); // Make sure the first element is already copied to "ready". From 85eeeb1d347303090f631605826d214645cd05e2 Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 25 Jul 2023 02:00:10 -0700 Subject: [PATCH 23/47] simplify schedule --- crates/valence_client/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/valence_client/src/lib.rs b/crates/valence_client/src/lib.rs index 6fd94da0a..e515f9b13 100644 --- a/crates/valence_client/src/lib.rs +++ b/crates/valence_client/src/lib.rs @@ -145,7 +145,6 @@ impl Plugin for ClientPlugin { .before(FlushPacketsSet), ClearEntityChangesSet.after(UpdateClientsSet), FlushPacketsSet, - UpdateLayersPostClientSet.after(FlushPacketsSet), ), ); From b305165782e98a103bf76c35200fb6a4a2632a82 Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 25 Jul 2023 03:10:06 -0700 Subject: [PATCH 24/47] Adjust view dist in many_players --- benches/many_players.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benches/many_players.rs b/benches/many_players.rs index fa9fd653e..45c363953 100644 --- a/benches/many_players.rs +++ b/benches/many_players.rs @@ -19,7 +19,7 @@ use valence_layer::LayerBundle; use valence_network::NetworkPlugin; pub fn many_players(c: &mut Criterion) { - run_many_players(c, "many_players", 3000, 20, 16); + run_many_players(c, "many_players", 3000, 16, 16); run_many_players(c, "many_players_spread_out", 3000, 8, 200); } From 43a181d41e17d0c4715ea2bb56d75dddd4cff488 Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 25 Jul 2023 19:36:30 -0700 Subject: [PATCH 25/47] More packet writers for layers --- crates/valence_client/src/lib.rs | 54 ++++++- crates/valence_core/src/block_pos.rs | 20 ++- crates/valence_core/src/chunk_pos.rs | 12 +- crates/valence_entity/src/lib.rs | 21 ++- crates/valence_layer/src/chunk.rs | 223 ++++++++++++++++++++++++++- crates/valence_layer/src/entity.rs | 201 +++++++++++++++++++++++- crates/valence_layer/src/lib.rs | 77 +++++---- examples/parkour.rs | 2 +- src/tests/layer.rs | 2 +- 9 files changed, 540 insertions(+), 72 deletions(-) diff --git a/crates/valence_client/src/lib.rs b/crates/valence_client/src/lib.rs index e515f9b13..8de483cfd 100644 --- a/crates/valence_client/src/lib.rs +++ b/crates/valence_client/src/lib.rs @@ -579,7 +579,7 @@ pub struct View { impl ViewItem<'_> { pub fn get(&self) -> ChunkView { - ChunkView::new(self.pos.chunk_pos(), self.view_dist.0) + ChunkView::new(self.pos.to_chunk_pos(), self.view_dist.0) } } @@ -692,8 +692,16 @@ fn handle_layer_messages( mut visible_entity_layers, old_visible_entity_layers, )| { + let block_pos = BlockPos::from_pos(old_view.old_pos.get()); let old_view = old_view.get(); + fn in_radius(p0: BlockPos, p1: BlockPos, radius_squared: u32) -> bool { + let dist_squared = + (p1.x - p0.x).pow(2) + (p1.y - p0.y).pow(2) + (p1.z - p0.z).pow(2); + + dist_squared as u32 <= radius_squared + } + // Chunk layer messages if let Ok(chunk_layer) = chunk_layers.get(old_visible_chunk_layer.get()) { let messages = chunk_layer.messages(); @@ -720,6 +728,28 @@ fn handle_layer_messages( valence_layer::chunk::LocalMsg::PacketAt { .. } => { client.write_packet_bytes(&bytes[range]); } + valence_layer::chunk::LocalMsg::PacketAtExcept { except, .. } => { + if self_entity != except { + client.write_packet_bytes(&bytes[range]); + } + } + valence_layer::chunk::LocalMsg::RadiusAt { + center, + radius_squared, + } => { + if in_radius(block_pos, center, radius_squared) { + client.write_packet_bytes(&bytes[range]); + } + } + valence_layer::chunk::LocalMsg::RadiusAtExcept { + center, + radius_squared, + except, + } => { + if self_entity != except && in_radius(block_pos, center, radius_squared) { + client.write_packet_bytes(&bytes[range]); + } + } valence_layer::chunk::LocalMsg::ChangeBiome { pos } => { chunk_biome_buf.push(ChunkBiome { pos, @@ -791,6 +821,24 @@ fn handle_layer_messages( client.write_packet_bytes(&bytes[range]); } } + valence_layer::entity::LocalMsg::RadiusAt { + center, + radius_squared, + } => { + if in_radius(block_pos, center, radius_squared) { + client.write_packet_bytes(&bytes[range]); + } + } + valence_layer::entity::LocalMsg::RadiusAtExcept { + center, + radius_squared, + except, + } => { + if self_entity != except && in_radius(block_pos, center, radius_squared) + { + client.write_packet_bytes(&bytes[range]); + } + } valence_layer::entity::LocalMsg::SpawnEntity { pos: _, src_layer } => { if !old_visible_entity_layers.0.contains(&src_layer) { let mut bytes = &bytes[range]; @@ -903,8 +951,8 @@ fn update_view_and_layers( view_dist, old_view_dist, )| { - let view = ChunkView::new(ChunkPos::from_dvec3(pos.0), view_dist.0); - let old_view = ChunkView::new(ChunkPos::from_dvec3(old_pos.get()), old_view_dist.0); + let view = ChunkView::new(ChunkPos::from_pos(pos.0), view_dist.0); + let old_view = ChunkView::new(ChunkPos::from_pos(old_pos.get()), old_view_dist.0); // Make sure the center chunk is set before loading chunks! Otherwise the client // may ignore the chunk. diff --git a/crates/valence_core/src/block_pos.rs b/crates/valence_core/src/block_pos.rs index 32c5694ea..fd44d6823 100644 --- a/crates/valence_core/src/block_pos.rs +++ b/crates/valence_core/src/block_pos.rs @@ -1,12 +1,14 @@ use std::io::Write; use anyhow::bail; +use glam::DVec3; +use crate::chunk_pos::ChunkPos; use crate::direction::Direction; use crate::protocol::{Decode, Encode}; /// Represents an absolute block position in world space. -#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, Debug)] +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct BlockPos { pub x: i32, pub y: i32, @@ -19,9 +21,17 @@ impl BlockPos { Self { x, y, z } } - /// Returns the block position a point is contained within. - pub fn at(pos: impl Into<[f64; 3]>) -> Self { - pos.into().map(|a| a.floor() as i32).into() + /// Returns the block position a point in world space is contained within. + pub fn from_pos(pos: DVec3) -> Self { + Self { + x: pos.x.floor() as i32, + y: pos.y.floor() as i32, + z: pos.z.floor() as i32, + } + } + + pub const fn to_chunk_pos(self) -> ChunkPos { + ChunkPos::from_block_pos(self) } /// Get a new [`BlockPos`] that is adjacent to this position in `dir` @@ -35,7 +45,7 @@ impl BlockPos { /// let adj = pos.get_in_direction(Direction::South); /// assert_eq!(adj, BlockPos::new(0, 0, 1)); /// ``` - pub fn get_in_direction(self, dir: Direction) -> BlockPos { + pub const fn get_in_direction(self, dir: Direction) -> BlockPos { match dir { Direction::Down => BlockPos::new(self.x, self.y - 1, self.z), Direction::Up => BlockPos::new(self.x, self.y + 1, self.z), diff --git a/crates/valence_core/src/chunk_pos.rs b/crates/valence_core/src/chunk_pos.rs index 14a47a8f0..78ed6c605 100644 --- a/crates/valence_core/src/chunk_pos.rs +++ b/crates/valence_core/src/chunk_pos.rs @@ -20,20 +20,14 @@ impl ChunkPos { /// Constructs a chunk position from a position in world space. Only the `x` /// and `z` components are used. - pub fn from_dvec3(pos: DVec3) -> Self { - Self::at(pos.x, pos.z) + pub fn from_pos(pos: DVec3) -> Self { + Self::new((pos.x / 16.0).floor() as i32, (pos.z / 16.0).floor() as i32) } - pub fn from_block_pos(pos: BlockPos) -> Self { + pub const fn from_block_pos(pos: BlockPos) -> Self { Self::new(pos.x.div_euclid(16), pos.z.div_euclid(16)) } - /// Takes an X and Z position in world space and returns the chunk position - /// containing the point. - pub fn at(x: f64, z: f64) -> Self { - Self::new((x / 16.0).floor() as i32, (z / 16.0).floor() as i32) - } - pub const fn distance_squared(self, other: Self) -> u64 { let diff_x = other.x as i64 - self.x as i64; let diff_z = other.z as i64 - self.z as i64; diff --git a/crates/valence_entity/src/lib.rs b/crates/valence_entity/src/lib.rs index 5b31fd6e3..ea0802d59 100644 --- a/crates/valence_entity/src/lib.rs +++ b/crates/valence_entity/src/lib.rs @@ -32,6 +32,7 @@ pub use manager::EntityManager; use paste::paste; use tracing::warn; use tracked_data::TrackedData; +use valence_core::block_pos::BlockPos; use valence_core::chunk_pos::ChunkPos; use valence_core::despawn::Despawned; use valence_core::protocol::var_int::VarInt; @@ -227,8 +228,12 @@ impl Position { Self(pos.into()) } - pub fn chunk_pos(&self) -> ChunkPos { - ChunkPos::from_dvec3(self.0) + pub fn to_chunk_pos(self) -> ChunkPos { + ChunkPos::from_pos(self.0) + } + + pub fn to_block_pos(self) -> BlockPos { + BlockPos::from_pos(self.0) } pub fn get(self) -> DVec3 { @@ -249,7 +254,7 @@ impl PartialEq for Position { /// The value of [`Position`] from the end of the previous tick. /// /// **NOTE**: You should not modify this component after the entity is spawned. -#[derive(Component, Copy, Clone, PartialEq, Default, Debug)] +#[derive(Component, Clone, PartialEq, Default, Debug)] pub struct OldPosition(DVec3); impl OldPosition { @@ -257,12 +262,16 @@ impl OldPosition { Self(pos.into()) } - pub fn get(self) -> DVec3 { + pub fn get(&self) -> DVec3 { self.0 } - pub fn chunk_pos(self) -> ChunkPos { - ChunkPos::from_dvec3(self.0) + pub fn chunk_pos(&self) -> ChunkPos { + ChunkPos::from_pos(self.0) + } + + pub fn to_block_pos(&self) -> BlockPos { + BlockPos::from_pos(self.0) } } diff --git a/crates/valence_layer/src/chunk.rs b/crates/valence_layer/src/chunk.rs index 11a033500..08e1c538d 100644 --- a/crates/valence_layer/src/chunk.rs +++ b/crates/valence_layer/src/chunk.rs @@ -82,22 +82,44 @@ pub enum GlobalMsg { #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub enum LocalMsg { /// Send packet data to all clients viewing the layer in view of `pos`. - PacketAt { pos: ChunkPos }, + PacketAt { + pos: ChunkPos, + }, + PacketAtExcept { + pos: ChunkPos, + except: Entity, + }, + RadiusAt { + center: BlockPos, + radius_squared: u32, + }, + RadiusAtExcept { + center: BlockPos, + radius_squared: u32, + except: Entity, + }, /// Instruct clients to load or unload the chunk at `pos`. Loading and /// unloading are combined into a single message so that load/unload order /// is not lost when messages are sorted. /// /// Message content is a single byte indicating load (1) or unload (0). - ChangeChunkState { pos: ChunkPos }, + ChangeChunkState { + pos: ChunkPos, + }, /// Message content is the data for a single biome in the "change biomes" /// packet. - ChangeBiome { pos: ChunkPos }, + ChangeBiome { + pos: ChunkPos, + }, } impl GetChunkPos for LocalMsg { fn chunk_pos(&self) -> ChunkPos { match *self { LocalMsg::PacketAt { pos } => pos, + LocalMsg::PacketAtExcept { pos, .. } => pos, + LocalMsg::RadiusAt { center, .. } => center.to_chunk_pos(), + LocalMsg::RadiusAtExcept { center, .. } => center.to_chunk_pos(), LocalMsg::ChangeBiome { pos } => pos, LocalMsg::ChangeChunkState { pos } => pos, } @@ -365,7 +387,7 @@ impl ChunkLayer { ) { let position = position.into(); - self.view_writer(ChunkPos::from_dvec3(position)) + self.view_writer(ChunkPos::from_pos(position)) .write_packet(&ParticleS2c { particle: Cow::Borrowed(particle), long_distance, @@ -390,7 +412,7 @@ impl ChunkLayer { ) { let position = position.into(); - self.view_writer(ChunkPos::from_dvec3(position)) + self.view_writer(ChunkPos::from_pos(position)) .write_packet(&PlaySoundS2c { id: sound.to_id(), category, @@ -403,14 +425,67 @@ impl ChunkLayer { } impl Layer for ChunkLayer { + type ExceptWriter<'a> = ExceptWriter<'a>; + type ViewWriter<'a> = ViewWriter<'a>; + type ViewExceptWriter<'a> = ViewExceptWriter<'a>; + + type RadiusWriter<'a> = RadiusWriter<'a>; + + type RadiusExceptWriter<'a> = RadiusExceptWriter<'a>; + + fn except_writer(&mut self, except: Entity) -> Self::ExceptWriter<'_> { + ExceptWriter { + layer: self, + except, + } + } + fn view_writer(&mut self, pos: impl Into) -> Self::ViewWriter<'_> { ViewWriter { layer: self, pos: pos.into(), } } + + fn view_except_writer( + &mut self, + pos: impl Into, + except: Entity, + ) -> Self::ViewExceptWriter<'_> { + ViewExceptWriter { + layer: self, + pos: pos.into(), + except, + } + } + + fn radius_writer( + &mut self, + center: impl Into, + radius: u32, + ) -> Self::RadiusWriter<'_> { + RadiusWriter { + layer: self, + center: center.into(), + radius, + } + } + + fn radius_except_writer( + &mut self, + center: impl Into, + radius: u32, + except: Entity, + ) -> Self::RadiusExceptWriter<'_> { + RadiusExceptWriter { + layer: self, + center: center.into(), + radius, + except, + } + } } impl WritePacket for ChunkLayer { @@ -429,12 +504,43 @@ impl WritePacket for ChunkLayer { } } +pub struct ExceptWriter<'a> { + layer: &'a mut ChunkLayer, + except: Entity, +} + +impl WritePacket for ExceptWriter<'_> { + fn write_packet_fallible

(&mut self, packet: &P) -> anyhow::Result<()> + where + P: Packet + Encode, + { + self.layer.messages.send_global( + GlobalMsg::PacketExcept { + except: self.except, + }, + |b| { + PacketWriter::new(b, self.layer.info.compression_threshold) + .write_packet_fallible(packet) + }, + ) + } + + fn write_packet_bytes(&mut self, bytes: &[u8]) { + self.layer.messages.send_global_infallible( + GlobalMsg::PacketExcept { + except: self.except, + }, + |b| b.extend_from_slice(bytes), + ) + } +} + pub struct ViewWriter<'a> { layer: &'a mut ChunkLayer, pos: ChunkPos, } -impl<'a> WritePacket for ViewWriter<'a> { +impl WritePacket for ViewWriter<'_> { fn write_packet_fallible

(&mut self, packet: &P) -> anyhow::Result<()> where P: Packet + Encode, @@ -456,6 +562,111 @@ impl<'a> WritePacket for ViewWriter<'a> { } } +pub struct ViewExceptWriter<'a> { + layer: &'a mut ChunkLayer, + pos: ChunkPos, + except: Entity, +} + +impl WritePacket for ViewExceptWriter<'_> { + fn write_packet_fallible

(&mut self, packet: &P) -> anyhow::Result<()> + where + P: Packet + Encode, + { + self.layer.messages.send_local( + LocalMsg::PacketAtExcept { + pos: self.pos, + except: self.except, + }, + |b| { + PacketWriter::new(b, self.layer.info.compression_threshold) + .write_packet_fallible(packet) + }, + ) + } + + fn write_packet_bytes(&mut self, bytes: &[u8]) { + self.layer.messages.send_local_infallible( + LocalMsg::PacketAtExcept { + pos: self.pos, + except: self.except, + }, + |b| b.extend_from_slice(bytes), + ); + } +} + +pub struct RadiusWriter<'a> { + layer: &'a mut ChunkLayer, + center: BlockPos, + radius: u32, +} + +impl WritePacket for RadiusWriter<'_> { + fn write_packet_fallible

(&mut self, packet: &P) -> anyhow::Result<()> + where + P: Packet + Encode, + { + self.layer.messages.send_local( + LocalMsg::RadiusAt { + center: self.center, + radius_squared: self.radius, + }, + |b| { + PacketWriter::new(b, self.layer.info.compression_threshold) + .write_packet_fallible(packet) + }, + ) + } + + fn write_packet_bytes(&mut self, bytes: &[u8]) { + self.layer.messages.send_local_infallible( + LocalMsg::RadiusAt { + center: self.center, + radius_squared: self.radius, + }, + |b| b.extend_from_slice(bytes), + ); + } +} + +pub struct RadiusExceptWriter<'a> { + layer: &'a mut ChunkLayer, + center: BlockPos, + radius: u32, + except: Entity, +} + +impl WritePacket for RadiusExceptWriter<'_> { + fn write_packet_fallible

(&mut self, packet: &P) -> anyhow::Result<()> + where + P: Packet + Encode, + { + self.layer.messages.send_local( + LocalMsg::RadiusAtExcept { + center: self.center, + radius_squared: self.radius, + except: self.except, + }, + |b| { + PacketWriter::new(b, self.layer.info.compression_threshold) + .write_packet_fallible(packet) + }, + ) + } + + fn write_packet_bytes(&mut self, bytes: &[u8]) { + self.layer.messages.send_local_infallible( + LocalMsg::RadiusAtExcept { + center: self.center, + radius_squared: self.radius, + except: self.except, + }, + |b| b.extend_from_slice(bytes), + ); + } +} + #[derive(Debug)] pub enum ChunkEntry<'a> { Occupied(OccupiedChunkEntry<'a>), diff --git a/crates/valence_layer/src/entity.rs b/crates/valence_layer/src/entity.rs index 1daf54049..2839c68b1 100644 --- a/crates/valence_layer/src/entity.rs +++ b/crates/valence_layer/src/entity.rs @@ -5,6 +5,7 @@ use bevy_app::prelude::*; use bevy_ecs::prelude::*; use bevy_ecs::query::Has; use rustc_hash::FxHashMap; +use valence_core::block_pos::BlockPos; use valence_core::chunk_pos::ChunkPos; use valence_core::despawn::Despawned; use valence_core::protocol::encode::{PacketWriter, WritePacket}; @@ -56,6 +57,15 @@ pub enum LocalMsg { /// except the client identified by `except`. Message data is serialized /// packet data. PacketAtExcept { pos: ChunkPos, except: Entity }, + RadiusAt { + center: BlockPos, + radius_squared: u32, + }, + RadiusAtExcept { + center: BlockPos, + radius_squared: u32, + except: Entity, + }, /// Despawn entities if the client is not already viewing `dest_layer`. /// Message data is the serialized form of `EntityId`. DespawnEntity { pos: ChunkPos, dest_layer: Entity }, @@ -69,6 +79,8 @@ impl GetChunkPos for LocalMsg { match *self { LocalMsg::PacketAt { pos } => pos, LocalMsg::PacketAtExcept { pos, .. } => pos, + LocalMsg::RadiusAt { center, .. } => center.to_chunk_pos(), + LocalMsg::RadiusAtExcept { center, .. } => center.to_chunk_pos(), LocalMsg::SpawnEntity { pos, .. } => pos, LocalMsg::SpawnEntityTransition { pos, .. } => pos, LocalMsg::DespawnEntity { pos, .. } => pos, @@ -105,14 +117,67 @@ impl EntityLayer { } impl Layer for EntityLayer { + type ExceptWriter<'a> = ExceptWriter<'a>; + type ViewWriter<'a> = ViewWriter<'a>; + type ViewExceptWriter<'a> = ViewExceptWriter<'a>; + + type RadiusWriter<'a> = RadiusWriter<'a>; + + type RadiusExceptWriter<'a> = RadiusExceptWriter<'a>; + + fn except_writer(&mut self, except: Entity) -> Self::ExceptWriter<'_> { + ExceptWriter { + layer: self, + except, + } + } + fn view_writer(&mut self, pos: impl Into) -> Self::ViewWriter<'_> { ViewWriter { layer: self, pos: pos.into(), } } + + fn view_except_writer( + &mut self, + pos: impl Into, + except: Entity, + ) -> Self::ViewExceptWriter<'_> { + ViewExceptWriter { + layer: self, + pos: pos.into(), + except, + } + } + + fn radius_writer( + &mut self, + center: impl Into, + radius: u32, + ) -> Self::RadiusWriter<'_> { + RadiusWriter { + layer: self, + center: center.into(), + radius_squared: radius.saturating_mul(radius), + } + } + + fn radius_except_writer( + &mut self, + center: impl Into, + radius: u32, + except: Entity, + ) -> Self::RadiusExceptWriter<'_> { + RadiusExceptWriter { + layer: self, + center: center.into(), + radius_squared: radius.saturating_mul(radius), + except, + } + } } impl WritePacket for EntityLayer { @@ -131,6 +196,36 @@ impl WritePacket for EntityLayer { } } +pub struct ExceptWriter<'a> { + layer: &'a mut EntityLayer, + except: Entity, +} + +impl WritePacket for ExceptWriter<'_> { + fn write_packet_fallible

(&mut self, packet: &P) -> anyhow::Result<()> + where + P: Packet + Encode, + { + self.layer.messages.send_global( + GlobalMsg::PacketExcept { + except: self.except, + }, + |b| { + PacketWriter::new(b, self.layer.compression_threshold).write_packet_fallible(packet) + }, + ) + } + + fn write_packet_bytes(&mut self, bytes: &[u8]) { + self.layer.messages.send_global_infallible( + GlobalMsg::PacketExcept { + except: self.except, + }, + |b| b.extend_from_slice(bytes), + ) + } +} + pub struct ViewWriter<'a> { layer: &'a mut EntityLayer, pos: ChunkPos, @@ -157,6 +252,108 @@ impl<'a> WritePacket for ViewWriter<'a> { } } +pub struct ViewExceptWriter<'a> { + layer: &'a mut EntityLayer, + pos: ChunkPos, + except: Entity, +} + +impl WritePacket for ViewExceptWriter<'_> { + fn write_packet_fallible

(&mut self, packet: &P) -> anyhow::Result<()> + where + P: Packet + Encode, + { + self.layer.messages.send_local( + LocalMsg::PacketAtExcept { + pos: self.pos, + except: self.except, + }, + |b| { + PacketWriter::new(b, self.layer.compression_threshold).write_packet_fallible(packet) + }, + ) + } + + fn write_packet_bytes(&mut self, bytes: &[u8]) { + self.layer.messages.send_local_infallible( + LocalMsg::PacketAtExcept { + pos: self.pos, + except: self.except, + }, + |b| b.extend_from_slice(bytes), + ); + } +} + +pub struct RadiusWriter<'a> { + layer: &'a mut EntityLayer, + center: BlockPos, + radius_squared: u32, +} + +impl WritePacket for RadiusWriter<'_> { + fn write_packet_fallible

(&mut self, packet: &P) -> anyhow::Result<()> + where + P: Packet + Encode, + { + self.layer.messages.send_local( + LocalMsg::RadiusAt { + center: self.center, + radius_squared: self.radius_squared, + }, + |b| { + PacketWriter::new(b, self.layer.compression_threshold).write_packet_fallible(packet) + }, + ) + } + + fn write_packet_bytes(&mut self, bytes: &[u8]) { + self.layer.messages.send_local_infallible( + LocalMsg::RadiusAt { + center: self.center, + radius_squared: self.radius_squared, + }, + |b| b.extend_from_slice(bytes), + ); + } +} + +pub struct RadiusExceptWriter<'a> { + layer: &'a mut EntityLayer, + center: BlockPos, + radius_squared: u32, + except: Entity, +} + +impl WritePacket for RadiusExceptWriter<'_> { + fn write_packet_fallible

(&mut self, packet: &P) -> anyhow::Result<()> + where + P: Packet + Encode, + { + self.layer.messages.send_local( + LocalMsg::RadiusAtExcept { + center: self.center, + radius_squared: self.radius_squared, + except: self.except, + }, + |b| { + PacketWriter::new(b, self.layer.compression_threshold).write_packet_fallible(packet) + }, + ) + } + + fn write_packet_bytes(&mut self, bytes: &[u8]) { + self.layer.messages.send_local_infallible( + LocalMsg::RadiusAtExcept { + center: self.center, + radius_squared: self.radius_squared, + except: self.except, + }, + |b| b.extend_from_slice(bytes), + ); + } +} + pub(super) fn build(app: &mut App) { app.add_systems( PostUpdate, @@ -190,7 +387,7 @@ fn change_entity_positions( mut layers: Query<&mut EntityLayer>, ) { for (entity, entity_id, pos, old_pos, layer_id, old_layer_id, despawned) in &entities { - let chunk_pos = pos.chunk_pos(); + let chunk_pos = pos.to_chunk_pos(); let old_chunk_pos = old_pos.chunk_pos(); if despawned { @@ -291,7 +488,7 @@ fn send_entity_update_messages( for cell in layer.entities.values_mut() { for &entity in cell.iter() { if let Ok((entity, update, is_client)) = entities.get(entity) { - let chunk_pos = update.pos.chunk_pos(); + let chunk_pos = update.pos.to_chunk_pos(); // Send the update packets to all viewers. If the entity being updated is a // client, then we need to be careful to exclude the client itself from diff --git a/crates/valence_layer/src/lib.rs b/crates/valence_layer/src/lib.rs index 2621a62c8..c350c4c81 100644 --- a/crates/valence_layer/src/lib.rs +++ b/crates/valence_layer/src/lib.rs @@ -31,6 +31,7 @@ use bevy_ecs::prelude::*; pub use chunk::ChunkLayer; pub use entity::EntityLayer; use valence_biome::BiomeRegistry; +use valence_core::block_pos::BlockPos; use valence_core::chunk_pos::ChunkPos; use valence_core::ident::Ident; use valence_core::protocol::encode::WritePacket; @@ -81,54 +82,52 @@ impl Plugin for LayerPlugin { } } -// pub trait Layer { -// type Global; -// type Local; - -// fn send_global(&mut self, msg: Self::Global, f: impl FnOnce(&mut -// Vec)); - -// fn send_local(&mut self, msg: Self::Local, f: impl FnOnce(&mut Vec)); - -// fn compression_threshold(&self) -> Option; - -// fn send_global_bytes(&mut self, msg: Self::Global, bytes: &[u8]) { -// self.send_global(msg, |b| b.extend_from_slice(bytes)); -// } - -// fn send_local_bytes(&mut self, msg: Self::Local, bytes: &[u8]) { -// self.send_local(msg, |b| b.extend_from_slice(bytes)); -// } - -// fn send_global_packet

(&mut self, msg: Self::Global, pkt: &P) -// where -// P: Encode + Packet, -// { -// let threshold = self.compression_threshold(); +pub trait Layer: WritePacket { + type ExceptWriter<'a>: WritePacket + where + Self: 'a; -// self.send_global(msg, |b| PacketWriter::new(b, -// threshold).write_packet(pkt)); } + type ViewWriter<'a>: WritePacket + where + Self: 'a; -// fn send_local_packet

(&mut self, msg: Self::Local, pkt: &P) -// where -// P: Encode + Packet, -// { -// let threshold = self.compression_threshold(); + type ViewExceptWriter<'a>: WritePacket + where + Self: 'a; -// self.send_local(msg, |b| PacketWriter::new(b, -// threshold).write_packet(pkt)); } -// } + type RadiusWriter<'a>: WritePacket + where + Self: 'a; -pub trait Layer: WritePacket { - type ViewWriter<'a>: WritePacket + type RadiusExceptWriter<'a>: WritePacket where Self: 'a; - /// Returns a [`WritePacket`] implementor for a chunk position. - /// - /// When writing packets to the chunk writer, only clients in view of `pos` + /// Returns a packet writer which sends packet data to all clients viewing + /// the layer, except the client identified by `except`. + fn except_writer(&mut self, except: Entity) -> Self::ExceptWriter<'_>; + + /// When writing packets to the view writer, only clients in view of `pos` /// will receive the packet. fn view_writer(&mut self, pos: impl Into) -> Self::ViewWriter<'_>; + + /// Like [`view_writer`](Self::view_writer), but packets written to the + /// returned [`ViewExceptWriter`](Self::ViewExceptWriter) are not sent to + /// the client identified by `except`. + fn view_except_writer( + &mut self, + pos: impl Into, + except: Entity, + ) -> Self::ViewExceptWriter<'_>; + + fn radius_writer(&mut self, pos: impl Into, radius: u32) -> Self::RadiusWriter<'_>; + + fn radius_except_writer( + &mut self, + pos: impl Into, + radius: u32, + except: Entity, + ) -> Self::RadiusExceptWriter<'_>; } /// Convenience [`Bundle`] for spawning a layer entity with both [`ChunkLayer`] diff --git a/examples/parkour.rs b/examples/parkour.rs index d5a5b94c0..b3e8ebfca 100644 --- a/examples/parkour.rs +++ b/examples/parkour.rs @@ -193,7 +193,7 @@ fn manage_blocks(mut clients: Query<(&mut Client, &Position, &mut GameState, &mu fn manage_chunks(mut clients: Query<(&Position, &OldPosition, &mut ChunkLayer), With>) { for (pos, old_pos, mut layer) in &mut clients { let old_view = ChunkView::new(old_pos.chunk_pos(), VIEW_DIST); - let view = ChunkView::new(pos.chunk_pos(), VIEW_DIST); + let view = ChunkView::new(pos.to_chunk_pos(), VIEW_DIST); if old_view != view { for pos in old_view.diff(view) { diff --git a/src/tests/layer.rs b/src/tests/layer.rs index 8443af2fe..86baee14a 100644 --- a/src/tests/layer.rs +++ b/src/tests/layer.rs @@ -68,7 +68,7 @@ fn block_create_destroy() { #[test] fn layer_chunk_view_change() { fn view(client: &EntityMut) -> ChunkView { - let chunk_pos = client.get::().unwrap().chunk_pos(); + let chunk_pos = client.get::().unwrap().to_chunk_pos(); let view_dist = client.get::().unwrap().get(); ChunkView::new(chunk_pos, view_dist) From a043c0f94b225d95c3dcead8f481fb533b981268 Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 27 Jul 2023 01:15:58 -0700 Subject: [PATCH 26/47] More doc strings --- crates/valence_client/src/lib.rs | 24 ++++++++++-------- crates/valence_client/src/weather.rs | 2 +- crates/valence_layer/src/bvh.rs | 2 ++ crates/valence_layer/src/chunk.rs | 7 ++++-- crates/valence_layer/src/chunk/chunk.rs | 5 ++-- crates/valence_layer/src/entity.rs | 6 +++-- crates/valence_layer/src/lib.rs | 33 +++++++++++++++++++------ crates/valence_world_border/src/lib.rs | 4 +-- 8 files changed, 56 insertions(+), 27 deletions(-) diff --git a/crates/valence_client/src/lib.rs b/crates/valence_client/src/lib.rs index 8de483cfd..7c4379a18 100644 --- a/crates/valence_client/src/lib.rs +++ b/crates/valence_client/src/lib.rs @@ -368,11 +368,6 @@ impl Client { } /// Puts a particle effect at the given position, only for this client. - /// - /// If you want to show a particle effect to all players, use - /// [`Instance::play_particle`] - /// - /// [`Instance::play_particle`]: Instance::play_particle pub fn play_particle( &mut self, particle: &Particle, @@ -393,11 +388,6 @@ impl Client { } /// Plays a sound effect at the given position, only for this client. - /// - /// If you want to play a sound effect to all players, use - /// [`Instance::play_sound`] - /// - /// [`Instance::play_sound`]: Instance::play_sound pub fn play_sound( &mut self, sound: Sound, @@ -605,6 +595,11 @@ impl Default for Ping { } } +/// A [`Component`] containing a handle to the [`ChunkLayer`] a client can +/// see. +/// +/// A client can only see one chunk layer at a time. Mutating this component +/// will cause the client to respawn in the new chunk layer. #[derive(Component, Copy, Clone, PartialEq, Eq, Debug)] pub struct VisibleChunkLayer(pub Entity); @@ -614,6 +609,7 @@ impl Default for VisibleChunkLayer { } } +/// The value of [`VisibleChunkLayer`] from the end of the previous tick. #[derive(Component, PartialEq, Eq, Debug)] pub struct OldVisibleChunkLayer(Entity); @@ -623,9 +619,17 @@ impl OldVisibleChunkLayer { } } +/// A [`Component`] containing the set of [`EntityLayer`]s a client can see. +/// All Minecraft entities from all layers in this set are potentially visible +/// to the client. +/// +/// This set can be mutated at any time to change which entity layers are +/// visible to the client. [`Despawned`] entity layers are automatically +/// removed. #[derive(Component, Default, Debug)] pub struct VisibleEntityLayers(pub BTreeSet); +/// The value of [`VisibleEntityLayers`] from the end of the previous tick. #[derive(Component, Default, Debug)] pub struct OldVisibleEntityLayers(BTreeSet); diff --git a/crates/valence_client/src/weather.rs b/crates/valence_client/src/weather.rs index a7a8e5ff3..8560961db 100644 --- a/crates/valence_client/src/weather.rs +++ b/crates/valence_client/src/weather.rs @@ -1,3 +1,4 @@ +/* //! The weather system. //! //! This module contains the systems and components needed to handle @@ -15,7 +16,6 @@ //! New joined players are handled, so that they are get weather events from //! the instance. -/* use super::*; use crate::packet::{GameEventKind, GameStateChangeS2c}; diff --git a/crates/valence_layer/src/bvh.rs b/crates/valence_layer/src/bvh.rs index e007d9120..f2c89c434 100644 --- a/crates/valence_layer/src/bvh.rs +++ b/crates/valence_layer/src/bvh.rs @@ -3,6 +3,7 @@ use std::ops::Range; use valence_core::chunk_pos::{ChunkPos, ChunkView}; +/// A bounding volume hierarchy for chunk positions. #[derive(Clone, Debug)] pub struct ChunkBvh { nodes: Vec, @@ -81,6 +82,7 @@ impl Aabb { } } +/// Obtains a chunk position for the purpose of placement in the BVH. pub trait GetChunkPos { fn chunk_pos(&self) -> ChunkPos; } diff --git a/crates/valence_layer/src/chunk.rs b/crates/valence_layer/src/chunk.rs index 08e1c538d..5efffbeff 100644 --- a/crates/valence_layer/src/chunk.rs +++ b/crates/valence_layer/src/chunk.rs @@ -1,5 +1,3 @@ -//! Contains the [`Instance`] component and methods. - mod chunk; pub mod loaded; mod paletted_container; @@ -34,6 +32,8 @@ use crate::bvh::GetChunkPos; use crate::message::Messages; use crate::{Layer, UpdateLayersPostClientSet, UpdateLayersPreClientSet}; +/// A [`Component`] containing the [chunks](LoadedChunk) and [dimension +/// information](valence_dimension::DimensionTypeId) of a Minecraft world. #[derive(Component, Debug)] pub struct ChunkLayer { messages: ChunkLayerMessages, @@ -41,6 +41,7 @@ pub struct ChunkLayer { info: ChunkLayerInfo, } +/// Chunk layer information. Not public API. #[doc(hidden)] pub struct ChunkLayerInfo { dimension_type_name: Ident, @@ -134,6 +135,7 @@ impl ChunkLayer { #[doc(hidden)] pub const OVERWRITE: u8 = 2; + /// Creates a new chunk layer. #[track_caller] pub fn new( dimension_type_name: impl Into>, @@ -175,6 +177,7 @@ impl ChunkLayer { } } + /// The name of the dimension this chunk layer is using. pub fn dimension_type_name(&self) -> Ident<&str> { self.info.dimension_type_name.as_str_ident() } diff --git a/crates/valence_layer/src/chunk/chunk.rs b/crates/valence_layer/src/chunk/chunk.rs index 4e39f6f59..3a7759f5d 100644 --- a/crates/valence_layer/src/chunk/chunk.rs +++ b/crates/valence_layer/src/chunk/chunk.rs @@ -4,8 +4,9 @@ use valence_nbt::Compound; use super::paletted_container::PalettedContainer; -/// Common operations on chunks. Notable implementors are [`LoadedChunk`] and -/// [`UnloadedChunk`]. +/// Common operations on chunks. Notable implementors are +/// [`LoadedChunk`](super::loaded::LoadedChunk) and +/// [`UnloadedChunk`](super::unloaded::UnloadedChunk). pub trait Chunk { /// Gets the height of this chunk in meters or blocks. fn height(&self) -> u32; diff --git a/crates/valence_layer/src/entity.rs b/crates/valence_layer/src/entity.rs index 2839c68b1..a08756223 100644 --- a/crates/valence_layer/src/entity.rs +++ b/crates/valence_layer/src/entity.rs @@ -18,6 +18,7 @@ use crate::bvh::GetChunkPos; use crate::message::Messages; use crate::{Layer, UpdateLayersPostClientSet, UpdateLayersPreClientSet}; +/// A [`Component`] containing Minecraft entities. #[derive(Component, Debug)] pub struct EntityLayer { messages: EntityLayerMessages, @@ -90,6 +91,7 @@ impl GetChunkPos for LocalMsg { } impl EntityLayer { + /// Creates a new entity layer. pub fn new(server: &Server) -> Self { Self { messages: Messages::new(), @@ -98,8 +100,8 @@ impl EntityLayer { } } - /// Returns a list of entities with positions within the provided chunk - /// position on this layer. + /// Returns an iterator over all entities contained within the given chunk + /// position in this layer. pub fn entities_at( &self, pos: impl Into, diff --git a/crates/valence_layer/src/lib.rs b/crates/valence_layer/src/lib.rs index c350c4c81..4f00aa9e5 100644 --- a/crates/valence_layer/src/lib.rs +++ b/crates/valence_layer/src/lib.rs @@ -82,46 +82,64 @@ impl Plugin for LayerPlugin { } } +/// Common functionality for layers. Notable implementors are [`ChunkLayer`] and +/// [`EntityLayer`]. +/// +/// Layers support sending packets to viewers of the layer under various +/// conditions. These are the "packet writers" exposed by this trait. +/// +/// Layers themselves implement the [`WritePacket`] trait. Writing directly to a +/// layer will send packets to all viewers unconditionally. pub trait Layer: WritePacket { + /// Packet writer returned by [`except_writer`](Self::except_writer). type ExceptWriter<'a>: WritePacket where Self: 'a; + /// Packet writer returned by [`view_writer`](Self::ViewWriter). type ViewWriter<'a>: WritePacket where Self: 'a; + /// Packet writer returned by + /// [`view_except_writer`](Self::ViewExceptWriter). type ViewExceptWriter<'a>: WritePacket where Self: 'a; + /// Packet writer returned by [`radius_writer`](Self::radius_writer). type RadiusWriter<'a>: WritePacket where Self: 'a; + /// Packet writer returned by + /// [`radius_except_writer`](Self::radius_except_writer). type RadiusExceptWriter<'a>: WritePacket where Self: 'a; - /// Returns a packet writer which sends packet data to all clients viewing - /// the layer, except the client identified by `except`. + /// Returns a packet writer which sends packets to all viewers not + /// identified by `except`. fn except_writer(&mut self, except: Entity) -> Self::ExceptWriter<'_>; - /// When writing packets to the view writer, only clients in view of `pos` - /// will receive the packet. + /// Returns a packet writer which sends packets to viewers in view of + /// the chunk position `pos`. fn view_writer(&mut self, pos: impl Into) -> Self::ViewWriter<'_>; - /// Like [`view_writer`](Self::view_writer), but packets written to the - /// returned [`ViewExceptWriter`](Self::ViewExceptWriter) are not sent to - /// the client identified by `except`. + /// Returns a packet writer which sends packets to viewers in + /// view of the chunk position `pos` and not identified by `except`. fn view_except_writer( &mut self, pos: impl Into, except: Entity, ) -> Self::ViewExceptWriter<'_>; + /// Returns a packet writer which sends packets to viewers within `radius` + /// blocks of the block position `pos`. fn radius_writer(&mut self, pos: impl Into, radius: u32) -> Self::RadiusWriter<'_>; + /// Returns a packet writer which sends packets to viewers within `radius` + /// blocks of the block position `pos` and not identified by `except`. fn radius_except_writer( &mut self, pos: impl Into, @@ -139,6 +157,7 @@ pub struct LayerBundle { } impl LayerBundle { + /// Returns a new layer bundle. pub fn new( dimension_type_name: impl Into>, dimensions: &DimensionTypeRegistry, diff --git a/crates/valence_world_border/src/lib.rs b/crates/valence_world_border/src/lib.rs index 14b4ac991..0576e4bce 100644 --- a/crates/valence_world_border/src/lib.rs +++ b/crates/valence_world_border/src/lib.rs @@ -1,3 +1,4 @@ +/* //! # World border //! This module contains Components and Systems needed to handle world border. //! @@ -75,9 +76,6 @@ pub mod packet; -// TODO: fix. - -/* use std::time::{Duration, Instant}; use bevy_app::prelude::*; From cfc0ec106699915dcb7993467b75c49cfdd243dc Mon Sep 17 00:00:00 2001 From: Ryan Date: Fri, 28 Jul 2023 20:23:31 -0700 Subject: [PATCH 27/47] Fix some clippy issues --- crates/valence_core_macros/src/decode.rs | 2 +- crates/valence_core_macros/src/encode.rs | 2 +- crates/valence_layer/src/chunk.rs | 1 + crates/valence_layer/src/message.rs | 10 ++++++++-- crates/valence_spatial_index/src/bvh.rs | 10 ++-------- src/testing.rs | 7 ++++++- 6 files changed, 19 insertions(+), 13 deletions(-) diff --git a/crates/valence_core_macros/src/decode.rs b/crates/valence_core_macros/src/decode.rs index a930ee2d6..a996eef97 100644 --- a/crates/valence_core_macros/src/decode.rs +++ b/crates/valence_core_macros/src/decode.rs @@ -83,7 +83,7 @@ pub(super) fn derive_decode(item: TokenStream) -> Result { }) } Data::Enum(enum_) => { - let variants = pair_variants_with_discriminants(enum_.variants.into_iter())?; + let variants = pair_variants_with_discriminants(enum_.variants)?; let decode_arms = variants .iter() diff --git a/crates/valence_core_macros/src/encode.rs b/crates/valence_core_macros/src/encode.rs index cd2152bc2..06e1aef6f 100644 --- a/crates/valence_core_macros/src/encode.rs +++ b/crates/valence_core_macros/src/encode.rs @@ -59,7 +59,7 @@ pub(super) fn derive_encode(item: TokenStream) -> Result { }) } Data::Enum(enum_) => { - let variants = pair_variants_with_discriminants(enum_.variants.into_iter())?; + let variants = pair_variants_with_discriminants(enum_.variants)?; let encode_arms = variants .iter() diff --git a/crates/valence_layer/src/chunk.rs b/crates/valence_layer/src/chunk.rs index 5efffbeff..03fde757e 100644 --- a/crates/valence_layer/src/chunk.rs +++ b/crates/valence_layer/src/chunk.rs @@ -1,3 +1,4 @@ +#[allow(clippy::module_inception)] mod chunk; pub mod loaded; mod paletted_container; diff --git a/crates/valence_layer/src/message.rs b/crates/valence_layer/src/message.rs index f70c8293d..e68c9385a 100644 --- a/crates/valence_layer/src/message.rs +++ b/crates/valence_layer/src/message.rs @@ -73,11 +73,17 @@ where } pub(crate) fn send_global_infallible(&mut self, msg: G, f: impl FnOnce(&mut Vec)) { - let _ = self.send_global::(msg, |b| Ok(f(b))); + let _ = self.send_global::(msg, |b| { + f(b); + Ok(()) + }); } pub(crate) fn send_local_infallible(&mut self, msg: L, f: impl FnOnce(&mut Vec)) { - let _ = self.send_local::(msg, |b| Ok(f(b))); + let _ = self.send_local::(msg, |b| { + f(b); + Ok(()) + }); } /// Readies messages to be read by clients. diff --git a/crates/valence_spatial_index/src/bvh.rs b/crates/valence_spatial_index/src/bvh.rs index 46d41881e..669262094 100644 --- a/crates/valence_spatial_index/src/bvh.rs +++ b/crates/valence_spatial_index/src/bvh.rs @@ -142,10 +142,7 @@ impl Bounded3D for Node<'_, T> { impl Clone for Node<'_, T> { fn clone(&self) -> Self { - match self { - Node::Internal(int) => Node::Internal(*int), - Node::Leaf(t) => Node::Leaf(t), - } + *self } } @@ -177,10 +174,7 @@ impl Bounded3D for Internal<'_, T> { impl Clone for Internal<'_, T> { fn clone(&self) -> Self { - Self { - bvh: self.bvh, - idx: self.idx, - } + *self } } diff --git a/src/testing.rs b/src/testing.rs index 1beca58be..7323fac31 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -22,7 +22,6 @@ use valence_network::NetworkPlugin; use crate::client::{ClientBundle, ClientConnection, ReceivedPacket}; use crate::DefaultPlugins; - pub struct ScenarioSingleClient { /// The new bevy application. pub app: App, @@ -79,6 +78,12 @@ impl ScenarioSingleClient { } } +impl Default for ScenarioSingleClient { + fn default() -> Self { + Self::new() + } +} + /// Creates a mock client bundle that can be used for unit testing. /// /// Returns the client, and a helper to inject packets as if the client sent From 8aaddee65861bc3f784dae0a0af3b62dbda5eec7 Mon Sep 17 00:00:00 2001 From: Ryan Date: Sat, 29 Jul 2023 04:01:16 -0700 Subject: [PATCH 28/47] Remove uuid -> entity map for now --- crates/valence_entity/src/lib.rs | 25 ++++--------------------- crates/valence_entity/src/manager.rs | 8 -------- 2 files changed, 4 insertions(+), 29 deletions(-) diff --git a/crates/valence_entity/src/lib.rs b/crates/valence_entity/src/lib.rs index ea0802d59..ca7796f9f 100644 --- a/crates/valence_entity/src/lib.rs +++ b/crates/valence_entity/src/lib.rs @@ -117,19 +117,10 @@ fn update_old_layer_id(mut query: Query<(&EntityLayerId, &mut OldEntityLayerId)> } fn init_entities( - mut entities: Query< - ( - Entity, - &mut EntityId, - &mut UniqueId, - &Position, - &mut OldPosition, - ), - Added, - >, + mut entities: Query<(Entity, &mut EntityId, &Position, &mut OldPosition), Added>, mut manager: ResMut, ) { - for (entity, mut id, uuid, pos, mut old_pos) in &mut entities { + for (entity, mut id, pos, mut old_pos) in &mut entities { *old_pos = OldPosition::new(pos.0); if *id == EntityId::default() { @@ -142,23 +133,15 @@ fn init_entities( id.0 ); } - - if let Some(conflict) = manager.uuid_to_entity.insert(uuid.0, entity) { - warn!( - "entity {entity:?} has conflicting UUID of {} with entity {conflict:?}", - uuid.0 - ); - } } } fn remove_despawned_from_manager( - entities: Query<(&EntityId, &UniqueId), (With, With)>, + entities: Query<&EntityId, (With, With)>, mut manager: ResMut, ) { - for (id, uuid) in &entities { + for id in &entities { manager.id_to_entity.remove(&id.0); - manager.uuid_to_entity.remove(&uuid.0); } } diff --git a/crates/valence_entity/src/manager.rs b/crates/valence_entity/src/manager.rs index 390902718..c79cd62ca 100644 --- a/crates/valence_entity/src/manager.rs +++ b/crates/valence_entity/src/manager.rs @@ -3,7 +3,6 @@ use std::num::Wrapping; use bevy_ecs::prelude::*; use rustc_hash::FxHashMap; use tracing::warn; -use uuid::Uuid; use super::EntityId; @@ -13,7 +12,6 @@ use super::EntityId; pub struct EntityManager { /// Maps protocol IDs to ECS entities. pub(super) id_to_entity: FxHashMap, - pub(super) uuid_to_entity: FxHashMap, next_id: Wrapping, } @@ -21,7 +19,6 @@ impl EntityManager { pub(super) fn new() -> Self { Self { id_to_entity: FxHashMap::default(), - uuid_to_entity: FxHashMap::default(), next_id: Wrapping(1), // Skip 0. } } @@ -45,9 +42,4 @@ impl EntityManager { pub fn get_by_id(&self, entity_id: i32) -> Option { self.id_to_entity.get(&entity_id).cloned() } - - /// Gets the entity with the given UUID. - pub fn get_by_uuid(&self, uuid: Uuid) -> Option { - self.uuid_to_entity.get(&uuid).cloned() - } } From 51b381b3b267390738ee2ffc50a2683b6ad93566 Mon Sep 17 00:00:00 2001 From: Ryan Date: Sat, 29 Jul 2023 04:24:09 -0700 Subject: [PATCH 29/47] Despawn entities before spawning --- crates/valence_client/src/lib.rs | 125 +++++++++++++++-------------- crates/valence_entity/src/lib.rs | 22 ++--- crates/valence_layer/src/entity.rs | 15 ++-- 3 files changed, 83 insertions(+), 79 deletions(-) diff --git a/crates/valence_client/src/lib.rs b/crates/valence_client/src/lib.rs index 7c4379a18..79f807b3a 100644 --- a/crates/valence_client/src/lib.rs +++ b/crates/valence_client/src/lib.rs @@ -125,7 +125,6 @@ impl Plugin for ClientPlugin { cleanup_chunks_after_client_despawn.after(update_view_and_layers), spawn::update_respawn_position.after(update_view_and_layers), spawn::respawn.after(spawn::update_respawn_position), - remove_entities.after(update_view_and_layers), update_old_view_dist.after(update_view_and_layers), update_game_mode, update_tracked_data, @@ -462,13 +461,20 @@ impl EntityRemoveBuf { "removing entity with protocol ID 0 (which should be reserved for clients)" ); - debug_assert!( - !self.0.contains(&VarInt(entity_id)), - "removing entity ID {entity_id} multiple times in a single tick!" - ); - self.0.push(VarInt(entity_id)); } + + /// Sends the entity remove packet and clears the buffer. Does nothing if + /// the buffer is empty. + pub fn send_and_clear(&mut self, mut w: impl WritePacket) { + if !self.0.is_empty() { + w.write_packet(&EntitiesDestroyS2c { + entity_ids: Cow::Borrowed(&self.0), + }); + + self.0.clear(); + } + } } #[derive(Component, Clone, PartialEq, Eq, Default, Debug)] @@ -817,30 +823,29 @@ fn handle_layer_messages( // Local messages messages.query_local(old_view, |msg, range| match msg { - valence_layer::entity::LocalMsg::PacketAt { pos: _ } => { - client.write_packet_bytes(&bytes[range]); - } - valence_layer::entity::LocalMsg::PacketAtExcept { pos: _, except } => { - if self_entity != except { - client.write_packet_bytes(&bytes[range]); - } - } - valence_layer::entity::LocalMsg::RadiusAt { - center, - radius_squared, - } => { - if in_radius(block_pos, center, radius_squared) { - client.write_packet_bytes(&bytes[range]); + valence_layer::entity::LocalMsg::DespawnEntity { pos: _, dest_layer } => { + if !old_visible_entity_layers.0.contains(&dest_layer) { + let mut bytes = &bytes[range]; + + while let Ok(id) = bytes.read_i32::() { + if self_entity_id.get() != id { + remove_buf.push(id); + } + } } } - valence_layer::entity::LocalMsg::RadiusAtExcept { - center, - radius_squared, - except, + valence_layer::entity::LocalMsg::DespawnEntityTransition { + pos: _, + dest_pos, } => { - if self_entity != except && in_radius(block_pos, center, radius_squared) - { - client.write_packet_bytes(&bytes[range]); + if !old_view.contains(dest_pos) { + let mut bytes = &bytes[range]; + + while let Ok(id) = bytes.read_i32::() { + if self_entity_id.get() != id { + remove_buf.push(id); + } + } } } valence_layer::entity::LocalMsg::SpawnEntity { pos: _, src_layer } => { @@ -852,6 +857,8 @@ fn handle_layer_messages( if self_entity != entity { if let Ok((init, old_pos)) = entities.get(entity) { + remove_buf.send_and_clear(&mut *client); + // Spawn at the entity's old position since we may get a // relative movement packet for this entity in a later // iteration of the loop. @@ -873,6 +880,8 @@ fn handle_layer_messages( if self_entity != entity { if let Ok((init, old_pos)) = entities.get(entity) { + remove_buf.send_and_clear(&mut *client); + // Spawn at the entity's old position since we may get a // relative movement packet for this entity in a later // iteration of the loop. @@ -882,32 +891,35 @@ fn handle_layer_messages( } } } - valence_layer::entity::LocalMsg::DespawnEntity { pos: _, dest_layer } => { - if !old_visible_entity_layers.0.contains(&dest_layer) { - let mut bytes = &bytes[range]; - - while let Ok(id) = bytes.read_i32::() { - if self_entity_id.get() != id { - remove_buf.push(id); - } - } + valence_layer::entity::LocalMsg::PacketAt { pos: _ } => { + client.write_packet_bytes(&bytes[range]); + } + valence_layer::entity::LocalMsg::PacketAtExcept { pos: _, except } => { + if self_entity != except { + client.write_packet_bytes(&bytes[range]); } } - valence_layer::entity::LocalMsg::DespawnEntityTransition { - pos: _, - dest_pos, + valence_layer::entity::LocalMsg::RadiusAt { + center, + radius_squared, } => { - if !old_view.contains(dest_pos) { - let mut bytes = &bytes[range]; - - while let Ok(id) = bytes.read_i32::() { - if self_entity_id.get() != id { - remove_buf.push(id); - } - } + if in_radius(block_pos, center, radius_squared) { + client.write_packet_bytes(&bytes[range]); + } + } + valence_layer::entity::LocalMsg::RadiusAtExcept { + center, + radius_squared, + except, + } => { + if self_entity != except && in_radius(block_pos, center, radius_squared) + { + client.write_packet_bytes(&bytes[range]); } } }); + + remove_buf.send_and_clear(&mut *client); } } }, @@ -1006,6 +1018,8 @@ fn update_view_and_layers( } } + remove_buf.send_and_clear(&mut *client); + // Load all entities in the new view from all new visible entity layers. for &layer in &visible_entity_layers.0 { if let Ok(layer) = entity_layers.get(layer) { @@ -1041,6 +1055,8 @@ fn update_view_and_layers( } } + remove_buf.send_and_clear(&mut *client); + // Load all entity layers that are newly visible in the old view. for &layer in visible_entity_layers .0 @@ -1131,21 +1147,6 @@ fn update_view_and_layers( ); } -/// Removes all the entities that are queued to be removed for each client. -fn remove_entities( - mut clients: Query<(&mut Client, &mut EntityRemoveBuf), Changed>, -) { - for (mut client, mut buf) in &mut clients { - if !buf.0.is_empty() { - client.write_packet(&EntitiesDestroyS2c { - entity_ids: Cow::Borrowed(&buf.0), - }); - - buf.0.clear(); - } - } -} - fn update_game_mode(mut clients: Query<(&mut Client, &GameMode), Changed>) { for (mut client, game_mode) in &mut clients { if client.is_added() { diff --git a/crates/valence_entity/src/lib.rs b/crates/valence_entity/src/lib.rs index ca7796f9f..edf8faf09 100644 --- a/crates/valence_entity/src/lib.rs +++ b/crates/valence_entity/src/lib.rs @@ -84,7 +84,7 @@ impl Plugin for EntityPlugin { ) .add_systems( PostUpdate, - (init_entities, remove_despawned_from_manager) + (remove_despawned_from_manager, init_entities) .chain() .in_set(InitEntitiesSet), ) @@ -116,8 +116,17 @@ fn update_old_layer_id(mut query: Query<(&EntityLayerId, &mut OldEntityLayerId)> } } +fn remove_despawned_from_manager( + entities: Query<&EntityId, (With, With)>, + mut manager: ResMut, +) { + for id in &entities { + manager.id_to_entity.remove(&id.0); + } +} + fn init_entities( - mut entities: Query<(Entity, &mut EntityId, &Position, &mut OldPosition), Added>, + mut entities: Query<(Entity, &mut EntityId, &Position, &mut OldPosition), (Added, Without)>, mut manager: ResMut, ) { for (entity, mut id, pos, mut old_pos) in &mut entities { @@ -136,15 +145,6 @@ fn init_entities( } } -fn remove_despawned_from_manager( - entities: Query<&EntityId, (With, With)>, - mut manager: ResMut, -) { - for id in &entities { - manager.id_to_entity.remove(&id.0); - } -} - fn clear_status_changes(mut statuses: Query<&mut EntityStatuses, Changed>) { for mut statuses in &mut statuses { statuses.0 = 0; diff --git a/crates/valence_layer/src/entity.rs b/crates/valence_layer/src/entity.rs index a08756223..56d926ab4 100644 --- a/crates/valence_layer/src/entity.rs +++ b/crates/valence_layer/src/entity.rs @@ -44,7 +44,14 @@ pub enum GlobalMsg { #[doc(hidden)] #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +// NOTE: Variant order is significant. Despawns should be ordered before spawns. pub enum LocalMsg { + /// Despawn entities if the client is not already viewing `dest_layer`. + /// Message data is the serialized form of `EntityId`. + DespawnEntity { pos: ChunkPos, dest_layer: Entity }, + /// Despawn entities if the client is not in view of `dest_pos`. Message + /// data is the serialized form of `EntityId`. + DespawnEntityTransition { pos: ChunkPos, dest_pos: ChunkPos }, /// Spawn entities if the client is not already viewing `src_layer`. Message /// data is the serialized form of [`Entity`]. SpawnEntity { pos: ChunkPos, src_layer: Entity }, @@ -58,21 +65,17 @@ pub enum LocalMsg { /// except the client identified by `except`. Message data is serialized /// packet data. PacketAtExcept { pos: ChunkPos, except: Entity }, + /// Send packet data to all clients in a sphere. RadiusAt { center: BlockPos, radius_squared: u32, }, + /// Send packet data to all clients in a sphere, except the client `except`. RadiusAtExcept { center: BlockPos, radius_squared: u32, except: Entity, }, - /// Despawn entities if the client is not already viewing `dest_layer`. - /// Message data is the serialized form of `EntityId`. - DespawnEntity { pos: ChunkPos, dest_layer: Entity }, - /// Despawn entities if the client is not in view of `dest_pos`. Message - /// data is the serialized form of `EntityId`. - DespawnEntityTransition { pos: ChunkPos, dest_pos: ChunkPos }, } impl GetChunkPos for LocalMsg { From 61dee4938e0876eb13a5b042feb9bbf192d74375 Mon Sep 17 00:00:00 2001 From: Ryan Date: Sat, 29 Jul 2023 04:41:44 -0700 Subject: [PATCH 30/47] merge with main --- crates/valence_core/src/text.rs | 216 ++++++++------------------------ examples/block_entities.rs | 23 +++- 2 files changed, 69 insertions(+), 170 deletions(-) diff --git a/crates/valence_core/src/text.rs b/crates/valence_core/src/text.rs index 394264974..5bb024c2e 100644 --- a/crates/valence_core/src/text.rs +++ b/crates/valence_core/src/text.rs @@ -3,6 +3,7 @@ use std::borrow::Cow; use std::io::Write; use std::ops::{Deref, DerefMut}; +use std::str::FromStr; use std::{fmt, ops}; use anyhow::Context; @@ -41,13 +42,13 @@ pub use into_text::IntoText; /// + "Green".color(Color::GREEN) /// + ", and also " /// + "Blue".color(Color::BLUE) -/// + "!\nAnd maybe even " +/// + "! And maybe even " /// + "Italic".italic() /// + "."; /// /// assert_eq!( /// txt.to_string(), -/// "The text is Red, Green, and also Blue!\nAnd maybe even Italic." +/// r#"{"text":"The text is ","extra":[{"text":"Red","color":"red"},{"text":", "},{"text":"Green","color":"green"},{"text":", and also "},{"text":"Blue","color":"blue"},{"text":"! And maybe even "},{"text":"Italic","italic":true},{"text":"."}]}"# /// ); /// ``` #[derive(Clone, PartialEq, Default, Serialize)] @@ -366,140 +367,6 @@ impl Text { })) } - /// Writes the string representation of this text object to the provided - /// writer. - pub fn write_string(&self, mut w: impl fmt::Write) -> fmt::Result { - fn write_string_inner(this: &Text, w: &mut impl fmt::Write) -> fmt::Result { - match &this.0.content { - TextContent::Text { text } => w.write_str(text.as_ref())?, - TextContent::Translate { translate, with } => { - w.write_str(translate.as_ref())?; - - if !with.is_empty() { - w.write_char('[')?; - for (i, slot) in with.iter().enumerate() { - if i > 0 { - w.write_str(", ")?; - } - w.write_char(char::from_digit((i + 1) as u32, 10).unwrap_or('?'))?; - w.write_char('=')?; - write_string_inner(slot, w)?; - } - w.write_char(']')?; - } - } - TextContent::ScoreboardValue { score } => { - let ScoreboardValueContent { - name, - objective, - value, - } = score; - - write!(w, "scoreboard_value[name={name}, objective={objective}")?; - - if let Some(value) = value { - if !value.is_empty() { - w.write_str(", value=")?; - w.write_str(value)?; - } - } - - w.write_char(']')?; - } - TextContent::EntityNames { - selector, - separator, - } => { - write!(w, "entity_names[selector={selector}")?; - - if let Some(separator) = separator { - if !separator.is_empty() { - w.write_str(", separator={separator}")?; - } - } - - w.write_char(']')?; - } - TextContent::Keybind { keybind } => write!(w, "keybind[{keybind}]")?, - TextContent::BlockNbt { - block, - nbt, - interpret, - separator, - } => { - write!(w, "block_nbt[nbt={nbt}")?; - - if let Some(interpret) = interpret { - write!(w, ", interpret={interpret}")?; - } - - if let Some(separator) = separator { - if !separator.is_empty() { - write!(w, "separator={separator}")?; - } - } - - write!(w, "block={block}")?; - - w.write_char(']')?; - } - TextContent::EntityNbt { - entity, - nbt, - interpret, - separator, - } => { - write!(w, "entity_nbt[nbt={nbt}")?; - - if let Some(interpret) = interpret { - write!(w, ", interpret={interpret}")?; - } - - if let Some(separator) = separator { - if !separator.is_empty() { - write!(w, "separator={separator}")?; - } - } - - write!(w, ", entity={entity}")?; - - w.write_char(']')?; - } - TextContent::StorageNbt { - storage, - nbt, - interpret, - separator, - } => { - write!(w, "storage_nbt[nbt={nbt}")?; - - if let Some(interpret) = interpret { - write!(w, ", interpret={interpret}")?; - } - - if let Some(separator) = separator { - if !separator.is_empty() { - write!(w, "separator=")?; - write_string_inner(separator, w)?; - } - } - - write!(w, ", storage={storage}")?; - - w.write_char(']')?; - } - } - - for child in &this.0.extra { - write_string_inner(child, w)?; - } - - Ok(()) - } - - write_string_inner(self, &mut w) - } - /// Returns `true` if the text contains no characters. Returns `false` /// otherwise. pub fn is_empty(&self) -> bool { @@ -675,24 +542,46 @@ impl<'a> From<&'a Text> for Cow<'a, Text> { } } +impl FromStr for Text { + type Err = serde_json::error::Error; + + fn from_str(s: &str) -> Result { + if s.is_empty() { + Ok(Text::default()) + } else { + serde_json::from_str(s) + } + } +} + +impl From for String { + fn from(value: Text) -> Self { + format!("{value}") + } +} + impl From for Value { fn from(value: Text) -> Self { - Value::String( - serde_json::to_string(&value) - .unwrap_or_else(|err| panic!("failed to jsonify text {value:?}\n{err}")), - ) + Value::String(value.into()) } } impl fmt::Debug for Text { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.write_string(f) + fmt::Display::fmt(self, f) } } impl fmt::Display for Text { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.write_string(f) + let string = if f.alternate() { + serde_json::to_string_pretty(self) + } else { + serde_json::to_string(self) + } + .map_err(|_| fmt::Error)?; + + f.write_str(&string) } } @@ -710,12 +599,9 @@ impl Encode for Text { impl Decode<'_> for Text { fn decode(r: &mut &[u8]) -> anyhow::Result { - let string = <&str>::decode(r)?; - if string.is_empty() { - Ok(Self::default()) - } else { - serde_json::from_str(string).context("decoding text JSON") - } + let str = <&str>::decode(r)?; + + Self::from_str(str).context("decoding text JSON") } } @@ -790,11 +676,9 @@ mod tests { + ("bar".obfuscated().color(Color::YELLOW) + "baz".underlined().not_bold().italic().color(Color::BLACK)); - assert_eq!(before.to_string(), "foobarbaz"); - - let json = serde_json::to_string_pretty(&before).unwrap(); + let json = format!("{before:#}"); - let after: Text = serde_json::from_str(&json).unwrap(); + let after = Text::from_str(&json).unwrap(); println!("==== Before ====\n"); println!("{before:#?}"); @@ -819,8 +703,8 @@ mod tests { translation_key::CHAT_TYPE_ADVANCEMENT_TASK, ["arg1".into_text(), "arg2".into_text()], ); - let serialized = serde_json::to_string(&txt).unwrap(); - let deserialized: Text = serde_json::from_str(&serialized).unwrap(); + let serialized = txt.to_string(); + let deserialized = Text::from_str(&serialized).unwrap(); assert_eq!( serialized, r#"{"translate":"chat.type.advancement.task","with":[{"text":"arg1"},{"text":"arg2"}]}"# @@ -831,8 +715,8 @@ mod tests { #[test] fn score() { let txt = Text::score("foo", "bar", Some(Cow::from("baz"))); - let serialized = serde_json::to_string(&txt).unwrap(); - let deserialized: Text = serde_json::from_str(&serialized).unwrap(); + let serialized = txt.to_string(); + let deserialized = Text::from_str(&serialized).unwrap(); assert_eq!( serialized, r#"{"score":{"name":"foo","objective":"bar","value":"baz"}}"# @@ -844,8 +728,8 @@ mod tests { fn selector() { let separator = Text::text("bar").color(Color::RED).bold(); let txt = Text::selector("foo", Some(separator)); - let serialized = serde_json::to_string(&txt).unwrap(); - let deserialized: Text = serde_json::from_str(&serialized).unwrap(); + let serialized = txt.to_string(); + let deserialized = Text::from_str(&serialized).unwrap(); assert_eq!( serialized, r##"{"selector":"foo","separator":{"text":"bar","color":"red","bold":true}}"## @@ -856,8 +740,8 @@ mod tests { #[test] fn keybind() { let txt = Text::keybind("foo"); - let serialized = serde_json::to_string(&txt).unwrap(); - let deserialized: Text = serde_json::from_str(&serialized).unwrap(); + let serialized = txt.to_string(); + let deserialized = Text::from_str(&serialized).unwrap(); assert_eq!(serialized, r#"{"keybind":"foo"}"#); assert_eq!(txt, deserialized); } @@ -865,8 +749,8 @@ mod tests { #[test] fn block_nbt() { let txt = Text::block_nbt("foo", "bar", Some(true), Some("baz".into_text())); - let serialized = serde_json::to_string(&txt).unwrap(); - let deserialized: Text = serde_json::from_str(&serialized).unwrap(); + let serialized = txt.to_string(); + let deserialized = Text::from_str(&serialized).unwrap(); let expected = r#"{"block":"foo","nbt":"bar","interpret":true,"separator":{"text":"baz"}}"#; assert_eq!(serialized, expected); assert_eq!(txt, deserialized); @@ -875,8 +759,8 @@ mod tests { #[test] fn entity_nbt() { let txt = Text::entity_nbt("foo", "bar", Some(true), Some("baz".into_text())); - let serialized = serde_json::to_string(&txt).unwrap(); - let deserialized: Text = serde_json::from_str(&serialized).unwrap(); + let serialized = txt.to_string(); + let deserialized = Text::from_str(&serialized).unwrap(); let expected = r#"{"entity":"foo","nbt":"bar","interpret":true,"separator":{"text":"baz"}}"#; assert_eq!(serialized, expected); @@ -886,8 +770,8 @@ mod tests { #[test] fn storage_nbt() { let txt = Text::storage_nbt(ident!("foo"), "bar", Some(true), Some("baz".into_text())); - let serialized = serde_json::to_string(&txt).unwrap(); - let deserialized: Text = serde_json::from_str(&serialized).unwrap(); + let serialized = txt.to_string(); + let deserialized = Text::from_str(&serialized).unwrap(); let expected = r#"{"storage":"minecraft:foo","nbt":"bar","interpret":true,"separator":{"text":"baz"}}"#; assert_eq!(serialized, expected); assert_eq!(txt, deserialized); diff --git a/examples/block_entities.rs b/examples/block_entities.rs index d891e2118..9e4564c87 100644 --- a/examples/block_entities.rs +++ b/examples/block_entities.rs @@ -52,7 +52,15 @@ fn setup( Block { state: BlockState::OAK_SIGN.set(PropName::Rotation, PropValue::_4), nbt: Some(compound! { - "Text1" => "Type in chat:".color(Color::RED), + "front_text" => compound! { + "messages" => List::String(vec![ + // All 4 lines are required, otherwise no text is displayed. + "Type in chat:".color(Color::RED).into(), + "".into_text().into(), + "".into_text().into(), + "".into_text().into(), + ]), + } }), }, ); @@ -113,9 +121,16 @@ fn event_handler( }; let nbt = layer.block_entity_mut(SIGN_POS).unwrap(); - - nbt.insert("Text2", message.to_string().color(Color::DARK_GREEN)); - nbt.insert("Text3", format!("~{username}").italic()); + nbt.merge(compound! { + "front_text" => compound! { + "messages" => List::String(vec![ + "Type in chat:".color(Color::RED).into(), + message.to_string().color(Color::DARK_GREEN).into(), + format!("~{username}").italic().into(), + "".into_text().into(), + ]), + }, + }); } for InteractBlockEvent { From 37ea3c8839505bf2b5a53766a44e2803b4953780 Mon Sep 17 00:00:00 2001 From: Ryan Date: Sun, 30 Jul 2023 02:29:23 -0700 Subject: [PATCH 31/47] update weather --- Cargo.toml | 8 +- crates/valence_client/src/lib.rs | 3 +- crates/valence_client/src/weather.rs | 216 ------------------------- crates/valence_weather/Cargo.toml | 14 ++ crates/valence_weather/src/lib.rs | 111 +++++++++++++ crates/valence_weather/src/packet.rs | 0 crates/valence_world_border/src/lib.rs | 2 +- examples/weather.rs | 84 ++++++++++ src/lib.rs | 7 + src/tests.rs | 1 - src/tests/weather.rs | 100 ------------ 11 files changed, 224 insertions(+), 322 deletions(-) delete mode 100644 crates/valence_client/src/weather.rs create mode 100644 crates/valence_weather/Cargo.toml create mode 100644 crates/valence_weather/src/lib.rs create mode 100644 crates/valence_weather/src/packet.rs create mode 100644 examples/weather.rs delete mode 100644 src/tests/weather.rs diff --git a/Cargo.toml b/Cargo.toml index be8a273ec..e49c7905f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ default = [ "network", "player_list", "world_border", + "weather", ] advancement = ["dep:valence_advancement"] anvil = ["dep:valence_anvil"] @@ -29,6 +30,7 @@ log = ["dep:bevy_log"] network = ["dep:valence_network"] player_list = ["dep:valence_player_list"] world_border = ["dep:valence_world_border"] +weather = ["dep:valence_weather"] [dependencies] anyhow.workspace = true @@ -55,13 +57,14 @@ valence_network = { workspace = true, optional = true } valence_player_list = { workspace = true, optional = true } valence_registry.workspace = true valence_world_border = { workspace = true, optional = true } +valence_weather = { workspace = true, optional = true } [dev-dependencies] anyhow.workspace = true clap.workspace = true criterion.workspace = true flume.workspace = true -noise.workspace = true # For the terrain example. +noise.workspace = true # For the terrain example. tracing.workspace = true [dev-dependencies.reqwest] @@ -103,7 +106,7 @@ bevy_app = { version = "0.11", default-features = false } bevy_ecs = { version = "0.11", default-features = false } bevy_hierarchy = { version = "0.11", default-features = false } bevy_log = { version = "0.11" } -bevy_mod_debugdump = { version = "0.8.0", default-features = false } +bevy_mod_debugdump = { version = "0.8.0", default-features = false } bevy_utils = { version = "0.11" } bitfield-struct = "0.3.1" byteorder = "1.4.3" @@ -178,5 +181,6 @@ valence_player_list.path = "crates/valence_player_list" valence_registry.path = "crates/valence_registry" valence_world_border.path = "crates/valence_world_border" valence_boss_bar.path = "crates/valence_boss_bar" +valence_weather.path = "crates/valence_weather" valence.path = "." zip = "0.6.3" diff --git a/crates/valence_client/src/lib.rs b/crates/valence_client/src/lib.rs index 79f807b3a..ad7ae9c22 100644 --- a/crates/valence_client/src/lib.rs +++ b/crates/valence_client/src/lib.rs @@ -89,7 +89,6 @@ pub mod spawn; pub mod status; pub mod teleport; pub mod title; -pub mod weather; pub struct ClientPlugin; @@ -106,7 +105,7 @@ pub struct FlushPacketsSet; pub struct SpawnClientsSet; /// The system set where various facets of the client are updated. Systems that -/// modify chunks should run _before_ this. +/// modify layers should run _before_ this. #[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] pub struct UpdateClientsSet; diff --git a/crates/valence_client/src/weather.rs b/crates/valence_client/src/weather.rs deleted file mode 100644 index 8560961db..000000000 --- a/crates/valence_client/src/weather.rs +++ /dev/null @@ -1,216 +0,0 @@ -/* -//! The weather system. -//! -//! This module contains the systems and components needed to handle -//! weather. -//! -//! # Components -//! -//! The components may be attached to clients or instances. -//! -//! - [`Rain`]: When attached, raining begin and rain level set events are -//! emitted. When removed, the end raining event is emitted. -//! - [`Thunder`]: When attached, thunder level set event is emitted. When -//! removed, the thunder level set to zero event is emitted. -//! -//! New joined players are handled, so that they are get weather events from -//! the instance. - -use super::*; -use crate::packet::{GameEventKind, GameStateChangeS2c}; - -#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] -struct UpdateWeatherPerInstanceSet; - -#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] -struct UpdateWeatherPerClientSet; - -pub(super) fn build(app: &mut App) { - app.configure_sets( - PostUpdate, - (UpdateWeatherPerInstanceSet, UpdateWeatherPerClientSet).before(FlushPacketsSet), - ) - .add_systems( - PostUpdate, - ( - rain_begin_per_instance, - rain_change_per_instance, - rain_end_per_instance, - thunder_change_per_instance, - thunder_end_per_instance, - ) - .chain() - .in_set(UpdateWeatherPerInstanceSet) - .before(UpdateWeatherPerClientSet), - ) - .add_systems( - PostUpdate, - ( - rain_begin_per_client, - rain_change_per_client, - rain_end_per_client, - thunder_change_per_client, - thunder_end_per_client, - ) - .chain() - .in_set(UpdateWeatherPerClientSet), - ) - .add_systems( - PostUpdate, - handle_weather_for_joined_player.before(UpdateWeatherPerClientSet), - ); -} - -/// Contains the rain level. -/// -/// Valid values are within `0.0..=1.0`. -#[derive(Component)] -pub struct Rain(pub f32); - -/// Contains the thunder level. -/// -/// Valid values are within `0.0..=1.0`. -#[derive(Component)] -pub struct Thunder(pub f32); - -fn handle_weather_for_joined_player( - mut clients: Query<(&mut Client, &EntityLayerId), Added>, - weathers: Query<(Option<&Rain>, Option<&Thunder>), With>, -) { - for (mut client, loc) in &mut clients { - if let Ok((rain, thunder)) = weathers.get(loc.0) { - if let Some(level) = rain { - client.write_packet(&GameStateChangeS2c { - kind: GameEventKind::BeginRaining, - value: 0.0, - }); - - client.write_packet(&GameStateChangeS2c { - kind: GameEventKind::RainLevelChange, - value: level.0, - }); - } - - if let Some(level) = thunder { - client.write_packet(&GameStateChangeS2c { - kind: GameEventKind::ThunderLevelChange, - value: level.0, - }); - } - } - } -} - -fn rain_begin_per_instance(mut instances: Query<&mut Instance, Added>) { - for mut instance in &mut instances { - instance.write_packet(&GameStateChangeS2c { - kind: GameEventKind::BeginRaining, - value: f32::default(), - }); - } -} - -fn rain_change_per_instance(mut instances: Query<(&mut Instance, &Rain), Changed>) { - for (mut instance, rain) in &mut instances { - instance.write_packet(&GameStateChangeS2c { - kind: GameEventKind::RainLevelChange, - value: rain.0, - }); - } -} - -fn rain_end_per_instance( - mut instances: Query<&mut Instance>, - mut removed: RemovedComponents, -) { - for entity in &mut removed { - if let Ok(mut instance) = instances.get_mut(entity) { - instance.write_packet(&GameStateChangeS2c { - kind: GameEventKind::EndRaining, - value: 0.0, - }); - } - } -} - -fn thunder_change_per_instance(mut instances: Query<(&mut Instance, &Thunder), Changed>) { - for (mut instance, thunder) in &mut instances { - instance.write_packet(&GameStateChangeS2c { - kind: GameEventKind::ThunderLevelChange, - value: thunder.0, - }); - } -} - -fn thunder_end_per_instance( - mut instances: Query<&mut Instance>, - mut removed: RemovedComponents, -) { - for entity in &mut removed { - if let Ok(mut instance) = instances.get_mut(entity) { - instance.write_packet(&GameStateChangeS2c { - kind: GameEventKind::ThunderLevelChange, - value: 0.0, - }); - } - } -} - -fn rain_begin_per_client(mut clients: Query<&mut Client, (Added, Without)>) { - for mut client in &mut clients { - client.write_packet(&GameStateChangeS2c { - kind: GameEventKind::BeginRaining, - value: 0.0, - }); - } -} - -#[allow(clippy::type_complexity)] -fn rain_change_per_client( - mut clients: Query<(&mut Client, &Rain), (Changed, Without)>, -) { - for (mut client, rain) in &mut clients { - client.write_packet(&GameStateChangeS2c { - kind: GameEventKind::RainLevelChange, - value: rain.0, - }); - } -} - -fn rain_end_per_client(mut clients: Query<&mut Client>, mut removed: RemovedComponents) { - for entity in &mut removed { - if let Ok(mut client) = clients.get_mut(entity) { - client.write_packet(&GameStateChangeS2c { - kind: GameEventKind::EndRaining, - value: f32::default(), - }); - } - } -} - -#[allow(clippy::type_complexity)] -fn thunder_change_per_client( - mut clients: Query<(&mut Client, &Thunder), (Changed, Without)>, -) { - for (mut client, thunder) in &mut clients { - client.write_packet(&GameStateChangeS2c { - kind: GameEventKind::ThunderLevelChange, - value: thunder.0, - }); - } -} - -fn thunder_end_per_client( - mut clients: Query<&mut Client, Without>, - mut removed: RemovedComponents, -) { - for entity in &mut removed { - if let Ok(mut client) = clients.get_mut(entity) { - client.write_packet(&GameStateChangeS2c { - kind: GameEventKind::ThunderLevelChange, - value: 0.0, - }); - } - } -} -*/ diff --git a/crates/valence_weather/Cargo.toml b/crates/valence_weather/Cargo.toml new file mode 100644 index 000000000..274b06cda --- /dev/null +++ b/crates/valence_weather/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "valence_weather" +version.workspace = true +edition.workspace = true +repository.workspace = true +documentation.workspace = true +license.workspace = true + +[dependencies] +valence_client.workspace = true +valence_layer.workspace = true +valence_core.workspace = true +bevy_ecs.workspace = true +bevy_app.workspace = true \ No newline at end of file diff --git a/crates/valence_weather/src/lib.rs b/crates/valence_weather/src/lib.rs new file mode 100644 index 000000000..e5ea3ec73 --- /dev/null +++ b/crates/valence_weather/src/lib.rs @@ -0,0 +1,111 @@ +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; +use valence_client::packet::{GameEventKind, GameStateChangeS2c}; +use valence_client::{Client, FlushPacketsSet, UpdateClientsSet, VisibleChunkLayer}; +use valence_core::protocol::encode::WritePacket; +use valence_layer::ChunkLayer; + +pub struct WeatherPlugin; + +impl Plugin for WeatherPlugin { + fn build(&self, app: &mut App) { + app.add_systems( + PostUpdate, + ( + init_weather_on_layer_join, + change_client_rain_level, + change_client_thunder_level, + ) + .before(FlushPacketsSet), + ) + .add_systems( + PostUpdate, + (change_layer_rain_level, change_layer_thunder_level).before(UpdateClientsSet), + ); + } +} + +/// Bundle containing rain and thunder components. `valence_weather` allows this +/// to be added to clients and chunk layer entities. +#[derive(Bundle, Default, PartialEq, PartialOrd)] +pub struct WeatherBundle { + pub rain: Rain, + pub thunder: Thunder, +} + +/// Component containing the rain level. Valid values are in \[0, 1] with 0 +/// being no rain and 1 being full rain. +#[derive(Component, Default, PartialEq, PartialOrd)] +pub struct Rain(pub f32); + +/// Component containing the thunder level. Valid values are in \[0, 1] with 0 +/// being no rain and 1 being full rain. +#[derive(Component, Default, PartialEq, PartialOrd)] +pub struct Thunder(pub f32); + +fn init_weather_on_layer_join( + mut clients: Query<(&mut Client, &VisibleChunkLayer), Changed>, + layers: Query<(Option<&Rain>, Option<&Thunder>), With>, +) { + for (mut client, visible_chunk_layer) in &mut clients { + if let Ok((rain, thunder)) = layers.get(visible_chunk_layer.0) { + if let Some(rain) = rain { + if rain.0 != 0.0 { + client.write_packet(&GameStateChangeS2c { + kind: GameEventKind::RainLevelChange, + value: rain.0, + }); + } + } + + if let Some(thunder) = thunder { + if thunder.0 != 0.0 { + client.write_packet(&GameStateChangeS2c { + kind: GameEventKind::ThunderLevelChange, + value: thunder.0, + }); + } + } + } + } +} + +fn change_layer_rain_level( + mut layers: Query<(&mut ChunkLayer, &Rain), (Changed, Without)>, +) { + for (mut layer, rain) in &mut layers { + layer.write_packet(&GameStateChangeS2c { + kind: GameEventKind::RainLevelChange, + value: rain.0, + }); + } +} + +fn change_layer_thunder_level( + mut layers: Query<(&mut ChunkLayer, &Thunder), (Changed, Without)>, +) { + for (mut layer, thunder) in &mut layers { + layer.write_packet(&GameStateChangeS2c { + kind: GameEventKind::ThunderLevelChange, + value: thunder.0, + }); + } +} + +fn change_client_rain_level(mut clients: Query<(&mut Client, &Rain), Changed>) { + for (mut client, rain) in &mut clients { + client.write_packet(&GameStateChangeS2c { + kind: GameEventKind::RainLevelChange, + value: rain.0, + }); + } +} + +fn change_client_thunder_level(mut clients: Query<(&mut Client, &Thunder), Changed>) { + for (mut client, thunder) in &mut clients { + client.write_packet(&GameStateChangeS2c { + kind: GameEventKind::RainLevelChange, + value: thunder.0, + }); + } +} diff --git a/crates/valence_weather/src/packet.rs b/crates/valence_weather/src/packet.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/valence_world_border/src/lib.rs b/crates/valence_world_border/src/lib.rs index 0576e4bce..eb62787e0 100644 --- a/crates/valence_world_border/src/lib.rs +++ b/crates/valence_world_border/src/lib.rs @@ -1,4 +1,3 @@ -/* //! # World border //! This module contains Components and Systems needed to handle world border. //! @@ -76,6 +75,7 @@ pub mod packet; +/* use std::time::{Duration, Instant}; use bevy_app::prelude::*; diff --git a/examples/weather.rs b/examples/weather.rs new file mode 100644 index 000000000..84ce9a652 --- /dev/null +++ b/examples/weather.rs @@ -0,0 +1,84 @@ +use std::f64::consts::TAU; + +use valence::prelude::*; +use valence::weather::Rain; +use valence_weather::{Thunder, WeatherBundle}; + +pub fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems( + Update, + (init_clients, despawn_disconnected_clients, change_weather), + ) + .run(); +} + +fn setup( + mut commands: Commands, + server: Res, + dimensions: Res, + biomes: Res, +) { + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + + for z in -5..5 { + for x in -5..5 { + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + } + } + + for z in -25..25 { + for x in -25..25 { + layer.chunk.set_block([x, 64, z], BlockState::GRASS_BLOCK); + } + } + + commands.spawn((layer, WeatherBundle::default())); +} + +fn init_clients( + mut clients: Query< + ( + &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut Position, + &mut GameMode, + ), + Added, + >, + layers: Query, With)>, +) { + for ( + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut pos, + mut game_mode, + ) in &mut clients + { + let layer = layers.single(); + + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); + pos.set([0.0, 65.0, 0.0]); + *game_mode = GameMode::Creative; + } +} + +fn change_weather( + mut layers: Query<(&mut Rain, &mut Thunder), With>, + server: Res, +) { + let period = 5.0; + + let level = ((server.current_tick() as f64 / 20.0 * TAU / period).sin() + 1.0) / 2.0; + + for (mut rain, mut thunder) in &mut layers { + rain.0 = level as f32; + thunder.0 = level as f32; + } +} diff --git a/src/lib.rs b/src/lib.rs index 79bdd1ca6..fd18eac96 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,6 +42,8 @@ pub use valence_inventory as inventory; pub use valence_network as network; #[cfg(feature = "player_list")] pub use valence_player_list as player_list; +#[cfg(feature = "weather")] +pub use valence_weather as weather; #[cfg(feature = "world_border")] pub use valence_world_border as world_border; pub use { @@ -179,6 +181,11 @@ impl PluginGroup for DefaultPlugins { group = group.add(valence_advancement::AdvancementPlugin) } + #[cfg(feature = "weather")] + { + group = group.add(valence_weather::WeatherPlugin); + } + // TODO // #[cfg(feature = "world_border")] // { diff --git a/src/tests.rs b/src/tests.rs index 3641539b0..2a41a8315 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -4,5 +4,4 @@ mod example; mod inventory; mod layer; mod player_list; -mod weather; mod world_border; diff --git a/src/tests/weather.rs b/src/tests/weather.rs deleted file mode 100644 index aa0b8f052..000000000 --- a/src/tests/weather.rs +++ /dev/null @@ -1,100 +0,0 @@ -/* -use bevy_app::App; -use valence_client::packet::GameStateChangeS2c; -use valence_client::weather::{Rain, Thunder}; -use valence_client::Client; -use valence_instance::Instance; - -use crate::testing::{PacketFrames, ScenarioSingleClient}; - -#[test] -fn test_weather_instance() { - let ScenarioSingleClient { - mut app, - client, - mut helper, - layer, - } = ScenarioSingleClient::new(); - - // Process a tick to get past the "on join" logic. - app.update(); - helper.clear_received(); - - // Insert a rain component to the instance. - app.world.entity_mut(layer).insert(Rain(0.5)); - for _ in 0..2 { - app.update(); - } - - // Alter a rain component of the instance. - app.world.entity_mut(layer).insert(Rain(1.0)); - app.update(); - - // Insert a thunder component to the instance. - app.world.entity_mut(layer).insert(Thunder(0.5)); - app.update(); - - // Alter a thunder component of the instance. - app.world.entity_mut(layer).insert(Thunder(1.0)); - app.update(); - - // Remove the rain component from the instance. - app.world.entity_mut(layer).remove::(); - for _ in 0..2 { - app.update(); - } - - // Make assertions. - let sent_packets = helper.collect_received(); - - assert_weather_packets(sent_packets); -} - -#[test] -fn test_weather_client() { - let ScenarioSingleClient { - mut app, - client, - mut helper, - layer, - } = ScenarioSingleClient::new(); - - // Process a tick to get past the "on join" logic. - app.update(); - helper.clear_received(); - - // Insert a rain component to the client. - app.world.entity_mut(client).insert(Rain(0.5)); - for _ in 0..2 { - app.update(); - } - - // Alter a rain component of the client. - app.world.entity_mut(client).insert(Rain(1.0)); - app.update(); - - // Insert a thunder component to the client. - app.world.entity_mut(client).insert(Thunder(0.5)); - app.update(); - - // Alter a thunder component of the client. - app.world.entity_mut(client).insert(Thunder(1.0)); - app.update(); - - // Remove the rain component from the client. - app.world.entity_mut(client).remove::(); - for _ in 0..2 { - app.update(); - } - - // Make assertions. - let sent_packets = helper.collect_received(); - - assert_weather_packets(sent_packets); -} - -#[track_caller] -fn assert_weather_packets(sent_packets: PacketFrames) { - sent_packets.assert_count::(6); -} -*/ From c228fbfe6835016f1bcaf7e09337f75a37f0942c Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 31 Jul 2023 00:04:55 -0700 Subject: [PATCH 32/47] redesign valence_world_border --- crates/README.md | 1 + crates/valence_core/src/protocol/var_long.rs | 6 + crates/valence_entity/src/lib.rs | 5 +- crates/valence_weather/src/lib.rs | 19 + crates/valence_world_border/README.md | 1 + crates/valence_world_border/src/lib.rs | 462 ++++++------------- crates/valence_world_border/src/packet.rs | 4 +- examples/world_border.rs | 156 ++++--- src/lib.rs | 9 +- 9 files changed, 268 insertions(+), 395 deletions(-) create mode 100644 crates/valence_world_border/README.md diff --git a/crates/README.md b/crates/README.md index 5937cbc32..73e0b6226 100644 --- a/crates/README.md +++ b/crates/README.md @@ -24,4 +24,5 @@ graph TD advancement --> client world_border --> client boss_bar --> client + weather --> client ``` diff --git a/crates/valence_core/src/protocol/var_long.rs b/crates/valence_core/src/protocol/var_long.rs index 4146e5190..12d26c424 100644 --- a/crates/valence_core/src/protocol/var_long.rs +++ b/crates/valence_core/src/protocol/var_long.rs @@ -110,6 +110,12 @@ impl Decode<'_> for VarLong { } } +impl From for VarLong { + fn from(value: i64) -> Self { + Self(value) + } +} + #[cfg(test)] mod tests { use rand::{thread_rng, Rng}; diff --git a/crates/valence_entity/src/lib.rs b/crates/valence_entity/src/lib.rs index edf8faf09..54dbf0c77 100644 --- a/crates/valence_entity/src/lib.rs +++ b/crates/valence_entity/src/lib.rs @@ -126,7 +126,10 @@ fn remove_despawned_from_manager( } fn init_entities( - mut entities: Query<(Entity, &mut EntityId, &Position, &mut OldPosition), (Added, Without)>, + mut entities: Query< + (Entity, &mut EntityId, &Position, &mut OldPosition), + (Added, Without), + >, mut manager: ResMut, ) { for (entity, mut id, pos, mut old_pos) in &mut entities { diff --git a/crates/valence_weather/src/lib.rs b/crates/valence_weather/src/lib.rs index e5ea3ec73..f94eec8e3 100644 --- a/crates/valence_weather/src/lib.rs +++ b/crates/valence_weather/src/lib.rs @@ -1,3 +1,22 @@ +#![allow(clippy::type_complexity)] +#![deny( + rustdoc::broken_intra_doc_links, + rustdoc::private_intra_doc_links, + rustdoc::missing_crate_level_docs, + rustdoc::invalid_codeblock_attributes, + rustdoc::invalid_rust_codeblocks, + rustdoc::bare_urls, + rustdoc::invalid_html_tags +)] +#![warn( + trivial_casts, + trivial_numeric_casts, + unused_lifetimes, + unused_import_braces, + unreachable_pub, + clippy::dbg_macro +)] + use bevy_app::prelude::*; use bevy_ecs::prelude::*; use valence_client::packet::{GameEventKind, GameStateChangeS2c}; diff --git a/crates/valence_world_border/README.md b/crates/valence_world_border/README.md new file mode 100644 index 000000000..41760adf1 --- /dev/null +++ b/crates/valence_world_border/README.md @@ -0,0 +1 @@ +# valence_world_border diff --git a/crates/valence_world_border/src/lib.rs b/crates/valence_world_border/src/lib.rs index eb62787e0..04dedcc23 100644 --- a/crates/valence_world_border/src/lib.rs +++ b/crates/valence_world_border/src/lib.rs @@ -1,59 +1,4 @@ -//! # World border -//! This module contains Components and Systems needed to handle world border. -//! -//! The world border is the current edge of a Minecraft dimension. It appears as -//! a series of animated, diagonal, narrow stripes. For more information, refer to the [wiki](https://minecraft.fandom.com/wiki/World_border) -//! -//! ## Enable world border per instance -//! By default, world border is not enabled. It can be enabled by inserting the -//! [`WorldBorderBundle`] bundle into a [`Instance`]. -//! Use [`WorldBorderBundle::default()`] to use Minecraft Vanilla border default -//! ``` -//! # use valence_world_border::WorldBorderBundle; -//! # fn example(commands: &mut bevy_ecs::system::Commands, instance_entity: bevy_ecs::entity::Entity) { -//! commands -//! .entity(instance_entity) -//! .insert(WorldBorderBundle::new([0.0, 0.0], 10.0)); -//! # } -//! ``` -//! -//! -//! ## Modify world border diameter -//! World border diameter can be changed using [`SetWorldBorderSizeEvent`]. -//! Setting duration to 0 will move the border to `new_diameter` immediately, -//! otherwise, it will interpolate to `new_diameter` over `duration` time. -//! ``` -//! # use bevy_ecs::event::EventWriter; -//! # use valence_world_border::SetWorldBorderSizeEvent; -//! # use core::time::Duration; -//! fn change_diameter( -//! mut event_writer: EventWriter, -//! instance: bevy_ecs::entity::Entity, -//! diameter: f64, -//! duration: Duration, -//! ) { -//! event_writer.send(SetWorldBorderSizeEvent { -//! instance, -//! new_diameter: diameter, -//! duration, -//! }) -//! } -//! ``` -//! -//! You can also modify the [`MovingWorldBorder`] if you want more control. But -//! it is not recommended. -//! -//! ## Querying world border diameter -//! World border diameter can be read by querying -//! [`WorldBorderDiameter::get()`]. Note: If you want to modify the -//! diameter size, do not modify the value directly! Use -//! [`SetWorldBorderSizeEvent`] instead. -//! -//! ## Access other world border properties. -//! Access to the rest of the world border properties is fairly straightforward -//! by querying their respective component. [`WorldBorderBundle`] contains -//! references for all properties of the world border and their respective -//! component +#![doc = include_str!("../README.md")] #![allow(clippy::type_complexity)] #![deny( rustdoc::broken_intra_doc_links, @@ -75,18 +20,12 @@ pub mod packet; -/* -use std::time::{Duration, Instant}; - use bevy_app::prelude::*; -use glam::DVec2; use packet::*; -use valence_client::{Client, FlushPacketsSet}; +use valence_client::{Client, UpdateClientsSet, VisibleChunkLayer}; use valence_core::protocol::encode::WritePacket; -use valence_core::protocol::var_int::VarInt; -use valence_core::protocol::var_long::VarLong; -use valence_entity::EntityLayerId; -use valence_layer::UpdateLayersPreClientSet; +use valence_core::CoreSettings; +use valence_layer::ChunkLayer; use valence_registry::*; // https://minecraft.fandom.com/wiki/World_border @@ -95,318 +34,215 @@ pub const DEFAULT_DIAMETER: f64 = (DEFAULT_PORTAL_LIMIT * 2) as f64; pub const DEFAULT_WARN_TIME: i32 = 15; pub const DEFAULT_WARN_BLOCKS: i32 = 5; -#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct UpdateWorldBorderPerInstanceSet; +pub struct WorldBorderPlugin; #[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct UpdateWorldBorderPerClientSet; - -pub struct WorldBorderPlugin; +pub struct UpdateWorldBorderSet; impl Plugin for WorldBorderPlugin { fn build(&self, app: &mut App) { - app.configure_sets( - PostUpdate, - ( - UpdateWorldBorderPerInstanceSet.before(UpdateLayersPreClientSet), - UpdateWorldBorderPerClientSet.before(FlushPacketsSet), - ), - ) - .add_event::() - .add_systems( - PostUpdate, - ( - wb_size_change.before(diameter_change), - diameter_change, - lerp_transition, - center_change, - warn_time_change, - warn_blocks_change, - portal_teleport_boundary_change, - ) - .in_set(UpdateWorldBorderPerInstanceSet), - ) - .add_systems( - PostUpdate, - border_for_player.in_set(UpdateWorldBorderPerClientSet), - ); + app.configure_set(PostUpdate, UpdateWorldBorderSet.before(UpdateClientsSet)) + .add_systems( + PostUpdate, + ( + init_world_border_for_new_clients, + tick_world_border_lerp, + change_world_border_center, + change_world_border_warning_blocks, + change_world_border_warning_time, + change_world_border_portal_tp_boundary, + ) + .in_set(UpdateWorldBorderSet), + ); } } -/// A bundle contains necessary component to enable world border. -/// This struct implements [`Default`] trait that returns a bundle using -/// Minecraft Vanilla defaults. -#[derive(Bundle)] +/// A bundle containing necessary components to enable world border +/// functionality. Add this to an entity with the [`ChunkLayer`] component. +#[derive(Bundle, Default, Debug)] pub struct WorldBorderBundle { pub center: WorldBorderCenter, - pub diameter: WorldBorderDiameter, + pub lerp: WorldBorderLerp, pub portal_teleport_boundary: WorldBorderPortalTpBoundary, - pub warning_time: WorldBorderWarnTime, - pub warning_blocks: WorldBorderWarnBlocks, - pub moving: MovingWorldBorder, -} - -impl WorldBorderBundle { - /// Create a new world border with specified center and diameter - pub fn new(center: impl Into, diameter: f64) -> Self { - Self { - center: WorldBorderCenter(center.into()), - diameter: WorldBorderDiameter(diameter), - portal_teleport_boundary: WorldBorderPortalTpBoundary(DEFAULT_PORTAL_LIMIT), - warning_time: WorldBorderWarnTime(DEFAULT_WARN_TIME), - warning_blocks: WorldBorderWarnBlocks(DEFAULT_WARN_BLOCKS), - moving: MovingWorldBorder { - old_diameter: diameter, - new_diameter: diameter, - duration: 0, - timestamp: Instant::now(), - }, - } - } -} + pub warn_time: WorldBorderWarnTime, + pub warn_blocks: WorldBorderWarnBlocks, +} + +#[derive(Component, Default, Copy, Clone, PartialEq, Debug)] +pub struct WorldBorderCenter { + pub x: f64, + pub z: f64, +} + +/// Component containing information to linearly interpolate the world border. +/// Contains the world border's diameter. +#[derive(Component, Clone, Copy, Debug)] +pub struct WorldBorderLerp { + /// The current diameter of the world border. This is updated automatically + /// as the remaining ticks count down. + pub current_diameter: f64, + /// The desired diameter of the world border after lerping has finished. + /// Modify this if you want to change the world border diameter. + pub target_diameter: f64, + /// Server ticks until the target diameter is reached. This counts down + /// automatically. + pub remaining_ticks: u64, +} +#[derive(Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub struct WorldBorderWarnTime(pub i32); -impl Default for WorldBorderBundle { +impl Default for WorldBorderWarnTime { fn default() -> Self { - Self::new([0.0, 0.0], DEFAULT_DIAMETER) + Self(DEFAULT_WARN_TIME) } } -#[derive(Component)] -pub struct WorldBorderCenter(pub DVec2); - -#[derive(Component)] -pub struct WorldBorderWarnTime(pub i32); - -#[derive(Component)] +#[derive(Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub struct WorldBorderWarnBlocks(pub i32); -#[derive(Component)] -pub struct WorldBorderPortalTpBoundary(pub i32); - -/// The world border diameter can be read by calling -/// [`WorldBorderDiameter::get()`]. If you want to modify the diameter -/// size, do not modify the value directly! Use [`SetWorldBorderSizeEvent`] -/// instead. -#[derive(Component)] -pub struct WorldBorderDiameter(f64); - -impl WorldBorderDiameter { - pub fn get(&self) -> f64 { - self.0 +impl Default for WorldBorderWarnBlocks { + fn default() -> Self { + Self(DEFAULT_WARN_BLOCKS) } } -/// This component represents the `Set Border Lerp Size` packet with timestamp. -/// It is used for actually lerping the world border diameter. -/// If you need to set the diameter, it is much better to use the -/// [`SetWorldBorderSizeEvent`] event -#[derive(Component)] -pub struct MovingWorldBorder { - pub old_diameter: f64, - pub new_diameter: f64, - /// equivalent to `speed` on wiki.vg - pub duration: i64, - pub timestamp: Instant, -} - -impl MovingWorldBorder { - pub fn current_diameter(&self) -> f64 { - if self.duration == 0 { - self.new_diameter - } else { - let t = self.current_duration() as f64 / self.duration as f64; - lerp(self.new_diameter, self.old_diameter, t) - } - } +#[derive(Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub struct WorldBorderPortalTpBoundary(pub i32); - pub fn current_duration(&self) -> i64 { - let speed = self.duration - self.timestamp.elapsed().as_millis() as i64; - speed.max(0) +impl Default for WorldBorderPortalTpBoundary { + fn default() -> Self { + Self(DEFAULT_PORTAL_LIMIT) } } -/// An event for controlling world border diameter. -/// Setting duration to 0 will move the border to `new_diameter` immediately, -/// otherwise it will interpolate to `new_diameter` over `duration` time. -/// -/// ``` -/// # use bevy_ecs::event::EventWriter; -/// # use valence_world_border::SetWorldBorderSizeEvent; -/// # use core::time::Duration; -/// fn change_diameter( -/// mut event_writer: EventWriter, -/// instance: bevy_ecs::entity::Entity, -/// diameter: f64, -/// duration: Duration, -/// ) { -/// event_writer.send(SetWorldBorderSizeEvent { -/// entity_layer: entity, -/// new_diameter: diameter, -/// duration, -/// }); -/// } -/// ``` -#[derive(Event, Clone, Debug)] -pub struct SetWorldBorderSizeEvent { - /// The [`EntityLayer`] to change border size. Note that this entity layer must contain - /// the [`WorldBorderBundle`] bundle. - pub entity_layer: Entity, - /// The new diameter of the world border - pub new_diameter: f64, - /// How long the border takes to reach it new_diameter in millisecond. Set - /// to 0 to move immediately. - pub duration: Duration, -} - -fn wb_size_change( - mut events: EventReader, - mut instances: Query<(&WorldBorderDiameter, Option<&mut MovingWorldBorder>)>, -) { - for SetWorldBorderSizeEvent { - entity_layer: instance, - new_diameter, - duration, - } in events.iter() - { - let Ok((diameter, mwb_opt)) = instances.get_mut(*instance) else { - continue; - }; - - if let Some(mut mvb) = mwb_opt { - mvb.new_diameter = *new_diameter; - mvb.old_diameter = diameter.get(); - mvb.duration = duration.as_millis() as i64; - mvb.timestamp = Instant::now(); +impl Default for WorldBorderLerp { + fn default() -> Self { + Self { + current_diameter: DEFAULT_DIAMETER, + target_diameter: DEFAULT_DIAMETER, + remaining_ticks: 0, } } } -fn border_for_player( - mut clients: Query<(&mut Client, &EntityLayerId), Changed>, - wbs: Query< - ( - &WorldBorderCenter, - &WorldBorderWarnTime, - &WorldBorderWarnBlocks, - &WorldBorderDiameter, - &WorldBorderPortalTpBoundary, - Option<&MovingWorldBorder>, - ), - With, - >, +fn init_world_border_for_new_clients( + mut clients: Query<(&mut Client, &VisibleChunkLayer), Changed>, + wbs: Query<( + &WorldBorderCenter, + &WorldBorderLerp, + &WorldBorderPortalTpBoundary, + &WorldBorderWarnTime, + &WorldBorderWarnBlocks, + )>, + settings: Res, ) { - for (mut client, location) in clients.iter_mut() { - if let Ok((c, wt, wb, diameter, ptb, wbl)) = wbs.get(location.0) { - let (new_diameter, speed) = if let Some(lerping) = wbl { - (lerping.new_diameter, lerping.current_duration()) - } else { - (diameter.0, 0) - }; + for (mut client, layer) in &mut clients { + if let Ok((center, lerp, portal_tp_boundary, warn_time, warn_blocks)) = wbs.get(layer.0) { + let millis = lerp.remaining_ticks as i64 * 1000 / settings.tick_rate.get() as i64; client.write_packet(&WorldBorderInitializeS2c { - x: c.0.x, - z: c.0.y, - old_diameter: diameter.0, - new_diameter, - portal_teleport_boundary: VarInt(ptb.0), - speed: VarLong(speed), - warning_blocks: VarInt(wb.0), - warning_time: VarInt(wt.0), + x: center.x, + z: center.z, + old_diameter: lerp.current_diameter, + new_diameter: lerp.target_diameter, + duration_millis: millis.into(), + portal_teleport_boundary: portal_tp_boundary.0.into(), + warning_blocks: warn_blocks.0.into(), + warning_time: warn_time.0.into(), }); } } } -fn diameter_change( - mut wbs: Query<(&mut Instance, &MovingWorldBorder), Changed>, +fn tick_world_border_lerp( + mut wbs: Query<(&mut ChunkLayer, &mut WorldBorderLerp)>, + settings: Res, ) { - for (mut ins, lerping) in wbs.iter_mut() { - if lerping.duration == 0 { - ins.write_packet(&WorldBorderSizeChangedS2c { - diameter: lerping.new_diameter, - }) - } else { - ins.write_packet(&WorldBorderInterpolateSizeS2c { - old_diameter: lerping.current_diameter(), - new_diameter: lerping.new_diameter, - speed: VarLong(lerping.current_duration()), - }); + for (mut layer, mut lerp) in &mut wbs { + if lerp.is_changed() { + if lerp.remaining_ticks == 0 { + layer.write_packet(&WorldBorderSizeChangedS2c { + diameter: lerp.target_diameter, + }); + + lerp.current_diameter = lerp.target_diameter; + } else { + let millis = lerp.remaining_ticks as i64 * 1000 / settings.tick_rate.get() as i64; + + layer.write_packet(&WorldBorderInterpolateSizeS2c { + old_diameter: lerp.current_diameter, + new_diameter: lerp.target_diameter, + duration_millis: millis.into(), + }); + } } - } -} -fn lerp_transition(mut wbs: Query<(&mut WorldBorderDiameter, &MovingWorldBorder)>) { - for (mut diameter, moving_wb) in wbs.iter_mut() { - if diameter.0 != moving_wb.new_diameter { - diameter.0 = moving_wb.current_diameter(); + if lerp.remaining_ticks > 0 { + let diff = lerp.target_diameter - lerp.current_diameter; + lerp.current_diameter += diff / lerp.remaining_ticks as f64; + + lerp.remaining_ticks -= 1; } } } -fn center_change(mut wbs: Query<(&mut Instance, &WorldBorderCenter), Changed>) { - for (mut ins, center) in wbs.iter_mut() { - ins.write_packet(&WorldBorderCenterChangedS2c { - x_pos: center.0.x, - z_pos: center.0.y, - }) +fn change_world_border_center( + mut wbs: Query<(&mut ChunkLayer, &WorldBorderCenter), Changed>, +) { + for (mut layer, center) in &mut wbs { + layer.write_packet(&WorldBorderCenterChangedS2c { + x_pos: center.x, + z_pos: center.z, + }); } } -fn warn_time_change( - mut wb_query: Query<(&mut Instance, &WorldBorderWarnTime), Changed>, +fn change_world_border_warning_blocks( + mut wbs: Query<(&mut ChunkLayer, &WorldBorderWarnBlocks), Changed>, ) { - for (mut ins, wt) in wb_query.iter_mut() { - ins.write_packet(&WorldBorderWarningTimeChangedS2c { - warning_time: VarInt(wt.0), - }) + for (mut layer, warn_blocks) in &mut wbs { + layer.write_packet(&WorldBorderWarningBlocksChangedS2c { + warning_blocks: warn_blocks.0.into(), + }); } } -fn warn_blocks_change( - mut wb_query: Query<(&mut Instance, &WorldBorderWarnBlocks), Changed>, +fn change_world_border_warning_time( + mut wbs: Query<(&mut ChunkLayer, &WorldBorderWarnTime), Changed>, ) { - for (mut ins, wb) in wb_query.iter_mut() { - ins.write_packet(&WorldBorderWarningBlocksChangedS2c { - warning_blocks: VarInt(wb.0), - }) + for (mut layer, warn_time) in &mut wbs { + layer.write_packet(&WorldBorderWarningTimeChangedS2c { + warning_time: warn_time.0.into(), + }); } } -fn portal_teleport_boundary_change( +fn change_world_border_portal_tp_boundary( mut wbs: Query< ( - &mut Instance, + &mut ChunkLayer, &WorldBorderCenter, + &WorldBorderLerp, + &WorldBorderPortalTpBoundary, &WorldBorderWarnTime, &WorldBorderWarnBlocks, - &WorldBorderDiameter, - &WorldBorderPortalTpBoundary, - Option<&MovingWorldBorder>, ), Changed, >, + settings: Res, ) { - for (mut ins, c, wt, wb, diameter, ptb, wbl) in wbs.iter_mut() { - let (new_diameter, speed) = if let Some(lerping) = wbl { - (lerping.new_diameter, lerping.current_duration()) - } else { - (diameter.0, 0) - }; - - ins.write_packet(&WorldBorderInitializeS2c { - x: c.0.x, - z: c.0.y, - old_diameter: diameter.0, - new_diameter, - portal_teleport_boundary: VarInt(ptb.0), - speed: VarLong(speed), - warning_blocks: VarInt(wb.0), - warning_time: VarInt(wt.0), + for (mut layer, center, lerp, portal_tp_boundary, warn_time, warn_blocks) in &mut wbs { + let millis = lerp.remaining_ticks as i64 * 1000 / settings.tick_rate.get() as i64; + + layer.write_packet(&WorldBorderInitializeS2c { + x: center.x, + z: center.z, + old_diameter: lerp.current_diameter, + new_diameter: lerp.target_diameter, + duration_millis: millis.into(), + portal_teleport_boundary: portal_tp_boundary.0.into(), + warning_blocks: warn_blocks.0.into(), + warning_time: warn_time.0.into(), }); } } - -fn lerp(start: f64, end: f64, t: f64) -> f64 { - start + (end - start) * t -} -*/ diff --git a/crates/valence_world_border/src/packet.rs b/crates/valence_world_border/src/packet.rs index 30465ab54..ae0b4ab88 100644 --- a/crates/valence_world_border/src/packet.rs +++ b/crates/valence_world_border/src/packet.rs @@ -16,7 +16,7 @@ pub struct WorldBorderInitializeS2c { pub z: f64, pub old_diameter: f64, pub new_diameter: f64, - pub speed: VarLong, + pub duration_millis: VarLong, pub portal_teleport_boundary: VarInt, pub warning_blocks: VarInt, pub warning_time: VarInt, @@ -27,7 +27,7 @@ pub struct WorldBorderInitializeS2c { pub struct WorldBorderInterpolateSizeS2c { pub old_diameter: f64, pub new_diameter: f64, - pub speed: VarLong, + pub duration_millis: VarLong, } #[derive(Clone, Debug, Encode, Decode, Packet)] diff --git a/examples/world_border.rs b/examples/world_border.rs index 52e7d9b40..affc78b91 100644 --- a/examples/world_border.rs +++ b/examples/world_border.rs @@ -1,10 +1,3 @@ -// TODO: fix - -fn main() {} - -/* -use std::time::Duration; - use bevy_app::App; use valence::client::despawn_disconnected_clients; use valence::client::message::ChatMessageEvent; @@ -22,11 +15,10 @@ fn main() { .add_systems( Update, ( - init_clients, despawn_disconnected_clients, - border_center_avg, - border_expand, + init_clients, border_controls, + display_diameter, ), ) .run(); @@ -54,9 +46,16 @@ fn setup( } } - commands - .spawn(layer) - .insert(WorldBorderBundle::new([0.0, 0.0], 1.0)); + commands.spawn(( + layer, + WorldBorderBundle { + lerp: WorldBorderLerp { + target_diameter: 10.0, + ..Default::default() + }, + ..Default::default() + }, + )); } fn init_clients( @@ -64,74 +63,87 @@ fn init_clients( ( &mut Client, &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, &mut Position, &mut Inventory, &HeldItem, ), Added, >, - instances: Query>, + layers: Query>, ) { - for (mut client, mut loc, mut pos, mut inv, main_slot) in &mut clients { - loc.0 = instances.single(); + for ( + mut client, + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut pos, + mut inv, + main_slot, + ) in &mut clients + { + let layer = layers.single(); + + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); pos.set([0.5, SPAWN_Y as f64 + 1.0, 0.5]); let pickaxe = Some(ItemStack::new(ItemKind::WoodenPickaxe, 1, None)); inv.set_slot(main_slot.slot(), pickaxe); - client.send_chat_message("Break block to increase border size!"); + client + .send_chat_message("Use `add` and `center` chat messages to change the world border."); } } -fn border_center_avg( - clients: Query<(&EntityLayerId, &Position)>, - mut instances: Query<(Entity, &mut WorldBorderCenter), With>, -) { - for (entity, mut center) in instances.iter_mut() { - let new_center = { - let (count, x, z) = clients - .iter() - .filter(|(loc, _)| loc.0 == entity) - .fold((0, 0.0, 0.0), |(count, x, z), (_, pos)| { - (count + 1, x + pos.0.x, z + pos.0.z) - }); - - DVec2 { - x: x / count.max(1) as f64, - y: z / count.max(1) as f64, - } - }; - - center.0 = new_center; - } -} - -fn border_expand( - mut events: EventReader, - clients: Query<&EntityLayerId, With>, - wbs: Query<&WorldBorderDiameter, With>, - mut event_writer: EventWriter, -) { - for digging in events.iter().filter(|d| d.state == DiggingState::Stop) { - let Ok(loc) = clients.get(digging.client) else { - continue; - }; - - let Ok(size) = wbs.get(loc.0) else { - continue; - }; - - event_writer.send(SetWorldBorderSizeEvent { - entity_layer: loc.0, - new_diameter: size.get() + 1.0, - duration: Duration::from_secs(1), - }); +// fn border_center_avg( +// clients: Query<(&EntityLayerId, &Position)>, +// mut layers: Query<(Entity, &mut WorldBorderCenter), With>, +// ) { for (entity, mut center) in layers.iter_mut() { let new_center = { let +// (count, x, z) = clients .iter() .filter(|(loc, _)| loc.0 == entity) +// .fold((0, 0.0, 0.0), |(count, x, z), (_, pos)| { (count + 1, x + pos.0.x, z +// + pos.0.z) }); + +// DVec2 { +// x: x / count.max(1) as f64, +// y: z / count.max(1) as f64, +// } +// }; + +// center.0 = new_center; +// } +// } + +// fn border_expand( +// mut events: EventReader, +// clients: Query<&EntityLayerId, With>, +// wbs: Query<&WorldBorderDiameter, With>, +// ) { for digging in events.iter().filter(|d| d.state == DiggingState::Stop) { +// let Ok(loc) = clients.get(digging.client) else { continue; }; + +// let Ok(size) = wbs.get(loc.0) else { +// continue; +// }; + +// event_writer.send(SetWorldBorderSizeEvent { +// entity_layer: loc.0, +// new_diameter: size.get() + 1.0, +// duration: Duration::from_secs(1), +// }); +// } +// } + +fn display_diameter(mut layers: Query<(&mut ChunkLayer, &WorldBorderLerp)>) { + for (mut layer, lerp) in &mut layers { + if lerp.remaining_ticks > 0 { + layer.send_chat_message(format!("diameter = {}", lerp.current_diameter)); + } } } -// Not needed for this demo, but useful for debugging fn border_controls( mut events: EventReader, - mut instances: Query<(Entity, &WorldBorderDiameter, &mut WorldBorderCenter), With>, - mut event_writer: EventWriter, + mut layers: Query<(&mut WorldBorderCenter, &mut WorldBorderLerp), With>, ) { for x in events.iter() { let parts: Vec<&str> = x.message.split(' ').collect(); @@ -141,19 +153,14 @@ fn border_controls( return; }; - let Ok(speed) = parts[2].parse::() else { + let Ok(ticks) = parts[2].parse::() else { return; }; - let Ok((entity, diameter, _)) = instances.get_single_mut() else { - return; - }; + let (_, mut lerp) = layers.single_mut(); - event_writer.send(SetWorldBorderSizeEvent { - entity_layer: entity, - new_diameter: diameter.get() + value, - duration: Duration::from_millis(speed as u64), - }) + lerp.target_diameter = lerp.current_diameter + value; + lerp.remaining_ticks = ticks; } "center" => { let Ok(x) = parts[1].parse::() else { @@ -164,10 +171,11 @@ fn border_controls( return; }; - instances.single_mut().2 .0 = DVec2 { x, y: z }; + let (mut center, _) = layers.single_mut(); + center.x = x; + center.z = z; } _ => (), } } } -*/ diff --git a/src/lib.rs b/src/lib.rs index fd18eac96..0e575098c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -186,11 +186,10 @@ impl PluginGroup for DefaultPlugins { group = group.add(valence_weather::WeatherPlugin); } - // TODO - // #[cfg(feature = "world_border")] - // { - // group = group.add(valence_world_border::WorldBorderPlugin); - // } + #[cfg(feature = "world_border")] + { + group = group.add(valence_world_border::WorldBorderPlugin); + } #[cfg(feature = "boss_bar")] { From c9d5f336902f8f4875f1703f4658046996a6e9f3 Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 31 Jul 2023 00:06:20 -0700 Subject: [PATCH 33/47] remove dead code from example --- examples/world_border.rs | 37 ------------------------------------- 1 file changed, 37 deletions(-) diff --git a/examples/world_border.rs b/examples/world_border.rs index affc78b91..e831a302d 100644 --- a/examples/world_border.rs +++ b/examples/world_border.rs @@ -96,43 +96,6 @@ fn init_clients( } } -// fn border_center_avg( -// clients: Query<(&EntityLayerId, &Position)>, -// mut layers: Query<(Entity, &mut WorldBorderCenter), With>, -// ) { for (entity, mut center) in layers.iter_mut() { let new_center = { let -// (count, x, z) = clients .iter() .filter(|(loc, _)| loc.0 == entity) -// .fold((0, 0.0, 0.0), |(count, x, z), (_, pos)| { (count + 1, x + pos.0.x, z -// + pos.0.z) }); - -// DVec2 { -// x: x / count.max(1) as f64, -// y: z / count.max(1) as f64, -// } -// }; - -// center.0 = new_center; -// } -// } - -// fn border_expand( -// mut events: EventReader, -// clients: Query<&EntityLayerId, With>, -// wbs: Query<&WorldBorderDiameter, With>, -// ) { for digging in events.iter().filter(|d| d.state == DiggingState::Stop) { -// let Ok(loc) = clients.get(digging.client) else { continue; }; - -// let Ok(size) = wbs.get(loc.0) else { -// continue; -// }; - -// event_writer.send(SetWorldBorderSizeEvent { -// entity_layer: loc.0, -// new_diameter: size.get() + 1.0, -// duration: Duration::from_secs(1), -// }); -// } -// } - fn display_diameter(mut layers: Query<(&mut ChunkLayer, &WorldBorderLerp)>) { for (mut layer, lerp) in &mut layers { if lerp.remaining_ticks > 0 { From ad7d2b28e77d3ae4588aaac43ab82c35265453ae Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 1 Aug 2023 00:10:58 -0700 Subject: [PATCH 34/47] Update `valence_anvil` --- crates/valence_anvil/src/lib.rs | 24 +++--- crates/valence_client/src/lib.rs | 9 +- examples/anvil_loading.rs | 39 ++++----- src/tests.rs | 1 - src/tests/world_border.rs | 137 ------------------------------- 5 files changed, 35 insertions(+), 175 deletions(-) delete mode 100644 src/tests/world_border.rs diff --git a/crates/valence_anvil/src/lib.rs b/crates/valence_anvil/src/lib.rs index 74fc4a8a8..ea718e62e 100644 --- a/crates/valence_anvil/src/lib.rs +++ b/crates/valence_anvil/src/lib.rs @@ -34,12 +34,12 @@ use flume::{Receiver, Sender}; use lru::LruCache; use tracing::warn; use valence_biome::{BiomeId, BiomeRegistry}; -use valence_client::{Client, OldView, UpdateClientsSet, View}; +use valence_client::{Client, OldView, View}; use valence_core::chunk_pos::ChunkPos; use valence_core::ident::Ident; use valence_entity::{EntityLayerId, OldEntityLayerId}; use valence_layer::chunk::UnloadedChunk; -use valence_layer::ChunkLayer; +use valence_layer::{ChunkLayer, UpdateLayersPreClientSet}; use valence_nbt::Compound; mod parse_chunk; @@ -280,7 +280,7 @@ impl Plugin for AnvilPlugin { PostUpdate, (init_anvil, update_client_views, send_recv_chunks) .chain() - .before(UpdateClientsSet), + .before(UpdateLayersPreClientSet), ); } } @@ -358,11 +358,11 @@ fn update_client_views( } fn send_recv_chunks( - mut instances: Query<(Entity, &mut ChunkLayer, &mut AnvilLevel)>, + mut layers: Query<(Entity, &mut ChunkLayer, &mut AnvilLevel)>, mut to_send: Local>, mut load_events: EventWriter, ) { - for (entity, mut layer, anvil) in &mut instances { + for (entity, mut layer, anvil) in &mut layers { let anvil = anvil.into_inner(); // Insert the chunks that are finished loading into the chunk layer and send @@ -380,7 +380,7 @@ fn send_recv_chunks( }; load_events.send(ChunkLoadEvent { - instance: entity, + chunk_layer: entity, pos, status, }); @@ -424,16 +424,16 @@ fn anvil_worker(mut state: ChunkWorkerState) { /// An event sent by `valence_anvil` after an attempt to load a chunk is made. #[derive(Event, Debug)] pub struct ChunkLoadEvent { - /// The [`Instance`] where the chunk is located. - pub instance: Entity, - /// The position of the chunk in the instance. + /// The [`ChunkLayer`] where the chunk is located. + pub chunk_layer: Entity, + /// The position of the chunk in the layer. pub pos: ChunkPos, pub status: ChunkLoadStatus, } #[derive(Debug)] pub enum ChunkLoadStatus { - /// A new chunk was successfully loaded and inserted into the instance. + /// A new chunk was successfully loaded and inserted into the layer. Success { /// The time this chunk was last modified, measured in seconds since the /// epoch. @@ -446,10 +446,10 @@ pub enum ChunkLoadStatus { Failed(anyhow::Error), } -/// An event sent by `valence_anvil` when a chunk is unloaded from an instance. +/// An event sent by `valence_anvil` when a chunk is unloaded from an layer. #[derive(Event, Debug)] pub struct ChunkUnloadEvent { - /// The [`Instance`] where the chunk was unloaded. + /// The [`ChunkLayer`] where the chunk was unloaded. pub chunk_layer: Entity, /// The position of the chunk that was unloaded. pub pos: ChunkPos, diff --git a/crates/valence_client/src/lib.rs b/crates/valence_client/src/lib.rs index ad7ae9c22..70762026f 100644 --- a/crates/valence_client/src/lib.rs +++ b/crates/valence_client/src/lib.rs @@ -770,17 +770,18 @@ fn handle_layer_messages( [ChunkLayer::LOAD, .., ChunkLayer::UNLOAD] => { // Chunk is being loaded and unloaded on the // same tick, so there's no need to do anything. + debug_assert!(chunk_layer.chunk(pos).is_none()); } [.., ChunkLayer::LOAD | ChunkLayer::OVERWRITE] => { // Load chunk. - if let Some(chunk) = chunk_layer.chunk(pos) { - chunk.write_init_packets(&mut *client, pos, chunk_layer.info()); - chunk.inc_viewer_count(); - } + let chunk = chunk_layer.chunk(pos).expect("chunk must exist"); + chunk.write_init_packets(&mut *client, pos, chunk_layer.info()); + chunk.inc_viewer_count(); } [.., ChunkLayer::UNLOAD] => { // Unload chunk. client.write_packet(&UnloadChunkS2c { pos }); + debug_assert!(chunk_layer.chunk(pos).is_none()); } _ => unreachable!("invalid message data while changing chunk state"), } diff --git a/examples/anvil_loading.rs b/examples/anvil_loading.rs index c6c3e6bf6..dba2a68c7 100644 --- a/examples/anvil_loading.rs +++ b/examples/anvil_loading.rs @@ -1,9 +1,5 @@ -// TODO - -fn main() {} - -/* use std::path::PathBuf; + use clap::Parser; use valence::prelude::*; use valence_anvil::{AnvilLevel, ChunkLoadEvent, ChunkLoadStatus}; @@ -56,7 +52,7 @@ fn setup( server: Res, cli: Res, ) { - let instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + let layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); let mut level = AnvilLevel::new(&cli.path, &biomes); // Force a 16x16 area of chunks around the origin to be loaded at all times. @@ -71,7 +67,7 @@ fn setup( } } - commands.spawn((instance, level)); + commands.spawn((layer, level)); } fn init_clients( @@ -100,16 +96,16 @@ fn init_clients( layer_id.0 = layer; visible_chunk_layer.0 = layer; visible_entity_layers.0.insert(layer); - pos.set([0.5, 65.0, 0.5]); - *game_mode = GameMode::Creative; + pos.set(SPAWN_POS); + *game_mode = GameMode::Spectator; } } fn handle_chunk_loads( mut events: EventReader, - mut instances: Query<&mut Instance, With>, + mut layers: Query<&mut ChunkLayer, With>, ) { - let mut inst = instances.single_mut(); + let mut layer = layers.single_mut(); for event in events.iter() { match &event.status { @@ -119,7 +115,7 @@ fn handle_chunk_loads( ChunkLoadStatus::Empty => { // There's no chunk here so let's insert an empty chunk. If we were doing // terrain generation we would prepare that here. - inst.insert_chunk(event.pos, UnloadedChunk::new()); + layer.insert_chunk(event.pos, UnloadedChunk::new()); } ChunkLoadStatus::Failed(e) => { // Something went wrong. @@ -129,25 +125,26 @@ fn handle_chunk_loads( ); eprintln!("{errmsg}"); - inst.send_chat_message(errmsg.color(Color::RED)); + layer.send_chat_message(errmsg.color(Color::RED)); - inst.insert_chunk(event.pos, UnloadedChunk::new()); + layer.insert_chunk(event.pos, UnloadedChunk::new()); } } } } // Display the number of loaded chunks in the action bar of all clients. -fn display_loaded_chunk_count(mut instances: Query<&mut Instance>, mut last_count: Local) { - let mut inst = instances.single_mut(); +fn display_loaded_chunk_count(mut layers: Query<&mut ChunkLayer>, mut last_count: Local) { + let mut layer = layers.single_mut(); - let cnt = inst.chunks().count(); + let cnt = layer.chunks().count(); if *last_count != cnt { *last_count = cnt; - inst.send_action_bar_message( - "Chunk Count: ".into_text() + (cnt as i32).color(Color::LIGHT_PURPLE), - ); + layer.send_action_bar_message("Chunk Count: ".into_text() + cnt.color(Color::LIGHT_PURPLE)); + } + + for (pos, chunk) in layer.chunks() { + assert_eq!(chunk.viewer_count(), 1, "bad! {pos:?}"); } } -*/ diff --git a/src/tests.rs b/src/tests.rs index 2a41a8315..90da2581b 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -4,4 +4,3 @@ mod example; mod inventory; mod layer; mod player_list; -mod world_border; diff --git a/src/tests/world_border.rs b/src/tests/world_border.rs deleted file mode 100644 index ac8ea58fc..000000000 --- a/src/tests/world_border.rs +++ /dev/null @@ -1,137 +0,0 @@ -// TODO: fix - -/* -use std::time::Duration; - -use bevy_app::App; -use valence_entity::EntityLayerId; -use valence_instance::Instance; -use valence_registry::{Entity, Mut}; -use valence_world_border::packet::*; -use valence_world_border::*; - -use crate::testing::{create_mock_client, MockClientHelper}; - -#[test] -fn test_intialize_on_join() { - let mut app = App::new(); - let (_, instance_ent) = prepare(&mut app); - - let (client, mut client_helper) = create_mock_client("test"); - let client_ent = app.world.spawn(client).id(); - - app.world.get_mut::(client_ent).unwrap().0 = instance_ent; - app.update(); - - client_helper - .collect_received() - .assert_count::(1); -} - -#[test] -fn test_resizing() { - let mut app = App::new(); - let (mut client_helper, instance_ent) = prepare(&mut app); - - app.world.send_event(SetWorldBorderSizeEvent { - new_diameter: 20.0, - duration: Duration::ZERO, - instance: instance_ent, - }); - - app.update(); - let frames = client_helper.collect_received(); - frames.assert_count::(1); -} - -#[test] -fn test_center() { - let mut app = App::new(); - let (mut client_helper, instance_ent) = prepare(&mut app); - - let mut ins_mut = app.world.entity_mut(instance_ent); - let mut center: Mut = ins_mut - .get_mut() - .expect("Expect world border to be present!"); - center.0 = [10.0, 10.0].into(); - - app.update(); - let frames = client_helper.collect_received(); - frames.assert_count::(1); -} - -#[test] -fn test_warn_time() { - let mut app = App::new(); - let (mut client_helper, instance_ent) = prepare(&mut app); - - let mut ins_mut = app.world.entity_mut(instance_ent); - let mut wt: Mut = ins_mut - .get_mut() - .expect("Expect world border to be present!"); - wt.0 = 100; - app.update(); - - let frames = client_helper.collect_received(); - frames.assert_count::(1); -} - -#[test] -fn test_warn_blocks() { - let mut app = App::new(); - let (mut client_helper, instance_ent) = prepare(&mut app); - - let mut ins_mut = app.world.entity_mut(instance_ent); - let mut wb: Mut = ins_mut - .get_mut() - .expect("Expect world border to be present!"); - wb.0 = 100; - app.update(); - - let frames = client_helper.collect_received(); - frames.assert_count::(1); -} - -#[test] -fn test_portal_tp_boundary() { - let mut app = App::new(); - let (mut client_helper, instance_ent) = prepare(&mut app); - - let mut ins_mut = app.world.entity_mut(instance_ent); - let mut tp: Mut = ins_mut - .get_mut() - .expect("Expect world border to be present!"); - tp.0 = 100; - app.update(); - - let frames = client_helper.collect_received(); - frames.assert_count::(1); -} - -fn prepare(app: &mut App) -> (MockClientHelper, Entity) { - let (_, mut client_helper) = scenario_single_client(app); - - // Process a tick to get past the "on join" logic. - app.update(); - client_helper.clear_received(); - - // Get the instance entity. - let instance_ent = app - .world - .iter_entities() - .find(|e| e.contains::()) - .expect("could not find instance") - .id(); - - // Insert a the world border bundle to the instance. - app.world - .entity_mut(instance_ent) - .insert(WorldBorderBundle::new([0.0, 0.0], 10.0)); - for _ in 0..2 { - app.update(); - } - - client_helper.clear_received(); - (client_helper, instance_ent) -} -*/ From e3566569c6c4518f820ba2d85931e8639f641d9d Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 1 Aug 2023 02:46:24 -0700 Subject: [PATCH 35/47] Add layer switching and chunk view count tests --- src/tests/layer.rs | 190 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 186 insertions(+), 4 deletions(-) diff --git a/src/tests/layer.rs b/src/tests/layer.rs index 86baee14a..5a55882ce 100644 --- a/src/tests/layer.rs +++ b/src/tests/layer.rs @@ -2,9 +2,11 @@ use std::collections::BTreeSet; use bevy_ecs::world::EntityMut; use valence_block::BlockState; -use valence_client::ViewDistance; +use valence_client::{ViewDistance, VisibleEntityLayers}; use valence_core::chunk_pos::ChunkView; +use valence_core::despawn::Despawned; use valence_core::protocol::Packet; +use valence_core::Server; use valence_entity::cow::CowEntityBundle; use valence_entity::packet::{EntitiesDestroyS2c, EntitySpawnS2c, MoveRelativeS2c}; use valence_entity::{EntityLayerId, Position}; @@ -12,7 +14,7 @@ use valence_layer::chunk::UnloadedChunk; use valence_layer::packet::{ BlockEntityUpdateS2c, ChunkDataS2c, ChunkDeltaUpdateS2c, UnloadChunkS2c, }; -use valence_layer::ChunkLayer; +use valence_layer::{ChunkLayer, EntityLayer}; use crate::testing::ScenarioSingleClient; @@ -146,6 +148,186 @@ fn layer_chunk_view_change() { } } +#[test] +fn chunk_viewer_count() { + let ScenarioSingleClient { + mut app, + client: client_ent, + mut helper, + layer: layer_ent, + } = ScenarioSingleClient::new(); + + let mut client = app.world.entity_mut(client_ent); + + client.get_mut::().unwrap().set([8.0, 64.0, 8.0]); + client.get_mut::().unwrap().set(2); + + let mut layer = app.world.get_mut::(layer_ent).unwrap(); + + // Create chunk at (0, 0). + layer.insert_chunk([0, 0], UnloadedChunk::new()); + + app.update(); // Tick. + + helper.collect_received().assert_count::(1); + + let mut layer = app.world.get_mut::(layer_ent).unwrap(); + + assert_eq!(layer.chunk_mut([0, 0]).unwrap().viewer_count(), 1); + + // Create new chunk next to the first chunk and move the client away from it on + // the same tick. + layer.insert_chunk([0, 1], UnloadedChunk::new()); + + let mut client = app.world.entity_mut(client_ent); + client.get_mut::().unwrap().set([100.0, 0.0, 0.0]); + + app.update(); // Tick. + + { + let recvd = helper.collect_received(); + + recvd.assert_count::(1); + recvd.assert_count::(2); + } + + let mut layer = app.world.get_mut::(layer_ent).unwrap(); + + // Viewer count of both chunks should be zero. + assert_eq!(layer.chunk([0, 0]).unwrap().viewer_count(), 0); + assert_eq!(layer.chunk([0, 1]).unwrap().viewer_count(), 0); + + // Create a third chunk adjacent to the others. + layer.insert_chunk([1, 0], UnloadedChunk::new()); + + // Move the client back in view of all three chunks. + let mut client = app.world.entity_mut(client_ent); + client.get_mut::().unwrap().set([8.0, 0.0, 8.0]); + + app.update(); // Tick. + + let mut layer = app.world.get_mut::(layer_ent).unwrap(); + + // All three chunks should have viewer count of one. + assert_eq!(layer.chunk_mut([0, 0]).unwrap().viewer_count(), 1); + assert_eq!(layer.chunk_mut([1, 0]).unwrap().viewer_count(), 1); + assert_eq!(layer.chunk_mut([0, 1]).unwrap().viewer_count(), 1); + + // Client should have received load packet for all three. + helper.collect_received().assert_count::(3); +} + +#[test] +fn entity_layer_switching() { + let ScenarioSingleClient { + mut app, + client: client_ent, + mut helper, + layer: l1, + } = ScenarioSingleClient::new(); + + let server = app.world.resource::(); + + let l2 = EntityLayer::new(server); + let l3 = EntityLayer::new(server); + + let l2 = app.world.spawn(l2).id(); + let _l3 = app.world.spawn(l3).id(); + + // Spawn three entities and put them all on the main layer to start. + + let e1 = CowEntityBundle { + layer: EntityLayerId(l1), + ..Default::default() + }; + + let e2 = CowEntityBundle { + layer: EntityLayerId(l1), + ..Default::default() + }; + + let e3 = CowEntityBundle { + layer: EntityLayerId(l1), + ..Default::default() + }; + + let e1 = app.world.spawn(e1).id(); + let _e2 = app.world.spawn(e2).id(); + let _e3 = app.world.spawn(e3).id(); + + app.update(); // Tick. + + // Can the client see all the new entities? + helper.collect_received().assert_count::(3); + + // Move e1 to l2 and add l2 to the visible layers set. + app.world.get_mut::(e1).unwrap().0 = l2; + app.world + .get_mut::(client_ent) + .unwrap() + .0 + .insert(l2); + + app.update(); // Tick. + + { + let recvd = helper.collect_received(); + + // Client received packets to despawn and then spawn the entity in the new + // layer. (this could be optimized away in the future) + recvd.assert_count::(1); + recvd.assert_count::(1); + recvd.assert_order::<(EntitiesDestroyS2c, EntitySpawnS2c)>(); + } + + // Remove the original layer from the visible layer set. + assert!(app + .world + .get_mut::(client_ent) + .unwrap() + .0 + .remove(&l1)); + + app.update(); // Tick. + + // Both entities on the original layer should be removed. + { + let recvd = helper.collect_received(); + recvd.assert_count::(1); + } + + // Despawn l2. + app.world.entity_mut(l2).insert(Despawned); + + app.update(); // Tick. + + // e1 should be removed. + helper + .collect_received() + .assert_count::(1); + + let mut visible_entity_layers = app + .world + .get_mut::(client_ent) + .unwrap(); + + // l2 should be automatically removed from the visible layers set. + assert!(!visible_entity_layers.0.contains(&e1)); + + // Add back the original layer. + assert!(visible_entity_layers.0.insert(l1)); + + app.update(); // Tick. + + // e2 and e3 should be spawned. + + { + let recvd = helper.collect_received(); + + recvd.assert_count::(2); + } +} + #[test] fn chunk_entity_spawn_despawn() { let ScenarioSingleClient { @@ -155,10 +337,10 @@ fn chunk_entity_spawn_despawn() { layer: layer_ent, } = ScenarioSingleClient::new(); - let mut chunks = app.world.get_mut::(layer_ent).unwrap(); + let mut layer = app.world.get_mut::(layer_ent).unwrap(); // Insert an empty chunk at (0, 0). - chunks.insert_chunk([0, 0], UnloadedChunk::new()); + layer.insert_chunk([0, 0], UnloadedChunk::new()); // Put an entity in the new chunk. let cow_ent = app From 7065023531c51e8b4a92ca408e3a9fbc5d48697e Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 1 Aug 2023 03:34:33 -0700 Subject: [PATCH 36/47] fix clippy --- examples/boss_bar.rs | 2 ++ examples/entity_hitbox.rs | 2 ++ examples/world_border.rs | 2 ++ 3 files changed, 6 insertions(+) diff --git a/examples/boss_bar.rs b/examples/boss_bar.rs index db4c950dd..f6858dfa1 100644 --- a/examples/boss_bar.rs +++ b/examples/boss_bar.rs @@ -1,3 +1,5 @@ +#![allow(clippy::type_complexity)] + use rand::seq::SliceRandom; use valence::prelude::*; use valence_boss_bar::{ diff --git a/examples/entity_hitbox.rs b/examples/entity_hitbox.rs index 7be49ab94..bff7e4b4d 100644 --- a/examples/entity_hitbox.rs +++ b/examples/entity_hitbox.rs @@ -1,3 +1,5 @@ +#![allow(clippy::type_complexity)] + use std::collections::HashMap; use bevy_app::App; diff --git a/examples/world_border.rs b/examples/world_border.rs index e831a302d..3264ee396 100644 --- a/examples/world_border.rs +++ b/examples/world_border.rs @@ -1,3 +1,5 @@ +#![allow(clippy::type_complexity)] + use bevy_app::App; use valence::client::despawn_disconnected_clients; use valence::client::message::ChatMessageEvent; From 0b0306e649903aa1ad7df1063438d057fa6d34b0 Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 1 Aug 2023 04:52:30 -0700 Subject: [PATCH 37/47] Fix chunk view bounding box bug --- crates/valence_core/src/chunk_pos.rs | 39 ++++++++++++++-------------- examples/anvil_loading.rs | 4 --- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/crates/valence_core/src/chunk_pos.rs b/crates/valence_core/src/chunk_pos.rs index 78ed6c605..88db14dbd 100644 --- a/crates/valence_core/src/chunk_pos.rs +++ b/crates/valence_core/src/chunk_pos.rs @@ -99,8 +99,7 @@ impl ChunkView { } /// Returns an iterator over all the chunk positions in this view. Positions - /// are sorted by the distance to [`pos`](Self::pos), with closer positions - /// appearing first. + /// are sorted by the distance to [`pos`](Self::pos) in ascending order. pub fn iter(self) -> impl DoubleEndedIterator + ExactSizeIterator + Clone { CHUNK_VIEW_LUT[self.dist as usize] .iter() @@ -112,20 +111,33 @@ impl ChunkView { /// Returns an iterator over all the chunk positions in `self`, excluding /// the positions that overlap with `other`. Positions are sorted by the - /// distance to [`pos`](Self::pos), with closer positions - /// appearing first. + /// distance to [`pos`](Self::pos) in ascending order. pub fn diff(self, other: Self) -> impl DoubleEndedIterator + Clone { self.iter().filter(move |&p| !other.contains(p)) } - /// Returns a `(min, max)` tuple of a tight bounding box containing this - /// view. + /// Returns a `(min, max)` tuple describing the tight axis-aligned bounding + /// box for this view. All chunk positions in the view are contained in the + /// bounding box. + /// + /// # Examples + /// + /// ``` + /// use valence_core::chunk_pos::{ChunkPos, ChunkView}; + /// + /// let view = ChunkView::new(ChunkPos::new(5, -4), 16); + /// let (min, max) = view.bounding_box(); + /// + /// for pos in view.iter() { + /// assert!(pos.x >= min.x && pos.x <= max.x && pos.z >= min.z && pos.z <= max.z); + /// } + /// ``` pub fn bounding_box(self) -> (ChunkPos, ChunkPos) { let r = self.dist as i32 + EXTRA_VIEW_RADIUS; ( ChunkPos::new(self.pos.x - r, self.pos.z - r), - ChunkPos::new(self.pos.x + r, self.pos.x + r), + ChunkPos::new(self.pos.x + r, self.pos.z + r), ) } } @@ -156,17 +168,4 @@ mod tests { assert_eq!(ChunkPos::from(<(i32, i32)>::from(p)), p); assert_eq!(ChunkPos::from(<[i32; 2]>::from(p)), p); } - - #[test] - fn view_bounding_box() { - let view = ChunkView::new(ChunkPos::new(5, -4), 32); - - let (min, max) = view.bounding_box(); - - for z in min.z..=max.z { - for x in min.x..=max.x { - assert!(view.contains(ChunkPos::new(x, z))); - } - } - } } diff --git a/examples/anvil_loading.rs b/examples/anvil_loading.rs index dba2a68c7..86f96c8b2 100644 --- a/examples/anvil_loading.rs +++ b/examples/anvil_loading.rs @@ -143,8 +143,4 @@ fn display_loaded_chunk_count(mut layers: Query<&mut ChunkLayer>, mut last_count *last_count = cnt; layer.send_action_bar_message("Chunk Count: ".into_text() + cnt.color(Color::LIGHT_PURPLE)); } - - for (pos, chunk) in layer.chunks() { - assert_eq!(chunk.viewer_count(), 1, "bad! {pos:?}"); - } } From e551fa6c010ed99931fd751bec843fb3404e4be7 Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 1 Aug 2023 04:59:22 -0700 Subject: [PATCH 38/47] Fix docs --- crates/valence_weather/README.md | 3 +++ crates/valence_weather/src/lib.rs | 1 + src/testing.rs | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 crates/valence_weather/README.md diff --git a/crates/valence_weather/README.md b/crates/valence_weather/README.md new file mode 100644 index 000000000..fe5b850c4 --- /dev/null +++ b/crates/valence_weather/README.md @@ -0,0 +1,3 @@ +# valence_weather + +Support for weather effects in layers. (rain, thunder, etc.) diff --git a/crates/valence_weather/src/lib.rs b/crates/valence_weather/src/lib.rs index f94eec8e3..7c340706f 100644 --- a/crates/valence_weather/src/lib.rs +++ b/crates/valence_weather/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] #![allow(clippy::type_complexity)] #![deny( rustdoc::broken_intra_doc_links, diff --git a/src/testing.rs b/src/testing.rs index 7323fac31..f61a53dc0 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -25,7 +25,7 @@ use crate::DefaultPlugins; pub struct ScenarioSingleClient { /// The new bevy application. pub app: App, - /// Entity handle of the single [`Client`]. + /// Entity handle for the single client. pub client: Entity, /// Helper for sending and receiving packets from the mock client. pub helper: MockClientHelper, From 3eaff91687146bf00fb548f37ba1dd7c67e1af73 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Tue, 1 Aug 2023 09:09:03 -0400 Subject: [PATCH 39/47] Capture the Flag Example Using Entity Layers (#426) # Objective Created to test out the visibility layer API in #424 requires #424 # Solution Added a `ctf` example, where only the players on your team are glowing. Currently somewhat bugged. --- Cargo.toml | 2 +- crates/valence_core/src/block_pos.rs | 25 + crates/valence_entity/src/lib.rs | 15 +- crates/valence_inventory/src/lib.rs | 56 +- examples/ctf.rs | 1028 ++++++++++++++++++++++++++ 5 files changed, 1121 insertions(+), 5 deletions(-) create mode 100644 examples/ctf.rs diff --git a/Cargo.toml b/Cargo.toml index e49c7905f..72de59782 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,7 +64,7 @@ anyhow.workspace = true clap.workspace = true criterion.workspace = true flume.workspace = true -noise.workspace = true # For the terrain example. +noise.workspace = true # For the terrain example. tracing.workspace = true [dev-dependencies.reqwest] diff --git a/crates/valence_core/src/block_pos.rs b/crates/valence_core/src/block_pos.rs index fd44d6823..f14024e69 100644 --- a/crates/valence_core/src/block_pos.rs +++ b/crates/valence_core/src/block_pos.rs @@ -1,4 +1,5 @@ use std::io::Write; +use std::ops::{Add, Sub}; use anyhow::bail; use glam::DVec3; @@ -108,6 +109,30 @@ impl From for [i32; 3] { } } +impl Add for BlockPos { + type Output = BlockPos; + + fn add(self, rhs: Self) -> Self::Output { + Self { + x: self.x + rhs.x, + y: self.y + rhs.y, + z: self.z + rhs.z, + } + } +} + +impl Sub for BlockPos { + type Output = BlockPos; + + fn sub(self, rhs: Self) -> Self::Output { + Self { + x: self.x - rhs.x, + y: self.y - rhs.y, + z: self.z - rhs.z, + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/valence_entity/src/lib.rs b/crates/valence_entity/src/lib.rs index 54dbf0c77..475d7bd54 100644 --- a/crates/valence_entity/src/lib.rs +++ b/crates/valence_entity/src/lib.rs @@ -270,9 +270,18 @@ impl PartialEq for OldPosition { /// Describes the direction an entity is looking using pitch and yaw angles. #[derive(Component, Copy, Clone, PartialEq, Default, Debug)] pub struct Look { - /// The yaw angle in degrees. + /// The yaw angle in degrees, where: + /// - `-90` is looking east (towards positive x). + /// - `0` is looking south (towards positive z). + /// - `90` is looking west (towards negative x). + /// - `180` is looking north (towards negative z). + /// + /// Values -180 to 180 are also valid. pub yaw: f32, - /// The pitch angle in degrees. + /// The pitch angle in degrees, where: + /// - `-90` is looking straight up. + /// - `0` is looking straight ahead. + /// - `90` is looking straight down. pub pitch: f32, } @@ -367,7 +376,7 @@ impl EntityStatuses { } } -#[derive(Component, Default, Debug)] +#[derive(Component, Default, Debug, Copy, Clone)] pub struct EntityAnimations(pub u8); impl EntityAnimations { diff --git a/crates/valence_inventory/src/lib.rs b/crates/valence_inventory/src/lib.rs index cf7f58247..37fbce870 100644 --- a/crates/valence_inventory/src/lib.rs +++ b/crates/valence_inventory/src/lib.rs @@ -35,7 +35,7 @@ use valence_client::event_loop::{EventLoopPreUpdate, PacketEvent}; use valence_client::packet::{PlayerAction, PlayerActionC2s}; use valence_client::{Client, FlushPacketsSet, SpawnClientsSet}; use valence_core::game_mode::GameMode; -use valence_core::item::ItemStack; +use valence_core::item::{ItemKind, ItemStack}; use valence_core::protocol::encode::WritePacket; use valence_core::protocol::var_int::VarInt; use valence_core::text::{IntoText, Text}; @@ -316,9 +316,63 @@ impl Inventory { /// inv.set_slot(3, ItemStack::new(ItemKind::IronIngot, 1, None)); /// assert_eq!(inv.first_empty_slot(), Some(1)); /// ``` + #[inline] pub fn first_empty_slot(&self) -> Option { self.first_empty_slot_in(0..self.slot_count()) } + + /// Returns the first slot with the given [`ItemKind`] in the inventory + /// where `count() < stack_max`, or `None` if there are no empty slots. + /// ``` + /// # use valence_inventory::*; + /// # use valence_core::item::*; + /// let mut inv = Inventory::new(InventoryKind::Generic9x1); + /// inv.set_slot(0, ItemStack::new(ItemKind::Diamond, 1, None)); + /// inv.set_slot(2, ItemStack::new(ItemKind::GoldIngot, 64, None)); + /// inv.set_slot(3, ItemStack::new(ItemKind::IronIngot, 1, None)); + /// inv.set_slot(4, ItemStack::new(ItemKind::GoldIngot, 1, None)); + /// assert_eq!( + /// inv.first_slot_with_item_in(ItemKind::GoldIngot, 64, 0..5), + /// Some(4) + /// ); + /// ``` + pub fn first_slot_with_item_in( + &self, + item: ItemKind, + stack_max: u8, + mut range: Range, + ) -> Option { + assert!( + (0..=self.slot_count()).contains(&range.start) + && (0..=self.slot_count()).contains(&range.end), + "slot range out of range" + ); + assert!(stack_max > 0, "stack_max must be greater than 0"); + + range.find(|&idx| { + self.slots[idx as usize] + .as_ref() + .map(|stack| stack.item == item && stack.count() < stack_max) + .unwrap_or(false) + }) + } + + /// Returns the first slot with the given [`ItemKind`] in the inventory + /// where `count() < stack_max`, or `None` if there are no empty slots. + /// ``` + /// # use valence_inventory::*; + /// # use valence_core::item::*; + /// let mut inv = Inventory::new(InventoryKind::Generic9x1); + /// inv.set_slot(0, ItemStack::new(ItemKind::Diamond, 1, None)); + /// inv.set_slot(2, ItemStack::new(ItemKind::GoldIngot, 64, None)); + /// inv.set_slot(3, ItemStack::new(ItemKind::IronIngot, 1, None)); + /// inv.set_slot(4, ItemStack::new(ItemKind::GoldIngot, 1, None)); + /// assert_eq!(inv.first_slot_with_item(ItemKind::GoldIngot, 64), Some(4)); + /// ``` + #[inline] + pub fn first_slot_with_item(&self, item: ItemKind, stack_max: u8) -> Option { + self.first_slot_with_item_in(item, stack_max, 0..self.slot_count()) + } } /// Miscellaneous inventory data. diff --git a/examples/ctf.rs b/examples/ctf.rs new file mode 100644 index 000000000..df5254754 --- /dev/null +++ b/examples/ctf.rs @@ -0,0 +1,1028 @@ +#![allow(clippy::type_complexity)] + +use std::collections::HashMap; + +use bevy_ecs::query::WorldQuery; +use glam::Vec3Swizzles; +use tracing::debug; +use valence::entity::EntityStatuses; +use valence::inventory::HeldItem; +use valence::nbt::{compound, List}; +use valence::prelude::*; +use valence_client::interact_block::InteractBlockEvent; +use valence_client::message::SendMessage; +use valence_client::status::RequestRespawnEvent; +use valence_entity::cow::CowEntityBundle; +use valence_entity::entity::Flags; +use valence_entity::living::Health; +use valence_entity::pig::PigEntityBundle; +use valence_entity::player::PlayerEntityBundle; +use valence_entity::{EntityAnimations, OnGround, Velocity}; + +const ARENA_Y: i32 = 64; +const ARENA_MID_WIDTH: i32 = 2; +const SPAWN_BOX: [i32; 3] = [0, ARENA_Y + 20, 0]; +const SPAWN_POS: [f64; 3] = [ + SPAWN_BOX[0] as f64, + SPAWN_BOX[1] as f64 + 1.0, + SPAWN_BOX[2] as f64, +]; +const SPAWN_BOX_WIDTH: i32 = 5; +const SPAWN_BOX_HEIGHT: i32 = 4; +const PLAYER_MAX_HEALTH: f32 = 20.0; + +pub fn main() { + App::new() + .insert_resource(NetworkSettings { + connection_mode: ConnectionMode::Offline, + ..Default::default() + }) + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(EventLoopUpdate, handle_combat_events) + .add_systems( + Update, + ( + init_clients, + despawn_disconnected_clients, + digging, + place_blocks, + do_team_selector_portals, + update_flag_visuals, + do_flag_capturing, + // visualize_triggers, + update_clones, + teleport_oob_clients, + necromancy, + ), + ) + .run(); +} + +fn setup( + mut commands: Commands, + server: Res, + dimensions: Res, + biomes: Res, +) { + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + + for z in -5..5 { + for x in -5..5 { + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + } + } + + for z in -50..50 { + for x in -50..50 { + let block = match x { + x if x < -ARENA_MID_WIDTH => BlockState::RED_CONCRETE, + x if x > ARENA_MID_WIDTH => BlockState::BLUE_CONCRETE, + _ => BlockState::WHITE_CONCRETE, + }; + layer.chunk.set_block([x, ARENA_Y, z], block); + } + } + + let red_flag = build_flag( + &mut layer, + Team::Red, + BlockPos { + x: -48, + y: ARENA_Y + 1, + z: 0, + }, + ); + let blue_flag = build_flag( + &mut layer, + Team::Blue, + BlockPos { + x: 48, + y: ARENA_Y + 1, + z: 0, + }, + ); + + build_spawn_box(&mut layer, SPAWN_BOX, &mut commands); + + commands.spawn(layer); + + let red_capture_trigger = TriggerArea::new( + red_flag - BlockPos::new(5, 3, 5), + red_flag + BlockPos::new(5, 3, 5), + ); + let blue_capture_trigger = TriggerArea::new( + blue_flag - BlockPos::new(5, 3, 5), + blue_flag + BlockPos::new(5, 3, 5), + ); + let mappos = CtfGlobals { + red_flag, + blue_flag, + + red_capture_trigger, + blue_capture_trigger, + }; + + commands.insert_resource(mappos); + commands.insert_resource(FlagManager { + red: None, + blue: None, + }); + + let ctf_team_layers = CtfLayers::init(&mut commands, &server); + + // add some debug entities to the ctf entity layers + let mut flags = Flags::default(); + flags.set_glowing(true); + let mut pig = commands.spawn(PigEntityBundle { + layer: EntityLayerId(ctf_team_layers.friendly_layers[&Team::Red]), + position: Position([-30.0, 65.0, 2.0].into()), + entity_flags: flags.clone(), + ..Default::default() + }); + pig.insert(Team::Red); + + let mut cow = commands.spawn(CowEntityBundle { + layer: EntityLayerId(ctf_team_layers.friendly_layers[&Team::Blue]), + position: Position([30.0, 65.0, 2.0].into()), + entity_flags: flags, + ..Default::default() + }); + cow.insert(Team::Blue); + + commands.insert_resource(ctf_team_layers); + commands.insert_resource(Score::default()); +} + +/// Build a flag at the given position. `pos` should be the position of the +/// bottom of the flag. +/// +/// Returns the block position of the flag. +fn build_flag(layer: &mut LayerBundle, team: Team, pos: impl Into) -> BlockPos { + let mut pos = pos.into(); + + // build the flag pole + for _ in 0..3 { + layer.chunk.set_block(pos, BlockState::OAK_FENCE); + pos.y += 1; + } + let moving_east = pos.x < 0; + layer.chunk.set_block( + pos, + BlockState::OAK_FENCE.set( + if moving_east { + PropName::East + } else { + PropName::West + }, + PropValue::True, + ), + ); + pos.x += if pos.x < 0 { 1 } else { -1 }; + layer.chunk.set_block( + pos, + BlockState::OAK_FENCE + .set(PropName::East, PropValue::True) + .set(PropName::West, PropValue::True), + ); + pos.x += if pos.x < 0 { 1 } else { -1 }; + layer.chunk.set_block( + pos, + BlockState::OAK_FENCE.set( + if moving_east { + PropName::West + } else { + PropName::East + }, + PropValue::True, + ), + ); + pos.y -= 1; + + // build the flag + layer.chunk.set_block( + pos, + match team { + Team::Red => BlockState::RED_WOOL, + Team::Blue => BlockState::BLUE_WOOL, + }, + ); + + return pos; +} + +fn build_spawn_box(layer: &mut LayerBundle, pos: impl Into, commands: &mut Commands) { + let pos = pos.into(); + + let spawn_box_block = BlockState::GLASS; + + // build floor and roof + for z in -SPAWN_BOX_WIDTH..=SPAWN_BOX_WIDTH { + for x in -SPAWN_BOX_WIDTH..=SPAWN_BOX_WIDTH { + layer + .chunk + .set_block([pos.x + x, pos.y, pos.z + z], spawn_box_block); + layer.chunk.set_block( + [pos.x + x, pos.y + SPAWN_BOX_HEIGHT, pos.z + z], + spawn_box_block, + ); + } + } + + // build walls + for z in [-SPAWN_BOX_WIDTH, SPAWN_BOX_WIDTH] { + for x in -SPAWN_BOX_WIDTH..=SPAWN_BOX_WIDTH { + for y in pos.y..=pos.y + SPAWN_BOX_HEIGHT - 1 { + layer + .chunk + .set_block([pos.x + x, y, pos.z + z], spawn_box_block); + } + } + } + + for x in [-SPAWN_BOX_WIDTH, SPAWN_BOX_WIDTH] { + for z in -SPAWN_BOX_WIDTH..=SPAWN_BOX_WIDTH { + for y in pos.y..=pos.y + SPAWN_BOX_HEIGHT - 1 { + layer + .chunk + .set_block([pos.x + x, y, pos.z + z], spawn_box_block); + } + } + } + + // build team selector portals + for (block, offset) in [ + ( + BlockState::RED_CONCRETE, + BlockPos::new(-SPAWN_BOX_WIDTH, 0, SPAWN_BOX_WIDTH - 2), + ), + ( + BlockState::BLUE_CONCRETE, + BlockPos::new(SPAWN_BOX_WIDTH - 2, 0, SPAWN_BOX_WIDTH - 2), + ), + ] { + for z in 0..3 { + for x in 0..3 { + layer.chunk.set_block( + [pos.x + offset.x + x, pos.y + offset.y, pos.z + offset.z + z], + block, + ); + } + } + } + + let red = [ + pos.x - SPAWN_BOX_WIDTH + 1, + pos.y, + pos.z + SPAWN_BOX_WIDTH - 1, + ]; + let red_area = TriggerArea::new(red, red); + let blue = [ + pos.x + SPAWN_BOX_WIDTH - 1, + pos.y, + pos.z + SPAWN_BOX_WIDTH - 1, + ]; + let blue_area = TriggerArea::new(blue, blue); + let portals = Portals { + portals: HashMap::from_iter(vec![(Team::Red, red_area), (Team::Blue, blue_area)]), + }; + + for area in portals.portals.values() { + for pos in area.iter_block_pos() { + layer.chunk.set_block(pos, BlockState::AIR); + } + layer + .chunk + .set_block(area.a - BlockPos::new(0, 1, 0), BlockState::BARRIER); + } + + commands.insert_resource(portals); + + // build instruction signs + + let sign_pos = pos + BlockPos::from([0, 2, SPAWN_BOX_WIDTH - 1]); + layer.chunk.set_block( + sign_pos, + Block { + state: BlockState::OAK_WALL_SIGN.set(PropName::Rotation, PropValue::_3), + nbt: Some(compound! { + "front_text" => compound! { + "messages" => List::String(vec![ + "Capture".color(Color::YELLOW).bold().to_string(), + "the".color(Color::YELLOW).bold().to_string(), + "Flag!".color(Color::YELLOW).bold().to_string(), + "Select a Team".color(Color::WHITE).italic().to_string(), + ]) + }, + }), + }, + ); + + layer.chunk.set_block( + sign_pos + BlockPos::from([-1, 0, 0]), + Block { + state: BlockState::OAK_WALL_SIGN.set(PropName::Rotation, PropValue::_3), + nbt: Some(compound! { + "front_text" => compound! { + "messages" => List::String(vec![ + "".into_text().to_string(), + ("Join ".bold().color(Color::WHITE) + Team::Red.team_text()).to_string(), + "=>".bold().color(Color::WHITE).to_string(), + "".into_text().to_string(), + ]) + }, + }), + }, + ); + + layer.chunk.set_block( + sign_pos + BlockPos::from([1, 0, 0]), + Block { + state: BlockState::OAK_WALL_SIGN.set(PropName::Rotation, PropValue::_3), + nbt: Some(compound! { + "front_text" => compound! { + "messages" => List::String(vec![ + "".into_text().to_string(), + ("Join ".bold().color(Color::WHITE) + Team::Blue.team_text()).to_string(), + "<=".bold().color(Color::WHITE).to_string(), + "".into_text().to_string(), + ]) + }, + }), + }, + ); +} + +fn init_clients( + mut clients: Query< + ( + &mut Client, + &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut Position, + &mut GameMode, + &mut Health, + ), + Added, + >, + main_layers: Query, With)>, +) { + for ( + mut client, + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut pos, + mut game_mode, + mut health, + ) in &mut clients + { + let layer = main_layers.single(); + + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); + pos.set(SPAWN_POS); + *game_mode = GameMode::Adventure; + health.0 = PLAYER_MAX_HEALTH; + + client.send_chat_message( + "Welcome to Valence! Select a team by jumping in the team's portal.".italic(), + ); + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Component)] +enum Team { + Red, + Blue, +} + +impl Team { + pub fn spawn_pos(&self) -> DVec3 { + [ + match self { + Team::Red => -40.0, + Team::Blue => 40.0, + }, + ARENA_Y as f64 + 1.0, + 0.0, + ] + .into() + } + + pub fn team_text(&self) -> Text { + match self { + Team::Red => "RED".color(Color::RED).bold(), + Team::Blue => "BLUE".color(Color::BLUE).bold(), + } + } + + pub fn iter() -> impl Iterator { + [Team::Red, Team::Blue].iter().copied() + } +} + +fn digging( + mut clients: Query<(&GameMode, &Team, Entity, &mut Client, &mut Inventory)>, + mut layers: Query<&mut ChunkLayer>, + mut events: EventReader, + mut commands: Commands, + globals: Res, + mut flag_manager: ResMut, +) { + let mut layer = layers.single_mut(); + + for event in events.iter() { + let Ok((game_mode, team, ent, mut client, mut inv)) = clients.get_mut(event.client) else { + continue; + }; + + if (*game_mode == GameMode::Creative && event.state == DiggingState::Start) + || (*game_mode == GameMode::Survival && event.state == DiggingState::Stop) + { + let Some(block) = layer.block(event.position) else { + continue; + }; + let is_flag = event.position == globals.red_flag || event.position == globals.blue_flag; + + match (team, block.state) { + (Team::Blue, BlockState::RED_WOOL) => { + if event.position == globals.red_flag { + commands.entity(event.client).insert(HasFlag(Team::Red)); + client.send_chat_message("You have the flag!".italic()); + flag_manager.red = Some(ent); + return; + } + } + (Team::Red, BlockState::BLUE_WOOL) => { + if event.position == globals.blue_flag { + commands.entity(event.client).insert(HasFlag(Team::Blue)); + client.send_chat_message("You have the flag!".italic()); + flag_manager.blue = Some(ent); + return; + } + } + _ => {} + } + + if event.position.y <= ARENA_Y + || block.state.to_kind() == BlockKind::OakFence + || is_flag + { + continue; + } + + let prev = layer.set_block(event.position, BlockState::AIR); + + if let Some(prev) = prev { + let kind: ItemKind = prev.state.to_kind().to_item_kind(); + if let Some(slot) = inv.first_slot_with_item_in(kind, 64, 9..45) { + let count = inv.slot(slot).unwrap().count(); + inv.set_slot_amount(slot, count + 1); + } else { + let stack = ItemStack::new(kind, 1, None); + if let Some(empty_slot) = inv.first_empty_slot_in(9..45) { + inv.set_slot(empty_slot, Some(stack)); + } else { + debug!("No empty slot to give item to player: {:?}", kind); + } + } + } + } + } +} + +fn place_blocks( + mut clients: Query<(&mut Inventory, &GameMode, &HeldItem)>, + mut layers: Query<&mut ChunkLayer>, + mut events: EventReader, +) { + let mut layer = layers.single_mut(); + + for event in events.iter() { + let Ok((mut inventory, game_mode, held)) = clients.get_mut(event.client) else { + continue; + }; + if event.hand != Hand::Main { + continue; + } + + // get the held item + let slot_id = held.slot(); + let Some(stack) = inventory.slot(slot_id) else { + // no item in the slot + continue; + }; + + let Some(block_kind) = BlockKind::from_item_kind(stack.item) else { + // can't place this item as a block + continue; + }; + + if *game_mode == GameMode::Survival { + // check if the player has the item in their inventory and remove + // it. + if stack.count() > 1 { + let count = stack.count(); + inventory.set_slot_amount(slot_id, count - 1); + } else { + inventory.set_slot(slot_id, None); + } + } + let real_pos = event.position.get_in_direction(event.face); + layer.set_block(real_pos, block_kind.to_state()); + } +} + +#[derive(Debug, Resource)] +struct Portals { + portals: HashMap, +} + +fn do_team_selector_portals( + mut players: Query< + ( + Entity, + &mut Position, + &mut Look, + &mut HeadYaw, + &mut GameMode, + &mut Client, + &mut VisibleEntityLayers, + &UniqueId, + ), + Without, + >, + portals: Res, + mut commands: Commands, + ctf_layers: Res, + main_layers: Query, With)>, +) { + for player in players.iter_mut() { + let ( + player, + mut pos, + mut look, + mut head_yaw, + mut game_mode, + mut client, + mut ent_layers, + unique_id, + ) = player; + if pos.0.y < SPAWN_BOX[1] as f64 - 5.0 { + pos.0 = SPAWN_POS.into(); + continue; + } + + let team = portals + .portals + .iter() + .filter(|(_, area)| area.contains_pos(pos.0)) + .map(|(team, _)| team) + .next() + .copied(); + + if let Some(team) = team { + *game_mode = GameMode::Survival; + let mut inventory = Inventory::new(InventoryKind::Player); + inventory.set_slot(36, Some(ItemStack::new(ItemKind::WoodenSword, 1, None))); + inventory.set_slot( + 37, + Some(ItemStack::new( + match team { + Team::Red => ItemKind::RedWool, + Team::Blue => ItemKind::BlueWool, + }, + 64, + None, + )), + ); + let combat_state = CombatState::default(); + commands + .entity(player) + .insert((team, inventory, combat_state)); + pos.0 = team.spawn_pos(); + let yaw = match team { + Team::Red => -90.0, + Team::Blue => 90.0, + }; + look.yaw = yaw; + look.pitch = 0.0; + head_yaw.0 = yaw; + let chat_text: Text = "You are on team ".into_text() + team.team_text() + "!"; + client.send_chat_message(chat_text); + + let main_layer = main_layers.single(); + ent_layers.as_mut().0.remove(&main_layer); + for t in Team::iter() { + let enemy_layer = ctf_layers.enemy_layers[&t]; + if t == team { + ent_layers.as_mut().0.remove(&enemy_layer); + } else { + ent_layers.as_mut().0.insert(enemy_layer); + } + } + let friendly_layer = ctf_layers.friendly_layers[&team]; + ent_layers.as_mut().0.insert(friendly_layer); + + // Copy the player entity to the friendly layer, and make them glow. + let mut flags = Flags::default(); + flags.set_glowing(true); + let mut player_glowing = commands.spawn(PlayerEntityBundle { + layer: EntityLayerId(friendly_layer), + uuid: *unique_id, + entity_flags: flags, + position: pos.clone(), + ..Default::default() + }); + player_glowing.insert(ClonedEntity(player)); + + let enemy_layer = ctf_layers.enemy_layers[&team]; + let mut player_enemy = commands.spawn(PlayerEntityBundle { + layer: EntityLayerId(enemy_layer), + uuid: *unique_id, + position: pos.clone(), + ..Default::default() + }); + player_enemy.insert(ClonedEntity(player)); + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct TriggerArea { + pub a: BlockPos, + pub b: BlockPos, +} + +impl TriggerArea { + pub fn new(a: impl Into, b: impl Into) -> Self { + Self { + a: a.into(), + b: b.into(), + } + } + + pub fn contains(&self, pos: BlockPos) -> bool { + let min = BlockPos::new( + self.a.x.min(self.b.x), + self.a.y.min(self.b.y), + self.a.z.min(self.b.z), + ); + let max = BlockPos::new( + self.a.x.max(self.b.x), + self.a.y.max(self.b.y), + self.a.z.max(self.b.z), + ); + + pos.x >= min.x + && pos.x <= max.x + && pos.y >= min.y + && pos.y <= max.y + && pos.z >= min.z + && pos.z <= max.z + } + + pub fn contains_pos(&self, pos: DVec3) -> bool { + self.contains(BlockPos::from_pos(pos)) + } + + pub fn iter_block_pos(&self) -> impl Iterator { + let min = BlockPos::new( + self.a.x.min(self.b.x), + self.a.y.min(self.b.y), + self.a.z.min(self.b.z), + ); + let max = BlockPos::new( + self.a.x.max(self.b.x), + self.a.y.max(self.b.y), + self.a.z.max(self.b.z), + ); + + (min.x..=max.x) + .flat_map(move |x| (min.y..=max.y).map(move |y| (x, y))) + .flat_map(move |(x, y)| (min.z..=max.z).map(move |z| BlockPos::new(x, y, z))) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Component)] +#[component(storage = "SparseSet")] +struct HasFlag(Team); + +#[derive(Debug, Resource)] +struct FlagManager { + red: Option, + blue: Option, +} + +#[derive(Debug, Resource)] +struct CtfGlobals { + pub red_flag: BlockPos, + pub blue_flag: BlockPos, + + pub red_capture_trigger: TriggerArea, + pub blue_capture_trigger: TriggerArea, +} + +fn update_flag_visuals( + flag_manager: Res, + globals: Res, + mut layers: Query<&mut ChunkLayer>, +) { + if !flag_manager.is_changed() { + return; + } + let red_flag_block = match flag_manager.red { + Some(_) => BlockState::AIR, + None => BlockState::RED_WOOL, + }; + let blue_flag_block = match flag_manager.blue { + Some(_) => BlockState::AIR, + None => BlockState::BLUE_WOOL, + }; + + layers + .single_mut() + .set_block(globals.red_flag, red_flag_block); + layers + .single_mut() + .set_block(globals.blue_flag, blue_flag_block); +} + +fn do_flag_capturing( + globals: Res, + mut players: Query<(Entity, &mut Client, &Team, &Position, &HasFlag)>, + mut commands: Commands, + mut flag_manager: ResMut, + mut score: ResMut, +) { + for (ent, mut client, team, position, has_flag) in players.iter_mut() { + let capture_trigger = match team { + Team::Red => &globals.red_capture_trigger, + Team::Blue => &globals.blue_capture_trigger, + }; + + if capture_trigger.contains_pos(position.0) { + client.send_chat_message("You captured the flag!".italic()); + score + .scores + .entry(*team) + .and_modify(|score| *score += 1) + .or_insert(1); + client.send_chat_message(score.render_scores()); + commands.entity(ent).remove::(); + match has_flag.0 { + Team::Red => flag_manager.red = None, + Team::Blue => flag_manager.blue = None, + } + } + } +} + +#[derive(Debug, Default, Resource)] +struct Score { + pub scores: HashMap, +} + +impl Score { + pub fn render_scores(&self) -> Text { + let mut text = "Scores:\n".into_text(); + for team in Team::iter() { + let score = self.scores.get(&team).unwrap_or(&0); + text += team.team_text() + ": " + score.to_string() + "\n"; + } + text + } +} + +#[allow(dead_code)] +/// Visualizes the trigger areas, for debugging. +fn visualize_triggers(globals: Res, mut layers: Query<&mut ChunkLayer>) { + fn vis_trigger(trigger: &TriggerArea, layer: &mut ChunkLayer) { + for pos in trigger.iter_block_pos() { + layer.play_particle( + &Particle::Crit, + false, + [pos.x as f64 + 0.5, pos.y as f64 + 0.5, pos.z as f64 + 0.5], + [0., 0., 0.], + 0.0, + 1, + ); + } + } + + for mut layer in layers.iter_mut() { + vis_trigger(&globals.red_capture_trigger, &mut layer); + vis_trigger(&globals.blue_capture_trigger, &mut layer); + } +} + +/// Keeps track of the entity layers per team. +#[derive(Debug, Resource)] +struct CtfLayers { + /// Maps a team to the entity layer that contains how friendly players + /// should be viewed. + /// + /// This is used to make friendly players glow. + pub friendly_layers: HashMap, + /// Ditto, but for enemy players. + pub enemy_layers: HashMap, +} + +impl CtfLayers { + pub fn init(commands: &mut Commands, server: &Server) -> Self { + let mut friendly_layers = HashMap::new(); + let mut enemy_layers = HashMap::new(); + + for team in Team::iter() { + let friendly_layer = commands.spawn((EntityLayer::new(server), team)).id(); + friendly_layers.insert(team, friendly_layer); + let enemy_layer = commands.spawn((EntityLayer::new(server), team)).id(); + enemy_layers.insert(team, enemy_layer); + } + + Self { + friendly_layers, + enemy_layers, + } + } +} + +/// A marker component for entities that have been cloned, and the primary +/// entity they were cloned from. +#[derive(Debug, Component)] +struct ClonedEntity(Entity); + +#[derive(Debug, WorldQuery)] +#[world_query(mutable)] +struct CloneQuery { + position: &'static mut Position, + head_yaw: &'static mut HeadYaw, + velocity: &'static mut Velocity, + look: &'static mut Look, + animations: &'static mut EntityAnimations, + on_ground: &'static mut OnGround, + statuses: &'static mut EntityStatuses, +} + +fn update_clones( + ents: Query>, + mut clone_ents: Query<(CloneQuery, &ClonedEntity, Entity)>, + mut commands: Commands, +) { + for clone in clone_ents.iter_mut() { + let (mut clone, cloned_from, ent) = clone; + let Ok(src) = ents + .get(cloned_from.0) else { + commands.entity(ent).insert(Despawned); + return; + }; + + *clone.position = *src.position; + *clone.head_yaw = *src.head_yaw; + *clone.velocity = *src.velocity; + *clone.look = *src.look; + *clone.animations = src.animations.clone(); + *clone.on_ground = *src.on_ground; + *clone.statuses = *src.statuses; + } +} + +/// Attached to every client. +#[derive(Component, Default)] +struct CombatState { + /// The tick the client was last attacked. + last_attacked_tick: i64, + has_bonus_knockback: bool, +} + +#[derive(WorldQuery)] +#[world_query(mutable)] +struct CombatQuery { + client: &'static mut Client, + pos: &'static Position, + state: &'static mut CombatState, + statuses: &'static mut EntityStatuses, + health: &'static mut Health, + inventory: &'static Inventory, + held_item: &'static HeldItem, + team: &'static Team, +} + +fn handle_combat_events( + server: Res, + mut clients: Query, + mut sprinting: EventReader, + mut interact_entity: EventReader, + clones: Query<&ClonedEntity>, +) { + for &SprintEvent { client, state } in sprinting.iter() { + if let Ok(mut client) = clients.get_mut(client) { + client.state.has_bonus_knockback = state == SprintState::Start; + } + } + + for &InteractEntityEvent { + client: attacker_client, + entity: victim_client, + .. + } in interact_entity.iter() + { + let true_victim_ent = clones + .get(victim_client) + .map(|cloned| cloned.0) + .unwrap_or(victim_client); + let Ok([mut attacker, mut victim]) = clients.get_many_mut([attacker_client, true_victim_ent]) + else { + debug!("Failed to get clients for combat event"); + // Victim or attacker does not exist, or the attacker is attacking itself. + continue; + }; + + if attacker.team == victim.team { + // Attacker and victim are on the same team. + continue; + } + + if server.current_tick() - victim.state.last_attacked_tick < 10 { + // Victim is still on attack cooldown. + continue; + } + + victim.state.last_attacked_tick = server.current_tick(); + + let victim_pos = victim.pos.0.xz(); + let attacker_pos = attacker.pos.0.xz(); + + let dir = (victim_pos - attacker_pos).normalize().as_vec2(); + + let knockback_xz = if attacker.state.has_bonus_knockback { + 18.0 + } else { + 8.0 + }; + let knockback_y = if attacker.state.has_bonus_knockback { + 8.432 + } else { + 6.432 + }; + + victim + .client + .set_velocity([dir.x * knockback_xz, knockback_y, dir.y * knockback_xz]); + + attacker.state.has_bonus_knockback = false; + + victim.client.trigger_status(EntityStatus::PlayAttackSound); + victim.statuses.trigger(EntityStatus::PlayAttackSound); + + let damage = if let Some(item) = attacker.inventory.slot(attacker.held_item.slot()) { + match item.item { + ItemKind::WoodenSword => 4.0, + ItemKind::StoneSword => 5.0, + ItemKind::IronSword => 6.0, + ItemKind::DiamondSword => 7.0, + _ => 1.0, + } + } else { + 1.0 + }; + victim.health.0 -= damage; + } +} + +fn teleport_oob_clients(mut clients: Query<(&mut Position, &Team), With>) { + for (mut pos, team) in &mut clients { + if pos.0.y < 0.0 { + pos.set(team.spawn_pos()); + } + } +} + +/// Handles respawning dead players. +fn necromancy( + mut clients: Query<( + &mut VisibleChunkLayer, + &mut RespawnPosition, + &Team, + &mut Health, + )>, + mut events: EventReader, + layers: Query, With)>, +) { + for event in events.iter() { + if let Ok((mut visible_chunk_layer, mut respawn_pos, team, mut health)) = + clients.get_mut(event.client) + { + respawn_pos.pos = BlockPos::from_pos(team.spawn_pos()); + health.0 = PLAYER_MAX_HEALTH; + + let main_layer = layers.single(); + + // this gets the client to get rid of the respawn screen + visible_chunk_layer.0 = main_layer; + } + } +} From e2aa2aeb578d39cbcc7e89ec5bcb16befa154696 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Tue, 1 Aug 2023 09:57:50 -0400 Subject: [PATCH 40/47] ctf: fix lints --- examples/ctf.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/ctf.rs b/examples/ctf.rs index df5254754..99dd1fdfe 100644 --- a/examples/ctf.rs +++ b/examples/ctf.rs @@ -208,7 +208,7 @@ fn build_flag(layer: &mut LayerBundle, team: Team, pos: impl Into) -> }, ); - return pos; + pos } fn build_spawn_box(layer: &mut LayerBundle, pos: impl Into, commands: &mut Commands) { @@ -634,7 +634,7 @@ fn do_team_selector_portals( layer: EntityLayerId(friendly_layer), uuid: *unique_id, entity_flags: flags, - position: pos.clone(), + position: *pos, ..Default::default() }); player_glowing.insert(ClonedEntity(player)); @@ -643,7 +643,7 @@ fn do_team_selector_portals( let mut player_enemy = commands.spawn(PlayerEntityBundle { layer: EntityLayerId(enemy_layer), uuid: *unique_id, - position: pos.clone(), + position: *pos, ..Default::default() }); player_enemy.insert(ClonedEntity(player)); @@ -884,7 +884,7 @@ fn update_clones( *clone.head_yaw = *src.head_yaw; *clone.velocity = *src.velocity; *clone.look = *src.look; - *clone.animations = src.animations.clone(); + *clone.animations = *src.animations; *clone.on_ground = *src.on_ground; *clone.statuses = *src.statuses; } From b9d5a1fdfc4a0a6b52872a84503fa43beac10ae4 Mon Sep 17 00:00:00 2001 From: Ryan Johnson Date: Wed, 2 Aug 2023 01:04:28 -0700 Subject: [PATCH 41/47] Update crates/valence_layer/src/chunk/loaded.rs Co-authored-by: Carson McManus --- crates/valence_layer/src/chunk/loaded.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/valence_layer/src/chunk/loaded.rs b/crates/valence_layer/src/chunk/loaded.rs index aefbedf06..d95729830 100644 --- a/crates/valence_layer/src/chunk/loaded.rs +++ b/crates/valence_layer/src/chunk/loaded.rs @@ -30,7 +30,7 @@ use crate::packet::{ pub struct LoadedChunk { /// A count of the clients viewing this chunk. Useful for knowing if it's /// necessary to record changes, since no client would be in view to receive - /// the changes if this were nonzero. + /// the changes if this were zero. viewer_count: AtomicU32, /// Block and biome data for the chunk. sections: Box<[Section]>, From d8e9ed03cfca764d3e4259df60156e418954c0b6 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 2 Aug 2023 03:42:38 -0700 Subject: [PATCH 42/47] Document `Messages` --- crates/valence_layer/src/message.rs | 46 +++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/crates/valence_layer/src/message.rs b/crates/valence_layer/src/message.rs index e68c9385a..a2e3ca18a 100644 --- a/crates/valence_layer/src/message.rs +++ b/crates/valence_layer/src/message.rs @@ -6,6 +6,26 @@ use valence_core::chunk_pos::{ChunkPos, ChunkView}; use crate::bvh::{ChunkBvh, GetChunkPos}; +/// A message buffer of global messages (`G`) and local messages (`L`) meant for +/// consumption by clients. Local messages are those that have some spatial +/// component to them and implement the [`GetChunkPos`] trait. Local messages +/// are placed in a bounding volume hierarchy for fast queries via +/// [`Self::query_local`]. Global messages do not necessarily have a spatial +/// component and all globals will be visited when using [`Self::iter_global`]. +/// +/// Every message is associated with an arbitrary span of bytes. The meaning of +/// the bytes is whatever the message needs it to be. +/// +/// At the end of the tick and before clients have access to the buffer, all +/// messages are sorted and then deduplicated by concatenating byte spans +/// together. This is done for a couple of reasons: +/// - Messages may rely on sorted message order for correctness, like in the +/// case of entity spawn & despawn messages. Sorting also makes deduplication +/// easy. +/// - Deduplication reduces the total number of messages that all clients must +/// examine. Consider the case of a message such as "send all clients in view +/// of this chunk position these packet bytes". If two of these messages have +/// the same chunk position, then they can just be combined together. pub struct Messages { global: Vec<(G, Range)>, local: Vec<(L, Range)>, @@ -17,13 +37,14 @@ pub struct Messages { impl Messages where - G: Copy + Ord, - L: Copy + Ord + GetChunkPos, + G: Clone + Ord, + L: Clone + Ord + GetChunkPos, { pub(crate) fn new() -> Self { Self::default() } + /// Adds a global message to this message buffer. pub(crate) fn send_global( &mut self, msg: G, @@ -48,6 +69,7 @@ where Ok(()) } + /// Adds a local message to this message buffer. pub(crate) fn send_local( &mut self, msg: L, @@ -72,6 +94,7 @@ where Ok(()) } + /// Like [`Self::send_global`] but writing bytes cannot fail. pub(crate) fn send_global_infallible(&mut self, msg: G, f: impl FnOnce(&mut Vec)) { let _ = self.send_global::(msg, |b| { f(b); @@ -79,6 +102,7 @@ where }); } + /// Like [`Self::send_local`] but writing bytes cannot fail. pub(crate) fn send_local_infallible(&mut self, msg: L, f: impl FnOnce(&mut Vec)) { let _ = self.send_local::(msg, |b| { f(b); @@ -95,13 +119,13 @@ where self.ready.reserve_exact(self.staging.len()); - fn sort_and_merge( + fn sort_and_merge( msgs: &mut Vec<(M, Range)>, staging: &[u8], ready: &mut Vec, ) { // Sort must be stable. - msgs.sort_by_key(|(msg, _)| *msg); + msgs.sort_by_key(|(msg, _)| msg.clone()); // Make sure the first element is already copied to "ready". if let Some((_, range)) = msgs.first_mut() { @@ -171,25 +195,35 @@ where self.ready.shrink_to_fit(); } + /// All message bytes. Use this in conjunction with [`Self::iter_global`] + /// and [`Self::query_local`]. pub fn bytes(&self) -> &[u8] { debug_assert!(self.is_ready); &self.ready } + /// Returns an iterator over all global messages and their span of bytes in + /// [`Self::bytes`]. pub fn iter_global(&self) -> impl Iterator)> + '_ { debug_assert!(self.is_ready); self.global .iter() - .map(|(m, r)| (*m, r.start as usize..r.end as usize)) + .map(|(m, r)| (m.clone(), r.start as usize..r.end as usize)) } + /// Takes a visitor function `f` and visits all local messages contained + /// within the chunk view `view`. `f` is called with the local + /// message and its span of bytes in [`Self::bytes`]. pub fn query_local(&self, view: ChunkView, mut f: impl FnMut(L, Range)) { debug_assert!(self.is_ready); self.bvh.query(view, |pair| { - f(pair.msg, pair.range.start as usize..pair.range.end as usize) + f( + pair.msg.clone(), + pair.range.start as usize..pair.range.end as usize, + ) }); } } From cc90a492f7cb8cfc001e70abe22b1ddf901581bc Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 2 Aug 2023 04:54:34 -0700 Subject: [PATCH 43/47] valence_layer: fix README.md --- crates/valence_layer/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/valence_layer/README.md b/crates/valence_layer/README.md index f87f5c14c..c6dd27dae 100644 --- a/crates/valence_layer/README.md +++ b/crates/valence_layer/README.md @@ -1 +1,5 @@ -# TODO \ No newline at end of file +# valence_layer + +Defines chunk layers and entity layers. Chunk layers contain the chunks and dimension data of a world, while entity layers contain all the Minecraft entities. + +These two together are analogous to Minecraft "levels" or "worlds". From 46f691c205201cac5c92eae16c51de5cf183928d Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 2 Aug 2023 05:40:13 -0700 Subject: [PATCH 44/47] More docs for `valence_world_border` --- crates/valence_world_border/README.md | 28 ++++++++++++++++++++++++++ crates/valence_world_border/src/lib.rs | 7 +++++++ 2 files changed, 35 insertions(+) diff --git a/crates/valence_world_border/README.md b/crates/valence_world_border/README.md index 41760adf1..18d82c81a 100644 --- a/crates/valence_world_border/README.md +++ b/crates/valence_world_border/README.md @@ -1 +1,29 @@ # valence_world_border + +Contains the plugin for working with Minecraft's [world border](https://minecraft.fandom.com/wiki/World_border). + +To enable world border functionality for a layer, insert the [`WorldBorderBundle`] component on the layer entity. +Note that the layer entity must have the [`ChunkLayer`] component for this to work. + +## Example + +```rust +use bevy_ecs::prelude::*; +use valence_world_border::*; + +fn example_system(mut world_borders: Query<(&mut WorldBorderCenter, &mut WorldBorderLerp)>) { + for (mut center, mut lerp) in &mut world_borders { + // Change the center position of the world border. + center.x = 123.0; + center.z = 456.0; + + // Change the diameter of the world border. + // If you want to change the diameter without interpolating, stop after this. + lerp.target_diameter = 100.0; + + // Have the world border linearly interpolate its diameter from 50 to 100 over 200 ticks. + // `current_diameter` and `remaining_ticks` will change automatically, but you can modify their values at any time. + lerp.current_diameter = 50.0; + lerp.remaining_ticks = 200; + } +} diff --git a/crates/valence_world_border/src/lib.rs b/crates/valence_world_border/src/lib.rs index 04dedcc23..06bd914c8 100644 --- a/crates/valence_world_border/src/lib.rs +++ b/crates/valence_world_border/src/lib.rs @@ -74,6 +74,13 @@ pub struct WorldBorderCenter { pub z: f64, } +impl WorldBorderCenter { + pub fn set(&mut self, x: f64, z: f64) { + self.x = x; + self.z = z; + } +} + /// Component containing information to linearly interpolate the world border. /// Contains the world border's diameter. #[derive(Component, Clone, Copy, Debug)] From 7de204b3e02f1411ed5c536f48f7886403164e8f Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 2 Aug 2023 05:40:34 -0700 Subject: [PATCH 45/47] `cargo fmt` for ctf example --- examples/ctf.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/ctf.rs b/examples/ctf.rs index 99dd1fdfe..452304df8 100644 --- a/examples/ctf.rs +++ b/examples/ctf.rs @@ -874,11 +874,10 @@ fn update_clones( ) { for clone in clone_ents.iter_mut() { let (mut clone, cloned_from, ent) = clone; - let Ok(src) = ents - .get(cloned_from.0) else { - commands.entity(ent).insert(Despawned); - return; - }; + let Ok(src) = ents.get(cloned_from.0) else { + commands.entity(ent).insert(Despawned); + return; + }; *clone.position = *src.position; *clone.head_yaw = *src.head_yaw; @@ -934,7 +933,8 @@ fn handle_combat_events( .get(victim_client) .map(|cloned| cloned.0) .unwrap_or(victim_client); - let Ok([mut attacker, mut victim]) = clients.get_many_mut([attacker_client, true_victim_ent]) + let Ok([mut attacker, mut victim]) = + clients.get_many_mut([attacker_client, true_victim_ent]) else { debug!("Failed to get clients for combat event"); // Victim or attacker does not exist, or the attacker is attacking itself. From 5d87ca9ee97f1a33bb7061a5d7819e2313a333d6 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 2 Aug 2023 07:32:36 -0700 Subject: [PATCH 46/47] Fix excessive memory usage in `many_players_spread_out` bench --- benches/main.rs | 1 - crates/valence_dimension/src/lib.rs | 6 +++++- crates/valence_layer/src/chunk.rs | 16 ---------------- crates/valence_layer/src/chunk/loaded.rs | 4 ++-- examples/terrain.rs | 4 ++++ 5 files changed, 11 insertions(+), 20 deletions(-) diff --git a/benches/main.rs b/benches/main.rs index 3e4e78c77..d5068d546 100644 --- a/benches/main.rs +++ b/benches/main.rs @@ -11,7 +11,6 @@ mod var_long; criterion_group! { benches, - // anvil::load, block::block, decode_array::decode_array, idle::idle_update, diff --git a/crates/valence_dimension/src/lib.rs b/crates/valence_dimension/src/lib.rs index 9c352e128..36c34a657 100644 --- a/crates/valence_dimension/src/lib.rs +++ b/crates/valence_dimension/src/lib.rs @@ -46,7 +46,11 @@ impl Plugin for DimensionPlugin { fn load_default_dimension_types(mut reg: ResMut, codec: Res) { let mut helper = move || -> anyhow::Result<()> { for value in codec.registry(DimensionTypeRegistry::KEY) { - let dimension_type = DimensionType::deserialize(value.element.clone())?; + let mut dimension_type = DimensionType::deserialize(value.element.clone())?; + + // HACK: We don't have a lighting engine implemented. To avoid shrouding the + // world in darkness, give all dimensions the max ambient light. + dimension_type.ambient_light = 1.0; reg.insert(value.name.clone(), dimension_type); } diff --git a/crates/valence_layer/src/chunk.rs b/crates/valence_layer/src/chunk.rs index 03fde757e..f389c606f 100644 --- a/crates/valence_layer/src/chunk.rs +++ b/crates/valence_layer/src/chunk.rs @@ -11,14 +11,12 @@ use std::fmt; use bevy_app::prelude::*; use bevy_ecs::prelude::*; use glam::{DVec3, Vec3}; -use num_integer::div_ceil; use rustc_hash::FxHashMap; use valence_biome::{BiomeId, BiomeRegistry}; use valence_core::block_pos::BlockPos; use valence_core::chunk_pos::ChunkPos; use valence_core::ident::Ident; use valence_core::particle::{Particle, ParticleS2c}; -use valence_core::protocol::array::LengthPrefixedArray; use valence_core::protocol::encode::{PacketWriter, WritePacket}; use valence_core::protocol::packet::sound::{PlaySoundS2c, Sound, SoundCategory}; use valence_core::protocol::{Encode, Packet}; @@ -50,9 +48,6 @@ pub struct ChunkLayerInfo { min_y: i32, biome_registry_len: usize, compression_threshold: Option, - // We don't have a proper lighting engine yet, so we just fill chunks with full brightness. - sky_light_mask: Box<[u64]>, - sky_light_arrays: Box<[LengthPrefixedArray]>, } impl fmt::Debug for ChunkLayerInfo { @@ -154,14 +149,6 @@ impl ChunkLayer { dim.height ); - let light_section_count = (dim.height / 16 + 2) as usize; - - let mut sky_light_mask = vec![0; div_ceil(light_section_count, 16)]; - - for i in 0..light_section_count { - sky_light_mask[i / 64] |= 1 << (i % 64); - } - Self { messages: Messages::new(), chunks: Default::default(), @@ -171,9 +158,6 @@ impl ChunkLayer { min_y: dim.min_y, biome_registry_len: biomes.iter().len(), compression_threshold: server.compression_threshold(), - sky_light_mask: sky_light_mask.into(), - sky_light_arrays: vec![LengthPrefixedArray([0xff; 2048]); light_section_count] - .into(), }, } } diff --git a/crates/valence_layer/src/chunk/loaded.rs b/crates/valence_layer/src/chunk/loaded.rs index d95729830..368bc86b1 100644 --- a/crates/valence_layer/src/chunk/loaded.rs +++ b/crates/valence_layer/src/chunk/loaded.rs @@ -372,11 +372,11 @@ impl LoadedChunk { heightmaps: Cow::Owned(heightmaps), blocks_and_biomes: &blocks_and_biomes, block_entities: Cow::Owned(block_entities), - sky_light_mask: Cow::Borrowed(&info.sky_light_mask), + sky_light_mask: Cow::Borrowed(&[]), block_light_mask: Cow::Borrowed(&[]), empty_sky_light_mask: Cow::Borrowed(&[]), empty_block_light_mask: Cow::Borrowed(&[]), - sky_light_arrays: Cow::Borrowed(&info.sky_light_arrays), + sky_light_arrays: Cow::Borrowed(&[]), block_light_arrays: Cow::Borrowed(&[]), }, ) diff --git a/examples/terrain.rs b/examples/terrain.rs index 20629ac2e..eca5448c5 100644 --- a/examples/terrain.rs +++ b/examples/terrain.rs @@ -10,6 +10,7 @@ use flume::{Receiver, Sender}; use noise::{NoiseFn, SuperSimplex}; use tracing::info; use valence::prelude::*; +use valence_client::spawn::IsFlat; const SPAWN_POS: DVec3 = DVec3::new(0.0, 200.0, 0.0); const HEIGHT: u32 = 384; @@ -117,6 +118,7 @@ fn init_clients( &mut VisibleEntityLayers, &mut Position, &mut GameMode, + &mut IsFlat, ), Added, >, @@ -128,6 +130,7 @@ fn init_clients( mut visible_entity_layers, mut pos, mut game_mode, + mut is_flat, ) in &mut clients { let layer = layers.single(); @@ -137,6 +140,7 @@ fn init_clients( visible_entity_layers.0.insert(layer); pos.set(SPAWN_POS); *game_mode = GameMode::Creative; + is_flat.0 = true; } } From faf042b2b30f49c196b474155ac5c393567756db Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 2 Aug 2023 07:39:07 -0700 Subject: [PATCH 47/47] Fix test --- crates/valence_layer/src/chunk/loaded.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/valence_layer/src/chunk/loaded.rs b/crates/valence_layer/src/chunk/loaded.rs index 368bc86b1..5e1720dba 100644 --- a/crates/valence_layer/src/chunk/loaded.rs +++ b/crates/valence_layer/src/chunk/loaded.rs @@ -639,8 +639,6 @@ mod tests { min_y: -16, biome_registry_len: 200, compression_threshold: None, - sky_light_mask: vec![].into(), - sky_light_arrays: vec![].into(), }; let mut buf = vec![];