diff --git a/src/biome.rs b/src/biome.rs index ad2b99c..afc26de 100644 --- a/src/biome.rs +++ b/src/biome.rs @@ -1,5 +1,8 @@ //! Biome configuration and identification. +use std::collections::HashSet; + +use anyhow::ensure; use valence_nbt::{compound, Compound}; use crate::ident; @@ -136,6 +139,27 @@ impl Biome { } } +pub(crate) fn validate_biomes(biomes: &[Biome]) -> anyhow::Result<()> { + ensure!(!biomes.is_empty(), "at least one biome must be present"); + + ensure!( + biomes.len() <= u16::MAX as _, + "more than u16::MAX biomes present" + ); + + let mut names = HashSet::new(); + + for biome in biomes { + ensure!( + names.insert(biome.name.clone()), + "biome \"{}\" already exists", + biome.name + ); + } + + Ok(()) +} + impl Default for Biome { fn default() -> Self { Self { diff --git a/src/client.rs b/src/client.rs index 099866b..c1ca124 100644 --- a/src/client.rs +++ b/src/client.rs @@ -10,10 +10,8 @@ pub use event::*; use flume::{Receiver, Sender, TrySendError}; use rayon::iter::ParallelIterator; use uuid::Uuid; -use valence_nbt::{compound, Compound, List}; use vek::Vec3; -use crate::biome::Biome; use crate::block_pos::BlockPos; use crate::chunk_pos::ChunkPos; use crate::config::Config; @@ -1069,7 +1067,7 @@ impl Client { gamemode: self.new_game_mode, previous_gamemode: self.old_game_mode, dimension_names, - registry_codec: make_registry_codec(shared), + registry_codec: shared.registry_codec().clone(), dimension_type_name: world.meta.dimension().dimension_type_name(), dimension_name: world.meta.dimension().dimension_name(), hashed_seed: 0, @@ -1536,41 +1534,3 @@ fn send_entity_events(send_opt: &mut SendOpt, entity_id: i32, events: &[entity:: } } } - -fn make_registry_codec(shared: &SharedServer) -> Compound { - compound! { - ident!("dimension_type") => compound! { - "type" => ident!("dimension_type"), - "value" => List::Compound(shared.dimensions().map(|(id, dim)| compound! { - "name" => id.dimension_type_name(), - "id" => id.0 as i32, - "element" => dim.to_dimension_registry_item(), - }).collect()), - }, - ident!("worldgen/biome") => compound! { - "type" => ident!("worldgen/biome"), - "value" => { - let mut biomes: Vec<_> = shared - .biomes() - .map(|(id, biome)| biome.to_biome_registry_item(id.0 as i32)) - .collect(); - - // The client needs a biome named "minecraft:plains" in the registry to - // connect. This is probably a bug in the client. - // - // If the issue is resolved, remove this if. - if !biomes.iter().any(|b| b["name"] == "plains".into()) { - let biome = Biome::default(); - assert_eq!(biome.name, ident!("plains")); - biomes.push(biome.to_biome_registry_item(biomes.len() as i32)); - } - - List::Compound(biomes) - } - }, - ident!("chat_type_registry") => compound! { - "type" => ident!("chat_type"), - "value" => List::Compound(Vec::new()), - }, - } -} diff --git a/src/config.rs b/src/config.rs index 0842f8c..1dd29b3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -165,7 +165,7 @@ pub trait Config: Sized + Send + Sync + UnwindSafe + RefUnwindSafe + 'static { /// /// # Default Implementation /// - /// Returns `vec![Dimension::default()]`. + /// Returns `vec![Biome::default()]`. fn biomes(&self) -> Vec { vec![Biome::default()] } diff --git a/src/dimension.rs b/src/dimension.rs index c314b31..c698436 100644 --- a/src/dimension.rs +++ b/src/dimension.rs @@ -1,5 +1,6 @@ //! Dimension configuration and identification. +use anyhow::ensure; use valence_nbt::{compound, Compound}; use crate::ident::Ident; @@ -117,6 +118,46 @@ impl Dimension { } } +pub(crate) fn validate_dimensions(dimensions: &[Dimension]) -> anyhow::Result<()> { + ensure!( + !dimensions.is_empty(), + "at least one dimension must be present" + ); + + ensure!( + dimensions.len() <= u16::MAX as usize, + "more than u16::MAX dimensions present" + ); + + for (i, dim) in dimensions.iter().enumerate() { + ensure!( + dim.min_y % 16 == 0 && (-2032..=2016).contains(&dim.min_y), + "invalid min_y in dimension #{i}", + ); + + ensure!( + dim.height % 16 == 0 + && (0..=4064).contains(&dim.height) + && dim.min_y.saturating_add(dim.height) <= 2032, + "invalid height in dimension #{i}", + ); + + ensure!( + (0.0..=1.0).contains(&dim.ambient_light), + "ambient_light is out of range in dimension #{i}", + ); + + if let Some(fixed_time) = dim.fixed_time { + ensure!( + (0..=24_000).contains(&fixed_time), + "fixed_time is out of range in dimension #{i}", + ); + } + } + + Ok(()) +} + impl Default for Dimension { fn default() -> Self { Self { diff --git a/src/server.rs b/src/server.rs index cdf72ed..1907448 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,6 +1,5 @@ //! The heart of the server. -use std::collections::HashSet; use std::error::Error; use std::iter::FusedIterator; use std::net::SocketAddr; @@ -26,11 +25,12 @@ use tokio::net::{TcpListener, TcpStream}; use tokio::runtime::{Handle, Runtime}; use tokio::sync::{oneshot, Semaphore}; use uuid::Uuid; +use valence_nbt::{compound, Compound, List}; -use crate::biome::{Biome, BiomeId}; +use crate::biome::{validate_biomes, Biome, BiomeId}; use crate::client::{Client, Clients}; use crate::config::{Config, ServerListPing}; -use crate::dimension::{Dimension, DimensionId}; +use crate::dimension::{validate_dimensions, Dimension, DimensionId}; use crate::entity::Entities; use crate::player_list::PlayerLists; use crate::player_textures::SignedPlayerTextures; @@ -48,7 +48,7 @@ use crate::protocol::packets::Property; use crate::protocol::{BoundedArray, BoundedString, VarInt}; use crate::util::valid_username; use crate::world::Worlds; -use crate::{Ticks, PROTOCOL_VERSION, VERSION_NAME}; +use crate::{ident, Ticks, PROTOCOL_VERSION, VERSION_NAME}; /// Contains the entire state of a running Minecraft server, accessible from /// within the [update](crate::config::Config::update) loop. @@ -95,6 +95,9 @@ struct SharedServerInner { _tokio_runtime: Option, dimensions: Vec, biomes: Vec, + /// Contains info about dimensions, biomes, and chats. + /// Sent to all clients when joining. + registry_codec: Compound, /// The instant the server was started. start_instant: Instant, /// Receiver for new clients past the login stage. @@ -241,6 +244,10 @@ impl SharedServer { .map(|(i, b)| (BiomeId(i as u16), b)) } + pub(crate) fn registry_codec(&self) -> &Compound { + &self.0.registry_codec + } + /// Returns the instant the server was started. pub fn start_instant(&self) -> Instant { self.0.start_instant @@ -315,62 +322,12 @@ fn setup_server(cfg: C) -> anyhow::Result> { ); let tokio_handle = cfg.tokio_handle(); + let dimensions = cfg.dimensions(); - - ensure!( - !dimensions.is_empty(), - "at least one dimension must be added" - ); - - ensure!( - dimensions.len() <= u16::MAX as usize, - "more than u16::MAX dimensions added" - ); - - for (i, dim) in dimensions.iter().enumerate() { - ensure!( - dim.min_y % 16 == 0 && (-2032..=2016).contains(&dim.min_y), - "invalid min_y in dimension #{i}", - ); - - ensure!( - dim.height % 16 == 0 - && (0..=4064).contains(&dim.height) - && dim.min_y.saturating_add(dim.height) <= 2032, - "invalid height in dimension #{i}", - ); - - ensure!( - (0.0..=1.0).contains(&dim.ambient_light), - "ambient_light is out of range in dimension #{i}", - ); - - if let Some(fixed_time) = dim.fixed_time { - assert!( - (0..=24_000).contains(&fixed_time), - "fixed_time is out of range in dimension #{i}", - ); - } - } + validate_dimensions(&dimensions)?; let biomes = cfg.biomes(); - - ensure!(!biomes.is_empty(), "at least one biome must be added"); - - ensure!( - biomes.len() <= u16::MAX as usize, - "more than u16::MAX biomes added" - ); - - let mut names = HashSet::new(); - - for biome in biomes.iter() { - ensure!( - names.insert(biome.name.clone()), - "biome \"{}\" already added", - biome.name - ); - } + validate_biomes(&biomes)?; let rsa_key = RsaPrivateKey::new(&mut OsRng, 1024)?; @@ -391,6 +348,8 @@ fn setup_server(cfg: C) -> anyhow::Result> { None => tokio_handle.unwrap(), }; + let registry_codec = make_registry_codec(&dimensions, &biomes); + let server = SharedServerInner { cfg, address, @@ -403,6 +362,7 @@ fn setup_server(cfg: C) -> anyhow::Result> { _tokio_runtime: runtime, dimensions, biomes, + registry_codec, start_instant: Instant::now(), new_clients_rx, new_clients_tx, @@ -417,6 +377,45 @@ fn setup_server(cfg: C) -> anyhow::Result> { Ok(SharedServer(Arc::new(server))) } +fn make_registry_codec(dimensions: &[Dimension], biomes: &[Biome]) -> Compound { + compound! { + ident!("dimension_type") => compound! { + "type" => ident!("dimension_type"), + "value" => List::Compound(dimensions.iter().enumerate().map(|(id, dim)| compound! { + "name" => DimensionId(id as u16).dimension_type_name(), + "id" => id as i32, + "element" => dim.to_dimension_registry_item(), + }).collect()), + }, + ident!("worldgen/biome") => compound! { + "type" => ident!("worldgen/biome"), + "value" => { + let mut biomes: Vec<_> = biomes + .iter() + .enumerate() + .map(|(id, biome)| biome.to_biome_registry_item(id as i32)) + .collect(); + + // The client needs a biome named "minecraft:plains" in the registry to + // connect. This is probably a bug in the client. + // + // If the issue is resolved, remove this if. + if !biomes.iter().any(|b| b["name"] == "plains".into()) { + let biome = Biome::default(); + assert_eq!(biome.name, ident!("plains")); + biomes.push(biome.to_biome_registry_item(biomes.len() as i32)); + } + + List::Compound(biomes) + } + }, + ident!("chat_type_registry") => compound! { + "type" => ident!("chat_type"), + "value" => List::Compound(Vec::new()), + }, + } +} + fn do_update_loop(server: &mut Server) -> ShutdownResult { let mut tick_start = Instant::now();