diff --git a/examples/conway.rs b/examples/conway.rs index 51f9891..8a74e05 100644 --- a/examples/conway.rs +++ b/examples/conway.rs @@ -10,8 +10,8 @@ use valence::client::{Event, GameMode}; use valence::config::{Config, ServerListPing}; use valence::text::Color; use valence::{ - async_trait, ident, Biome, BlockState, Client, Dimension, DimensionId, Server, ShutdownResult, - Text, TextFormat, WorldId, Worlds, + async_trait, ident, Biome, BlockState, Dimension, DimensionId, Server, SharedServer, + ShutdownResult, TextFormat, }; pub fn main() -> ShutdownResult { @@ -57,6 +57,13 @@ impl Config for Game { false } + fn dimensions(&self) -> Vec { + vec![Dimension { + fixed_time: Some(6000), + ..Dimension::default() + }] + } + fn biomes(&self) -> Vec { vec![Biome { name: ident!("valence:default_biome"), @@ -65,14 +72,11 @@ impl Config for Game { }] } - fn dimensions(&self) -> Vec { - vec![Dimension { - fixed_time: Some(6000), - ..Dimension::default() - }] - } - - async fn server_list_ping(&self, _server: &Server, _remote_addr: SocketAddr) -> ServerListPing { + async fn server_list_ping( + &self, + _server: &SharedServer, + _remote_addr: SocketAddr, + ) -> ServerListPing { ServerListPing::Respond { online_players: self.player_count.load(Ordering::SeqCst) as i32, max_players: MAX_PLAYERS as i32, @@ -81,26 +85,8 @@ impl Config for Game { } } - fn join( - &self, - _server: &Server, - _client: &mut Client, - worlds: &mut Worlds, - ) -> Result { - if let Ok(_) = self - .player_count - .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { - (count < MAX_PLAYERS).then(|| count + 1) - }) - { - Ok(worlds.iter().next().unwrap().0) - } else { - Err("The server is full!".into()) - } - } - - fn init(&self, _server: &Server, worlds: &mut Worlds) { - let world = worlds.create(DimensionId::default()).1; + fn init(&self, server: &mut Server) { + let world = server.worlds.create(DimensionId::default()).1; world.meta.set_flat(true); for chunk_z in -2..Integer::div_ceil(&(SIZE_X as i32), &16) + 2 { @@ -110,8 +96,8 @@ impl Config for Game { } } - fn update(&self, server: &Server, worlds: &mut Worlds) { - let world = worlds.iter_mut().next().unwrap().1; + fn update(&self, server: &mut Server) { + let (world_id, world) = server.worlds.iter_mut().next().unwrap(); let spawn_pos = [ SIZE_X as f64 / 2.0, @@ -119,10 +105,21 @@ impl Config for Game { SIZE_Z as f64 / 2.0, ]; - world.clients.retain(|_, client| { - if client.created_tick() == server.current_tick() { - client.set_game_mode(GameMode::Survival); + server.clients.retain(|_, client| { + if client.created_tick() == server.shared.current_tick() { + if self + .player_count + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { + (count < MAX_PLAYERS).then_some(count + 1) + }) + .is_err() + { + client.disconnect("The server is full!".color(Color::RED)); + return false; + } + client.set_world(world_id); + client.set_game_mode(GameMode::Survival); client.teleport(spawn_pos, 0.0, 0.0); world.meta.player_list_mut().insert( @@ -148,7 +145,7 @@ impl Config for Game { let State { board, board_buf } = &mut *self.state.lock().unwrap(); - for (_, client) in world.clients.iter_mut() { + for (_, client) in server.clients.iter_mut() { while let Some(event) = client.pop_event() { match event { Event::Digging(e) => { @@ -171,7 +168,7 @@ impl Config for Game { } } - if server.current_tick() % 4 != 0 { + if server.shared.current_tick() % 4 != 0 { return; } @@ -201,7 +198,7 @@ impl Config for Game { mem::swap(board, board_buf); - let min_y = server.dimensions().next().unwrap().1.min_y; + let min_y = server.shared.dimensions().next().unwrap().1.min_y; for chunk_x in 0..Integer::div_ceil(&SIZE_X, &16) { for chunk_z in 0..Integer::div_ceil(&SIZE_Z, &16) { diff --git a/examples/cow_sphere.rs b/examples/cow_sphere.rs index bfee1e4..7726d66 100644 --- a/examples/cow_sphere.rs +++ b/examples/cow_sphere.rs @@ -9,8 +9,8 @@ use valence::config::{Config, ServerListPing}; use valence::text::Color; use valence::util::to_yaw_and_pitch; use valence::{ - async_trait, Client, DimensionId, EntityId, EntityType, Server, ShutdownResult, Text, - TextFormat, WorldId, Worlds, + async_trait, DimensionId, EntityId, EntityType, Server, SharedServer, ShutdownResult, + TextFormat, }; use vek::{Mat3, Vec3}; @@ -45,7 +45,11 @@ impl Config for Game { false } - async fn server_list_ping(&self, _server: &Server, _remote_addr: SocketAddr) -> ServerListPing { + async fn server_list_ping( + &self, + _server: &SharedServer, + _remote_addr: SocketAddr, + ) -> ServerListPing { ServerListPing::Respond { online_players: self.player_count.load(Ordering::SeqCst) as i32, max_players: MAX_PLAYERS as i32, @@ -54,26 +58,8 @@ impl Config for Game { } } - fn join( - &self, - _server: &Server, - _client: &mut Client, - worlds: &mut Worlds, - ) -> Result { - if let Ok(_) = self - .player_count - .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { - (count < MAX_PLAYERS).then(|| count + 1) - }) - { - Ok(worlds.iter().next().unwrap().0) - } else { - Err("The server is full!".into()) - } - } - - fn init(&self, _server: &Server, worlds: &mut Worlds) { - let world = worlds.create(DimensionId::default()).1; + fn init(&self, server: &mut Server) { + let (world_id, world) = server.worlds.create(DimensionId::default()); world.meta.set_flat(true); let size = 5; @@ -84,17 +70,30 @@ impl Config for Game { } self.cows.lock().unwrap().extend((0..200).map(|_| { - let (id, e) = world.entities.create(); + let (id, e) = server.entities.create(); + e.set_world(world_id); e.set_type(EntityType::Cow); id })); } - fn update(&self, server: &Server, worlds: &mut Worlds) { - let world = worlds.iter_mut().next().unwrap().1; + fn update(&self, server: &mut Server) { + let (world_id, world) = server.worlds.iter_mut().next().unwrap(); - world.clients.retain(|_, client| { - if client.created_tick() == server.current_tick() { + server.clients.retain(|_, client| { + if client.created_tick() == server.shared.current_tick() { + if self + .player_count + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { + (count < MAX_PLAYERS).then_some(count + 1) + }) + .is_err() + { + client.disconnect("The server is full!".color(Color::RED)); + return false; + } + + client.set_world(world_id); client.set_game_mode(GameMode::Creative); client.teleport([0.0, 200.0, 0.0], 0.0, 0.0); @@ -116,7 +115,7 @@ impl Config for Game { } }); - let time = server.current_tick() as f64 / server.tick_rate() as f64; + let time = server.shared.current_tick() as f64 / server.shared.tick_rate() as f64; let rot = Mat3::rotation_x(time * TAU * 0.1) .rotated_y(time * TAU * 0.2) @@ -128,7 +127,7 @@ impl Config for Game { let radius = 6.0 + ((time * TAU / 2.5).sin() + 1.0) / 2.0 * 10.0; // TODO: use eye position. - let player_pos = world + let player_pos = server .clients .iter() .next() @@ -136,7 +135,7 @@ impl Config for Game { .unwrap_or_default(); for (cow_id, p) in cows.iter().cloned().zip(fibonacci_spiral(cow_count)) { - let cow = world.entities.get_mut(cow_id).expect("missing cow"); + let cow = server.entities.get_mut(cow_id).expect("missing cow"); let rotated = p * rot; let transformed = rotated * radius + [0.0, 100.0, 0.0]; diff --git a/examples/terrain.rs b/examples/terrain.rs index 9808c4d..450db2a 100644 --- a/examples/terrain.rs +++ b/examples/terrain.rs @@ -11,8 +11,7 @@ use valence::config::{Config, ServerListPing}; use valence::text::Color; use valence::util::chunks_in_view_distance; use valence::{ - async_trait, ChunkPos, Client, DimensionId, Server, ShutdownResult, Text, TextFormat, WorldId, - Worlds, + async_trait, ChunkPos, DimensionId, Server, SharedServer, ShutdownResult, TextFormat, }; use vek::Lerp; @@ -57,7 +56,11 @@ impl Config for Game { false } - async fn server_list_ping(&self, _server: &Server, _remote_addr: SocketAddr) -> ServerListPing { + async fn server_list_ping( + &self, + _server: &SharedServer, + _remote_addr: SocketAddr, + ) -> ServerListPing { ServerListPing::Respond { online_players: self.player_count.load(Ordering::SeqCst) as i32, max_players: MAX_PLAYERS as i32, @@ -66,41 +69,35 @@ impl Config for Game { } } - fn join( - &self, - _server: &Server, - _client: &mut Client, - worlds: &mut Worlds, - ) -> Result { - if let Ok(_) = self - .player_count - .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { - (count < MAX_PLAYERS).then(|| count + 1) - }) - { - Ok(worlds.iter().next().unwrap().0) - } else { - Err("The server is full!".into()) - } - } - - fn init(&self, _server: &Server, worlds: &mut Worlds) { - let (_, world) = worlds.create(DimensionId::default()); + fn init(&self, server: &mut Server) { + let (_, world) = server.worlds.create(DimensionId::default()); world.meta.set_flat(true); } - fn update(&self, server: &Server, worlds: &mut Worlds) { - let world = worlds.iter_mut().next().unwrap().1; + fn update(&self, server: &mut Server) { + let (world_id, world) = server.worlds.iter_mut().next().unwrap(); let mut chunks_to_unload = HashSet::<_>::from_iter(world.chunks.iter().map(|t| t.0)); - world.clients.retain(|_, client| { + server.clients.retain(|_, client| { if client.is_disconnected() { self.player_count.fetch_sub(1, Ordering::SeqCst); return false; } - if client.created_tick() == server.current_tick() { + if client.created_tick() == server.shared.current_tick() { + if self + .player_count + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { + (count < MAX_PLAYERS).then_some(count + 1) + }) + .is_err() + { + client.disconnect("The server is full!".color(Color::RED)); + return false; + } + + client.set_world(world_id); client.set_game_mode(GameMode::Creative); client.set_max_view_distance(32); client.teleport([0.0, 200.0, 0.0], 0.0, 0.0); @@ -115,6 +112,10 @@ impl Config for Game { ); client.send_message("Welcome to the terrain example!".italic()); + client.send_message( + "This demonstrates how to create infinite procedurally generated terrain." + .italic(), + ); } let dist = client.view_distance(); @@ -135,53 +136,54 @@ impl Config for Game { } world.chunks.par_iter_mut().for_each(|(pos, chunk)| { - if chunk.created_tick() == server.current_tick() { - for z in 0..16 { - for x in 0..16 { - let block_x = x as i64 + pos.x as i64 * 16; - let block_z = z as i64 + pos.z as i64 * 16; + if chunk.created_tick() != server.shared.current_tick() { + return; + } - let mut in_terrain = false; - let mut depth = 0; + for z in 0..16 { + for x in 0..16 { + let block_x = x as i64 + pos.x as i64 * 16; + let block_z = z as i64 + pos.z as i64 * 16; - for y in (0..chunk.height()).rev() { - let b = terrain_column( - self, - block_x, - y as i64, - block_z, - &mut in_terrain, - &mut depth, + let mut in_terrain = false; + let mut depth = 0; + + for y in (0..chunk.height()).rev() { + let b = terrain_column( + self, + block_x, + y as i64, + block_z, + &mut in_terrain, + &mut depth, + ); + chunk.set_block_state(x, y, z, b); + } + + // Add grass + for y in (0..chunk.height()).rev() { + if chunk.get_block_state(x, y, z).is_air() + && chunk.get_block_state(x, y - 1, z) == BlockState::GRASS_BLOCK + { + let density = fbm( + &self.grass_noise, + [block_x, y as i64, block_z].map(|a| a as f64 / 5.0), + 4, + 2.0, + 0.7, ); - chunk.set_block_state(x, y, z, b); - } - // Add grass - for y in (0..chunk.height()).rev() { - if chunk.get_block_state(x, y, z).is_air() - && chunk.get_block_state(x, y - 1, z) == BlockState::GRASS_BLOCK - { - let density = fbm( - &self.grass_noise, - [block_x, y as i64, block_z].map(|a| a as f64 / 5.0), - 4, - 2.0, - 0.7, - ); + if density > 0.55 { + if density > 0.7 && chunk.get_block_state(x, y + 1, z).is_air() { + let upper = BlockState::TALL_GRASS + .set(PropName::Half, PropValue::Upper); + let lower = BlockState::TALL_GRASS + .set(PropName::Half, PropValue::Lower); - if density > 0.55 { - if density > 0.7 && chunk.get_block_state(x, y + 1, z).is_air() - { - let upper = BlockState::TALL_GRASS - .set(PropName::Half, PropValue::Upper); - let lower = BlockState::TALL_GRASS - .set(PropName::Half, PropValue::Lower); - - chunk.set_block_state(x, y + 1, z, upper); - chunk.set_block_state(x, y, z, lower); - } else { - chunk.set_block_state(x, y, z, BlockState::GRASS); - } + chunk.set_block_state(x, y + 1, z, upper); + chunk.set_block_state(x, y, z, lower); + } else { + chunk.set_block_state(x, y, z, BlockState::GRASS); } } } @@ -257,8 +259,8 @@ fn has_terrain_at(g: &Game, x: i64, y: i64, z: i64) -> bool { noise01(&g.hilly_noise, [x, y, z].map(|a| a as f64 / 400.0)).powi(2), ); - let lower = 15.0 + 75.0 * hilly; - let upper = lower + 125.0 * hilly; + let lower = 15.0 + 100.0 * hilly; + let upper = lower + 100.0 * hilly; if y as f64 <= lower { return true; @@ -266,7 +268,7 @@ fn has_terrain_at(g: &Game, x: i64, y: i64, z: i64) -> bool { return false; } - let density = (1.0 - lerpstep(lower, upper, y as f64)).sqrt(); + let density = 1.0 - lerpstep(lower, upper, y as f64); let n = fbm( &g.density_noise, diff --git a/src/chunk.rs b/src/chunk.rs index 73a7eaa..7fb7f1b 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -13,16 +13,16 @@ use crate::protocol::packets::play::s2c::{ BlockUpdate, LevelChunkHeightmaps, LevelChunkWithLight, S2cPlayPacket, SectionBlocksUpdate, }; use crate::protocol::{Encode, Nbt, VarInt, VarLong}; -use crate::{BiomeId, BlockPos, ChunkPos, DimensionId, Server, Ticks}; +use crate::{BiomeId, BlockPos, ChunkPos, DimensionId, SharedServer, Ticks}; pub struct Chunks { chunks: HashMap, - server: Server, + server: SharedServer, dimension: DimensionId, } impl Chunks { - pub(crate) fn new(server: Server, dimension: DimensionId) -> Self { + pub(crate) fn new(server: SharedServer, dimension: DimensionId) -> Self { Self { chunks: HashMap::new(), server, diff --git a/src/client.rs b/src/client.rs index d0e592e..9e1350e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -31,8 +31,8 @@ use crate::server::C2sPacketChannels; use crate::slotmap::{Key, SlotMap}; use crate::util::{chunks_in_view_distance, is_chunk_in_view_distance}; use crate::{ - ident, BlockPos, ChunkPos, Chunks, DimensionId, Entities, EntityId, NewClientData, Server, - SpatialIndex, Text, Ticks, WorldMeta, LIBRARY_NAMESPACE, + ident, BlockPos, ChunkPos, DimensionId, Entities, EntityId, NewClientData, SharedServer, Text, + Ticks, WorldId, Worlds, LIBRARY_NAMESPACE, }; pub struct Clients { @@ -44,7 +44,7 @@ impl Clients { Self { sm: SlotMap::new() } } - pub(crate) fn create(&mut self, client: Client) -> (ClientId, &mut Client) { + pub(crate) fn insert(&mut self, client: Client) -> (ClientId, &mut Client) { let (id, client) = self.sm.insert(client); (ClientId(id), client) } @@ -98,6 +98,8 @@ pub struct Client { uuid: Uuid, username: String, textures: Option, + new_world: Option, + old_world: Option, on_ground: bool, new_position: Vec3, old_position: Vec3, @@ -143,7 +145,7 @@ pub struct Client { impl Client { pub(crate) fn new( packet_channels: C2sPacketChannels, - server: &Server, + server: &SharedServer, ncd: NewClientData, ) -> Self { let (send, recv) = packet_channels; @@ -155,6 +157,8 @@ impl Client { uuid: ncd.uuid, username: ncd.username, textures: ncd.textures, + new_world: None, + old_world: None, on_ground: false, new_position: Vec3::default(), old_position: Vec3::default(), @@ -199,6 +203,14 @@ impl Client { self.textures.as_ref() } + pub fn world(&self) -> Option { + self.new_world + } + + pub fn set_world(&mut self, world: WorldId) { + self.new_world = Some(world); + } + /// Sends a system message to the player. pub fn send_message(&mut self, msg: impl Into) { self.msgs_to_send.push(msg.into()); @@ -554,26 +566,33 @@ impl Client { } } - pub(crate) fn update( - &mut self, - server: &Server, - entities: &Entities, - spatial_index: &SpatialIndex, - chunks: &Chunks, - meta: &WorldMeta, - ) { + pub(crate) fn update(&mut self, shared: &SharedServer, entities: &Entities, worlds: &Worlds) { // Mark the client as disconnected when appropriate. if self.recv.is_disconnected() || self.send.as_ref().map_or(true, |s| s.is_disconnected()) { self.send = None; return; } - let current_tick = server.current_tick(); + let world = match self.new_world.and_then(|id| worlds.get(id)) { + Some(world) => world, + None => { + log::warn!( + "client {} is in an invalid world and must be disconnected", + self.username() + ); + self.disconnect_no_reason(); + return; + } + }; + + let current_tick = shared.current_tick(); // Send the join game packet and other initial packets. We defer this until now // so that the user can set the client's location, game mode, etc. if self.created_tick == current_tick { - meta.player_list() + world + .meta + .player_list() .initial_packets(|pkt| self.send_packet(pkt)); self.send_packet(Login { @@ -581,16 +600,19 @@ impl Client { is_hardcore: false, // TODO gamemode: self.new_game_mode, previous_gamemode: self.old_game_mode, - dimension_names: server + dimension_names: shared .dimensions() .map(|(id, _)| ident!("{LIBRARY_NAMESPACE}:dimension_{}", id.0)) .collect(), - registry_codec: Nbt(make_dimension_codec(server)), + registry_codec: Nbt(make_dimension_codec(shared)), dimension_type_name: ident!( "{LIBRARY_NAMESPACE}:dimension_type_{}", - meta.dimension().0 + world.meta.dimension().0 + ), + dimension_name: ident!( + "{LIBRARY_NAMESPACE}:dimension_{}", + world.meta.dimension().0 ), - dimension_name: ident!("{LIBRARY_NAMESPACE}:dimension_{}", meta.dimension().0), hashed_seed: 0, max_players: VarInt(0), view_distance: BoundedInt(VarInt(self.new_max_view_distance as i32)), @@ -598,7 +620,7 @@ impl Client { reduced_debug_info: false, enable_respawn_screen: false, is_debug: false, - is_flat: meta.is_flat(), + is_flat: world.meta.is_flat(), last_death_location: self .death_location .map(|(id, pos)| (ident!("{LIBRARY_NAMESPACE}:dimension_{}", id.0), pos)), @@ -606,6 +628,13 @@ impl Client { self.teleport(self.position(), self.yaw(), self.pitch()); } else { + if self.new_world != self.old_world { + self.loaded_entities.clear(); + self.loaded_chunks.clear(); + + todo!("send respawn packet"); + } + if self.old_game_mode != self.new_game_mode { self.old_game_mode = self.new_game_mode; self.send_packet(GameEvent { @@ -614,7 +643,10 @@ impl Client { }); } - meta.player_list().diff_packets(|pkt| self.send_packet(pkt)); + world + .meta + .player_list() + .diff_packets(|pkt| self.send_packet(pkt)); } // Update the players spawn position (compass position) @@ -638,7 +670,7 @@ impl Client { } // Check if it's time to send another keepalive. - if current_tick % (server.tick_rate() * 8) == 0 { + if current_tick % (shared.tick_rate() * 8) == 0 { if self.got_keepalive { let id = rand::random(); self.send_packet(KeepAlive { id }); @@ -671,7 +703,7 @@ impl Client { } } - let dimension = server.dimension(meta.dimension()); + let dimension = shared.dimension(world.meta.dimension()); // Update existing chunks and unload those outside the view distance. Chunks // that have been overwritten also need to be unloaded. @@ -680,7 +712,7 @@ impl Client { // moves to an adjacent chunk and back to the original. let cache = 2; - if let Some(chunk) = chunks.get(pos) { + if let Some(chunk) = world.chunks.get(pos) { if is_chunk_in_view_distance(center, pos, view_dist + cache) && chunk.created_tick() != current_tick { @@ -703,7 +735,7 @@ impl Client { // Load new chunks within the view distance for pos in chunks_in_view_distance(center, view_dist) { - if let Some(chunk) = chunks.get(pos) { + if let Some(chunk) = world.chunks.get(pos) { if self.loaded_chunks.insert(pos) { self.send_packet(chunk.chunk_data_packet(pos)); chunk.block_change_packets(pos, dimension.min_y, |pkt| self.send_packet(pkt)); @@ -866,7 +898,7 @@ impl Client { // Spawn new entities within the view distance. let pos = self.position(); - spatial_index.query::<_, _, ()>( + world.spatial_index.query::<_, _, ()>( |bb| bb.projected_point(pos).distance(pos) <= view_dist as f64 * 16.0, |id, _| { if self.loaded_entities.insert(id) { @@ -888,6 +920,7 @@ impl Client { ); self.old_position = self.new_position; + self.old_world = self.new_world; } } @@ -906,9 +939,9 @@ fn send_packet(send_opt: &mut Option>, pkt: impl Into RegistryCodec { +fn make_dimension_codec(shared: &SharedServer) -> RegistryCodec { let mut dims = Vec::new(); - for (id, dim) in server.dimensions() { + for (id, dim) in shared.dimensions() { let id = id.0 as i32; dims.push(DimensionTypeRegistryEntry { name: ident!("{LIBRARY_NAMESPACE}:dimension_type_{id}"), @@ -918,7 +951,7 @@ fn make_dimension_codec(server: &Server) -> RegistryCodec { } let mut biomes = Vec::new(); - for (id, biome) in server.biomes() { + for (id, biome) in shared.biomes() { biomes.push(to_biome_registry_item(biome, id.0 as i32)); } diff --git a/src/config.rs b/src/config.rs index 4acf897..f0c6a58 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,7 +5,7 @@ use std::panic::{RefUnwindSafe, UnwindSafe}; use async_trait::async_trait; use tokio::runtime::Handle as TokioHandle; -use crate::{Biome, Client, Dimension, NewClientData, Server, Text, Ticks, WorldId, Worlds}; +use crate::{Biome, Dimension, NewClientData, Server, SharedServer, Text, Ticks}; /// A trait containing callbacks which are invoked by the running Minecraft /// server. @@ -151,7 +151,11 @@ pub trait Config: Any + Send + Sync + UnwindSafe + RefUnwindSafe { /// /// # Default Implementation /// The query is ignored. - async fn server_list_ping(&self, server: &Server, remote_addr: SocketAddr) -> ServerListPing { + async fn server_list_ping( + &self, + shared: &SharedServer, + remote_addr: SocketAddr, + ) -> ServerListPing { ServerListPing::Ignore } @@ -163,31 +167,16 @@ pub trait Config: Any + Send + Sync + UnwindSafe + RefUnwindSafe { /// /// This method is the appropriate place to perform asynchronous /// operations such as database queries which may take some time to - /// complete. If you need access to the worlds on the server and don't need - /// async, see [`Config::join`]. + /// complete. /// /// This method is called from within a tokio runtime. /// /// # Default Implementation /// The client is allowed to join unconditionally. - async fn login(&self, server: &Server, ncd: &NewClientData) -> Result<(), Text> { + async fn login(&self, shared: &SharedServer, ncd: &NewClientData) -> Result<(), Text> { Ok(()) } - /// Called after a successful [`Config::login`] to determine what world the - /// new client should join. If this method returns with `Err(reason)`, then - /// the client is immediately disconnected with the given reason. - /// - /// If the returned [`WorldId`] is invalid, then the client is disconnected. - /// - /// This method is called from within a tokio runtime. - fn join( - &self, - server: &Server, - client: &mut Client, - worlds: &mut Worlds, - ) -> Result; - /// Called after the server is created, but prior to accepting connections /// and entering the update loop. /// @@ -195,7 +184,7 @@ pub trait Config: Any + Send + Sync + UnwindSafe + RefUnwindSafe { /// no connections to the server will be made until this function returns. /// /// This method is called from within a tokio runtime. - fn init(&self, server: &Server, worlds: &mut Worlds) {} + fn init(&self, server: &mut Server) {} /// Called once at the beginning of every server update (also known as /// a "tick"). @@ -207,7 +196,7 @@ pub trait Config: Any + Send + Sync + UnwindSafe + RefUnwindSafe { /// /// # Default Implementation /// The default implementation does nothing. - fn update(&self, server: &Server, worlds: &mut Worlds); + fn update(&self, server: &mut Server); } /// The result of the [`server_list_ping`](Handler::server_list_ping) callback. diff --git a/src/entity.rs b/src/entity.rs index f23b37b..2841dce 100644 --- a/src/entity.rs +++ b/src/entity.rs @@ -18,6 +18,7 @@ use crate::protocol::packets::play::s2c::{ use crate::protocol::{ByteAngle, RawBytes, VarInt}; use crate::slotmap::{Key, SlotMap}; use crate::util::aabb_from_bottom_and_size; +use crate::WorldId; pub struct Entities { sm: SlotMap, @@ -56,6 +57,7 @@ impl Entities { let (k, e) = self.sm.insert(Entity { flags: EntityFlags(0), meta: EntityMeta::new(EntityType::Marker), + world: None, new_position: Vec3::default(), old_position: Vec3::default(), yaw: 0.0, @@ -174,6 +176,7 @@ impl EntityId { pub struct Entity { flags: EntityFlags, meta: EntityMeta, + world: Option, new_position: Vec3, old_position: Vec3, yaw: f32, @@ -226,6 +229,14 @@ impl Entity { self.flags.set_type_modified(true); } + pub fn world(&self) -> Option { + self.world + } + + pub fn set_world(&mut self, world: impl Into>) { + self.world = world.into(); + } + /// Returns the position of this entity in the world it inhabits. pub fn position(&self) -> Vec3 { self.new_position diff --git a/src/lib.rs b/src/lib.rs index 350b92a..8d49407 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,7 +43,7 @@ pub use config::Config; pub use dimension::{Dimension, DimensionId}; pub use entity::{Entities, Entity, EntityId, EntityType}; pub use ident::Ident; -pub use server::{start_server, NewClientData, Server, ShutdownResult}; +pub use server::{start_server, NewClientData, Server, SharedServer, ShutdownResult}; pub use spatial_index::SpatialIndex; pub use text::{Text, TextFormat}; pub use uuid::Uuid; diff --git a/src/server.rs b/src/server.rs index 239cdef..9920eb9 100644 --- a/src/server.rs +++ b/src/server.rs @@ -40,16 +40,24 @@ use crate::protocol::{BoundedArray, BoundedString, VarInt}; use crate::util::valid_username; use crate::world::Worlds; use crate::{ - Biome, BiomeId, Client, Dimension, DimensionId, Ticks, PROTOCOL_VERSION, VERSION_NAME, + Biome, BiomeId, Client, Clients, Dimension, DimensionId, Entities, Ticks, PROTOCOL_VERSION, + VERSION_NAME, }; +pub struct Server { + pub shared: SharedServer, + pub clients: Clients, + pub entities: Entities, + pub worlds: Worlds, +} + /// A handle to a running Minecraft server containing state which is accessible /// outside the update loop. Servers are internally refcounted and can be shared /// between threads. #[derive(Clone)] -pub struct Server(Arc); +pub struct SharedServer(Arc); -struct ServerInner { +struct SharedServerInner { cfg: Box, address: SocketAddr, tick_rate: Ticks, @@ -104,7 +112,7 @@ pub type ShutdownError = Box; pub(crate) type S2cPacketChannels = (Sender, Receiver); pub(crate) type C2sPacketChannels = (Sender, Receiver); -impl Server { +impl SharedServer { pub fn config(&self) -> &(impl Config + ?Sized) { self.0.cfg.as_ref() } @@ -207,20 +215,25 @@ impl Server { /// The function returns when the server has shut down, a runtime error /// occurs, or the configuration is invalid. pub fn start_server(config: impl Config) -> ShutdownResult { - let server = setup_server(config).map_err(ShutdownError::from)?; + let shared = setup_server(config).map_err(ShutdownError::from)?; - let _guard = server.tokio_handle().enter(); + let _guard = shared.tokio_handle().enter(); - let mut worlds = Worlds::new(server.clone()); + let mut server = Server { + shared: shared.clone(), + clients: Clients::new(), + entities: Entities::new(), + worlds: Worlds::new(shared.clone()), + }; - server.config().init(&server, &mut worlds); + shared.config().init(&mut server); - tokio::spawn(do_accept_loop(server.clone())); + tokio::spawn(do_accept_loop(shared)); - do_update_loop(server, &mut worlds) + do_update_loop(&mut server) } -fn setup_server(cfg: impl Config) -> anyhow::Result { +fn setup_server(cfg: impl Config) -> anyhow::Result { let max_connections = cfg.max_connections(); let address = cfg.address(); let tick_rate = cfg.tick_rate(); @@ -320,7 +333,7 @@ fn setup_server(cfg: impl Config) -> anyhow::Result { None => tokio_handle.unwrap(), }; - let server = ServerInner { + let server = SharedServerInner { cfg: Box::new(cfg), address, tick_rate, @@ -343,33 +356,32 @@ fn setup_server(cfg: impl Config) -> anyhow::Result { http_client: HttpClient::new(), }; - Ok(Server(Arc::new(server))) + Ok(SharedServer(Arc::new(server))) } -fn do_update_loop(server: Server, worlds: &mut Worlds) -> ShutdownResult { +fn do_update_loop(server: &mut Server) -> ShutdownResult { let mut tick_start = Instant::now(); + let shared = server.shared.clone(); loop { - if let Some(res) = server.0.shutdown_result.lock().unwrap().take() { + if let Some(res) = shared.0.shutdown_result.lock().unwrap().take() { return res; } - while let Ok(msg) = server.0.new_clients_rx.try_recv() { - join_player(&server, worlds, msg); + while let Ok(msg) = shared.0.new_clients_rx.try_recv() { + join_player(server, msg); } // Get serverbound packets first so they are not dealt with a tick late. - worlds.par_iter_mut().for_each(|(_, world)| { - world.clients.par_iter_mut().for_each(|(_, client)| { - client.handle_serverbound_packets(&world.entities); - }); + server.clients.par_iter_mut().for_each(|(_, client)| { + client.handle_serverbound_packets(&server.entities); }); - server.config().update(&server, worlds); + shared.config().update(server); - worlds.par_iter_mut().for_each(|(_, world)| { + server.worlds.par_iter_mut().for_each(|(id, world)| { world.chunks.par_iter_mut().for_each(|(_, chunk)| { - if chunk.created_tick() == server.current_tick() { + if chunk.created_tick() == shared.current_tick() { // Chunks created this tick can have their changes applied immediately because // they have not been observed by clients yet. Clients will not have to be sent // the block change packet in this case. @@ -377,20 +389,16 @@ fn do_update_loop(server: Server, worlds: &mut Worlds) -> ShutdownResult { } }); - world.spatial_index.update(&world.entities); + world.spatial_index.update(&server.entities, id); + }); - world.clients.par_iter_mut().for_each(|(_, client)| { - client.update( - &server, - &world.entities, - &world.spatial_index, - &world.chunks, - &world.meta, - ); - }); + server.clients.par_iter_mut().for_each(|(_, client)| { + client.update(&shared, &server.entities, &server.worlds); + }); - world.entities.update(); + server.entities.update(); + server.worlds.par_iter_mut().for_each(|(_, world)| { world.chunks.par_iter_mut().for_each(|(_, chunk)| { chunk.apply_modifications(); }); @@ -399,53 +407,43 @@ fn do_update_loop(server: Server, worlds: &mut Worlds) -> ShutdownResult { }); // Sleep for the remainder of the tick. - let tick_duration = Duration::from_secs_f64((server.0.tick_rate as f64).recip()); + let tick_duration = Duration::from_secs_f64((shared.0.tick_rate as f64).recip()); thread::sleep(tick_duration.saturating_sub(tick_start.elapsed())); tick_start = Instant::now(); - server.0.tick_counter.fetch_add(1, Ordering::SeqCst); + shared.0.tick_counter.fetch_add(1, Ordering::SeqCst); } } -fn join_player(server: &Server, worlds: &mut Worlds, msg: NewClientMessage) { - let (clientbound_tx, clientbound_rx) = flume::bounded(server.0.outgoing_packet_capacity); - let (serverbound_tx, serverbound_rx) = flume::bounded(server.0.incoming_packet_capacity); +fn join_player(server: &mut Server, msg: NewClientMessage) { + let (clientbound_tx, clientbound_rx) = flume::bounded(server.shared.0.outgoing_packet_capacity); + let (serverbound_tx, serverbound_rx) = flume::bounded(server.shared.0.incoming_packet_capacity); let s2c_packet_channels: S2cPacketChannels = (serverbound_tx, clientbound_rx); let c2s_packet_channels: C2sPacketChannels = (clientbound_tx, serverbound_rx); let _ = msg.reply.send(s2c_packet_channels); - let mut client = Client::new(c2s_packet_channels, server, msg.ncd); + let client = Client::new(c2s_packet_channels, &server.shared, msg.ncd); - match server.0.cfg.join(server, &mut client, worlds) { - Ok(world_id) => { - if let Some(world) = worlds.get_mut(world_id) { - if world.entities.get_with_uuid(client.uuid()).is_none() { - world.clients.create(client); - } else { - log::warn!( - "client '{}' cannot join the server because their UUID ({}) conflicts \ - with an existing entity", - client.username(), - client.uuid() - ); - } - } else { - log::warn!( - "client '{}' cannot join the server because the WorldId returned by \ - Config::join is invalid.", - client.username() - ); - } - } - Err(errmsg) => client.disconnect(errmsg), + if server.entities.get_with_uuid(client.uuid()).is_none() { + server.clients.insert(client); + } else { + log::warn!( + "client '{}' cannot join the server because their UUID ({}) conflicts with an \ + existing entity", + client.username(), + client.uuid() + ); } } -type Codec = (Encoder, Decoder); +struct Codec { + enc: Encoder, + dec: Decoder, +} -async fn do_accept_loop(server: Server) { +async fn do_accept_loop(server: SharedServer) { log::trace!("entering accept loop"); let listener = match TcpListener::bind(server.0.address).await { @@ -485,18 +483,21 @@ async fn do_accept_loop(server: Server) { } async fn handle_connection( - server: Server, + server: SharedServer, stream: TcpStream, remote_addr: SocketAddr, ) -> anyhow::Result<()> { let timeout = Duration::from_secs(10); let (read, write) = stream.into_split(); - let mut c: Codec = (Encoder::new(write, timeout), Decoder::new(read, timeout)); + let mut c = Codec { + enc: Encoder::new(write, timeout), + dec: Decoder::new(read, timeout), + }; // TODO: peek stream for 0xFE legacy ping - match c.1.read_packet::().await?.next_state { + match c.dec.read_packet::().await?.next_state { HandshakeNextState::Status => handle_status(server, &mut c, remote_addr) .await .context("error during status"), @@ -513,11 +514,11 @@ async fn handle_connection( } async fn handle_status( - server: Server, + server: SharedServer, c: &mut Codec, remote_addr: SocketAddr, ) -> anyhow::Result<()> { - c.1.read_packet::().await?; + c.dec.read_packet::().await?; match server.0.cfg.server_list_ping(&server, remote_addr).await { ServerListPing::Respond { @@ -547,48 +548,50 @@ async fn handle_status( .insert("favicon".to_string(), Value::String(buf)); } - c.0.write_packet(&StatusResponse { - json_response: json.to_string(), - }) - .await?; + c.enc + .write_packet(&StatusResponse { + json_response: json.to_string(), + }) + .await?; } ServerListPing::Ignore => return Ok(()), } - let PingRequest { payload } = c.1.read_packet().await?; + let PingRequest { payload } = c.dec.read_packet().await?; - c.0.write_packet(&PongResponse { payload }).await?; + c.enc.write_packet(&PongResponse { payload }).await?; Ok(()) } /// Handle the login process and return the new player's data if successful. async fn handle_login( - server: &Server, + server: &SharedServer, c: &mut Codec, remote_addr: SocketAddr, ) -> anyhow::Result> { let LoginStart { username: BoundedString(username), sig_data: _, // TODO - } = c.1.read_packet().await?; + } = c.dec.read_packet().await?; ensure!(valid_username(&username), "invalid username '{username}'"); let (uuid, textures) = if server.0.online_mode { let my_verify_token: [u8; 16] = rand::random(); - c.0.write_packet(&EncryptionRequest { - server_id: Default::default(), // Always empty - public_key: server.0.public_key_der.to_vec(), - verify_token: my_verify_token.to_vec().into(), - }) - .await?; + c.enc + .write_packet(&EncryptionRequest { + server_id: Default::default(), // Always empty + public_key: server.0.public_key_der.to_vec(), + verify_token: my_verify_token.to_vec().into(), + }) + .await?; let EncryptionResponse { shared_secret: BoundedArray(encrypted_shared_secret), token_or_sig, - } = c.1.read_packet().await?; + } = c.dec.read_packet().await?; let shared_secret = server .0 @@ -618,8 +621,8 @@ async fn handle_login( .try_into() .context("shared secret has the wrong length")?; - c.0.enable_encryption(&crypt_key); - c.1.enable_encryption(&crypt_key); + c.enc.enable_encryption(&crypt_key); + c.dec.enable_encryption(&crypt_key); #[derive(Debug, Deserialize)] struct AuthResponse { @@ -667,13 +670,14 @@ async fn handle_login( }; let compression_threshold = 256; - c.0.write_packet(&SetCompression { - threshold: VarInt(compression_threshold as i32), - }) - .await?; + c.enc + .write_packet(&SetCompression { + threshold: VarInt(compression_threshold as i32), + }) + .await?; - c.0.enable_compression(compression_threshold); - c.1.enable_compression(compression_threshold); + c.enc.enable_compression(compression_threshold); + c.dec.enable_compression(compression_threshold); let npd = NewClientData { uuid, @@ -684,21 +688,24 @@ async fn handle_login( if let Err(reason) = server.0.cfg.login(server, &npd).await { log::info!("Disconnect at login: \"{reason}\""); - c.0.write_packet(&login::s2c::Disconnect { reason }).await?; + c.enc + .write_packet(&login::s2c::Disconnect { reason }) + .await?; return Ok(None); } - c.0.write_packet(&LoginSuccess { - uuid: npd.uuid, - username: npd.username.clone().into(), - properties: Vec::new(), - }) - .await?; + c.enc + .write_packet(&LoginSuccess { + uuid: npd.uuid, + username: npd.username.clone().into(), + properties: Vec::new(), + }) + .await?; Ok(Some(npd)) } -async fn handle_play(server: &Server, c: Codec, ncd: NewClientData) -> anyhow::Result<()> { +async fn handle_play(server: &SharedServer, c: Codec, ncd: NewClientData) -> anyhow::Result<()> { let (reply_tx, reply_rx) = oneshot::channel(); server @@ -715,11 +722,11 @@ async fn handle_play(server: &Server, c: Codec, ncd: NewClientData) -> anyhow::R Err(_) => return Ok(()), // Server closed }; - let (mut encoder, mut decoder) = c; + let Codec { mut enc, mut dec } = c; tokio::spawn(async move { while let Ok(pkt) = packet_rx.recv_async().await { - if let Err(e) = encoder.write_packet(&pkt).await { + if let Err(e) = enc.write_packet(&pkt).await { log::debug!("error while sending play packet: {e:#}"); break; } @@ -727,7 +734,7 @@ async fn handle_play(server: &Server, c: Codec, ncd: NewClientData) -> anyhow::R }); loop { - let pkt = decoder.read_packet().await?; + let pkt = dec.read_packet().await?; if packet_tx.send_async(pkt).await.is_err() { break; } diff --git a/src/spatial_index.rs b/src/spatial_index.rs index c157b77..3b4291d 100644 --- a/src/spatial_index.rs +++ b/src/spatial_index.rs @@ -2,7 +2,7 @@ use vek::{Aabb, Vec3}; use crate::bvh::Bvh; pub use crate::bvh::TraverseStep; -use crate::{Entities, EntityId}; +use crate::{Entities, EntityId, WorldId}; pub struct SpatialIndex { bvh: Bvh, @@ -96,9 +96,13 @@ impl SpatialIndex { ) } - pub(crate) fn update(&mut self, entities: &Entities) { - self.bvh - .build(entities.iter().map(|(id, e)| (id, e.hitbox()))) + pub(crate) fn update(&mut self, entities: &Entities, id: WorldId) { + self.bvh.build( + entities + .iter() + .filter(|(_, e)| e.world() == Some(id)) + .map(|(id, e)| (id, e.hitbox())), + ) } } diff --git a/src/util.rs b/src/util.rs index 5ed61b1..e3f4974 100644 --- a/src/util.rs +++ b/src/util.rs @@ -66,8 +66,8 @@ pub fn to_yaw_and_pitch(d: Vec3) -> (f32, f32) { (yaw, pitch) } -// /// Takes yaw and pitch angles (in degrees) and returns a normalized direction -// /// vector. +// /// Takes yaw and pitch angles (in degrees) and returns a normalized +// direction /// vector. // /// // /// This function is the inverse of [`to_yaw_and_pitch`]. // pub fn from_yaw_and_pitch(yaw: f32, pitch: f32) -> Vec3 { diff --git a/src/world.rs b/src/world.rs index 5cd4a73..fa09b23 100644 --- a/src/world.rs +++ b/src/world.rs @@ -4,18 +4,18 @@ use rayon::iter::ParallelIterator; use crate::player_list::PlayerList; use crate::slotmap::{Key, SlotMap}; -use crate::{Chunks, Clients, DimensionId, Entities, Server, SpatialIndex}; +use crate::{Chunks, DimensionId, SharedServer, SpatialIndex}; pub struct Worlds { sm: SlotMap, - server: Server, + server: SharedServer, } #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct WorldId(Key); impl Worlds { - pub(crate) fn new(server: Server) -> Self { + pub(crate) fn new(server: SharedServer) -> Self { Self { sm: SlotMap::new(), server, @@ -24,8 +24,6 @@ impl Worlds { pub fn create(&mut self, dim: DimensionId) -> (WorldId, &mut World) { let (id, world) = self.sm.insert(World { - clients: Clients::new(), - entities: Entities::new(), spatial_index: SpatialIndex::new(), chunks: Chunks::new(self.server.clone(), dim), meta: WorldMeta { @@ -81,8 +79,6 @@ impl Worlds { } pub struct World { - pub clients: Clients, - pub entities: Entities, pub spatial_index: SpatialIndex, pub chunks: Chunks, pub meta: WorldMeta,