diff --git a/benches/idle.rs b/benches/idle.rs index d773cc2c7..174f6cb31 100644 --- a/benches/idle.rs +++ b/benches/idle.rs @@ -25,17 +25,19 @@ fn setup( biomes: Res, server: Res, ) { - let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = CombinedLayerBundle::new(Default::default(), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk_index.insert([x, z], Chunk::new()); } } for z in -50..50 { for x in -50..50 { - layer.chunk.set_block([x, 64, z], BlockState::GRASS_BLOCK); + layer + .chunk_index + .set_block([x, 64, z], BlockState::GRASS_BLOCK); } } diff --git a/benches/many_players.rs b/benches/many_players.rs index 89d369210..026bec535 100644 --- a/benches/many_players.rs +++ b/benches/many_players.rs @@ -5,8 +5,8 @@ use criterion::Criterion; use rand::Rng; use valence::entity::Position; use valence::keepalive::KeepaliveSettings; -use valence::layer::chunk::UnloadedChunk; -use valence::layer::LayerBundle; +use valence::layer_old::chunk::Chunk; +use valence::layer_old::LayerBundle; use valence::math::DVec3; use valence::network::NetworkPlugin; use valence::protocol::packets::play::{FullC2s, HandSwingC2s}; @@ -51,9 +51,7 @@ fn run_many_players( for z in -world_size..world_size { for x in -world_size..world_size { - layer - .chunk - .insert_chunk(ChunkPos::new(x, z), UnloadedChunk::new()); + layer.chunk_index.insert(ChunkPos::new(x, z), Chunk::new()); } } diff --git a/crates/valence_advancement/src/lib.rs b/crates/valence_advancement/src/lib.rs index a5229f025..e2a37e2ac 100644 --- a/crates/valence_advancement/src/lib.rs +++ b/crates/valence_advancement/src/lib.rs @@ -9,6 +9,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use bevy_app::prelude::*; use bevy_ecs::prelude::*; +use bevy_ecs::schedule::SystemSet; use bevy_ecs::system::SystemParam; pub use bevy_hierarchy; use bevy_hierarchy::{Children, HierarchyPlugin, Parent}; @@ -27,21 +28,12 @@ use valence_server::{Ident, ItemStack, Text}; pub struct AdvancementPlugin; #[derive(SystemSet, Clone, Copy, Eq, PartialEq, Hash, Debug)] -pub struct WriteAdvancementPacketToClientsSet; - -#[derive(SystemSet, Clone, Copy, Eq, PartialEq, Hash, Debug)] -pub struct WriteAdvancementToCacheSet; +pub struct AdvancementSet; impl Plugin for AdvancementPlugin { fn build(&self, app: &mut bevy_app::App) { app.add_plugins(HierarchyPlugin) - .configure_sets( - PostUpdate, - ( - WriteAdvancementPacketToClientsSet.before(FlushPacketsSet), - WriteAdvancementToCacheSet.before(WriteAdvancementPacketToClientsSet), - ), - ) + .configure_set(PostUpdate, AdvancementSet.before(FlushPacketsSet)) .add_event::() .add_systems( PreUpdate, @@ -53,9 +45,10 @@ impl Plugin for AdvancementPlugin { .add_systems( PostUpdate, ( - update_advancement_cached_bytes.in_set(WriteAdvancementToCacheSet), - send_advancement_update_packet.in_set(WriteAdvancementPacketToClientsSet), - ), + update_advancement_cached_bytes.before(send_advancement_update_packet), + send_advancement_update_packet, + ) + .in_set(AdvancementSet), ); } } diff --git a/crates/valence_anvil/src/lib.rs b/crates/valence_anvil/src/lib.rs index 3770f1d94..5f23f9ba8 100644 --- a/crates/valence_anvil/src/lib.rs +++ b/crates/valence_anvil/src/lib.rs @@ -23,20 +23,20 @@ use std::num::NonZeroUsize; use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; -#[cfg(feature = "bevy_plugin")] -pub use bevy::*; use bitfield_struct::bitfield; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use flate2::bufread::{GzDecoder, ZlibDecoder}; use flate2::write::{GzEncoder, ZlibEncoder}; use lru::LruCache; +#[cfg(feature = "bevy_plugin")] +pub use plugin::*; use thiserror::Error; use valence_nbt::Compound; -#[cfg(feature = "bevy_plugin")] -mod bevy; #[cfg(feature = "parsing")] pub mod parsing; +#[cfg(feature = "bevy_plugin")] +mod plugin; const LRU_CACHE_SIZE: NonZeroUsize = match NonZeroUsize::new(256) { Some(n) => n, diff --git a/crates/valence_anvil/src/parsing.rs b/crates/valence_anvil/src/parsing.rs index 898aa4436..45dee9363 100644 --- a/crates/valence_anvil/src/parsing.rs +++ b/crates/valence_anvil/src/parsing.rs @@ -5,12 +5,12 @@ use std::path::PathBuf; use num_integer::div_ceil; use thiserror::Error; use valence_server::block::{PropName, PropValue}; -use valence_server::layer::chunk::{Chunk, UnloadedChunk}; +use valence_server::dimension_layer::chunk::ChunkOps; use valence_server::nbt::{Compound, List, Value}; use valence_server::protocol::BlockKind; use valence_server::registry::biome::BiomeId; use valence_server::registry::BiomeRegistry; -use valence_server::{ChunkPos, Ident}; +use valence_server::{Chunk, ChunkPos, Ident}; use crate::{RegionError, RegionFolder}; @@ -55,7 +55,7 @@ impl DimensionFolder { /// A chunk parsed to show block information, biome information etc. pub struct ParsedChunk { - pub chunk: UnloadedChunk, + pub chunk: Chunk, pub timestamp: u32, } @@ -119,17 +119,16 @@ pub enum ParseChunkError { fn parse_chunk( mut nbt: Compound, biome_map: &BTreeMap, BiomeId>, // TODO: replace with biome registry arg. -) -> Result { +) -> Result { let Some(Value::List(List::Compound(sections))) = nbt.remove("sections") else { return Err(ParseChunkError::MissingSections); }; if sections.is_empty() { - return Ok(UnloadedChunk::new()); + return Ok(Chunk::new()); } - let mut chunk = - UnloadedChunk::with_height((sections.len() * 16).try_into().unwrap_or(u32::MAX)); + let mut chunk = Chunk::with_height((sections.len() * 16).try_into().unwrap_or(i32::MAX)); let min_sect_y = sections .iter() diff --git a/crates/valence_anvil/src/bevy.rs b/crates/valence_anvil/src/plugin.rs similarity index 89% rename from crates/valence_anvil/src/bevy.rs rename to crates/valence_anvil/src/plugin.rs index 97b4fea64..bec87310d 100644 --- a/crates/valence_anvil/src/bevy.rs +++ b/crates/valence_anvil/src/plugin.rs @@ -7,11 +7,12 @@ use bevy_app::prelude::*; use bevy_ecs::prelude::*; use flume::{Receiver, Sender}; use valence_server::client::{Client, OldView, View}; -use valence_server::entity::{EntityLayerId, OldEntityLayerId}; -use valence_server::layer::UpdateLayersPreClientSet; +use valence_server::dimension_layer::{ + ChunkIndex, DimensionInfo, DimensionLayerQuery, UpdateDimensionLayerSet, +}; use valence_server::protocol::anyhow; use valence_server::registry::BiomeRegistry; -use valence_server::{ChunkLayer, ChunkPos}; +use valence_server::{ChunkPos, LayerId, OldLayerId}; use crate::parsing::{DimensionFolder, ParsedChunk}; @@ -92,21 +93,25 @@ struct ChunkWorkerState { pub struct AnvilPlugin; +#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct AnvilSet; + impl Plugin for AnvilPlugin { fn build(&self, app: &mut App) { app.add_event::() .add_event::() .add_systems(PreUpdate, remove_unviewed_chunks) + .configure_set(PostUpdate, AnvilSet.before(UpdateDimensionLayerSet)) .add_systems( PostUpdate, (init_anvil, update_client_views, send_recv_chunks) .chain() - .before(UpdateLayersPreClientSet), + .in_set(AnvilSet), ); } } -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)); @@ -119,12 +124,12 @@ fn init_anvil(mut query: Query<&mut AnvilLevel, (Added, With, + mut chunk_layers: Query<(Entity, DimensionLayerQuery, &AnvilLevel)>, mut unload_events: EventWriter, ) { 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) { + if chunk.viewer_count() > 0 || anvil.ignored_chunks.contains(&pos) { true } else { unload_events.send(ChunkUnloadEvent { @@ -138,20 +143,20 @@ fn remove_unviewed_chunks( } fn update_client_views( - clients: Query<(&EntityLayerId, Ref, View, OldView), With>, - mut chunk_layers: Query<(&ChunkLayer, &mut AnvilLevel)>, + clients: Query<(&LayerId, Ref, View, OldView), With>, + mut chunk_layers: Query<(&ChunkIndex, &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((layer, mut anvil)) = chunk_layers.get_mut(loc.0) else { + let Ok((chunk_index, mut anvil)) = chunk_layers.get_mut(loc.0) else { continue; }; let queue_pos = |pos| { - if !anvil.ignored_chunks.contains(&pos) && layer.chunk(pos).is_none() { + if !anvil.ignored_chunks.contains(&pos) && chunk_index.get(pos).is_none() { // Chunks closer to clients are prioritized. match anvil.pending.entry(pos) { Entry::Occupied(mut oe) => { @@ -179,7 +184,7 @@ fn update_client_views( } fn send_recv_chunks( - mut layers: Query<(Entity, &mut ChunkLayer, &mut AnvilLevel)>, + mut layers: Query<(Entity, DimensionLayerQuery, &mut AnvilLevel)>, mut to_send: Local>, mut load_events: EventWriter, ) { diff --git a/crates/valence_boss_bar/src/components.rs b/crates/valence_boss_bar/src/components.rs deleted file mode 100644 index 6548fff5c..000000000 --- a/crates/valence_boss_bar/src/components.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::borrow::Cow; - -use bevy_ecs::prelude::{Bundle, Component}; -use derive_more::{Deref, DerefMut}; -use valence_entity::EntityLayerId; -use valence_server::protocol::packets::play::boss_bar_s2c::{ - BossBarAction, BossBarColor, BossBarDivision, BossBarFlags, -}; -use valence_server::{Text, UniqueId}; - -/// The bundle of components that make up a boss bar. -#[derive(Bundle, Default)] -pub struct BossBarBundle { - pub id: UniqueId, - pub title: BossBarTitle, - pub health: BossBarHealth, - pub style: BossBarStyle, - pub flags: BossBarFlags, - pub layer: EntityLayerId, -} - -/// The title of a boss bar. -#[derive(Component, Clone, Default, Deref, DerefMut)] -pub struct BossBarTitle(pub Text); - -impl ToPacketAction for BossBarTitle { - fn to_packet_action(&self) -> BossBarAction { - BossBarAction::UpdateTitle(Cow::Borrowed(&self.0)) - } -} - -/// The health of a boss bar. -#[derive(Component, Default, Deref, DerefMut)] -pub struct BossBarHealth(pub f32); - -impl ToPacketAction for BossBarHealth { - fn to_packet_action(&self) -> BossBarAction { - BossBarAction::UpdateHealth(self.0) - } -} - -/// The style of a boss bar. This includes the color and division of the boss -/// bar. -#[derive(Component, Default)] -pub struct BossBarStyle { - pub color: BossBarColor, - pub division: BossBarDivision, -} - -impl ToPacketAction for BossBarStyle { - fn to_packet_action(&self) -> BossBarAction { - BossBarAction::UpdateStyle(self.color, self.division) - } -} - -impl ToPacketAction for BossBarFlags { - fn to_packet_action(&self) -> BossBarAction { - BossBarAction::UpdateFlags(*self) - } -} - -/// Trait for converting a component to a boss bar action. -pub(crate) trait ToPacketAction { - fn to_packet_action(&self) -> BossBarAction; -} diff --git a/crates/valence_boss_bar/src/lib.rs b/crates/valence_boss_bar/src/lib.rs index ea4b9ec29..c9e61cae2 100644 --- a/crates/valence_boss_bar/src/lib.rs +++ b/crates/valence_boss_bar/src/lib.rs @@ -22,147 +22,107 @@ use std::borrow::Cow; use bevy_app::prelude::*; use bevy_ecs::prelude::*; -use valence_server::client::{ - Client, OldViewDistance, OldVisibleEntityLayers, ViewDistance, VisibleEntityLayers, -}; -use valence_server::layer::UpdateLayersPreClientSet; +use bevy_ecs::query::WorldQuery; +use derive_more::{Deref, DerefMut}; +use valence_server::client::Client; +use valence_server::layer::message::LayerMessages; +use valence_server::layer::{BroadcastLayerMessagesSet, OldVisibleLayers, VisibleLayers}; pub use valence_server::protocol::packets::play::boss_bar_s2c::{ BossBarAction, BossBarColor, BossBarDivision, BossBarFlags, }; use valence_server::protocol::packets::play::BossBarS2c; use valence_server::protocol::WritePacket; -use valence_server::{ChunkView, Despawned, EntityLayer, Layer, UniqueId}; - -mod components; -pub use components::*; -use valence_entity::{EntityLayerId, OldPosition, Position}; +use valence_server::{Despawned, LayerId, OldLayerId, Text, UniqueId}; pub struct BossBarPlugin; +#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct BossBarSet; + impl Plugin for BossBarPlugin { fn build(&self, app: &mut bevy_app::App) { - app.add_systems( - PostUpdate, - ( - update_boss_bar::, - update_boss_bar::, - update_boss_bar::, - update_boss_bar::, - update_boss_bar_layer_view, - update_boss_bar_chunk_view, - boss_bar_despawn, - ) - .before(UpdateLayersPreClientSet), - ); + app.configure_set(PostUpdate, BossBarSet.before(BroadcastLayerMessagesSet)) + .add_systems( + PostUpdate, + ( + init_boss_bar_for_client, + update_boss_bar_layer, + update_boss_bar_title, + update_boss_bar_health, + update_boss_bar_style, + update_boss_bar_flags, + despawn_boss_bar, + ) + .chain() + .in_set(BossBarSet), + ); } } -fn update_boss_bar( - boss_bars_query: Query<(&UniqueId, &T, &EntityLayerId, Option<&Position>), Changed>, - mut entity_layers_query: Query<&mut EntityLayer>, -) { - for (id, part, entity_layer_id, pos) in boss_bars_query.iter() { - if let Ok(mut entity_layer) = entity_layers_query.get_mut(entity_layer_id.0) { - let packet = BossBarS2c { - id: id.0, - action: part.to_packet_action(), - }; - if let Some(pos) = pos { - entity_layer.view_writer(pos.0).write_packet(&packet); - } else { - entity_layer.write_packet(&packet); - } - } - } +/// The bundle of components that make up a boss bar. +#[derive(Bundle, Default)] +pub struct BossBarBundle { + pub uuid: UniqueId, + pub title: BossBarTitle, + pub health: BossBarHealth, + pub color: BossBarColor, + pub division: BossBarDivision, + pub flags: BossBarFlags, + pub layer: LayerId, + pub old_layer: OldLayerId, } -fn update_boss_bar_layer_view( - mut clients_query: Query< - ( - &mut Client, - &VisibleEntityLayers, - &OldVisibleEntityLayers, - &Position, - &OldPosition, - &ViewDistance, - &OldViewDistance, - ), - Changed, - >, - boss_bars_query: Query<( - &UniqueId, - &BossBarTitle, - &BossBarHealth, - &BossBarStyle, - &BossBarFlags, - &EntityLayerId, - Option<&Position>, - )>, -) { - for ( - mut client, - visible_entity_layers, - old_visible_entity_layers, - position, - _old_position, - view_distance, - _old_view_distance, - ) in clients_query.iter_mut() - { - let view = ChunkView::new(position.0.into(), view_distance.get()); +/// The title of a boss bar. +#[derive(Component, Clone, Default, Deref, DerefMut)] +pub struct BossBarTitle(pub Text); + +/// The health of a boss bar. +#[derive(Component, Default, Deref, DerefMut)] +pub struct BossBarHealth(pub f32); - let old_layers = old_visible_entity_layers.get(); - let current_layers = &visible_entity_layers.0; +#[derive(WorldQuery)] +struct FullBossBarQuery { + uuid: &'static UniqueId, + title: &'static BossBarTitle, + health: &'static BossBarHealth, + color: &'static BossBarColor, + division: &'static BossBarDivision, + flags: &'static BossBarFlags, + layer: &'static LayerId, +} + +fn init_boss_bar_for_client( + mut clients: Query<(&mut Client, &VisibleLayers, &OldVisibleLayers), Changed>, + boss_bars: Query, +) { + for (mut client, layers, old_layers) in &mut clients { + // TODO: this could be improved with fragmenting relations. - for &added_layer in current_layers.difference(old_layers) { - for (id, title, health, style, flags, _, boss_bar_position) in boss_bars_query - .iter() - .filter(|(_, _, _, _, _, layer_id, _)| layer_id.0 == added_layer) - { - if let Some(position) = boss_bar_position { - if view.contains(position.0.into()) { - client.write_packet(&BossBarS2c { - id: id.0, - action: BossBarAction::Add { - title: Cow::Borrowed(&title.0), - health: health.0, - color: style.color, - division: style.division, - flags: *flags, - }, - }); - } - } else { + // Remove boss bars from old layers. + for &layer in old_layers.difference(&layers) { + for bb in &boss_bars { + if bb.layer.0 == layer { client.write_packet(&BossBarS2c { - id: id.0, - action: BossBarAction::Add { - title: Cow::Borrowed(&title.0), - health: health.0, - color: style.color, - division: style.division, - flags: *flags, - }, + id: bb.uuid.0, + action: BossBarAction::Remove, }); } } } - for &removed_layer in old_layers.difference(current_layers) { - for (id, _, _, _, _, _, boss_bar_position) in boss_bars_query - .iter() - .filter(|(_, _, _, _, _, layer_id, _)| layer_id.0 == removed_layer) - { - if let Some(position) = boss_bar_position { - if view.contains(position.0.into()) { - client.write_packet(&BossBarS2c { - id: id.0, - action: BossBarAction::Remove, - }); - } - } else { + // Add boss bars from new layers. + for &layer in layers.difference(&old_layers) { + for bb in &boss_bars { + if bb.layer.0 == layer { client.write_packet(&BossBarS2c { - id: id.0, - action: BossBarAction::Remove, + id: bb.uuid.0, + action: BossBarAction::Add { + title: Cow::Borrowed(&bb.title.0), + health: bb.health.0, + color: *bb.color, + division: *bb.division, + flags: *bb.flags, + }, }); } } @@ -170,88 +130,104 @@ fn update_boss_bar_layer_view( } } -fn update_boss_bar_chunk_view( - mut clients_query: Query< - ( - &mut Client, - &VisibleEntityLayers, - &OldVisibleEntityLayers, - &Position, - &OldPosition, - &ViewDistance, - &OldViewDistance, - ), - Changed, +fn update_boss_bar_layer( + boss_bars: Query<(FullBossBarQuery, &OldLayerId), Changed>, + mut layers: Query<&mut LayerMessages>, +) { + for (bb, old_layer) in &boss_bars { + // Remove from old layer. + if let Ok(mut msgs) = layers.get_mut(old_layer.get()) { + msgs.write_packet(&BossBarS2c { + id: bb.uuid.0, + action: BossBarAction::Remove, + }) + } + + // Init in new layer. + if let Ok(mut msgs) = layers.get_mut(bb.layer.0) { + msgs.write_packet(&BossBarS2c { + id: bb.uuid.0, + action: BossBarAction::Add { + title: Cow::Borrowed(&bb.title.0), + health: bb.health.0, + color: *bb.color, + division: *bb.division, + flags: *bb.flags, + }, + }); + } + } +} + +fn update_boss_bar_title( + boss_bars: Query<(&UniqueId, &LayerId, &BossBarTitle), Changed>, + mut layers: Query<&mut LayerMessages>, +) { + for (uuid, layer, title) in &boss_bars { + if let Ok(mut msgs) = layers.get_mut(layer.0) { + msgs.write_packet(&BossBarS2c { + id: uuid.0, + action: BossBarAction::UpdateTitle(Cow::Borrowed(&title.0)), + }); + } + } +} + +fn update_boss_bar_health( + boss_bars: Query<(&UniqueId, &LayerId, &BossBarHealth), Changed>, + mut layers: Query<&mut LayerMessages>, +) { + for (uuid, layer, health) in &boss_bars { + if let Ok(mut msgs) = layers.get_mut(layer.0) { + msgs.write_packet(&BossBarS2c { + id: uuid.0, + action: BossBarAction::UpdateHealth(health.0), + }); + } + } +} + +fn update_boss_bar_style( + boss_bars: Query< + (&UniqueId, &LayerId, &BossBarColor, &BossBarDivision), + Or<(Changed, Changed)>, >, - boss_bars_query: Query<( - &UniqueId, - &BossBarTitle, - &BossBarHealth, - &BossBarStyle, - &BossBarFlags, - &EntityLayerId, - &Position, - )>, + mut layers: Query<&mut LayerMessages>, ) { - for ( - mut client, - visible_entity_layers, - _old_visible_entity_layers, - position, - old_position, - view_distance, - old_view_distance, - ) in clients_query.iter_mut() - { - let view = ChunkView::new(position.0.into(), view_distance.get()); - let old_view = ChunkView::new(old_position.get().into(), old_view_distance.get()); + for (uuid, layer, color, division) in &boss_bars { + if let Ok(mut msgs) = layers.get_mut(layer.0) { + msgs.write_packet(&BossBarS2c { + id: uuid.0, + action: BossBarAction::UpdateStyle(*color, *division), + }); + } + } +} - for layer in visible_entity_layers.0.iter() { - for (id, title, health, style, flags, _, boss_bar_position) in boss_bars_query - .iter() - .filter(|(_, _, _, _, _, layer_id, _)| layer_id.0 == *layer) - { - if view.contains(boss_bar_position.0.into()) - && !old_view.contains(boss_bar_position.0.into()) - { - client.write_packet(&BossBarS2c { - id: id.0, - action: BossBarAction::Add { - title: Cow::Borrowed(&title.0), - health: health.0, - color: style.color, - division: style.division, - flags: *flags, - }, - }); - } else if !view.contains(boss_bar_position.0.into()) - && old_view.contains(boss_bar_position.0.into()) - { - client.write_packet(&BossBarS2c { - id: id.0, - action: BossBarAction::Remove, - }); - } - } +fn update_boss_bar_flags( + boss_bars: Query<(&UniqueId, &LayerId, &BossBarFlags), Changed>, + mut layers: Query<&mut LayerMessages>, +) { + for (uuid, layer, flags) in &boss_bars { + if let Ok(mut msgs) = layers.get_mut(layer.0) { + msgs.write_packet(&BossBarS2c { + id: uuid.0, + action: BossBarAction::UpdateFlags(*flags), + }); } } } -fn boss_bar_despawn( - boss_bars_query: Query<(&UniqueId, &EntityLayerId, Option<&Position>), With>, - mut entity_layer_query: Query<&mut EntityLayer>, +fn despawn_boss_bar( + boss_bars: Query<(&UniqueId, &LayerId), With>, + mut layers: Query<&mut LayerMessages>, ) { - for (id, entity_layer_id, position) in boss_bars_query.iter() { - if let Ok(mut entity_layer) = entity_layer_query.get_mut(entity_layer_id.0) { - let packet = BossBarS2c { - id: id.0, + for (uuid, layer_id) in &boss_bars { + if let Ok(mut msgs) = layers.get_mut(layer_id.0) { + msgs.write_packet(&BossBarS2c { + id: uuid.0, action: BossBarAction::Remove, - }; - if let Some(pos) = position { - entity_layer.view_writer(pos.0).write_packet(&packet); - } else { - entity_layer.write_packet(&packet); - } + }); } } } diff --git a/crates/valence_entity/build.rs b/crates/valence_entity/build.rs index 7f9384946..400af7bcc 100644 --- a/crates/valence_entity/build.rs +++ b/crates/valence_entity/build.rs @@ -386,8 +386,8 @@ fn build() -> anyhow::Result { pub kind: super::EntityKind, pub id: super::EntityId, pub uuid: super::UniqueId, - pub layer: super::EntityLayerId, - pub old_layer: super::OldEntityLayerId, + pub layer: super::LayerId, + pub old_layer: super::OldLayerId, 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 f66ffa79f..2ddbd68f2 100644 --- a/crates/valence_entity/src/lib.rs +++ b/crates/valence_entity/src/lib.rs @@ -33,7 +33,7 @@ use tracing::warn; use tracked_data::TrackedData; use valence_math::{DVec3, Vec3}; use valence_protocol::{Decode, Encode, VarInt}; -use valence_server_common::{Despawned, UniqueId}; +use valence_server_common::{Despawned, LayerId, OldLayerId, UniqueId}; include!(concat!(env!("OUT_DIR"), "/entity.rs")); pub struct EntityPlugin; @@ -90,7 +90,6 @@ impl Plugin for EntityPlugin { clear_animation_changes, clear_tracked_data_changes, update_old_position, - update_old_layer_id, ) .in_set(ClearEntityChangesSet), ); @@ -105,12 +104,6 @@ fn update_old_position(mut query: Query<(&Position, &mut OldPosition)>) { } } -fn update_old_layer_id(mut query: Query<(&EntityLayerId, &mut OldEntityLayerId)>) { - for (loc, mut old_loc) in &mut query { - old_loc.0 = loc.0; - } -} - fn remove_despawned_from_manager( entities: Query<&EntityId, (With, With)>, mut manager: ResMut, @@ -163,44 +156,6 @@ fn clear_tracked_data_changes(mut tracked_data: Query<&mut TrackedData, Changed< } } -/// Contains the entity layer an entity is on. -#[derive(Component, Copy, Clone, PartialEq, Eq, Debug, Deref)] -pub struct EntityLayerId(pub Entity); - -impl Default for EntityLayerId { - fn default() -> Self { - Self(Entity::PLACEHOLDER) - } -} - -impl PartialEq for EntityLayerId { - fn eq(&self, other: &OldEntityLayerId) -> bool { - self.0 == other.0 - } -} - -/// The value of [`EntityLayerId`] from the end of the previous tick. -#[derive(Component, Copy, Clone, PartialEq, Eq, Debug, Deref)] -pub struct OldEntityLayerId(Entity); - -impl OldEntityLayerId { - pub fn get(&self) -> Entity { - self.0 - } -} - -impl Default for OldEntityLayerId { - fn default() -> Self { - Self(Entity::PLACEHOLDER) - } -} - -impl PartialEq for OldEntityLayerId { - fn eq(&self, other: &EntityLayerId) -> bool { - self.0 == other.0 - } -} - #[derive(Component, Copy, Clone, PartialEq, Default, Debug, Deref, DerefMut)] pub struct Position(pub DVec3); @@ -225,9 +180,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, Clone, PartialEq, Default, Debug, Deref)] +#[derive(Component, Clone, PartialEq, Debug, Deref)] pub struct OldPosition(DVec3); impl OldPosition { @@ -240,6 +193,12 @@ impl OldPosition { } } +impl Default for OldPosition { + fn default() -> Self { + Self(DVec3::NAN) + } +} + impl PartialEq for OldPosition { fn eq(&self, other: &Position) -> bool { self.0 == other.0 diff --git a/crates/valence_entity/src/query.rs b/crates/valence_entity/src/query.rs index eff6dbd90..2e3649d66 100644 --- a/crates/valence_entity/src/query.rs +++ b/crates/valence_entity/src/query.rs @@ -12,12 +12,12 @@ use valence_protocol::packets::play::{ }; use valence_protocol::var_int::VarInt; use valence_protocol::ByteAngle; -use valence_server_common::UniqueId; +use valence_server_common::{LayerId, OldLayerId, UniqueId}; use crate::tracked_data::TrackedData; use crate::{ - EntityAnimations, EntityId, EntityKind, EntityLayerId, EntityStatuses, HeadYaw, Look, - ObjectData, OldEntityLayerId, OldPosition, OnGround, Position, Velocity, + EntityAnimations, EntityId, EntityKind, EntityStatuses, HeadYaw, Look, ObjectData, OldPosition, + OnGround, Position, Velocity, }; #[derive(WorldQuery)] @@ -89,8 +89,8 @@ pub struct UpdateEntityQuery { pub id: &'static EntityId, pub pos: &'static Position, pub old_pos: &'static OldPosition, - pub loc: &'static EntityLayerId, - pub old_loc: &'static OldEntityLayerId, + pub layer: &'static LayerId, + pub old_layer: &'static OldLayerId, pub look: Ref<'static, Look>, pub head_yaw: Ref<'static, HeadYaw>, pub on_ground: &'static OnGround, diff --git a/crates/valence_inventory/src/lib.rs b/crates/valence_inventory/src/lib.rs index 4498b9aac..604aa2491 100644 --- a/crates/valence_inventory/src/lib.rs +++ b/crates/valence_inventory/src/lib.rs @@ -25,6 +25,7 @@ use std::ops::Range; use bevy_app::prelude::*; use bevy_ecs::prelude::*; +use bevy_ecs::schedule::SystemSet; use derive_more::{Deref, DerefMut}; use tracing::{debug, warn}; use valence_server::client::{Client, FlushPacketsSet, SpawnClientsSet}; @@ -44,36 +45,44 @@ mod validate; pub struct InventoryPlugin; +#[derive(SystemSet, Clone, Copy, Eq, PartialEq, Hash, Debug)] +pub struct InventorySet; + impl Plugin for InventoryPlugin { fn build(&self, app: &mut bevy_app::App) { - app.add_systems( - PreUpdate, - init_new_client_inventories.after(SpawnClientsSet), - ) - .add_systems( - PostUpdate, - ( - update_client_on_close_inventory.before(update_open_inventories), - update_open_inventories, - update_player_inventories, + app.configure_set(PostUpdate, InventorySet.before(FlushPacketsSet)) + .add_systems( + PreUpdate, + init_new_client_inventories + .after(SpawnClientsSet) + .in_set(InventorySet), + ) + .add_systems( + PostUpdate, + ( + update_client_on_close_inventory.before(update_open_inventories), + update_open_inventories, + update_player_inventories, + ) + .before(FlushPacketsSet) + .in_set(InventorySet), + ) + .add_systems( + EventLoopPreUpdate, + ( + handle_update_selected_slot, + handle_click_slot, + handle_creative_inventory_action, + handle_close_handled_screen, + handle_player_actions, + ) + .in_set(InventorySet), ) - .before(FlushPacketsSet), - ) - .add_systems( - EventLoopPreUpdate, - ( - handle_update_selected_slot, - handle_click_slot, - handle_creative_inventory_action, - handle_close_handled_screen, - handle_player_actions, - ), - ) - .init_resource::() - .add_event::() - .add_event::() - .add_event::() - .add_event::(); + .init_resource::() + .add_event::() + .add_event::() + .add_event::() + .add_event::(); } } diff --git a/crates/valence_player_list/src/lib.rs b/crates/valence_player_list/src/lib.rs index db42f68b5..8a50f687f 100644 --- a/crates/valence_player_list/src/lib.rs +++ b/crates/valence_player_list/src/lib.rs @@ -24,8 +24,8 @@ use bevy_app::prelude::*; use bevy_ecs::prelude::*; use derive_more::{Deref, DerefMut}; use valence_server::client::{Client, Properties, Username}; +use valence_server::entity_layer::UpdateEntityLayerSet; use valence_server::keepalive::Ping; -use valence_server::layer::UpdateLayersPreClientSet; use valence_server::protocol::encode::PacketWriter; use valence_server::protocol::packets::play::{ player_list_s2c as packet, PlayerListHeaderS2c, PlayerListS2c, PlayerRemoveS2c, @@ -47,7 +47,7 @@ impl Plugin for PlayerListPlugin { PostUpdate, // Needs to happen before player entities are initialized. Otherwise, they will // appear invisible. - PlayerListSet.before(UpdateLayersPreClientSet), + PlayerListSet.before(UpdateEntityLayerSet), ) .add_systems( PostUpdate, diff --git a/crates/valence_protocol/src/packets/play/boss_bar_s2c.rs b/crates/valence_protocol/src/packets/play/boss_bar_s2c.rs index 409417c2c..988e28002 100644 --- a/crates/valence_protocol/src/packets/play/boss_bar_s2c.rs +++ b/crates/valence_protocol/src/packets/play/boss_bar_s2c.rs @@ -30,7 +30,7 @@ pub enum BossBarAction<'a> { } /// The color of a boss bar. -#[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode, Default)] +#[derive(Component, Copy, Clone, PartialEq, Eq, Debug, Encode, Decode, Default)] pub enum BossBarColor { #[default] Pink, @@ -43,7 +43,7 @@ pub enum BossBarColor { } /// The division of a boss bar. -#[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode, Default)] +#[derive(Component, Copy, Clone, PartialEq, Eq, Debug, Encode, Decode, Default)] pub enum BossBarDivision { #[default] NoDivision, diff --git a/crates/valence_registry/src/biome.rs b/crates/valence_registry/src/biome.rs index 6b8e5c288..c69c803b8 100644 --- a/crates/valence_registry/src/biome.rs +++ b/crates/valence_registry/src/biome.rs @@ -18,7 +18,7 @@ use valence_ident::{ident, Ident}; use valence_nbt::serde::CompoundSerializer; use crate::codec::{RegistryCodec, RegistryValue}; -use crate::{Registry, RegistryIdx, RegistrySet}; +use crate::{CachePacketsSet, Registry, RegistryIdx, UpdateRegistrySet}; pub struct BiomePlugin; @@ -26,7 +26,12 @@ impl Plugin for BiomePlugin { fn build(&self, app: &mut App) { app.init_resource::() .add_systems(PreStartup, load_default_biomes) - .add_systems(PostUpdate, update_biome_registry.before(RegistrySet)); + .add_systems( + PostUpdate, + update_biome_registry + .in_set(UpdateRegistrySet) + .before(CachePacketsSet), + ); } } diff --git a/crates/valence_registry/src/codec.rs b/crates/valence_registry/src/codec.rs index ee8d9f0ab..729109889 100644 --- a/crates/valence_registry/src/codec.rs +++ b/crates/valence_registry/src/codec.rs @@ -6,11 +6,11 @@ use tracing::error; use valence_ident::Ident; use valence_nbt::{compound, Compound, List, Value}; -use crate::RegistrySet; +use crate::CachePacketsSet; pub(super) fn build(app: &mut App) { app.init_resource::() - .add_systems(PostUpdate, cache_registry_codec.in_set(RegistrySet)); + .add_systems(PostUpdate, cache_registry_codec.in_set(CachePacketsSet)); } /// Contains the registry codec sent to all players while joining. This contains diff --git a/crates/valence_registry/src/dimension_type.rs b/crates/valence_registry/src/dimension_type.rs index 05786d8a3..1ab2085fc 100644 --- a/crates/valence_registry/src/dimension_type.rs +++ b/crates/valence_registry/src/dimension_type.rs @@ -16,7 +16,7 @@ use valence_ident::{ident, Ident}; use valence_nbt::serde::CompoundSerializer; use crate::codec::{RegistryCodec, RegistryValue}; -use crate::{Registry, RegistryIdx, RegistrySet}; +use crate::{CachePacketsSet, Registry, RegistryIdx, UpdateRegistrySet}; pub struct DimensionTypePlugin; impl Plugin for DimensionTypePlugin { @@ -25,7 +25,9 @@ impl Plugin for DimensionTypePlugin { .add_systems(PreStartup, load_default_dimension_types) .add_systems( PostUpdate, - update_dimension_type_registry.before(RegistrySet), + update_dimension_type_registry + .in_set(UpdateRegistrySet) + .before(CachePacketsSet), ); } } diff --git a/crates/valence_registry/src/lib.rs b/crates/valence_registry/src/lib.rs index d9a59b105..1651c7e7d 100644 --- a/crates/valence_registry/src/lib.rs +++ b/crates/valence_registry/src/lib.rs @@ -39,17 +39,19 @@ use valence_ident::Ident; pub struct RegistryPlugin; -/// The [`SystemSet`] where the [`RegistryCodec`](codec::RegistryCodec) and -/// [`TagsRegistry`](tags::TagsRegistry) caches are rebuilt. Systems that modify -/// the registry codec or tags registry should run _before_ this. -/// -/// This set lives in [`PostUpdate`]. +/// The [`SystemSet`] where the dynamic registry caches are rebuilt. Systems +/// that modify any of the dynamic registries should run _before_ this. Systems +/// that read the cached packets should run _after_ this. #[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct RegistrySet; +pub struct UpdateRegistrySet; + +#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct CachePacketsSet; impl Plugin for RegistryPlugin { fn build(&self, app: &mut bevy_app::App) { - app.configure_set(PostUpdate, RegistrySet); + app.configure_set(PostUpdate, UpdateRegistrySet) + .configure_set(PostUpdate, CachePacketsSet.in_set(UpdateRegistrySet)); codec::build(app); tags::build(app); @@ -109,10 +111,34 @@ impl Registry { self.items.get_mut(name.as_str()) } + #[track_caller] + pub fn by_index(&self, idx: I) -> (Ident<&str>, &V) { + let (k, v) = self + .items + .get_index(idx.to_index()) + .unwrap_or_else(|| panic!("out of bounds registry index of {}", idx.to_index())); + + (k.as_str_ident(), v) + } + + #[track_caller] + pub fn by_index_mut(&mut self, idx: I) -> (Ident<&str>, &mut V) { + let (k, v) = self + .items + .get_index_mut(idx.to_index()) + .unwrap_or_else(|| panic!("out of bounds registry index of {}", idx.to_index())); + + (k.as_str_ident(), v) + } + pub fn index_of(&self, name: Ident<&str>) -> Option { self.items.get_index_of(name.as_str()).map(I::from_index) } + pub fn len(&self) -> usize { + self.items.len() + } + pub fn iter( &self, ) -> impl DoubleEndedIterator, &V)> + ExactSizeIterator + '_ { @@ -136,19 +162,13 @@ impl Index for Registry { type Output = V; fn index(&self, index: I) -> &Self::Output { - self.items - .get_index(index.to_index()) - .unwrap_or_else(|| panic!("out of bounds registry index of {}", index.to_index())) - .1 + self.by_index(index).1 } } impl IndexMut for Registry { fn index_mut(&mut self, index: I) -> &mut Self::Output { - self.items - .get_index_mut(index.to_index()) - .unwrap_or_else(|| panic!("out of bounds registry index of {}", index.to_index())) - .1 + self.by_index_mut(index).1 } } diff --git a/crates/valence_registry/src/tags.rs b/crates/valence_registry/src/tags.rs index abcd9b92e..e3851fd99 100644 --- a/crates/valence_registry/src/tags.rs +++ b/crates/valence_registry/src/tags.rs @@ -7,7 +7,7 @@ pub use valence_protocol::packets::play::synchronize_tags_s2c::RegistryMap; use valence_protocol::packets::play::SynchronizeTagsS2c; use valence_server_common::Server; -use crate::RegistrySet; +use crate::CachePacketsSet; #[derive(Debug, Resource, Default)] pub struct TagsRegistry { @@ -18,7 +18,7 @@ pub struct TagsRegistry { pub(super) fn build(app: &mut App) { app.init_resource::() .add_systems(PreStartup, init_tags_registry) - .add_systems(PostUpdate, cache_tags_packet.in_set(RegistrySet)); + .add_systems(PostUpdate, cache_tags_packet.in_set(CachePacketsSet)); } impl TagsRegistry { diff --git a/crates/valence_scoreboard/README.md b/crates/valence_scoreboard/README.md index 886df9e36..7dbb77b67 100644 --- a/crates/valence_scoreboard/README.md +++ b/crates/valence_scoreboard/README.md @@ -2,7 +2,7 @@ This crate provides functionality for creating and managing scoreboards. In Minecraft, a scoreboard references an [`Objective`], which is a mapping from strings to scores. Typically, the string is a player name, and the score is a number of points, but the string can be any arbitrary string <= 40 chars, and the score can be any integer. -In Valence, scoreboards obey the rules implied by [`EntityLayer`]s, meaning that every Objective must have an [`EntityLayerId`] associated with it. Scoreboards are only transmitted to clients if the [`EntityLayer`] is visible to the client. +In Valence, scoreboards obey the rules implied by layers, meaning that every Objective must have an [`LayerId`] associated with it. Scoreboards are only transmitted to clients if the [`EntityLayer`] is visible to the client. To create a scoreboard, spawn an [`ObjectiveBundle`]. The [`Objective`] component represents the identifier that the client uses to reference the scoreboard. diff --git a/crates/valence_scoreboard/src/components.rs b/crates/valence_scoreboard/src/components.rs index 46974782a..ceaa2259e 100644 --- a/crates/valence_scoreboard/src/components.rs +++ b/crates/valence_scoreboard/src/components.rs @@ -2,14 +2,13 @@ use std::collections::HashMap; use bevy_ecs::prelude::*; use derive_more::{Deref, DerefMut}; -use valence_server::entity::EntityLayerId; use valence_server::protocol::packets::play::scoreboard_display_s2c::ScoreboardPosition; use valence_server::protocol::packets::play::scoreboard_objective_update_s2c::ObjectiveRenderType; use valence_server::text::IntoText; -use valence_server::Text; +use valence_server::{LayerId, Text}; /// A string that identifies an objective. There is one scoreboard per -/// objective.It's generally not safe to modify this after it's been created. +/// objective. It's generally not safe to modify this after it's been created. /// Limited to 16 characters. /// /// Directly analogous to an Objective's Name. @@ -103,7 +102,7 @@ pub struct ObjectiveBundle { pub scores: ObjectiveScores, pub old_scores: OldObjectiveScores, pub position: ScoreboardPosition, - pub layer: EntityLayerId, + pub layer: LayerId, } impl Default for ObjectiveBundle { diff --git a/crates/valence_scoreboard/src/lib.rs b/crates/valence_scoreboard/src/lib.rs index e7a2b9a68..e41b58e7e 100644 --- a/crates/valence_scoreboard/src/lib.rs +++ b/crates/valence_scoreboard/src/lib.rs @@ -26,9 +26,9 @@ use bevy_ecs::change_detection::DetectChanges; use bevy_ecs::prelude::*; pub use components::*; use tracing::{debug, warn}; -use valence_server::client::{Client, OldVisibleEntityLayers, VisibleEntityLayers}; -use valence_server::entity::EntityLayerId; -use valence_server::layer::UpdateLayersPreClientSet; +use valence_server::client::Client; +use valence_server::layer::message::LayerMessages; +use valence_server::layer::{BroadcastLayerMessagesSet, OldVisibleLayers, VisibleLayers}; use valence_server::protocol::packets::play::scoreboard_display_s2c::ScoreboardPosition; use valence_server::protocol::packets::play::scoreboard_objective_update_s2c::{ ObjectiveMode, ObjectiveRenderType, @@ -39,35 +39,27 @@ use valence_server::protocol::packets::play::{ }; use valence_server::protocol::{VarInt, WritePacket}; use valence_server::text::IntoText; -use valence_server::{Despawned, EntityLayer}; +use valence_server::{Despawned, LayerId}; /// Provides all necessary systems to manage scoreboards. pub struct ScoreboardPlugin; impl Plugin for ScoreboardPlugin { fn build(&self, app: &mut App) { - app.configure_set(PostUpdate, ScoreboardSet.before(UpdateLayersPreClientSet)); - - app.add_systems( - PostUpdate, - ( - create_or_update_objectives, - display_objectives.after(create_or_update_objectives), - ) - .in_set(ScoreboardSet), - ) - .add_systems( - PostUpdate, - remove_despawned_objectives.in_set(ScoreboardSet), - ) - .add_systems(PostUpdate, handle_new_clients.in_set(ScoreboardSet)) - .add_systems( - PostUpdate, - update_scores - .after(create_or_update_objectives) - .after(handle_new_clients) - .in_set(ScoreboardSet), - ); + app.configure_set(PostUpdate, ScoreboardSet.before(BroadcastLayerMessagesSet)) + .add_systems( + PostUpdate, + ( + handle_new_clients, + create_or_update_objectives, + display_objectives.after(create_or_update_objectives), + remove_despawned_objectives, + update_scores + .after(create_or_update_objectives) + .after(handle_new_clients), + ) + .in_set(ScoreboardSet), + ); } } @@ -80,11 +72,11 @@ fn create_or_update_objectives( Ref, &ObjectiveDisplay, &ObjectiveRenderType, - &EntityLayerId, + &LayerId, ), Or<(Changed, Changed)>, >, - mut layers: Query<&mut EntityLayer>, + mut layers: Query<&mut LayerMessages>, ) { for (objective, display, render_type, entity_layer) in objectives.iter() { if objective.name().is_empty() { @@ -119,11 +111,8 @@ fn create_or_update_objectives( /// Must occur after `create_or_update_objectives`. fn display_objectives( - objectives: Query< - (&Objective, Ref, &EntityLayerId), - Changed, - >, - mut layers: Query<&mut EntityLayer>, + objectives: Query<(&Objective, Ref, &LayerId), Changed>, + mut layers: Query<&mut LayerMessages>, ) { for (objective, position, entity_layer) in objectives.iter() { let packet = ScoreboardDisplayS2c { @@ -145,8 +134,8 @@ fn display_objectives( fn remove_despawned_objectives( mut commands: Commands, - objectives: Query<(Entity, &Objective, &EntityLayerId), With>, - mut layers: Query<&mut EntityLayer>, + objectives: Query<(Entity, &Objective, &LayerId), With>, + mut layers: Query<&mut LayerMessages>, ) { for (entity, objective, entity_layer) in objectives.iter() { commands.entity(entity).despawn(); @@ -167,8 +156,8 @@ fn remove_despawned_objectives( fn handle_new_clients( mut clients: Query< - (&mut Client, &VisibleEntityLayers, &OldVisibleEntityLayers), - Or<(Added, Changed)>, + (&mut Client, &VisibleLayers, &OldVisibleLayers), + Or<(Added, Changed)>, >, objectives: Query< ( @@ -177,7 +166,7 @@ fn handle_new_clients( &ObjectiveRenderType, &ScoreboardPosition, &ObjectiveScores, - &EntityLayerId, + &LayerId, ), Without, >, @@ -185,10 +174,8 @@ fn handle_new_clients( // Remove objectives from the old visible layers that are not in the new visible // layers. for (mut client, visible_layers, old_visible_layers) in clients.iter_mut() { - let removed_layers: BTreeSet<_> = old_visible_layers - .get() - .difference(&visible_layers.0) - .collect(); + let removed_layers: BTreeSet<_> = + old_visible_layers.difference(&visible_layers.0).collect(); for (objective, _, _, _, _, layer) in objectives.iter() { if !removed_layers.contains(&layer.0) { @@ -211,7 +198,7 @@ fn handle_new_clients( } else { visible_layers .0 - .difference(old_visible_layers.get()) + .difference(&old_visible_layers) .copied() .collect::>() }; @@ -254,11 +241,11 @@ fn update_scores( &Objective, &ObjectiveScores, &mut OldObjectiveScores, - &EntityLayerId, + &LayerId, ), (Changed, Without), >, - mut layers: Query<&mut EntityLayer>, + mut layers: Query<&mut LayerMessages>, ) { for (objective, scores, mut old_scores, entity_layer) in objectives.iter_mut() { let Ok(mut layer) = layers.get_mut(entity_layer.0) else { diff --git a/crates/valence_server/src/abilities.rs b/crates/valence_server/src/abilities.rs index 35b22f913..e46e8955c 100644 --- a/crates/valence_server/src/abilities.rs +++ b/crates/valence_server/src/abilities.rs @@ -5,8 +5,9 @@ pub use valence_protocol::packets::play::player_abilities_s2c::PlayerAbilitiesFl use valence_protocol::packets::play::{PlayerAbilitiesS2c, UpdatePlayerAbilitiesC2s}; use valence_protocol::{GameMode, WritePacket}; -use crate::client::{update_game_mode, Client, UpdateClientsSet}; +use crate::client::{Client, FlushPacketsSet}; use crate::event_loop::{EventLoopPreUpdate, PacketEvent}; +use crate::game_mode::UpdateGameModeSet; /// [`Component`] that stores the player's flying speed ability. /// @@ -34,13 +35,13 @@ impl Default for FovModifier { } /// Send if the client sends [`UpdatePlayerAbilitiesC2s::StartFlying`] -#[derive(Event)] +#[derive(Event, Debug)] pub struct PlayerStartFlyingEvent { pub client: Entity, } /// Send if the client sends [`UpdatePlayerAbilitiesC2s::StopFlying`] -#[derive(Event)] +#[derive(Event, Debug)] pub struct PlayerStopFlyingEvent { pub client: Entity, } @@ -62,18 +63,24 @@ pub struct PlayerStopFlyingEvent { /// according to the packet pub struct AbilitiesPlugin; +#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct AbilitiesSet; + impl Plugin for AbilitiesPlugin { fn build(&self, app: &mut App) { app.add_event::() .add_event::() + .configure_set( + PostUpdate, + AbilitiesSet + .before(FlushPacketsSet) + .after(UpdateGameModeSet), + ) .add_systems( PostUpdate, - ( - update_client_player_abilities, - update_player_abilities.before(update_client_player_abilities), - ) - .in_set(UpdateClientsSet) - .after(update_game_mode), + (update_player_abilities, update_client_player_abilities) + .chain() + .in_set(AbilitiesSet), ) .add_systems(EventLoopPreUpdate, update_server_player_abilities); } diff --git a/crates/valence_server/src/action.rs b/crates/valence_server/src/action.rs index cb45fb71e..8e0c16a82 100644 --- a/crates/valence_server/src/action.rs +++ b/crates/valence_server/src/action.rs @@ -5,19 +5,20 @@ use valence_protocol::packets::play::player_action_c2s::PlayerAction; use valence_protocol::packets::play::{PlayerActionC2s, PlayerActionResponseS2c}; use valence_protocol::{BlockPos, Direction, VarInt, WritePacket}; -use crate::client::{Client, UpdateClientsSet}; +use crate::client::{Client, FlushPacketsSet}; use crate::event_loop::{EventLoopPreUpdate, PacketEvent}; pub struct ActionPlugin; +#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct ActionSet; + impl Plugin for ActionPlugin { fn build(&self, app: &mut App) { app.add_event::() + .configure_set(PostUpdate, ActionSet.before(FlushPacketsSet)) .add_systems(EventLoopPreUpdate, handle_player_action) - .add_systems( - PostUpdate, - acknowledge_player_actions.in_set(UpdateClientsSet), - ); + .add_systems(PostUpdate, acknowledge_player_actions.in_set(ActionSet)); } } diff --git a/crates/valence_server/src/chunk_view.rs b/crates/valence_server/src/chunk_view.rs index 134949faf..b5e81e2ae 100644 --- a/crates/valence_server/src/chunk_view.rs +++ b/crates/valence_server/src/chunk_view.rs @@ -32,9 +32,9 @@ impl ChunkView { self.dist } - pub const fn contains(self, pos: ChunkPos) -> bool { + pub fn contains(self, pos: impl Into) -> bool { let true_dist = self.dist as u64 + EXTRA_VIEW_RADIUS as u64; - self.pos.distance_squared(pos) <= true_dist * true_dist + self.pos.distance_squared(pos.into()) <= true_dist * true_dist } /// Returns an iterator over all the chunk positions in this view. Positions diff --git a/crates/valence_server/src/client.rs b/crates/valence_server/src/client.rs index 0cca3cf27..d8ec8a73f 100644 --- a/crates/valence_server/src/client.rs +++ b/crates/valence_server/src/client.rs @@ -1,5 +1,4 @@ use std::borrow::Cow; -use std::collections::BTreeSet; use std::fmt; use std::net::IpAddr; use std::time::Instant; @@ -8,36 +7,29 @@ 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 derive_more::{Deref, DerefMut, From, Into}; use tracing::warn; use uuid::Uuid; use valence_entity::player::PlayerEntityBundle; -use valence_entity::query::EntityInitQuery; use valence_entity::tracked_data::TrackedData; -use valence_entity::{ - ClearEntityChangesSet, EntityId, EntityStatus, OldPosition, Position, Velocity, -}; +use valence_entity::{EntityStatus, OldPosition, Position, Velocity}; use valence_math::{DVec3, Vec3}; use valence_protocol::encode::{PacketEncoder, WritePacket}; -use valence_protocol::packets::play::chunk_biome_data_s2c::ChunkBiome; use valence_protocol::packets::play::game_state_change_s2c::GameEventKind; use valence_protocol::packets::play::particle_s2c::Particle; use valence_protocol::packets::play::{ - ChunkBiomeDataS2c, ChunkLoadDistanceS2c, ChunkRenderDistanceCenterS2c, DeathMessageS2c, - DisconnectS2c, EntitiesDestroyS2c, EntityStatusS2c, EntityTrackerUpdateS2c, - EntityVelocityUpdateS2c, GameStateChangeS2c, ParticleS2c, PlaySoundS2c, UnloadChunkS2c, + DeathMessageS2c, DisconnectS2c, EntitiesDestroyS2c, EntityStatusS2c, EntityTrackerUpdateS2c, + EntityVelocityUpdateS2c, GameStateChangeS2c, ParticleS2c, PlaySoundS2c, }; use valence_protocol::profile::Property; use valence_protocol::sound::{Sound, SoundCategory, SoundId}; use valence_protocol::text::{IntoText, Text}; use valence_protocol::var_int::VarInt; -use valence_protocol::{BlockPos, ChunkPos, Encode, GameMode, Packet}; -use valence_registry::RegistrySet; +use valence_protocol::{Encode, GameMode, Packet}; use valence_server_common::{Despawned, UniqueId}; -use crate::layer::{ChunkLayer, EntityLayer, UpdateLayersPostClientSet, UpdateLayersPreClientSet}; +use crate::layer::{OldVisibleLayers, VisibleLayers}; use crate::ChunkView; pub struct ClientPlugin; @@ -54,47 +46,23 @@ pub struct FlushPacketsSet; #[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] pub struct SpawnClientsSet; -/// The system set where various facets of the client are updated. Systems that -/// modify layers should run _before_ this. #[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct UpdateClientsSet; +pub struct SelfTrackedDataSet; impl Plugin for ClientPlugin { fn build(&self, app: &mut App) { - app.add_systems( - PostUpdate, - ( + app.init_resource::() + .configure_set(PostUpdate, SelfTrackedDataSet.before(FlushPacketsSet)) + .add_systems( + PostUpdate, ( - crate::spawn::initial_join.after(RegistrySet), - update_chunk_load_dist, - handle_layer_messages.after(update_chunk_load_dist), - update_view_and_layers - .after(crate::spawn::initial_join) - .after(handle_layer_messages), - cleanup_chunks_after_client_despawn.after(update_view_and_layers), - crate::spawn::update_respawn_position.after(update_view_and_layers), - crate::spawn::respawn.after(crate::spawn::update_respawn_position), - 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), - ), - ) - .configure_set(PreUpdate, SpawnClientsSet) - .configure_sets( - PostUpdate, - ( - UpdateClientsSet - .after(UpdateLayersPreClientSet) - .before(UpdateLayersPostClientSet) - .before(FlushPacketsSet), - ClearEntityChangesSet.after(UpdateClientsSet), - FlushPacketsSet, - ), - ); + flush_packets.in_set(FlushPacketsSet), + (init_self_tracked_data, update_self_tracked_data) + .chain() + .in_set(SelfTrackedDataSet), + despawn_disconnected_clients, + ), + ); } } @@ -104,23 +72,21 @@ impl Plugin for ClientPlugin { pub struct ClientBundle { pub marker: ClientMarker, pub client: Client, + pub visible_layers: VisibleLayers, + pub old_visible_layers: OldVisibleLayers, pub settings: crate::client_settings::ClientSettings, pub entity_remove_buf: EntityRemoveBuf, pub username: Username, pub ip: Ip, pub properties: Properties, - pub respawn_pos: crate::spawn::RespawnPosition, + pub respawn_pos: crate::movement::RespawnPosition, pub op_level: crate::op_level::OpLevel, pub action_sequence: crate::action::ActionSequence, pub view_distance: ViewDistance, pub old_view_distance: OldViewDistance, - 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: crate::keepalive::KeepaliveState, pub ping: crate::keepalive::Ping, - pub teleport_state: crate::teleport::TeleportState, + pub teleport_state: crate::movement::TeleportState, pub game_mode: GameMode, pub prev_game_mode: crate::spawn::PrevGameMode, pub death_location: crate::spawn::DeathLocation, @@ -145,6 +111,8 @@ impl ClientBundle { conn: args.conn, enc: args.enc, }, + visible_layers: Default::default(), + old_visible_layers: Default::default(), settings: Default::default(), entity_remove_buf: Default::default(), username: Username(args.username), @@ -154,14 +122,10 @@ impl ClientBundle { op_level: Default::default(), action_sequence: Default::default(), view_distance: Default::default(), - old_view_distance: OldViewDistance(2), - visible_chunk_layer: Default::default(), - old_visible_chunk_layer: OldVisibleChunkLayer(Entity::PLACEHOLDER), - visible_entity_layers: Default::default(), - old_visible_entity_layers: OldVisibleEntityLayers(BTreeSet::new()), - keepalive_state: crate::keepalive::KeepaliveState::new(), + old_view_distance: Default::default(), + keepalive_state: Default::default(), ping: Default::default(), - teleport_state: crate::teleport::TeleportState::new(), + teleport_state: Default::default(), game_mode: GameMode::default(), prev_game_mode: Default::default(), death_location: Default::default(), @@ -275,13 +239,9 @@ impl Client { } /// Flushes the packet queue to the underlying connection. - /// - /// This is called automatically at the end of the tick and when the client - /// is dropped. Unless you're in a hurry, there's usually no reason to - /// call this method yourself. - /// - /// Returns an error if flushing was unsuccessful. - pub fn flush_packets(&mut self) -> anyhow::Result<()> { + // Note: This is private because flushing packets early would make us miss + // prepended packets. + fn flush_packets(&mut self) -> anyhow::Result<()> { let bytes = self.enc.take(); if !bytes.is_empty() { self.conn.try_send(bytes) @@ -483,9 +443,15 @@ impl Default for ViewDistance { /// The [`ViewDistance`] at the end of the previous tick. Automatically updated /// as [`ViewDistance`] is changed. -#[derive(Component, Clone, PartialEq, Eq, Default, Debug, Deref)] +#[derive(Component, Clone, PartialEq, Eq, Debug, Deref)] pub struct OldViewDistance(u8); +impl Default for OldViewDistance { + fn default() -> Self { + Self(2) + } +} + impl OldViewDistance { pub fn get(&self) -> u8 { self.0 @@ -516,564 +482,35 @@ impl OldViewItem<'_> { } } -/// 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, Deref, DerefMut)] -pub struct VisibleChunkLayer(pub Entity); - -impl Default for VisibleChunkLayer { - fn default() -> Self { - Self(Entity::PLACEHOLDER) - } -} - -/// The value of [`VisibleChunkLayer`] from the end of the previous tick. -#[derive(Component, PartialEq, Eq, Debug, Deref)] -pub struct OldVisibleChunkLayer(Entity); - -impl OldVisibleChunkLayer { - pub fn get(&self) -> Entity { - self.0 - } +#[derive(Resource, Debug)] +pub struct ClientDespawnSettings { + /// If disconnected clients should automatically have the [`Despawned`] + /// component added to them. Without this enabled, clients entities must be + /// removed from the world manually. + pub despawn_disconnected_clients: bool, } -/// 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, Deref)] -pub struct OldVisibleEntityLayers(BTreeSet); - -impl OldVisibleEntityLayers { - pub fn get(&self) -> &BTreeSet { - &self.0 +impl Default for ClientDespawnSettings { + fn default() -> Self { + Self { + despawn_disconnected_clients: true, + } } } /// A system for adding [`Despawned`] components to disconnected clients. This /// works by listening for removed [`Client`] components. -pub fn despawn_disconnected_clients( +fn despawn_disconnected_clients( mut commands: Commands, mut disconnected_clients: RemovedComponents, + cfg: Res, ) { - for entity in disconnected_clients.iter() { - if let Some(mut entity) = commands.get_entity(entity) { - entity.insert(Despawned); - } - } -} - -fn update_chunk_load_dist( - mut clients: Query<(&mut Client, &ViewDistance, &OldViewDistance), Changed>, -) { - for (mut client, dist, old_dist) in &mut clients { - if client.is_added() { - // Join game packet includes the view distance. - continue; - } - - if dist.0 != old_dist.0 { - // Note: This packet is just aesthetic. - client.write_packet(&ChunkLoadDistanceS2c { - view_distance: VarInt(dist.0.into()), - }); - } - } -} - -fn handle_layer_messages( - mut clients: Query<( - Entity, - &EntityId, - &mut Client, - &mut EntityRemoveBuf, - OldView, - &OldVisibleChunkLayer, - &mut VisibleEntityLayers, - &OldVisibleEntityLayers, - )>, - chunk_layers: Query<&ChunkLayer>, - entity_layers: Query<&EntityLayer>, - entities: Query<(EntityInitQuery, &OldPosition)>, -) { - clients.par_iter_mut().for_each_mut( - |( - self_entity, - self_entity_id, - mut client, - mut remove_buf, - old_view, - old_visible_chunk_layer, - mut visible_entity_layers, - old_visible_entity_layers, - )| { - let block_pos = BlockPos::from(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 + if cfg.despawn_disconnected_clients { + for entity in disconnected_clients.iter() { + if let Some(mut entity) = commands.get_entity(entity) { + entity.insert(Despawned); } - - // 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 { - crate::layer::chunk::GlobalMsg::Packet => { - client.write_packet_bytes(&bytes[range]); - } - crate::layer::chunk::GlobalMsg::PacketExcept { except } => { - if self_entity != except { - client.write_packet_bytes(&bytes[range]); - } - } - } - } - - let mut chunk_biome_buf = vec![]; - - // Local messages - messages.query_local(old_view, |msg, range| match msg { - crate::layer::chunk::LocalMsg::PacketAt { .. } => { - client.write_packet_bytes(&bytes[range]); - } - crate::layer::chunk::LocalMsg::PacketAtExcept { except, .. } => { - if self_entity != except { - client.write_packet_bytes(&bytes[range]); - } - } - crate::layer::chunk::LocalMsg::RadiusAt { - center, - radius_squared, - } => { - if in_radius(block_pos, center, radius_squared) { - client.write_packet_bytes(&bytes[range]); - } - } - crate::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]); - } - } - crate::layer::chunk::LocalMsg::ChangeBiome { pos } => { - chunk_biome_buf.push(ChunkBiome { - pos, - data: &bytes[range], - }); - } - crate::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. - debug_assert!(chunk_layer.chunk(pos).is_none()); - } - [.., ChunkLayer::LOAD | ChunkLayer::OVERWRITE] => { - // Load chunk. - 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"), - } - } - }); - - if !chunk_biome_buf.is_empty() { - client.write_packet(&ChunkBiomeDataS2c { - chunks: chunk_biome_buf.into(), - }); - } - } - - // 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 { - crate::layer::entity::GlobalMsg::Packet => { - client.write_packet_bytes(&bytes[range]); - } - crate::layer::entity::GlobalMsg::PacketExcept { except } => { - if self_entity != except { - client.write_packet_bytes(&bytes[range]); - } - } - crate::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 { - crate::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); - } - } - } - } - crate::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); - } - } - } - } - crate::layer::entity::LocalMsg::SpawnEntity { pos: _, src_layer } => { - if !old_visible_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) { - 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. - init.write_init_packets(old_pos.get(), &mut *client); - } - } - } - } - } - crate::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) { - 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. - init.write_init_packets(old_pos.get(), &mut *client); - } - } - } - } - } - crate::layer::entity::LocalMsg::PacketAt { pos: _ } => { - client.write_packet_bytes(&bytes[range]); - } - crate::layer::entity::LocalMsg::PacketAtExcept { pos: _, except } => { - if self_entity != except { - client.write_packet_bytes(&bytes[range]); - } - } - crate::layer::entity::LocalMsg::RadiusAt { - center, - radius_squared, - } => { - if in_radius(block_pos, center, radius_squared) { - client.write_packet_bytes(&bytes[range]); - } - } - crate::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); - } - } - }, - ); -} - -pub(crate) fn update_view_and_layers( - mut clients: Query< - ( - Entity, - &mut Client, - &mut EntityRemoveBuf, - &VisibleChunkLayer, - &mut OldVisibleChunkLayer, - Ref, - &mut OldVisibleEntityLayers, - &Position, - &OldPosition, - &ViewDistance, - &OldViewDistance, - ), - Or<( - Changed, - Changed, - Changed, - Changed, - )>, - >, - 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, - chunk_layer, - mut old_chunk_layer, - visible_entity_layers, - mut old_visible_entity_layers, - pos, - old_pos, - view_dist, - old_view_dist, - )| { - let view = ChunkView::new(ChunkPos::from(pos.0), view_dist.0); - let old_view = ChunkView::new(ChunkPos::from(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 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) { - client.write_packet(&UnloadChunkS2c { pos }); - 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) { - chunk.write_init_packets(&mut *client, pos, layer.info()); - chunk.inc_viewer_count(); - } - } - } - - // 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()); - } - } - } - } - } - } - - 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) { - 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); - } - } - } - } - } - } - } else { - // Update the client's visible entity layers. - 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 - .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()); - } - } - } - } - } - } - - 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 - .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); - } - } - } - } - } - } - } - - // 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. - - // 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) { - 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) { - chunk.write_init_packets(&mut *client, pos, layer.info()); - chunk.inc_viewer_count(); - } - } - } - - // 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()); - } - } - } - } - } - } - - // 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); - } - } - } - } - } - } - } - } - - // 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); - } - }, - ); -} - -pub(crate) fn update_game_mode(mut clients: Query<(&mut Client, &GameMode), Changed>) { - for (mut client, game_mode) in &mut clients { - if client.is_added() { - // Game join packet includes the initial game mode. - continue; } - - client.write_packet(&GameStateChangeS2c { - kind: GameEventKind::ChangeGameMode, - value: *game_mode as i32 as f32, - }) } } @@ -1097,7 +534,7 @@ fn flush_packets( } } -fn init_tracked_data(mut clients: Query<(&mut Client, &TrackedData), Added>) { +fn init_self_tracked_data(mut clients: Query<(&mut Client, &TrackedData), Added>) { for (mut client, tracked_data) in &mut clients { if let Some(init_data) = tracked_data.init_data() { client.write_packet(&EntityTrackerUpdateS2c { @@ -1108,7 +545,7 @@ fn init_tracked_data(mut clients: Query<(&mut Client, &TrackedData), Added) { +fn update_self_tracked_data(mut clients: Query<(&mut Client, &TrackedData)>) { for (mut client, tracked_data) in &mut clients { if let Some(update_data) = tracked_data.update_data() { client.write_packet(&EntityTrackerUpdateS2c { @@ -1118,19 +555,3 @@ 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_server/src/dimension_layer.rs b/crates/valence_server/src/dimension_layer.rs new file mode 100644 index 000000000..949b8f01d --- /dev/null +++ b/crates/valence_server/src/dimension_layer.rs @@ -0,0 +1,310 @@ +pub mod batch; +pub mod block; +pub mod chunk; +pub mod chunk_index; +mod plugin; + +use bevy_ecs::prelude::*; +use bevy_ecs::query::WorldQuery; +use block::BlockRef; +pub use chunk::{Chunk, LoadedChunk}; +pub use chunk_index::ChunkIndex; +pub use plugin::*; +use valence_protocol::packets::play::UnloadChunkS2c; +use valence_protocol::{BiomePos, BlockPos, ChunkPos, CompressionThreshold, WritePacket}; +use valence_registry::biome::BiomeId; +use valence_registry::dimension_type::DimensionTypeId; +use valence_registry::{BiomeRegistry, DimensionTypeRegistry}; +use valence_server_common::Server; + +use self::batch::Batch; +use self::block::Block; +use crate::layer::message::{LayerMessages, MessageScope}; +use crate::layer::{ChunkViewIndex, LayerViewers}; + +#[derive(Component)] +pub struct DimensionLayerBundle { + pub chunk_index: ChunkIndex, + pub block_batch: Batch, + pub dimension_info: DimensionInfo, + pub chunk_view_index: ChunkViewIndex, + pub layer_viewers: LayerViewers, + pub layer_messages: LayerMessages, +} + +impl DimensionLayerBundle { + pub fn new( + dimension_type: DimensionTypeId, + dimensions: &DimensionTypeRegistry, + biomes: &BiomeRegistry, + server: &Server, + ) -> Self { + let dim = &dimensions[dimension_type]; + + Self { + chunk_index: ChunkIndex::new(dim.height, dim.min_y), + block_batch: Default::default(), + dimension_info: DimensionInfo { + dimension_type, + height: dim.height, + min_y: dim.min_y, + biome_registry_len: biomes.iter().len() as i32, + threshold: server.compression_threshold(), + }, + chunk_view_index: Default::default(), + layer_viewers: Default::default(), + layer_messages: LayerMessages::new(server.compression_threshold()), + } + } +} + +#[derive(WorldQuery)] +#[world_query(mutable)] +pub struct DimensionLayerQuery { + pub chunk_index: &'static mut ChunkIndex, + pub batch: &'static mut Batch, + pub dimension_info: &'static DimensionInfo, + pub chunk_view_index: &'static mut ChunkViewIndex, + pub layer_viewers: &'static LayerViewers, + pub layer_messages: &'static mut LayerMessages, +} + +macro_rules! immutable_query_methods { + () => { + pub fn dimension_type(&self) -> DimensionTypeId { + self.dimension_info.dimension_type + } + + pub fn height(&self) -> i32 { + self.dimension_info.height + } + + pub fn min_y(&self) -> i32 { + self.dimension_info.min_y + } + + pub fn biome(&self, pos: impl Into) -> Option { + self.chunk_index.biome(pos) + } + + pub fn block(&self, pos: impl Into) -> Option { + self.chunk_index.block(pos) + } + + pub fn chunk(&self, pos: impl Into) -> Option<&LoadedChunk> { + self.chunk_index.get(pos) + } + }; +} + +impl DimensionLayerQueryItem<'_> { + immutable_query_methods!(); + + pub fn set_block(&mut self, pos: impl Into, block: impl Into) { + self.batch.set_block(pos, block) + } + + pub fn set_biome(&mut self, pos: impl Into, biome: BiomeId) { + self.batch.set_biome(pos, biome) + } + + /// Apply the batched block and biome mutations to the layer. + pub fn apply_batch(&mut self) { + self.batch.apply( + &mut *self.chunk_index, + self.dimension_info, + &mut *self.layer_messages, + ); + + self.batch.clear(); + } + + pub fn chunk_mut(&mut self, pos: impl Into) -> Option<&mut LoadedChunk> { + self.chunk_index.get_mut(pos) + } + + pub fn insert_chunk(&mut self, pos: impl Into, chunk: Chunk) -> Option { + match self.chunk_entry(pos) { + Entry::Occupied(mut entry) => Some(entry.insert(chunk)), + Entry::Vacant(entry) => { + entry.insert(chunk); + None + } + } + } + + pub fn remove_chunk(&mut self, pos: impl Into) -> Option { + match self.chunk_entry(pos) { + Entry::Occupied(entry) => Some(entry.remove()), + Entry::Vacant(_) => None, + } + } + + pub fn chunk_entry(&mut self, pos: impl Into) -> Entry { + match self.chunk_index.entry(pos) { + chunk_index::Entry::Occupied(entry) => Entry::Occupied(OccupiedEntry { + layer_messages: self.layer_messages.reborrow(), + info: &self.dimension_info, + entry, + }), + chunk_index::Entry::Vacant(entry) => Entry::Vacant(VacantEntry { + chunk_view_index: &*self.chunk_view_index, + layer_messages: self.layer_messages.reborrow(), + info: &self.dimension_info, + entry, + }), + } + } + + pub fn retain_chunks(&mut self, mut f: impl FnMut(ChunkPos, &mut LoadedChunk) -> bool) { + self.chunk_index.retain(|pos, chunk| { + if f(pos, chunk) { + true + } else { + if chunk.viewer_count > 0 { + self.layer_messages + .send_packet(MessageScope::ChunkView { pos }, &UnloadChunkS2c { pos }); + } + + false + } + }); + } +} + +impl DimensionLayerQueryReadOnlyItem<'_> { + immutable_query_methods!(); +} + +#[derive(Component, Clone, Debug)] +pub struct DimensionInfo { + dimension_type: DimensionTypeId, + height: i32, + min_y: i32, + biome_registry_len: i32, + threshold: CompressionThreshold, +} + +impl DimensionInfo { + pub fn dimension_type(&self) -> DimensionTypeId { + self.dimension_type + } + + pub fn height(&self) -> i32 { + self.height + } + + pub fn min_y(&self) -> i32 { + self.height + } +} + +#[derive(Debug)] +pub enum Entry<'a> { + Occupied(OccupiedEntry<'a>), + Vacant(VacantEntry<'a>), +} + +impl<'a> Entry<'a> { + pub fn or_default(self) -> &'a mut LoadedChunk { + match self { + Entry::Occupied(oe) => oe.into_mut(), + Entry::Vacant(ve) => ve.insert(Chunk::new()), + } + } +} + +#[derive(Debug)] +pub struct OccupiedEntry<'a> { + layer_messages: Mut<'a, LayerMessages>, + info: &'a DimensionInfo, + entry: chunk_index::OccupiedEntry<'a>, +} + +impl<'a> OccupiedEntry<'a> { + pub fn get(&self) -> &LoadedChunk { + self.entry.get() + } + + pub fn get_mut(&mut self) -> &mut LoadedChunk { + self.entry.get_mut() + } + + pub fn insert(&mut self, chunk: Chunk) -> Chunk { + let pos = *self.key(); + + let viewer_count = self.entry.get().viewer_count; + let res = self.entry.insert(chunk); + + if viewer_count > 0 { + let loaded = self.entry.get_mut(); + + let w = self + .layer_messages + .packet_writer(MessageScope::ChunkView { pos }); + + loaded.write_chunk_init_packet(w, pos, self.info); + loaded.viewer_count = viewer_count; + } + + res + } + + pub fn into_mut(self) -> &'a mut LoadedChunk { + self.entry.into_mut() + } + + pub fn key(&self) -> &ChunkPos { + self.entry.key() + } + + pub fn remove(self) -> Chunk { + self.remove_entry().1 + } + + pub fn remove_entry(mut self) -> (ChunkPos, Chunk) { + if self.get().viewer_count > 0 { + self.layer_messages + .write_packet(&UnloadChunkS2c { pos: *self.key() }); + } + + self.entry.remove_entry() + } +} + +#[derive(Debug)] +pub struct VacantEntry<'a> { + chunk_view_index: &'a ChunkViewIndex, + layer_messages: Mut<'a, LayerMessages>, + info: &'a DimensionInfo, + entry: chunk_index::VacantEntry<'a>, +} + +impl<'a> VacantEntry<'a> { + pub fn insert(mut self, chunk: Chunk) -> &'a mut LoadedChunk { + let pos = *self.key(); + + let viewer_count = self.chunk_view_index.get(pos).len(); + + let loaded = self.entry.insert(chunk); + + if viewer_count > 0 { + let w = self + .layer_messages + .packet_writer(MessageScope::ChunkView { pos }); + + loaded.write_chunk_init_packet(w, pos, self.info); + loaded.viewer_count = viewer_count as u32; + } + + loaded + } + + pub fn into_key(self) -> ChunkPos { + self.entry.into_key() + } + + pub fn key(&self) -> &ChunkPos { + self.entry.key() + } +} diff --git a/crates/valence_server/src/dimension_layer/batch.rs b/crates/valence_server/src/dimension_layer/batch.rs new file mode 100644 index 000000000..51928b186 --- /dev/null +++ b/crates/valence_server/src/dimension_layer/batch.rs @@ -0,0 +1,248 @@ +use std::borrow::Cow; + +use bevy_ecs::prelude::*; +use bitfield_struct::bitfield; +use valence_generated::block::BlockEntityKind; +use valence_nbt::Compound; +use valence_protocol::packets::play::chunk_delta_update_s2c::ChunkDeltaUpdateEntry; +use valence_protocol::packets::play::{BlockEntityUpdateS2c, BlockUpdateS2c, ChunkDeltaUpdateS2c}; +use valence_protocol::{BiomePos, BlockPos, ChunkSectionPos, WritePacket}; +use valence_registry::biome::BiomeId; + +use super::block::Block; +use super::chunk::LoadedChunk; +use super::{ChunkIndex, DimensionInfo}; +use crate::dimension_layer::chunk::ChunkOps; +use crate::layer::message::{LayerMessages, MessageScope}; +use crate::BlockState; + +/// Batched block and biome mutations. +/// +/// Changes are automatically applied at the end of the tick or when +/// [`apply_batch`] is called. +/// +/// [`apply_batch`]: super::DimensionLayerQueryItem::apply_batch +#[derive(Component, Default)] +pub struct Batch { + state_updates: Vec, + block_entities: Vec<(BlockPos, BlockEntityKind, Compound)>, + sect_update_buf: Vec, +} + +impl Batch { + pub fn set_block(&mut self, pos: impl Into, block: impl Into) { + let pos = pos.into(); + let block = block.into(); + + self.state_updates + .push(StateUpdate::from_parts(pos, block.state)); + + if let (Some(nbt), Some(kind)) = (block.nbt, block.state.block_entity_kind()) { + self.block_entities.push((pos, kind, nbt)); + } + } + + pub fn set_biome(&mut self, pos: impl Into, biome: BiomeId) { + todo!() + } + + pub fn clear(&mut self) { + self.state_updates.clear(); + self.block_entities.clear(); + } + + pub fn shrink_to_fit(&mut self) { + let Self { + state_updates, + block_entities, + sect_update_buf, + } = self; + + state_updates.shrink_to_fit(); + block_entities.shrink_to_fit(); + sect_update_buf.shrink_to_fit(); + } + + pub fn is_empty(&self) -> bool { + self.state_updates.is_empty() + } + + pub(super) fn apply( + &mut self, + chunks: &mut ChunkIndex, + info: &DimensionInfo, + messages: &mut LayerMessages, + ) { + debug_assert!(self.sect_update_buf.is_empty()); + + // Sort block state updates so that they're grouped by chunk section. + // Sort in reverse so the dedup keeps the last of consecutive elements. + self.state_updates.sort_unstable_by(|a, b| b.cmp(a)); + + // Eliminate redundant block assignments. + self.state_updates + .dedup_by_key(|u| u.0 & StateUpdate::BLOCK_POS_MASK); + + let mut chunk: Option<&mut LoadedChunk> = None; + let mut sect_pos = ChunkSectionPos::new(i32::MIN, i32::MIN, i32::MIN); + + let mut flush_sect_updates = + |sect_pos: ChunkSectionPos, buf: &mut Vec| { + let mut w = messages.packet_writer(MessageScope::ChunkView { + pos: sect_pos.into(), + }); + + match buf.as_slice() { + // Zero updates. Do nothing. + &[] => {} + // One update. Send singular block update packet. + &[update] => { + w.write_packet(&BlockUpdateS2c { + position: BlockPos { + x: sect_pos.x * 16 + update.off_x() as i32, + y: sect_pos.y * 16 + update.off_y() as i32, + z: sect_pos.z * 16 + update.off_z() as i32, + }, + block_id: BlockState::from_raw(update.block_state() as u16).unwrap(), + }); + + buf.clear(); + } + // >1 updates. Send special section update packet. + updates => { + w.write_packet(&ChunkDeltaUpdateS2c { + chunk_sect_pos: sect_pos, + blocks: Cow::Borrowed(updates), + }); + + buf.clear(); + } + } + }; + + // For each block state change... + for (pos, state) in self.state_updates.drain(..).map(StateUpdate::to_parts) { + let new_sect_pos = ChunkSectionPos::from(pos); + + // Is this block in a new section? If it is, then flush the changes we've + // accumulated for the old section. + if sect_pos != new_sect_pos { + flush_sect_updates(sect_pos, &mut self.sect_update_buf); + + // Update the chunk ref if the chunk pos changed. + if sect_pos.x != new_sect_pos.x || sect_pos.z != new_sect_pos.z { + chunk = chunks.get_mut(new_sect_pos); + } + + // Update section pos + sect_pos = new_sect_pos; + } + + // Apply block state update to chunk. + if let Some(chunk) = &mut chunk { + let chunk_y = pos.y.wrapping_sub(info.min_y) as u32; + + // Is the block pos in bounds of the chunk? + if chunk_y < info.height as u32 { + let chunk_x = pos.x.rem_euclid(16); + let chunk_z = pos.z.rem_euclid(16); + + // Note that we're using `set_block` and not `set_block_state`. + let old_state = chunk + .set_block(chunk_x as u32, chunk_y, chunk_z as u32, state) + .state; + + // Was the change observable? + if old_state != state && chunk.viewer_count > 0 { + self.sect_update_buf.push( + ChunkDeltaUpdateEntry::new() + .with_off_x(chunk_x as u8) + .with_off_y((chunk_y % 16) as u8) + .with_off_z(chunk_z as u8) + .with_block_state(state.to_raw() as u32), + ); + } + } + } + } + + // Flush remaining block state changes. + flush_sect_updates(sect_pos, &mut self.sect_update_buf); + + // Send block entity updates. This needs to happen after block states are set. + for (pos, kind, nbt) in self.block_entities.drain(..) { + let min_y = info.min_y; + let height = info.height; + + if let Some(chunk) = chunks.get_mut(pos) { + let chunk_y = pos.y.wrapping_sub(min_y) as u32; + + // Is the block pos in bounds of the chunk? + if chunk_y < height as u32 { + let chunk_x = pos.x.rem_euclid(16); + let chunk_z = pos.z.rem_euclid(16); + + let mut w = messages.packet_writer(MessageScope::ChunkView { pos: pos.into() }); + w.write_packet(&BlockEntityUpdateS2c { + position: pos, + kind, + data: Cow::Borrowed(&nbt), + }); + + chunk.set_block_entity(chunk_x as u32, chunk_y, chunk_z as u32, Some(nbt)); + } + } + } + } +} + +#[bitfield(u128, order = Msb)] +#[derive(PartialEq, Eq, PartialOrd, Ord)] +struct StateUpdate { + // Section coordinate. + #[bits(28)] + section_x: i32, + #[bits(28)] + section_z: i32, + #[bits(28)] + section_y: i32, + // Coordinate within the section. + #[bits(4)] + off_x: u32, + #[bits(4)] + off_z: u32, + #[bits(4)] + off_y: u32, + /// Bits of the [`BlockState`]. + state: u32, +} + +impl StateUpdate { + const CHUNK_POS_MASK: u128 = u128::MAX << 72; + const SECTION_POS_MASK: u128 = u128::MAX << 44; + const BLOCK_POS_MASK: u128 = u128::MAX << 32; + + fn from_parts(pos: BlockPos, state: BlockState) -> Self { + Self::new() + .with_section_x(pos.x.div_euclid(16)) + .with_section_y(pos.y.div_euclid(16)) + .with_section_z(pos.z.div_euclid(16)) + .with_off_x(pos.x.rem_euclid(16) as u32) + .with_off_y(pos.y.rem_euclid(16) as u32) + .with_off_z(pos.z.rem_euclid(16) as u32) + .with_state(state.to_raw() as u32) + } + + fn to_parts(self) -> (BlockPos, BlockState) { + ( + BlockPos { + x: self.section_x() * 16 + self.off_x() as i32, + y: self.section_y() * 16 + self.off_y() as i32, + z: self.section_z() * 16 + self.off_z() as i32, + }, + BlockState::from_raw(self.state() as u16).unwrap(), + ) + } +} + +// TODO: unit test. diff --git a/crates/valence_server/src/dimension_layer/block.rs b/crates/valence_server/src/dimension_layer/block.rs new file mode 100644 index 000000000..335baeafb --- /dev/null +++ b/crates/valence_server/src/dimension_layer/block.rs @@ -0,0 +1,59 @@ +use valence_nbt::Compound; +pub use valence_protocol::BlockState; + +/// Represents a complete block, which is a pair of block state and optional NBT +/// data for the block entity. +#[derive(Clone, PartialEq, Default, Debug)] +pub struct Block { + pub state: BlockState, + pub nbt: Option, +} + +impl Block { + pub const fn new(state: BlockState, nbt: Option) -> Self { + Self { state, nbt } + } +} + +impl From for Block { + fn from(state: BlockState) -> Self { + Self { state, nbt: None } + } +} + +/// Like [`Block`] but immutably referenced. +#[derive(Copy, Clone, PartialEq, Default, Debug)] +pub struct BlockRef<'a> { + pub state: BlockState, + pub nbt: Option<&'a Compound>, +} + +impl<'a> BlockRef<'a> { + pub const fn new(state: BlockState, nbt: Option<&'a Compound>) -> Self { + Self { state, nbt } + } +} + +impl From> for Block { + fn from(value: BlockRef<'_>) -> Self { + Self { + state: value.state, + nbt: value.nbt.cloned(), + } + } +} + +impl From for BlockRef<'_> { + fn from(state: BlockState) -> Self { + Self { state, nbt: None } + } +} + +impl<'a> From<&'a Block> for BlockRef<'a> { + fn from(value: &'a Block) -> Self { + Self { + state: value.state, + nbt: value.nbt.as_ref(), + } + } +} diff --git a/crates/valence_server/src/layer/chunk/chunk.rs b/crates/valence_server/src/dimension_layer/chunk.rs similarity index 76% rename from crates/valence_server/src/layer/chunk/chunk.rs rename to crates/valence_server/src/dimension_layer/chunk.rs index e5b53eefc..db016b29d 100644 --- a/crates/valence_server/src/layer/chunk/chunk.rs +++ b/crates/valence_server/src/dimension_layer/chunk.rs @@ -1,13 +1,20 @@ use valence_nbt::Compound; -use valence_protocol::BlockState; +use valence_protocol::{BlockPos, BlockState}; use valence_registry::biome::BiomeId; -use super::paletted_container::PalettedContainer; +mod loaded; +mod paletted_container; +mod unloaded; + +pub use loaded::LoadedChunk; +pub use unloaded::Chunk; + +use super::{BiomePos, Block, BlockRef}; /// Common operations on chunks. Notable implementors are /// [`LoadedChunk`](super::loaded::LoadedChunk) and /// [`UnloadedChunk`](super::unloaded::UnloadedChunk). -pub trait Chunk { +pub trait ChunkOps { /// Gets the height of this chunk in meters or blocks. fn height(&self) -> u32; @@ -33,16 +40,28 @@ pub trait Chunk { /// /// May panic if the position is out of bounds. #[track_caller] - fn set_block(&mut self, x: u32, y: u32, z: u32, block: impl IntoBlock) -> Block { - let block = block.into_block(); - let state = self.set_block_state(x, y, z, block.state); - let nbt = self.set_block_entity(x, y, z, block.nbt); + fn set_block(&mut self, x: u32, y: u32, z: u32, block: impl Into) -> Block { + let block = block.into(); + let old_state = self.set_block_state(x, y, z, block.state); + + let old_nbt = if block.nbt.is_none() && block.state.block_entity_kind().is_some() { + // If the block state is associated with a block entity, make sure there's + // always some NBT data. Otherwise, the block will appear invisible to clients + // when loading the chunk. + self.set_block_entity(x, y, z, Some(Compound::default())) + } else { + self.set_block_entity(x, y, z, block.nbt) + }; - Block { state, nbt } + Block { + state: old_state, + nbt: old_nbt, + } } + /* /// Sets all the blocks in the entire chunk to the provided block. - fn fill_blocks(&mut self, block: impl IntoBlock) { + fn fill_blocks(&mut self, block: impl Into) { let block = block.into_block(); self.fill_block_states(block.state); @@ -59,6 +78,7 @@ pub trait Chunk { self.clear_block_entities(); } } + */ /// Gets the block state at the provided position in this chunk. `x` and `z` /// are in the range `0..16` while `y` is in the range `0..height`. @@ -211,116 +231,50 @@ pub trait Chunk { fn shrink_to_fit(&mut self); } -/// Represents a complete block, which is a pair of block state and optional NBT -/// data for the block entity. -#[derive(Clone, PartialEq, Default, Debug)] -pub struct Block { - pub state: BlockState, - pub nbt: Option, -} - -impl Block { - pub const fn new(state: BlockState, nbt: Option) -> Self { - Self { state, nbt } - } -} - -/// Like [`Block`], but immutably referenced. -#[derive(Copy, Clone, PartialEq, Default, Debug)] -pub struct BlockRef<'a> { - pub state: BlockState, - pub nbt: Option<&'a Compound>, -} - -impl<'a> BlockRef<'a> { - pub const fn new(state: BlockState, nbt: Option<&'a Compound>) -> Self { - Self { state, nbt } - } -} - -pub trait IntoBlock { - // TODO: parameterize this with block registry ref? - fn into_block(self) -> Block; -} - -impl IntoBlock for Block { - fn into_block(self) -> Block { - self - } -} - -impl<'a> IntoBlock for BlockRef<'a> { - fn into_block(self) -> Block { - Block { - state: self.state, - nbt: self.nbt.cloned(), - } - } -} - -/// This will initialize the block with a new empty compound if the block state -/// is associated with a block entity. -impl IntoBlock for BlockState { - fn into_block(self) -> Block { - Block { - state: self, - nbt: self.block_entity_kind().map(|_| Compound::new()), - } - } -} - -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; -pub(super) type BlockStateContainer = - PalettedContainer; - -pub(super) type BiomeContainer = - PalettedContainer; +pub(super) const SECTION_BLOCK_COUNT: usize = 16 * 16 * 16; +pub(super) const SECTION_BIOME_COUNT: usize = 4 * 4 * 4; -#[inline] -#[track_caller] -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" - ); +/// Returns the minimum number of bits needed to represent the integer `n`. +pub(super) const fn bit_width(n: usize) -> usize { + (usize::BITS - n.leading_zeros()) as _ } #[inline] -#[track_caller] -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" - ); +pub(super) fn block_offsets(block_pos: BlockPos, min_y: i32, height: i32) -> Option<[u32; 3]> { + let off_x = block_pos.x.rem_euclid(16); + let off_z = block_pos.z.rem_euclid(16); + let off_y = block_pos.y.wrapping_sub(min_y); + + if off_y < height { + Some([off_x as u32, off_y as u32, off_z as u32]) + } else { + None + } } #[inline] -#[track_caller] -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" - ); -} - -/// Returns the minimum number of bits needed to represent the integer `n`. -pub(super) const fn bit_width(n: usize) -> usize { - (usize::BITS - n.leading_zeros()) as _ +pub(super) fn biome_offsets(biome_pos: BiomePos, min_y: i32, height: i32) -> Option<[u32; 3]> { + let off_x = biome_pos.x.rem_euclid(4); + let off_z = biome_pos.z.rem_euclid(4); + let off_y = biome_pos.y.wrapping_sub(min_y / 4); + + if off_y < height / 4 { + Some([off_x as u32, off_y as u32, off_z as u32]) + } else { + None + } } #[cfg(test)] mod tests { use super::*; - use crate::layer::chunk::{LoadedChunk, UnloadedChunk}; #[test] fn chunk_get_set() { - fn check(mut chunk: impl Chunk) { + fn check(mut chunk: impl ChunkOps) { assert_eq!( chunk.set_block_state(1, 2, 3, BlockState::CHAIN), BlockState::AIR @@ -334,7 +288,7 @@ mod tests { assert_eq!(chunk.set_block_entity(1, 2, 3, None), Some(Compound::new())); } - let unloaded = UnloadedChunk::with_height(512); + let unloaded = Chunk::with_height(512); let loaded = LoadedChunk::new(512); check(unloaded); @@ -345,7 +299,7 @@ mod tests { #[test] #[should_panic] fn chunk_debug_oob_0() { - let mut chunk = UnloadedChunk::with_height(512); + let mut chunk = Chunk::with_height(512); chunk.set_block_state(0, 0, 16, BlockState::AIR); } @@ -361,7 +315,7 @@ mod tests { #[test] #[should_panic] fn chunk_debug_oob_2() { - let mut chunk = UnloadedChunk::with_height(512); + let mut chunk = Chunk::with_height(512); chunk.set_block_entity(0, 0, 16, None); } @@ -377,7 +331,7 @@ mod tests { #[test] #[should_panic] fn chunk_debug_oob_4() { - let mut chunk = UnloadedChunk::with_height(512); + let mut chunk = Chunk::with_height(512); chunk.set_biome(0, 0, 4, BiomeId::DEFAULT); } @@ -393,7 +347,7 @@ mod tests { #[test] #[should_panic] fn chunk_debug_oob_6() { - let mut chunk = UnloadedChunk::with_height(512); + let mut chunk = Chunk::with_height(512); chunk.fill_block_state_section(chunk.height() / 16, BlockState::AIR); } @@ -409,7 +363,7 @@ mod tests { #[test] #[should_panic] fn chunk_debug_oob_8() { - let mut chunk = UnloadedChunk::with_height(512); + let mut chunk = Chunk::with_height(512); chunk.fill_biome_section(chunk.height() / 16, BiomeId::DEFAULT); } diff --git a/crates/valence_server/src/dimension_layer/chunk/loaded.rs b/crates/valence_server/src/dimension_layer/chunk/loaded.rs new file mode 100644 index 000000000..63d689e81 --- /dev/null +++ b/crates/valence_server/src/dimension_layer/chunk/loaded.rs @@ -0,0 +1,312 @@ +use std::borrow::Cow; +use std::mem; + +use valence_nbt::{compound, Compound}; +use valence_protocol::encode::{PacketWriter, WritePacket}; +use valence_protocol::packets::play::chunk_data_s2c::ChunkDataBlockEntity; +use valence_protocol::packets::play::ChunkDataS2c; +use valence_protocol::{BlockState, ChunkPos, Encode}; +use valence_registry::biome::BiomeId; +use valence_registry::RegistryIdx; + +use super::unloaded::Chunk; +use super::{bit_width, ChunkOps, SECTION_BLOCK_COUNT}; +use crate::dimension_layer::DimensionInfo; + +/// A chunk that is actively loaded in a dimension layer. This is only +/// accessible behind a reference. +/// +/// Like [`Chunk`], loaded chunks implement the [`ChunkOps`] trait so you can +/// use many of the same methods. +/// +/// **NOTE:** Loaded chunks are a low-level API. Mutations directly to loaded +/// chunks are intentionally not synchronized with clients. Consider using the +/// relevant methods from [`DimensionLayerQueryItem`] instead. +/// +/// [`DimensionLayerQueryItem`]: super::DimensionLayerQueryItem +#[derive(Debug)] +pub struct LoadedChunk { + /// Chunk data for this loaded chunk. + chunk: Chunk, + /// 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 zero. + pub(in super::super) viewer_count: u32, + /// 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_chunk_packet: Vec, +} + +impl LoadedChunk { + pub(crate) fn new(height: i32) -> Self { + Self { + viewer_count: 0, + chunk: Chunk::with_height(height), + cached_chunk_packet: vec![], + } + } + + /// Sets the content of this loaded chunk to the supplied [`Chunk`]. The + /// given unloaded chunk is [resized] to match the height of this loaded + /// chunk prior to insertion. + /// + /// The previous chunk data is returned. + /// + /// [resized]: UnloadedChunk::set_height + pub fn replace(&mut self, mut chunk: Chunk) -> Chunk { + chunk.set_height(self.height()); + + self.cached_chunk_packet.clear(); + + mem::replace(&mut self.chunk, chunk) + } + + pub(in super::super) fn into_chunk(self) -> Chunk { + self.chunk + } + + /// Clones this chunk's data into the returned [`Chunk`]. + pub fn to_chunk(&self) -> Chunk { + self.chunk.clone() + } + + /// Returns the number of clients in view of this chunk. + pub fn viewer_count(&self) -> u32 { + self.viewer_count + } + + /// Writes the packet data needed to initialize this chunk. + pub(crate) fn write_chunk_init_packet( + &mut self, + mut writer: impl WritePacket, + pos: ChunkPos, + info: &DimensionInfo, + ) { + if self.cached_chunk_packet.is_empty() { + let heightmaps = compound! { + // TODO: MOTION_BLOCKING and WORLD_SURFACE heightmaps. + }; + + let mut blocks_and_biomes: Vec = vec![]; + + for sect in &self.chunk.sections { + sect.count_non_air_blocks() + .encode(&mut blocks_and_biomes) + .unwrap(); + + sect.block_states + .encode_mc_format( + &mut blocks_and_biomes, + |b| b.to_raw().into(), + 4, + 8, + bit_width(BlockState::max_raw().into()), + ) + .expect("paletted container encode should always succeed"); + + sect.biomes + .encode_mc_format( + &mut blocks_and_biomes, + |b| b.to_index() as _, + 0, + 3, + bit_width(info.biome_registry_len as usize - 1), + ) + .expect("paletted container encode should always succeed"); + } + + let block_entities: Vec<_> = self + .chunk + .block_entities + .iter() + .filter_map(|(&idx, nbt)| { + let x = idx % 16; + let z = idx / 16 % 16; + let y = idx / 16 / 16; + + let kind = self.chunk.sections[y as usize / 16] + .block_states + .get(idx as usize % SECTION_BLOCK_COUNT) + .block_entity_kind(); + + kind.map(|kind| ChunkDataBlockEntity { + packed_xz: ((x << 4) | z) as i8, + y: y as i16 + info.min_y as i16, + kind, + data: Cow::Borrowed(nbt), + }) + }) + .collect(); + + PacketWriter::new(&mut self.cached_chunk_packet, info.threshold).write_packet( + &ChunkDataS2c { + pos, + heightmaps: Cow::Owned(heightmaps), + blocks_and_biomes: &blocks_and_biomes, + block_entities: Cow::Owned(block_entities), + 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(&[]), + block_light_arrays: Cow::Borrowed(&[]), + }, + ) + } + + writer.write_packet_bytes(&self.cached_chunk_packet); + } +} + +impl ChunkOps for LoadedChunk { + fn height(&self) -> u32 { + self.chunk.height() + } + + fn block_state(&self, x: u32, y: u32, z: u32) -> BlockState { + self.chunk.block_state(x, y, z) + } + + fn set_block_state(&mut self, x: u32, y: u32, z: u32, block: BlockState) -> BlockState { + let old_block = self.chunk.set_block_state(x, y, z, block); + + if block != old_block { + self.cached_chunk_packet.clear(); + } + + old_block + } + + fn fill_block_state_section(&mut self, sect_y: u32, block: BlockState) { + self.chunk.fill_block_state_section(sect_y, block); + + // TODO: do some checks to avoid calling this sometimes. + self.cached_chunk_packet.clear(); + } + + fn block_entity(&self, x: u32, y: u32, z: u32) -> Option<&Compound> { + self.chunk.block_entity(x, y, z) + } + + fn block_entity_mut(&mut self, x: u32, y: u32, z: u32) -> Option<&mut Compound> { + let res = self.chunk.block_entity_mut(x, y, z); + + if res.is_some() { + self.cached_chunk_packet.clear(); + } + + res + } + + fn set_block_entity( + &mut self, + x: u32, + y: u32, + z: u32, + block_entity: Option, + ) -> Option { + self.cached_chunk_packet.clear(); + + self.chunk.set_block_entity(x, y, z, block_entity) + } + + fn clear_block_entities(&mut self) { + if self.chunk.block_entities.is_empty() { + return; + } + + self.chunk.clear_block_entities(); + + self.cached_chunk_packet.clear(); + } + + fn biome(&self, x: u32, y: u32, z: u32) -> BiomeId { + self.chunk.biome(x, y, z) + } + + fn set_biome(&mut self, x: u32, y: u32, z: u32, biome: BiomeId) -> BiomeId { + let old_biome = self.chunk.set_biome(x, y, z, biome); + + if biome != old_biome { + self.cached_chunk_packet.clear(); + } + + old_biome + } + + fn fill_biome_section(&mut self, sect_y: u32, biome: BiomeId) { + self.chunk.fill_biome_section(sect_y, biome); + + self.cached_chunk_packet.clear(); + } + + fn shrink_to_fit(&mut self) { + self.cached_chunk_packet.shrink_to_fit(); + self.chunk.shrink_to_fit(); + } +} + +#[cfg(test)] +mod tests { + use valence_protocol::CompressionThreshold; + use valence_registry::dimension_type::DimensionTypeId; + + use super::*; + + #[test] + fn loaded_chunk_changes_clear_packet_cache() { + #[track_caller] + fn check(chunk: &mut LoadedChunk, change: impl FnOnce(&mut LoadedChunk) -> T) { + let info = DimensionInfo { + dimension_type: DimensionTypeId::default(), + height: 512, + min_y: -16, + biome_registry_len: 200, + threshold: CompressionThreshold(-1), + }; + + let mut buf = vec![]; + let mut writer = PacketWriter::new(&mut buf, CompressionThreshold(-1)); + + // Rebuild cache. + chunk.write_chunk_init_packet(&mut writer, ChunkPos::new(3, 4), &info); + + // Check that the cache is built. + assert!(!chunk.cached_chunk_packet.is_empty()); + + // Making a change should clear the cache. + change(chunk); + assert!(chunk.cached_chunk_packet.is_empty()); + + // Rebuild cache again. + chunk.write_chunk_init_packet(&mut writer, ChunkPos::new(3, 4), &info); + assert!(!chunk.cached_chunk_packet.is_empty()); + } + + let mut chunk = LoadedChunk::new(512); + + check(&mut chunk, |c| { + c.set_block_state(0, 4, 0, BlockState::ACACIA_WOOD) + }); + check(&mut chunk, |c| c.set_biome(1, 2, 3, BiomeId::from_index(4))); + check(&mut chunk, |c| c.fill_biomes(BiomeId::DEFAULT)); + check(&mut chunk, |c| c.fill_block_states(BlockState::WET_SPONGE)); + check(&mut chunk, |c| { + c.set_block_entity(3, 40, 5, Some(compound! {})) + }); + check(&mut chunk, |c| { + c.block_entity_mut(3, 40, 5).unwrap(); + }); + check(&mut chunk, |c| c.set_block_entity(3, 40, 5, None)); + + // Old block state is the same as new block state, so the cache should still be + // intact. + assert_eq!( + chunk.set_block_state(0, 0, 0, BlockState::WET_SPONGE), + BlockState::WET_SPONGE + ); + + assert!(!chunk.cached_chunk_packet.is_empty()); + } +} diff --git a/crates/valence_server/src/layer/chunk/paletted_container.rs b/crates/valence_server/src/dimension_layer/chunk/paletted_container.rs similarity index 99% rename from crates/valence_server/src/layer/chunk/paletted_container.rs rename to crates/valence_server/src/dimension_layer/chunk/paletted_container.rs index 302151d83..12520af34 100644 --- a/crates/valence_server/src/layer/chunk/paletted_container.rs +++ b/crates/valence_server/src/dimension_layer/chunk/paletted_container.rs @@ -5,7 +5,7 @@ use arrayvec::ArrayVec; use num_integer::div_ceil; use valence_protocol::{Encode, VarInt}; -use super::chunk::bit_width; +use crate::dimension_layer::chunk::bit_width; /// `HALF_LEN` must be equal to `ceil(LEN / 2)`. #[derive(Clone, Debug)] diff --git a/crates/valence_server/src/layer/chunk/unloaded.rs b/crates/valence_server/src/dimension_layer/chunk/unloaded.rs similarity index 67% rename from crates/valence_server/src/layer/chunk/unloaded.rs rename to crates/valence_server/src/dimension_layer/chunk/unloaded.rs index cd19114e1..710bd3fed 100644 --- a/crates/valence_server/src/layer/chunk/unloaded.rs +++ b/crates/valence_server/src/dimension_layer/chunk/unloaded.rs @@ -5,36 +5,68 @@ use valence_nbt::Compound; use valence_protocol::BlockState; use valence_registry::biome::BiomeId; -use super::chunk::{ - check_biome_oob, check_block_oob, check_section_oob, BiomeContainer, BlockStateContainer, - Chunk, MAX_HEIGHT, SECTION_BLOCK_COUNT, -}; +use super::paletted_container::PalettedContainer; +use super::{ChunkOps, MAX_HEIGHT, SECTION_BIOME_COUNT, SECTION_BLOCK_COUNT}; #[derive(Clone, Default, Debug)] -pub struct UnloadedChunk { +pub struct Chunk { pub(super) sections: Vec
, pub(super) block_entities: BTreeMap, } #[derive(Clone, Default, Debug)] pub(super) struct Section { - pub(super) block_states: BlockStateContainer, - pub(super) biomes: BiomeContainer, + pub(super) block_states: + PalettedContainer, + pub(super) biomes: PalettedContainer, } -impl UnloadedChunk { - pub fn new() -> Self { - Self::default() +impl Section { + pub(super) fn count_non_air_blocks(&self) -> u16 { + let mut count = 0; + + match &self.block_states { + PalettedContainer::Single(s) => { + if !s.is_air() { + count += SECTION_BLOCK_COUNT as u16; + } + } + PalettedContainer::Indirect(ind) => { + for i in 0..SECTION_BLOCK_COUNT { + if !ind.get(i).is_air() { + count += 1; + } + } + } + PalettedContainer::Direct(dir) => { + for s in dir.as_ref() { + if !s.is_air() { + count += 1; + } + } + } + } + + count + } +} + +impl Chunk { + pub const fn new() -> Self { + Self { + sections: vec![], + block_entities: BTreeMap::new(), + } } - pub fn with_height(height: u32) -> Self { + pub fn with_height(height: i32) -> Self { Self { - sections: vec![Section::default(); height as usize / 16], + sections: vec![Section::default(); height.max(0) as usize / 16], block_entities: BTreeMap::new(), } } - /// Sets the height of this chunk in meters. The chunk is truncated or + /// Sets the height of this chunk in blocks. The chunk is truncated or /// extended with [`BlockState::AIR`] and [`BiomeId::default()`] from the /// top. /// @@ -63,7 +95,7 @@ impl UnloadedChunk { } } -impl Chunk for UnloadedChunk { +impl ChunkOps for Chunk { fn height(&self) -> u32 { self.sections.len() as u32 * 16 } @@ -157,13 +189,40 @@ impl Chunk for UnloadedChunk { } } +#[inline] +#[track_caller] +pub(super) fn check_block_oob(chunk: &impl ChunkOps, 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" + ); +} + +#[inline] +#[track_caller] +pub(super) fn check_biome_oob(chunk: &impl ChunkOps, 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" + ); +} + +#[inline] +#[track_caller] +pub(super) fn check_section_oob(chunk: &impl ChunkOps, sect_y: u32) { + assert!( + sect_y < chunk.height() / 16, + "chunk section offset of {sect_y} is out of bounds" + ); +} + #[cfg(test)] mod tests { use super::*; #[test] - fn unloaded_chunk_resize_removes_block_entities() { - let mut chunk = UnloadedChunk::with_height(32); + fn chunk_resize_removes_block_entities() { + let mut chunk = Chunk::with_height(32); assert_eq!(chunk.height(), 32); diff --git a/crates/valence_server/src/dimension_layer/chunk_index.rs b/crates/valence_server/src/dimension_layer/chunk_index.rs new file mode 100644 index 000000000..363df53d0 --- /dev/null +++ b/crates/valence_server/src/dimension_layer/chunk_index.rs @@ -0,0 +1,191 @@ +pub use bevy_ecs::prelude::*; +use rustc_hash::FxHashMap; +use valence_protocol::{BiomePos, BlockPos, ChunkPos}; +use valence_registry::biome::BiomeId; + +use super::block::{Block, BlockRef}; +use super::chunk::{biome_offsets, block_offsets, Chunk, ChunkOps, LoadedChunk}; + +/// The mapping of chunk positions to [`LoadedChunk`]s in a dimension layer. +/// +/// **NOTE**: By design, directly modifying the chunk index does not send +/// packets to synchronize state with clients. +#[derive(Component, Debug)] +pub struct ChunkIndex { + map: FxHashMap, + height: i32, + min_y: i32, +} + +impl ChunkIndex { + pub(crate) fn new(height: i32, min_y: i32) -> Self { + Self { + map: Default::default(), + height, + min_y, + } + } + + pub fn get(&self, pos: impl Into) -> Option<&LoadedChunk> { + self.map.get(&pos.into()) + } + + pub fn get_mut(&mut self, pos: impl Into) -> Option<&mut LoadedChunk> { + self.map.get_mut(&pos.into()) + } + + pub fn insert(&mut self, pos: impl Into, chunk: Chunk) -> Option { + match self.entry(pos.into()) { + Entry::Occupied(mut o) => Some(o.insert(chunk)), + Entry::Vacant(v) => { + v.insert(chunk); + None + } + } + } + + pub fn remove(&mut self, pos: impl Into) -> Option { + match self.entry(pos.into()) { + Entry::Occupied(o) => Some(o.remove()), + Entry::Vacant(_) => None, + } + } + + pub fn entry(&mut self, pos: impl Into) -> Entry { + match self.map.entry(pos.into()) { + std::collections::hash_map::Entry::Occupied(o) => { + Entry::Occupied(OccupiedEntry { entry: o }) + } + std::collections::hash_map::Entry::Vacant(v) => Entry::Vacant(VacantEntry { + entry: v, + height: self.height, + }), + } + } + + pub fn block(&self, pos: impl Into) -> Option { + let pos = pos.into(); + let chunk = self.get(pos)?; + + let [x, y, z] = block_offsets(pos, self.min_y, self.height)?; + Some(chunk.block(x, y, z)) + } + + pub fn set_block( + &mut self, + pos: impl Into, + block: impl Into, + ) -> Option { + let pos = pos.into(); + let chunk = self.map.get_mut(&ChunkPos::from(pos))?; + + let [x, y, z] = block_offsets(pos, self.min_y, self.height)?; + Some(chunk.set_block(x, y, z, block)) + } + + pub fn biome(&self, pos: impl Into) -> Option { + let pos = pos.into(); + let chunk_pos = ChunkPos::from(pos); + + let chunk = self.get(chunk_pos)?; + let [x, y, z] = biome_offsets(pos, self.min_y, self.height)?; + Some(chunk.biome(x, y, z)) + } + + pub fn set_biome(&mut self, pos: impl Into, biome: BiomeId) -> Option { + let pos = pos.into(); + let chunk = self.map.get_mut(&ChunkPos::from(pos))?; + + let [x, y, z] = biome_offsets(pos, self.min_y, self.height)?; + Some(chunk.set_biome(x, y, z, biome)) + } + + pub fn retain(&mut self, mut f: impl FnMut(ChunkPos, &mut LoadedChunk) -> bool) { + self.map.retain(|pos, chunk| f(*pos, chunk)) + } + + pub fn clear(&mut self) { + self.map.clear(); + } + + pub fn shrink_to_fit(&mut self) { + self.map.shrink_to_fit() + } + + // TODO: iter, iter_mut +} + +#[derive(Debug)] +pub enum Entry<'a> { + Occupied(OccupiedEntry<'a>), + Vacant(VacantEntry<'a>), +} + +impl<'a> Entry<'a> { + pub fn or_default(self) -> &'a mut LoadedChunk { + match self { + Entry::Occupied(oe) => oe.into_mut(), + Entry::Vacant(ve) => ve.insert(Chunk::new()), + } + } +} + +#[derive(Debug)] +pub struct OccupiedEntry<'a> { + entry: std::collections::hash_map::OccupiedEntry<'a, ChunkPos, LoadedChunk>, +} + +impl<'a> OccupiedEntry<'a> { + pub fn get(&self) -> &LoadedChunk { + self.entry.get() + } + + pub fn get_mut(&mut self) -> &mut LoadedChunk { + self.entry.get_mut() + } + + pub fn insert(&mut self, chunk: Chunk) -> Chunk { + self.entry.get_mut().replace(chunk) + } + + pub fn into_mut(self) -> &'a mut LoadedChunk { + self.entry.into_mut() + } + + pub fn key(&self) -> &ChunkPos { + self.entry.key() + } + + pub fn remove(self) -> Chunk { + self.remove_entry().1 + } + + pub fn remove_entry(self) -> (ChunkPos, Chunk) { + let (pos, chunk) = self.entry.remove_entry(); + + (pos, chunk.into_chunk()) + } +} + +#[derive(Debug)] +pub struct VacantEntry<'a> { + entry: std::collections::hash_map::VacantEntry<'a, ChunkPos, LoadedChunk>, + height: i32, +} + +impl<'a> VacantEntry<'a> { + pub fn insert(self, chunk: Chunk) -> &'a mut LoadedChunk { + let mut loaded = LoadedChunk::new(self.height); + loaded.replace(chunk); + + self.entry.insert(loaded) + } + + pub fn into_key(self) -> ChunkPos { + *self.entry.key() + } + + pub fn key(&self) -> &ChunkPos { + self.entry.key() + } +} diff --git a/crates/valence_server/src/dimension_layer/plugin.rs b/crates/valence_server/src/dimension_layer/plugin.rs new file mode 100644 index 000000000..6b151ffa3 --- /dev/null +++ b/crates/valence_server/src/dimension_layer/plugin.rs @@ -0,0 +1,151 @@ +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; +use valence_entity::Position; +use valence_protocol::packets::play::{ + ChunkLoadDistanceS2c, ChunkRenderDistanceCenterS2c, UnloadChunkS2c, +}; +use valence_protocol::{VarInt, WritePacket}; +use valence_server_common::Despawned; + +use super::{ChunkIndex, DimensionInfo}; +use crate::client::{Client, ClientMarker, OldView, View}; +use crate::layer::{BroadcastLayerMessagesSet, OldVisibleLayers, VisibleLayers}; + +pub struct DimensionLayerPlugin; + +/// When dimension layers are updated. This is where chunk packets are sent to +/// clients and chunk viewer counts are updated as client views change. +#[derive(SystemSet, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct UpdateDimensionLayerSet; + +impl Plugin for DimensionLayerPlugin { + fn build(&self, app: &mut App) { + app.configure_set( + PostUpdate, + UpdateDimensionLayerSet.before(BroadcastLayerMessagesSet), + ) + .add_systems( + PostUpdate, + ( + update_dimension_layer_views, + update_dimension_layer_views_client_despawn, + ) + .chain() + .in_set(UpdateDimensionLayerSet), + ); + } +} + +fn update_dimension_layer_views( + mut clients: Query< + ( + Entity, + &mut Client, + OldView, + View, + &OldVisibleLayers, + Ref, + ), + Changed, + >, + mut layers: Query<(&mut ChunkIndex, &DimensionInfo)>, +) { + for (client_id, mut client, old_view, view, old_visible, visible) in &mut clients { + let old_view = old_view.get(); + let view = view.get(); + + // Set render distance center before loading new chunks. Otherwise, client may + // ignore them. + if old_view.pos != view.pos { + client.write_packet(&ChunkRenderDistanceCenterS2c { + chunk_x: view.pos.x.into(), + chunk_z: view.pos.z.into(), + }); + } + + // Update view distance fog. + // Note: this is just aesthetic. + if old_view.dist() != view.dist() { + client.write_packet(&ChunkLoadDistanceS2c { + view_distance: VarInt(view.dist().into()), + }); + } + + let mut changed_dimension = false; + + if visible.is_changed() { + // Send despawn packets for chunks in the old dimension layer. + for &layer in old_visible.difference(&visible) { + if let Ok((mut index, _)) = layers.get_mut(layer) { + for pos in old_view.iter() { + if let Some(chunk) = index.get_mut(pos) { + client.write_packet(&UnloadChunkS2c { pos }); + chunk.viewer_count -= 1; + } + } + + changed_dimension = true; + break; + } + } + + // Send spawn packets for chunks in the new layer. + for &layer in visible.difference(&old_visible) { + if let Ok((mut index, info)) = layers.get_mut(layer) { + for pos in view.iter() { + if let Some(chunk) = index.get_mut(pos) { + chunk.write_chunk_init_packet(&mut *client, pos, info); + chunk.viewer_count += 1; + } + } + + changed_dimension = true; + break; + } + } + } + + if !changed_dimension && old_view != view { + for &layer in visible.iter() { + if let Ok((mut index, info)) = layers.get_mut(layer) { + // Unload old chunks in view. + for pos in old_view.diff(view) { + if let Some(chunk) = index.get_mut(pos) { + client.write_packet(&UnloadChunkS2c { pos }); + chunk.viewer_count -= 1; + } + } + + // Load new chunks in view. + for pos in view.diff(old_view) { + if let Some(chunk) = index.get_mut(pos) { + chunk.write_chunk_init_packet(&mut *client, pos, info); + chunk.viewer_count += 1; + } + } + + break; + } + } + } + } +} + +fn update_dimension_layer_views_client_despawn( + mut clients: Query<(Entity, OldView, &OldVisibleLayers), (With, With)>, + mut chunks: Query<&mut ChunkIndex>, +) { + for (client_id, old_view, old_layers) in &mut clients { + for &layer in old_layers.iter() { + if let Ok(mut chunks) = chunks.get_mut(layer) { + for pos in old_view.get().iter() { + if let Some(chunk) = chunks.get_mut(pos) { + chunk.viewer_count -= 1; + } + } + + break; + } + } + } +} diff --git a/crates/valence_server/src/entity_layer.rs b/crates/valence_server/src/entity_layer.rs new file mode 100644 index 000000000..6497620af --- /dev/null +++ b/crates/valence_server/src/entity_layer.rs @@ -0,0 +1,20 @@ +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; + +use crate::layer::BroadcastLayerMessagesSet; + +pub struct EntityLayerPlugin; + +/// When entity changes are written to entity layers and clients are sent +/// spawn/despawn packets. +#[derive(SystemSet, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct UpdateEntityLayerSet; + +impl Plugin for EntityLayerPlugin { + fn build(&self, app: &mut App) { + app.configure_set( + PostUpdate, + UpdateEntityLayerSet.before(BroadcastLayerMessagesSet), + ); + } +} diff --git a/crates/valence_server/src/game_mode.rs b/crates/valence_server/src/game_mode.rs new file mode 100644 index 000000000..8f0fe826b --- /dev/null +++ b/crates/valence_server/src/game_mode.rs @@ -0,0 +1,35 @@ +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; +use valence_protocol::packets::play::game_state_change_s2c::GameEventKind; +use valence_protocol::packets::play::GameStateChangeS2c; +pub use valence_protocol::GameMode; +use valence_protocol::WritePacket; + +use crate::client::FlushPacketsSet; +use crate::Client; + +pub struct UpdateGameModePlugin; + +#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct UpdateGameModeSet; + +impl Plugin for UpdateGameModePlugin { + fn build(&self, app: &mut App) { + app.configure_set(PostUpdate, UpdateGameModeSet.before(FlushPacketsSet)) + .add_systems(PostUpdate, update_game_mode.in_set(UpdateGameModeSet)); + } +} + +pub(crate) fn update_game_mode(mut clients: Query<(&mut Client, &GameMode), Changed>) { + for (mut client, game_mode) in &mut clients { + if client.is_added() { + // Game join packet includes the initial game mode. + continue; + } + + client.write_packet(&GameStateChangeS2c { + kind: GameEventKind::ChangeGameMode, + value: *game_mode as i32 as f32, + }); + } +} diff --git a/crates/valence_server/src/keepalive.rs b/crates/valence_server/src/keepalive.rs index a13a7c333..49d04c697 100644 --- a/crates/valence_server/src/keepalive.rs +++ b/crates/valence_server/src/keepalive.rs @@ -7,15 +7,19 @@ use tracing::warn; use valence_protocol::packets::play::{KeepAliveC2s, KeepAliveS2c}; use valence_protocol::WritePacket; -use crate::client::{Client, UpdateClientsSet}; +use crate::client::{Client, FlushPacketsSet}; use crate::event_loop::{EventLoopPreUpdate, PacketEvent}; pub struct KeepalivePlugin; +#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct SendKeepaliveSet; + impl Plugin for KeepalivePlugin { fn build(&self, app: &mut App) { app.init_resource::() - .add_systems(PostUpdate, send_keepalive.in_set(UpdateClientsSet)) + .configure_set(PostUpdate, SendKeepaliveSet.before(FlushPacketsSet)) + .add_systems(PostUpdate, send_keepalive.in_set(SendKeepaliveSet)) .add_systems(EventLoopPreUpdate, handle_keepalive_response); } } @@ -41,28 +45,30 @@ pub struct KeepaliveState { last_send: Instant, } -/// Delay measured in milliseconds. Negative values indicate absence. -#[derive(Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deref)] -pub struct Ping(pub i32); - -impl Default for Ping { - fn default() -> Self { - Self(-1) +impl KeepaliveState { + /// When the last keepalive was sent for this client. + pub fn last_send(&self) -> Instant { + self.last_send } } -impl KeepaliveState { - pub(super) fn new() -> Self { +impl Default for KeepaliveState { + fn default() -> Self { Self { got_keepalive: true, last_keepalive_id: 0, last_send: Instant::now(), } } +} - /// When the last keepalive was sent for this client. - pub fn last_send(&self) -> Instant { - self.last_send +/// Delay measured in milliseconds. Negative values indicate absence. +#[derive(Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deref)] +pub struct Ping(pub i32); + +impl Default for Ping { + fn default() -> Self { + Self(-1) } } diff --git a/crates/valence_server/src/layer.rs b/crates/valence_server/src/layer.rs index 9a5d2fcbe..79bb12164 100644 --- a/crates/valence_server/src/layer.rs +++ b/crates/valence_server/src/layer.rs @@ -1,139 +1,221 @@ -//! 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". - -pub mod bvh; -pub mod chunk; -pub mod entity; +mod chunk_view_index; pub mod message; +use std::collections::BTreeSet; + use bevy_app::prelude::*; use bevy_ecs::prelude::*; -pub use chunk::ChunkLayer; -pub use entity::EntityLayer; -use valence_entity::{InitEntitiesSet, UpdateTrackedDataSet}; -use valence_protocol::encode::WritePacket; -use valence_protocol::{BlockPos, ChunkPos, Ident}; +use bevy_ecs::query::Has; +pub use chunk_view_index::ChunkViewIndex; +use derive_more::{Deref, DerefMut}; +use valence_entity::{OldPosition, Position}; +use valence_protocol::{ChunkPos, WritePacket}; +use valence_registry::dimension_type::DimensionTypeId; use valence_registry::{BiomeRegistry, DimensionTypeRegistry}; -use valence_server_common::Server; +use valence_server_common::{Despawned, Server}; -pub struct LayerPlugin; +use self::message::{LayerMessages, MessageKind}; +use crate::client::FlushPacketsSet; +use crate::dimension_layer::batch::Batch; +use crate::dimension_layer::{ChunkIndex, DimensionInfo}; +use crate::layer::message::MessageScope; +use crate::{Client, DimensionLayerBundle}; -/// 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 UpdateLayersPreClientSet; +/// Enables core functionality for layers. +pub struct LayerPlugin; -/// 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 UpdateLayersPostClientSet; +/// When queued messages in layers are written to the [`Client`] packet buffer +/// of all viewers. +#[derive(SystemSet, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct BroadcastLayerMessagesSet; impl Plugin for LayerPlugin { fn build(&self, app: &mut App) { - app.configure_sets( + app.configure_set( + PostUpdate, + BroadcastLayerMessagesSet.before(FlushPacketsSet), + ) + .add_systems( PostUpdate, ( - UpdateLayersPreClientSet - .after(InitEntitiesSet) - .after(UpdateTrackedDataSet), - UpdateLayersPostClientSet.after(UpdateLayersPreClientSet), - ), + update_view_index, + update_old_visible_layers, + broadcast_layer_messages, + ) + .chain() + .in_set(BroadcastLayerMessagesSet), ); - - chunk::build(app); - entity::build(app); } } -/// 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 packets to all viewers not - /// identified by `except`. - fn except_writer(&mut self, except: Entity) -> Self::ExceptWriter<'_>; - - /// 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<'_>; - - /// 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, - radius: u32, - except: Entity, - ) -> Self::RadiusExceptWriter<'_>; -} - -/// Convenience [`Bundle`] for spawning a layer entity with both [`ChunkLayer`] -/// and [`EntityLayer`] components. +/// Combination of components from [`DimensionLayerBundle`] and +/// [`EntityLayerBundle`]. The spawned entity from this bundle is considered +/// both a "dimension layer" and an "entity layer". #[derive(Bundle)] -pub struct LayerBundle { - pub chunk: ChunkLayer, - pub entity: EntityLayer, +pub struct CombinedLayerBundle { + pub chunk_index: ChunkIndex, + pub block_batch: Batch, + pub dimension_info: DimensionInfo, + // TODO: entity layer components. + pub chunk_view_index: ChunkViewIndex, + pub layer_viewers: LayerViewers, + pub layer_messages: LayerMessages, } -impl LayerBundle { - /// Returns a new layer bundle. +impl CombinedLayerBundle { pub fn new( - dimension_type_name: impl Into>, + dimension_type: DimensionTypeId, dimensions: &DimensionTypeRegistry, biomes: &BiomeRegistry, server: &Server, ) -> Self { + let DimensionLayerBundle { + chunk_index, + block_batch, + dimension_info, + chunk_view_index, + layer_viewers, + layer_messages, + } = DimensionLayerBundle::new(dimension_type, dimensions, biomes, server); + Self { - chunk: ChunkLayer::new(dimension_type_name, dimensions, biomes, server), - entity: EntityLayer::new(server), + chunk_index, + block_batch, + dimension_info, + chunk_view_index, + layer_viewers, + layer_messages, } } } + +/// The set of layers a client is viewing. +#[derive(Component, Clone, Default, DerefMut, Deref, Debug)] +pub struct VisibleLayers(pub BTreeSet); + +/// The contents of [`VisibleLayers`] from the previous tick. +#[derive(Component, Default, Deref, Debug)] +pub struct OldVisibleLayers(BTreeSet); + +/// The set of clients that are viewing a layer. +/// +/// This is updated automatically at the same time as [`ChunkViewIndex`]. +#[derive(Component, Clone, Default, Deref, Debug)] +pub struct LayerViewers(BTreeSet); + +fn update_view_index( + clients: Query<( + Entity, + Has, + &OldPosition, + Ref, + &OldVisibleLayers, + Ref, + )>, + mut layers: Query<(&mut LayerViewers, &mut ChunkViewIndex)>, +) { + for (client, is_despawned, old_pos, pos, old_visible, visible) in &clients { + if is_despawned { + // Remove from old layers. + for &layer in old_visible.iter() { + if let Ok((mut viewers, mut index)) = layers.get_mut(layer) { + let removed = viewers.0.remove(&client); + debug_assert!(removed); + + let removed = index.remove(old_pos.get(), client); + debug_assert!(removed); + } + } + } else if visible.is_changed() { + // Remove from old layers. + for &layer in old_visible.iter() { + if let Ok((mut viewers, mut index)) = layers.get_mut(layer) { + let removed = viewers.0.remove(&client); + debug_assert!(removed); + + let removed = index.remove(old_pos.get(), client); + debug_assert!(removed); + } + } + + // Insert in new layers. + for &layer in visible.iter() { + if let Ok((mut viewers, mut index)) = layers.get_mut(layer) { + let inserted = viewers.0.insert(client); + debug_assert!(inserted); + + let inserted = index.insert(pos.0, client); + debug_assert!(inserted); + } + } + } else if pos.is_changed() { + // Change chunk cell in layers. + + let old_pos = ChunkPos::from(old_pos.get()); + let pos = ChunkPos::from(pos.0); + + if old_pos != pos { + for &layer in visible.iter() { + if let Ok((_, mut index)) = layers.get_mut(layer) { + let removed = index.remove(old_pos, client); + debug_assert!(removed); + + let inserted = index.insert(pos, client); + debug_assert!(inserted); + } + } + } + } + } +} + +fn update_old_visible_layers( + mut layers: Query<(&mut OldVisibleLayers, &VisibleLayers), Changed>, +) { + for (mut old, new) in &mut layers { + old.0.clone_from(&new.0); + } +} + +fn broadcast_layer_messages( + mut layers: Query<(&mut LayerMessages, &LayerViewers, &ChunkViewIndex)>, + mut clients: Query<&mut Client>, +) { + for (mut messages, viewers, view_index) in &mut layers { + let mut acc = 0; + + for (scope, kind) in messages.messages() { + let mut send = |client: Entity| { + if let Ok(mut client) = clients.get_mut(client) { + match kind { + MessageKind::Packet { len } => { + client.write_packet_bytes(&messages.bytes()[acc..acc + len]); + } + MessageKind::EntityDespawn { entity } => todo!(), + } + } + }; + + match scope { + MessageScope::All => viewers.iter().copied().for_each(send), + MessageScope::Only { only } => send(only), + MessageScope::Except { except } => viewers + .iter() + .copied() + .filter(|&c| c != except) + .for_each(send), + MessageScope::ChunkView { pos } => view_index.get(pos).for_each(send), + MessageScope::ChunkViewExcept { pos, except } => { + view_index.get(pos).filter(|&e| e != except).for_each(send) + } + MessageScope::TransitionChunkView { include, exclude } => todo!(), + } + + if let MessageKind::Packet { len } = kind { + acc += len; + } + } + + messages.clear(); + } +} diff --git a/crates/valence_server/src/layer/bvh.rs b/crates/valence_server/src/layer/bvh.rs deleted file mode 100644 index 4f63cd01b..000000000 --- a/crates/valence_server/src/layer/bvh.rs +++ /dev/null @@ -1,339 +0,0 @@ -use std::mem; -use std::ops::Range; - -use valence_protocol::ChunkPos; - -use crate::ChunkView; - -/// A bounding volume hierarchy for chunk positions. -#[derive(Clone, Debug)] -pub struct ChunkBvh { - nodes: Vec, - values: Vec, -} - -impl Default for ChunkBvh { - fn default() -> Self { - Self::new() - } -} - -#[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 - } -} - -/// Obtains a chunk position for the purpose of placement in the BVH. -pub trait GetChunkPos { - fn chunk_pos(&self) -> ChunkPos; -} - -impl GetChunkPos for ChunkPos { - fn chunk_pos(&self) -> ChunkPos { - *self - } -} - -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(); - - 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's probably not worth it. - - 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 query(&self, view: ChunkView, mut f: impl FnMut(&T)) { - if let Some(root) = self.nodes.last() { - let (min, max) = view.bounding_box(); - self.query_rec(root, view, Aabb { min, max }, &mut f); - } - } - - fn query_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.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 } => { - 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) - } - } - } - } - } - } - - 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() { - 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 query_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 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("😔"); - viewed_positions.remove(idx); - }); - - assert!(viewed_positions.is_empty()); - } -} diff --git a/crates/valence_server/src/layer/chunk.rs b/crates/valence_server/src/layer/chunk.rs deleted file mode 100644 index 4da1c42e1..000000000 --- a/crates/valence_server/src/layer/chunk.rs +++ /dev/null @@ -1,793 +0,0 @@ -#[allow(clippy::module_inception)] -mod chunk; -pub mod loaded; -mod paletted_container; -pub mod unloaded; - -use std::borrow::Cow; -use std::collections::hash_map::{Entry, OccupiedEntry, VacantEntry}; -use std::fmt; - -use bevy_app::prelude::*; -use bevy_ecs::prelude::*; -pub use chunk::{MAX_HEIGHT, *}; -pub use loaded::LoadedChunk; -use rustc_hash::FxHashMap; -pub use unloaded::UnloadedChunk; -use valence_math::{DVec3, Vec3}; -use valence_nbt::Compound; -use valence_protocol::encode::{PacketWriter, WritePacket}; -use valence_protocol::packets::play::particle_s2c::Particle; -use valence_protocol::packets::play::{ParticleS2c, PlaySoundS2c}; -use valence_protocol::sound::{Sound, SoundCategory, SoundId}; -use valence_protocol::{BiomePos, BlockPos, ChunkPos, CompressionThreshold, Encode, Ident, Packet}; -use valence_registry::biome::{BiomeId, BiomeRegistry}; -use valence_registry::DimensionTypeRegistry; -use valence_server_common::Server; - -use super::bvh::GetChunkPos; -use super::message::Messages; -use super::{Layer, UpdateLayersPostClientSet, UpdateLayersPreClientSet}; - -/// A [`Component`] containing the [chunks](LoadedChunk) and [dimension -/// information](valence_registry::dimension_type::DimensionTypeId) of a -/// Minecraft world. -#[derive(Component, Debug)] -pub struct ChunkLayer { - messages: ChunkLayerMessages, - chunks: FxHashMap, - info: ChunkLayerInfo, -} - -/// Chunk layer information. -pub(crate) struct ChunkLayerInfo { - dimension_type_name: Ident, - height: u32, - min_y: i32, - biome_registry_len: usize, - threshold: CompressionThreshold, -} - -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("threshold", &self.threshold) - // Ignore sky light mask and array. - .finish() - } -} - -type ChunkLayerMessages = Messages; - -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] -pub(crate) enum GlobalMsg { - /// Send packet data to all clients viewing the layer. - Packet, - /// 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)] -pub(crate) enum LocalMsg { - /// Send packet data to all clients viewing the layer in view of `pos`. - 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, - }, - /// Message content is the data for a single biome in the "change biomes" - /// packet. - 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.into(), - LocalMsg::RadiusAtExcept { center, .. } => center.into(), - LocalMsg::ChangeBiome { pos } => pos, - LocalMsg::ChangeChunkState { pos } => pos, - } - } -} - -impl ChunkLayer { - pub(crate) const LOAD: u8 = 0; - pub(crate) const UNLOAD: u8 = 1; - pub(crate) const OVERWRITE: u8 = 2; - - /// Creates a new chunk layer. - #[track_caller] - pub fn new( - dimension_type_name: impl Into>, - dimensions: &DimensionTypeRegistry, - biomes: &BiomeRegistry, - server: &Server, - ) -> Self { - let dimension_type_name = dimension_type_name.into(); - - let dim = &dimensions[dimension_type_name.as_str_ident()]; - - assert!( - (0..MAX_HEIGHT as i32).contains(&dim.height), - "invalid dimension height of {}", - dim.height - ); - - Self { - messages: Messages::new(), - chunks: Default::default(), - info: ChunkLayerInfo { - dimension_type_name, - height: dim.height as u32, - min_y: dim.min_y, - biome_registry_len: biomes.iter().len(), - threshold: server.compression_threshold(), - }, - } - } - - /// 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() - } - - /// The height of this instance's dimension. - pub fn height(&self) -> u32 { - self.info.height - } - - /// The `min_y` of this instance's dimension. - pub fn min_y(&self) -> i32 { - self.info.min_y - } - - /// Get a reference to the chunk at the given position, if it is loaded. - pub fn chunk(&self, pos: impl Into) -> Option<&LoadedChunk> { - self.chunks.get(&pos.into()) - } - - /// Get a mutable reference to the chunk at the given position, if it is - /// loaded. - pub fn chunk_mut(&mut self, pos: impl Into) -> Option<&mut LoadedChunk> { - self.chunks.get_mut(&pos.into()) - } - - /// Insert a chunk into the instance at the given position. The previous - /// chunk data is returned. - pub fn insert_chunk( - &mut self, - pos: impl Into, - chunk: UnloadedChunk, - ) -> Option { - match self.chunk_entry(pos) { - ChunkEntry::Occupied(mut oe) => Some(oe.insert(chunk)), - ChunkEntry::Vacant(ve) => { - ve.insert(chunk); - None - } - } - } - - /// Unload the chunk at the given position, if it is loaded. Returns the - /// chunk if it was loaded. - pub fn remove_chunk(&mut self, pos: impl Into) -> Option { - match self.chunk_entry(pos) { - ChunkEntry::Occupied(oe) => Some(oe.remove()), - ChunkEntry::Vacant(_) => None, - } - } - - /// Unload all chunks in this instance. - pub fn clear_chunks(&mut self) { - self.retain_chunks(|_, _| false) - } - - /// Retain only the chunks for which the given predicate returns `true`. - pub fn retain_chunks(&mut self, mut f: F) - where - F: FnMut(ChunkPos, &mut LoadedChunk) -> bool, - { - self.chunks.retain(|pos, chunk| { - if !f(*pos, chunk) { - self.messages - .send_local_infallible(LocalMsg::ChangeChunkState { pos: *pos }, |b| { - b.push(Self::UNLOAD) - }); - - 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 { - messages: &mut self.messages, - entry: oe, - }), - Entry::Vacant(ve) => ChunkEntry::Vacant(VacantChunkEntry { - height: self.info.height, - messages: &mut self.messages, - entry: ve, - }), - } - } - - /// Get an iterator over all loaded chunks in the instance. The order of the - /// chunks is undefined. - pub fn chunks(&self) -> impl Iterator + Clone + '_ { - self.chunks.iter().map(|(pos, chunk)| (*pos, chunk)) - } - - /// Get an iterator over all loaded chunks in the instance, mutably. The - /// order of the chunks is undefined. - pub fn chunks_mut(&mut self) -> impl Iterator + '_ { - self.chunks.iter_mut().map(|(pos, chunk)| (*pos, chunk)) - } - - /// Optimizes the memory usage of the instance. - pub fn shrink_to_fit(&mut self) { - for (_, chunk) in self.chunks_mut() { - chunk.shrink_to_fit(); - } - - self.chunks.shrink_to_fit(); - self.messages.shrink_to_fit(); - } - - pub fn block(&self, pos: impl Into) -> Option { - let pos = pos.into(); - - let y = pos - .y - .checked_sub(self.info.min_y) - .and_then(|y| y.try_into().ok())?; - - if y >= self.info.height { - return None; - } - - let chunk = self.chunk(pos)?; - - let x = pos.x.rem_euclid(16) as u32; - let z = pos.z.rem_euclid(16) as u32; - - Some(chunk.block(x, y, z)) - } - - pub fn set_block(&mut self, pos: impl Into, block: impl IntoBlock) -> Option { - let pos = pos.into(); - - let y = pos - .y - .checked_sub(self.info.min_y) - .and_then(|y| y.try_into().ok())?; - - if y >= self.info.height { - return None; - } - - let chunk = self.chunk_mut(pos)?; - - let x = pos.x.rem_euclid(16) as u32; - let z = pos.z.rem_euclid(16) as u32; - - Some(chunk.set_block(x, y, z, block)) - } - - pub fn block_entity_mut(&mut self, pos: impl Into) -> Option<&mut Compound> { - let pos = pos.into(); - - let y = pos - .y - .checked_sub(self.info.min_y) - .and_then(|y| y.try_into().ok())?; - - if y >= self.info.height { - return None; - } - - let chunk = self.chunk_mut(pos)?; - - let x = pos.x.rem_euclid(16) as u32; - let z = pos.z.rem_euclid(16) as u32; - - chunk.block_entity_mut(x, y, z) - } - - pub fn biome(&self, pos: impl Into) -> Option { - let pos = pos.into(); - - let y = pos - .y - .checked_sub(self.info.min_y / 4) - .and_then(|y| y.try_into().ok())?; - - if y >= self.info.height / 4 { - return None; - } - - let chunk = self.chunk(pos)?; - - let x = pos.x.rem_euclid(4) as u32; - let z = pos.z.rem_euclid(4) as u32; - - Some(chunk.biome(x, y, z)) - } - - pub fn set_biome(&mut self, pos: impl Into, biome: BiomeId) -> Option { - let pos = pos.into(); - - let y = pos - .y - .checked_sub(self.info.min_y / 4) - .and_then(|y| y.try_into().ok())?; - - if y >= self.info.height / 4 { - return None; - } - - let chunk = self.chunk_mut(pos)?; - - let x = pos.x.rem_euclid(4) as u32; - let z = pos.z.rem_euclid(4) as u32; - - Some(chunk.set_biome(x, y, z, biome)) - } - - pub(crate) fn info(&self) -> &ChunkLayerInfo { - &self.info - } - - pub(crate) fn messages(&self) -> &ChunkLayerMessages { - &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 - /// appropriate chunk in view. - pub fn play_particle( - &mut self, - particle: &Particle, - long_distance: bool, - position: impl Into, - offset: impl Into, - max_speed: f32, - count: i32, - ) { - let position = position.into(); - - self.view_writer(position).write_packet(&ParticleS2c { - particle: Cow::Borrowed(particle), - long_distance, - position, - offset: offset.into(), - max_speed, - count, - }); - } - - // TODO: move to `valence_sound`. - /// Plays a sound effect at the given position in the world. The sound - /// effect is audible to all players in the instance with the - /// appropriate chunk in view. - pub fn play_sound( - &mut self, - sound: Sound, - category: SoundCategory, - position: impl Into, - volume: f32, - pitch: f32, - ) { - let position = position.into(); - - self.view_writer(position).write_packet(&PlaySoundS2c { - id: SoundId::Direct { - id: sound.to_ident().into(), - range: None, - }, - category, - position: (position * 8.0).as_ivec3(), - volume, - pitch, - seed: rand::random(), - }); - } -} - -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 { - 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.threshold).write_packet_fallible(packet) - }) - } - - fn write_packet_bytes(&mut self, bytes: &[u8]) { - self.messages - .send_global_infallible(GlobalMsg::Packet, |b| b.extend_from_slice(bytes)); - } -} - -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.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 WritePacket for ViewWriter<'_> { - 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.threshold).write_packet_fallible(packet) - }) - } - - fn write_packet_bytes(&mut self, bytes: &[u8]) { - self.layer - .messages - .send_local_infallible(LocalMsg::PacketAt { pos: self.pos }, |b| { - b.extend_from_slice(bytes) - }); - } -} - -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.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.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.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>), - Vacant(VacantChunkEntry<'a>), -} - -impl<'a> ChunkEntry<'a> { - pub fn or_default(self) -> &'a mut LoadedChunk { - match self { - ChunkEntry::Occupied(oe) => oe.into_mut(), - ChunkEntry::Vacant(ve) => ve.insert(UnloadedChunk::new()), - } - } -} - -#[derive(Debug)] -pub struct OccupiedChunkEntry<'a> { - messages: &'a mut ChunkLayerMessages, - entry: OccupiedEntry<'a, ChunkPos, LoadedChunk>, -} - -impl<'a> OccupiedChunkEntry<'a> { - pub fn get(&self) -> &LoadedChunk { - self.entry.get() - } - - pub fn get_mut(&mut self) -> &mut LoadedChunk { - self.entry.get_mut() - } - - pub fn insert(&mut self, chunk: UnloadedChunk) -> UnloadedChunk { - self.messages.send_local_infallible( - LocalMsg::ChangeChunkState { - pos: *self.entry.key(), - }, - |b| b.push(ChunkLayer::OVERWRITE), - ); - - self.entry.get_mut().insert(chunk) - } - - pub fn into_mut(self) -> &'a mut LoadedChunk { - self.entry.into_mut() - } - - pub fn key(&self) -> &ChunkPos { - self.entry.key() - } - - pub fn remove(self) -> UnloadedChunk { - self.messages.send_local_infallible( - LocalMsg::ChangeChunkState { - pos: *self.entry.key(), - }, - |b| b.push(ChunkLayer::UNLOAD), - ); - - self.entry.remove().remove() - } - - pub fn remove_entry(mut self) -> (ChunkPos, UnloadedChunk) { - let pos = *self.entry.key(); - let chunk = self.entry.get_mut().remove(); - - self.messages.send_local_infallible( - LocalMsg::ChangeChunkState { - pos: *self.entry.key(), - }, - |b| b.push(ChunkLayer::UNLOAD), - ); - - (pos, chunk) - } -} - -#[derive(Debug)] -pub struct VacantChunkEntry<'a> { - height: u32, - messages: &'a mut ChunkLayerMessages, - 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); - loaded.insert(chunk); - - self.messages.send_local_infallible( - LocalMsg::ChangeChunkState { - pos: *self.entry.key(), - }, - |b| b.push(ChunkLayer::LOAD), - ); - - self.entry.insert(loaded) - } - - pub fn into_key(self) -> ChunkPos { - *self.entry.key() - } - - pub fn key(&self) -> &ChunkPos { - self.entry.key() - } -} - -pub(super) fn build(app: &mut App) { - app.add_systems( - PostUpdate, - ( - update_chunk_layers_pre_client.in_set(UpdateLayersPreClientSet), - update_chunk_layers_post_client.in_set(UpdateLayersPostClientSet), - ), - ); -} - -fn update_chunk_layers_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_chunk_layers_post_client(mut layers: Query<&mut ChunkLayer>) { - for mut layer in &mut layers { - layer.messages.unready(); - } -} diff --git a/crates/valence_server/src/layer/chunk/loaded.rs b/crates/valence_server/src/layer/chunk/loaded.rs deleted file mode 100644 index 5f71b75b2..000000000 --- a/crates/valence_server/src/layer/chunk/loaded.rs +++ /dev/null @@ -1,693 +0,0 @@ -use std::borrow::Cow; -use std::collections::{BTreeMap, BTreeSet}; -use std::mem; -use std::sync::atomic::{AtomicU32, Ordering}; - -use parking_lot::Mutex; // Using nonstandard mutex to avoid poisoning API. -use valence_nbt::{compound, Compound}; -use valence_protocol::encode::{PacketWriter, WritePacket}; -use valence_protocol::packets::play::chunk_data_s2c::ChunkDataBlockEntity; -use valence_protocol::packets::play::chunk_delta_update_s2c::ChunkDeltaUpdateEntry; -use valence_protocol::packets::play::{ - BlockEntityUpdateS2c, BlockUpdateS2c, ChunkDataS2c, ChunkDeltaUpdateS2c, -}; -use valence_protocol::{BlockPos, BlockState, ChunkPos, ChunkSectionPos, Encode}; -use valence_registry::biome::BiomeId; -use valence_registry::RegistryIdx; - -use super::chunk::{ - bit_width, check_biome_oob, check_block_oob, check_section_oob, BiomeContainer, - BlockStateContainer, Chunk, SECTION_BLOCK_COUNT, -}; -use super::paletted_container::PalettedContainer; -use super::unloaded::{self, UnloadedChunk}; -use super::{ChunkLayerInfo, ChunkLayerMessages, LocalMsg}; - -#[derive(Debug)] -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 zero. - viewer_count: AtomicU32, - /// Block and biome data for the chunk. - sections: Box<[Section]>, - /// The block entities in this chunk. - block_entities: BTreeMap, - /// The set of block entities that have been modified this tick. - changed_block_entities: BTreeSet, - /// If any biomes in this chunk have been modified this tick. - changed_biomes: bool, - /// 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>, -} - -#[derive(Clone, Default, Debug)] -struct Section { - block_states: BlockStateContainer, - biomes: BiomeContainer, - /// Contains modifications for the update section packet. (Or the regular - /// block update packet if len == 1). - section_updates: Vec, -} - -impl Section { - fn count_non_air_blocks(&self) -> u16 { - let mut count = 0; - - match &self.block_states { - PalettedContainer::Single(s) => { - if !s.is_air() { - count += SECTION_BLOCK_COUNT as u16; - } - } - PalettedContainer::Indirect(ind) => { - for i in 0..SECTION_BLOCK_COUNT { - if !ind.get(i).is_air() { - count += 1; - } - } - } - PalettedContainer::Direct(dir) => { - for s in dir.as_ref() { - if !s.is_air() { - count += 1; - } - } - } - } - - count - } -} - -impl LoadedChunk { - pub(crate) fn new(height: u32) -> Self { - Self { - 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![]), - } - } - - /// Sets the content of this chunk to the supplied [`UnloadedChunk`]. The - /// given unloaded chunk is [resized] to match the height of this loaded - /// chunk prior to insertion. - /// - /// The previous chunk data is returned. - /// - /// [resized]: UnloadedChunk::set_height - pub(crate) fn insert(&mut self, mut chunk: UnloadedChunk) -> UnloadedChunk { - chunk.set_height(self.height()); - - let old_sections = self - .sections - .iter_mut() - .zip(chunk.sections) - .map(|(sect, other_sect)| { - sect.section_updates.clear(); - - unloaded::Section { - block_states: mem::replace(&mut sect.block_states, other_sect.block_states), - biomes: mem::replace(&mut sect.biomes, other_sect.biomes), - } - }) - .collect(); - let old_block_entities = mem::replace(&mut self.block_entities, chunk.block_entities); - self.changed_block_entities.clear(); - self.changed_biomes = false; - self.cached_init_packets.get_mut().clear(); - - self.assert_no_changes(); - - UnloadedChunk { - sections: old_sections, - block_entities: old_block_entities, - } - } - - pub(crate) fn remove(&mut self) -> UnloadedChunk { - let old_sections = self - .sections - .iter_mut() - .map(|sect| { - sect.section_updates.clear(); - - unloaded::Section { - block_states: mem::take(&mut sect.block_states), - biomes: mem::take(&mut sect.biomes), - } - }) - .collect(); - let old_block_entities = mem::take(&mut self.block_entities); - self.changed_block_entities.clear(); - self.changed_biomes = false; - self.cached_init_packets.get_mut().clear(); - - self.assert_no_changes(); - - UnloadedChunk { - sections: old_sections, - block_entities: old_block_entities, - } - } - - /// Returns the number of clients in view of this chunk. - pub fn viewer_count(&self) -> u32 { - self.viewer_count.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() - } - - /// Increments the viewer count. - pub(crate) fn inc_viewer_count(&self) { - self.viewer_count.fetch_add(1, Ordering::Relaxed); - } - - /// Decrements the viewer count. - #[track_caller] - pub(crate) 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. - /// - Chunk change messages are written to the layer. - /// - Recorded changes are cleared. - pub(crate) fn update_pre_client( - &mut self, - pos: ChunkPos, - info: &ChunkLayerInfo, - messages: &mut ChunkLayerMessages, - ) { - 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(); - - return; - } - - // Block states - for (sect_y, sect) in self.sections.iter_mut().enumerate() { - match sect.section_updates.as_slice() { - &[] => {} - &[entry] => { - let global_x = pos.x * 16 + entry.off_x() as i32; - let global_y = info.min_y + sect_y as i32 * 16 + entry.off_y() as i32; - let global_z = pos.z * 16 + entry.off_z() as i32; - - messages.send_local_infallible(LocalMsg::PacketAt { pos }, |buf| { - let mut writer = PacketWriter::new(buf, info.threshold); - - writer.write_packet(&BlockUpdateS2c { - position: BlockPos::new(global_x, global_y, global_z), - block_id: BlockState::from_raw(entry.block_state() as u16).unwrap(), - }); - }); - } - entries => { - let chunk_sect_pos = ChunkSectionPos { - x: pos.x, - y: sect_y as i32 + info.min_y.div_euclid(16), - z: pos.z, - }; - - messages.send_local_infallible(LocalMsg::PacketAt { pos }, |buf| { - let mut writer = PacketWriter::new(buf, info.threshold); - - writer.write_packet(&ChunkDeltaUpdateS2c { - chunk_sect_pos, - blocks: Cow::Borrowed(entries), - }); - }); - } - } - - 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; - - messages.send_local_infallible(LocalMsg::PacketAt { pos }, |buf| { - let mut writer = PacketWriter::new(buf, info.threshold); - - writer.write_packet(&BlockEntityUpdateS2c { - position: BlockPos::new(global_x, global_y, global_z), - kind, - data: Cow::Borrowed(nbt), - }); - }); - } - - self.changed_block_entities.clear(); - - // Biomes - if self.changed_biomes { - self.changed_biomes = false; - - messages.send_local_infallible(LocalMsg::ChangeBiome { pos }, |buf| { - for sect in self.sections.iter() { - sect.biomes - .encode_mc_format( - &mut *buf, - |b| b.to_index() as _, - 0, - 3, - bit_width(info.biome_registry_len - 1), - ) - .expect("paletted container encode should always succeed"); - } - }); - } - - // All changes should be cleared. - self.assert_no_changes(); - } - - /// Writes the packet data needed to initialize this chunk. - pub(crate) fn write_init_packets( - &self, - mut writer: impl WritePacket, - pos: ChunkPos, - info: &ChunkLayerInfo, - ) { - let mut init_packets = self.cached_init_packets.lock(); - - if init_packets.is_empty() { - let heightmaps = compound! { - // TODO: MOTION_BLOCKING and WORLD_SURFACE heightmaps. - }; - - let mut blocks_and_biomes: Vec = vec![]; - - for sect in self.sections.iter() { - sect.count_non_air_blocks() - .encode(&mut blocks_and_biomes) - .unwrap(); - - sect.block_states - .encode_mc_format( - &mut blocks_and_biomes, - |b| b.to_raw().into(), - 4, - 8, - bit_width(BlockState::max_raw().into()), - ) - .expect("paletted container encode should always succeed"); - - sect.biomes - .encode_mc_format( - &mut blocks_and_biomes, - |b| b.to_index() as _, - 0, - 3, - bit_width(info.biome_registry_len - 1), - ) - .expect("paletted container encode should always succeed"); - } - - let block_entities: Vec<_> = self - .block_entities - .iter() - .filter_map(|(&idx, nbt)| { - let x = idx % 16; - let z = idx / 16 % 16; - let y = idx / 16 / 16; - - let kind = self.sections[y as usize / 16] - .block_states - .get(idx as usize % SECTION_BLOCK_COUNT) - .block_entity_kind(); - - kind.map(|kind| ChunkDataBlockEntity { - packed_xz: ((x << 4) | z) as i8, - y: y as i16 + info.min_y as i16, - kind, - data: Cow::Borrowed(nbt), - }) - }) - .collect(); - - PacketWriter::new(&mut init_packets, info.threshold).write_packet(&ChunkDataS2c { - pos, - heightmaps: Cow::Owned(heightmaps), - blocks_and_biomes: &blocks_and_biomes, - block_entities: Cow::Owned(block_entities), - 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(&[]), - block_light_arrays: Cow::Borrowed(&[]), - }) - } - - writer.write_packet_bytes(&init_packets); - } - - /// Asserts that no changes to this chunk are currently recorded. - #[track_caller] - fn assert_no_changes(&self) { - #[cfg(debug_assertions)] - { - assert!(!self.changed_biomes); - assert!(self.changed_block_entities.is_empty()); - - for sect in self.sections.iter() { - assert!(sect.section_updates.is_empty()); - } - } - } -} - -impl Chunk for LoadedChunk { - fn height(&self) -> u32 { - self.sections.len() as u32 * 16 - } - - fn block_state(&self, x: u32, y: u32, z: u32) -> BlockState { - check_block_oob(self, x, y, z); - - let idx = x + z * 16 + y % 16 * 16 * 16; - self.sections[y as usize / 16] - .block_states - .get(idx as usize) - } - - fn set_block_state(&mut self, x: u32, y: u32, z: u32, block: BlockState) -> BlockState { - check_block_oob(self, x, y, z); - - let sect_y = y / 16; - let sect = &mut self.sections[sect_y as usize]; - let idx = x + z * 16 + y % 16 * 16 * 16; - - let old_block = sect.block_states.set(idx as usize, block); - - if block != old_block { - self.cached_init_packets.get_mut().clear(); - - if *self.viewer_count.get_mut() > 0 { - sect.section_updates.push( - ChunkDeltaUpdateEntry::new() - .with_off_x(x as u8) - .with_off_y((y % 16) as u8) - .with_off_z(z as u8) - .with_block_state(block.to_raw().into()), - ); - } - } - - old_block - } - - fn fill_block_state_section(&mut self, sect_y: u32, block: BlockState) { - check_section_oob(self, sect_y); - - let sect = &mut self.sections[sect_y as usize]; - - if let PalettedContainer::Single(b) = §.block_states { - if *b != block { - self.cached_init_packets.get_mut().clear(); - - if *self.viewer_count.get_mut() > 0 { - // The whole section is being modified, so any previous modifications would - // be overwritten. - sect.section_updates.clear(); - - // Push section updates for all the blocks in the section. - sect.section_updates.reserve_exact(SECTION_BLOCK_COUNT); - for z in 0..16 { - for x in 0..16 { - for y in 0..16 { - sect.section_updates.push( - ChunkDeltaUpdateEntry::new() - .with_off_x(x) - .with_off_y(y) - .with_off_z(z) - .with_block_state(block.to_raw().into()), - ); - } - } - } - } - } - } else { - for z in 0..16 { - for x in 0..16 { - for y in 0..16 { - let idx = x + z * 16 + (sect_y * 16 + y) * (16 * 16); - - if block != sect.block_states.get(idx as usize) { - self.cached_init_packets.get_mut().clear(); - - if *self.viewer_count.get_mut() > 0 { - sect.section_updates.push( - ChunkDeltaUpdateEntry::new() - .with_off_x(x as u8) - .with_off_y(y as u8) - .with_off_z(z as u8) - .with_block_state(block.to_raw().into()), - ); - } - } - } - } - } - } - - sect.block_states.fill(block); - } - - fn block_entity(&self, x: u32, y: u32, z: u32) -> Option<&Compound> { - check_block_oob(self, x, y, z); - - let idx = x + z * 16 + y * 16 * 16; - self.block_entities.get(&idx) - } - - fn block_entity_mut(&mut self, x: u32, y: u32, z: u32) -> Option<&mut Compound> { - check_block_oob(self, x, y, z); - - let idx = x + z * 16 + y * 16 * 16; - - if let Some(be) = self.block_entities.get_mut(&idx) { - if *self.viewer_count.get_mut() > 0 { - self.changed_block_entities.insert(idx); - } - self.cached_init_packets.get_mut().clear(); - - Some(be) - } else { - None - } - } - - fn set_block_entity( - &mut self, - x: u32, - y: u32, - z: u32, - block_entity: Option, - ) -> Option { - check_block_oob(self, x, y, z); - - let idx = x + z * 16 + y * 16 * 16; - - match block_entity { - Some(nbt) => { - if *self.viewer_count.get_mut() > 0 { - self.changed_block_entities.insert(idx); - } - self.cached_init_packets.get_mut().clear(); - - self.block_entities.insert(idx, nbt) - } - None => { - let res = self.block_entities.remove(&idx); - - if res.is_some() { - self.cached_init_packets.get_mut().clear(); - } - - res - } - } - } - - fn clear_block_entities(&mut self) { - if self.block_entities.is_empty() { - return; - } - - self.cached_init_packets.get_mut().clear(); - - if *self.viewer_count.get_mut() > 0 { - self.changed_block_entities - .extend(mem::take(&mut self.block_entities).into_keys()); - } else { - self.block_entities.clear(); - } - } - - fn biome(&self, x: u32, y: u32, z: u32) -> BiomeId { - check_biome_oob(self, x, y, z); - - let idx = x + z * 4 + y % 4 * 4 * 4; - self.sections[y as usize / 4].biomes.get(idx as usize) - } - - fn set_biome(&mut self, x: u32, y: u32, z: u32, biome: BiomeId) -> BiomeId { - check_biome_oob(self, x, y, z); - - let idx = x + z * 4 + y % 4 * 4 * 4; - let old_biome = self.sections[y as usize / 4] - .biomes - .set(idx as usize, biome); - - if biome != old_biome { - self.cached_init_packets.get_mut().clear(); - - if *self.viewer_count.get_mut() > 0 { - self.changed_biomes = true; - } - } - - old_biome - } - - fn fill_biome_section(&mut self, sect_y: u32, biome: BiomeId) { - check_section_oob(self, sect_y); - - let sect = &mut self.sections[sect_y as usize]; - - if let PalettedContainer::Single(b) = §.biomes { - if *b != biome { - self.cached_init_packets.get_mut().clear(); - self.changed_biomes = *self.viewer_count.get_mut() > 0; - } - } else { - self.cached_init_packets.get_mut().clear(); - self.changed_biomes = *self.viewer_count.get_mut() > 0; - } - - sect.biomes.fill(biome); - } - - 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.shrink_to_fit(); - sect.biomes.shrink_to_fit(); - sect.section_updates.shrink_to_fit(); - } - } -} - -#[cfg(test)] -mod tests { - use valence_protocol::{ident, CompressionThreshold}; - - use super::*; - - #[test] - fn loaded_chunk_unviewed_no_changes() { - let mut chunk = LoadedChunk::new(512); - - chunk.set_block(0, 10, 0, BlockState::MAGMA_BLOCK); - chunk.assert_no_changes(); - - chunk.set_biome(0, 0, 0, BiomeId::from_index(5)); - chunk.assert_no_changes(); - - chunk.fill_block_states(BlockState::ACACIA_BUTTON); - chunk.assert_no_changes(); - - chunk.fill_biomes(BiomeId::from_index(42)); - chunk.assert_no_changes(); - } - - #[test] - fn loaded_chunk_changes_clear_packet_cache() { - #[track_caller] - fn check(chunk: &mut LoadedChunk, change: impl FnOnce(&mut LoadedChunk) -> T) { - let info = ChunkLayerInfo { - dimension_type_name: ident!("whatever").into(), - height: 512, - min_y: -16, - biome_registry_len: 200, - threshold: CompressionThreshold(-1), - }; - - let mut buf = vec![]; - let mut writer = PacketWriter::new(&mut buf, CompressionThreshold(-1)); - - // Rebuild cache. - chunk.write_init_packets(&mut writer, ChunkPos::new(3, 4), &info); - - // Check that the cache is built. - assert!(!chunk.cached_init_packets.get_mut().is_empty()); - - // Making a change should clear the cache. - change(chunk); - assert!(chunk.cached_init_packets.get_mut().is_empty()); - - // Rebuild cache again. - chunk.write_init_packets(&mut writer, ChunkPos::new(3, 4), &info); - assert!(!chunk.cached_init_packets.get_mut().is_empty()); - } - - let mut chunk = LoadedChunk::new(512); - - check(&mut chunk, |c| { - c.set_block_state(0, 4, 0, BlockState::ACACIA_WOOD) - }); - check(&mut chunk, |c| c.set_biome(1, 2, 3, BiomeId::from_index(4))); - check(&mut chunk, |c| c.fill_biomes(BiomeId::DEFAULT)); - check(&mut chunk, |c| c.fill_block_states(BlockState::WET_SPONGE)); - check(&mut chunk, |c| { - c.set_block_entity(3, 40, 5, Some(compound! {})) - }); - check(&mut chunk, |c| { - c.block_entity_mut(3, 40, 5).unwrap(); - }); - check(&mut chunk, |c| c.set_block_entity(3, 40, 5, None)); - - // Old block state is the same as new block state, so the cache should still be - // intact. - assert_eq!( - chunk.set_block_state(0, 0, 0, BlockState::WET_SPONGE), - BlockState::WET_SPONGE - ); - - assert!(!chunk.cached_init_packets.get_mut().is_empty()); - } -} diff --git a/crates/valence_server/src/layer/chunk_view_index.rs b/crates/valence_server/src/layer/chunk_view_index.rs new file mode 100644 index 000000000..1a9f72cb2 --- /dev/null +++ b/crates/valence_server/src/layer/chunk_view_index.rs @@ -0,0 +1,63 @@ +use std::collections::hash_map::Entry; + +use bevy_ecs::prelude::*; +use rustc_hash::FxHashMap; +use valence_protocol::ChunkPos; + +/// Maps chunk positions to the set of clients in view of the chunk. +#[derive(Component, Default, Debug)] +pub struct ChunkViewIndex { + map: FxHashMap>, +} + +impl ChunkViewIndex { + pub fn get( + &self, + pos: impl Into, + ) -> impl ExactSizeIterator + Clone + '_ { + self.map + .get(&pos.into()) + .map(|v| v.iter().copied()) + .unwrap_or_default() + } + + pub(super) fn insert(&mut self, pos: impl Into, client: Entity) -> bool { + match self.map.entry(pos.into()) { + Entry::Occupied(oe) => { + let v = oe.into_mut(); + + if !v.contains(&client) { + v.push(client); + true + } else { + false + } + } + Entry::Vacant(ve) => { + ve.insert(vec![client]); + true + } + } + } + + pub(super) fn remove(&mut self, pos: impl Into, client: Entity) -> bool { + match self.map.entry(pos.into()) { + Entry::Occupied(mut oe) => { + let v = oe.get_mut(); + + if let Some(idx) = v.iter().copied().position(|e| e == client) { + v.swap_remove(idx); + + if v.is_empty() { + oe.remove(); + } + + true + } else { + false + } + } + Entry::Vacant(_) => false, + } + } +} diff --git a/crates/valence_server/src/layer/entity.rs b/crates/valence_server/src/layer/entity.rs deleted file mode 100644 index 85080efba..000000000 --- a/crates/valence_server/src/layer/entity.rs +++ /dev/null @@ -1,529 +0,0 @@ -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_entity::query::UpdateEntityQuery; -use valence_entity::{EntityId, EntityLayerId, OldEntityLayerId, OldPosition, Position}; -use valence_protocol::encode::{PacketWriter, WritePacket}; -use valence_protocol::{BlockPos, ChunkPos, CompressionThreshold, Encode, Packet}; -use valence_server_common::{Despawned, Server}; - -use super::bvh::GetChunkPos; -use super::message::Messages; -use super::{Layer, UpdateLayersPostClientSet, UpdateLayersPreClientSet}; -use crate::client::Client; - -/// A [`Component`] containing Minecraft entities. -#[derive(Component, Debug)] -pub struct EntityLayer { - messages: EntityLayerMessages, - entities: FxHashMap>, - threshold: CompressionThreshold, -} - -type EntityLayerMessages = Messages; - -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] -pub(crate) enum GlobalMsg { - /// 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)] -// NOTE: Variant order is significant. Despawns should be ordered before spawns. -pub(crate) 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 }, - /// 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`. - /// 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`. 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, - }, -} - -impl GetChunkPos for LocalMsg { - fn chunk_pos(&self) -> ChunkPos { - match *self { - LocalMsg::PacketAt { pos } => pos, - LocalMsg::PacketAtExcept { pos, .. } => pos, - LocalMsg::RadiusAt { center, .. } => center.into(), - LocalMsg::RadiusAtExcept { center, .. } => center.into(), - LocalMsg::SpawnEntity { pos, .. } => pos, - LocalMsg::SpawnEntityTransition { pos, .. } => pos, - LocalMsg::DespawnEntity { pos, .. } => pos, - LocalMsg::DespawnEntityTransition { pos, .. } => pos, - } - } -} - -impl EntityLayer { - /// Creates a new entity layer. - pub fn new(server: &Server) -> Self { - Self { - messages: Messages::new(), - entities: Default::default(), - threshold: server.compression_threshold(), - } - } - - /// Returns an iterator over all entities contained within the given chunk - /// position in 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()) - } - - pub(crate) fn messages(&self) -> &EntityLayerMessages { - &self.messages - } -} - -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 { - 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.threshold).write_packet_fallible(packet) - }) - } - - fn write_packet_bytes(&mut self, bytes: &[u8]) { - self.messages - .send_global_infallible(GlobalMsg::Packet, |b| b.extend_from_slice(bytes)); - } -} - -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.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, -} - -impl<'a> WritePacket for ViewWriter<'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.threshold).write_packet_fallible(packet) - }) - } - - fn write_packet_bytes(&mut self, bytes: &[u8]) { - self.layer - .messages - .send_local_infallible(LocalMsg::PacketAt { pos: self.pos }, |b| { - b.extend_from_slice(bytes) - }); - } -} - -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.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.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.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, - ( - ( - change_entity_positions, - send_entity_update_messages, - send_layer_despawn_messages, - ready_entity_layers, - ) - .chain() - .in_set(UpdateLayersPreClientSet), - unready_entity_layers.in_set(UpdateLayersPostClientSet), - ), - ); -} - -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 = ChunkPos::from(pos.0); - let old_chunk_pos = ChunkPos::from(old_pos.get()); - - 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_infallible( - 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_infallible( - 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_infallible( - 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_infallible( - 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_infallible( - LocalMsg::SpawnEntityTransition { - pos: chunk_pos, - src_pos: old_chunk_pos, - }, - |b| b.extend_from_slice(&entity.to_bits().to_ne_bytes()), - ); - } - } - } - } -} - -fn send_entity_update_messages( - 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 = ChunkPos::from(update.pos.0); - - // 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_infallible(msg, |b| { - update.write_update_packets(PacketWriter::new(b, layer.threshold)) - }); - } else { - panic!( - "Entity {entity:?} was not properly removed from entity layer. Did you \ - forget to use the `Despawned` component?" - ); - } - } - } - } -} - -fn send_layer_despawn_messages(mut layers: Query<&mut EntityLayer, With>) { - for mut layer in &mut layers { - layer - .messages - .send_global_infallible(GlobalMsg::DespawnLayer, |_| {}); - } -} - -fn ready_entity_layers(mut layers: Query<&mut EntityLayer>) { - for mut layer in &mut layers { - layer.messages.ready(); - } -} - -fn unready_entity_layers(mut layers: Query<&mut EntityLayer>) { - for mut layer in &mut layers { - layer.messages.unready(); - } -} diff --git a/crates/valence_server/src/layer/message.rs b/crates/valence_server/src/layer/message.rs index c52269d1d..868e83f47 100644 --- a/crates/valence_server/src/layer/message.rs +++ b/crates/valence_server/src/layer/message.rs @@ -1,311 +1,167 @@ -use core::fmt; -use std::convert::Infallible; -use std::ops::Range; - -use valence_protocol::ChunkPos; - -use crate::layer::bvh::{ChunkBvh, GetChunkPos}; -use crate::ChunkView; - -/// 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)>, - bvh: ChunkBvh>, - staging: Vec, - ready: Vec, - is_ready: bool, +use bevy_ecs::prelude::*; +use valence_entity::{OldPosition, Position}; +use valence_protocol::encode::PacketWriter; +use valence_protocol::{ChunkPos, CompressionThreshold, Encode, Packet, WritePacket}; + +use super::ChunkViewIndex; +use crate::Client; + +#[derive(Component, Debug)] +pub struct LayerMessages { + bytes: Vec, + messages: Vec, + threshold: CompressionThreshold, } -impl Messages -where - 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, - f: impl FnOnce(&mut Vec) -> Result<(), E>, - ) -> Result<(), E> { - debug_assert!(!self.is_ready); +pub(super) type Message = (MessageScope, MessageKind); + +#[derive(Debug, Copy, Clone, PartialEq, Default)] +#[non_exhaustive] +pub enum MessageScope { + /// All clients viewing the layer will receive the message. + #[default] + All, + /// Only the client identified by `only` will receive the message. + Only { + only: Entity, + }, + /// All clients viewing the layer will receive the message, except the + /// client identified by `except`. + Except { + except: Entity, + }, + /// All clients in view of the chunk position `pos` will receive the + /// message. + ChunkView { + pos: ChunkPos, + }, + ChunkViewExcept { + pos: ChunkPos, + except: Entity, + }, + /// All clients in view of `include` but _not_ in view of `exclude` will + /// receive the message. + TransitionChunkView { + include: ChunkPos, + exclude: ChunkPos, + }, +} - let start = self.staging.len(); - f(&mut self.staging)?; - let end = self.staging.len(); +#[derive(Debug, Copy, Clone)] +pub(super) enum MessageKind { + Packet { len: usize }, + EntityDespawn { entity: Entity }, +} - if let Some((m, range)) = self.global.last_mut() { - if msg == *m { - // Extend the existing message. - range.end = end as u32; - return Ok(()); - } +impl LayerMessages { + pub fn new(threshold: CompressionThreshold) -> Self { + Self { + bytes: vec![], + messages: vec![], + threshold, } - - self.global.push((msg, start as u32..end as u32)); - - Ok(()) } - /// Adds a local message to this message buffer. - 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)?; - 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 Ok(()); - } - } - - self.local.push((msg, start as u32..end as u32)); - - Ok(()) + pub fn send_packet

(&mut self, scope: MessageScope, packet: &P) + where + P: Packet + Encode, + { + self.packet_writer(scope).write_packet(packet) } - /// 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); - Ok(()) - }); - } - - /// 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); - Ok(()) - }); - } - - /// 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()); + pub fn packet_writer(&mut self, scope: MessageScope) -> impl WritePacket + '_ { + struct Writer<'a> { + messages: &'a mut LayerMessages, + scope: MessageScope, + } - self.ready.reserve_exact(self.staging.len()); + impl WritePacket for Writer<'_> { + fn write_packet_fallible

(&mut self, packet: &P) -> anyhow::Result<()> + where + P: Packet + Encode, + { + let start = self.messages.bytes.len(); + let mut writer = + PacketWriter::new(&mut self.messages.bytes, self.messages.threshold); + + // Check if we're able to extend the last message instead of pushing a new one. + if let Some((last_scope, last_kind)) = self.messages.messages.last_mut() { + if let MessageKind::Packet { len } = last_kind { + if *last_scope == self.scope { + writer.write_packet_fallible(packet)?; + let end = writer.buf.len(); + + *len += end - start; + + return Ok(()); + } + } + } - fn sort_and_merge( - msgs: &mut Vec<(M, Range)>, - staging: &[u8], - ready: &mut Vec, - ) { - // Sort must be stable. - msgs.sort_by_key(|(msg, _)| msg.clone()); + writer.write_packet_fallible(packet)?; + let end = writer.buf.len(); - // 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(); + self.messages + .messages + .push((self.scope, MessageKind::Packet { len: end - start })); - *range = start as u32..end as u32; + Ok(()) } - 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]; + fn write_packet_bytes(&mut self, bytes: &[u8]) { + self.messages.bytes.extend_from_slice(bytes); - ready.extend_from_slice(right_bytes); + if let Some((last_scope, last_kind)) = self.messages.messages.last_mut() { + if let MessageKind::Packet { len } = last_kind { + if *last_scope == self.scope { + *len += bytes.len(); - 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 + return; + } + } } - }); - } - 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(); - } - - /// 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.messages + .messages + .push((self.scope, MessageKind::Packet { len: bytes.len() })); + } + } - self.global - .iter() - .map(|(m, r)| (m.clone(), r.start as usize..r.end as usize)) + Writer { + messages: self, + scope, + } } - /// 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.clone(), - pair.range.start as usize..pair.range.end as usize, - ) - }); + pub fn clear(&mut self) { + self.messages.clear(); + self.bytes.clear(); } -} -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(), - } + pub fn threshold(&self) -> CompressionThreshold { + self.threshold } -} -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() + pub(super) fn bytes(&self) -> &[u8] { + &self.bytes } -} - -#[derive(Debug)] -struct MessagePair { - msg: M, - range: Range, -} -impl GetChunkPos for MessagePair { - fn chunk_pos(&self) -> ChunkPos { - self.msg.chunk_pos() + pub(super) fn messages(&self) -> impl Iterator + '_ { + self.messages.iter().copied() } } -#[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!() - } +impl WritePacket for LayerMessages { + fn write_packet_fallible

(&mut self, packet: &P) -> anyhow::Result<()> + where + P: Packet + Encode, + { + self.packet_writer(MessageScope::All) + .write_packet_fallible(packet) } - #[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_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(); - - 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(); + fn write_packet_bytes(&mut self, bytes: &[u8]) { + self.packet_writer(MessageScope::All) + .write_packet_bytes(bytes) } } diff --git a/crates/valence_server/src/lib.rs b/crates/valence_server/src/lib.rs index feb9a5902..194d8ea42 100644 --- a/crates/valence_server/src/lib.rs +++ b/crates/valence_server/src/lib.rs @@ -25,7 +25,10 @@ pub mod client; pub mod client_command; pub mod client_settings; pub mod custom_payload; +pub mod dimension_layer; +pub mod entity_layer; pub mod event_loop; +pub mod game_mode; pub mod hand_swing; pub mod interact_block; pub mod interact_entity; @@ -38,18 +41,19 @@ pub mod op_level; pub mod resource_pack; pub mod spawn; pub mod status; -pub mod teleport; pub mod title; pub use chunk_view::ChunkView; +pub use client::Client; +pub use dimension_layer::{Chunk, DimensionLayerBundle, LoadedChunk}; pub use event_loop::{EventLoopPostUpdate, EventLoopPreUpdate, EventLoopUpdate}; -pub use layer::{ChunkLayer, EntityLayer, Layer, LayerBundle}; +pub use layer::{CombinedLayerBundle, OldVisibleLayers, VisibleLayers}; pub use valence_protocol::{ block, ident, item, math, text, uuid, BiomePos, BlockPos, BlockState, ChunkPos, CompressionThreshold, Difficulty, Direction, GameMode, Hand, Ident, ItemKind, ItemStack, Text, MINECRAFT_VERSION, PROTOCOL_VERSION, }; -pub use valence_server_common::*; +pub use valence_server_common::{LayerId, OldLayerId, *}; pub use { bevy_app as app, bevy_ecs as ecs, rand, valence_entity as entity, valence_nbt as nbt, valence_protocol as protocol, valence_registry as registry, diff --git a/crates/valence_server/src/movement.rs b/crates/valence_server/src/movement.rs index 5b5835799..ff941c870 100644 --- a/crates/valence_server/src/movement.rs +++ b/crates/valence_server/src/movement.rs @@ -1,21 +1,176 @@ use bevy_app::prelude::*; use bevy_ecs::prelude::*; +use tracing::warn; use valence_entity::{HeadYaw, Look, OnGround, Position}; use valence_math::DVec3; +use valence_protocol::packets::play::player_position_look_s2c::PlayerPositionLookFlags; use valence_protocol::packets::play::{ - FullC2s, LookAndOnGroundC2s, OnGroundOnlyC2s, PositionAndOnGroundC2s, VehicleMoveC2s, + FullC2s, LookAndOnGroundC2s, OnGroundOnlyC2s, PlayerPositionLookS2c, PlayerSpawnPositionS2c, + PositionAndOnGroundC2s, TeleportConfirmC2s, VehicleMoveC2s, }; +use valence_protocol::{BlockPos, WritePacket}; +use crate::client::FlushPacketsSet; use crate::event_loop::{EventLoopPreUpdate, PacketEvent}; -use crate::teleport::TeleportState; +use crate::layer::BroadcastLayerMessagesSet; +use crate::Client; +/// Handles client movement and teleports. pub struct MovementPlugin; +/// When client positions are synchronized by sending the clientbound position +/// packet. This set also includes the system that updates the client's respawn +/// position. +#[derive(SystemSet, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct SyncPositionSet; + impl Plugin for MovementPlugin { fn build(&self, app: &mut App) { - app.init_resource::() + app + .init_resource::() .add_event::() - .add_systems(EventLoopPreUpdate, handle_client_movement); + .add_systems(EventLoopPreUpdate, (handle_teleport_confirmations, handle_client_movement)) + // Sync position after chunks are loaded so the client doesn't fall through the floor. + // Setting the respawn position also closes the "downloading terrain" screen. + .configure_set(PostUpdate, SyncPositionSet.after(BroadcastLayerMessagesSet).before(FlushPacketsSet)) + .add_systems(PostUpdate, (update_respawn_position, teleport).chain().in_set(SyncPositionSet)); + } +} + +#[derive(Component, Debug)] +pub struct TeleportState { + /// Counts up as teleports are made. + teleport_id_counter: u32, + /// The number of pending client teleports that have yet to receive a + /// confirmation. Inbound client position packets should be ignored while + /// this is nonzero. + pending_teleports: u32, + synced_pos: DVec3, + synced_look: Look, +} + +impl TeleportState { + pub fn teleport_id_counter(&self) -> u32 { + self.teleport_id_counter + } + + pub fn pending_teleports(&self) -> u32 { + self.pending_teleports + } +} + +impl Default for TeleportState { + fn default() -> Self { + Self { + teleport_id_counter: 0, + pending_teleports: 0, + // Set initial synced pos and look to NaN so a teleport always happens when first + // joining. + synced_pos: DVec3::NAN, + synced_look: Look { + yaw: f32::NAN, + pitch: f32::NAN, + }, + } + } +} + +/// Syncs the client's position and look with the server. +/// +/// This should happen after chunks are loaded so the client doesn't fall though +/// the floor. +#[allow(clippy::type_complexity)] +fn teleport( + mut clients: Query< + (&mut Client, &mut TeleportState, &Position, &Look), + Or<(Changed, Changed)>, + >, +) { + for (mut client, mut state, pos, look) in &mut clients { + let changed_pos = pos.0 != state.synced_pos; + let changed_yaw = look.yaw != state.synced_look.yaw; + let changed_pitch = look.pitch != state.synced_look.pitch; + + if changed_pos || changed_yaw || changed_pitch { + state.synced_pos = pos.0; + state.synced_look = *look; + + let flags = PlayerPositionLookFlags::new() + .with_x(!changed_pos) + .with_y(!changed_pos) + .with_z(!changed_pos) + .with_y_rot(!changed_yaw) + .with_x_rot(!changed_pitch); + + client.write_packet(&PlayerPositionLookS2c { + position: if changed_pos { pos.0 } else { DVec3::ZERO }, + yaw: if changed_yaw { look.yaw } else { 0.0 }, + pitch: if changed_pitch { look.pitch } else { 0.0 }, + flags, + teleport_id: (state.teleport_id_counter as i32).into(), + }); + + state.pending_teleports = state.pending_teleports.wrapping_add(1); + state.teleport_id_counter = state.teleport_id_counter.wrapping_add(1); + } + } +} + +fn handle_teleport_confirmations( + mut packets: EventReader, + mut clients: Query<&mut TeleportState>, + mut commands: Commands, +) { + for packet in packets.iter() { + if let Some(pkt) = packet.decode::() { + if let Ok(mut state) = clients.get_mut(packet.client) { + if state.pending_teleports == 0 { + warn!( + "unexpected teleport confirmation from client {:?}", + packet.client + ); + commands.entity(packet.client).remove::(); + } + + let got = pkt.teleport_id.0 as u32; + let expected = state + .teleport_id_counter + .wrapping_sub(state.pending_teleports); + + if got == expected { + state.pending_teleports -= 1; + } else { + warn!( + "unexpected teleport ID for client {:?} (expected {expected}, got {got})", + packet.client + ); + commands.entity(packet.client).remove::(); + } + } + } + } +} + +/// The position and angle that clients will respawn with. Also +/// controls the position that compasses point towards. +#[derive(Component, Copy, Clone, PartialEq, Default, Debug)] +pub struct RespawnPosition { + /// The position that clients will respawn at. This can be changed at any + /// time to set the position that compasses point towards. + pub pos: BlockPos, + /// The yaw angle that clients will respawn with (in degrees). + pub yaw: f32, +} + +/// Sets the client's respawn and compass position. +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, + }); } } @@ -48,6 +203,32 @@ fn handle_client_movement( )>, mut movement_events: EventWriter, ) { + fn handle( + mov: MovementEvent, + mut pos: Mut, + mut look: Mut, + mut head_yaw: Mut, + mut on_ground: Mut, + mut teleport_state: Mut, + movement_events: &mut EventWriter, + ) { + if teleport_state.pending_teleports() != 0 { + return; + } + + // TODO: check that the client isn't moving too fast / flying. + // TODO: check that the client isn't clipping through blocks. + + pos.set_if_neq(Position(mov.position)); + teleport_state.synced_pos = mov.position; + look.set_if_neq(mov.look); + teleport_state.synced_look = mov.look; + head_yaw.set_if_neq(HeadYaw(mov.look.yaw)); + on_ground.set_if_neq(OnGround(mov.on_ground)); + + movement_events.send(mov); + } + for packet in packets.iter() { if let Some(pkt) = packet.decode::() { if let Ok((pos, look, head_yaw, on_ground, teleport_state)) = @@ -181,29 +362,3 @@ fn handle_client_movement( } } } - -fn handle( - mov: MovementEvent, - mut pos: Mut, - mut look: Mut, - mut head_yaw: Mut, - mut on_ground: Mut, - mut teleport_state: Mut, - movement_events: &mut EventWriter, -) { - if teleport_state.pending_teleports() != 0 { - return; - } - - // TODO: check that the client isn't moving too fast / flying. - // TODO: check that the client isn't clipping through blocks. - - pos.set_if_neq(Position(mov.position)); - teleport_state.synced_pos = mov.position; - look.set_if_neq(mov.look); - teleport_state.synced_look = mov.look; - head_yaw.set_if_neq(HeadYaw(mov.look.yaw)); - on_ground.set_if_neq(OnGround(mov.on_ground)); - - movement_events.send(mov); -} diff --git a/crates/valence_server/src/op_level.rs b/crates/valence_server/src/op_level.rs index 55da0b13c..29442ff38 100644 --- a/crates/valence_server/src/op_level.rs +++ b/crates/valence_server/src/op_level.rs @@ -4,13 +4,17 @@ use derive_more::Deref; use valence_protocol::packets::play::EntityStatusS2c; use valence_protocol::WritePacket; -use crate::client::{Client, UpdateClientsSet}; +use crate::client::{Client, FlushPacketsSet}; pub struct OpLevelPlugin; +#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct UpdateOpLevelSet; + impl Plugin for OpLevelPlugin { fn build(&self, app: &mut App) { - app.add_systems(PostUpdate, update_op_level.in_set(UpdateClientsSet)); + app.configure_set(PostUpdate, UpdateOpLevelSet.before(FlushPacketsSet)) + .add_systems(PostUpdate, update_op_level.in_set(UpdateOpLevelSet)); } } diff --git a/crates/valence_server/src/spawn.rs b/crates/valence_server/src/spawn.rs index 627c2dc5c..00222c960 100644 --- a/crates/valence_server/src/spawn.rs +++ b/crates/valence_server/src/spawn.rs @@ -3,23 +3,78 @@ use std::borrow::Cow; use std::collections::BTreeSet; +use bevy_app::prelude::*; use bevy_ecs::prelude::*; use bevy_ecs::query::WorldQuery; use derive_more::{Deref, DerefMut}; -use valence_entity::EntityLayerId; -use valence_protocol::packets::play::{GameJoinS2c, PlayerRespawnS2c, PlayerSpawnPositionS2c}; +use valence_protocol::packets::play::{GameJoinS2c, PlayerRespawnS2c}; use valence_protocol::{BlockPos, GameMode, GlobalPos, Ident, VarInt, WritePacket}; use valence_registry::tags::TagsRegistry; -use valence_registry::{BiomeRegistry, RegistryCodec}; +use valence_registry::{DimensionTypeRegistry, RegistryCodec, UpdateRegistrySet}; + +use crate::client::{Client, FlushPacketsSet, ViewDistance}; +use crate::dimension_layer::{DimensionInfo, UpdateDimensionLayerSet}; +use crate::layer::VisibleLayers; + +/// Handles spawning and respawning of clients. +pub struct SpawnPlugin; + +/// When clients are sent the "respawn" packet after their dimension layer has +/// changed. +#[derive(SystemSet, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct RespawnSet; + +/// When the initial join packets are written to clients. +#[derive(SystemSet, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct JoinGameSet; + +impl Plugin for SpawnPlugin { + fn build(&self, app: &mut App) { + app + // Send the respawn packet before chunks are sent. + .configure_set(PostUpdate, RespawnSet.before(UpdateDimensionLayerSet)) + .add_systems(PostUpdate, respawn.in_set(RespawnSet)) + // The join game packet is prepended to the client's packet buffer, so + // it can be sent any time before packets are flushed. Additionally, + // this must be scheduled after registries are updated because we read + // the cached packets. + .configure_set(PostUpdate, JoinGameSet.after(UpdateRegistrySet).before(FlushPacketsSet)) + .add_systems(PostUpdate, initial_join.in_set(JoinGameSet)); + } +} -use crate::client::{Client, ViewDistance, VisibleChunkLayer}; -use crate::layer::ChunkLayer; +/// 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, +} // Components for the join game and respawn packet. -#[derive(Component, Clone, PartialEq, Eq, Default, Debug)] +#[derive(Component, Clone, PartialEq, Eq, Default, Debug, Deref, DerefMut)] pub struct DeathLocation(pub Option<(Ident, BlockPos)>); +impl DeathLocation { + pub fn as_global_pos(&self) -> Option { + self.0.as_ref().map(|(name, pos)| GlobalPos { + dimension_name: name.as_str_ident().into(), + position: *pos, + }) + } +} + #[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug, Deref, DerefMut)] pub struct IsHardcore(pub bool); @@ -33,6 +88,12 @@ pub struct ReducedDebugInfo(pub bool); #[derive(Component, Copy, Clone, PartialEq, Eq, Debug, Deref, DerefMut)] pub struct HasRespawnScreen(pub bool); +impl Default for HasRespawnScreen { + fn default() -> Self { + Self(true) + } +} + /// If the client is spawning into a debug world. #[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug, Deref, DerefMut)] pub struct IsDebug(pub bool); @@ -48,64 +109,28 @@ pub struct PortalCooldown(pub i32); #[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug, Deref, DerefMut)] pub struct PrevGameMode(pub Option); -impl Default for HasRespawnScreen { - fn default() -> Self { - Self(true) - } -} - -/// The position and angle that clients will respawn with. Also -/// controls the position that compasses point towards. -#[derive(Component, Copy, Clone, PartialEq, Default, Debug)] -pub struct RespawnPosition { - /// The position that clients will respawn at. This can be changed at any - /// time to set the position that compasses point towards. - pub pos: BlockPos, - /// The yaw angle that clients will respawn with (in degrees). - pub yaw: f32, -} - -/// 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, -} - pub(super) fn initial_join( + mut clients: Query<(&mut Client, &VisibleLayers, ClientSpawnQueryReadOnly), Added>, codec: Res, tags: Res, - mut clients: Query<(&mut Client, &VisibleChunkLayer, ClientSpawnQueryReadOnly), Added>, - chunk_layers: Query<&ChunkLayer>, + dimensions: Res, + dimension_layers: Query<&DimensionInfo>, ) { - for (mut client, visible_chunk_layer, spawn) in &mut clients { - let Ok(chunk_layer) = chunk_layers.get(visible_chunk_layer.0) else { + for (mut client, vis_layers, spawn) in &mut clients { + let Some(info) = vis_layers + .iter() + .find_map(|&layer| dimension_layers.get(layer).ok()) + else { continue; }; let dimension_names: BTreeSet>> = codec - .registry(BiomeRegistry::KEY) + .registry(DimensionTypeRegistry::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, - }); + let dimension_name = dimensions.by_index(info.dimension_type()).0; // The login packet is prepended so that it's sent before all the other packets. // Some packets don't work correctly when sent before the game join packet. @@ -116,17 +141,17 @@ pub(super) fn initial_join( previous_game_mode: spawn.prev_game_mode.0.into(), dimension_names: Cow::Owned(dimension_names), registry_codec: Cow::Borrowed(codec.cached_codec()), - dimension_type_name: dimension_name.clone(), - dimension_name, + dimension_type_name: dimension_name.into(), + dimension_name: dimension_name.into(), hashed_seed: spawn.hashed_seed.0 as i64, max_players: VarInt(0), // Ignored by clients. view_distance: VarInt(spawn.view_distance.get() as i32), - simulation_distance: VarInt(16), // TODO. + simulation_distance: VarInt(16), // Ignored? 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, + last_death_location: spawn.death_loc.as_global_pos(), portal_cooldown: VarInt(spawn.portal_cooldown.0), }); @@ -141,11 +166,11 @@ pub(super) fn initial_join( } } -pub(super) fn respawn( +fn respawn( mut clients: Query< ( &mut Client, - &EntityLayerId, + &VisibleLayers, &DeathLocation, &HashedSeed, &GameMode, @@ -153,23 +178,35 @@ pub(super) fn respawn( &IsDebug, &IsFlat, ), - Changed, + Changed, >, - chunk_layers: Query<&ChunkLayer>, + dimension_layers: Query<&DimensionInfo>, + dimensions: Res, ) { - for (mut client, loc, death_loc, hashed_seed, game_mode, prev_game_mode, is_debug, is_flat) in - &mut clients + for ( + mut client, + vis_layers, + 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 { + let Some(info) = vis_layers + .iter() + .find_map(|&layer| dimension_layers.get(layer).ok()) + else { continue; }; - let dimension_name = chunk_layer.dimension_type_name(); + let dimension_name = dimensions.by_index(info.dimension_type()).0; let last_death_location = death_loc.0.as_ref().map(|(id, pos)| GlobalPos { dimension_name: id.as_str_ident().into(), @@ -190,18 +227,3 @@ pub(super) fn respawn( }); } } - -/// 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_server/src/teleport.rs b/crates/valence_server/src/teleport.rs deleted file mode 100644 index 28b5d837b..000000000 --- a/crates/valence_server/src/teleport.rs +++ /dev/null @@ -1,139 +0,0 @@ -use bevy_app::prelude::*; -use bevy_ecs::prelude::*; -use tracing::warn; -use valence_entity::{Look, Position}; -use valence_math::DVec3; -use valence_protocol::packets::play::player_position_look_s2c::PlayerPositionLookFlags; -use valence_protocol::packets::play::{PlayerPositionLookS2c, TeleportConfirmC2s}; -use valence_protocol::WritePacket; - -use crate::client::{update_view_and_layers, Client, UpdateClientsSet}; -use crate::event_loop::{EventLoopPreUpdate, PacketEvent}; -use crate::spawn::update_respawn_position; - -pub struct TeleportPlugin; - -impl Plugin for TeleportPlugin { - fn build(&self, app: &mut App) { - app.add_systems( - PostUpdate, - teleport - .after(update_view_and_layers) - .before(update_respawn_position) - .in_set(UpdateClientsSet), - ) - .add_systems(EventLoopPreUpdate, handle_teleport_confirmations); - } -} - -#[derive(Component, Debug)] -pub struct TeleportState { - /// Counts up as teleports are made. - teleport_id_counter: u32, - /// The number of pending client teleports that have yet to receive a - /// confirmation. Inbound client position packets should be ignored while - /// this is nonzero. - pending_teleports: u32, - pub(super) synced_pos: DVec3, - pub(super) synced_look: Look, -} - -impl TeleportState { - pub(super) fn new() -> Self { - Self { - teleport_id_counter: 0, - pending_teleports: 0, - // Set initial synced pos and look to NaN so a teleport always happens when first - // joining. - synced_pos: DVec3::NAN, - synced_look: Look { - yaw: f32::NAN, - pitch: f32::NAN, - }, - } - } - - pub fn teleport_id_counter(&self) -> u32 { - self.teleport_id_counter - } - - pub fn pending_teleports(&self) -> u32 { - self.pending_teleports - } -} - -/// Syncs the client's position and look with the server. -/// -/// This should happen after chunks are loaded so the client doesn't fall though -/// the floor. -#[allow(clippy::type_complexity)] -fn teleport( - mut clients: Query< - (&mut Client, &mut TeleportState, &Position, &Look), - Or<(Changed, Changed)>, - >, -) { - for (mut client, mut state, pos, look) in &mut clients { - let changed_pos = pos.0 != state.synced_pos; - let changed_yaw = look.yaw != state.synced_look.yaw; - let changed_pitch = look.pitch != state.synced_look.pitch; - - if changed_pos || changed_yaw || changed_pitch { - state.synced_pos = pos.0; - state.synced_look = *look; - - let flags = PlayerPositionLookFlags::new() - .with_x(!changed_pos) - .with_y(!changed_pos) - .with_z(!changed_pos) - .with_y_rot(!changed_yaw) - .with_x_rot(!changed_pitch); - - client.write_packet(&PlayerPositionLookS2c { - position: if changed_pos { pos.0 } else { DVec3::ZERO }, - yaw: if changed_yaw { look.yaw } else { 0.0 }, - pitch: if changed_pitch { look.pitch } else { 0.0 }, - flags, - teleport_id: (state.teleport_id_counter as i32).into(), - }); - - state.pending_teleports = state.pending_teleports.wrapping_add(1); - state.teleport_id_counter = state.teleport_id_counter.wrapping_add(1); - } - } -} - -fn handle_teleport_confirmations( - mut packets: EventReader, - mut clients: Query<&mut TeleportState>, - mut commands: Commands, -) { - for packet in packets.iter() { - if let Some(pkt) = packet.decode::() { - if let Ok(mut state) = clients.get_mut(packet.client) { - if state.pending_teleports == 0 { - warn!( - "unexpected teleport confirmation from client {:?}", - packet.client - ); - commands.entity(packet.client).remove::(); - } - - let got = pkt.teleport_id.0 as u32; - let expected = state - .teleport_id_counter - .wrapping_sub(state.pending_teleports); - - if got == expected { - state.pending_teleports -= 1; - } else { - warn!( - "unexpected teleport ID for client {:?} (expected {expected}, got {got}", - packet.client - ); - commands.entity(packet.client).remove::(); - } - } - } - } -} diff --git a/crates/valence_server_common/src/entity_event.rs b/crates/valence_server_common/src/entity_event.rs new file mode 100644 index 000000000..e53846d53 --- /dev/null +++ b/crates/valence_server_common/src/entity_event.rs @@ -0,0 +1,20 @@ +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; +use derive_more::{Deref, DerefMut}; + +pub trait AddEntityEvent { + fn add_entity_event(&mut self); +} + +impl AddEntityEvent for App { + fn add_entity_event(&mut self) { + self.add_systems(Last, clear_entity_events::); + } +} + +fn clear_entity_events(mut events: Query<&mut EntityEvents>) { + events.iter_mut().for_each(|mut e| e.clear()); +} + +#[derive(Component, Clone, DerefMut, Deref)] +pub struct EntityEvents(pub Vec); diff --git a/crates/valence_server_common/src/layer_id.rs b/crates/valence_server_common/src/layer_id.rs new file mode 100644 index 000000000..ae60ec1e5 --- /dev/null +++ b/crates/valence_server_common/src/layer_id.rs @@ -0,0 +1,48 @@ +use bevy_ecs::prelude::*; + +/// The pointer to the layer this entity is a member of. +#[derive(Component, Copy, Clone, PartialEq, Eq, Debug)] +pub struct LayerId(pub Entity); + +impl Default for LayerId { + fn default() -> Self { + Self(Entity::PLACEHOLDER) + } +} + +impl PartialEq for LayerId { + fn eq(&self, other: &OldLayerId) -> bool { + self.0 == other.0 + } +} + +/// Value of [`LayerId`] from the previous tick. Not intended to be modified +/// manually. +#[derive(Component, Copy, Clone, PartialEq, Eq, Debug)] +pub struct OldLayerId(Entity); + +impl OldLayerId { + pub fn get(&self) -> Entity { + self.0 + } +} + +impl Default for OldLayerId { + fn default() -> Self { + Self(Entity::PLACEHOLDER) + } +} + +impl PartialEq for OldLayerId { + fn eq(&self, other: &LayerId) -> bool { + self.0 == other.0 + } +} + +pub(super) fn update_old_layer_id( + mut entities: Query<(&LayerId, &mut OldLayerId), Changed>, +) { + for (new, mut old) in &mut entities { + old.0 = new.0 + } +} diff --git a/crates/valence_server_common/src/lib.rs b/crates/valence_server_common/src/lib.rs index 06e105978..1339e0954 100644 --- a/crates/valence_server_common/src/lib.rs +++ b/crates/valence_server_common/src/lib.rs @@ -19,6 +19,8 @@ #![allow(clippy::unusual_byte_groupings)] mod despawn; +pub mod entity_event; +mod layer_id; mod uuid; use std::num::NonZeroU32; @@ -31,7 +33,9 @@ pub use despawn::*; use valence_protocol::CompressionThreshold; use crate::despawn::despawn_marked_entities; -pub use crate::uuid::*; +use crate::layer_id::update_old_layer_id; +pub use crate::layer_id::{LayerId, OldLayerId}; +pub use crate::uuid::UniqueId; /// Minecraft's standard ticks per second (TPS). pub const DEFAULT_TPS: NonZeroU32 = match NonZeroU32::new(20) { @@ -103,7 +107,14 @@ impl Plugin for ServerPlugin { server.current_tick += 1; } - app.add_systems(Last, (increment_tick_counter, despawn_marked_entities)); + app.add_systems( + Last, + ( + increment_tick_counter, + despawn_marked_entities, + update_old_layer_id, + ), + ); } } diff --git a/crates/valence_weather/src/lib.rs b/crates/valence_weather/src/lib.rs index 49fbe6b45..64c47bbdb 100644 --- a/crates/valence_weather/src/lib.rs +++ b/crates/valence_weather/src/lib.rs @@ -21,35 +21,39 @@ use bevy_app::prelude::*; use bevy_ecs::prelude::*; use derive_more::{Deref, DerefMut}; -use valence_server::client::{Client, FlushPacketsSet, UpdateClientsSet, VisibleChunkLayer}; +use valence_server::client::Client; +use valence_server::layer::message::LayerMessages; +use valence_server::layer::{BroadcastLayerMessagesSet, OldVisibleLayers, VisibleLayers}; use valence_server::protocol::packets::play::game_state_change_s2c::GameEventKind; use valence_server::protocol::packets::play::GameStateChangeS2c; use valence_server::protocol::WritePacket; -use valence_server::ChunkLayer; pub struct WeatherPlugin; +#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct UpdateWeatherSet; + impl Plugin for WeatherPlugin { fn build(&self, app: &mut App) { - app.add_systems( + app.configure_set( PostUpdate, - ( - init_weather_on_layer_join, - change_client_rain_level, - change_client_thunder_level, - ) - .before(FlushPacketsSet), + UpdateWeatherSet.before(BroadcastLayerMessagesSet), ) .add_systems( PostUpdate, - (change_layer_rain_level, change_layer_thunder_level).before(UpdateClientsSet), + ( + init_weather_on_layer_join, + update_rain_level, + update_thunder_level, + ) + .in_set(UpdateWeatherSet), ); } } -/// Bundle containing rain and thunder components. `valence_weather` allows this -/// to be added to clients and chunk layer entities. -#[derive(Bundle, Default, PartialEq, PartialOrd)] +/// Bundle containing rain and thunder components. This can be added to any +/// layer. +#[derive(Bundle, Default, Debug)] pub struct WeatherBundle { pub rain: Rain, pub thunder: Thunder, @@ -57,44 +61,41 @@ pub struct WeatherBundle { /// 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, Deref, DerefMut)] +#[derive(Component, Default, PartialEq, PartialOrd, Deref, DerefMut, Debug)] 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, Deref, DerefMut)] +#[derive(Component, Default, PartialEq, PartialOrd, Deref, DerefMut, Debug)] 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>, + mut clients: Query<(&mut Client, &VisibleLayers, &OldVisibleLayers), Changed>, + layers: Query<(Option<&Rain>, Option<&Thunder>)>, ) { - for (mut client, visible_chunk_layer) in &mut clients { - if let Ok((rain, thunder)) = layers.get(visible_chunk_layer.0) { + for (mut client, vis_layers, old_vis_layers) in &mut clients { + if let Some((rain, thunder)) = vis_layers + .difference(old_vis_layers) + .find_map(|&layer| layers.get(layer).ok()) + { if let Some(rain) = rain { - if rain.0 != 0.0 { - client.write_packet(&GameStateChangeS2c { - kind: GameEventKind::RainLevelChange, - value: rain.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, - }); - } + client.write_packet(&GameStateChangeS2c { + kind: GameEventKind::ThunderLevelChange, + value: thunder.0, + }); } } } } -fn change_layer_rain_level( - mut layers: Query<(&mut ChunkLayer, &Rain), (Changed, Without)>, -) { +fn update_rain_level(mut layers: Query<(&mut LayerMessages, &Rain), Changed>) { for (mut layer, rain) in &mut layers { layer.write_packet(&GameStateChangeS2c { kind: GameEventKind::RainLevelChange, @@ -103,9 +104,7 @@ fn change_layer_rain_level( } } -fn change_layer_thunder_level( - mut layers: Query<(&mut ChunkLayer, &Thunder), (Changed, Without)>, -) { +fn update_thunder_level(mut layers: Query<(&mut LayerMessages, &Thunder), Changed>) { for (mut layer, thunder) in &mut layers { layer.write_packet(&GameStateChangeS2c { kind: GameEventKind::ThunderLevelChange, @@ -113,21 +112,3 @@ fn change_layer_thunder_level( }); } } - -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_world_border/src/lib.rs b/crates/valence_world_border/src/lib.rs index c5553627c..ab56f92e6 100644 --- a/crates/valence_world_border/src/lib.rs +++ b/crates/valence_world_border/src/lib.rs @@ -21,14 +21,16 @@ use bevy_app::prelude::*; use bevy_ecs::prelude::*; use derive_more::{Deref, DerefMut}; -use valence_server::client::{Client, UpdateClientsSet, VisibleChunkLayer}; +use valence_server::client::Client; +use valence_server::layer::message::LayerMessages; +use valence_server::layer::{BroadcastLayerMessagesSet, OldVisibleLayers, VisibleLayers}; use valence_server::protocol::packets::play::{ WorldBorderCenterChangedS2c, WorldBorderInitializeS2c, WorldBorderInterpolateSizeS2c, WorldBorderSizeChangedS2c, WorldBorderWarningBlocksChangedS2c, WorldBorderWarningTimeChangedS2c, }; use valence_server::protocol::WritePacket; -use valence_server::{ChunkLayer, Server}; +use valence_server::Server; // https://minecraft.fandom.com/wiki/World_border pub const DEFAULT_PORTAL_LIMIT: i32 = 29999984; @@ -43,24 +45,27 @@ pub struct UpdateWorldBorderSet; impl Plugin for WorldBorderPlugin { fn build(&self, app: &mut App) { - 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), - ); + app.configure_set( + PostUpdate, + UpdateWorldBorderSet.before(BroadcastLayerMessagesSet), + ) + .add_systems( + PostUpdate, + ( + init_world_border_on_layer_join, + tick_world_border_lerp, + update_world_border_center, + update_world_border_warning_blocks, + update_world_border_warning_time, + update_world_border_portal_tp_boundary, + ) + .in_set(UpdateWorldBorderSet), + ); } } /// A bundle containing necessary components to enable world border -/// functionality. Add this to an entity with the [`ChunkLayer`] component. +/// functionality. This can be added to "layer" entities. #[derive(Bundle, Default, Debug)] pub struct WorldBorderBundle { pub center: WorldBorderCenter, @@ -97,6 +102,7 @@ pub struct WorldBorderLerp { /// automatically. pub remaining_ticks: u64, } + #[derive(Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deref, DerefMut)] pub struct WorldBorderWarnTime(pub i32); @@ -134,9 +140,9 @@ impl Default for WorldBorderLerp { } } -fn init_world_border_for_new_clients( - mut clients: Query<(&mut Client, &VisibleChunkLayer), Changed>, - wbs: Query<( +fn init_world_border_on_layer_join( + mut clients: Query<(&mut Client, &VisibleLayers, &OldVisibleLayers), Changed>, + layers: Query<( &WorldBorderCenter, &WorldBorderLerp, &WorldBorderPortalTpBoundary, @@ -145,8 +151,11 @@ fn init_world_border_for_new_clients( )>, server: Res, ) { - for (mut client, layer) in &mut clients { - if let Ok((center, lerp, portal_tp_boundary, warn_time, warn_blocks)) = wbs.get(layer.0) { + for (mut client, vis_layers, old_vis_layers) in &mut clients { + if let Some((center, lerp, portal_tp_boundary, warn_time, warn_blocks)) = vis_layers + .difference(old_vis_layers) + .find_map(|&layer| layers.get(layer).ok()) + { let millis = lerp.remaining_ticks as i64 * 1000 / server.tick_rate().get() as i64; client.write_packet(&WorldBorderInitializeS2c { @@ -164,13 +173,13 @@ fn init_world_border_for_new_clients( } fn tick_world_border_lerp( - mut wbs: Query<(&mut ChunkLayer, &mut WorldBorderLerp)>, + mut layers: Query<(&mut LayerMessages, &mut WorldBorderLerp)>, server: Res, ) { - for (mut layer, mut lerp) in &mut wbs { + for (mut msgs, mut lerp) in &mut layers { if lerp.is_changed() { if lerp.remaining_ticks == 0 { - layer.write_packet(&WorldBorderSizeChangedS2c { + msgs.write_packet(&WorldBorderSizeChangedS2c { diameter: lerp.target_diameter, }); @@ -178,7 +187,7 @@ fn tick_world_border_lerp( } else { let millis = lerp.remaining_ticks as i64 * 1000 / server.tick_rate().get() as i64; - layer.write_packet(&WorldBorderInterpolateSizeS2c { + msgs.write_packet(&WorldBorderInterpolateSizeS2c { old_diameter: lerp.current_diameter, new_diameter: lerp.target_diameter, duration_millis: millis.into(), @@ -195,41 +204,41 @@ fn tick_world_border_lerp( } } -fn change_world_border_center( - mut wbs: Query<(&mut ChunkLayer, &WorldBorderCenter), Changed>, +fn update_world_border_center( + mut layers: Query<(&mut LayerMessages, &WorldBorderCenter), Changed>, ) { - for (mut layer, center) in &mut wbs { - layer.write_packet(&WorldBorderCenterChangedS2c { + for (mut msgs, center) in &mut layers { + msgs.write_packet(&WorldBorderCenterChangedS2c { x_pos: center.x, z_pos: center.z, }); } } -fn change_world_border_warning_blocks( - mut wbs: Query<(&mut ChunkLayer, &WorldBorderWarnBlocks), Changed>, +fn update_world_border_warning_blocks( + mut layers: Query<(&mut LayerMessages, &WorldBorderWarnBlocks), Changed>, ) { - for (mut layer, warn_blocks) in &mut wbs { - layer.write_packet(&WorldBorderWarningBlocksChangedS2c { + for (mut msgs, warn_blocks) in &mut layers { + msgs.write_packet(&WorldBorderWarningBlocksChangedS2c { warning_blocks: warn_blocks.0.into(), }); } } -fn change_world_border_warning_time( - mut wbs: Query<(&mut ChunkLayer, &WorldBorderWarnTime), Changed>, +fn update_world_border_warning_time( + mut layers: Query<(&mut LayerMessages, &WorldBorderWarnTime), Changed>, ) { - for (mut layer, warn_time) in &mut wbs { - layer.write_packet(&WorldBorderWarningTimeChangedS2c { + for (mut msgs, warn_time) in &mut layers { + msgs.write_packet(&WorldBorderWarningTimeChangedS2c { warning_time: warn_time.0.into(), }); } } -fn change_world_border_portal_tp_boundary( - mut wbs: Query< +fn update_world_border_portal_tp_boundary( + mut layers: Query< ( - &mut ChunkLayer, + &mut LayerMessages, &WorldBorderCenter, &WorldBorderLerp, &WorldBorderPortalTpBoundary, @@ -240,10 +249,10 @@ fn change_world_border_portal_tp_boundary( >, server: Res, ) { - for (mut layer, center, lerp, portal_tp_boundary, warn_time, warn_blocks) in &mut wbs { + for (mut msgs, center, lerp, portal_tp_boundary, warn_time, warn_blocks) in &mut layers { let millis = lerp.remaining_ticks as i64 * 1000 / server.tick_rate().get() as i64; - layer.write_packet(&WorldBorderInitializeS2c { + msgs.write_packet(&WorldBorderInitializeS2c { x: center.x, z: center.z, old_diameter: lerp.current_diameter, diff --git a/examples/advancement.rs b/examples/advancement.rs index 57a224b6e..b56e641b8 100644 --- a/examples/advancement.rs +++ b/examples/advancement.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use valence::advancement::bevy_hierarchy::{BuildChildren, Children, Parent}; use valence::advancement::ForceTabUpdate; use valence::prelude::*; +use valence_server::dimension_layer::DimensionInfo; #[derive(Component)] struct RootCriteria; @@ -47,17 +48,19 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = CombinedLayerBundle::new(Default::default(), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk_index.insert([x, z], Chunk::new()); } } for z in -25..25 { for x in -25..25 { - layer.chunk.set_block([x, 64, z], BlockState::GRASS_BLOCK); + layer + .chunk_index + .set_block([x, 64, z], BlockState::GRASS_BLOCK); } } @@ -167,29 +170,20 @@ fn setup( fn init_clients( mut clients: Query< ( - &mut EntityLayerId, - &mut VisibleChunkLayer, - &mut VisibleEntityLayers, + &mut LayerId, + &mut VisibleLayers, &mut Position, &mut GameMode, ), Added, >, - layers: Query>, + layers: Query>, ) { - for ( - mut layer_id, - mut visible_chunk_layer, - mut visible_entity_layers, - mut pos, - mut game_mode, - ) in &mut clients - { + for (mut layer_id, mut visible_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); + visible_layers.0.insert(layer); pos.set([0.5, 65.0, 0.5]); *game_mode = GameMode::Creative; } diff --git a/examples/anvil_loading.rs b/examples/anvil_loading.rs index 664ba21dd..07955412a 100644 --- a/examples/anvil_loading.rs +++ b/examples/anvil_loading.rs @@ -7,6 +7,7 @@ use valence::abilities::{FlyingSpeed, FovModifier, PlayerAbilitiesFlags}; use valence::message::SendMessage; use valence::prelude::*; use valence_anvil::{AnvilLevel, ChunkLoadEvent, ChunkLoadStatus}; +use valence_server::dimension_layer::DimensionInfo; const SPAWN_POS: DVec3 = DVec3::new(0.0, 256.0, 0.0); @@ -40,7 +41,6 @@ pub fn main() { .add_systems( Update, ( - despawn_disconnected_clients, (init_clients, handle_chunk_loads).chain(), display_loaded_chunk_count, ), @@ -55,7 +55,7 @@ fn setup( server: Res, cli: Res, ) { - let layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + let layer = CombinedLayerBundle::new(Default::default(), &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. @@ -76,9 +76,8 @@ fn setup( fn init_clients( mut clients: Query< ( - &mut EntityLayerId, - &mut VisibleChunkLayer, - &mut VisibleEntityLayers, + &mut LayerId, + &mut VisibleLayers, &mut Position, &mut GameMode, &mut PlayerAbilitiesFlags, @@ -87,12 +86,11 @@ fn init_clients( ), Added, >, - layers: Query>, + layers: Query>, ) { for ( mut layer_id, - mut visible_chunk_layer, - mut visible_entity_layers, + mut visible_layers, mut pos, mut game_mode, mut abilities, @@ -103,8 +101,7 @@ fn init_clients( let layer = layers.single(); layer_id.0 = layer; - visible_chunk_layer.0 = layer; - visible_entity_layers.0.insert(layer); + visible_layers.0.insert(layer); pos.set(SPAWN_POS); *game_mode = GameMode::Adventure; abilities.set_allow_flying(true); @@ -127,7 +124,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. - layer.insert_chunk(event.pos, UnloadedChunk::new()); + layer.insert_chunk(event.pos, Chunk::new()); } ChunkLoadStatus::Failed(e) => { // Something went wrong. @@ -139,7 +136,7 @@ fn handle_chunk_loads( eprintln!("{errmsg}"); layer.send_chat_message(errmsg.color(Color::RED)); - layer.insert_chunk(event.pos, UnloadedChunk::new()); + layer.insert_chunk(event.pos, Chunk::new()); } } } diff --git a/examples/bench_players.rs b/examples/bench_players.rs index d5810c810..132ddab98 100644 --- a/examples/bench_players.rs +++ b/examples/bench_players.rs @@ -2,9 +2,9 @@ use std::time::Instant; -use valence::client::{VisibleChunkLayer, VisibleEntityLayers}; use valence::prelude::*; use valence::ServerSettings; +use valence_server::dimension_layer::DimensionInfo; const SPAWN_Y: i32 = 64; @@ -26,7 +26,7 @@ fn main() { .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems(First, record_tick_start_time) - .add_systems(Update, (init_clients, despawn_disconnected_clients)) + .add_systems(Update, init_clients) .add_systems(Last, print_tick_time) .run(); } @@ -51,18 +51,18 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = CombinedLayerBundle::new(Default::default(), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk_index.insert([x, z], Chunk::new()); } } for z in -50..50 { for x in -50..50 { layer - .chunk + .chunk_index .set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); } } @@ -73,29 +73,20 @@ fn setup( fn init_clients( mut clients: Query< ( - &mut EntityLayerId, - &mut VisibleChunkLayer, - &mut VisibleEntityLayers, + &mut LayerId, + &mut VisibleLayers, &mut Position, &mut GameMode, ), Added, >, - layers: Query, With)>, + layers: Query>, ) { - for ( - mut layer_id, - mut visible_chunk_layer, - mut visible_entity_layers, - mut pos, - mut game_mode, - ) in &mut clients - { + for (mut layer_id, mut visible_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); + visible_layers.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 1e9c44392..2bb4b1de5 100644 --- a/examples/biomes.rs +++ b/examples/biomes.rs @@ -4,6 +4,7 @@ use rand::seq::IteratorRandom; use rand::Rng; use valence::prelude::*; use valence::registry::biome::BiomeEffects; +use valence_server::dimension_layer::DimensionInfo; use valence_server::BiomePos; const SPAWN_Y: i32 = 0; @@ -13,10 +14,7 @@ pub fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) - .add_systems( - Update, - (init_clients, despawn_disconnected_clients, set_biomes), - ) + .add_systems(Update, (init_clients, set_biomes)) .run(); } @@ -49,11 +47,11 @@ fn setup( biomes.insert(name, biome); } - let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = CombinedLayerBundle::new(Default::default(), &dimensions, &biomes, &server); for z in -SIZE..SIZE { for x in -SIZE..SIZE { - layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk_index.insert([x, z], Chunk::new()); } } @@ -90,29 +88,20 @@ fn set_biomes(mut layers: Query<&mut ChunkLayer>, biomes: Res) { fn init_clients( mut clients: Query< ( - &mut EntityLayerId, - &mut VisibleChunkLayer, - &mut VisibleEntityLayers, + &mut LayerId, + &mut VisibleLayers, &mut Position, &mut GameMode, ), Added, >, - layers: Query, With)>, + layers: Query>, ) { - for ( - mut layer_id, - mut visible_chunk_layer, - mut visible_entity_layers, - mut pos, - mut game_mode, - ) in &mut clients - { + for (mut layer_id, mut visible_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); + visible_layers.0.insert(layer); pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); *game_mode = GameMode::Creative; } diff --git a/examples/block_entities.rs b/examples/block_entities.rs index 937db43ca..67876bccd 100644 --- a/examples/block_entities.rs +++ b/examples/block_entities.rs @@ -4,6 +4,7 @@ use valence::interact_block::InteractBlockEvent; use valence::message::ChatMessageEvent; use valence::nbt::{compound, List}; use valence::prelude::*; +use valence_server::dimension_layer::DimensionInfo; const FLOOR_Y: i32 = 64; const SIGN_POS: [i32; 3] = [3, FLOOR_Y + 1, 2]; @@ -13,10 +14,7 @@ pub fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) - .add_systems( - Update, - (event_handler, init_clients, despawn_disconnected_clients), - ) + .add_systems(Update, (event_handler, init_clients)) .run(); } @@ -26,11 +24,11 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = CombinedLayerBundle::new(Default::default(), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk_index.insert([x, z], Chunk::new()); } } @@ -42,12 +40,12 @@ fn setup( } } - layer.chunk.set_block( + layer.chunk_index.set_block( [3, FLOOR_Y + 1, 1], BlockState::CHEST.set(PropName::Facing, PropValue::West), ); - layer.chunk.set_block( + layer.chunk_index.set_block( SIGN_POS, Block { state: BlockState::OAK_SIGN.set(PropName::Rotation, PropValue::_4), @@ -65,7 +63,7 @@ fn setup( }, ); - layer.chunk.set_block( + layer.chunk_index.set_block( SKULL_POS, BlockState::PLAYER_HEAD.set(PropName::Rotation, PropValue::_12), ); @@ -76,29 +74,20 @@ fn setup( fn init_clients( mut clients: Query< ( - &mut EntityLayerId, - &mut VisibleChunkLayer, - &mut VisibleEntityLayers, + &mut LayerId, + &mut VisibleLayers, &mut Position, &mut GameMode, ), Added, >, - layers: Query, With)>, + layers: Query>, ) { - for ( - mut layer_id, - mut visible_chunk_layer, - mut visible_entity_layers, - mut pos, - mut game_mode, - ) in &mut clients - { + for (mut layer_id, mut visible_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); + visible_layers.0.insert(layer); pos.set([0.0, FLOOR_Y as f64 + 1.0, 0.0]); *game_mode = GameMode::Creative; } diff --git a/examples/boss_bar.rs b/examples/boss_bar.rs index a8aba6968..6e9527f20 100644 --- a/examples/boss_bar.rs +++ b/examples/boss_bar.rs @@ -6,6 +6,7 @@ use valence_boss_bar::{ BossBarBundle, BossBarColor, BossBarDivision, BossBarFlags, BossBarHealth, BossBarStyle, BossBarTitle, }; +use valence_server::dimension_layer::DimensionInfo; use valence_server::entity::cow::CowEntityBundle; use valence_server::message::ChatMessageEvent; use valence_text::color::NamedColor; @@ -19,10 +20,7 @@ pub fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) - .add_systems( - Update, - (init_clients, despawn_disconnected_clients, listen_messages), - ) + .add_systems(Update, (init_clients, listen_messages)) .run(); } @@ -32,11 +30,11 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = CombinedLayerBundle::new(Default::default(), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk_index.insert([x, z], Chunk::new()); } } @@ -54,7 +52,7 @@ fn setup( BossBarBundle { title: BossBarTitle("Boss Bar".into_text()), health: BossBarHealth(0.5), - layer: EntityLayerId(layer_id), + layer: LayerId(layer_id), ..Default::default() }, CustomBossBar, @@ -63,7 +61,7 @@ fn setup( commands.spawn(( CowEntityBundle { position: Position::new([0.0, SPAWN_Y as f64 + 1.0, 0.0]), - layer: EntityLayerId(layer_id), + layer: LayerId(layer_id), ..Default::default() }, BossBarTitle("Louis XVI".color(NamedColor::Red)), @@ -80,30 +78,21 @@ fn init_clients( mut clients_query: Query< ( &mut Client, - &mut EntityLayerId, - &mut VisibleChunkLayer, - &mut VisibleEntityLayers, + &mut LayerId, + &mut VisibleLayers, &mut Position, &mut GameMode, ), Added, >, - layers_query: Query, With)>, + layers_query: Query>, ) { let layer = layers_query.single(); - for ( - mut client, - mut layer_id, - mut visible_chunk_layer, - mut visible_entity_layers, - mut pos, - mut game_mode, - ) in &mut clients_query + for (mut client, mut layer_id, mut visible_layers, mut pos, mut game_mode) in &mut clients_query { layer_id.0 = layer; - visible_chunk_layer.0 = layer; - visible_entity_layers.0.insert(layer); + visible_layers.0.insert(layer); pos.set([0.5, SPAWN_Y as f64 + 1.0, 0.5]); *game_mode = GameMode::Creative; @@ -144,7 +133,7 @@ fn listen_messages( &mut BossBarFlags, &mut BossBarHealth, &mut BossBarTitle, - &EntityLayerId, + &LayerId, ), With, >, diff --git a/examples/building.rs b/examples/building.rs index cf3484946..3f0cdadd2 100644 --- a/examples/building.rs +++ b/examples/building.rs @@ -3,6 +3,7 @@ use valence::interact_block::InteractBlockEvent; use valence::inventory::HeldItem; use valence::prelude::*; +use valence_server::dimension_layer::DimensionInfo; const SPAWN_Y: i32 = 64; @@ -14,7 +15,6 @@ pub fn main() { Update, ( init_clients, - despawn_disconnected_clients, toggle_gamemode_on_sneak, digging, place_blocks, @@ -29,11 +29,11 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = CombinedLayerBundle::new(Default::default(), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk_index.insert([x, z], Chunk::new()); } } @@ -52,30 +52,20 @@ fn init_clients( mut clients: Query< ( &mut Client, - &mut EntityLayerId, - &mut VisibleChunkLayer, - &mut VisibleEntityLayers, + &mut LayerId, + &mut VisibleLayers, &mut Position, &mut GameMode, ), Added, >, - layers: Query, With)>, + layers: Query>, ) { - for ( - mut client, - mut layer_id, - mut visible_chunk_layer, - mut visible_entity_layers, - mut pos, - mut game_mode, - ) in &mut clients - { + for (mut client, mut layer_id, mut visible_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); + visible_layers.0.insert(layer); pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); *game_mode = GameMode::Creative; diff --git a/examples/chest.rs b/examples/chest.rs index 57add8e28..4f20c7bf6 100644 --- a/examples/chest.rs +++ b/examples/chest.rs @@ -2,6 +2,7 @@ use valence::interact_block::InteractBlockEvent; use valence::prelude::*; +use valence_server::dimension_layer::DimensionInfo; const SPAWN_Y: i32 = 64; const CHEST_POS: [i32; 3] = [0, SPAWN_Y + 1, 3]; @@ -10,15 +11,7 @@ pub fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) - .add_systems( - Update, - ( - init_clients, - toggle_gamemode_on_sneak, - open_chest, - despawn_disconnected_clients, - ), - ) + .add_systems(Update, (init_clients, toggle_gamemode_on_sneak, open_chest)) .run(); } @@ -28,11 +21,11 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = CombinedLayerBundle::new(Default::default(), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk_index.insert([x, z], Chunk::new()); } } @@ -44,7 +37,7 @@ fn setup( } } - layer.chunk.set_block(CHEST_POS, BlockState::CHEST); + layer.chunk_index.set_block(CHEST_POS, BlockState::CHEST); commands.spawn(layer); @@ -59,24 +52,16 @@ fn setup( fn init_clients( mut clients: Query< ( - &mut EntityLayerId, - &mut VisibleChunkLayer, - &mut VisibleEntityLayers, + &mut LayerId, + &mut VisibleLayers, &mut Position, &mut GameMode, ), Added, >, - layers: Query, With)>, + layers: Query>, ) { - for ( - mut layer_id, - mut visible_chunk_layer, - mut visible_entity_layers, - mut pos, - mut game_mode, - ) in &mut clients - { + for (mut layer_id, mut visible_layers, mut pos, mut game_mode) in &mut clients { let layer = layers.single(); layer_id.0 = layer; diff --git a/examples/combat.rs b/examples/combat.rs index 6a79a267f..b19b447c9 100644 --- a/examples/combat.rs +++ b/examples/combat.rs @@ -5,6 +5,7 @@ use rand::Rng; use valence::entity::EntityStatuses; use valence::math::Vec3Swizzles; use valence::prelude::*; +use valence_server::dimension_layer::DimensionInfo; const SPAWN_Y: i32 = 64; const ARENA_RADIUS: i32 = 32; @@ -22,14 +23,7 @@ pub fn main() { .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems(EventLoopUpdate, handle_combat_events) - .add_systems( - Update, - ( - init_clients, - despawn_disconnected_clients, - teleport_oob_clients, - ), - ) + .add_systems(Update, (init_clients, teleport_oob_clients)) .run(); } @@ -39,11 +33,11 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = CombinedLayerBundle::new(Default::default(), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk_index.insert([x, z], Chunk::new()); } } @@ -65,7 +59,7 @@ fn setup( }; for y in 0..SPAWN_Y { - layer.chunk.set_block([x, y, z], block); + layer.chunk_index.set_block([x, y, z], block); } } } @@ -76,24 +70,16 @@ fn setup( fn init_clients( mut clients: Query< ( - &mut EntityLayerId, - &mut VisibleChunkLayer, - &mut VisibleEntityLayers, + &mut LayerId, + &mut VisibleLayers, &mut Position, &mut GameMode, ), Added, >, - layers: Query, With)>, + layers: Query>, ) { - for ( - mut layer_id, - mut visible_chunk_layer, - mut visible_entity_layers, - mut pos, - mut game_mode, - ) in &mut clients - { + for (mut layer_id, mut visible_layers, mut pos, mut game_mode) in &mut clients { let layer = layers.single(); layer_id.0 = layer; diff --git a/examples/cow_sphere.rs b/examples/cow_sphere.rs index e9f9ba124..43041efc8 100644 --- a/examples/cow_sphere.rs +++ b/examples/cow_sphere.rs @@ -6,6 +6,7 @@ use valence::abilities::{PlayerStartFlyingEvent, PlayerStopFlyingEvent}; use valence::math::{DQuat, EulerRot}; use valence::message::SendMessage; use valence::prelude::*; +use valence_server::dimension_layer::DimensionInfo; use valence_text::color::NamedColor; type SpherePartBundle = valence::entity::cow::CowEntityBundle; @@ -26,15 +27,7 @@ fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) - .add_systems( - Update, - ( - init_clients, - update_sphere, - despawn_disconnected_clients, - display_is_flying, - ), - ) + .add_systems(Update, (init_clients, update_sphere, display_is_flying)) .run(); } @@ -44,22 +37,22 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = CombinedLayerBundle::new(Default::default(), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk_index.insert([x, z], Chunk::new()); } } - layer.chunk.set_block(SPAWN_POS, BlockState::BEDROCK); + layer.chunk_index.set_block(SPAWN_POS, BlockState::BEDROCK); let layer_id = commands.spawn(layer).id(); commands.spawn_batch([0; SPHERE_AMOUNT].map(|_| { ( SpherePartBundle { - layer: EntityLayerId(layer_id), + layer: LayerId(layer_id), ..Default::default() }, SpherePart, @@ -70,29 +63,20 @@ fn setup( fn init_clients( mut clients: Query< ( - &mut EntityLayerId, - &mut VisibleChunkLayer, - &mut VisibleEntityLayers, + &mut LayerId, + &mut VisibleLayers, &mut Position, &mut GameMode, ), Added, >, - layers: Query, With)>, + layers: Query>, ) { - for ( - mut layer_id, - mut visible_chunk_layer, - mut visible_entity_layers, - mut pos, - mut game_mode, - ) in &mut clients - { + for (mut layer_id, mut visible_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); + visible_layers.0.insert(layer); pos.set([ SPAWN_POS.x as f64 + 0.5, SPAWN_POS.y as f64 + 1.0, diff --git a/examples/ctf.rs b/examples/ctf.rs index b6ad1491f..f399d2cad 100644 --- a/examples/ctf.rs +++ b/examples/ctf.rs @@ -17,6 +17,7 @@ use valence::nbt::{compound, List}; use valence::prelude::*; use valence::scoreboard::*; use valence::status::RequestRespawnEvent; +use valence_server::dimension_layer::DimensionInfo; const ARENA_Y: i32 = 64; const ARENA_MID_WIDTH: i32 = 2; @@ -43,7 +44,6 @@ pub fn main() { Update, ( init_clients, - despawn_disconnected_clients, digging, place_blocks, do_team_selector_portals, @@ -65,11 +65,11 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = CombinedLayerBundle::new(Default::default(), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk_index.insert([x, z], Chunk::new()); } } @@ -80,7 +80,7 @@ fn setup( x if x > ARENA_MID_WIDTH => BlockState::BLUE_CONCRETE, _ => BlockState::WHITE_CONCRETE, }; - layer.chunk.set_block([x, ARENA_Y, z], block); + layer.chunk_index.set_block([x, ARENA_Y, z], block); } } @@ -111,7 +111,7 @@ fn setup( let ctf_objective = ObjectiveBundle { name: Objective::new("ctf-captures"), display: ObjectiveDisplay("Captures".into_text()), - layer: EntityLayerId(ctf_objective_layer), + layer: LayerId(ctf_objective_layer), ..Default::default() }; commands.spawn(ctf_objective); @@ -142,7 +142,7 @@ fn setup( let mut flags = Flags::default(); flags.set_glowing(true); let mut pig = commands.spawn(PigEntityBundle { - layer: EntityLayerId(ctf_team_layers.friendly_layers[&Team::Red]), + layer: LayerId(ctf_team_layers.friendly_layers[&Team::Red]), position: Position([-30.0, 65.0, 2.0].into()), entity_flags: flags.clone(), ..Default::default() @@ -150,7 +150,7 @@ fn setup( pig.insert(Team::Red); let mut cow = commands.spawn(CowEntityBundle { - layer: EntityLayerId(ctf_team_layers.friendly_layers[&Team::Blue]), + layer: LayerId(ctf_team_layers.friendly_layers[&Team::Blue]), position: Position([30.0, 65.0, 2.0].into()), entity_flags: flags, ..Default::default() @@ -170,11 +170,11 @@ fn build_flag(layer: &mut LayerBundle, team: Team, pos: impl Into) -> // build the flag pole for _ in 0..3 { - layer.chunk.set_block(pos, BlockState::OAK_FENCE); + layer.chunk_index.set_block(pos, BlockState::OAK_FENCE); pos.y += 1; } let moving_east = pos.x < 0; - layer.chunk.set_block( + layer.chunk_index.set_block( pos, BlockState::OAK_FENCE.set( if moving_east { @@ -186,14 +186,14 @@ fn build_flag(layer: &mut LayerBundle, team: Team, pos: impl Into) -> ), ); pos.x += if pos.x < 0 { 1 } else { -1 }; - layer.chunk.set_block( + layer.chunk_index.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( + layer.chunk_index.set_block( pos, BlockState::OAK_FENCE.set( if moving_east { @@ -207,7 +207,7 @@ fn build_flag(layer: &mut LayerBundle, team: Team, pos: impl Into) -> pos.y -= 1; // build the flag - layer.chunk.set_block( + layer.chunk_index.set_block( pos, match team { Team::Red => BlockState::RED_WOOL, @@ -229,7 +229,7 @@ fn build_spawn_box(layer: &mut LayerBundle, pos: impl Into, commands: layer .chunk .set_block([pos.x + x, pos.y, pos.z + z], spawn_box_block); - layer.chunk.set_block( + layer.chunk_index.set_block( [pos.x + x, pos.y + SPAWN_BOX_HEIGHT, pos.z + z], spawn_box_block, ); @@ -270,7 +270,7 @@ fn build_spawn_box(layer: &mut LayerBundle, pos: impl Into, commands: ] { for z in 0..3 { for x in 0..3 { - layer.chunk.set_block( + layer.chunk_index.set_block( [pos.x + offset.x + x, pos.y + offset.y, pos.z + offset.z + z], block, ); @@ -296,7 +296,7 @@ fn build_spawn_box(layer: &mut LayerBundle, pos: impl Into, commands: for area in portals.portals.values() { for pos in area.iter_block_pos() { - layer.chunk.set_block(pos, BlockState::AIR); + layer.chunk_index.set_block(pos, BlockState::AIR); } layer .chunk @@ -308,7 +308,7 @@ fn build_spawn_box(layer: &mut LayerBundle, pos: impl Into, commands: // build instruction signs let sign_pos = pos.offset(0, 2, SPAWN_BOX_WIDTH - 1); - layer.chunk.set_block( + layer.chunk_index.set_block( sign_pos, Block { state: BlockState::OAK_WALL_SIGN.set(PropName::Rotation, PropValue::_3), @@ -325,7 +325,7 @@ fn build_spawn_box(layer: &mut LayerBundle, pos: impl Into, commands: }, ); - layer.chunk.set_block( + layer.chunk_index.set_block( sign_pos.offset(-1, 0, 0), Block { state: BlockState::OAK_WALL_SIGN.set(PropName::Rotation, PropValue::_3), @@ -342,7 +342,7 @@ fn build_spawn_box(layer: &mut LayerBundle, pos: impl Into, commands: }, ); - layer.chunk.set_block( + layer.chunk_index.set_block( sign_pos.offset(1, 0, 0), Block { state: BlockState::OAK_WALL_SIGN.set(PropName::Rotation, PropValue::_3), @@ -364,34 +364,25 @@ fn init_clients( mut clients: Query< ( &mut Client, - &mut EntityLayerId, - &mut VisibleChunkLayer, - &mut VisibleEntityLayers, + &mut LayerId, + &mut VisibleLayers, &mut Position, &mut GameMode, &mut Health, ), Added, >, - main_layers: Query, With)>, + main_layers: Query>, globals: Res, ) { - for ( - mut client, - mut layer_id, - mut visible_chunk_layer, - mut visible_entity_layers, - mut pos, - mut game_mode, - mut health, - ) in &mut clients + for (mut client, mut layer_id, mut visible_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); - visible_entity_layers.0.insert(globals.scoreboard_layer); + visible_layers.0.insert(layer); + visible_layers.0.insert(globals.scoreboard_layer); pos.set(SPAWN_POS); *game_mode = GameMode::Adventure; health.0 = PLAYER_MAX_HEALTH; @@ -567,7 +558,7 @@ fn do_team_selector_portals( portals: Res, mut commands: Commands, ctf_layers: Res, - main_layers: Query, With)>, + main_layers: Query>, ) { for player in players.iter_mut() { let ( @@ -640,7 +631,7 @@ fn do_team_selector_portals( let mut flags = Flags::default(); flags.set_glowing(true); let mut player_glowing = commands.spawn(PlayerEntityBundle { - layer: EntityLayerId(friendly_layer), + layer: LayerId(friendly_layer), uuid: *unique_id, entity_flags: flags, position: *pos, @@ -650,7 +641,7 @@ fn do_team_selector_portals( let enemy_layer = ctf_layers.enemy_layers[&team]; let mut player_enemy = commands.spawn(PlayerEntityBundle { - layer: EntityLayerId(enemy_layer), + layer: LayerId(enemy_layer), uuid: *unique_id, position: *pos, ..Default::default() @@ -1013,17 +1004,12 @@ fn teleport_oob_clients(mut clients: Query<(&mut Position, &Team), With> /// Handles respawning dead players. fn necromancy( - mut clients: Query<( - &mut VisibleChunkLayer, - &mut RespawnPosition, - &Team, - &mut Health, - )>, + mut clients: Query<(&mut VisibleLayers, &mut RespawnPosition, &Team, &mut Health)>, mut events: EventReader, - layers: Query, With)>, + layers: Query>, ) { for event in events.iter() { - if let Ok((mut visible_chunk_layer, mut respawn_pos, team, mut health)) = + if let Ok((mut visible_layers, mut respawn_pos, team, mut health)) = clients.get_mut(event.client) { respawn_pos.pos = team.spawn_pos().into(); diff --git a/examples/death.rs b/examples/death.rs index 645d76d45..c5feb0297 100644 --- a/examples/death.rs +++ b/examples/death.rs @@ -2,6 +2,7 @@ use valence::prelude::*; use valence::status::RequestRespawnEvent; +use valence_server::dimension_layer::DimensionInfo; const SPAWN_Y: i32 = 64; @@ -9,15 +10,7 @@ pub fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) - .add_systems( - Update, - ( - init_clients, - squat_and_die, - necromancy, - despawn_disconnected_clients, - ), - ) + .add_systems(Update, (init_clients, squat_and_die, necromancy)) .run(); } @@ -32,17 +25,17 @@ fn setup( BlockState::DEEPSLATE, BlockState::MAGMA_BLOCK, ] { - let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = CombinedLayerBundle::new(Default::default(), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk_index.insert([x, z], Chunk::new()); } } for z in -25..25 { for x in -25..25 { - layer.chunk.set_block([x, SPAWN_Y, z], block); + layer.chunk_index.set_block([x, SPAWN_Y, z], block); } } @@ -54,30 +47,20 @@ fn init_clients( mut clients: Query< ( &mut Client, - &mut EntityLayerId, - &mut VisibleChunkLayer, - &mut VisibleEntityLayers, + &mut LayerId, + &mut VisibleLayers, &mut Position, &mut GameMode, ), Added, >, - layers: Query, With)>, + layers: Query>, ) { - for ( - mut client, - mut layer_id, - mut visible_chunk_layer, - mut visible_entity_layers, - mut pos, - mut game_mode, - ) in &mut clients - { + for (mut client, mut layer_id, mut visible_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); + visible_layers.0.insert(layer); pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); *game_mode = GameMode::Creative; @@ -99,21 +82,17 @@ fn squat_and_die(mut clients: Query<&mut Client>, mut events: EventReader, mut events: EventReader, - layers: Query, With)>, + layers: Query>, ) { for event in events.iter() { - if let Ok(( - mut layer_id, - mut visible_chunk_layer, - mut visible_entity_layers, - mut respawn_pos, - )) = clients.get_mut(event.client) + if let Ok((mut layer_id, mut visible_layers, mut respawn_pos)) = + clients.get_mut(event.client) { respawn_pos.pos = BlockPos::new(0, SPAWN_Y, 0); diff --git a/examples/entity_hitbox.rs b/examples/entity_hitbox.rs index 59eff1a31..93fe9f2b3 100644 --- a/examples/entity_hitbox.rs +++ b/examples/entity_hitbox.rs @@ -14,6 +14,7 @@ use valence::entity::zombie_horse::ZombieHorseEntityBundle; use valence::entity::{entity, Pose}; use valence::prelude::*; use valence::rand::Rng; +use valence_server::dimension_layer::DimensionInfo; pub fn main() { App::new() @@ -29,17 +30,19 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = CombinedLayerBundle::new(Default::default(), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk_index.insert([x, z], Chunk::new()); } } for z in -25..25 { for x in -25..25 { - layer.chunk.set_block([x, 64, z], BlockState::GRASS_BLOCK); + layer + .chunk_index + .set_block([x, 64, z], BlockState::GRASS_BLOCK); } } @@ -50,30 +53,20 @@ fn init_clients( mut clients: Query< ( &mut Client, - &mut EntityLayerId, - &mut VisibleChunkLayer, - &mut VisibleEntityLayers, + &mut LayerId, + &mut VisibleLayers, &mut Position, &mut GameMode, ), Added, >, - layers: Query, With)>, + layers: Query>, ) { - for ( - mut client, - mut layer_id, - mut visible_chunk_layer, - mut visible_entity_layers, - mut pos, - mut game_mode, - ) in &mut clients - { + for (mut client, mut layer_id, mut visible_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); + visible_layers.0.insert(layer); pos.set([0.0, 65.0, 0.0]); *game_mode = GameMode::Creative; @@ -84,7 +77,7 @@ fn init_clients( fn spawn_entity( mut commands: Commands, mut sneaking: EventReader, - client_query: Query<(&Position, &EntityLayerId)>, + client_query: Query<(&Position, &LayerId)>, ) { 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 d7624fd43..c8559dfad 100644 --- a/examples/game_of_life.rs +++ b/examples/game_of_life.rs @@ -3,6 +3,7 @@ use std::mem; use valence::prelude::*; +use valence_server::dimension_layer::DimensionInfo; const BOARD_MIN_X: i32 = -30; const BOARD_MAX_X: i32 = 30; @@ -27,7 +28,6 @@ pub fn main() { Update, ( init_clients, - despawn_disconnected_clients, toggle_cell_on_dig, update_board, pause_on_crouch, @@ -47,17 +47,19 @@ fn setup( biome.effects.grass_color = Some(0x00ff00); } - let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = CombinedLayerBundle::new(Default::default(), &dimensions, &biomes, &server); for z in -10..10 { for x in -10..10 { - layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk_index.insert([x, z], Chunk::new()); } } for z in BOARD_MIN_Z..=BOARD_MAX_Z { for x in BOARD_MIN_X..=BOARD_MAX_X { - layer.chunk.set_block([x, BOARD_Y, z], BlockState::DIRT); + layer + .chunk_index + .set_block([x, BOARD_Y, z], BlockState::DIRT); } } @@ -74,30 +76,20 @@ fn init_clients( mut clients: Query< ( &mut Client, - &mut EntityLayerId, - &mut VisibleChunkLayer, - &mut VisibleEntityLayers, + &mut LayerId, + &mut VisibleLayers, &mut Position, &mut GameMode, ), Added, >, - layers: Query, With)>, + layers: Query>, ) { - for ( - mut client, - mut layer_id, - mut visible_chunk_layer, - mut visible_entity_layers, - mut pos, - mut game_mode, - ) in &mut clients - { + for (mut client, mut layer_id, mut visible_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); + visible_layers.0.insert(layer); pos.set([0.0, 65.0, 0.0]); *game_mode = GameMode::Survival; diff --git a/examples/parkour.rs b/examples/parkour.rs index 806d1cdd7..58b60669d 100644 --- a/examples/parkour.rs +++ b/examples/parkour.rs @@ -32,7 +32,6 @@ pub fn main() { reset_clients.after(init_clients), manage_chunks.after(reset_clients).before(manage_blocks), manage_blocks, - despawn_disconnected_clients, ), ) .run(); @@ -52,7 +51,7 @@ fn init_clients( ( Entity, &mut Client, - &mut VisibleChunkLayer, + &mut VisibleLayers, &mut IsFlat, &mut GameMode, ), diff --git a/examples/particles.rs b/examples/particles.rs index f230cd9f9..0e4663a36 100644 --- a/examples/particles.rs +++ b/examples/particles.rs @@ -3,6 +3,7 @@ use std::fmt; use valence::prelude::*; +use valence_server::dimension_layer::DimensionInfo; const SPAWN_Y: i32 = 64; @@ -10,10 +11,7 @@ pub fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) - .add_systems( - Update, - (init_clients, despawn_disconnected_clients, manage_particles), - ) + .add_systems(Update, (init_clients, manage_particles)) .run(); } @@ -26,15 +24,17 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = CombinedLayerBundle::new(Default::default(), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk_index.insert([x, z], Chunk::new()); } } - layer.chunk.set_block([0, SPAWN_Y, 0], BlockState::BEDROCK); + layer + .chunk_index + .set_block([0, SPAWN_Y, 0], BlockState::BEDROCK); commands.spawn(layer); @@ -44,29 +44,20 @@ fn setup( fn init_clients( mut clients: Query< ( - &mut EntityLayerId, - &mut VisibleChunkLayer, - &mut VisibleEntityLayers, + &mut LayerId, + &mut VisibleLayers, &mut Position, &mut GameMode, ), Added, >, - layers: Query, With)>, + layers: Query>, ) { - for ( - mut layer_id, - mut visible_chunk_layer, - mut visible_entity_layers, - mut pos, - mut game_mode, - ) in &mut clients - { + for (mut layer_id, mut visible_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); + visible_layers.0.insert(layer); pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); *game_mode = GameMode::Creative; } diff --git a/examples/player_list.rs b/examples/player_list.rs index 8b3635286..3e3ff0b9c 100644 --- a/examples/player_list.rs +++ b/examples/player_list.rs @@ -4,6 +4,7 @@ use rand::Rng; use valence::keepalive::Ping; use valence::player_list::{DisplayName, PlayerListEntryBundle}; use valence::prelude::*; +use valence_server::dimension_layer::DimensionInfo; const SPAWN_Y: i32 = 64; const PLAYER_UUID_1: Uuid = Uuid::from_u128(1); @@ -15,12 +16,7 @@ fn main() { .add_systems(Startup, setup) .add_systems( Update, - ( - init_clients, - override_display_name, - update_player_list, - despawn_disconnected_clients, - ), + (init_clients, override_display_name, update_player_list), ) .run(); } @@ -31,11 +27,11 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = CombinedLayerBundle::new(Default::default(), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk_index.insert([x, z], Chunk::new()); } } @@ -60,30 +56,20 @@ fn init_clients( mut clients: Query< ( &mut Client, - &mut EntityLayerId, - &mut VisibleChunkLayer, - &mut VisibleEntityLayers, + &mut LayerId, + &mut VisibleLayers, &mut Position, &mut GameMode, ), Added, >, - layers: Query, With)>, + layers: Query>, ) { - for ( - mut client, - mut layer_id, - mut visible_chunk_layer, - mut visible_entity_layers, - mut pos, - mut game_mode, - ) in &mut clients - { + for (mut client, mut layer_id, mut visible_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); + visible_layers.0.insert(layer); pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); *game_mode = GameMode::Creative; diff --git a/examples/resource_pack.rs b/examples/resource_pack.rs index b337aadad..02fc8fff4 100644 --- a/examples/resource_pack.rs +++ b/examples/resource_pack.rs @@ -5,6 +5,7 @@ use valence::message::SendMessage; use valence::prelude::*; use valence::protocol::packets::play::ResourcePackStatusC2s; use valence::resource_pack::ResourcePackStatusEvent; +use valence_server::dimension_layer::DimensionInfo; const SPAWN_Y: i32 = 64; @@ -14,12 +15,7 @@ pub fn main() { .add_systems(Startup, setup) .add_systems( Update, - ( - init_clients, - prompt_on_punch, - on_resource_pack_status, - despawn_disconnected_clients, - ), + (init_clients, prompt_on_punch, on_resource_pack_status), ) .run(); } @@ -30,24 +26,26 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = CombinedLayerBundle::new(Default::default(), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk_index.insert([x, z], Chunk::new()); } } for z in -25..25 { for x in -25..25 { - layer.chunk.set_block([x, SPAWN_Y, z], BlockState::BEDROCK); + layer + .chunk_index + .set_block([x, SPAWN_Y, z], BlockState::BEDROCK); } } let layer_ent = commands.spawn(layer).id(); commands.spawn(SheepEntityBundle { - layer: EntityLayerId(layer_ent), + layer: LayerId(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), @@ -59,30 +57,20 @@ fn init_clients( mut clients: Query< ( &mut Client, - &mut EntityLayerId, - &mut VisibleChunkLayer, - &mut VisibleEntityLayers, + &mut LayerId, + &mut VisibleLayers, &mut Position, &mut GameMode, ), Added, >, - layers: Query, With)>, + layers: Query>, ) { - for ( - mut client, - mut layer_id, - mut visible_chunk_layer, - mut visible_entity_layers, - mut pos, - mut game_mode, - ) in &mut clients - { + for (mut client, mut layer_id, mut visible_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); + visible_layers.0.insert(layer); pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); *game_mode = GameMode::Creative; diff --git a/examples/terrain.rs b/examples/terrain.rs index f7aee57d0..40ca52f4b 100644 --- a/examples/terrain.rs +++ b/examples/terrain.rs @@ -11,12 +11,13 @@ use noise::{NoiseFn, SuperSimplex}; use tracing::info; use valence::prelude::*; use valence::spawn::IsFlat; +use valence_server::dimension_layer::DimensionInfo; const SPAWN_POS: DVec3 = DVec3::new(0.0, 200.0, 0.0); const HEIGHT: u32 = 384; struct ChunkWorkerState { - sender: Sender<(ChunkPos, UnloadedChunk)>, + sender: Sender<(ChunkPos, Chunk)>, receiver: Receiver, // Noise functions density: SuperSimplex, @@ -32,7 +33,7 @@ struct GameState { /// been sent to the thread pool. pending: HashMap>, sender: Sender, - receiver: Receiver<(ChunkPos, UnloadedChunk)>, + receiver: Receiver<(ChunkPos, Chunk)>, } /// The order in which chunks should be processed by the thread pool. Smaller @@ -45,16 +46,13 @@ pub fn main() { .add_systems(Startup, setup) .add_systems( Update, - ( - ( - init_clients, - remove_unviewed_chunks, - update_client_views, - send_recv_chunks, - ) - .chain(), - despawn_disconnected_clients, - ), + (( + init_clients, + remove_unviewed_chunks, + update_client_views, + send_recv_chunks, + ) + .chain(),), ) .run(); } @@ -105,7 +103,7 @@ fn setup( receiver: finished_receiver, }); - let layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + let layer = CombinedLayerBundle::new(Default::default(), &dimensions, &biomes, &server); commands.spawn(layer); } @@ -113,31 +111,21 @@ fn setup( fn init_clients( mut clients: Query< ( - &mut EntityLayerId, - &mut VisibleChunkLayer, - &mut VisibleEntityLayers, + &mut LayerId, + &mut VisibleLayers, &mut Position, &mut GameMode, &mut IsFlat, ), Added, >, - layers: Query, With)>, + layers: Query>, ) { - for ( - mut layer_id, - mut visible_chunk_layer, - mut visible_entity_layers, - mut pos, - mut game_mode, - mut is_flat, - ) in &mut clients - { + for (mut layer_id, mut visible_layers, mut pos, mut game_mode, mut is_flat) in &mut clients { let layer = layers.single(); layer_id.0 = layer; - visible_chunk_layer.0 = layer; - visible_entity_layers.0.insert(layer); + visible_layers.0.insert(layer); pos.set(SPAWN_POS); *game_mode = GameMode::Creative; is_flat.0 = true; @@ -218,7 +206,7 @@ fn send_recv_chunks(mut layers: Query<&mut ChunkLayer>, state: ResMut fn chunk_worker(state: Arc) { while let Ok(pos) = state.receiver.recv() { - let mut chunk = UnloadedChunk::with_height(HEIGHT); + let mut chunk = Chunk::with_height(HEIGHT); for offset_z in 0..16 { for offset_x in 0..16 { diff --git a/examples/text.rs b/examples/text.rs index 8a7ed9eb7..4d848dcc9 100644 --- a/examples/text.rs +++ b/examples/text.rs @@ -2,6 +2,7 @@ use valence::lang::keys; use valence::prelude::*; +use valence_server::dimension_layer::DimensionInfo; const SPAWN_Y: i32 = 64; @@ -9,7 +10,7 @@ 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) .run(); } @@ -19,18 +20,18 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = CombinedLayerBundle::new(Default::default(), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk_index.insert([x, z], Chunk::new()); } } for z in -25..25 { for x in -25..25 { layer - .chunk + .chunk_index .set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); } } @@ -43,30 +44,20 @@ fn init_clients( ( &mut Client, &mut Position, - &mut EntityLayerId, - &mut VisibleChunkLayer, - &mut VisibleEntityLayers, + &mut LayerId, + &mut VisibleLayers, &mut GameMode, ), Added, >, - layers: Query, With)>, + layers: Query>, ) { - for ( - mut client, - mut pos, - mut layer_id, - mut visible_chunk_layer, - mut visible_entity_layers, - mut game_mode, - ) in &mut clients - { + for (mut client, mut pos, mut layer_id, mut visible_layers, mut game_mode) in &mut clients { let layer = layers.single(); pos.0 = [0.0, SPAWN_Y as f64 + 1.0, 0.0].into(); layer_id.0 = layer; - visible_chunk_layer.0 = layer; - visible_entity_layers.0.insert(layer); + visible_layers.insert(layer); *game_mode = GameMode::Creative; client.send_chat_message("Welcome to the text example.".bold()); diff --git a/examples/weather.rs b/examples/weather.rs index e0c227668..8c3fbd723 100644 --- a/examples/weather.rs +++ b/examples/weather.rs @@ -2,15 +2,13 @@ use std::f64::consts::TAU; use valence::prelude::*; use valence::weather::{Rain, Thunder, WeatherBundle}; +use valence_server::dimension_layer::DimensionInfo; pub fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) - .add_systems( - Update, - (init_clients, despawn_disconnected_clients, change_weather), - ) + .add_systems(Update, (init_clients, change_weather)) .run(); } @@ -20,17 +18,19 @@ fn setup( dimensions: Res, biomes: Res, ) { - let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = CombinedLayerBundle::new(Default::default(), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk_index.insert([x, z], Chunk::new()); } } for z in -25..25 { for x in -25..25 { - layer.chunk.set_block([x, 64, z], BlockState::GRASS_BLOCK); + layer + .chunk_index + .set_block([x, 64, z], BlockState::GRASS_BLOCK); } } @@ -40,36 +40,27 @@ fn setup( fn init_clients( mut clients: Query< ( - &mut EntityLayerId, - &mut VisibleChunkLayer, - &mut VisibleEntityLayers, + &mut LayerId, + &mut VisibleLayers, &mut Position, &mut GameMode, ), Added, >, - layers: Query, With)>, + layers: Query>, ) { - for ( - mut layer_id, - mut visible_chunk_layer, - mut visible_entity_layers, - mut pos, - mut game_mode, - ) in &mut clients - { + for (mut layer_id, mut visible_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); + visible_layers.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>, + mut layers: Query<(&mut Rain, &mut Thunder), With>, server: Res, ) { let period = 5.0; diff --git a/examples/world_border.rs b/examples/world_border.rs index bb7111c30..471ee4537 100644 --- a/examples/world_border.rs +++ b/examples/world_border.rs @@ -1,11 +1,12 @@ #![allow(clippy::type_complexity)] use bevy_app::App; -use valence::client::despawn_disconnected_clients; use valence::inventory::HeldItem; use valence::message::{ChatMessageEvent, SendMessage}; use valence::prelude::*; use valence::world_border::*; +use valence_server::dimension_layer::DimensionInfo; +use valence_server::layer::message::LayerMessages; const SPAWN_Y: i32 = 64; @@ -13,15 +14,7 @@ fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) - .add_systems( - Update, - ( - despawn_disconnected_clients, - init_clients, - border_controls, - display_diameter, - ), - ) + .add_systems(Update, (init_clients, border_controls, display_diameter)) .run(); } @@ -31,18 +24,18 @@ fn setup( biomes: Res, dimensions: Res, ) { - let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = CombinedLayerBundle::new(Default::default(), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk_index.insert([x, z], Chunk::new()); } } for z in -25..25 { for x in -25..25 { layer - .chunk + .chunk_index .set_block([x, SPAWN_Y, z], BlockState::MOSSY_COBBLESTONE); } } @@ -63,32 +56,22 @@ fn init_clients( mut clients: Query< ( &mut Client, - &mut EntityLayerId, - &mut VisibleChunkLayer, - &mut VisibleEntityLayers, + &mut LayerId, + &mut VisibleLayers, &mut Position, &mut Inventory, &HeldItem, ), Added, >, - layers: Query>, + layers: Query>, ) { - for ( - mut client, - mut layer_id, - mut visible_chunk_layer, - mut visible_entity_layers, - mut pos, - mut inv, - main_slot, - ) in &mut clients + for (mut client, mut layer_id, mut visible_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); + visible_layers.0.insert(layer); pos.set([0.5, SPAWN_Y as f64 + 1.0, 0.5]); let pickaxe = ItemStack::new(ItemKind::WoodenPickaxe, 1, None); inv.set_slot(main_slot.slot(), pickaxe); @@ -97,17 +80,17 @@ fn init_clients( } } -fn display_diameter(mut layers: Query<(&mut ChunkLayer, &WorldBorderLerp)>) { - for (mut layer, lerp) in &mut layers { +fn display_diameter(mut layers: Query<(&mut LayerMessages, &WorldBorderLerp)>) { + for (mut msgs, lerp) in &mut layers { if lerp.remaining_ticks > 0 { - layer.send_chat_message(format!("diameter = {}", lerp.current_diameter)); + msgs.send_chat_message(format!("diameter = {}", lerp.current_diameter)); } } } fn border_controls( mut events: EventReader, - mut layers: Query<(&mut WorldBorderCenter, &mut WorldBorderLerp), With>, + mut layers: Query<(&mut WorldBorderCenter, &mut WorldBorderLerp), With>, ) { for x in events.iter() { let parts: Vec<&str> = x.message.split(' ').collect(); diff --git a/src/lib.rs b/src/lib.rs index dfffdb3db..1c697c1c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,9 +59,12 @@ use valence_server::client::ClientPlugin; use valence_server::client_command::ClientCommandPlugin; use valence_server::client_settings::ClientSettingsPlugin; use valence_server::custom_payload::CustomPayloadPlugin; +use valence_server::dimension_layer::DimensionLayerPlugin; use valence_server::entity::hitbox::HitboxPlugin; use valence_server::entity::EntityPlugin; +use valence_server::entity_layer::EntityLayerPlugin; use valence_server::event_loop::EventLoopPlugin; +use valence_server::game_mode::UpdateGameModePlugin; use valence_server::hand_swing::HandSwingPlugin; use valence_server::interact_block::InteractBlockPlugin; use valence_server::interact_entity::InteractEntityPlugin; @@ -72,8 +75,8 @@ use valence_server::message::MessagePlugin; use valence_server::movement::MovementPlugin; use valence_server::op_level::OpLevelPlugin; use valence_server::resource_pack::ResourcePackPlugin; +use valence_server::spawn::SpawnPlugin; use valence_server::status::StatusPlugin; -use valence_server::teleport::TeleportPlugin; pub use valence_server::*; #[cfg(feature = "weather")] pub use valence_weather as weather; @@ -117,8 +120,7 @@ pub mod prelude { pub use valence_server::action::{DiggingEvent, DiggingState}; pub use valence_server::block::{BlockKind, BlockState, PropName, PropValue}; pub use valence_server::client::{ - despawn_disconnected_clients, Client, Ip, OldView, OldViewDistance, Properties, Username, - View, ViewDistance, VisibleChunkLayer, VisibleEntityLayers, + Client, Ip, OldView, OldViewDistance, Properties, Username, View, ViewDistance, }; pub use valence_server::client_command::{ ClientCommand, JumpWithHorseEvent, JumpWithHorseState, LeaveBedEvent, SneakEvent, @@ -126,28 +128,26 @@ pub mod prelude { }; pub use valence_server::entity::hitbox::{Hitbox, HitboxShape}; pub use valence_server::entity::{ - EntityAnimation, EntityKind, EntityLayerId, EntityManager, EntityStatus, HeadYaw, Look, - OldEntityLayerId, OldPosition, Position, + EntityAnimation, EntityKind, EntityManager, EntityStatus, HeadYaw, Look, OldPosition, + Position, }; pub use valence_server::event_loop::{ EventLoopPostUpdate, EventLoopPreUpdate, EventLoopUpdate, }; pub use valence_server::ident::Ident; pub use valence_server::interact_entity::{EntityInteraction, InteractEntityEvent}; - pub use valence_server::layer::chunk::{ - Block, BlockRef, Chunk, ChunkLayer, LoadedChunk, UnloadedChunk, - }; - pub use valence_server::layer::{EntityLayer, LayerBundle}; pub use valence_server::math::{DVec2, DVec3, Vec2, Vec3}; pub use valence_server::message::SendMessage as _; + pub use valence_server::movement::{MovementEvent, RespawnPosition}; pub use valence_server::nbt::Compound; pub use valence_server::protocol::packets::play::particle_s2c::Particle; pub use valence_server::protocol::text::{Color, IntoText, Text}; - pub use valence_server::spawn::{ClientSpawnQuery, ClientSpawnQueryReadOnly, RespawnPosition}; + pub use valence_server::spawn::{ClientSpawnQuery, ClientSpawnQueryReadOnly}; pub use valence_server::title::SetTitle as _; pub use valence_server::{ - ident, BlockPos, ChunkPos, ChunkView, Despawned, Direction, GameMode, Hand, ItemKind, - ItemStack, Server, UniqueId, + ident, BlockPos, Chunk, ChunkPos, ChunkView, CombinedLayerBundle, Despawned, Direction, + GameMode, Hand, ItemKind, ItemStack, LayerId, LoadedChunk, OldLayerId, OldVisibleLayers, + Server, UniqueId, VisibleLayers, MINECRAFT_VERSION, }; pub use super::DefaultPlugins; @@ -169,18 +169,21 @@ impl PluginGroup for DefaultPlugins { .add(RegistryPlugin) .add(BiomePlugin) .add(DimensionTypePlugin) + .add(LayerPlugin) + .add(DimensionLayerPlugin) + .add(EntityLayerPlugin) .add(EntityPlugin) .add(HitboxPlugin) - .add(LayerPlugin) .add(ClientPlugin) .add(EventLoopPlugin) + .add(SpawnPlugin) .add(MovementPlugin) + .add(UpdateGameModePlugin) .add(ClientCommandPlugin) .add(KeepalivePlugin) .add(InteractEntityPlugin) .add(ClientSettingsPlugin) .add(ActionPlugin) - .add(TeleportPlugin) .add(MessagePlugin) .add(CustomPayloadPlugin) .add(HandSwingPlugin) diff --git a/src/testing.rs b/src/testing.rs index ba83c02d3..832574e5c 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -7,18 +7,19 @@ use bevy_app::prelude::*; use bevy_ecs::prelude::*; use bytes::{Buf, BufMut, BytesMut}; use uuid::Uuid; -use valence_ident::ident; use valence_network::NetworkPlugin; use valence_registry::{BiomeRegistry, DimensionTypeRegistry}; use valence_server::client::ClientBundleArgs; use valence_server::keepalive::KeepaliveSettings; +use valence_server::layer::CombinedLayerBundle; use valence_server::protocol::decode::PacketFrame; use valence_server::protocol::packets::play::{PlayerPositionLookS2c, TeleportConfirmC2s}; use valence_server::protocol::{Decode, Encode, Packet, PacketDecoder, PacketEncoder, VarInt}; -use valence_server::{ChunkLayer, EntityLayer, Server, ServerSettings}; +use valence_server::{Server, ServerSettings}; use crate::client::{ClientBundle, ClientConnection, ReceivedPacket}; use crate::DefaultPlugins; + pub struct ScenarioSingleClient { /// The new bevy application. pub app: App, @@ -26,13 +27,13 @@ pub struct ScenarioSingleClient { pub client: Entity, /// Helper for sending and receiving packets from the mock client. pub helper: MockClientHelper, - /// Entity with [`ChunkLayer`] and [`EntityLayer`] components. + /// Entity with [`CombinedLayerBundle`] 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. + /// Sets up Valence with a single mock client and dimension+entity layer. + /// The client is configured to be placed within the layer. /// /// Reduces boilerplate in unit tests. pub fn new() -> Self { @@ -49,19 +50,19 @@ impl ScenarioSingleClient { 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 layer = app + .world + .spawn(CombinedLayerBundle::new( + Default::default(), + app.world.resource::(), + app.world.resource::(), + app.world.resource::(), + )) + .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); + client.visible_layers.insert(layer); let client = app.world.spawn(client).id(); ScenarioSingleClient { diff --git a/src/tests/boss_bar.rs b/src/tests/boss_bar.rs index c2b6cf2a6..7790d63b6 100644 --- a/src/tests/boss_bar.rs +++ b/src/tests/boss_bar.rs @@ -3,7 +3,7 @@ use valence_boss_bar::{ BossBarTitle, }; use valence_server::client::VisibleEntityLayers; -use valence_server::entity::EntityLayerId; +use valence_server::entity::LayerId; use valence_server::protocol::packets::play::BossBarS2c; use valence_server::text::IntoText; use valence_server::Despawned; @@ -23,7 +23,7 @@ fn test_initialize_on_join() { .insert(BossBarBundle { title: BossBarTitle("Boss Bar".into_text()), health: BossBarHealth(0.5), - layer: EntityLayerId(scenario.layer), + layer: LayerId(scenario.layer), ..Default::default() }); @@ -183,7 +183,7 @@ fn prepare() -> ScenarioSingleClient { s.app.world.entity_mut(s.layer).insert(BossBarBundle { title: BossBarTitle("Boss Bar".into_text()), health: BossBarHealth(0.5), - layer: EntityLayerId(s.layer), + layer: LayerId(s.layer), ..Default::default() }); diff --git a/src/tests/client.rs b/src/tests/client.rs index 0055469f3..8b4a10340 100644 --- a/src/tests/client.rs +++ b/src/tests/client.rs @@ -1,6 +1,6 @@ use crate::abilities::PlayerAbilitiesFlags; -use crate::layer::chunk::UnloadedChunk; -use crate::layer::ChunkLayer; +use crate::layer_old::chunk::Chunk; +use crate::layer_old::ChunkLayer; use crate::math::DVec3; use crate::protocol::packets::play::{ FullC2s, MoveRelativeS2c, PlayerPositionLookS2c, TeleportConfirmC2s, @@ -21,7 +21,7 @@ fn client_teleport_and_move() { for z in -10..10 { for x in -10..10 { - layer.insert_chunk(ChunkPos::new(x, z), UnloadedChunk::new()); + layer.insert_chunk(ChunkPos::new(x, z), Chunk::new()); } } diff --git a/src/tests/layer.rs b/src/tests/layer.rs index 5f33e052a..6c9cffe68 100644 --- a/src/tests/layer.rs +++ b/src/tests/layer.rs @@ -4,8 +4,8 @@ use bevy_ecs::world::EntityMut; use crate::client::{ViewDistance, VisibleEntityLayers}; use crate::entity::cow::CowEntityBundle; -use crate::entity::{EntityLayerId, Position}; -use crate::layer::chunk::UnloadedChunk; +use crate::entity::Position; +use crate::layer::chunk::Chunk; use crate::layer::{ChunkLayer, EntityLayer}; use crate::protocol::packets::play::{ BlockEntityUpdateS2c, ChunkDataS2c, ChunkDeltaUpdateS2c, EntitiesDestroyS2c, EntitySpawnS2c, @@ -27,7 +27,7 @@ fn block_create_destroy() { let mut layer = app.world.get_mut::(layer_ent).unwrap(); // Insert an empty chunk at (0, 0). - layer.insert_chunk([0, 0], UnloadedChunk::new()); + layer.insert_chunk([0, 0], Chunk::new()); // Wait until the next tick to start sending changes. app.update(); @@ -84,7 +84,7 @@ fn layer_chunk_view_change() { for z in -30..30 { for x in -30..30 { - layer.insert_chunk([x, z], UnloadedChunk::new()); + layer.insert_chunk([x, z], Chunk::new()); } } @@ -162,7 +162,7 @@ fn chunk_viewer_count() { let mut layer = app.world.get_mut::(layer_ent).unwrap(); // Create chunk at (0, 0). - layer.insert_chunk([0, 0], UnloadedChunk::new()); + layer.insert_chunk([0, 0], Chunk::new()); app.update(); // Tick. @@ -174,7 +174,7 @@ fn chunk_viewer_count() { // 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()); + layer.insert_chunk([0, 1], Chunk::new()); let mut client = app.world.entity_mut(client_ent); client.get_mut::().unwrap().set([100.0, 0.0, 0.0]); @@ -195,7 +195,7 @@ fn chunk_viewer_count() { 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()); + layer.insert_chunk([1, 0], Chunk::new()); // Move the client back in view of all three chunks. let mut client = app.world.entity_mut(client_ent); @@ -234,17 +234,17 @@ fn entity_layer_switching() { // Spawn three entities and put them all on the main layer to start. let e1 = CowEntityBundle { - layer: EntityLayerId(l1), + layer: LayerId(l1), ..Default::default() }; let e2 = CowEntityBundle { - layer: EntityLayerId(l1), + layer: LayerId(l1), ..Default::default() }; let e3 = CowEntityBundle { - layer: EntityLayerId(l1), + layer: LayerId(l1), ..Default::default() }; @@ -258,7 +258,7 @@ fn entity_layer_switching() { 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::(e1).unwrap().0 = l2; app.world .get_mut::(client_ent) .unwrap() @@ -337,14 +337,14 @@ fn chunk_entity_spawn_despawn() { let mut layer = app.world.get_mut::(layer_ent).unwrap(); // Insert an empty chunk at (0, 0). - layer.insert_chunk([0, 0], UnloadedChunk::new()); + layer.insert_chunk([0, 0], Chunk::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), + layer: LayerId(layer_ent), ..Default::default() }) .id(); @@ -390,7 +390,7 @@ fn chunk_entity_spawn_despawn() { let mut layer = app.world.get_mut::(layer_ent).unwrap(); - assert!(layer.insert_chunk([0, 0], UnloadedChunk::new()).is_none()); + assert!(layer.insert_chunk([0, 0], Chunk::new()).is_none()); app.update(); @@ -446,7 +446,7 @@ fn chunk_entity_spawn_despawn() { let mut layer = app.world.get_mut::(layer_ent).unwrap(); - layer.insert_chunk([0, 1], UnloadedChunk::new()); + layer.insert_chunk([0, 1], Chunk::new()); layer.remove_chunk([0, 1]).unwrap(); app.world diff --git a/src/tests/player_list.rs b/src/tests/player_list.rs index 2a3e4954b..971e0b56b 100644 --- a/src/tests/player_list.rs +++ b/src/tests/player_list.rs @@ -1,4 +1,4 @@ -use crate::layer::chunk::UnloadedChunk; +use crate::layer_old::chunk::Chunk; use crate::protocol::packets::play::{PlayerListS2c, PlayerSpawnS2c}; use crate::testing::{create_mock_client, ScenarioSingleClient}; use crate::ChunkLayer; @@ -16,7 +16,7 @@ fn player_list_arrives_before_player_spawn() { for z in -5..5 { for x in -5..5 { - layer.insert_chunk([x, z], UnloadedChunk::new()); + layer.insert_chunk([x, z], Chunk::new()); } } diff --git a/src/tests/scoreboard.rs b/src/tests/scoreboard.rs index 867af687a..4c10ef11e 100644 --- a/src/tests/scoreboard.rs +++ b/src/tests/scoreboard.rs @@ -1,8 +1,7 @@ use valence_scoreboard::*; use crate::client::VisibleEntityLayers; -use crate::entity::EntityLayerId; -use crate::layer::EntityLayer; +use crate::layer_old::EntityLayer; use crate::protocol::packets::play::{ ScoreboardDisplayS2c, ScoreboardObjectiveUpdateS2c, ScoreboardPlayerUpdateS2c, }; @@ -38,7 +37,7 @@ fn show_scoreboard_when_added_to_layer() { name: Objective::new("foo"), display: ObjectiveDisplay("Foo".into_text()), scores: ObjectiveScores::new(), - layer: EntityLayerId(obj_layer), + layer: LayerId(obj_layer), ..Default::default() }); @@ -78,7 +77,7 @@ fn show_scoreboard_when_client_join() { name: Objective::new("foo"), display: ObjectiveDisplay("Foo".into_text()), scores: ObjectiveScores::new(), - layer: EntityLayerId(obj_layer), + layer: LayerId(obj_layer), ..Default::default() }); @@ -121,7 +120,7 @@ fn should_update_score() { name: Objective::new("foo"), display: ObjectiveDisplay("Foo".into_text()), scores: ObjectiveScores::with_map([("foo".to_owned(), 0)]), - layer: EntityLayerId(obj_layer), + layer: LayerId(obj_layer), ..Default::default() }) .id(); @@ -169,7 +168,7 @@ fn should_only_update_score_diff() { name: Objective::new("foo"), display: ObjectiveDisplay("Foo".into_text()), scores: ObjectiveScores::with_map([("foo".to_owned(), 0), ("bar".to_owned(), 0)]), - layer: EntityLayerId(obj_layer), + layer: LayerId(obj_layer), ..Default::default() }) .id(); diff --git a/tools/playground/src/playground.template.rs b/tools/playground/src/playground.template.rs index aa17ada01..e924030bf 100644 --- a/tools/playground/src/playground.template.rs +++ b/tools/playground/src/playground.template.rs @@ -1,7 +1,7 @@ -use valence::client::despawn_disconnected_clients; use valence::log::LogPlugin; use valence::network::ConnectionMode; use valence::prelude::*; +use valence_server::dimension_layer::DimensionInfo; #[allow(unused_imports)] use crate::extras::*; @@ -16,7 +16,7 @@ pub fn build_app(app: &mut App) { .add_plugins(DefaultPlugins.build().disable::()) .add_systems(Startup, setup) .add_systems(EventLoopUpdate, toggle_gamemode_on_sneak) - .add_systems(Update, (init_clients, despawn_disconnected_clients)) + .add_systems(Update, init_clients) .run(); } @@ -26,11 +26,11 @@ fn setup( biomes: Res, dimensions: Res, ) { - let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = CombinedLayerBundle::new(Default::default(), &dimensions, &biomes, &server); for z in -5..5 { for x in -5..5 { - layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk_index.insert([x, z], UnloadedChunk::new()); } } @@ -48,29 +48,20 @@ fn setup( fn init_clients( mut clients: Query< ( - &mut EntityLayerId, - &mut VisibleChunkLayer, - &mut VisibleEntityLayers, + &mut LayerId, + &mut VisibleLayers, &mut Position, &mut GameMode, ), Added, >, - layers: Query, With)>, + layers: Query>, ) { - for ( - mut layer_id, - mut visible_chunk_layer, - mut visible_entity_layers, - mut pos, - mut game_mode, - ) in &mut clients - { + for (mut layer_id, mut visible_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); + visible_layers.0.insert(layer); pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); *game_mode = GameMode::Creative; } diff --git a/website/book/1-getting-started/setup.md b/website/book/1-getting-started/setup.md index ea0758c43..2684e2138 100644 --- a/website/book/1-getting-started/setup.md +++ b/website/book/1-getting-started/setup.md @@ -39,12 +39,12 @@ fn setup( biomes: Res, dimensions: Res, ) { - let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + let mut layer = CombinedLayerBundle::new(Default::default(), &dimensions, &biomes, &server); // We have to add chunks to the world first, they start empty. for z in -5..5 { for x in -5..5 { - layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + layer.chunk_index.insert([x, z], UnloadedChunk::new()); } } @@ -64,29 +64,20 @@ Now we need to handle clients when they join the server. Valence automatically s fn init_clients( mut clients: Query< ( - &mut EntityLayerId, - &mut VisibleChunkLayer, - &mut VisibleEntityLayers, + &mut LayerId, + &mut VisibleLayers, &mut Position, &mut GameMode, ), Added, >, - layers: Query, With)>, + layers: Query>, ) { - for ( - mut layer_id, - mut visible_chunk_layer, - mut visible_entity_layers, - mut pos, - mut game_mode, - ) in &mut clients - { + for (mut layer_id, mut visible_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.insert(layer); + visible_layers.insert(layer); pos.set([0.5, 65.0, 0.5]); *game_mode = GameMode::Creative; }