From 3f150b4c8a5bcb20794424a66b6e8f3cc7c57d1c Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 11 Jul 2022 05:08:02 -0700 Subject: [PATCH] Document most items --- README.md | 4 - build/block.rs | 16 +- examples/conway.rs | 28 +- src/biome.rs | 84 ++++- src/block.rs | 6 +- src/block_pos.rs | 5 +- src/bvh.rs | 5 + src/chunk.rs | 59 ++- src/chunk_pos.rs | 3 + src/client.rs | 345 ++++++++++-------- src/client/event.rs | 60 ++- src/config.rs | 55 ++- src/dimension.rs | 67 +++- src/entity.rs | 133 +++++-- src/entity/data.rs | 222 ++++++++++- src/entity/meta.rs | 216 ----------- src/entity/types.rs | 12 + src/ident.rs | 26 +- src/lib.rs | 136 ++++++- src/player_list.rs | 57 ++- src/player_textures.rs | 18 +- src/{protocol.rs => protocol_inner.rs} | 4 +- .../byte_angle.rs | 10 +- src/{protocol => protocol_inner}/codec.rs | 4 +- src/{protocol => protocol_inner}/packets.rs | 14 +- src/{protocol => protocol_inner}/var_int.rs | 5 +- src/{protocol => protocol_inner}/var_long.rs | 3 +- src/server.rs | 60 +-- src/spatial_index.rs | 68 +++- src/text.rs | 23 +- src/util.rs | 26 +- src/world.rs | 55 ++- 32 files changed, 1230 insertions(+), 599 deletions(-) delete mode 100644 src/entity/meta.rs create mode 100644 src/entity/types.rs rename src/{protocol.rs => protocol_inner.rs} (99%) rename src/{protocol => protocol_inner}/byte_angle.rs (76%) rename src/{protocol => protocol_inner}/codec.rs (98%) rename src/{protocol => protocol_inner}/packets.rs (99%) rename src/{protocol => protocol_inner}/var_int.rs (97%) rename src/{protocol => protocol_inner}/var_long.rs (95%) diff --git a/README.md b/README.md index 308856e..e69de29 100644 --- a/README.md +++ b/README.md @@ -1,4 +0,0 @@ -# valence -Coming soon to a package manager near you! - -(Valence is a WIP Rust library.) diff --git a/build/block.rs b/build/block.rs index fb2dbb4..4b68106 100644 --- a/build/block.rs +++ b/build/block.rs @@ -400,16 +400,16 @@ pub fn build() -> anyhow::Result<()> { #default_block_states } - /// An enumeration of all block types. + /// An enumeration of all block kinds. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub enum BlockKind { #(#block_kind_variants,)* } impl BlockKind { - /// Construct a block type from its snake_case name. + /// Construct a block kind from its snake_case name. /// - /// Returns `None` if the given name is not valid. + /// Returns `None` if the name is invalid. pub fn from_str(name: &str) -> Option { match name { #block_kind_from_str_arms @@ -417,19 +417,19 @@ pub fn build() -> anyhow::Result<()> { } } - /// Get the snake_case name of this block type. + /// Get the snake_case name of this block kind. pub const fn to_str(self) -> &'static str { match self { #block_kind_to_str_arms } } - /// Returns the default block state for a given block type. + /// Returns the default block state for a given block kind. pub const fn to_state(self) -> BlockState { BlockState::from_kind(self) } - /// Returns a slice of all properties this block type has. + /// Returns a slice of all properties this block kind has. pub const fn props(self) -> &'static [PropName] { match self { #block_kind_props_arms @@ -437,11 +437,11 @@ pub fn build() -> anyhow::Result<()> { } } - /// An array of all block types. + /// An array of all block kinds. pub const ALL: [Self; #block_kind_count] = [#(Self::#block_kind_variants,)*]; } - /// The default block type is `air`. + /// The default block kind is `air`. impl Default for BlockKind { fn default() -> Self { Self::Air diff --git a/examples/conway.rs b/examples/conway.rs index 9fba71e..7a6b0a8 100644 --- a/examples/conway.rs +++ b/examples/conway.rs @@ -9,10 +9,10 @@ use num::Integer; use rayon::iter::{IndexedParallelIterator, IntoParallelRefMutIterator, ParallelIterator}; use valence::biome::Biome; use valence::block::BlockState; -use valence::client::{ClientEvent, ClientId, GameMode, Hand}; +use valence::client::{Event, ClientId, GameMode, Hand}; use valence::config::{Config, ServerListPing}; use valence::dimension::{Dimension, DimensionId}; -use valence::entity::meta::Pose; +use valence::entity::data::Pose; use valence::entity::{EntityData, EntityId, EntityKind}; use valence::server::{Server, SharedServer, ShutdownResult}; use valence::text::{Color, TextFormat}; @@ -175,17 +175,15 @@ impl Config for Game { while let Some(event) = client.pop_event() { match event { - ClientEvent::Digging(e) => { - let pos = e.position; - - if (0..SIZE_X as i32).contains(&pos.x) - && (0..SIZE_Z as i32).contains(&pos.z) - && pos.y == BOARD_Y + Event::Digging { position, .. } => { + if (0..SIZE_X as i32).contains(&position.x) + && (0..SIZE_Z as i32).contains(&position.z) + && position.y == BOARD_Y { - board[pos.x as usize + pos.z as usize * SIZE_X] = true; + board[position.x as usize + position.z as usize * SIZE_X] = true; } } - ClientEvent::Movement { .. } => { + Event::Movement { .. } => { if client.position().y <= 0.0 { client.teleport(spawn_pos, client.yaw(), client.pitch()); } @@ -197,29 +195,29 @@ impl Config for Game { player.set_pitch(client.pitch()); player.set_on_ground(client.on_ground()); } - ClientEvent::StartSneaking => { + Event::StartSneaking => { if let EntityData::Player(e) = player.data_mut() { e.set_crouching(true); e.set_pose(Pose::Sneaking); } } - ClientEvent::StopSneaking => { + Event::StopSneaking => { if let EntityData::Player(e) = player.data_mut() { e.set_pose(Pose::Standing); e.set_crouching(false); } } - ClientEvent::StartSprinting => { + Event::StartSprinting => { if let EntityData::Player(e) = player.data_mut() { e.set_sprinting(true); } } - ClientEvent::StopSprinting => { + Event::StopSprinting => { if let EntityData::Player(e) = player.data_mut() { e.set_sprinting(false); } } - ClientEvent::ArmSwing(hand) => { + Event::ArmSwing(hand) => { if let EntityData::Player(e) = player.data_mut() { match hand { Hand::Main => e.trigger_swing_main_arm(), diff --git a/src/biome.rs b/src/biome.rs index 6d82bf4..c02c0e7 100644 --- a/src/biome.rs +++ b/src/biome.rs @@ -1,19 +1,23 @@ +//! Biome definitions. + use crate::ident; use crate::ident::Ident; +use crate::protocol_inner::packets::play::s2c::Biome as BiomeRegistryBiome; -/// Identifies a particular [`Biome`]. +/// Identifies a particular [`Biome`] on the server. /// -/// Biome IDs are always valid and are cheap to copy and store. +/// The default biome ID refers to the first biome added in the server's +/// [configuration](crate::config::Config). +/// +/// To obtain biome IDs for other biomes, call +/// [`biomes`](crate::server::SharedServer::biomes). #[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct BiomeId(pub(crate) u16); -impl BiomeId { - pub fn to_index(self) -> usize { - self.0 as usize - } -} - /// Contains the configuration for a biome. +/// +/// Biomes are registered once at startup through +/// [`biomes`](crate::config::Config::biomes). #[derive(Clone, Debug)] pub struct Biome { /// The unique name for this biome. The name can be @@ -42,6 +46,70 @@ pub struct Biome { // * temperature_modifier } +impl Biome { + pub(crate) fn to_biome_registry_item(&self, id: i32) -> BiomeRegistryBiome { + use crate::protocol_inner::packets::play::s2c::{ + BiomeAdditionsSound, BiomeEffects, BiomeMoodSound, BiomeMusic, BiomeParticle, + BiomeParticleOptions, BiomeProperty, + }; + + BiomeRegistryBiome { + name: self.name.clone(), + id, + element: BiomeProperty { + precipitation: match self.precipitation { + BiomePrecipitation::Rain => "rain", + BiomePrecipitation::Snow => "snow", + BiomePrecipitation::None => "none", + } + .into(), + depth: 0.125, + temperature: 0.8, + scale: 0.05, + downfall: 0.4, + category: "none".into(), + temperature_modifier: None, + effects: BiomeEffects { + sky_color: self.sky_color as i32, + water_fog_color: self.water_fog_color as i32, + fog_color: self.fog_color as i32, + water_color: self.water_color as i32, + foliage_color: self.foliage_color.map(|x| x as i32), + grass_color: self.grass_color.map(|x| x as i32), + grass_color_modifier: match self.grass_color_modifier { + BiomeGrassColorModifier::Swamp => Some("swamp".into()), + BiomeGrassColorModifier::DarkForest => Some("dark_forest".into()), + BiomeGrassColorModifier::None => None, + }, + music: self.music.as_ref().map(|bm| BiomeMusic { + replace_current_music: bm.replace_current_music, + sound: bm.sound.clone(), + max_delay: bm.max_delay, + min_delay: bm.min_delay, + }), + ambient_sound: self.ambient_sound.clone(), + additions_sound: self.additions_sound.as_ref().map(|a| BiomeAdditionsSound { + sound: a.sound.clone(), + tick_chance: a.tick_chance, + }), + mood_sound: self.mood_sound.as_ref().map(|m| BiomeMoodSound { + sound: m.sound.clone(), + tick_delay: m.tick_delay, + offset: m.offset, + block_search_extent: m.block_search_extent, + }), + }, + particle: self.particle.as_ref().map(|p| BiomeParticle { + probability: p.probability, + options: BiomeParticleOptions { + kind: p.kind.clone(), + }, + }), + }, + } + } +} + impl Default for Biome { fn default() -> Self { Self { diff --git a/src/block.rs b/src/block.rs index f6d27a4..0b533a3 100644 --- a/src/block.rs +++ b/src/block.rs @@ -6,7 +6,7 @@ use std::io::{Read, Write}; use anyhow::Context; pub use crate::block_pos::BlockPos; -use crate::protocol::{Decode, Encode, VarInt}; +use crate::protocol_inner::{Decode, Encode, VarInt}; include!(concat!(env!("OUT_DIR"), "/block.rs")); @@ -17,7 +17,7 @@ impl fmt::Debug for BlockState { } impl Display for BlockState { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt_block_state(*self, f) } } @@ -35,7 +35,7 @@ fn fmt_block_state(bs: BlockState, f: &mut fmt::Formatter) -> fmt::Result { struct KeyVal<'a>(&'a str, &'a str); impl<'a> fmt::Debug for KeyVal<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}={}", self.0, self.1) } } diff --git a/src/block_pos.rs b/src/block_pos.rs index edc5be0..eddb6b8 100644 --- a/src/block_pos.rs +++ b/src/block_pos.rs @@ -3,8 +3,9 @@ use std::io::{Read, Write}; use anyhow::bail; use vek::Vec3; -use crate::protocol::{Decode, Encode}; +use crate::protocol_inner::{Decode, Encode}; +/// Represents an absolute block position in a world. #[derive(Clone, Copy, Default, PartialEq, Eq, Hash, Debug)] pub struct BlockPos { pub x: i32, @@ -13,10 +14,12 @@ pub struct BlockPos { } impl BlockPos { + /// Constructs a new block position. pub const fn new(x: i32, y: i32, z: i32) -> Self { Self { x, y, z } } + /// Returns the block position a point is contained within. pub fn at(pos: impl Into>) -> Self { pos.into().floor().as_::().into() } diff --git a/src/bvh.rs b/src/bvh.rs index e72ba85..0bfd128 100644 --- a/src/bvh.rs +++ b/src/bvh.rs @@ -1,3 +1,8 @@ +//! The [bounding volume hierarchy][bvh] contained in the [`SpatialIndex`] +//! +//! [bvh]: https://en.wikipedia.org/wiki/Bounding_volume_hierarchy +//! [`SpatialIndex`]: crate::spatial_index::SpatialIndex + use std::mem; use approx::relative_eq; diff --git a/src/chunk.rs b/src/chunk.rs index a7252df..4bbd6c6 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -14,13 +14,14 @@ use crate::block::BlockState; use crate::block_pos::BlockPos; pub use crate::chunk_pos::ChunkPos; use crate::dimension::DimensionId; -use crate::protocol::packets::play::s2c::{ +use crate::protocol_inner::packets::play::s2c::{ BlockUpdate, LevelChunkHeightmaps, LevelChunkWithLight, S2cPlayPacket, SectionBlocksUpdate, }; -use crate::protocol::{Encode, Nbt, VarInt, VarLong}; +use crate::protocol_inner::{Encode, Nbt, VarInt, VarLong}; use crate::server::SharedServer; use crate::Ticks; +/// A container for all [`Chunks`]s in a [`World`](crate::world::World). pub struct Chunks { chunks: HashMap, server: SharedServer, @@ -36,6 +37,16 @@ impl Chunks { } } + /// Creates an empty chunk at the provided position and returns a mutable + /// refernce to it. + /// + /// If a chunk at the position already exists, then the old chunk + /// is overwritten. + /// + /// **Note**: For the vanilla Minecraft client to see a chunk, all chunks + /// adjacent to it must also be loaded. It is also important that clients + /// are not spawned within unloaded chunks via + /// [`spawn`](crate::client::Client::spawn). pub fn create(&mut self, pos: impl Into) -> &mut Chunk { let section_count = (self.server.dimension(self.dimension).height / 16) as u32; let chunk = Chunk::new(section_count, self.server.current_tick()); @@ -49,42 +60,69 @@ impl Chunks { } } - pub fn delete(&mut self, pos: ChunkPos) -> bool { - self.chunks.remove(&pos).is_some() + /// Removes a chunk at the provided position. + /// + /// If a chunk exists at the position, then it is deleted and `true` is + /// returned. Otherwise, `false` is returned. + pub fn delete(&mut self, pos: impl Into) -> bool { + self.chunks.remove(&pos.into()).is_some() } + /// Returns the number of loaded chunks. pub fn count(&self) -> usize { self.chunks.len() } + /// Gets a shared reference to the chunk at the provided position. + /// + /// If there is no chunk at the position, then `None` is returned. pub fn get(&self, pos: impl Into) -> Option<&Chunk> { self.chunks.get(&pos.into()) } + /// Gets an exclusive reference to the chunk at the provided position. + /// + /// If there is no chunk at the position, then `None` is returned. pub fn get_mut(&mut self, pos: impl Into) -> Option<&mut Chunk> { self.chunks.get_mut(&pos.into()) } + /// Deletes all chunks. pub fn clear(&mut self) { self.chunks.clear(); } + /// Returns an immutable iterator over all chunks in the world in an + /// unspecified order. pub fn iter(&self) -> impl FusedIterator + Clone + '_ { self.chunks.iter().map(|(&pos, chunk)| (pos, chunk)) } + /// Returns a mutable iterator over all chunks in the world in an + /// unspecified order. pub fn iter_mut(&mut self) -> impl FusedIterator + '_ { self.chunks.iter_mut().map(|(&pos, chunk)| (pos, chunk)) } + /// Returns a parallel immutable iterator over all chunks in the world in an + /// unspecified order. pub fn par_iter(&self) -> impl ParallelIterator + Clone + '_ { self.chunks.par_iter().map(|(&pos, chunk)| (pos, chunk)) } + /// Returns a parallel mutable iterator over all chunks in the world in an + /// unspecified order. pub fn par_iter_mut(&mut self) -> impl ParallelIterator + '_ { self.chunks.par_iter_mut().map(|(&pos, chunk)| (pos, chunk)) } + /// Gets the block state at a position. + /// + /// If the position is not inside of a chunk, then `None` is returned. + /// + /// Note: if you need to get a large number of blocks, it may be more + /// efficient to read from the chunks directly with + /// [`Chunk::get_block_state`]. pub fn get_block_state(&self, pos: impl Into) -> Option { let pos = pos.into(); let chunk_pos = ChunkPos::from(pos); @@ -106,6 +144,14 @@ impl Chunks { } } + /// Sets the block state at a position. + /// + /// If the position is inside of a chunk, then `true` is returned. + /// Otherwise, `false` is returned. + /// + /// Note: if you need to set a large number of blocks, it may be more + /// efficient write to the chunks directly with + /// [`Chunk::set_block_state`]. pub fn set_block_state(&mut self, pos: impl Into, block: BlockState) -> bool { let pos = pos.into(); let chunk_pos = ChunkPos::from(pos); @@ -130,6 +176,11 @@ impl Chunks { } } +/// A chunk is a 16x16-block segment of a world with a height determined by the +/// [`Dimension`](crate::dimension::Dimension) of the world. +/// +/// In addition to blocks, chunks also contain [biomes](crate::biome::Biome). +/// Every 4x4x4 segment of blocks in a chunk corresponds to a biome. pub struct Chunk { sections: Box<[ChunkSection]>, // TODO block_entities: HashMap, diff --git a/src/chunk_pos.rs b/src/chunk_pos.rs index f784626..d7ca246 100644 --- a/src/chunk_pos.rs +++ b/src/chunk_pos.rs @@ -10,10 +10,13 @@ pub struct ChunkPos { } impl ChunkPos { + /// Constructs a new chunk position. pub const fn new(x: i32, z: i32) -> Self { Self { x, z } } + /// Takes an absolute position and returns the chunk position + /// containing the point. pub fn at(x: f64, z: f64) -> Self { Self::new((x / 16.0).floor() as i32, (z / 16.0).floor() as i32) } diff --git a/src/client.rs b/src/client.rs index 1597f54..17fba17 100644 --- a/src/client.rs +++ b/src/client.rs @@ -11,29 +11,28 @@ use rayon::iter::ParallelIterator; use uuid::Uuid; use vek::Vec3; -use crate::biome::{Biome, BiomeGrassColorModifier, BiomePrecipitation}; +use crate::biome::Biome; use crate::block_pos::BlockPos; use crate::chunk_pos::ChunkPos; -use crate::dimension::{Dimension, DimensionEffects, DimensionId}; -use crate::entity::data::Player; +use crate::dimension::DimensionId; +use crate::entity::types::Player; use crate::entity::{velocity_to_packet_units, Entities, Entity, EntityId, EntityKind}; use crate::player_textures::SignedPlayerTextures; -use crate::protocol::packets::play::c2s::{ +use crate::protocol_inner::packets::play::c2s::{ C2sPlayPacket, DiggingStatus, InteractKind, PlayerCommandId, }; -pub use crate::protocol::packets::play::s2c::SetTitleAnimationTimes as TitleAnimationTimes; -use crate::protocol::packets::play::s2c::{ - Animate, Biome as BiomeRegistryBiome, BiomeAdditionsSound, BiomeEffects, BiomeMoodSound, - BiomeMusic, BiomeParticle, BiomeParticleOptions, BiomeProperty, BiomeRegistry, BlockChangeAck, - ChatType, ChatTypeChat, ChatTypeNarration, ChatTypeRegistry, ChatTypeRegistryEntry, - ClearTitles, DimensionType, DimensionTypeRegistry, DimensionTypeRegistryEntry, Disconnect, - EntityEvent, ForgetLevelChunk, GameEvent, GameEventReason, KeepAlive, Login, - MoveEntityPosition, MoveEntityPositionAndRotation, MoveEntityRotation, PlayerPosition, - PlayerPositionFlags, RegistryCodec, RemoveEntities, Respawn, RotateHead, S2cPlayPacket, - SetChunkCacheCenter, SetChunkCacheRadius, SetEntityMetadata, SetEntityMotion, SetSubtitleText, - SetTitleText, SpawnPosition, SystemChat, TeleportEntity, ENTITY_EVENT_MAX_BOUND, +pub use crate::protocol_inner::packets::play::s2c::SetTitleAnimationTimes as TitleAnimationTimes; +use crate::protocol_inner::packets::play::s2c::{ + Animate, BiomeRegistry, BlockChangeAck, ChatType, ChatTypeChat, ChatTypeNarration, + ChatTypeRegistry, ChatTypeRegistryEntry, ClearTitles, DimensionTypeRegistry, + DimensionTypeRegistryEntry, Disconnect, EntityEvent, ForgetLevelChunk, GameEvent, + GameEventReason, KeepAlive, Login, MoveEntityPosition, MoveEntityPositionAndRotation, + MoveEntityRotation, PlayerPosition, PlayerPositionFlags, RegistryCodec, RemoveEntities, + Respawn, RotateHead, S2cPlayPacket, SetChunkCacheCenter, SetChunkCacheRadius, + SetEntityMetadata, SetEntityMotion, SetSubtitleText, SetTitleText, SpawnPosition, SystemChat, + TeleportEntity, ENTITY_EVENT_MAX_BOUND, }; -use crate::protocol::{BoundedInt, ByteAngle, Nbt, RawBytes, VarInt}; +use crate::protocol_inner::{BoundedInt, ByteAngle, Nbt, RawBytes, VarInt}; use crate::server::{C2sPacketChannels, NewClientData, SharedServer}; use crate::slotmap::{Key, SlotMap}; use crate::text::Text; @@ -41,6 +40,11 @@ use crate::util::{chunks_in_view_distance, is_chunk_in_view_distance}; use crate::world::{WorldId, Worlds}; use crate::{ident, Ticks, LIBRARY_NAMESPACE}; +/// A container for all [`Client`]s on a [`Server`](crate::server::Server). +/// +/// New clients are automatically inserted into this container but +/// are not automatically deleted. It is your responsibility to delete them once +/// they disconnect. This can be checked with [`Client::is_disconnected`]. pub struct Clients { sm: SlotMap, } @@ -55,51 +59,103 @@ impl Clients { (ClientId(id), client) } + /// Removes a client from the server. + /// + /// If the given client ID is valid, `true` is returned and the client is + /// deleted. Otherwise, `false` is returned and the function has no effect. pub fn delete(&mut self, client: ClientId) -> bool { self.sm.remove(client.0).is_some() } + /// Deletes all clients from the server (as if by [`Self::delete`]) for + /// which `f` returns `true`. + /// + /// All clients are visited in an unspecified order. pub fn retain(&mut self, mut f: impl FnMut(ClientId, &mut Client) -> bool) { self.sm.retain(|k, v| f(ClientId(k), v)) } + /// Returns the number of clients on the server. This includes clients + /// which may be disconnected. pub fn count(&self) -> usize { self.sm.len() } + /// Returns a shared reference to the client with the given ID. If + /// the ID is invalid, then `None` is returned. pub fn get(&self, client: ClientId) -> Option<&Client> { self.sm.get(client.0) } + /// Returns an exclusive reference to the client with the given ID. If the + /// ID is invalid, then `None` is returned. pub fn get_mut(&mut self, client: ClientId) -> Option<&mut Client> { self.sm.get_mut(client.0) } + /// Returns an immutable iterator over all clients on the server in an + /// unspecified order. pub fn iter(&self) -> impl FusedIterator + Clone + '_ { self.sm.iter().map(|(k, v)| (ClientId(k), v)) } + /// Returns a mutable iterator over all clients on the server in an + /// unspecified order. pub fn iter_mut(&mut self) -> impl FusedIterator + '_ { self.sm.iter_mut().map(|(k, v)| (ClientId(k), v)) } + /// Returns a parallel immutable iterator over all clients on the server in + /// an unspecified order. pub fn par_iter(&self) -> impl ParallelIterator + Clone + '_ { self.sm.par_iter().map(|(k, v)| (ClientId(k), v)) } + /// Returns a parallel mutable iterator over all clients on the server in an + /// unspecified order. pub fn par_iter_mut(&mut self) -> impl ParallelIterator + '_ { self.sm.par_iter_mut().map(|(k, v)| (ClientId(k), v)) } } +/// A key for a [`Client`] on the server. +/// +/// Client IDs are either _valid_ or _invalid_. Valid client IDs point to +/// clients that have not been deleted, while invalid IDs point to those that +/// have. Once an ID becomes invalid, it will never become valid again. +/// +/// The [`Ord`] instance on this type is correct but otherwise unspecified. This +/// is useful for storing IDs in containers such as +/// [`BTreeMap`](std::collections::BTreeMap). #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] pub struct ClientId(Key); impl ClientId { + /// The value of the default client ID which is always invalid. pub const NULL: Self = Self(Key::NULL); } -/// Represents a client connected to the server after logging in. +/// Represents a remote connection to a client after successfully logging in. +/// +/// Much like an [`Entity`], clients posess a location, rotation, and UUID. +/// Clients are handled separately from entities and are partially +/// controlled by the library. +/// +/// By default, clients have no influence over the worlds they reside in. They +/// cannot break blocks, hurt entities, or see other clients. Interactions with +/// the server must be handled explicitly with [`Self::pop_event`]. +/// +/// Additionally, clients posess [`Player`] entity data which is only visible to +/// themselves. This can be accessed with [`Self::player`] and +/// [`Self::player_mut`]. +/// +/// # The Difference Between a "Client" and a "Player" +/// +/// Normally in Minecraft, players and clients are one and the same. Players are +/// simply a special type of entity which is backed by a remote connection. +/// +/// In Valence however, clients and players have been decoupled. This separation +/// was done primarily to enable multithreaded client updates. pub struct Client { /// Setting this to `None` disconnects the client. send: SendOpt, @@ -125,7 +181,7 @@ pub struct Client { spawn_position: BlockPos, spawn_position_yaw: f32, death_location: Option<(DimensionId, BlockPos)>, - events: VecDeque, + events: VecDeque, /// The ID of the last keepalive sent. last_keepalive_id: i64, new_max_view_distance: u8, @@ -207,42 +263,58 @@ impl Client { } } + /// Gets the tick that this client was created. pub fn created_tick(&self) -> Ticks { self.created_tick } + /// Gets the client's UUID. pub fn uuid(&self) -> Uuid { self.uuid } + /// Gets the username of this client, which is always valid. pub fn username(&self) -> &str { &self.username } + /// Gets the player textures of this client. If the client does not have + /// a skin, then `None` is returned. pub fn textures(&self) -> Option<&SignedPlayerTextures> { self.textures.as_ref() } + /// Gets the world this client is located in. pub fn world(&self) -> WorldId { self.world } + /// Changes the world this client is located in. + /// + /// The given [`WorldId`] must be valid. Otherwise, the client is + /// disconnected. pub fn spawn(&mut self, world: WorldId) { self.world = world; self.flags.set_spawn(true); } - /// Sends a system message to the player. + /// Sends a system message to the player which is visible in the chat. pub fn send_message(&mut self, msg: impl Into) { // We buffer messages because weird things happen if we send them before the // login packet. self.msgs_to_send.push(msg.into()); } + /// Gets the absolute position of this client in the world it is located + /// in. pub fn position(&self) -> Vec3 { self.new_position } + /// Changes the position and rotation of this client in the world it is + /// located in. + /// + /// If you want to change the client's world, use [`Self::spawn`]. pub fn teleport(&mut self, pos: impl Into>, yaw: f32, pitch: f32) { self.new_position = pos.into(); @@ -265,22 +337,24 @@ impl Client { } } + /// Gets this client's yaw. pub fn yaw(&self) -> f32 { self.yaw } + /// Gets this client's pitch. pub fn pitch(&self) -> f32 { self.pitch } - /// Gets the spawn position. The client will see regular compasses point at - /// the returned position. + /// Gets the spawn position. The client will see `minecraft:compass` items + /// point at the returned position. pub fn spawn_position(&self) -> BlockPos { self.spawn_position } - /// Sets the spawn position. The client will see regular compasses point at - /// the provided position. + /// Sets the spawn position. The client will see `minecraft:compass` items + /// point at the provided position. pub fn set_spawn_position(&mut self, pos: impl Into, yaw_degrees: f32) { let pos = pos.into(); if pos != self.spawn_position || yaw_degrees != self.spawn_position_yaw { @@ -290,18 +364,20 @@ impl Client { } } - /// Gets the last death location. The client will see recovery compasses - /// point at the returned position. If the client's current dimension - /// differs from the returned dimension or the location is `None` then the - /// compass will spin randomly. + /// Gets the last death location of this client. The client will see + /// `minecraft:recovery_compass` items point at the returned position. + /// If the client's current dimension differs from the returned + /// dimension or the location is `None` then the compass will spin + /// randomly. pub fn death_location(&self) -> Option<(DimensionId, BlockPos)> { self.death_location } - /// Sets the last death location. The client will see recovery compasses - /// point at the provided position. If the client's current dimension - /// differs from the provided dimension or the location is `None` then the - /// compass will spin randomly. + /// Sets the last death location. The client will see + /// `minecraft:recovery_compass` items point at the provided position. + /// If the client's current dimension differs from the provided + /// dimension or the location is `None` then the compass will spin + /// randomly. /// /// Changes to the last death location take effect when the client /// (re)spawns. @@ -309,14 +385,22 @@ impl Client { self.death_location = location; } + /// Gets the client's game mode. pub fn game_mode(&self) -> GameMode { self.new_game_mode } - pub fn set_game_mode(&mut self, new_game_mode: GameMode) { - self.new_game_mode = new_game_mode; + /// Sets the client's game mode. + pub fn set_game_mode(&mut self, game_mode: GameMode) { + self.new_game_mode = game_mode; } + /// Sets the title this client sees. + /// + /// A title is a large piece of text displayed in the center of the screen + /// which may also include a subtitle underneath it. The title + /// can be configured to fade in and out using the + /// [`TitleAnimationTimes`] struct. pub fn set_title( &mut self, title: impl Into, @@ -339,19 +423,30 @@ impl Client { } } + /// Removes the current title from the client's screen. pub fn clear_title(&mut self) { self.send_packet(ClearTitles { reset: true }); } + /// Gets if the client is on the ground, as determined by the client. pub fn on_ground(&self) -> bool { self.flags.on_ground() } + /// Gets whether or not the client is connected to the server. + /// + /// A disconnected client object will never become reconnected. It is your + /// responsibility to remove disconnected clients from the [`Clients`] + /// container. pub fn is_disconnected(&self) -> bool { self.send.is_none() } - pub fn pop_event(&mut self) -> Option { + /// Removes an [`Event`] from the queue. + /// + /// Any remaining client events not popped are deleted at the end of the + /// current tick. + pub fn pop_event(&mut self) -> Option { self.events.pop_front() } @@ -363,28 +458,44 @@ impl Client { .min(self.max_view_distance()) } + /// Gets the maximum view distance. The client will not be able to see + /// chunks and entities past this distance. + /// + /// The value returned is measured in chunks. pub fn max_view_distance(&self) -> u8 { self.new_max_view_distance } - /// The new view distance is clamped to `2..=32`. + /// Sets the maximum view distance. The client will not be able to see + /// chunks and entities past this distance. + /// + /// The new view distance is measured in chunks and is clamped to `2..=32`. pub fn set_max_view_distance(&mut self, dist: u8) { self.new_max_view_distance = dist.clamp(2, 32); } - /// Must be set on the same tick the client joins the game. + /// Enables hardcore mode. This changes the design of the client's hearts. + /// + /// To have any visible effect, this function must be called on the same + /// tick the client joins the server. pub fn set_hardcore(&mut self, hardcore: bool) { self.flags.set_hardcore(hardcore); } + /// Gets if hardcore mode is enabled. pub fn is_hardcore(&mut self) -> bool { self.flags.hardcore() } + /// Gets the client's current settings. pub fn settings(&self) -> Option<&Settings> { self.settings.as_ref() } + /// Disconnects this client from the server with the provided reason. This + /// has no effect if the client is already disconnected. + /// + /// All future calls to [`Self::is_disconnected`] will return `true`. pub fn disconnect(&mut self, reason: impl Into) { if self.send.is_some() { let txt = reason.into(); @@ -396,6 +507,8 @@ impl Client { } } + /// Like [`Self::disconnect`], but no reason for the disconnect is + /// displayed. pub fn disconnect_no_reason(&mut self) { if self.send.is_some() { log::info!("disconnecting client '{}'", self.username); @@ -403,11 +516,15 @@ impl Client { } } - pub fn data(&self) -> &Player { + /// Returns an immutable reference to the client's own [`Player`] data. + pub fn player(&self) -> &Player { &self.player_data } - pub fn data_mut(&mut self) -> &mut Player { + /// Returns a mutable reference to the client's own [`Player`] data. + /// + /// Changes made to this data is only visible to this client. + pub fn player_mut(&mut self) -> &mut Player { &mut self.player_data } @@ -442,7 +559,7 @@ impl Client { if client.pending_teleports == 0 { // TODO: validate movement using swept AABB collision with the blocks. // TODO: validate that the client is actually inside/outside the vehicle? - let event = ClientEvent::Movement { + let event = Event::Movement { position: client.new_position, yaw: client.yaw, pitch: client.pitch, @@ -485,7 +602,7 @@ impl Client { C2sPlayPacket::BlockEntityTagQuery(_) => {} C2sPlayPacket::ChangeDifficulty(_) => {} C2sPlayPacket::ChatCommand(_) => {} - C2sPlayPacket::Chat(p) => self.events.push_back(ClientEvent::ChatMessage { + C2sPlayPacket::Chat(p) => self.events.push_back(Event::ChatMessage { message: p.message.0, timestamp: Duration::from_millis(p.timestamp), }), @@ -502,7 +619,7 @@ impl Client { allow_server_listings: p.allow_server_listings, }); - self.events.push_back(ClientEvent::SettingsChanged(old)); + self.events.push_back(Event::SettingsChanged(old)); } C2sPlayPacket::CommandSuggestion(_) => {} C2sPlayPacket::ContainerButtonClick(_) => {} @@ -515,14 +632,14 @@ impl Client { // TODO: verify that the client has line of sight to the targeted entity and // that the distance is <=4 blocks. - self.events.push_back(ClientEvent::InteractWithEntity { + self.events.push_back(Event::InteractWithEntity { id, sneaking: p.sneaking, kind: match p.kind { - InteractKind::Interact(hand) => InteractWithEntity::Interact(hand), - InteractKind::Attack => InteractWithEntity::Attack, + InteractKind::Interact(hand) => InteractWithEntityKind::Interact(hand), + InteractKind::Attack => InteractWithEntityKind::Attack, InteractKind::InteractAt((target, hand)) => { - InteractWithEntity::InteractAt { target, hand } + InteractWithEntityKind::InteractAt { target, hand } } }, }); @@ -575,7 +692,7 @@ impl Client { ); } C2sPlayPacket::PaddleBoat(p) => { - self.events.push_back(ClientEvent::SteerBoat { + self.events.push_back(Event::SteerBoat { left_paddle_turning: p.left_paddle_turning, right_paddle_turning: p.right_paddle_turning, }); @@ -592,21 +709,21 @@ impl Client { } self.events.push_back(match p.status { - DiggingStatus::StartedDigging => ClientEvent::Digging(Digging { + DiggingStatus::StartedDigging => Event::Digging { status: event::DiggingStatus::Start, position: p.location, face: p.face, - }), - DiggingStatus::CancelledDigging => ClientEvent::Digging(Digging { + }, + DiggingStatus::CancelledDigging => Event::Digging { status: event::DiggingStatus::Cancel, position: p.location, face: p.face, - }), - DiggingStatus::FinishedDigging => ClientEvent::Digging(Digging { + }, + DiggingStatus::FinishedDigging => Event::Digging { status: event::DiggingStatus::Finish, position: p.location, face: p.face, - }), + }, DiggingStatus::DropItemStack => return, DiggingStatus::DropItem => return, DiggingStatus::ShootArrowOrFinishEating => return, @@ -623,31 +740,33 @@ impl Client { self.events.push_back(match e.action_id { PlayerCommandId::StartSneaking => { self.flags.set_sneaking(true); - ClientEvent::StartSneaking + Event::StartSneaking } PlayerCommandId::StopSneaking => { self.flags.set_sneaking(false); - ClientEvent::StopSneaking + Event::StopSneaking } - PlayerCommandId::LeaveBed => ClientEvent::LeaveBed, + PlayerCommandId::LeaveBed => Event::LeaveBed, PlayerCommandId::StartSprinting => { self.flags.set_sprinting(true); - ClientEvent::StartSprinting + Event::StartSprinting } PlayerCommandId::StopSprinting => { self.flags.set_sprinting(false); - ClientEvent::StopSprinting + Event::StopSprinting } PlayerCommandId::StartJumpWithHorse => { self.flags.set_jumping_with_horse(true); - ClientEvent::StartJumpWithHorse(e.jump_boost.0 .0 as u8) + Event::StartJumpWithHorse { + jump_boost: e.jump_boost.0 .0 as u8, + } } PlayerCommandId::StopJumpWithHorse => { self.flags.set_jumping_with_horse(false); - ClientEvent::StopJumpWithHorse + Event::StopJumpWithHorse } - PlayerCommandId::OpenHorseInventory => ClientEvent::OpenHorseInventory, - PlayerCommandId::StartFlyingWithElytra => ClientEvent::StartFlyingWithElytra, + PlayerCommandId::OpenHorseInventory => Event::OpenHorseInventory, + PlayerCommandId::StartFlyingWithElytra => Event::StartFlyingWithElytra, }); } C2sPlayPacket::PlayerInput(_) => {} @@ -666,7 +785,7 @@ impl Client { C2sPlayPacket::SetJigsawBlock(_) => {} C2sPlayPacket::SetStructureBlock(_) => {} C2sPlayPacket::SignUpdate(_) => {} - C2sPlayPacket::Swing(p) => self.events.push_back(ClientEvent::ArmSwing(p.hand)), + C2sPlayPacket::Swing(p) => self.events.push_back(Event::ArmSwing(p.hand)), C2sPlayPacket::TeleportToEntity(_) => {} C2sPlayPacket::UseItemOn(_) => {} C2sPlayPacket::UseItem(_) => {} @@ -715,7 +834,7 @@ impl Client { gamemode: self.new_game_mode, previous_gamemode: self.old_game_mode, dimension_names, - registry_codec: Nbt(make_dimension_codec(shared)), + registry_codec: Nbt(make_registry_codec(shared)), dimension_type_name: ident!( "{LIBRARY_NAMESPACE}:dimension_type_{}", world.meta.dimension().0 @@ -1128,21 +1247,21 @@ fn send_entity_events(send_opt: &mut SendOpt, id: EntityId, entity: &Entity) { } } -fn make_dimension_codec(shared: &SharedServer) -> RegistryCodec { +fn make_registry_codec(shared: &SharedServer) -> RegistryCodec { let mut dims = Vec::new(); for (id, dim) in shared.dimensions() { let id = id.0 as i32; dims.push(DimensionTypeRegistryEntry { name: ident!("{LIBRARY_NAMESPACE}:dimension_type_{id}"), id, - element: to_dimension_registry_item(dim), + element: dim.to_dimension_registry_item(), }) } - let mut biomes = Vec::new(); - for (id, biome) in shared.biomes() { - biomes.push(to_biome_registry_item(biome, id.0 as i32)); - } + 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. @@ -1151,7 +1270,7 @@ fn make_dimension_codec(shared: &SharedServer) -> RegistryCodec { if !biomes.iter().any(|b| b.name == ident!("plains")) { let biome = Biome::default(); assert_eq!(biome.name, ident!("plains")); - biomes.push(to_biome_registry_item(&biome, biomes.len() as i32)); + biomes.push(biome.to_biome_registry_item(biomes.len() as i32)); } RegistryCodec { @@ -1171,94 +1290,10 @@ fn make_dimension_codec(shared: &SharedServer) -> RegistryCodec { element: ChatType { chat: ChatTypeChat {}, narration: ChatTypeNarration { - priority: "system".to_owned(), + priority: "system".into(), }, }, }], }, } } - -fn to_dimension_registry_item(dim: &Dimension) -> DimensionType { - DimensionType { - piglin_safe: true, - has_raids: true, - monster_spawn_light_level: 0, - monster_spawn_block_light_limit: 0, - natural: dim.natural, - ambient_light: dim.ambient_light, - fixed_time: dim.fixed_time.map(|t| t as i64), - infiniburn: "#minecraft:infiniburn_overworld".into(), - respawn_anchor_works: true, - has_skylight: true, - bed_works: true, - effects: match dim.effects { - DimensionEffects::Overworld => ident!("overworld"), - DimensionEffects::TheNether => ident!("the_nether"), - DimensionEffects::TheEnd => ident!("the_end"), - }, - min_y: dim.min_y, - height: dim.height, - logical_height: dim.height, - coordinate_scale: 1.0, - ultrawarm: false, - has_ceiling: false, - } -} - -fn to_biome_registry_item(biome: &Biome, id: i32) -> BiomeRegistryBiome { - BiomeRegistryBiome { - name: biome.name.clone(), - id, - element: BiomeProperty { - precipitation: match biome.precipitation { - BiomePrecipitation::Rain => "rain", - BiomePrecipitation::Snow => "snow", - BiomePrecipitation::None => "none", - } - .into(), - depth: 0.125, - temperature: 0.8, - scale: 0.05, - downfall: 0.4, - category: "none".into(), - temperature_modifier: None, - effects: BiomeEffects { - sky_color: biome.sky_color as i32, - water_fog_color: biome.water_fog_color as i32, - fog_color: biome.fog_color as i32, - water_color: biome.water_color as i32, - foliage_color: biome.foliage_color.map(|x| x as i32), - grass_color: biome.grass_color.map(|x| x as i32), - grass_color_modifier: match biome.grass_color_modifier { - BiomeGrassColorModifier::Swamp => Some("swamp".into()), - BiomeGrassColorModifier::DarkForest => Some("dark_forest".into()), - BiomeGrassColorModifier::None => None, - }, - music: biome.music.as_ref().map(|bm| BiomeMusic { - replace_current_music: bm.replace_current_music, - sound: bm.sound.clone(), - max_delay: bm.max_delay, - min_delay: bm.min_delay, - }), - ambient_sound: biome.ambient_sound.clone(), - additions_sound: biome.additions_sound.as_ref().map(|a| BiomeAdditionsSound { - sound: a.sound.clone(), - tick_chance: a.tick_chance, - }), - mood_sound: biome.mood_sound.as_ref().map(|m| BiomeMoodSound { - sound: m.sound.clone(), - tick_delay: m.tick_delay, - offset: m.offset, - block_search_extent: m.block_search_extent, - }), - }, - particle: biome.particle.as_ref().map(|p| BiomeParticle { - probability: p.probability, - options: BiomeParticleOptions { - kind: p.kind.clone(), - }, - }), - }, - } -} diff --git a/src/client/event.rs b/src/client/event.rs index eb58273..537cee9 100644 --- a/src/client/event.rs +++ b/src/client/event.rs @@ -4,50 +4,76 @@ use vek::Vec3; use crate::block_pos::BlockPos; use crate::entity::EntityId; -use crate::protocol::packets::play::c2s::BlockFace; -pub use crate::protocol::packets::play::c2s::{ChatMode, DisplayedSkinParts, Hand, MainHand}; -pub use crate::protocol::packets::play::s2c::GameMode; +use crate::protocol_inner::packets::play::c2s::BlockFace; +pub use crate::protocol_inner::packets::play::c2s::{ChatMode, DisplayedSkinParts, Hand, MainHand}; +pub use crate::protocol_inner::packets::play::s2c::GameMode; +/// Represents an action performed by a client. +/// +/// Client events can be obtained from +/// [`pop_event`](crate::client::Client::pop_event). #[derive(Debug)] -pub enum ClientEvent { +pub enum Event { + /// A regular message was sent to the chat. ChatMessage { + /// The content of the message message: String, + /// The time the message was sent. timestamp: Duration, }, - /// Settings were changed. The value in this variant is the previous client - /// settings. + /// Settings were changed. The value in this variant is the _previous_ + /// client settings. SettingsChanged(Option), - /// The client has moved. The values in this + /// The client moved. The values in this /// variant are the _previous_ position and look. Movement { + /// Absolute coordinates of the previous position. position: Vec3, + /// The previous yaw (in degrees). yaw: f32, + /// The previous pitch (in degrees). pitch: f32, + /// If the client was previously on the ground. on_ground: bool, }, StartSneaking, StopSneaking, StartSprinting, StopSprinting, - StartJumpWithHorse(u8), + /// A jump while on a horse started. + StartJumpWithHorse { + /// The power of the horse jump. + jump_boost: u8, + }, + /// A jump while on a horse stopped. StopJumpWithHorse, + /// The client left a bed. LeaveBed, + /// The inventory was opened while on a horse. OpenHorseInventory, StartFlyingWithElytra, ArmSwing(Hand), + /// Left or right click interaction with an entity's hitbox. InteractWithEntity { /// The ID of the entity being interacted with. id: EntityId, /// If the client was sneaking during the interaction. sneaking: bool, - /// The type of interaction that occurred. - kind: InteractWithEntity, + /// The kind of interaction that occurred. + kind: InteractWithEntityKind, }, SteerBoat { left_paddle_turning: bool, right_paddle_turning: bool, }, - Digging(Digging), + Digging { + /// The kind of digging event this is. + status: DiggingStatus, + /// The position of the block being broken. + position: BlockPos, + /// The face of the block being broken. + face: BlockFace, + }, } #[derive(Clone, PartialEq, Debug)] @@ -67,22 +93,18 @@ pub struct Settings { } #[derive(Clone, PartialEq, Debug)] -pub enum InteractWithEntity { +pub enum InteractWithEntityKind { Interact(Hand), InteractAt { target: Vec3, hand: Hand }, Attack, } -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub struct Digging { - pub status: DiggingStatus, - pub position: BlockPos, - pub face: BlockFace, -} - #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum DiggingStatus { + /// The client started digging a block. Start, + /// The client stopped digging a block before it was fully broken. Cancel, + /// The client finished digging a block successfully. Finish, } diff --git a/src/config.rs b/src/config.rs index 499dd5d..6ef7625 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,14 +15,15 @@ use crate::Ticks; /// server. /// /// The config is used from multiple threads and must therefore implement -/// `Send` and `Sync`. From within a single thread, methods are never invoked -/// recursively by the library. In other words, a mutex can be aquired at -/// the beginning of a method and released at the end without risk of +/// [`Send`] and [`Sync`]. From within a single thread, methods are never +/// invoked recursively by the library. In other words, a mutex can be aquired +/// at the beginning of a method and released at the end without risk of /// deadlocking. /// -/// This trait uses the [async_trait](https://docs.rs/async-trait/latest/async_trait/) attribute macro. -/// This will be removed once `impl Trait` in return position in traits is -/// available in stable rust. +/// This trait uses the [async_trait] attribute macro. It is exported at the +/// root of this crate. +/// +/// [async_trait]: https://docs.rs/async-trait/latest/async_trait/ #[async_trait] #[allow(unused_variables)] pub trait Config: Any + Send + Sync + UnwindSafe + RefUnwindSafe { @@ -39,6 +40,7 @@ pub trait Config: Any + Send + Sync + UnwindSafe + RefUnwindSafe { /// be bound to. /// /// # Default Implementation + /// /// Returns `127.0.0.1:25565`. fn address(&self) -> SocketAddr { SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 25565).into() @@ -57,6 +59,7 @@ pub trait Config: Any + Send + Sync + UnwindSafe + RefUnwindSafe { /// so there is little benefit to a tick rate higher than 20. /// /// # Default Implementation + /// /// Returns `20`, which is the same as Minecraft's official server. fn tick_rate(&self) -> Ticks { 20 @@ -73,6 +76,7 @@ pub trait Config: Any + Send + Sync + UnwindSafe + RefUnwindSafe { /// internet. /// /// # Default Implementation + /// /// Returns `true`. fn online_mode(&self) -> bool { true @@ -85,6 +89,7 @@ pub trait Config: Any + Send + Sync + UnwindSafe + RefUnwindSafe { /// potential memory usage. /// /// # Default Implementation + /// /// An unspecified value is returned that should be adequate in most /// situations. fn incoming_packet_capacity(&self) -> usize { @@ -98,6 +103,7 @@ pub trait Config: Any + Send + Sync + UnwindSafe + RefUnwindSafe { /// but increases potential memory usage. /// /// # Default Implementation + /// /// An unspecified value is returned that should be adequate in most /// situations. fn outgoing_packet_capacity(&self) -> usize { @@ -111,6 +117,7 @@ pub trait Config: Any + Send + Sync + UnwindSafe + RefUnwindSafe { /// runtime. /// /// # Default Implementation + /// /// Returns `None`. fn tokio_handle(&self) -> Option { None @@ -119,14 +126,15 @@ pub trait Config: Any + Send + Sync + UnwindSafe + RefUnwindSafe { /// Called once at startup to get the list of [`Dimension`]s usable on the /// server. /// - /// The dimensions traversed by [`Server::dimensions`] will be in the same - /// order as the `Vec` returned by this function. + /// The dimensions returned by [`SharedServer::dimensions`] will be in the + /// same order as the `Vec` returned by this function. /// - /// The number of elements in the returned `Vec` must be in \[1, u16::MAX]. + /// The number of elements in the returned `Vec` must be in `1..=u16::MAX`. /// Additionally, the documented requirements on the fields of [`Dimension`] /// must be met. /// /// # Default Implementation + /// /// Returns `vec![Dimension::default()]`. fn dimensions(&self) -> Vec { vec![Dimension::default()] @@ -135,14 +143,15 @@ pub trait Config: Any + Send + Sync + UnwindSafe + RefUnwindSafe { /// Called once at startup to get the list of [`Biome`]s usable on the /// server. /// - /// The biomes traversed by [`Server::biomes`] will be in the same + /// The biomes returned by [`SharedServer::biomes`] will be in the same /// order as the `Vec` returned by this function. /// - /// The number of elements in the returned `Vec` must be in \[1, u16::MAX]. + /// The number of elements in the returned `Vec` must be in `1..=u16::MAX`. /// Additionally, the documented requirements on the fields of [`Biome`] /// must be met. /// /// # Default Implementation + /// /// Returns `vec![Dimension::default()]`. fn biomes(&self) -> Vec { vec![Biome::default()] @@ -154,6 +163,7 @@ pub trait Config: Any + Send + Sync + UnwindSafe + RefUnwindSafe { /// This method is called from within a tokio runtime. /// /// # Default Implementation + /// /// The query is ignored. async fn server_list_ping( &self, @@ -164,10 +174,10 @@ pub trait Config: Any + Send + Sync + UnwindSafe + RefUnwindSafe { } /// Called asynchronously for each client after successful authentication - /// (if online mode is enabled) to determine if they can continue to join - /// the server. On success, [`Config::join`] is called with the new - /// client. If this method returns with `Err(reason)`, then the client is - /// immediately disconnected with the given reason. + /// (if online mode is enabled) to determine if they can join + /// the server. On success, the new client is added to the server's + /// [`Clients`]. If this method returns with `Err(reason)`, then the + /// client is immediately disconnected with the given reason. /// /// This method is the appropriate place to perform asynchronous /// operations such as database queries which may take some time to @@ -176,7 +186,10 @@ pub trait Config: Any + Send + Sync + UnwindSafe + RefUnwindSafe { /// This method is called from within a tokio runtime. /// /// # Default Implementation + /// /// The client is allowed to join unconditionally. + /// + /// [`Clients`]: crate::client::Clients async fn login(&self, shared: &SharedServer, ncd: &NewClientData) -> Result<(), Text> { Ok(()) } @@ -191,25 +204,29 @@ pub trait Config: Any + Send + Sync + UnwindSafe + RefUnwindSafe { fn init(&self, server: &mut Server) {} /// Called once at the beginning of every server update (also known as - /// a "tick"). + /// "tick"). This is likely where the majority of your code will be. /// - /// The frequency of server updates can be configured by `update_duration` - /// in [`ServerConfig`]. + /// The frequency of ticks can be configured by [`Self::tick_rate`]. /// /// This method is called from within a tokio runtime. /// /// # Default Implementation + /// /// The default implementation does nothing. fn update(&self, server: &mut Server); } -/// The result of the [`server_list_ping`](Handler::server_list_ping) callback. +/// The result of the [`server_list_ping`](Config::server_list_ping) callback. #[derive(Debug)] pub enum ServerListPing<'a> { /// Responds to the server list ping with the given information. Respond { + /// Displayed as the number of players on the server. online_players: i32, + /// Displayed as the maximum number of players allowed on the server at + /// a time. max_players: i32, + /// A description of the server. description: Text, /// The server's icon as the bytes of a PNG image. /// The image must be 64x64 pixels. diff --git a/src/dimension.rs b/src/dimension.rs index 9c4aa43..1c4e101 100644 --- a/src/dimension.rs +++ b/src/dimension.rs @@ -1,17 +1,18 @@ -/// A handle to a particular [`Dimension`] on the server. +use crate::ident; +use crate::protocol_inner::packets::play::s2c::DimensionType; + +/// Identifies a particular [`Dimension`] on the server. /// -/// Dimension IDs must only be used on servers from which they originate. +/// The default dimension ID refers to the first dimension added in the server's +/// [configuration](crate::config::Config). +/// +/// To obtain dimension IDs for other dimensions, call +/// [`dimensions`](crate::server::SharedServer::dimensions). #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct DimensionId(pub(crate) u16); -impl DimensionId { - pub fn to_index(self) -> usize { - self.0 as usize - } -} - /// The default dimension ID corresponds to the first element in the `Vec` -/// returned by [`Config::dimensions`]. +/// returned by [`crate::config::Config::dimensions`]. impl Default for DimensionId { fn default() -> Self { Self(0) @@ -20,14 +21,19 @@ impl Default for DimensionId { /// Contains the configuration for a dimension type. /// -/// In Minecraft, "dimension" and "dimension type" are two different concepts. +/// On creation, each [`World`] in Valence is assigned a dimension. The +/// dimension determines certain properties of the world such as its height and +/// ambient lighting. +/// +/// In Minecraft, "dimension" and "dimension type" are two distinct 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 [`Dimension`] represents a -/// dimension type. +/// [`World`] while [`Dimension`] represents a dimension type. +/// +/// [`World`]: crate::world::World #[derive(Clone, Debug)] pub struct Dimension { - /// When false, compases will spin randomly. + /// When false, compasses will spin randomly. pub natural: bool, /// Must be between 0.0 and 1.0. pub ambient_light: f32, @@ -63,21 +69,52 @@ pub struct Dimension { // * has_ceiling } +impl Dimension { + pub(crate) fn to_dimension_registry_item(&self) -> DimensionType { + DimensionType { + piglin_safe: true, + has_raids: true, + monster_spawn_light_level: 0, + monster_spawn_block_light_limit: 0, + natural: self.natural, + ambient_light: self.ambient_light, + fixed_time: self.fixed_time.map(|t| t as i64), + infiniburn: "#minecraft:infiniburn_overworld".into(), + respawn_anchor_works: true, + has_skylight: true, + bed_works: true, + effects: match self.effects { + DimensionEffects::Overworld => ident!("overworld"), + DimensionEffects::TheNether => ident!("the_nether"), + DimensionEffects::TheEnd => ident!("the_end"), + }, + min_y: self.min_y, + height: self.height, + logical_height: self.height, + coordinate_scale: 1.0, + ultrawarm: false, + has_ceiling: false, + } + } +} + impl Default for Dimension { fn default() -> Self { Self { natural: true, ambient_light: 1.0, fixed_time: None, - effects: DimensionEffects::Overworld, + effects: DimensionEffects::default(), min_y: -64, height: 384, } } } -#[derive(Clone, Copy, PartialEq, Eq, Debug)] +/// Determines what skybox/fog effects to use in dimensions. +#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)] pub enum DimensionEffects { + #[default] Overworld, TheNether, TheEnd, diff --git a/src/entity.rs b/src/entity.rs index a2ed36d..1d70318 100644 --- a/src/entity.rs +++ b/src/entity.rs @@ -1,5 +1,5 @@ pub mod data; -pub mod meta; +pub mod types; use std::collections::hash_map::Entry; use std::collections::HashMap; @@ -7,19 +7,29 @@ use std::iter::FusedIterator; use std::num::NonZeroU32; use bitfield_struct::bitfield; -pub use data::{EntityData, EntityKind}; use rayon::iter::ParallelIterator; +pub use types::{EntityData, EntityKind}; use uuid::Uuid; use vek::{Aabb, Vec3}; -use crate::protocol::packets::play::s2c::{ +use crate::protocol_inner::packets::play::s2c::{ AddEntity, AddExperienceOrb, AddPlayer, S2cPlayPacket, SetEntityMetadata, }; -use crate::protocol::{ByteAngle, RawBytes, VarInt}; +use crate::protocol_inner::{ByteAngle, RawBytes, VarInt}; use crate::slotmap::{Key, SlotMap}; use crate::util::aabb_from_bottom_and_size; use crate::world::WorldId; +/// A container for all [`Entity`]s on a [`Server`](crate::server::Server). +/// +/// # Spawning Player Entities +/// +/// [`Player`] entities are treated specially by the client. For the player +/// entity to be visible to clients, the player's UUID must be added to the +/// [`PlayerList`] _before_ being loaded by the client. +/// +/// [`Player`]: crate::entity::types::Player +/// [`PlayerList`]: crate::player_list::PlayerList pub struct Entities { sm: SlotMap, uuid_to_entity: HashMap, @@ -35,18 +45,18 @@ impl Entities { } } - /// Spawns a new entity with the default data. The new entity's [`EntityId`] - /// is returned. + /// Spawns a new entity with a random UUID. A reference to the entity along + /// with its ID is returned. pub fn create(&mut self, kind: EntityKind) -> (EntityId, &mut Entity) { self.create_with_uuid(kind, Uuid::from_bytes(rand::random())) .expect("UUID collision") } - /// Like [`create`](Entities::create), but requires specifying the new + /// Like [`Self::create`], but requires specifying the new /// entity's UUID. /// - /// The provided UUID must not conflict with an existing entity UUID in this - /// world. If it does, `None` is returned and the entity is not spawned. + /// The provided UUID must not conflict with an existing entity UUID. If it + /// does, `None` is returned and the entity is not spawned. pub fn create_with_uuid( &mut self, kind: EntityKind, @@ -58,7 +68,7 @@ impl Entities { let (k, e) = self.sm.insert(Entity { flags: EntityFlags(0), data: EntityData::new(kind), - world: None, + world: WorldId::NULL, new_position: Vec3::default(), old_position: Vec3::default(), yaw: 0.0, @@ -78,6 +88,10 @@ impl Entities { } } + /// Removes an entity from the server. + /// + /// If the given entity ID is valid, `true` is returned and the entity is + /// deleted. Otherwise, `false` is returned and the function has no effect. pub fn delete(&mut self, entity: EntityId) -> bool { if let Some(e) = self.sm.remove(entity.0) { self.uuid_to_entity @@ -94,6 +108,9 @@ impl Entities { } } + /// Removes all entities from the server for which `f` returns `true`. + /// + /// All entities are visited in an unspecified order. pub fn retain(&mut self, mut f: impl FnMut(EntityId, &mut Entity) -> bool) { self.sm.retain(|k, v| { if f(EntityId(k), v) { @@ -112,7 +129,7 @@ impl Entities { }); } - /// Returns the number of live entities. + /// Returns the number of entities in this container. pub fn count(&self) -> usize { self.sm.len() } @@ -120,16 +137,21 @@ impl Entities { /// Gets the [`EntityId`] of the entity with the given UUID in an efficient /// manner. /// - /// Returns `None` if there is no entity with the provided UUID. Returns - /// `Some` otherwise. + /// If there is no entity with the UUID, `None` is returned. pub fn get_with_uuid(&self, uuid: Uuid) -> Option { self.uuid_to_entity.get(&uuid).cloned() } + /// Gets a shared reference to the entity with the given [`EntityId`]. + /// + /// If the ID is invalid, `None` is returned. pub fn get(&self, entity: EntityId) -> Option<&Entity> { self.sm.get(entity.0) } + /// Gets an exclusive reference to the entity with the given [`EntityId`]. + /// + /// If the ID is invalid, `None` is returned. pub fn get_mut(&mut self, entity: EntityId) -> Option<&mut Entity> { self.sm.get_mut(entity.0) } @@ -140,18 +162,26 @@ impl Entities { Some(EntityId(Key::new(index, version))) } + /// Returns an immutable iterator over all entities on the server in an + /// unspecified order. pub fn iter(&self) -> impl FusedIterator + Clone + '_ { self.sm.iter().map(|(k, v)| (EntityId(k), v)) } + /// Returns a mutable iterator over all entities on the server in an + /// unspecified order. pub fn iter_mut(&mut self) -> impl FusedIterator + '_ { self.sm.iter_mut().map(|(k, v)| (EntityId(k), v)) } + /// Returns a parallel immutable iterator over all entities on the server in + /// an unspecified order. pub fn par_iter(&self) -> impl ParallelIterator + Clone + '_ { self.sm.par_iter().map(|(k, v)| (EntityId(k), v)) } + /// Returns a parallel mutable iterator over all clients on the server in an + /// unspecified order. pub fn par_iter_mut(&mut self) -> impl ParallelIterator + '_ { self.sm.par_iter_mut().map(|(k, v)| (EntityId(k), v)) } @@ -168,10 +198,20 @@ impl Entities { } } +/// A key for an [`Entity`] on the server. +/// +/// Entity IDs are either _valid_ or _invalid_. Valid entity IDs point to +/// entities that have not been deleted, while invalid IDs point to those that +/// have. Once an ID becomes invalid, it will never become valid again. +/// +/// The [`Ord`] instance on this type is correct but otherwise unspecified. This +/// is useful for storing IDs in containers such as +/// [`BTreeMap`](std::collections::BTreeMap). #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] pub struct EntityId(Key); impl EntityId { + /// The value of the default entity ID which is always invalid. pub const NULL: Self = Self(Key::NULL); pub(crate) fn to_network_id(self) -> i32 { @@ -179,10 +219,19 @@ impl EntityId { } } +/// Represents an entity on the server. +/// +/// In essence, an entity is anything in a world that isn't a block or client. +/// Entities include paintings, falling blocks, zombies, fireballs, and more. +/// +/// Every entity has common state which is accessible directly from +/// this struct. This includes position, rotation, velocity, UUID, and hitbox. +/// To access data that is not common to every kind of entity, see +/// [`Self::data`]. pub struct Entity { flags: EntityFlags, data: EntityData, - world: Option, + world: WorldId, new_position: Vec3, old_position: Vec3, yaw: f32, @@ -192,8 +241,6 @@ pub struct Entity { uuid: Uuid, } -/// Contains a bit for certain fields in [`Entity`] to track if they have been -/// modified. #[bitfield(u8)] pub(crate) struct EntityFlags { pub yaw_or_pitch_modified: bool, @@ -209,51 +256,64 @@ impl Entity { self.flags } - /// Returns a reference to this entity's [`EntityData`]. + /// Gets a reference to this entity's [`EntityData`]. pub fn data(&self) -> &EntityData { &self.data } - /// Returns a mutable reference to this entity's [`EntityData`]. + /// Gets a mutable reference to this entity's + /// [`EntityData`]. pub fn data_mut(&mut self) -> &mut EntityData { &mut self.data } - /// Returns the [`EntityKind`] of this entity. + /// Gets the [`EntityKind`] of this entity. pub fn kind(&self) -> EntityKind { self.data.kind() } - pub fn world(&self) -> Option { + /// Gets the [`WorldId`](crate::world::WorldId) of the world this entity is + /// located in. + /// + /// By default, entities are located in + /// [`WorldId::NULL`](crate::world::WorldId::NULL). + pub fn world(&self) -> WorldId { self.world } - pub fn set_world(&mut self, world: impl Into>) { - self.world = world.into(); + /// Sets the world this entity is located in. + pub fn set_world(&mut self, world: WorldId) { + self.world = world; } - /// Returns the position of this entity in the world it inhabits. + /// Gets the position of this entity in the world it inhabits. + /// + /// The position of an entity is located on the botton of its + /// hitbox and not the center. pub fn position(&self) -> Vec3 { self.new_position } /// Sets the position of this entity in the world it inhabits. + /// + /// The position of an entity is located on the botton of its + /// hitbox and not the center. pub fn set_position(&mut self, pos: impl Into>) { self.new_position = pos.into(); } /// Returns the position of this entity as it existed at the end of the /// previous tick. - pub fn old_position(&self) -> Vec3 { + pub(crate) fn old_position(&self) -> Vec3 { self.old_position } - /// Gets the yaw of this entity (in degrees). + /// Gets the yaw of this entity in degrees. pub fn yaw(&self) -> f32 { self.yaw } - /// Sets the yaw of this entity (in degrees). + /// Sets the yaw of this entity in degrees. pub fn set_yaw(&mut self, yaw: f32) { if self.yaw != yaw { self.yaw = yaw; @@ -261,12 +321,12 @@ impl Entity { } } - /// Gets the pitch of this entity (in degrees). + /// Gets the pitch of this entity in degrees. pub fn pitch(&self) -> f32 { self.pitch } - /// Sets the pitch of this entity (in degrees). + /// Sets the pitch of this entity in degrees. pub fn set_pitch(&mut self, pitch: f32) { if self.pitch != pitch { self.pitch = pitch; @@ -274,12 +334,12 @@ impl Entity { } } - /// Gets the head yaw of this entity (in degrees). + /// Gets the head yaw of this entity in degrees. pub fn head_yaw(&self) -> f32 { self.head_yaw } - /// Sets the head yaw of this entity (in degrees). + /// Sets the head yaw of this entity in degrees. pub fn set_head_yaw(&mut self, head_yaw: f32) { if self.head_yaw != head_yaw { self.head_yaw = head_yaw; @@ -292,6 +352,7 @@ impl Entity { self.velocity } + /// Sets the velocity of this entity in meters per second. pub fn set_velocity(&mut self, velocity: impl Into>) { let new_vel = velocity.into(); @@ -301,18 +362,30 @@ impl Entity { } } + /// Gets the value of the "on ground" flag. pub fn on_ground(&self) -> bool { self.flags.on_ground() } + /// Sets the value of the "on ground" flag. pub fn set_on_ground(&mut self, on_ground: bool) { self.flags.set_on_ground(on_ground); } + /// Gets the UUID of this entity. pub fn uuid(&self) -> Uuid { self.uuid } + /// Returns the hitbox of this entity. + /// + /// The hitbox describes the space that an entity occupies. Clients interact + /// with this space to create an [interact event]. + /// + /// The hitbox of an entity is determined by its position, entity type, and + /// other state specific to that type. + /// + /// [interact event]: crate::client::Event::InteractWithEntity pub fn hitbox(&self) -> Aabb { let dims = match &self.data { EntityData::Allay(_) => [0.6, 0.35, 0.6], diff --git a/src/entity/data.rs b/src/entity/data.rs index 801642b..01dfdf2 100644 --- a/src/entity/data.rs +++ b/src/entity/data.rs @@ -1,10 +1,216 @@ -#![allow(clippy::all, missing_docs)] +//! Types used in [`EntityData`](crate::entity::EntityData). -use crate::block::{BlockPos, BlockState}; -use crate::entity::meta::*; -use crate::entity::EntityId; -use crate::protocol::{Encode, VarInt}; -use crate::text::Text; -use crate::uuid::Uuid; +use std::io::Write; -include!(concat!(env!("OUT_DIR"), "/entity.rs")); +use crate::protocol_inner::{Encode, VarInt}; + +#[derive(Clone, Copy, Default, PartialEq, PartialOrd, Debug)] +pub struct ArmorStandRotations { + /// Rotation on the X axis in degrees. + pub x: f32, + /// Rotation on the Y axis in degrees. + pub y: f32, + /// Rotation on the Z axis in degrees. + pub z: f32, +} + +impl ArmorStandRotations { + pub fn new(x: f32, y: f32, z: f32) -> Self { + Self { x, y, z } + } +} + +impl Encode for ArmorStandRotations { + fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { + self.x.encode(w)?; + self.y.encode(w)?; + self.z.encode(w) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub enum Direction { + Down, + Up, + North, + South, + West, + East, +} + +impl Encode for Direction { + fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { + VarInt(*self as i32).encode(w) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct VillagerData { + pub kind: VillagerKind, + pub profession: VillagerProfession, + pub level: i32, +} + +impl VillagerData { + pub const fn new(kind: VillagerKind, profession: VillagerProfession, level: i32) -> Self { + Self { + kind, + profession, + level, + } + } +} + +impl Default for VillagerData { + fn default() -> Self { + Self { + kind: Default::default(), + profession: Default::default(), + level: 1, + } + } +} + +impl Encode for VillagerData { + fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { + VarInt(self.kind as i32).encode(w)?; + VarInt(self.profession as i32).encode(w)?; + VarInt(self.level).encode(w) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] +pub enum VillagerKind { + Desert, + Jungle, + #[default] + Plains, + Savanna, + Snow, + Swamp, + Taiga, +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] +pub enum VillagerProfession { + #[default] + None, + Armorer, + Butcher, + Cartographer, + Cleric, + Farmer, + Fisherman, + Fletcher, + Leatherworker, + Librarian, + Mason, + Nitwit, + Shepherd, + Toolsmith, + Weaponsmith, +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] +pub enum Pose { + #[default] + Standing, + FallFlying, + Sleeping, + Swimming, + SpinAttack, + Sneaking, + LongJumping, + Dying, + Croaking, + UsingTongue, + Roaring, + Sniffing, + Emerging, + Digging, +} + +impl Encode for Pose { + fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { + VarInt(*self as i32).encode(w) + } +} + +/// The main hand of a player. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] +pub enum MainHand { + Left, + #[default] + Right, +} + +impl Encode for MainHand { + fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { + (*self as u8).encode(w) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] +pub enum BoatKind { + #[default] + Oak, + Spruce, + Birch, + Jungle, + Acacia, + DarkOak, +} + +impl Encode for BoatKind { + fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { + VarInt(*self as i32).encode(w) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] +pub enum CatKind { + Tabby, + #[default] + Black, + Red, + Siamese, + BritishShorthair, + Calico, + Persian, + Ragdoll, + White, + Jellie, + AllBlack, +} + +impl Encode for CatKind { + fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { + VarInt(*self as i32).encode(w) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] +pub enum FrogKind { + #[default] + Temperate, + Warm, + Cold, +} + +impl Encode for FrogKind { + fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { + VarInt(*self as i32).encode(w) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] +pub enum PaintingKind { + #[default] + Default, // TODO +} + +impl Encode for PaintingKind { + fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { + VarInt(*self as i32).encode(w) + } +} diff --git a/src/entity/meta.rs b/src/entity/meta.rs deleted file mode 100644 index 026ec01..0000000 --- a/src/entity/meta.rs +++ /dev/null @@ -1,216 +0,0 @@ -#![allow(missing_docs)] - -use std::io::Write; - -use crate::protocol::{Encode, VarInt}; - -#[derive(Clone, Copy, Default, PartialEq, PartialOrd, Debug)] -pub struct ArmorStandRotations { - /// Rotation on the X axis in degrees. - pub x: f32, - /// Rotation on the Y axis in degrees. - pub y: f32, - /// Rotation on the Z axis in degrees. - pub z: f32, -} - -impl ArmorStandRotations { - pub fn new(x: f32, y: f32, z: f32) -> Self { - Self { x, y, z } - } -} - -impl Encode for ArmorStandRotations { - fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { - self.x.encode(w)?; - self.y.encode(w)?; - self.z.encode(w) - } -} - -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] -pub enum Direction { - Down, - Up, - North, - South, - West, - East, -} - -impl Encode for Direction { - fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { - VarInt(*self as i32).encode(w) - } -} - -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] -pub struct VillagerData { - pub kind: VillagerKind, - pub profession: VillagerProfession, - pub level: i32, -} - -impl VillagerData { - pub const fn new(kind: VillagerKind, profession: VillagerProfession, level: i32) -> Self { - Self { - kind, - profession, - level, - } - } -} - -impl Default for VillagerData { - fn default() -> Self { - Self { - kind: Default::default(), - profession: Default::default(), - level: 1, - } - } -} - -impl Encode for VillagerData { - fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { - VarInt(self.kind as i32).encode(w)?; - VarInt(self.profession as i32).encode(w)?; - VarInt(self.level).encode(w) - } -} - -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] -pub enum VillagerKind { - Desert, - Jungle, - #[default] - Plains, - Savanna, - Snow, - Swamp, - Taiga, -} - -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] -pub enum VillagerProfession { - #[default] - None, - Armorer, - Butcher, - Cartographer, - Cleric, - Farmer, - Fisherman, - Fletcher, - Leatherworker, - Librarian, - Mason, - Nitwit, - Shepherd, - Toolsmith, - Weaponsmith, -} - -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] -pub enum Pose { - #[default] - Standing, - FallFlying, - Sleeping, - Swimming, - SpinAttack, - Sneaking, - LongJumping, - Dying, - Croaking, - UsingTongue, - Roaring, - Sniffing, - Emerging, - Digging, -} - -impl Encode for Pose { - fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { - VarInt(*self as i32).encode(w) - } -} - -/// The main hand of a player. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] -pub enum MainHand { - Left, - #[default] - Right, -} - -impl Encode for MainHand { - fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { - (*self as u8).encode(w) - } -} - -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] -pub enum BoatKind { - #[default] - Oak, - Spruce, - Birch, - Jungle, - Acacia, - DarkOak, -} - -impl Encode for BoatKind { - fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { - VarInt(*self as i32).encode(w) - } -} - -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] -pub enum CatKind { - Tabby, - #[default] - Black, - Red, - Siamese, - BritishShorthair, - Calico, - Persian, - Ragdoll, - White, - Jellie, - AllBlack, -} - -impl Encode for CatKind { - fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { - VarInt(*self as i32).encode(w) - } -} - -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] -pub enum FrogKind { - #[default] - Temperate, - Warm, - Cold, -} - -impl Encode for FrogKind { - fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { - VarInt(*self as i32).encode(w) - } -} - -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] -pub enum PaintingKind { - #[default] - Default, // TODO -} - -impl Encode for PaintingKind { - fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { - VarInt(*self as i32).encode(w) - } -} diff --git a/src/entity/types.rs b/src/entity/types.rs new file mode 100644 index 0000000..2712ac1 --- /dev/null +++ b/src/entity/types.rs @@ -0,0 +1,12 @@ +//! Contains a struct for each variant in [`EntityKind`]. + +#![allow(clippy::all, missing_docs)] + +use crate::block::{BlockPos, BlockState}; +use crate::entity::data::*; +use crate::entity::EntityId; +use crate::protocol_inner::{Encode, VarInt}; +use crate::text::Text; +use crate::uuid::Uuid; + +include!(concat!(env!("OUT_DIR"), "/entity.rs")); diff --git a/src/ident.rs b/src/ident.rs index 1568ef1..0f761c0 100644 --- a/src/ident.rs +++ b/src/ident.rs @@ -7,15 +7,15 @@ use serde::de::Visitor; use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::protocol::{encode_string_bounded, BoundedString, Decode, Encode}; +use crate::protocol_inner::{encode_string_bounded, BoundedString, Decode, Encode}; /// An identifier is a string split into a "namespace" part and a "name" part. /// For instance `minecraft:apple` and `apple` are both valid identifiers. /// /// If the namespace part is left off (the part before and including the colon) -/// the namespace is considered to be "minecraft". +/// the namespace is considered to be "minecraft" for the purposes of equality. /// -/// The entire identifier must match the regex `([a-z0-9_-]+:)?[a-z0-9_\/.-]+`. +/// The identifier must match the regex `^([a-z0-9_-]+:)?[a-z0-9_\/.-]+$`. #[derive(Clone, Eq)] pub struct Ident { ident: Cow<'static, AsciiStr>, @@ -239,10 +239,26 @@ impl<'de> Visitor<'de> for IdentifierVisitor { } } -/// Convenience macro for constructing an identifier from a format string. +/// Convenience macro for constructing an [`Ident`] from a format string. /// -/// The macro will panic if the formatted string is not a valid +/// The arguments to this macro are forwarded to [`std::format_args`]. +/// +/// # Panics +/// +/// The macro will cause a panic if the formatted string is not a valid /// identifier. +/// +/// # Examples +/// +/// ``` +/// use valence::ident; +/// +/// let namespace = "my_namespace"; +/// let apple = ident!("{namespace}:apple"); +/// +/// assert_eq!(apple.namespace(), Some("my_namespace")); +/// assert_eq!(apple.name(), "apple"); +/// ``` #[macro_export] macro_rules! ident { ($($arg:tt)*) => {{ diff --git a/src/lib.rs b/src/lib.rs index a2df103..1830f43 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,111 @@ +//! A Rust framework for building efficient Minecraft servers. +//! +//! Valence is a Rust library which provides the necessary abstractions over +//! Minecraft's protocol to build servers. Very few assumptions about the +//! desired server are made, which allows for greater flexibility in its design. +//! +//! At a high level, a Valence [`Server`] is a collection of [`Clients`], +//! [`Entities`], and [`Worlds`]. When a client connects to the server, they are +//! added to the server's [`Clients`]. After connecting, clients are assigned to +//! a [`World`] where they are able to interact with the entities and +//! [`Chunks`] that are a part of it. +//! +//! The Valence documentation assumes some familiarity with Minecraft and its +//! mechanics. See the [Minecraft Wiki] for general information and [wiki.vg] +//! for protocol documentation. +//! +//! [Minecraft Wiki]: https://minecraft.fandom.com/wiki/Minecraft_Wiki +//! [wiki.vg]: https://wiki.vg/Main_Page +//! +//! # Logging +//! +//! Valence uses the [log] crate to report errors and other information. You may +//! want to use a logging implementation such as [env_logger] to see these +//! messages. +//! +//! [log]: https://docs.rs/log/latest/log/ +//! [env_logger]: https://docs.rs/env_logger/latest/env_logger/ +//! +//! # An Important Note on [`mem::swap`] +//! +//! In Valence, many types are owned by the library but given out as mutable +//! references for the user to modify. Examples of such types include [`World`], +//! [`Chunk`], [`Entity`], and [`Client`]. +//! +//! **You must not call [`mem::swap`] on these references (or any other +//! function that would move their location in memory).** Doing so breaks +//! invariants within the library and the resulting behavior is unspecified. +//! These types should be considered pinned in memory. +//! +//! Preventing this illegal behavior using Rust's type system was considered too +//! cumbersome, so a note has been left here instead. +//! +//! [`mem::swap`]: std::mem::swap +//! +//! # Examples +//! +//! The following is a minimal server implementation. You should be able to +//! connect to the server at `localhost`. +//! +//! ``` +//! use valence::config::Config; +//! use valence::server::{Server, ShutdownResult}; +//! +//! pub fn main() -> ShutdownResult { +//! valence::start_server(Game) +//! } +//! +//! struct Game; +//! +//! impl Config for Game { +//! fn max_connections(&self) -> usize { +//! 256 +//! } +//! +//! fn update(&self, server: &mut Server) { +//! server.clients.retain(|_, client| { +//! if client.created_tick() == server.shared.current_tick() { +//! println!("{} joined!", client.username()); +//! } +//! +//! if client.is_disconnected() { +//! println!("{} left!", client.username()); +//! false +//! } else { +//! true +//! } +//! }); +//! # server.shared.shutdown::<_, std::convert::Infallible>(Ok(())); +//! } +//! } +//! ``` +//! +//! For more complete examples, see the [examples] in the source repository. +//! +//! [examples]: https://github.com/rj00a/valence/tree/main/examples +//! +//! # Feature Flags +//! +//! * `protocol`: Enables low-level access to the [`protocol`] module, which +//! could be used to build your own proxy or client. This feature is +//! considered experimental and is subject to change. +//! +//! [`Server`]: crate::server::Server +//! [`Clients`]: crate::client::Clients +//! [`Entities`]: crate::entity::Entities +//! [`Worlds`]: crate::world::Worlds +//! [`World`]: crate::world::World +//! [`Chunks`]: crate::chunk::Chunks +//! [`Chunk`]: crate::chunk::Chunk +//! [`Entity`]: crate::entity::Entity +//! [`Client`]: crate::client::Client + #![forbid(unsafe_code)] #![warn( trivial_casts, trivial_numeric_casts, unused_lifetimes, - unused_import_braces, - // missing_docs + unused_import_braces )] pub mod biome; @@ -18,31 +119,36 @@ pub mod config; pub mod dimension; pub mod entity; pub mod ident; -mod player_list; +pub mod player_list; pub mod player_textures; -#[cfg(not(feature = "protocol"))] -#[allow(unused)] -mod protocol; -#[cfg(feature = "protocol")] -pub mod protocol; +#[allow(dead_code)] +mod protocol_inner; pub mod server; mod slotmap; -mod spatial_index; +pub mod spatial_index; pub mod text; pub mod util; pub mod world; +#[cfg(feature = "protocol")] +pub mod protocol { + pub use crate::protocol_inner::*; +} + pub use async_trait::async_trait; pub use server::start_server; -pub use spatial_index::SpatialIndex; pub use {nbt, uuid, vek}; -/// The Minecraft protocol version that this library targets. +/// The Minecraft protocol version this library currently targets. pub const PROTOCOL_VERSION: i32 = 759; -/// The name of the Minecraft version that this library targets. +/// The name of the Minecraft version this library currently targets, e.g. +/// "1.8.2" pub const VERSION_NAME: &str = "1.19"; -/// The namespace for this library used internally for namespaced identifiers. +/// The namespace for this library used internally for +/// [identifiers](crate::ident::Ident). +/// +/// You should avoid using this namespace in your own identifiers. const LIBRARY_NAMESPACE: &str = "valence"; /// A discrete unit of time where 1 tick is the duration of a @@ -51,3 +157,7 @@ const LIBRARY_NAMESPACE: &str = "valence"; /// The duration of a game update depends on the current configuration, which /// may or may not be the same as Minecraft's standard 20 ticks/second. pub type Ticks = i64; + +/// Whatever dude +#[cfg(feature = "whatever")] +pub type Whatever = i64; diff --git a/src/player_list.rs b/src/player_list.rs index 50c0292..cb9e9af 100644 --- a/src/player_list.rs +++ b/src/player_list.rs @@ -6,13 +6,20 @@ use uuid::Uuid; use crate::client::GameMode; use crate::player_textures::SignedPlayerTextures; -use crate::protocol::packets::play::s2c::{ +use crate::protocol_inner::packets::play::s2c::{ PlayerInfo, PlayerInfoAddPlayer, S2cPlayPacket, TabList, }; -use crate::protocol::packets::Property; -use crate::protocol::VarInt; +use crate::protocol_inner::packets::Property; +use crate::protocol_inner::VarInt; use crate::text::Text; +/// The list of players on a server visible by pressing the tab key by default. +/// +/// Each entry in the player list is intended to represent a connected client to +/// the server. +/// +/// In addition to a list of players, the player list has a header and a footer +/// which can contain arbitrary text. pub struct PlayerList { entries: HashMap, removed: HashSet, @@ -32,6 +39,10 @@ impl PlayerList { } } + /// Inserts a player into the player list. + /// + /// If the given UUID conflicts with an existing entry, the entry is + /// overwritten and `false` is returned. Otherwise, `true` is returned. pub fn insert( &mut self, uuid: Uuid, @@ -40,7 +51,7 @@ impl PlayerList { game_mode: GameMode, ping: i32, display_name: impl Into>, - ) { + ) -> bool { match self.entries.entry(uuid) { Entry::Occupied(mut oe) => { let e = oe.get_mut(); @@ -62,6 +73,7 @@ impl PlayerList { e.set_ping(ping); e.set_display_name(display_name); } + false } Entry::Vacant(ve) => { ve.insert(PlayerListEntry { @@ -72,10 +84,13 @@ impl PlayerList { display_name: display_name.into(), flags: EntryFlags::new().with_created_this_tick(true), }); + true } } } + /// Removes an entry from the player list with the given UUID. Returns + /// whether the entry was present in the list. pub fn remove(&mut self, uuid: Uuid) -> bool { if self.entries.remove(&uuid).is_some() { self.removed.insert(uuid); @@ -85,10 +100,31 @@ impl PlayerList { } } + /// Removes all entries from the player list for which `f` returns `true`. + /// + /// All entries are visited in an unspecified order. + pub fn retain(&mut self, mut f: impl FnMut(Uuid, &mut PlayerListEntry) -> bool) { + self.entries.retain(|&uuid, entry| { + if !f(uuid, entry) { + self.removed.insert(uuid); + false + } else { + true + } + }) + } + + /// Removes all entries from the player list. + pub fn clear(&mut self) { + self.removed.extend(self.entries.drain().map(|p| p.0)) + } + + /// Gets the header part of the player list. pub fn header(&self) -> &Text { &self.header } + /// Sets the header part of the player list. pub fn set_header(&mut self, header: impl Into) { let header = header.into(); if self.header != header { @@ -97,10 +133,12 @@ impl PlayerList { } } + /// Gets the footer part of the player list. pub fn footer(&self) -> &Text { &self.footer } + /// Sets the footer part of the player list. pub fn set_footer(&mut self, footer: impl Into) { let footer = footer.into(); if self.footer != footer { @@ -109,10 +147,13 @@ impl PlayerList { } } + /// Returns an iterator over all entries in an unspecified order. pub fn entries(&self) -> impl Iterator + '_ { self.entries.iter().map(|(k, v)| (*k, v)) } + /// Returns an iterator which allows modifications over all entries. The + /// entries are visited in an unspecified order. pub fn entries_mut(&mut self) -> impl Iterator + '_ { self.entries.iter_mut().map(|(k, v)| (*k, v)) } @@ -240,6 +281,7 @@ impl PlayerList { } } +/// Represents a player entry in the [`PlayerList`]. pub struct PlayerListEntry { username: String, textures: Option, @@ -250,6 +292,7 @@ pub struct PlayerListEntry { } impl PlayerListEntry { + /// Gets the username of this entry. pub fn username(&self) -> &str { &self.username } @@ -258,10 +301,12 @@ impl PlayerListEntry { self.textures.as_ref() } + /// Gets the game mode of this entry. pub fn game_mode(&self) -> GameMode { self.game_mode } + /// Sets the game mode of this entry. pub fn set_game_mode(&mut self, game_mode: GameMode) { if self.game_mode != game_mode { self.game_mode = game_mode; @@ -269,10 +314,12 @@ impl PlayerListEntry { } } + /// Gets the ping (latency) of this entry measured in milliseconds. pub fn ping(&self) -> i32 { self.ping } + /// Sets the ping (latency) of this entry measured in milliseconds. pub fn set_ping(&mut self, ping: i32) { if self.ping != ping { self.ping = ping; @@ -280,10 +327,12 @@ impl PlayerListEntry { } } + /// Gets the display name of this entry. pub fn display_name(&self) -> Option<&Text> { self.display_name.as_ref() } + /// Sets the display name of this entry. pub fn set_display_name(&mut self, display_name: impl Into>) { let display_name = display_name.into(); if self.display_name != display_name { diff --git a/src/player_textures.rs b/src/player_textures.rs index b8507cd..c48d1a9 100644 --- a/src/player_textures.rs +++ b/src/player_textures.rs @@ -1,7 +1,13 @@ +//! Player skins and capes. + use anyhow::Context; use serde::{Deserialize, Serialize}; use url::Url; +/// Contains URLs to the skin and cape of a player. +/// +/// This data has been cryptographically signed to ensure it will not be altered +/// by the server. #[derive(Clone, PartialEq, Debug)] pub struct SignedPlayerTextures { payload: Box<[u8]>, @@ -9,14 +15,15 @@ pub struct SignedPlayerTextures { } impl SignedPlayerTextures { - pub fn payload(&self) -> &[u8] { + pub(crate) fn payload(&self) -> &[u8] { &self.payload } - pub fn signature(&self) -> &[u8] { + pub(crate) fn signature(&self) -> &[u8] { &self.signature } + /// Gets the unsigned texture URLs. pub fn to_textures(&self) -> PlayerTextures { self.to_textures_fallible() .expect("payload should have been validated earlier") @@ -49,9 +56,14 @@ impl SignedPlayerTextures { } } +/// Contains URLs to the skin and cape of a player. #[derive(Clone, PartialEq, Default, Debug)] pub struct PlayerTextures { + /// A URL to the skin of a player. Is `None` if the player does not have a + /// skin. pub skin: Option, + /// A URL to the cape of a player. Is `None` if the player does not have a + /// cape. pub cape: Option, } @@ -63,7 +75,7 @@ impl From for PlayerTextures { #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] #[serde(rename_all = "UPPERCASE")] -pub struct PlayerTexturesPayload { +struct PlayerTexturesPayload { #[serde(default, skip_serializing_if = "Option::is_none")] skin: Option, #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/src/protocol.rs b/src/protocol_inner.rs similarity index 99% rename from src/protocol.rs rename to src/protocol_inner.rs index 09d2d5a..a6baa70 100644 --- a/src/protocol.rs +++ b/src/protocol_inner.rs @@ -21,12 +21,12 @@ use vek::{Vec2, Vec3, Vec4}; use crate::entity::EntityId; -/// Trait for types that can be written to the Minecraft protocol. +/// Types that can be written to the Minecraft protocol. pub trait Encode { fn encode(&self, w: &mut impl Write) -> anyhow::Result<()>; } -/// Trait for types that can be read from the Minecraft protocol. +/// Types that can be read from the Minecraft protocol. pub trait Decode: Sized { fn decode(r: &mut impl Read) -> anyhow::Result; } diff --git a/src/protocol/byte_angle.rs b/src/protocol_inner/byte_angle.rs similarity index 76% rename from src/protocol/byte_angle.rs rename to src/protocol_inner/byte_angle.rs index a3c78f5..d303138 100644 --- a/src/protocol/byte_angle.rs +++ b/src/protocol_inner/byte_angle.rs @@ -1,6 +1,6 @@ use std::io::{Read, Write}; -use crate::protocol::{Decode, Encode}; +use crate::protocol_inner::{Decode, Encode}; /// Represents an angle in steps of 1/256 of a full turn. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] @@ -11,9 +11,9 @@ impl ByteAngle { ByteAngle((f.rem_euclid(360.0) / 360.0 * 256.0).round() as u8) } - // pub fn to_degrees(self) -> f32 { - // self.0 as f32 / 256.0 * 360.0 - // } + pub fn to_degrees(self) -> f32 { + self.0 as f32 / 256.0 * 360.0 + } } impl Encode for ByteAngle { @@ -24,6 +24,6 @@ impl Encode for ByteAngle { impl Decode for ByteAngle { fn decode(r: &mut impl Read) -> anyhow::Result { - Ok(ByteAngle(u8::decode(r)?)) + u8::decode(r).map(ByteAngle) } } diff --git a/src/protocol/codec.rs b/src/protocol_inner/codec.rs similarity index 98% rename from src/protocol/codec.rs rename to src/protocol_inner/codec.rs index 1120f24..c8b73cb 100644 --- a/src/protocol/codec.rs +++ b/src/protocol_inner/codec.rs @@ -12,7 +12,7 @@ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio::time::timeout; use super::packets::{DecodePacket, EncodePacket}; -use crate::protocol::{Decode, Encode, VarInt, MAX_PACKET_SIZE}; +use crate::protocol_inner::{Decode, Encode, VarInt, MAX_PACKET_SIZE}; pub struct Encoder { write: W, @@ -249,7 +249,7 @@ mod tests { use tokio::sync::oneshot; use super::*; - use crate::protocol::packets::test::TestPacket; + use crate::protocol_inner::packets::test::TestPacket; #[tokio::test] async fn encode_decode() { diff --git a/src/protocol/packets.rs b/src/protocol_inner/packets.rs similarity index 99% rename from src/protocol/packets.rs rename to src/protocol_inner/packets.rs index 3d8e0ba..21ee823 100644 --- a/src/protocol/packets.rs +++ b/src/protocol_inner/packets.rs @@ -1,8 +1,6 @@ -//! Contains packet definitions and some types contained within them. +//! Packet definitions and some types contained within them. //! -//! See for up to date protocol information. - -#![allow(dead_code)] +//! See for more packet documentation. use std::fmt; use std::io::{Read, Write}; @@ -17,7 +15,7 @@ use vek::Vec3; use crate::block_pos::BlockPos; use crate::ident::Ident; -use crate::protocol::{ +use crate::protocol_inner::{ BoundedArray, BoundedInt, BoundedString, ByteAngle, Decode, Encode, Nbt, RawBytes, VarInt, VarLong, }; @@ -319,13 +317,13 @@ macro_rules! def_bitfield { } } - impl $crate::protocol::Encode for $name { + impl Encode for $name { fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { self.0.encode(w) } } - impl $crate::protocol::Decode for $name { + impl Decode for $name { fn decode(r: &mut impl Read) -> anyhow::Result { <$inner_ty>::decode(r).map(Self) } @@ -457,7 +455,7 @@ pub mod status { def_struct! { PongResponse 0x01 { - /// Should be the same as the payload from [`Ping`]. + /// Should be the same as the payload from ping. payload: u64 } } diff --git a/src/protocol/var_int.rs b/src/protocol_inner/var_int.rs similarity index 97% rename from src/protocol/var_int.rs rename to src/protocol_inner/var_int.rs index 469da17..95005c5 100644 --- a/src/protocol/var_int.rs +++ b/src/protocol_inner/var_int.rs @@ -1,12 +1,11 @@ -// TODO: use derive_more? - use std::io::{Read, Write}; use anyhow::bail; use byteorder::{ReadBytesExt, WriteBytesExt}; -use crate::protocol::{Decode, Encode}; +use crate::protocol_inner::{Decode, Encode}; +/// An `i32` encoded with variable length. #[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct VarInt(pub i32); diff --git a/src/protocol/var_long.rs b/src/protocol_inner/var_long.rs similarity index 95% rename from src/protocol/var_long.rs rename to src/protocol_inner/var_long.rs index 42d5512..add19b9 100644 --- a/src/protocol/var_long.rs +++ b/src/protocol_inner/var_long.rs @@ -3,8 +3,9 @@ use std::io::{Read, Write}; use anyhow::bail; use byteorder::{ReadBytesExt, WriteBytesExt}; -use crate::protocol::{Decode, Encode}; +use crate::protocol_inner::{Decode, Encode}; +/// An `i64` encoded with variable length. #[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct VarLong(pub(crate) i64); diff --git a/src/server.rs b/src/server.rs index b68f23c..aa29211 100644 --- a/src/server.rs +++ b/src/server.rs @@ -31,30 +31,42 @@ use crate::config::{Config, ServerListPing}; use crate::dimension::{Dimension, DimensionId}; use crate::entity::Entities; use crate::player_textures::SignedPlayerTextures; -use crate::protocol::codec::{Decoder, Encoder}; -use crate::protocol::packets::handshake::{Handshake, HandshakeNextState}; -use crate::protocol::packets::login::c2s::{EncryptionResponse, LoginStart, VerifyTokenOrMsgSig}; -use crate::protocol::packets::login::s2c::{EncryptionRequest, LoginSuccess, SetCompression}; -use crate::protocol::packets::play::c2s::C2sPlayPacket; -use crate::protocol::packets::play::s2c::S2cPlayPacket; -use crate::protocol::packets::status::c2s::{PingRequest, StatusRequest}; -use crate::protocol::packets::status::s2c::{PongResponse, StatusResponse}; -use crate::protocol::packets::{login, Property}; -use crate::protocol::{BoundedArray, BoundedString, VarInt}; +use crate::protocol_inner::codec::{Decoder, Encoder}; +use crate::protocol_inner::packets::handshake::{Handshake, HandshakeNextState}; +use crate::protocol_inner::packets::login::c2s::{ + EncryptionResponse, LoginStart, VerifyTokenOrMsgSig, +}; +use crate::protocol_inner::packets::login::s2c::{EncryptionRequest, LoginSuccess, SetCompression}; +use crate::protocol_inner::packets::play::c2s::C2sPlayPacket; +use crate::protocol_inner::packets::play::s2c::S2cPlayPacket; +use crate::protocol_inner::packets::status::c2s::{PingRequest, StatusRequest}; +use crate::protocol_inner::packets::status::s2c::{PongResponse, StatusResponse}; +use crate::protocol_inner::packets::{login, Property}; +use crate::protocol_inner::{BoundedArray, BoundedString, VarInt}; use crate::util::valid_username; use crate::world::Worlds; use crate::{Ticks, PROTOCOL_VERSION, VERSION_NAME}; +/// Contains the entire state of a running Minecraft server, accessible from +/// within the [update](crate::config::Config::update) loop. pub struct Server { + /// A handle to this server's [`SharedServer`]. pub shared: SharedServer, + /// All of the clients in the server. pub clients: Clients, + /// All of entities in the server. pub entities: Entities, + /// All of the worlds in the server. 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. +/// A handle to a Minecraft server containing the subset of functionality which +/// is accessible outside the [update][update] loop. +/// +/// `SharedServer`s are internally refcounted and can +/// be shared between threads. +/// +/// [update]: crate::config::Config::update #[derive(Clone)] pub struct SharedServer(Arc); @@ -105,43 +117,49 @@ struct NewClientMessage { reply: oneshot::Sender, } -/// The result type returned from [`ServerConfig::start`] after the server is -/// shut down. -pub type ShutdownResult = Result<(), ShutdownError>; -pub type ShutdownError = Box; +/// The result type returned from [`start_server`]. +pub type ShutdownResult = Result<(), Box>; pub(crate) type S2cPacketChannels = (Sender, Receiver); pub(crate) type C2sPacketChannels = (Sender, Receiver); impl SharedServer { + /// Gets a reference to the config object used to start the server. pub fn config(&self) -> &(impl Config + ?Sized) { self.0.cfg.as_ref() } + /// Gets the socket address this server is bound to. pub fn address(&self) -> SocketAddr { self.0.address } + /// Gets the configured tick rate of this server. pub fn tick_rate(&self) -> Ticks { self.0.tick_rate } + /// Gets whether online mode is enabled on this server. pub fn online_mode(&self) -> bool { self.0.online_mode } + /// Gets the maximum number of connections allowed to the server at once. pub fn max_connections(&self) -> usize { self.0.max_connections } + /// Gets the configured incoming packet capacity. pub fn incoming_packet_capacity(&self) -> usize { self.0.incoming_packet_capacity } + /// Gets the configured outgoing incoming packet capacity. pub fn outgoing_packet_capacity(&self) -> usize { self.0.outgoing_packet_capacity } + /// Gets a handle to the tokio instance this server is using. pub fn tokio_handle(&self) -> &Handle { &self.0.tokio_handle } @@ -197,7 +215,7 @@ impl SharedServer { } /// Immediately stops new connections to the server and initiates server - /// shutdown. The given result is returned through [`ServerConfig::start`]. + /// shutdown. The given result is returned through [`start_server`]. /// /// You may want to disconnect all players with a message prior to calling /// this function. @@ -213,10 +231,10 @@ impl SharedServer { /// Consumes the configuration and starts the server. /// -/// The function returns when the server has shut down, a runtime error +/// The function returns once the server has shut down, a runtime error /// occurs, or the configuration is invalid. pub fn start_server(config: impl Config) -> ShutdownResult { - let shared = setup_server(config).map_err(ShutdownError::from)?; + let shared = setup_server(config).map_err(Box::::from)?; let _guard = shared.tokio_handle().enter(); @@ -462,7 +480,7 @@ async fn do_accept_loop(server: SharedServer) { return; } } - log::debug!("connection to {remote_addr} ended: {e:#}"); + log::error!("connection to {remote_addr} ended: {e:#}"); } drop(permit); }); diff --git a/src/spatial_index.rs b/src/spatial_index.rs index d9ca842..96967b9 100644 --- a/src/spatial_index.rs +++ b/src/spatial_index.rs @@ -1,3 +1,5 @@ +//! Efficient spatial entity queries. + use vek::{Aabb, Vec3}; use crate::bvh::Bvh; @@ -5,6 +7,14 @@ pub use crate::bvh::TraverseStep; use crate::entity::{Entities, EntityId}; use crate::world::WorldId; +/// A data structure for fast spatial queries on entity [hitboxes]. This is used +/// to accelerate tasks such as collision detection and ray tracing. +/// +/// The spatial index is only updated at the end of each tick. Any modification +/// to an entity that would change its hitbox is not reflected in the spatial +/// index until the end of the tick. +/// +/// [hitboxes]: crate::entity::Entity::hitbox pub struct SpatialIndex { bvh: Bvh, } @@ -14,13 +24,46 @@ impl SpatialIndex { Self { bvh: Bvh::new() } } - pub fn traverse(&self, mut f: F) -> Option - where - F: FnMut(Option, Aabb) -> TraverseStep, - { - self.bvh.traverse(|e, bb| f(e.cloned(), bb)) + #[doc(hidden)] + #[deprecated = "This is for documentation tests only"] + pub fn example_new() -> Self { + println!("Don't call me!"); + Self::new() } + /// Invokes `f` with every entity in the spatial index considered + /// colliding according to `collides`. + /// + /// `collides` takes an AABB and returns whether or not a collision + /// occurred with the given AABB. + /// + /// `f` is called with the entity ID and hitbox of all colliding entities. + /// If `f` returns with `Some(x)`, then `query` exits early with + /// `Some(x)`. If `f` never returns with `Some`, then query returns `None`. + /// + /// # Examples + /// + /// Visit all entities intersecting a 10x10x10 cube centered at the origin. + /// + /// ``` + /// # #[allow(deprecated)] + /// # let si = valence::spatial_index::SpatialIndex::example_new(); + /// use valence::vek::*; + /// + /// let cube = Aabb { + /// min: Vec3::new(-5.0, -5.0, -5.0), + /// max: Vec3::new(5.0, 5.0, 5.0), + /// }; + /// + /// let collides = |aabb: Aabb| aabb.collides_with_aabb(cube); + /// let f = |id, _| -> Option<()> { + /// println!("Found entity: {id:?}"); + /// None + /// }; + /// + /// // Assume `si` is the spatial index + /// si.query(collides, f); + /// ``` pub fn query(&self, mut collides: C, mut f: F) -> Option where C: FnMut(Aabb) -> bool, @@ -97,11 +140,22 @@ impl SpatialIndex { ) } + /// Explores the spatial index according to `f`. + /// + /// This is a low-level function that should only be used if the other + /// methods on this type are too restrictive. + pub fn traverse(&self, mut f: F) -> Option + where + F: FnMut(Option, Aabb) -> TraverseStep, + { + self.bvh.traverse(|e, bb| f(e.cloned(), bb)) + } + pub(crate) fn update(&mut self, entities: &Entities, id: WorldId) { self.bvh.build( entities .iter() - .filter(|(_, e)| e.world() == Some(id)) + .filter(|(_, e)| e.world() == id) .map(|(id, e)| (id, e.hitbox())), ) } @@ -113,7 +167,7 @@ impl SpatialIndex { pub struct RaycastHit { /// The [`EntityId`] of the entity that was hit by the ray. pub entity: EntityId, - /// The bounding box of the entity that was hit. + /// The bounding box (hitbox) of the entity that was hit. pub bb: Aabb, /// The distance from the ray origin to the closest intersection point. /// If the origin of the ray is inside the bounding box, then this will be diff --git a/src/text.rs b/src/text.rs index a724038..c4fc6b4 100644 --- a/src/text.rs +++ b/src/text.rs @@ -1,6 +1,3 @@ -// TODO: Documentation. -// TODO: make fields of Text public? - use std::borrow::Cow; use std::fmt; use std::io::{Read, Write}; @@ -9,19 +6,22 @@ use serde::de::Visitor; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::ident::Ident; -use crate::protocol::{BoundedString, Decode, Encode}; +use crate::protocol_inner::{BoundedString, Decode, Encode}; /// Represents formatted text in Minecraft's JSON text format. /// /// Text is used in various places such as chat, window titles, /// disconnect messages, written books, signs, and more. /// -/// For more information, see the relevant [Minecraft wiki article](https://minecraft.fandom.com/wiki/Raw_JSON_text_format). +/// For more information, see the relevant [Minecraft Wiki article]. /// /// Note that the current `Deserialize` implementation on this type recognizes /// only a subset of the full JSON chat component format. /// -/// ## Example +/// [Minecraft Wiki article]: https://minecraft.fandom.com/wiki/Raw_JSON_text_format +/// +/// # Examples +/// /// With [`TextFormat`] in scope, you can write the following: /// ``` /// use valence::text::{Color, Text, TextFormat}; @@ -82,6 +82,8 @@ pub struct Text { } impl Text { + /// Returns `true` if the text contains no characters. Returns `false` + /// otherwise. pub fn is_empty(&self) -> bool { for extra in &self.extra { if !extra.is_empty() { @@ -99,7 +101,7 @@ impl Text { /// Provides the methods necessary for working with [`Text`] objects. /// /// This trait exists to allow using `Into` types without having to first -/// convert the type into [`Text`]. It is automatically implemented for all +/// convert the type into [`Text`]. A blanket implementation exists for all /// `Into` types, including [`Text`] itself. pub trait TextFormat: Into { fn into_text(self) -> Text { @@ -366,8 +368,9 @@ impl Text { } pub fn write_plain(&self, w: &mut impl fmt::Write) -> fmt::Result { - if let TextContent::Text { text } = &self.content { - w.write_str(text.as_ref())?; + match &self.content { + TextContent::Text { text } => w.write_str(text.as_ref())?, + TextContent::Translate { translate } => w.write_str(translate.as_ref())?, } for child in &self.extra { @@ -376,8 +379,6 @@ impl Text { Ok(()) } - - // TODO: getters } impl> TextFormat for T {} diff --git a/src/util.rs b/src/util.rs index cf0ad0d..0a0d79b 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,3 +1,5 @@ +//! Miscellaneous utilities. + use std::iter::FusedIterator; use num::cast::AsPrimitive; @@ -8,6 +10,20 @@ use crate::chunk_pos::ChunkPos; /// Returns true if the given string meets the criteria for a valid Minecraft /// username. +/// +/// Usernames are valid if they match the regex `^[a-zA-Z0-9_]{3,16}$`. +/// +/// # Examples +/// +/// ``` +/// use valence::util::valid_username; +/// +/// assert!(valid_username("00a")); +/// assert!(valid_username("jeb_")); +/// +/// assert!(!valid_username("notavalidusername")); +/// assert!(!valid_username("NotValid!")) +/// ``` pub fn valid_username(s: &str) -> bool { (3..=16).contains(&s.len()) && s.chars() @@ -16,6 +32,8 @@ pub fn valid_username(s: &str) -> bool { const EXTRA_RADIUS: i32 = 3; +/// Returns an iterator over all chunk positions within a view distance, +/// centered on a particular chunk position. pub fn chunks_in_view_distance( center: ChunkPos, distance: u8, @@ -26,8 +44,8 @@ pub fn chunks_in_view_distance( .filter(move |&p| is_chunk_in_view_distance(center, p, distance)) } -pub fn is_chunk_in_view_distance(center: ChunkPos, other: ChunkPos, distance: u8) -> bool { - (center.x as f64 - other.x as f64).powi(2) + (center.z as f64 - other.z as f64).powi(2) +pub fn is_chunk_in_view_distance(p0: ChunkPos, p1: ChunkPos, distance: u8) -> bool { + (p0.x as f64 - p1.x as f64).powi(2) + (p0.z as f64 - p1.z as f64).powi(2) <= (distance as f64 + EXTRA_RADIUS as f64).powi(2) } @@ -54,10 +72,10 @@ where aabb } -/// Takes a normalized direction vector and returns a (yaw, pitch) tuple in +/// Takes a normalized direction vector and returns a `(yaw, pitch)` tuple in /// degrees. /// -/// This function is the inverse of [`from_yaw_and_pitch`]. +// /// This function is the inverse of [`from_yaw_and_pitch`]. pub fn to_yaw_and_pitch(d: Vec3) -> (f32, f32) { debug_assert!(d.is_normalized(), "the given vector should be normalized"); diff --git a/src/world.rs b/src/world.rs index 2c084f8..76fb420 100644 --- a/src/world.rs +++ b/src/world.rs @@ -7,17 +7,27 @@ use crate::dimension::DimensionId; use crate::player_list::PlayerList; use crate::server::SharedServer; use crate::slotmap::{Key, SlotMap}; -use crate::SpatialIndex; +use crate::spatial_index::SpatialIndex; pub struct Worlds { sm: SlotMap, server: SharedServer, } +/// A key for a [`World`] on the server. +/// +/// World IDs are either _valid_ or _invalid_. Valid world IDs point to +/// worlds that have not been deleted, while invalid IDs point to those that +/// have. Once an ID becomes invalid, it will never become valid again. +/// +/// The [`Ord`] instance on this type is correct but otherwise unspecified. This +/// is useful for storing IDs in containers such as +/// [`BTreeMap`](std::collections::BTreeMap). #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] pub struct WorldId(Key); impl WorldId { + /// The value of the default world ID which is always invalid. pub const NULL: Self = Self(Key::NULL); } @@ -29,6 +39,8 @@ impl Worlds { } } + /// Creates a new world on the server with the provided dimension. A + /// reference to the world along with its ID is returned. pub fn create(&mut self, dim: DimensionId) -> (WorldId, &mut World) { let (id, world) = self.sm.insert(World { spatial_index: SpatialIndex::new(), @@ -43,54 +55,76 @@ impl Worlds { (WorldId(id), world) } - /// Deletes a world from the server. Any [`WorldId`] referring to the - /// deleted world will be invalidated. + /// Deletes a world from the server. /// - /// Note that any entities with positions inside the deleted world will not - /// be deleted themselves. + /// Note that entities located in the world are not deleted themselves. + /// Additionally, any clients that are still in the deleted world at the end + /// of the tick are disconnected. pub fn delete(&mut self, world: WorldId) -> bool { self.sm.remove(world.0).is_some() } + /// Deletes all worlds from the server (as if by [`Self::delete`]) for which + /// `f` returns `true`. + /// + /// All worlds are visited in an unspecified order. pub fn retain(&mut self, mut f: impl FnMut(WorldId, &mut World) -> bool) { self.sm.retain(|k, v| f(WorldId(k), v)) } + /// Returns the number of worlds on the server. pub fn count(&self) -> usize { self.sm.len() } + /// Returns a shared reference to the world with the given ID. If + /// the ID is invalid, then `None` is returned. pub fn get(&self, world: WorldId) -> Option<&World> { self.sm.get(world.0) } + /// Returns an exclusive reference to the world with the given ID. If the + /// ID is invalid, then `None` is returned. pub fn get_mut(&mut self, world: WorldId) -> Option<&mut World> { self.sm.get_mut(world.0) } + /// Returns an immutable iterator over all worlds on the server in an + /// unspecified order. pub fn iter(&self) -> impl FusedIterator + Clone + '_ { self.sm.iter().map(|(k, v)| (WorldId(k), v)) } + /// Returns a mutable iterator over all worlds on the server in an + /// unspecified ordder. pub fn iter_mut(&mut self) -> impl FusedIterator + '_ { self.sm.iter_mut().map(|(k, v)| (WorldId(k), v)) } + /// Returns a parallel immutable iterator over all worlds on the server in + /// an unspecified order. pub fn par_iter(&self) -> impl ParallelIterator + Clone + '_ { self.sm.par_iter().map(|(k, v)| (WorldId(k), v)) } + /// Returns a parallel mutable iterator over all worlds on the server in an + /// unspecified order. pub fn par_iter_mut(&mut self) -> impl ParallelIterator + '_ { self.sm.par_iter_mut().map(|(k, v)| (WorldId(k), v)) } } +/// A space for chunks, entities, and clients to occupy. pub struct World { + /// Contains all of the entities in this world. pub spatial_index: SpatialIndex, + /// All of the chunks in this world. pub chunks: Chunks, + /// This world's metadata. pub meta: WorldMeta, } +/// Contains miscellaneous world state. pub struct WorldMeta { dimension: DimensionId, is_flat: bool, @@ -99,22 +133,33 @@ pub struct WorldMeta { } impl WorldMeta { + /// Gets the dimension the world was created with. pub fn dimension(&self) -> DimensionId { self.dimension } + /// Gets if this world is considered a superflat world. Superflat worlds + /// have a horizon line at y=0. pub fn is_flat(&self) -> bool { self.is_flat } + /// Sets if this world is considered a superflat world. Superflat worlds + /// have a horizon line at y=0. + /// + /// Clients already in the world must be respawned to see any changes. pub fn set_flat(&mut self, flat: bool) { self.is_flat = flat; } + /// Returns a shared reference to the world's + /// [`PlayerList`](crate::player_list::PlayerList). pub fn player_list(&self) -> &PlayerList { &self.player_list } + /// Returns an exclusive reference to the world's + /// [`PlayerList`](crate::player_list::PlayerList). pub fn player_list_mut(&mut self) -> &mut PlayerList { &mut self.player_list }