mirror of
https://github.com/italicsjenga/valence.git
synced 2024-12-25 23:31:30 +11:00
523 lines
16 KiB
Rust
523 lines
16 KiB
Rust
|
// TODO: rate limit, view distance?
|
||
|
|
||
|
use std::any::Any;
|
||
|
use std::collections::HashSet;
|
||
|
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
|
||
|
use std::sync::Arc;
|
||
|
use std::time::Duration;
|
||
|
|
||
|
use anyhow::ensure;
|
||
|
use async_trait::async_trait;
|
||
|
use tokio::runtime::Handle as TokioHandle;
|
||
|
|
||
|
use crate::server::{start_server, ShutdownError};
|
||
|
use crate::{ident, Identifier, NewClientData, Server, SharedServer, ShutdownResult, Text};
|
||
|
|
||
|
/// A builder type used to configure and start the server.
|
||
|
pub struct ServerConfig {
|
||
|
pub(crate) handler: Option<Box<dyn Handler>>,
|
||
|
pub(crate) address: SocketAddr,
|
||
|
pub(crate) update_duration: Duration,
|
||
|
pub(crate) online_mode: bool,
|
||
|
pub(crate) max_clients: usize,
|
||
|
pub(crate) clientbound_packet_capacity: usize,
|
||
|
pub(crate) serverbound_packet_capacity: usize,
|
||
|
pub(crate) tokio_handle: Option<TokioHandle>,
|
||
|
pub(crate) dimensions: Vec<Dimension>,
|
||
|
pub(crate) biomes: Vec<Biome>,
|
||
|
}
|
||
|
|
||
|
impl ServerConfig {
|
||
|
/// Constructs a new server configuration with the provided handler.
|
||
|
pub fn new() -> Self {
|
||
|
Self {
|
||
|
handler: None,
|
||
|
address: SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 25565).into(),
|
||
|
update_duration: Duration::from_secs_f64(1.0 / 20.0),
|
||
|
online_mode: false,
|
||
|
max_clients: 32,
|
||
|
clientbound_packet_capacity: 128,
|
||
|
serverbound_packet_capacity: 32,
|
||
|
tokio_handle: None,
|
||
|
dimensions: Vec::new(),
|
||
|
biomes: Vec::new(),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Sets the [`Handler`] to use for this server.
|
||
|
pub fn handler(&mut self, handler: impl Handler) {
|
||
|
self.handler = Some(Box::new(handler));
|
||
|
}
|
||
|
|
||
|
/// Sets the socket address that the server will be bound to.
|
||
|
///
|
||
|
/// The default is `127.0.0.1:25565`.
|
||
|
pub fn address(&mut self, addr: impl Into<SocketAddr>) {
|
||
|
self.address = addr.into();
|
||
|
}
|
||
|
|
||
|
/// Sets the duration of each game update.
|
||
|
///
|
||
|
/// On each game update (a.k.a. tick), the server is expected to update game
|
||
|
/// logic and respond to packets from clients. Once this is complete,
|
||
|
/// the server will sleep for any remaining time until the full update
|
||
|
/// duration has passed.
|
||
|
///
|
||
|
/// If the server is running behind schedule due to heavy load or some other
|
||
|
/// reason, the actual duration of a game update will exceed what has been
|
||
|
/// specified.
|
||
|
///
|
||
|
/// The duration must be nonzero.
|
||
|
///
|
||
|
/// The default value is the same as Minecraft's official server (20 ticks
|
||
|
/// per second). You may want to use a shorter duration if you can afford to
|
||
|
/// do so.
|
||
|
pub fn update_duration(&mut self, duration: Duration) {
|
||
|
self.update_duration = duration;
|
||
|
}
|
||
|
|
||
|
/// Sets the state of "online mode", which determines if client
|
||
|
/// authentication and encryption should occur.
|
||
|
///
|
||
|
/// When online mode is disabled, malicious clients can give themselves any
|
||
|
/// username and UUID they want, potentially gaining privileges they
|
||
|
/// might not otherwise have. Additionally, encryption is only enabled in
|
||
|
/// online mode. For these reasons online mode should only be disabled
|
||
|
/// for development purposes and enabled on servers exposed to the
|
||
|
/// internet.
|
||
|
///
|
||
|
/// By default, online mode is enabled.
|
||
|
pub fn online_mode(&mut self, online_mode: bool) {
|
||
|
self.online_mode = online_mode;
|
||
|
}
|
||
|
|
||
|
/// Sets the maximum number of clients (past the login stage) allowed on the
|
||
|
/// server simultaneously.
|
||
|
///
|
||
|
/// The default is 32.
|
||
|
pub fn max_clients(&mut self, clients: usize) {
|
||
|
self.max_clients = clients;
|
||
|
}
|
||
|
|
||
|
/// The capacity of the buffer used to hold clientbound packets.
|
||
|
///
|
||
|
/// A larger capcity reduces the chance of packet loss but increases
|
||
|
/// potential memory usage. The default value is unspecified but should be
|
||
|
/// adequate for most situations.
|
||
|
///
|
||
|
/// The capacity must be nonzero.
|
||
|
pub fn clientbound_packet_capacity(&mut self, cap: usize) {
|
||
|
self.clientbound_packet_capacity = cap;
|
||
|
}
|
||
|
|
||
|
/// Sets the capacity of the buffer used to hold serverbound packets.
|
||
|
///
|
||
|
/// A larger capcity reduces the chance of packet loss but increases
|
||
|
/// potential memory usage. The default value is unspecified but should be
|
||
|
/// adequate for most situations.
|
||
|
///
|
||
|
/// The capacity must be nonzero.
|
||
|
pub fn serverbound_packet_capacity(&mut self, cap: usize) {
|
||
|
self.serverbound_packet_capacity = cap;
|
||
|
}
|
||
|
|
||
|
/// Sets the handle to the tokio runtime the server will use.
|
||
|
///
|
||
|
/// If a handle is not provided, the server will create its own tokio
|
||
|
/// runtime.
|
||
|
pub fn tokio_handle(&mut self, handle: TokioHandle) {
|
||
|
self.tokio_handle = Some(handle);
|
||
|
}
|
||
|
|
||
|
/// Adds a new dimension to the server which is identified by the returned
|
||
|
/// [`DimensionId`]. The default dimension is added if none are provided.
|
||
|
///
|
||
|
/// Additionally, the documented requirements on the fields of [`Dimension`]
|
||
|
/// must be met. No more than `u16::MAX` dimensions may be added.
|
||
|
pub fn push_dimension(&mut self, dimension: Dimension) -> DimensionId {
|
||
|
let id = self.biomes.len();
|
||
|
self.dimensions.push(dimension);
|
||
|
DimensionId(id as u16)
|
||
|
}
|
||
|
|
||
|
/// Adds a new biome to the server which is identified by the returned
|
||
|
/// [`BiomeId`]. The default biome is added if none are provided.
|
||
|
///
|
||
|
/// Additionally, the documented requirements on the fields of [`Biome`]
|
||
|
/// must be met. No more than `u16::MAX` biomes may be added.
|
||
|
pub fn push_biome(&mut self, biome: Biome) -> BiomeId {
|
||
|
let id = self.biomes.len();
|
||
|
self.biomes.push(biome);
|
||
|
BiomeId(id as u16)
|
||
|
}
|
||
|
|
||
|
/// Consumes the configuration and starts the server.
|
||
|
///
|
||
|
/// The function returns once the server has been shut down, a runtime error
|
||
|
/// occurs, or the configuration is invalid.
|
||
|
pub fn start(mut self) -> ShutdownResult {
|
||
|
if self.biomes.is_empty() {
|
||
|
self.biomes.push(Biome::default());
|
||
|
}
|
||
|
|
||
|
if self.dimensions.is_empty() {
|
||
|
self.dimensions.push(Dimension::default());
|
||
|
}
|
||
|
|
||
|
self.validate().map_err(ShutdownError::from)?;
|
||
|
start_server(self)
|
||
|
}
|
||
|
|
||
|
fn validate(&self) -> anyhow::Result<()> {
|
||
|
ensure!(
|
||
|
self.dimensions.len() <= u16::MAX as usize,
|
||
|
"more than u16::MAX dimensions added"
|
||
|
);
|
||
|
|
||
|
ensure!(
|
||
|
self.biomes.len() <= u16::MAX as usize,
|
||
|
"more than u16::MAX biomes added"
|
||
|
);
|
||
|
|
||
|
ensure!(
|
||
|
self.update_duration != Duration::ZERO,
|
||
|
"update duration must be nonzero"
|
||
|
);
|
||
|
|
||
|
ensure!(
|
||
|
self.clientbound_packet_capacity > 0,
|
||
|
"clientbound packet capacity must be nonzero"
|
||
|
);
|
||
|
|
||
|
ensure!(
|
||
|
self.serverbound_packet_capacity > 0,
|
||
|
"serverbound packet capacity must be nonzero"
|
||
|
);
|
||
|
|
||
|
for (i, dim) in self.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}",
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let mut names = HashSet::new();
|
||
|
|
||
|
for biome in self.biomes.iter() {
|
||
|
ensure!(
|
||
|
names.insert(biome.name.clone()),
|
||
|
"biome \"{}\" already added",
|
||
|
biome.name
|
||
|
);
|
||
|
}
|
||
|
|
||
|
Ok(())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl Default for ServerConfig {
|
||
|
fn default() -> Self {
|
||
|
Self::new()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// A trait containing callbacks which are invoked by the running Minecraft
|
||
|
/// server.
|
||
|
///
|
||
|
/// The handler is used from multiple threads and must therefore implement
|
||
|
/// `Send` and `Sync`. From within a single thread, callbacks are never invoked
|
||
|
/// recursively. In other words, a mutex can be aquired at the beginning of a
|
||
|
/// callback and released at the end without risk of deadlocking.
|
||
|
///
|
||
|
/// All methods are called from within a tokio context.
|
||
|
#[async_trait]
|
||
|
#[allow(unused_variables)]
|
||
|
pub trait Handler: Any + Send + Sync {
|
||
|
/// Called after the server is created, but prior to accepting connections
|
||
|
/// and entering the update loop.
|
||
|
///
|
||
|
/// This is useful for performing initialization work with a guarantee that
|
||
|
/// no connections to the server will be made until this function returns.
|
||
|
///
|
||
|
/// # Default Implementation
|
||
|
/// The default implementation does nothing.
|
||
|
fn init(&self, server: &mut Server) {}
|
||
|
|
||
|
/// Called once at the beginning of every server update (also known as
|
||
|
/// a "tick").
|
||
|
///
|
||
|
/// The frequency of server updates can be configured by `update_duration`
|
||
|
/// in [`ServerConfig`].
|
||
|
///
|
||
|
/// # Default Implementation
|
||
|
/// The default implementation does nothing.
|
||
|
fn update(&self, server: &mut Server) {}
|
||
|
|
||
|
/// Called when the server receives a Server List Ping query.
|
||
|
/// Data for the query can be provided or the query can be ignored.
|
||
|
///
|
||
|
/// # Default Implementation
|
||
|
/// A placeholder response is returned.
|
||
|
async fn server_list_ping(
|
||
|
&self,
|
||
|
server: &SharedServer,
|
||
|
remote_addr: SocketAddr,
|
||
|
) -> ServerListPing {
|
||
|
ServerListPing::Respond {
|
||
|
online_players: server.client_count() as i32,
|
||
|
max_players: server.max_clients() as i32,
|
||
|
description: "A Minecraft Server".into(),
|
||
|
favicon_png: None,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Called when a client is disconnected due to the server being full.
|
||
|
/// The return value is the disconnect message to use.
|
||
|
///
|
||
|
/// # Default Implementation
|
||
|
/// A placeholder message is returned.
|
||
|
async fn max_client_message(&self, server: &SharedServer, npd: &NewClientData) -> Text {
|
||
|
// TODO: Standard translated text for this purpose?
|
||
|
"The server is full!".into()
|
||
|
}
|
||
|
|
||
|
/// Called asynchronously for each client after successful authentication
|
||
|
/// (if online mode is enabled) to determine if they are allowed to join the
|
||
|
/// server. On success, a client-backed entity is spawned.
|
||
|
///
|
||
|
/// This function is the appropriate place to perform
|
||
|
/// whitelist checks, database queries, etc.
|
||
|
///
|
||
|
/// # Default Implementation
|
||
|
/// The client is allowed to join unconditionally.
|
||
|
async fn login(&self, server: &SharedServer, ncd: &NewClientData) -> Login {
|
||
|
Login::Join
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// The result of the [`server_list_ping`](Handler::server_list_ping) callback.
|
||
|
pub enum ServerListPing {
|
||
|
/// Responds to the server list ping with the given information.
|
||
|
Respond {
|
||
|
online_players: i32,
|
||
|
max_players: i32,
|
||
|
description: Text,
|
||
|
/// The server's icon as the bytes of a PNG image.
|
||
|
/// The image must be 64x64 pixels.
|
||
|
///
|
||
|
/// No icon is used if the value is `None`.
|
||
|
favicon_png: Option<Arc<[u8]>>,
|
||
|
},
|
||
|
/// Ignores the query and disconnects from the client.
|
||
|
Ignore,
|
||
|
}
|
||
|
|
||
|
/// The result of the [`login`](Handler::login) callback.
|
||
|
#[derive(Debug)]
|
||
|
pub enum Login {
|
||
|
/// The client may join the server.
|
||
|
Join,
|
||
|
/// The client may not join the server and will be disconnected with the
|
||
|
/// provided reason.
|
||
|
Disconnect(Text),
|
||
|
}
|
||
|
|
||
|
/// Identifies a particular [`Dimension`].
|
||
|
///
|
||
|
/// Dimension IDs are always valid and are cheap to copy and store.
|
||
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash, Debug)]
|
||
|
pub struct DimensionId(pub(crate) u16);
|
||
|
|
||
|
/// Contains the configuration for a custom dimension type.
|
||
|
///
|
||
|
/// In Minecraft, "dimension" and "dimension type" are two different concepts.
|
||
|
/// For instance, the Overworld and Nether are dimensions, each with
|
||
|
/// their own dimension type. A dimension in this library is analogous to a
|
||
|
/// [`World`](crate::World) while the [`Dimension`] struct represents a
|
||
|
/// dimension type.
|
||
|
#[derive(Clone, Debug)]
|
||
|
pub struct Dimension {
|
||
|
/// When false, compases will spin randomly.
|
||
|
pub natural: bool,
|
||
|
/// Must be between 0.0 and 1.0.
|
||
|
pub ambient_light: f32,
|
||
|
/// Must be between 0 and 24000.
|
||
|
pub fixed_time: Option<u16>,
|
||
|
/// Determines what skybox/fog effects to use.
|
||
|
pub effects: DimensionEffects,
|
||
|
/// The minimum height in which blocks can exist in this dimension.
|
||
|
///
|
||
|
/// `min_y` must meet the following conditions:
|
||
|
/// * `min_y % 16 == 0`
|
||
|
/// * `-2032 <= min_y <= 2016`
|
||
|
pub min_y: i32,
|
||
|
/// The total height in which blocks can exist in this dimension.
|
||
|
///
|
||
|
/// `height` must meet the following conditions:
|
||
|
/// * `height % 16 == 0`
|
||
|
/// * `0 <= height <= 4064`
|
||
|
/// * `min_y + height <= 2032`
|
||
|
pub height: i32,
|
||
|
// TODO: The following fields should be added if they can affect the
|
||
|
// appearance of the dimension to clients.
|
||
|
// * infiniburn
|
||
|
// * respawn_anchor_works
|
||
|
// * has_skylight
|
||
|
// * bed_works
|
||
|
// * has_raids
|
||
|
// * logical_height
|
||
|
// * coordinate_scale
|
||
|
// * ultrawarm
|
||
|
// * has_ceiling
|
||
|
}
|
||
|
|
||
|
impl Default for Dimension {
|
||
|
fn default() -> Self {
|
||
|
Self {
|
||
|
natural: true,
|
||
|
ambient_light: 0.0,
|
||
|
fixed_time: None,
|
||
|
effects: DimensionEffects::Overworld,
|
||
|
min_y: -64,
|
||
|
height: 384,
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||
|
pub enum DimensionEffects {
|
||
|
Overworld,
|
||
|
TheNether,
|
||
|
TheEnd,
|
||
|
}
|
||
|
|
||
|
/// Identifies a particular [`Biome`].
|
||
|
///
|
||
|
/// Biome IDs are always valid and are cheap to copy and store.
|
||
|
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||
|
pub struct BiomeId(pub(crate) u16);
|
||
|
|
||
|
/// Contains the configuration for a custom biome.
|
||
|
#[derive(Clone, Debug)]
|
||
|
pub struct Biome {
|
||
|
/// The unique name for this biome. The name can be
|
||
|
/// seen in the F3 debug menu.
|
||
|
pub name: Identifier,
|
||
|
pub precipitation: BiomePrecipitation,
|
||
|
pub sky_color: u32,
|
||
|
pub water_fog_color: u32,
|
||
|
pub fog_color: u32,
|
||
|
pub water_color: u32,
|
||
|
pub foliage_color: Option<u32>,
|
||
|
pub grass_color_modifier: BiomeGrassColorModifier,
|
||
|
pub music: Option<BiomeMusic>,
|
||
|
pub ambient_sound: Option<Identifier>,
|
||
|
pub additions_sound: Option<BiomeAdditionsSound>,
|
||
|
pub mood_sound: Option<BiomeMoodSound>,
|
||
|
pub particle: Option<BiomeParticle>,
|
||
|
// TODO: The following fields should be added if they can affect the appearance of the biome to
|
||
|
// clients.
|
||
|
// * depth: f32
|
||
|
// * temperature: f32
|
||
|
// * scale: f32
|
||
|
// * downfall: f32
|
||
|
// * category
|
||
|
// * temperature_modifier
|
||
|
// * grass_color (misleading name?)
|
||
|
}
|
||
|
|
||
|
impl Default for Biome {
|
||
|
fn default() -> Self {
|
||
|
Self {
|
||
|
name: ident!("plains"),
|
||
|
precipitation: BiomePrecipitation::Rain,
|
||
|
sky_color: 7907327,
|
||
|
water_fog_color: 329011,
|
||
|
fog_color: 12638463,
|
||
|
water_color: 4159204,
|
||
|
foliage_color: None,
|
||
|
grass_color_modifier: BiomeGrassColorModifier::None,
|
||
|
music: None,
|
||
|
ambient_sound: None,
|
||
|
additions_sound: None,
|
||
|
mood_sound: None,
|
||
|
particle: None,
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||
|
pub enum BiomePrecipitation {
|
||
|
Rain,
|
||
|
Snow,
|
||
|
None,
|
||
|
}
|
||
|
|
||
|
impl Default for BiomePrecipitation {
|
||
|
fn default() -> Self {
|
||
|
Self::Rain
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Minecraft handles grass colors for swamps and dark oak forests in a special
|
||
|
/// way.
|
||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||
|
pub enum BiomeGrassColorModifier {
|
||
|
Swamp,
|
||
|
DarkForest,
|
||
|
None,
|
||
|
}
|
||
|
|
||
|
impl Default for BiomeGrassColorModifier {
|
||
|
fn default() -> Self {
|
||
|
Self::None
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#[derive(Clone, Debug)]
|
||
|
pub struct BiomeMusic {
|
||
|
pub replace_current_music: bool,
|
||
|
pub sound: Identifier,
|
||
|
pub min_delay: i32,
|
||
|
pub max_delay: i32,
|
||
|
}
|
||
|
|
||
|
#[derive(Clone, Debug)]
|
||
|
pub struct BiomeAdditionsSound {
|
||
|
pub sound: Identifier,
|
||
|
pub tick_chance: f64,
|
||
|
}
|
||
|
|
||
|
#[derive(Clone, Debug)]
|
||
|
pub struct BiomeMoodSound {
|
||
|
pub sound: Identifier,
|
||
|
pub tick_delay: i32,
|
||
|
pub offset: f64,
|
||
|
pub block_search_extent: i32,
|
||
|
}
|
||
|
|
||
|
#[derive(Clone, Debug)]
|
||
|
pub struct BiomeParticle {
|
||
|
pub probability: f32,
|
||
|
pub typ: Identifier,
|
||
|
}
|