diff --git a/examples/basic.rs b/examples/chunk.rs similarity index 95% rename from examples/basic.rs rename to examples/chunk.rs index 655dbda..60f0f37 100644 --- a/examples/basic.rs +++ b/examples/chunk.rs @@ -109,12 +109,6 @@ impl Config for Game { } } } - - let entity_id = world.entities.create(); - let mut entity = world.entities.get_mut(entity_id).unwrap(); - - entity.set_type(EntityType::Cow); - entity.set_position([0.0, 50.0, 0.0]); } fn update(&self, server: &Server, mut worlds: WorldsMut) { diff --git a/examples/cow_sphere.rs b/examples/cow_sphere.rs new file mode 100644 index 0000000..80a4a6e --- /dev/null +++ b/examples/cow_sphere.rs @@ -0,0 +1,119 @@ +use std::net::SocketAddr; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use log::LevelFilter; +use valence::block::BlockState; +use valence::client::GameMode; +use valence::config::{Config, ServerListPing}; +use valence::text::Color; +use valence::{ + async_trait, ChunkPos, ClientMut, DimensionId, EntityType, Server, ShutdownResult, Text, + TextFormat, WorldId, WorldsMut, +}; + +pub fn main() -> ShutdownResult { + env_logger::Builder::new() + .filter_module("valence", LevelFilter::Trace) + .parse_default_env() + .init(); + + valence::start_server(Game { + player_count: AtomicUsize::new(0), + }) +} + +struct Game { + player_count: AtomicUsize, +} + +const MAX_PLAYERS: usize = 10; + +#[async_trait] +impl Config for Game { + fn max_connections(&self) -> usize { + // We want status pings to be successful even if the server is full. + MAX_PLAYERS + 64 + } + + fn online_mode(&self) -> bool { + // You'll want this to be true on real servers. + false + } + + async fn server_list_ping(&self, _server: &Server, _remote_addr: SocketAddr) -> ServerListPing { + ServerListPing::Respond { + online_players: self.player_count.load(Ordering::SeqCst) as i32, + max_players: MAX_PLAYERS as i32, + description: "Hello Valence!".color(Color::AQUA), + favicon_png: Some(include_bytes!("favicon.png")), + } + } + + fn join( + &self, + _server: &Server, + _client: ClientMut, + worlds: WorldsMut, + ) -> Result { + if let Ok(_) = self + .player_count + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { + (count < MAX_PLAYERS).then(|| count + 1) + }) + { + Ok(worlds.iter().next().unwrap().0) + } else { + Err("The server is full!".into()) + } + } + + fn init(&self, _server: &Server, mut worlds: WorldsMut) { + let world_id = worlds.create(DimensionId::default()); + let mut world = worlds.get_mut(world_id).unwrap(); + world.meta.set_flat(true); + + let size = 5; + for z in -size..size { + for x in -size..size { + world.chunks.create([x, z]); + } + } + + let entity_id = world.entities.create(); + let mut entity = world.entities.get_mut(entity_id).unwrap(); + + entity.set_type(EntityType::Cow); + entity.set_position([0.0, 100.0, 0.0]); + //entity.set_yaw(30.0); + //entity.set_pitch(0.0); + } + + fn update(&self, server: &Server, mut worlds: WorldsMut) { + let mut world = worlds.iter_mut().next().unwrap().1; + + world.clients.retain(|_, mut client| { + if client.created_tick() == server.current_tick() { + client.set_game_mode(GameMode::Creative); + client.teleport([0.0, 200.0, 0.0], 0.0, 0.0); + } + + if client.is_disconnected() { + self.player_count.fetch_sub(1, Ordering::SeqCst); + false + } else { + true + } + }); + + for (_, mut e) in world.entities.iter_mut() { + let time = server.current_tick() as f64 / server.tick_rate() as f64; + + if e.typ() == EntityType::Cow { + e.set_position(e.position() + [0.0, 0.0, 0.02]); + let yaw = (time % 1.0 * 360.0) as f32; + e.set_yaw(yaw); + e.set_head_yaw(yaw); + } + } + } +} diff --git a/src/chunk.rs b/src/chunk.rs index 6c59ac0..94a80ea 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -418,10 +418,11 @@ fn encode_paletted_container( let palette_idx = palette .iter() .position(|&e| e == entry) - .expect("entry should be in the palette") as u64; - + .expect("entry should be in the palette") + as u64; + val |= palette_idx << (i * bits_per_idx); - } + } } val.encode(w)?; } diff --git a/src/client.rs b/src/client.rs index c54219b..62e3b00 100644 --- a/src/client.rs +++ b/src/client.rs @@ -8,25 +8,29 @@ use uuid::Uuid; use vek::Vec3; use crate::block_pos::BlockPos; +use crate::byte_angle::ByteAngle; use crate::config::{ Biome, BiomeGrassColorModifier, BiomePrecipitation, Dimension, DimensionEffects, DimensionId, }; -use crate::entity::EntityType; +use crate::entity::{velocity_to_packet_units, EntityType}; pub use crate::packets::play::GameMode; use crate::packets::play::{ Biome as BiomeRegistryBiome, BiomeAdditionsSound, BiomeEffects, BiomeMoodSound, BiomeMusic, BiomeParticle, BiomeParticleOptions, BiomeProperty, BiomeRegistry, ChangeGameState, ChangeGameStateReason, ClientPlayPacket, DestroyEntities, DimensionCodec, DimensionType, - DimensionTypeRegistry, DimensionTypeRegistryEntry, Disconnect, JoinGame, KeepAliveClientbound, - PlayerPositionAndLook, PlayerPositionAndLookFlags, ServerPlayPacket, SpawnPosition, - UnloadChunk, UpdateViewDistance, UpdateViewPosition, + DimensionTypeRegistry, DimensionTypeRegistryEntry, Disconnect, EntityHeadLook, EntityPosition, + EntityPositionAndRotation, EntityRotation, EntityTeleport, EntityVelocity, JoinGame, + KeepAliveClientbound, PlayerPositionAndLook, PlayerPositionAndLookFlags, ServerPlayPacket, + SpawnPosition, UnloadChunk, UpdateViewDistance, UpdateViewPosition, }; use crate::protocol::{BoundedInt, Nbt}; use crate::server::ServerPacketChannels; use crate::slotmap::{Key, SlotMap}; use crate::util::{chunks_in_view_distance, is_chunk_in_view_distance}; use crate::var_int::VarInt; -use crate::{ident, ChunkPos, Chunks, Entities, EntityId, Server, Text, Ticks, LIBRARY_NAMESPACE}; +use crate::{ + ident, ChunkPos, Chunks, Entities, EntityId, Server, Text, Ticks, WorldMeta, LIBRARY_NAMESPACE, +}; pub struct Clients { sm: SlotMap, @@ -328,7 +332,7 @@ impl<'a> ClientMut<'a> { server: &Server, entities: &Entities, chunks: &Chunks, - dimension_id: DimensionId, + meta: &WorldMeta, ) { self.0.events.clear(); @@ -347,11 +351,13 @@ impl<'a> ClientMut<'a> { return; } - let dimension = server.dimension(dimension_id); + let dimension = server.dimension(meta.dimension()); + + let current_tick = server.current_tick(); // Send the join game packet and other initial packets. We defer this until now // so that the user can set the client's location, game mode, etc. - if self.created_tick == server.current_tick() { + if self.created_tick == current_tick { self.send_packet(JoinGame { entity_id: 0, // EntityId 0 is reserved for clients. is_hardcore: false, // TODO @@ -363,7 +369,7 @@ impl<'a> ClientMut<'a> { .collect(), dimension_codec: Nbt(make_dimension_codec(server)), dimension: Nbt(to_dimension_registry_item(dimension)), - dimension_name: ident!("{LIBRARY_NAMESPACE}:dimension_{}", dimension_id.0), + dimension_name: ident!("{LIBRARY_NAMESPACE}:dimension_{}", meta.dimension().0), hashed_seed: 0, max_players: VarInt(0), view_distance: BoundedInt(VarInt(self.new_max_view_distance as i32)), @@ -396,7 +402,7 @@ impl<'a> ClientMut<'a> { // Update view distance fog on the client if necessary. if self.0.old_max_view_distance != self.0.new_max_view_distance { self.0.old_max_view_distance = self.0.new_max_view_distance; - if self.0.created_tick != server.current_tick() { + if self.0.created_tick != current_tick { self.send_packet(UpdateViewDistance { view_distance: BoundedInt(VarInt(self.0.new_max_view_distance as i32)), }) @@ -404,7 +410,7 @@ impl<'a> ClientMut<'a> { } // Check if it's time to send another keepalive. - if server.current_tick() % (server.tick_rate() * 8) == 0 { + if current_tick % (server.tick_rate() * 8) == 0 { if self.0.got_keepalive { let id = rand::random(); self.send_packet(KeepAliveClientbound { id }); @@ -451,7 +457,7 @@ impl<'a> ClientMut<'a> { if let Some(chunk) = chunks.get(pos) { if is_chunk_in_view_distance(center, pos, view_dist + cache) - && chunk.created_tick() != server.current_tick() + && chunk.created_tick() != current_tick { if let Some(pkt) = chunk.block_change_packet(pos) { send_packet(&mut self.0.send, pkt); @@ -484,7 +490,6 @@ impl<'a> ClientMut<'a> { // This is done after the chunks are loaded so that the "downloading terrain" // screen is closed at the appropriate time. - if self.0.teleported_this_tick { self.0.teleported_this_tick = false; @@ -504,10 +509,90 @@ impl<'a> ClientMut<'a> { // longer visible. self.0.loaded_entities.retain(|&id| { if let Some(entity) = entities.get(id) { - if self.0.new_position.distance(entity.position()) <= view_dist as f64 * 16.0 { + debug_assert!(entity.typ() != EntityType::Marker); + if self.0.new_position.distance(entity.position()) <= view_dist as f64 * 16.0 + && !entity.flags().type_modified() + { if let Some(meta) = entity.updated_metadata_packet(id) { send_packet(&mut self.0.send, meta); } + + let position_delta = entity.position() - entity.old_position(); + let needs_teleport = position_delta.map(f64::abs).reduce_partial_max() >= 8.0; + let flags = entity.flags(); + + if entity.position() != entity.old_position() + && !needs_teleport + && flags.yaw_or_pitch_modified() + { + send_packet( + &mut self.0.send, + EntityPositionAndRotation { + entity_id: VarInt(id.to_network_id()), + delta: (position_delta * 4096.0).as_(), + yaw: ByteAngle::from_degrees(entity.yaw()), + pitch: ByteAngle::from_degrees(entity.pitch()), + on_ground: entity.on_ground(), + }, + ); + } else { + if entity.position() != entity.old_position() && !needs_teleport { + send_packet( + &mut self.0.send, + EntityPosition { + entity_id: VarInt(id.to_network_id()), + delta: (position_delta * 4096.0).as_(), + on_ground: entity.on_ground(), + }, + ); + } + + if flags.yaw_or_pitch_modified() { + send_packet( + &mut self.0.send, + EntityRotation { + entity_id: VarInt(id.to_network_id()), + yaw: ByteAngle::from_degrees(entity.yaw()), + pitch: ByteAngle::from_degrees(entity.pitch()), + on_ground: entity.on_ground(), + }, + ); + } + } + + if needs_teleport { + send_packet( + &mut self.0.send, + EntityTeleport { + entity_id: VarInt(id.to_network_id()), + position: entity.position(), + yaw: ByteAngle::from_degrees(entity.yaw()), + pitch: ByteAngle::from_degrees(entity.pitch()), + on_ground: entity.on_ground(), + }, + ); + } + + if flags.velocity_modified() { + send_packet( + &mut self.0.send, + EntityVelocity { + entity_id: VarInt(id.to_network_id()), + velocity: velocity_to_packet_units(entity.velocity()), + }, + ); + } + + if flags.head_yaw_modified() { + send_packet( + &mut self.0.send, + EntityHeadLook { + entity_id: VarInt(id.to_network_id()), + head_yaw: ByteAngle::from_degrees(entity.head_yaw()), + }, + ) + } + return true; } } @@ -523,6 +608,7 @@ impl<'a> ClientMut<'a> { } // Spawn new entities within the view distance. + // TODO: use BVH for (id, entity) in entities.iter() { if self.position().distance(entity.position()) <= view_dist as f64 * 16.0 && entity.typ() != EntityType::Marker diff --git a/src/entity.rs b/src/entity.rs index ba78439..7683e1e 100644 --- a/src/entity.rs +++ b/src/entity.rs @@ -162,7 +162,10 @@ impl<'a> EntitiesMut<'a> { for (_, e) in self.iter_mut() { e.0.old_position = e.new_position; e.0.meta.clear_modifications(); + + let on_ground = e.0.flags.on_ground(); e.0.flags = EntityFlags(0); + e.0.flags.set_on_ground(on_ground); } } } @@ -204,11 +207,12 @@ impl<'a> Deref for EntityMut<'a> { /// modified. #[bitfield(u8)] pub(crate) struct EntityFlags { - meta_modified: bool, - yaw_or_pitch_modified: bool, - head_yaw_modified: bool, - head_pitch_modified: bool, - velocity_modified: bool, + /// When the type of this entity changes. + pub type_modified: bool, + pub yaw_or_pitch_modified: bool, + pub head_yaw_modified: bool, + pub velocity_modified: bool, + pub on_ground: bool, #[bits(3)] _pad: u8, } @@ -254,16 +258,15 @@ impl Entity { self.head_yaw } - /// Gets the head pitch of this entity (in degrees). - pub fn head_pitch(&self) -> f32 { - self.head_pitch - } - /// Gets the velocity of this entity in meters per second. pub fn velocity(&self) -> Vec3 { self.velocity } + pub fn on_ground(&self) -> bool { + self.flags.on_ground() + } + /// Gets the metadata packet to send to clients after this entity has been /// spawned. /// @@ -361,7 +364,7 @@ impl Entity { position: self.new_position, yaw: ByteAngle::from_degrees(self.yaw), pitch: ByteAngle::from_degrees(self.pitch), - head_pitch: ByteAngle::from_degrees(self.head_pitch), + head_yaw: ByteAngle::from_degrees(self.head_yaw), velocity: velocity_to_packet_units(self.velocity), })) } @@ -507,7 +510,7 @@ impl Entity { } } -fn velocity_to_packet_units(vel: Vec3) -> Vec3 { +pub(crate) fn velocity_to_packet_units(vel: Vec3) -> Vec3 { // The saturating cast to i16 is desirable. (vel * 400.0).as_() } @@ -528,8 +531,8 @@ impl<'a> EntityMut<'a> { /// All metadata of this entity is reset to the default values. pub fn set_type(&mut self, typ: EntityType) { self.0.meta = EntityMeta::new(typ); - // All metadata is lost, so we must mark it as modified unconditionally. - self.0.flags.set_meta_modified(true); + // All metadata is lost so we must mark it as modified unconditionally. + self.0.flags.set_type_modified(true); } /// Sets the position of this entity in the world it inhabits. @@ -539,43 +542,40 @@ impl<'a> EntityMut<'a> { /// Sets the yaw of this entity (in degrees). pub fn set_yaw(&mut self, yaw: f32) { - self.0.yaw = yaw; - if ByteAngle::from_degrees(self.yaw) != ByteAngle::from_degrees(yaw) { + if self.0.yaw != yaw { + self.0.yaw = yaw; self.0.flags.set_yaw_or_pitch_modified(true); } } /// Sets the pitch of this entity (in degrees). pub fn set_pitch(&mut self, pitch: f32) { - self.0.pitch = pitch; - if ByteAngle::from_degrees(self.pitch) != ByteAngle::from_degrees(pitch) { + if self.0.pitch != pitch { + self.0.pitch = pitch; self.0.flags.set_yaw_or_pitch_modified(true); } } /// Sets the head yaw of this entity (in degrees). pub fn set_head_yaw(&mut self, head_yaw: f32) { - self.0.head_yaw = head_yaw; - if ByteAngle::from_degrees(self.head_yaw) != ByteAngle::from_degrees(head_yaw) { + if self.0.head_yaw != head_yaw { + self.0.head_yaw = head_yaw; self.0.flags.set_head_yaw_modified(true); } } - /// Sets the head pitch of this entity (in degrees). - pub fn set_head_pitch(&mut self, head_pitch: f32) { - self.0.head_pitch = head_pitch; - if ByteAngle::from_degrees(self.head_pitch) != ByteAngle::from_degrees(head_pitch) { - self.0.flags.set_head_pitch_modified(true); - } - } - pub fn set_velocity(&mut self, velocity: impl Into>) { let new_vel = velocity.into(); - self.0.velocity = new_vel; - if velocity_to_packet_units(self.velocity) != velocity_to_packet_units(new_vel) { + + if self.0.velocity != new_vel { + self.0.velocity = new_vel; self.0.flags.set_velocity_modified(true); } } + + pub fn set_on_ground(&mut self, on_ground: bool) { + self.0.flags.set_on_ground(on_ground); + } } pub(crate) enum EntitySpawnPacket { diff --git a/src/lib.rs b/src/lib.rs index ce6f368..e6b8a18 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,7 +36,7 @@ pub use identifier::Identifier; pub use server::{start_server, NewClientData, Server, ShutdownResult}; pub use text::{Text, TextFormat}; pub use uuid::Uuid; -pub use world::{WorldId, WorldMut, WorldRef, Worlds, WorldsMut}; +pub use world::{WorldId, WorldMeta, WorldMetaMut, WorldMut, WorldRef, Worlds, WorldsMut}; pub use {nbt, uuid, vek}; /// The Minecraft protocol version that this library targets. diff --git a/src/packets.rs b/src/packets.rs index 3705c65..c31174d 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -13,7 +13,7 @@ use num::{One, Zero}; use paste::paste; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use vek::{Vec2, Vec3}; +use vek::Vec3; use crate::block_pos::BlockPos; use crate::byte_angle::ByteAngle; @@ -469,7 +469,7 @@ pub mod play { position: Vec3, yaw: ByteAngle, pitch: ByteAngle, - head_pitch: ByteAngle, + head_yaw: ByteAngle, velocity: Vec3, } } @@ -964,6 +964,33 @@ pub mod play { } } + def_struct! { + EntityPosition 0x29 { + entity_id: VarInt, + delta: Vec3, + on_ground: bool, + } + } + + def_struct! { + EntityPositionAndRotation 0x2a { + entity_id: VarInt, + delta: Vec3, + yaw: ByteAngle, + pitch: ByteAngle, + on_ground: bool, + } + } + + def_struct! { + EntityRotation 0x2b { + entity_id: VarInt, + yaw: ByteAngle, + pitch: ByteAngle, + on_ground: bool, + } + } + def_struct! { PlayerPositionAndLook 0x38 { position: Vec3, @@ -991,6 +1018,13 @@ pub mod play { } } + def_struct! { + EntityHeadLook 0x3e { + entity_id: VarInt, + head_yaw: ByteAngle, + } + } + def_struct! { MultiBlockChange 0x3f { chunk_section_position: u64, @@ -1032,6 +1066,13 @@ pub mod play { } } + def_struct! { + EntityVelocity 0x4f { + entity_id: VarInt, + velocity: Vec3, + } + } + def_struct! { TimeUpdate 0x59 { /// The age of the world in 1/20ths of a second. @@ -1043,6 +1084,16 @@ pub mod play { } } + def_struct! { + EntityTeleport 0x62 { + entity_id: VarInt, + position: Vec3, + yaw: ByteAngle, + pitch: ByteAngle, + on_ground: bool, + } + } + macro_rules! def_client_play_packet_enum { { $($packet:ident),* $(,)? @@ -1115,14 +1166,20 @@ pub mod play { KeepAliveClientbound, ChunkDataAndUpdateLight, JoinGame, + EntityPosition, + EntityPositionAndRotation, + EntityRotation, PlayerPositionAndLook, DestroyEntities, + EntityHeadLook, MultiBlockChange, HeldItemChangeClientbound, UpdateViewPosition, UpdateViewDistance, SpawnPosition, EntityMetadata, + EntityVelocity, + EntityTeleport, TimeUpdate, } diff --git a/src/server.rs b/src/server.rs index d92b6d1..bb5b0ff 100644 --- a/src/server.rs +++ b/src/server.rs @@ -368,7 +368,12 @@ fn do_update_loop(server: Server, mut worlds: WorldsMut) -> ShutdownResult { }); world.clients.par_iter_mut().for_each(|(_, mut client)| { - client.update(&server, &world.entities, &world.chunks, world.dimension); + client.update( + &server, + &world.entities, + &world.chunks, + &world.meta, + ); }); world.entities.update(); diff --git a/src/world.rs b/src/world.rs index a5a31a1..e606b95 100644 --- a/src/world.rs +++ b/src/world.rs @@ -1,15 +1,11 @@ -use std::collections::{HashMap, HashSet}; use std::iter::FusedIterator; use std::ops::Deref; -use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +use rayon::iter::ParallelIterator; -use crate::chunk::ChunkPos; use crate::config::DimensionId; use crate::slotmap::{Key, SlotMap}; -use crate::{ - Chunks, ChunksMut, Clients, ClientsMut, Entities, EntitiesMut, Entity, EntityId, Server, -}; +use crate::{Chunks, ChunksMut, Clients, ClientsMut, Entities, EntitiesMut, Server}; pub struct Worlds { sm: SlotMap, @@ -67,7 +63,10 @@ impl<'a> WorldsMut<'a> { self.server.clone(), (self.server.dimension(dim).height / 16) as u32, ), - dimension: dim, + meta: WorldMeta { + dimension: dim, + is_flat: false, + }, })) } @@ -123,7 +122,7 @@ pub(crate) struct World { clients: Clients, entities: Entities, chunks: Chunks, - dimension: DimensionId, + meta: WorldMeta, } /// A bag of immutable references to the components of a world. @@ -131,7 +130,7 @@ pub struct WorldRef<'a> { pub clients: &'a Clients, pub entities: &'a Entities, pub chunks: &'a Chunks, - pub dimension: DimensionId, + pub meta: &'a WorldMeta, } impl<'a> WorldRef<'a> { @@ -140,7 +139,7 @@ impl<'a> WorldRef<'a> { clients: &w.clients, entities: &w.entities, chunks: &w.chunks, - dimension: w.dimension, + meta: &w.meta, } } } @@ -150,7 +149,7 @@ pub struct WorldMut<'a> { pub clients: ClientsMut<'a>, pub entities: EntitiesMut<'a>, pub chunks: ChunksMut<'a>, - pub dimension: DimensionId, + pub meta: WorldMetaMut<'a>, } impl<'a> WorldMut<'a> { @@ -159,7 +158,7 @@ impl<'a> WorldMut<'a> { clients: ClientsMut::new(&mut w.clients), entities: EntitiesMut::new(&mut w.entities), chunks: ChunksMut::new(&mut w.chunks), - dimension: w.dimension, + meta: WorldMetaMut(&mut w.meta), } } @@ -168,7 +167,38 @@ impl<'a> WorldMut<'a> { clients: &self.clients, entities: &self.entities, chunks: &self.chunks, - dimension: self.dimension, + meta: &self.meta, } } } + +pub struct WorldMeta { + dimension: DimensionId, + is_flat: bool, +} + +impl WorldMeta { + pub fn dimension(&self) -> DimensionId { + self.dimension + } + + pub fn is_flat(&self) -> bool { + self.is_flat + } +} + +pub struct WorldMetaMut<'a>(&'a mut WorldMeta); + +impl<'a> Deref for WorldMetaMut<'a> { + type Target = WorldMeta; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'a> WorldMetaMut<'a> { + pub fn set_flat(&mut self, flat: bool) { + self.0.is_flat = flat; + } +}