diff --git a/Cargo.toml b/Cargo.toml index 4aedfbc..70ce7f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,19 +10,21 @@ build = "build/main.rs" [dependencies] aes = "0.7" anyhow = "1" +arrayvec = "0.7" ascii = "1" async-trait = "0.1" base64 = "0.13" +bitfield-struct = "0.1" bitvec = "1" -bytes = "1" byteorder = "1" +bytes = "1" cfb8 = "0.7" flate2 = "1" flume = "0.10" futures = "0.3" hematite-nbt = "0.5" log = "0.4" -nalgebra-glm = "0.16" +nalgebra-glm = "0.17" num = "0.4" parking_lot = "0.12" paste = "1" @@ -36,7 +38,7 @@ sha1 = "0.10" sha2 = "0.10" thiserror = "1" tokio = { version = "1", features = ["full"] } -uuid = "0.8" +uuid = "1" [dependencies.reqwest] version = "0.11" diff --git a/build/block.rs b/build/block.rs index e027f28..1580430 100644 --- a/build/block.rs +++ b/build/block.rs @@ -513,6 +513,7 @@ pub fn build() -> anyhow::Result<()> { } } + /// Converts a `bool` to a `True` or `False` property value. pub const fn from_bool(b: bool) -> Self { if b { Self::True @@ -521,6 +522,8 @@ pub fn build() -> anyhow::Result<()> { } } + /// Convers a `True` or `False` property value to a `bool`. + /// Returns `None` if this property value is not `True` or `False` pub const fn to_bool(self) -> Option { match self { Self::True => Some(true), diff --git a/build/entity.rs b/build/entity.rs index 405bb27..110d49e 100644 --- a/build/entity.rs +++ b/build/entity.rs @@ -41,7 +41,6 @@ enum Type { Nbt, Particle, VillagerData, - OptVarInt, Pose, // ==== Specialized ==== // OptEntityId, @@ -49,6 +48,76 @@ enum Type { MainHand, } +impl Type { + pub fn default_expr(&self) -> TokenStream { + match self { + Type::BitFields(bfs) => { + let mut default = 0; + for bf in *bfs { + default = (bf.default as u8) << bf.offset; + } + quote! { #default } + } + Type::Byte(d) => quote! { #d }, + Type::VarInt(d) => quote! { VarInt(#d) }, + Type::Float(d) => quote! { #d }, + Type::String(d) => quote! { #d.into() }, + Type::Text => quote! { Default::default() }, + Type::OptText(d) => match d { + Some(d) => quote! { Some(Box::new(Text::from(#d))) }, + None => quote! { None }, + }, + Type::Slot => quote! { () }, // TODO + Type::Bool(d) => quote! { #d }, + Type::ArmorStandRotations(x, y, z) => { + quote! { ArmorStandRotations::new(#x, #y, #z) } + } + Type::BlockPos(x, y, z) => quote! { BlockPos::new(#x, #y, #z) }, + Type::OptBlockPos(d) => match d { + Some((x, y, z)) => quote! { Some(BlockPos::new(#x, #y, #z)) }, + None => quote! { None }, + }, + Type::Direction => quote! { Direction::Down }, + Type::OptUuid => quote! { None }, + Type::BlockState => quote! { BlockState::AIR }, + Type::Nbt => quote! { nbt::Blob::new() }, + Type::Particle => quote! { () }, // TODO + Type::VillagerData => quote! { VillagerData::default() }, + Type::Pose => quote! { Pose::default() }, + Type::OptEntityId => quote! { None }, + Type::BoatVariant => quote! { BoatVariant::default() }, + Type::MainHand => quote! { MainHand::default() }, + } + } + + pub fn type_id(&self) -> i32 { + match self { + Type::BitFields(_) => 0, + Type::Byte(_) => 0, + Type::VarInt(_) => 1, + Type::Float(_) => 2, + Type::String(_) => 3, + Type::Text => 4, + Type::OptText(_) => 5, + Type::Slot => 6, + Type::Bool(_) => 7, + Type::ArmorStandRotations(_, _, _) => 8, + Type::BlockPos(_, _, _) => 9, + Type::OptBlockPos(_) => 10, + Type::Direction => 11, + Type::OptUuid => 12, + Type::BlockState => 13, + Type::Nbt => 14, + Type::Particle => 15, + Type::VillagerData => 16, + Type::Pose => 18, + Type::OptEntityId => 17, + Type::BoatVariant => 1, + Type::MainHand => 0, + } + } +} + struct BitField { name: &'static str, offset: u8, @@ -60,7 +129,7 @@ const BASE_ENTITY: Class = Class { inherit: None, fields: &[ Field { - name: "base_entity_bits", + name: "base_entity_flags", typ: Type::BitFields(&[ BitField { name: "on_fire", @@ -135,7 +204,7 @@ const ABSTRACT_ARROW: Class = Class { inherit: Some(&BASE_ENTITY), fields: &[ Field { - name: "abstract_arrow_bits", + name: "abstract_arrow_flags", typ: Type::BitFields(&[ BitField { name: "critical", @@ -161,7 +230,7 @@ const LIVING_ENTITY: Class = Class { inherit: Some(&BASE_ENTITY), fields: &[ Field { - name: "living_entity_bits", + name: "living_entity_flags", typ: Type::BitFields(&[ BitField { name: "hand_active", @@ -211,7 +280,7 @@ const MOB: Class = Class { name: "mob", inherit: Some(&LIVING_ENTITY), fields: &[Field { - name: "mob_bits", + name: "mob_flags", typ: Type::BitFields(&[ BitField { name: "ai_disabled", @@ -279,7 +348,7 @@ const ABSTRACT_HORSE: Class = Class { inherit: Some(&ANIMAL), fields: &[ Field { - name: "horse_bits", + name: "horse_flags", typ: Type::BitFields(&[ BitField { name: "tame", @@ -339,7 +408,7 @@ const TAMEABLE_ANIMAL: Class = Class { name: "tameable_animal", inherit: Some(&ANIMAL), fields: &[Field { - name: "tameable_animal_bits", + name: "tameable_animal_flags", typ: Type::BitFields(&[ BitField { name: "sitting", @@ -435,7 +504,7 @@ const SPIDER: Class = Class { name: "spider", inherit: Some(&MONSTER), fields: &[Field { - name: "spider_bits", + name: "spider_flags", typ: Type::BitFields(&[BitField { name: "climbing", offset: 0, @@ -843,7 +912,7 @@ const ENTITIES: &[Class] = &[ inherit: Some(&LIVING_ENTITY), fields: &[ Field { - name: "armor_stand_bits", + name: "armor_stand_flags", typ: Type::BitFields(&[ BitField { name: "small", @@ -851,7 +920,7 @@ const ENTITIES: &[Class] = &[ default: false, }, BitField { - name: "has_arms", + name: "arms", offset: 1, default: false, }, @@ -861,7 +930,7 @@ const ENTITIES: &[Class] = &[ default: false, }, BitField { - name: "is_marker", + name: "marker", offset: 3, default: false, }, @@ -897,7 +966,7 @@ const ENTITIES: &[Class] = &[ name: "bat", inherit: Some(&AMBIENT_CREATURE), fields: &[Field { - name: "bat_bits", + name: "bat_flags", typ: Type::BitFields(&[BitField { name: "hanging", offset: 0, @@ -1034,7 +1103,7 @@ const ENTITIES: &[Class] = &[ inherit: Some(&ANIMAL), fields: &[ Field { - name: "bee_bits", + name: "bee_flags", typ: Type::BitFields(&[ BitField { name: "angry", @@ -1068,7 +1137,7 @@ const ENTITIES: &[Class] = &[ typ: Type::VarInt(0), // TODO: 0 for red, 1 for snow }, Field { - name: "fox_bits", + name: "fox_flags", typ: Type::BitFields(&[ BitField { name: "sitting", @@ -1151,7 +1220,7 @@ const ENTITIES: &[Class] = &[ typ: Type::Byte(0), }, Field { - name: "panda_bits", + name: "panda_flags", typ: Type::BitFields(&[ BitField { name: "sneezing", @@ -1355,7 +1424,7 @@ const ENTITIES: &[Class] = &[ name: "iron_golem", inherit: Some(&ABSTRACT_GOLEM), fields: &[Field { - name: "iron_golem_bits", + name: "iron_golem_flags", typ: Type::BitFields(&[BitField { name: "player_created", offset: 0, @@ -1367,7 +1436,7 @@ const ENTITIES: &[Class] = &[ name: "snow_golem", inherit: Some(&ABSTRACT_GOLEM), fields: &[Field { - name: "snow_golem_bits", + name: "snow_golem_flags", typ: Type::BitFields(&[BitField { name: "pumpkin_hat", offset: 4, @@ -1430,7 +1499,7 @@ const ENTITIES: &[Class] = &[ name: "blaze", inherit: Some(&MONSTER), fields: &[Field { - name: "blaze_bits", + name: "blaze_flags", typ: Type::BitFields(&[BitField { name: "blaze_on_fire", // TODO: better name for this? offset: 0, @@ -1522,7 +1591,7 @@ const ENTITIES: &[Class] = &[ name: "vex", inherit: Some(&MONSTER), fields: &[Field { - name: "vex_bits", + name: "vex_flags", typ: Type::BitFields(&[BitField { name: "attacking", offset: 0, @@ -1665,7 +1734,10 @@ const ENTITIES: &[Class] = &[ Class { name: "magma_cube", inherit: Some(&MOB), - fields: &[], // TODO: what are the fields? + fields: &[Field { + name: "size", + typ: Type::VarInt(1), + }], }, Class { name: "llama_spit", @@ -1777,6 +1849,49 @@ pub fn build() -> anyhow::Result<()> { .map(|c| ident(c.name.to_pascal_case())) .collect::>(); + /* + let set_type_arms = entities.iter().map(|&entity| { + let entity_name = ident(entity.name.to_pascal_case()); + + let mut old_fields = Vec::new(); + collect_class_fields(entity, &mut old_fields); + + let new_type_arms = entities.iter().map(|&new_entity| { + let new_entity_name = ident(new_entity.name.to_pascal_case()); + + let mut new_fields = Vec::new(); + collect_class_fields(new_entity, &mut new_fields); + + let assign_fields = new_fields + .iter() + .cloned() + .filter(|&new_field| old_fields.iter().any(|&f| f.name == new_field.name)) + .map(|new_field| { + let name = ident(new_field.name.to_snake_case()); + quote! { + new.#name = old.#name; + } + }); + + quote! { + EntityType::#new_entity_name => { + let mut new = #new_entity_name::new(); + + #(#assign_fields)* + + *self = Self::#new_entity_name(new); + } + } + }); + + quote! { + Self::#entity_name(old) => match new_type { + #(#new_type_arms)* + }, + } + }); + */ + let entity_structs = entities.iter().map(|&class| { let mut fields = Vec::new(); collect_class_fields(class, &mut fields); @@ -1787,7 +1902,7 @@ pub fn build() -> anyhow::Result<()> { let typ = match f.typ { Type::BitFields(_) => quote! { u8 }, Type::Byte(_) => quote! { u8 }, - Type::VarInt(_) => quote! { i32 }, + Type::VarInt(_) => quote! { VarInt }, Type::Float(_) => quote! { f32 }, Type::String(_) => quote! { Box }, Type::Text => quote! { Box }, @@ -1803,7 +1918,6 @@ pub fn build() -> anyhow::Result<()> { Type::Nbt => quote! { nbt::Blob }, Type::Particle => quote! { () }, // TODO Type::VillagerData => quote! { VillagerData }, - Type::OptVarInt => quote! { OptVarInt }, Type::Pose => quote! { Pose }, Type::OptEntityId => quote! { Option }, Type::BoatVariant => quote! { BoatVariant }, @@ -1816,46 +1930,7 @@ pub fn build() -> anyhow::Result<()> { let constructor_fields = fields.iter().map(|field| { let name = ident(field.name.to_snake_case()); - let val = match field.typ { - Type::BitFields(bfs) => { - let mut default = 0; - for bf in bfs { - default = (bf.default as u8) << bf.offset; - } - quote! { #default } - } - Type::Byte(d) => quote! { #d }, - Type::VarInt(d) => quote! { #d }, - Type::Float(d) => quote! { #d }, - Type::String(d) => quote! { #d.into() }, - Type::Text => quote! { Default::default() }, - Type::OptText(d) => match d { - Some(d) => quote! { Some(Box::new(Text::from(#d))) }, - None => quote! { None }, - }, - Type::Slot => quote! { () }, // TODO - Type::Bool(d) => quote! { #d }, - Type::ArmorStandRotations(x, y, z) => { - quote! { ArmorStandRotations::new(#x, #y, #z) } - } - Type::BlockPos(x, y, z) => quote! { BlockPos::new(#x, #y, #z) }, - Type::OptBlockPos(d) => match d { - Some((x, y, z)) => quote! { Some(BlockPos::new(#x, #y, #z)) }, - None => quote! { None }, - }, - Type::Direction => quote! { Direction::Down }, - Type::OptUuid => quote! { None }, - Type::BlockState => quote! { BlockState::AIR }, - Type::Nbt => quote! { nbt::Blob::new() }, - Type::Particle => quote! { () }, // TODO - Type::VillagerData => quote! { VillagerData::default() }, - Type::OptVarInt => quote! { 0 }, - Type::Pose => quote! { Pose::default() }, - Type::OptEntityId => quote! { None }, - Type::BoatVariant => quote! { BoatVariant::default() }, - Type::MainHand => quote! { MainHand::default() }, - }; - + let val = field.typ.default_expr(); quote! { #name: #val, } @@ -1881,7 +1956,7 @@ pub fn build() -> anyhow::Result<()> { pub fn #setter_name(&mut self, #name: #type_name) { if self.#name != #name { - self.modified_bits |= 1 << #field_offset; + self.modified_flags |= 1 << #field_offset; } self.#name = #name; @@ -1892,9 +1967,6 @@ pub fn build() -> anyhow::Result<()> { Type::BitFields(bfs) => bfs .iter() .map(|bf| { - if bf.name.to_snake_case().is_empty() { - eprintln!("{}", field.name); - } let bit_name = ident(bf.name.to_snake_case()); let getter_name = ident(format!("get_{}", bit_name.to_string())); @@ -1908,19 +1980,28 @@ pub fn build() -> anyhow::Result<()> { } pub fn #setter_name(&mut self, #bit_name: bool) { - let orig = self.#getter_name(); - - self.#name = (self.#name & !(1 << #offset)) | ((#bit_name as u8) << #offset); - - if orig != self.#getter_name() { - self.modified_bits |= 1 << #field_offset; + if self.#getter_name() != #bit_name { + self.#name = (self.#name & !(1 << #offset)) | ((#bit_name as u8) << #offset); + self.modified_flags |= 1 << #field_offset; } } } }) .collect(), Type::Byte(_) => standard_getter_setter(quote!(u8)), - Type::VarInt(_) => standard_getter_setter(quote!(i32)), + Type::VarInt(_) => quote! { + pub fn #getter_name(&self) -> i32 { + self.#name.0 + } + + pub fn #setter_name(&mut self, #name: i32) { + if self.#name.0 != #name { + self.modified_flags |= 1 << #field_offset; + } + + self.#name = VarInt(#name); + } + }, Type::Float(_) => standard_getter_setter(quote!(f32)), Type::String(_) => quote! { pub fn #getter_name(&self) -> &str { @@ -1931,7 +2012,7 @@ pub fn build() -> anyhow::Result<()> { let #name = #name.into(); if self.#name != #name { - self.modified_bits |= 1 << #field_offset; + self.modified_flags |= 1 << #field_offset; } self.#name = #name; @@ -1946,7 +2027,7 @@ pub fn build() -> anyhow::Result<()> { let #name = Box::new(#name.into()); if self.#name != #name { - self.modified_bits |= 1 << #field_offset; + self.modified_flags |= 1 << #field_offset; } self.#name = #name; @@ -1961,7 +2042,7 @@ pub fn build() -> anyhow::Result<()> { let #name = #name.map(|x| Box::new(x.into())); if self.#name != #name { - self.modified_bits |= 1 << #field_offset; + self.modified_flags |= 1 << #field_offset; } self.#name = #name; @@ -1982,7 +2063,7 @@ pub fn build() -> anyhow::Result<()> { pub fn #setter_name(&mut self, #name: nbt::Blob) { if self.#name != #name { - self.modified_bits |= 1 << #field_offset; + self.modified_flags |= 1 << #field_offset; } self.#name = #name; @@ -1990,21 +2071,8 @@ pub fn build() -> anyhow::Result<()> { }, Type::Particle => quote! {}, // TODO Type::VillagerData => standard_getter_setter(quote!(VillagerData)), - Type::OptVarInt => quote! { - pub fn #getter_name(&self) -> i32 { - self.#name.0 - } - - pub fn #setter_name(&mut self, #name: i32) { - if self.#name.0 != #name { - self.modified_bits |= 1 << #field_offset; - } - - self.#name = OptVarInt(#name); - } - }, Type::Pose => standard_getter_setter(quote!(Pose)), - Type::OptEntityId => quote! {}, // TODO + Type::OptEntityId => standard_getter_setter(quote!(Option)), Type::BoatVariant => standard_getter_setter(quote!(BoatVariant)), Type::MainHand => standard_getter_setter(quote!(MainHand)), } @@ -2014,14 +2082,14 @@ pub fn build() -> anyhow::Result<()> { quote! { pub struct #name { /// Contains a set bit for each modified field. - modified_bits: u32, + modified_flags: u32, #(#struct_fields)* } impl #name { pub(super) fn new() -> Self { Self { - modified_bits: 0, + modified_flags: 0, #(#constructor_fields)* } } @@ -2031,23 +2099,64 @@ pub fn build() -> anyhow::Result<()> { } }); - let finished = quote! { - pub enum EntityData { - #(#entity_type_variants(#entity_type_variants),)* - } + let initial_metadata_arms = entities.iter().map(|&entity| { + let name = ident(entity.name.to_pascal_case()); + let mut fields = Vec::new(); + collect_class_fields(entity, &mut fields); - impl EntityData { - pub(super) fn new() -> Self { - Self::Marker(Marker::new()) - } - - pub fn typ(&self) -> EntityType { - match self { - #(Self::#entity_type_variants(_) => EntityType::#entity_type_variants,)* + let check_fields = fields.into_iter().enumerate().map(|(idx, f)| { + let name = ident(f.name.to_snake_case()); + let default = f.typ.default_expr(); + let index: u8 = idx.try_into().unwrap(); + let type_id = f.typ.type_id(); + quote! { + if m.#name != #default { + data.push(#index); + VarInt(#type_id).encode(&mut data).unwrap(); + m.#name.encode(&mut data).unwrap(); } } - } + }); + quote! { + Self::#name(m) => { + #(#check_fields)* + } + } + }); + + let updated_metadata_arms = entities.iter().map(|&entity| { + let name = ident(entity.name.to_pascal_case()); + let mut fields = Vec::new(); + collect_class_fields(entity, &mut fields); + + let update_fields = fields.into_iter().enumerate().map(|(idx, f)| { + let name = ident(f.name.to_snake_case()); + let u8_index: u8 = idx.try_into().unwrap(); + let u32_index = idx as u32; + let type_id = f.typ.type_id(); + quote! { + if (m.modified_flags >> #u32_index) & 1 == 1 { + data.push(#u8_index); + VarInt(#type_id).encode(&mut data).unwrap(); + m.#name.encode(&mut data).unwrap(); + } + } + }); + + quote! { + Self::#name(m) => { + if m.modified_flags == 0 { + return None; + } + + #(#update_fields)* + } + } + }); + + let finished = quote! { + /// Identifies a type of entity, such as `chicken`, `zombie` or `item`. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub enum EntityType { #(#entity_type_variants,)* @@ -2060,6 +2169,64 @@ pub fn build() -> anyhow::Result<()> { } #(#entity_structs)* + + /// An enum encoding the type of n entity along with its metadata. + /// + /// Metadata encompases most of an entity's state, except for some + /// basic pieces of information such as position and rotation. + pub enum EntityMeta { + #(#entity_type_variants(#entity_type_variants),)* + } + + impl EntityMeta { + pub(super) fn new(typ: EntityType) -> Self { + match typ { + #(EntityType::#entity_type_variants => Self::#entity_type_variants(#entity_type_variants::new()),)* + } + } + + pub(super) fn typ(&self) -> EntityType { + match self { + #(Self::#entity_type_variants(_) => EntityType::#entity_type_variants,)* + } + } + + pub(super) fn initial_metadata(&self) -> Option> { + let mut data = Vec::new(); + + match self { + #(#initial_metadata_arms)* + } + + if data.is_empty() { + None + } else { + data.push(0xff); + Some(data) + } + } + + pub(super) fn updated_metadata(&self) -> Option> { + let mut data = Vec::new(); + + match self { + #(#updated_metadata_arms)* + } + + if data.is_empty() { + None + } else { + data.push(0xff); + Some(data) + } + } + + pub(super) fn clear_modifications(&mut self) { + match self { + #(Self::#entity_type_variants(m) => m.modified_flags = 0,)* + } + } + } }; write_to_out_path("entity.rs", &finished.to_string()) diff --git a/examples/basic.rs b/examples/basic.rs index acbe4e4..cbd9e32 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -4,15 +4,16 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use log::LevelFilter; use valence::block::BlockState; use valence::client::GameMode; -use valence::config::{Config, Login, ServerListPing}; +use valence::config::{Config, ServerListPing}; use valence::text::Color; use valence::{ - async_trait, DimensionId, NewClientData, Server, SharedServer, ShutdownResult, TextFormat, + async_trait, ChunkPos, ClientMut, DimensionId, Server, ShutdownResult, Text, + TextFormat, WorldId, WorldsMut, }; pub fn main() -> ShutdownResult { env_logger::Builder::new() - .filter_level(LevelFilter::Trace) + .filter_module("valence", LevelFilter::Trace) .parse_default_env() .init(); @@ -39,63 +40,7 @@ impl Config for Game { false } - fn init(&self, server: &mut Server) { - let world_id = server.worlds.create(DimensionId::default()); - let world = server.worlds.get_mut(world_id).unwrap(); - - let chunk_radius = 5; - - for z in -chunk_radius..chunk_radius { - for x in -chunk_radius..chunk_radius { - let chunk_id = server.chunks.create(384); - let chunk = server.chunks.get_mut(chunk_id).unwrap(); - - // Chunks are only visible to clients if all adjacent chunks are loaded. - // This will make the perimiter chunks contain only air. - if x != -chunk_radius - && x != chunk_radius - 1 - && z != -chunk_radius - && z != chunk_radius - 1 - { - for z in 0..16 { - for x in 0..16 { - for y in 0..50 { - chunk.set_block_state(x, y, z, BlockState::STONE); - } - } - } - } - - world.chunks_mut().insert((x, z).into(), chunk_id); - } - } - } - - fn update(&self, server: &mut Server) { - let world_id = server.worlds.iter().next().unwrap().0; - - server.clients.retain(|_, client| { - if client.created_tick() == server.other.current_tick() { - client.set_world(Some(world_id)); - client.set_game_mode(GameMode::Creative); - client.teleport([0.0, 200.0, 0.0], 0.0, 0.0); - } - - if client.is_disconnected() { - server.entities.delete(client.entity()); - self.player_count.fetch_sub(1, Ordering::SeqCst); - false - } else { - true - } - }); - } - - async fn server_list_ping( - &self, - _server: &SharedServer, - _remote_addr: SocketAddr, - ) -> ServerListPing { + 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, @@ -104,17 +49,65 @@ impl Config for Game { } } - async fn login(&self, _server: &SharedServer, _ncd: &NewClientData) -> Login { - let res = self + 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) - }); - - if res.is_ok() { - Login::Join + }) + { + Ok(worlds.iter().next().unwrap().0) } else { - Login::Disconnect("The server is full!".into()) + 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(); + + let size = 5; + for z in -size..size { + for x in -size..size { + let pos = ChunkPos::new(x, z); + world.chunks.create(pos); + let mut chunk = world.chunks.get_mut(pos).unwrap(); + + // Chunks are only visible to clients if all adjacent chunks are loaded. + // This will make the perimiter chunks contain only air. + if x != -size && x != size - 1 && z != -size && z != size - 1 { + for z in 0..16 { + for x in 0..16 { + for y in 0..50 { + chunk.set_block_state(x, y, z, BlockState::STONE); + } + } + } + } + } + } + } + + 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 + } + }); + } } diff --git a/src/aabb.rs b/src/aabb.rs index 9d74218..184656c 100644 --- a/src/aabb.rs +++ b/src/aabb.rs @@ -1,9 +1,16 @@ use std::cmp::Ordering; use std::hash::{Hash, Hasher}; -use crate::glm::{self, Number, RealNumber, TVec}; +use num::cast::AsPrimitive; -/// An Axis-aligned bounding box in an arbitrary dimension. +use crate::glm::{self, Number, RealNumber, TVec, TVec3}; + +/// An Axis-aligned bounding box in an arbitrary dimension, defined by its +/// minimum and maximum corners. +/// +/// This type maintains the invariant that `min <= max` componentwise. +/// +/// The generic type `T` can be an integer, which is useful for AABBs on grids. #[derive(Clone, Copy, Debug)] pub struct Aabb { min: TVec, @@ -20,7 +27,8 @@ impl Aabb { } } - pub fn point(pos: TVec) -> Self { + pub fn point(pos: impl Into>) -> Self { + let pos = pos.into(); Self { min: pos, max: pos } } @@ -36,6 +44,24 @@ impl Aabb { self.max - self.min } + /// Moves this AABB by some vector. + pub fn translate(&self, v: impl Into>) -> Self { + let v = v.into(); + Self { + min: self.min + v, + max: self.max + v, + } + } + + /// Calculates the AABB union, which is the smallest AABB completely + /// encompassing both AABBs. + pub fn union(&self, other: Self) -> Self { + Self { + min: glm::min2(&self.min, &other.min), + max: glm::max2(&self.max, &other.max), + } + } + pub fn collides_with_aabb(&self, other: &Self) -> bool { let l = glm::less_than_equal(&self.min, &other.max); let r = glm::greater_than_equal(&self.max, &other.min); @@ -43,31 +69,71 @@ impl Aabb { } } +impl Aabb +where + i32: AsPrimitive, +{ + /// Returns the center (centroid) of this AABB. + pub fn center(&self) -> TVec { + (self.min + self.max).map(|c| c / 2.as_()) + } +} + impl Aabb { /// Construct an AABB from a center (centroid) and the dimensions of the box /// along each axis. - pub fn from_center_and_dimensions(center: TVec, dims: TVec) -> Self { - let half = dims * T::from_subset(&0.5); + pub fn from_center_and_dimensions( + center: impl Into>, + dims: impl Into>, + ) -> Self { + let half = dims.into() * T::from_subset(&0.5); + let center = center.into(); Self { min: center - half, max: center + half, } } - pub fn center(&self) -> TVec { - // TODO: distribute multiplication to avoid intermediate overflow? - (self.min + self.max) * T::from_subset(&0.5) + pub fn collides_with_sphere( + &self, + center: impl Into>, + radius: impl Into, + ) -> bool { + self.distance_to_point(center.into()) <= radius.into() } - pub fn collides_with_sphere(&self, center: TVec, radius: T) -> bool { - self.distance_to_point(center) <= radius - } - - pub fn distance_to_point(&self, p: TVec) -> T { + pub fn distance_to_point(&self, p: impl Into>) -> T { + let p = p.into(); glm::distance(&p, &glm::clamp_vec(&p, &self.min, &self.max)) } } +impl Aabb +where + i32: AsPrimitive, +{ + pub fn surface_area(&self) -> T { + let d = self.dimensions(); + (d.x * d.y + d.y * d.z + d.z * d.x) * 2.as_() + } +} + +impl Aabb { + /// Constructs an AABB from a position and the dimensions of the box along + /// each axis. The position is the center of the bottom face of the AABB. + pub fn from_bottom_and_dimensions( + bottom: impl Into>, + dims: impl Into>, + ) -> Self { + let dims = dims.into(); + Self::from_center_and_dimensions(bottom, dims).translate([ + T::from_subset(&0.0), + dims.y * T::from_subset(&0.5), + T::from_subset(&0.0), + ]) + } +} + impl Default for Aabb { fn default() -> Self { let d = T::default(); diff --git a/src/block.rs b/src/block.rs index 0920d50..1c2cd35 100644 --- a/src/block.rs +++ b/src/block.rs @@ -1,4 +1,4 @@ -#![allow(clippy::all)] +#![allow(clippy::all, missing_docs)] use std::fmt; use std::io::{Read, Write}; diff --git a/src/block_pos.rs b/src/block_pos.rs index 48fb59c..fdc4c47 100644 --- a/src/block_pos.rs +++ b/src/block_pos.rs @@ -47,6 +47,18 @@ impl Decode for BlockPos { } } +impl From<(i32, i32, i32)> for BlockPos { + fn from((x, y, z): (i32, i32, i32)) -> Self { + BlockPos::new(x, y, z) + } +} + +impl From<[i32; 3]> for BlockPos { + fn from([x, y, z]: [i32; 3]) -> Self { + BlockPos::new(x, y, z) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/byte_angle.rs b/src/byte_angle.rs index c269ca0..741bc7b 100644 --- a/src/byte_angle.rs +++ b/src/byte_angle.rs @@ -1,6 +1,3 @@ -// TODO: rename to ByteAngle? - -use std::f64::consts::TAU; use std::io::{Read, Write}; use crate::protocol::{Decode, Encode}; @@ -10,12 +7,12 @@ use crate::protocol::{Decode, Encode}; pub struct ByteAngle(pub u8); impl ByteAngle { - pub fn from_radians_f64(f: f64) -> ByteAngle { - ByteAngle((f.rem_euclid(TAU) / TAU * 256.0).round() as u8) + pub fn from_degrees(f: f32) -> ByteAngle { + ByteAngle((f.rem_euclid(360.0) / 360.0 * 256.0).round() as u8) } - pub fn to_radians_f64(self) -> f64 { - self.0 as f64 / 256.0 * TAU + pub fn to_degrees(self) -> f32 { + self.0 as f32 / 256.0 * 360.0 } } diff --git a/src/chunk.rs b/src/chunk.rs index bfa9b49..603fae1 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -1,12 +1,14 @@ // TODO: https://github.com/rust-lang/rust/issues/88581 for div_ceil +use std::collections::HashMap; use std::io::Write; use std::iter::FusedIterator; +use std::ops::Deref; use bitvec::bitvec; use bitvec::vec::BitVec; use num::Integer; -use rayon::iter::ParallelIterator; +use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator}; use crate::block::BlockState; use crate::glm::DVec2; @@ -14,73 +16,99 @@ use crate::packets::play::{ BlockChange, ChunkDataAndUpdateLight, ChunkDataHeightmaps, ClientPlayPacket, MultiBlockChange, }; use crate::protocol::{Encode, Nbt}; -use crate::slotmap::{Key, SlotMap}; use crate::var_int::VarInt; -use crate::BiomeId; +use crate::{BiomeId, Server, Ticks}; -pub struct ChunkStore { - sm: SlotMap, +pub struct Chunks { + chunks: HashMap, + server: Server, + section_count: u32, } -impl ChunkStore { - pub(crate) fn new() -> Self { - Self { sm: SlotMap::new() } +impl Chunks { + pub(crate) fn new(server: Server, section_count: u32) -> Self { + Self { + chunks: HashMap::new(), + server, + section_count, + } } pub fn count(&self) -> usize { - self.sm.count() + self.chunks.len() } - pub fn create(&mut self, section_count: usize) -> ChunkId { - ChunkId(self.sm.insert(Chunk::new(section_count))) - } - - pub fn delete(&mut self, chunk: ChunkId) -> bool { - self.sm.remove(chunk.0).is_some() - } - - pub fn get(&self, chunk: ChunkId) -> Option<&Chunk> { - self.sm.get(chunk.0) - } - - pub fn get_mut(&mut self, chunk: ChunkId) -> Option<&mut Chunk> { - self.sm.get_mut(chunk.0) + pub fn get(&self, pos: ChunkPos) -> Option<&Chunk> { + self.chunks.get(&pos) } pub fn clear(&mut self) { - self.sm.clear(); + self.chunks.clear(); } - pub fn iter(&self) -> impl FusedIterator + Clone + '_ { - self.sm.iter().map(|(k, v)| (ChunkId(k), v)) + pub fn iter(&self) -> impl FusedIterator + Clone + '_ { + self.chunks.iter().map(|(&pos, chunk)| (pos, chunk)) } - pub fn iter_mut(&mut self) -> impl FusedIterator + '_ { - self.sm.iter_mut().map(|(k, v)| (ChunkId(k), v)) - } - - pub fn par_iter(&self) -> impl ParallelIterator + Clone + '_ { - self.sm.par_iter().map(|(k, v)| (ChunkId(k), v)) - } - - pub fn par_iter_mut(&mut self) -> impl ParallelIterator + '_ { - self.sm.par_iter_mut().map(|(k, v)| (ChunkId(k), v)) + pub fn par_iter(&self) -> impl ParallelIterator + Clone + '_ { + self.chunks.par_iter().map(|(&pos, chunk)| (pos, chunk)) } } -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] -pub struct ChunkId(Key); +impl<'a> ChunksMut<'a> { + pub(crate) fn new(chunks: &'a mut Chunks) -> Self { + Self(chunks) + } + + pub fn create(&mut self, pos: ChunkPos) -> bool { + let chunk = Chunk::new(self.section_count, self.server.current_tick()); + self.0.chunks.insert(pos, chunk).is_none() + } + + pub fn delete(&mut self, pos: ChunkPos) -> bool { + self.0.chunks.remove(&pos).is_some() + } + + pub fn get_mut(&mut self, pos: ChunkPos) -> Option { + self.0.chunks.get_mut(&pos).map(ChunkMut) + } + + pub fn iter_mut(&mut self) -> impl FusedIterator + '_ { + self.0 + .chunks + .iter_mut() + .map(|(&pos, chunk)| (pos, ChunkMut(chunk))) + } + + pub fn par_iter_mut(&mut self) -> impl ParallelIterator + '_ { + self.0 + .chunks + .par_iter_mut() + .map(|(&pos, chunk)| (pos, ChunkMut(chunk))) + } +} + +pub struct ChunksMut<'a>(&'a mut Chunks); + +impl<'a> Deref for ChunksMut<'a> { + type Target = Chunks; + + fn deref(&self) -> &Self::Target { + self.0 + } +} pub struct Chunk { sections: Box<[ChunkSection]>, // TODO block_entities: HashMap, + /// The MOTION_BLOCKING heightmap heightmap: Vec, modified: bool, - created_this_tick: bool, + created_tick: Ticks, } impl Chunk { - pub(crate) fn new(section_count: usize) -> Self { + pub(crate) fn new(section_count: u32, current_tick: Ticks) -> Self { let sect = ChunkSection { blocks: [BlockState::default(); 4096], biomes: [BiomeId::default(); 64], @@ -89,22 +117,18 @@ impl Chunk { }; let mut chunk = Self { - sections: vec![sect; section_count].into(), + sections: vec![sect; section_count as usize].into(), heightmap: Vec::new(), modified: true, - created_this_tick: true, + created_tick: current_tick, }; - chunk.apply_modifications(); + ChunkMut(&mut chunk).apply_modifications(); chunk } - pub fn created_this_tick(&self) -> bool { - self.created_this_tick - } - - pub(crate) fn clear_created_this_tick(&mut self) { - self.created_this_tick = false; + pub fn created_tick(&self) -> Ticks { + self.created_tick } pub fn height(&self) -> usize { @@ -119,20 +143,6 @@ impl Chunk { } } - pub fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState) { - if x < 16 && y < self.height() && z < 16 { - let sec = &mut self.sections[y / 16]; - let idx = x + z * 16 + y % 16 * 16 * 16; - if block != sec.blocks[idx] { - sec.blocks[idx] = block; - // TODO: set the modified bit. - sec.modified = true; - self.modified = true; - // TODO: update block entity if b could have block entity data. - } - } - } - pub fn get_biome(&self, x: usize, y: usize, z: usize) -> BiomeId { if x < 4 && y < self.height() / 4 && z < 4 { self.sections[y / 4].biomes[x + z * 4 + y % 4 * 4 * 4] @@ -141,57 +151,21 @@ impl Chunk { } } - pub fn set_biome(&mut self, x: usize, y: usize, z: usize, b: BiomeId) { - if x < 4 && y < self.height() / 4 && z < 4 { - self.sections[y / 4].biomes[x + z * 4 + y % 4 * 4 * 4] = b; - } - } - - /// Gets the chunk data packet with the given number of sections for this - /// chunk. This does not include unapplied changes. - pub(crate) fn chunk_data_packet( - &self, - pos: ChunkPos, - section_count: usize, - ) -> ChunkDataAndUpdateLight { + /// Gets the chunk data packet for this chunk with the given position. This + /// does not include unapplied changes. + pub(crate) fn chunk_data_packet(&self, pos: ChunkPos) -> ChunkDataAndUpdateLight { let mut blocks_and_biomes = Vec::new(); - for i in 0..section_count { - match self.sections.get(i) { - Some(sect) => { - blocks_and_biomes.extend_from_slice(§.compact_data); - } - None => { - // Extra chunk sections are encoded as empty with the default biome. - - // non air block count - 0i16.encode(&mut blocks_and_biomes).unwrap(); - // blocks - encode_paletted_container_single(0, &mut blocks_and_biomes).unwrap(); - // biomes - encode_paletted_container_single(0, &mut blocks_and_biomes).unwrap(); - } - } + for sect in self.sections.iter() { + blocks_and_biomes.extend_from_slice(§.compact_data); } - let motion_blocking = if section_count == self.sections.len() { - self.heightmap.clone() - } else { - // This is bad for two reasons: - // - Rebuilding the heightmap from scratch is slow. - // - The new heightmap is subtly wrong because we are building it from blocks - // with the modifications applied already. - // But you shouldn't be using chunks with heights different than the dimensions - // they're residing in anyway, so whatever. - let mut heightmap = Vec::new(); - build_heightmap(&self.sections, &mut heightmap); - heightmap - }; - ChunkDataAndUpdateLight { chunk_x: pos.x, chunk_z: pos.z, - heightmaps: Nbt(ChunkDataHeightmaps { motion_blocking }), + heightmaps: Nbt(ChunkDataHeightmaps { + motion_blocking: self.heightmap.clone(), + }), blocks_and_biomes, block_entities: Vec::new(), // TODO trust_edges: true, @@ -215,12 +189,44 @@ impl Chunk { // TODO None } +} + +pub struct ChunkMut<'a>(&'a mut Chunk); + +impl<'a> Deref for ChunkMut<'a> { + type Target = Chunk; + + fn deref(&self) -> &Self::Target { + self.0 + } +} + +impl<'a> ChunkMut<'a> { + pub fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState) { + if x < 16 && y < self.height() && z < 16 { + let sec = &mut self.0.sections[y / 16]; + let idx = x + z * 16 + y % 16 * 16 * 16; + if block != sec.blocks[idx] { + sec.blocks[idx] = block; + // TODO: set the modified bit. + sec.modified = true; + self.0.modified = true; + // TODO: update block entity if b could have block entity data. + } + } + } + + pub fn set_biome(&mut self, x: usize, y: usize, z: usize, b: BiomeId) { + if x < 4 && y < self.height() / 4 && z < 4 { + self.0.sections[y / 4].biomes[x + z * 4 + y % 4 * 4 * 4] = b; + } + } pub(crate) fn apply_modifications(&mut self) { if self.modified { - self.modified = false; + self.0.modified = false; - for sect in self.sections.iter_mut() { + for sect in self.0.sections.iter_mut() { if sect.modified { sect.modified = false; @@ -253,7 +259,7 @@ impl Chunk { } } - build_heightmap(&self.sections, &mut self.heightmap); + build_heightmap(&self.0.sections, &mut self.0.heightmap); } } } @@ -303,6 +309,24 @@ impl From<(i32, i32)> for ChunkPos { } } +impl Into<(i32, i32)> for ChunkPos { + fn into(self) -> (i32, i32) { + (self.x, self.z) + } +} + +impl From<[i32; 2]> for ChunkPos { + fn from([x, z]: [i32; 2]) -> Self { + (x, z).into() + } +} + +impl Into<[i32; 2]> for ChunkPos { + fn into(self) -> [i32; 2] { + [self.x, self.z] + } +} + /// A 16x16x16 section of blocks, biomes, and light in a chunk. #[derive(Clone)] struct ChunkSection { diff --git a/src/client.rs b/src/client.rs index 2930a79..544f04b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,41 +1,50 @@ -use std::collections::hash_map::Entry; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::iter::FusedIterator; +use std::ops::Deref; use flume::{Receiver, Sender, TrySendError}; use glm::DVec3; use rayon::iter::ParallelIterator; +use uuid::Uuid; use crate::block_pos::BlockPos; -use crate::chunk::ChunkId; use crate::config::{ Biome, BiomeGrassColorModifier, BiomePrecipitation, Dimension, DimensionEffects, DimensionId, }; +use crate::entity::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, DimensionCodec, DimensionType, DimensionTypeRegistry, - DimensionTypeRegistryEntry, Disconnect, JoinGame, KeepAliveClientbound, PlayerPositionAndLook, - PlayerPositionAndLookFlags, ServerPlayPacket, SpawnPosition, UnloadChunk, UpdateViewDistance, - UpdateViewPosition, + ChangeGameStateReason, ClientPlayPacket, DestroyEntities, DimensionCodec, DimensionType, + DimensionTypeRegistry, DimensionTypeRegistryEntry, Disconnect, JoinGame, KeepAliveClientbound, + PlayerPositionAndLook, PlayerPositionAndLookFlags, ServerPlayPacket, SpawnPosition, + UnloadChunk, UpdateViewDistance, UpdateViewPosition, }; use crate::protocol::{BoundedInt, Nbt}; -use crate::server::{Other, ServerPacketChannels}; +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::world::WorldId; use crate::{ - glm, ident, ChunkPos, ChunkStore, EntityId, EntityStore, Server, Text, Ticks, WorldStore, - LIBRARY_NAMESPACE, + glm, ident, ChunkPos, Chunks, Entities, EntityId, Server, Text, Ticks, LIBRARY_NAMESPACE, }; -pub struct ClientStore { +pub struct Clients { sm: SlotMap, } -impl ClientStore { +pub struct ClientsMut<'a>(&'a mut Clients); + +impl<'a> Deref for ClientsMut<'a> { + type Target = Clients; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Clients { pub(crate) fn new() -> Self { Self { sm: SlotMap::new() } } @@ -44,43 +53,58 @@ impl ClientStore { self.sm.count() } - pub(crate) fn create(&mut self, client: Client) -> ClientId { - ClientId(self.sm.insert(client)) - } - - pub fn delete(&mut self, client: ClientId) -> bool { - self.sm.remove(client.0).is_some() - } - - pub fn retain(&mut self, mut f: impl FnMut(ClientId, &mut Client) -> bool) { - self.sm.retain(|k, v| f(ClientId(k), v)) - } - pub fn get(&self, client: ClientId) -> Option<&Client> { self.sm.get(client.0) } - pub fn get_mut(&mut self, client: ClientId) -> Option<&mut Client> { - self.sm.get_mut(client.0) - } - pub fn iter(&self) -> impl FusedIterator + Clone + '_ { self.sm.iter().map(|(k, v)| (ClientId(k), v)) } - pub fn iter_mut(&mut self) -> impl FusedIterator + '_ { - self.sm.iter_mut().map(|(k, v)| (ClientId(k), v)) - } - pub fn par_iter(&self) -> impl ParallelIterator + Clone + '_ { self.sm.par_iter().map(|(k, v)| (ClientId(k), v)) } - - pub fn par_iter_mut(&mut self) -> impl ParallelIterator + '_ { - self.sm.par_iter_mut().map(|(k, v)| (ClientId(k), v)) - } } +impl<'a> ClientsMut<'a> { + pub(crate) fn new(c: &'a mut Clients) -> Self { + Self(c) + } + + pub fn reborrow(&mut self) -> ClientsMut { + ClientsMut(self.0) + } + + pub(crate) fn create(&mut self, client: Client) -> ClientId { + ClientId(self.0.sm.insert(client)) + } + + pub fn delete(&mut self, client: ClientId) -> bool { + self.0.sm.remove(client.0).is_some() + } + + pub fn retain(&mut self, mut f: impl FnMut(ClientId, ClientMut) -> bool) { + self.0.sm.retain(|k, v| f(ClientId(k), ClientMut(v))) + } + + pub fn get_mut(&mut self, client: ClientId) -> Option { + self.0.sm.get_mut(client.0).map(ClientMut) + } + + pub fn iter_mut(&mut self) -> impl FusedIterator + '_ { + self.0 + .sm + .iter_mut() + .map(|(k, v)| (ClientId(k), ClientMut(v))) + } + + pub fn par_iter_mut(&mut self) -> impl ParallelIterator + '_ { + self.0 + .sm + .par_iter_mut() + .map(|(k, v)| (ClientId(k), ClientMut(v))) + } +} pub struct ClientId(Key); /// Represents a client connected to the server after logging in. @@ -88,11 +112,10 @@ pub struct Client { /// Setting this to `None` disconnects the client. send: Option>, recv: Receiver, - /// The entity this client is associated with. - entity: EntityId, /// The tick this client was created. created_tick: Ticks, username: String, + uuid: Uuid, on_ground: bool, new_position: DVec3, old_position: DVec3, @@ -113,9 +136,6 @@ pub struct Client { spawn_position_yaw: f32, /// If spawn_position or spawn_position_yaw were modified this tick. modified_spawn_position: bool, - /// The world that this client was in at the end of the previous tick. - new_world: Option, - old_world: Option, events: Vec, /// The ID of the last keepalive sent. last_keepalive_id: i64, @@ -127,9 +147,7 @@ pub struct Client { /// This is used to determine what entity create/destroy packets should be /// sent. loaded_entities: HashSet, - hidden_entities: HashSet, - /// Loaded chunks and their positions. - loaded_chunks: HashMap, + loaded_chunks: HashSet, new_game_mode: GameMode, old_game_mode: GameMode, settings: Option, @@ -137,11 +155,21 @@ pub struct Client { // TODO: time, weather } +pub struct ClientMut<'a>(&'a mut Client); + +impl<'a> Deref for ClientMut<'a> { + type Target = Client; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + impl Client { pub(crate) fn new( packet_channels: ServerPacketChannels, - entity: EntityId, username: String, + uuid: Uuid, server: &Server, ) -> Self { let (send, recv) = packet_channels; @@ -149,9 +177,9 @@ impl Client { Self { send: Some(send), recv, - entity, created_tick: server.current_tick(), username, + uuid, on_ground: false, new_position: DVec3::default(), old_position: DVec3::default(), @@ -163,16 +191,13 @@ impl Client { spawn_position: BlockPos::default(), spawn_position_yaw: 0.0, modified_spawn_position: true, - old_world: None, - new_world: None, events: Vec::new(), last_keepalive_id: 0, got_keepalive: true, new_max_view_distance: 16, old_max_view_distance: 0, loaded_entities: HashSet::new(), - hidden_entities: HashSet::new(), - loaded_chunks: HashMap::new(), + loaded_chunks: HashSet::new(), new_game_mode: GameMode::Survival, old_game_mode: GameMode::Survival, settings: None, @@ -187,6 +212,10 @@ impl Client { &self.username } + pub fn uuid(&self) -> Uuid { + self.uuid + } + pub fn position(&self) -> DVec3 { self.new_position } @@ -199,80 +228,14 @@ impl Client { self.pitch } - pub fn teleport(&mut self, pos: impl Into, yaw: f32, pitch: f32) { - self.new_position = pos.into(); - self.yaw = yaw; - self.pitch = pitch; - - if !self.teleported_this_tick { - self.teleported_this_tick = true; - - self.pending_teleports = match self.pending_teleports.checked_add(1) { - Some(n) => n, - None => { - self.disconnect("Too many pending teleports"); - return; - } - }; - - self.teleport_id_counter = self.teleport_id_counter.wrapping_add(1); - } - } - 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; - } - pub fn on_ground(&self) -> bool { self.on_ground } - /// Changes the point at which compasses point at. - 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 { - self.spawn_position = pos; - self.spawn_position_yaw = yaw_degrees; - self.modified_spawn_position = true; - } - } - - pub fn world(&self) -> Option { - self.new_world - } - - pub fn set_world(&mut self, new_world: Option) { - self.new_world = new_world; - } - - /// Attempts to enqueue a play packet to be sent to this client. The client - /// is disconnected if the clientbound packet buffer is full. - pub(crate) fn send_packet(&mut self, packet: impl Into) { - send_packet(&mut self.send, packet); - } - - pub fn disconnect(&mut self, reason: impl Into) { - if self.send.is_some() { - let txt = reason.into(); - log::info!("disconnecting client '{}': \"{txt}\"", self.username); - - self.send_packet(Disconnect { reason: txt }); - - self.send = None; - } - } - - pub fn disconnect_no_reason(&mut self) { - if self.send.is_some() { - log::info!("disconnecting client '{}' (no reason)", self.username); - self.send = None; - } - } - pub fn is_disconnected(&self) -> bool { self.send.is_none() } @@ -285,28 +248,91 @@ impl Client { self.new_max_view_distance } - /// The new view distance is clamped to `2..=32`. - pub fn set_max_view_distance(&mut self, dist: u8) { - self.new_max_view_distance = dist.clamp(2, 32); - } - pub fn settings(&self) -> Option<&Settings> { self.settings.as_ref() } +} - /// Returns the entity this client is backing. - pub fn entity(&self) -> EntityId { - self.entity +impl<'a> ClientMut<'a> { + pub(crate) fn new(client: &'a mut Client) -> Self { + Self(client) + } + + pub fn reborrow(&mut self) -> ClientMut { + ClientMut(self.0) + } + + pub fn teleport(&mut self, pos: impl Into, yaw: f32, pitch: f32) { + self.0.new_position = pos.into(); + self.0.yaw = yaw; + self.0.pitch = pitch; + + if !self.teleported_this_tick { + self.0.teleported_this_tick = true; + + self.0.pending_teleports = match self.0.pending_teleports.checked_add(1) { + Some(n) => n, + None => { + self.disconnect("Too many pending teleports"); + return; + } + }; + + self.0.teleport_id_counter = self.0.teleport_id_counter.wrapping_add(1); + } + } + + pub fn set_game_mode(&mut self, new_game_mode: GameMode) { + self.0.new_game_mode = new_game_mode; + } + + /// Changes the point at which compasses point at. + pub fn set_spawn_position(&mut self, pos: impl Into, yaw_degrees: f32) { + let pos = pos.into(); + if pos != self.0.spawn_position || yaw_degrees != self.0.spawn_position_yaw { + self.0.spawn_position = pos; + self.0.spawn_position_yaw = yaw_degrees; + self.0.modified_spawn_position = true; + } + } + + /// Attempts to enqueue a play packet to be sent to this client. The client + /// is disconnected if the clientbound packet buffer is full. + pub(crate) fn send_packet(&mut self, packet: impl Into) { + send_packet(&mut self.0.send, packet); + } + + pub fn disconnect(&mut self, reason: impl Into) { + if self.0.send.is_some() { + let txt = reason.into(); + log::info!("disconnecting client '{}': \"{txt}\"", self.0.username); + + self.send_packet(Disconnect { reason: txt }); + + self.0.send = None; + } + } + + pub fn disconnect_no_reason(&mut self) { + if self.0.send.is_some() { + log::info!("disconnecting client '{}' (no reason)", self.0.username); + self.0.send = None; + } + } + + /// The new view distance is clamped to `2..=32`. + pub fn set_max_view_distance(&mut self, dist: u8) { + self.0.new_max_view_distance = dist.clamp(2, 32); } pub(crate) fn update( &mut self, - entities: &EntityStore, - worlds: &WorldStore, - chunks: &ChunkStore, - other: &Other, + server: &Server, + entities: &Entities, + chunks: &Chunks, + dimension_id: DimensionId, ) { - self.events.clear(); + self.0.events.clear(); if self.is_disconnected() { return; @@ -318,34 +344,28 @@ impl Client { // Mark the client as disconnected when appropriate. // We do this check after handling serverbound packets so that none are lost. - if self.recv.is_disconnected() - || self.send.as_ref().map_or(true, |s| s.is_disconnected()) - || entities.get(self.entity).is_none() - { - self.send = None; + if self.recv.is_disconnected() || self.send.as_ref().map_or(true, |s| s.is_disconnected()) { + self.0.send = None; return; } - let world = self.new_world.and_then(|w| worlds.get(w)); - - let dim_id = world.map_or(DimensionId::default(), |w| w.dimension()); - let dim = other.dimension(dim_id); + let dimension = server.dimension(dimension_id); // 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 == other.current_tick() { + if self.created_tick == server.current_tick() { self.send_packet(JoinGame { - entity_id: self.entity.to_network_id(), + entity_id: 0, // EntityId 0 is reserved for clients. is_hardcore: false, // TODO gamemode: self.new_game_mode, previous_gamemode: self.old_game_mode, - dimension_names: other + dimension_names: server .dimensions() .map(|(id, _)| ident!("{LIBRARY_NAMESPACE}:dimension_{}", id.0)) .collect(), - dimension_codec: Nbt(make_dimension_codec(other)), - dimension: Nbt(to_dimension_registry_item(dim)), - dimension_name: ident!("{LIBRARY_NAMESPACE}:dimension_{}", dim_id.0), + dimension_codec: Nbt(make_dimension_codec(server)), + dimension: Nbt(to_dimension_registry_item(dimension)), + dimension_name: ident!("{LIBRARY_NAMESPACE}:dimension_{}", dimension_id.0), hashed_seed: 0, max_players: VarInt(0), view_distance: BoundedInt(VarInt(self.new_max_view_distance as i32)), @@ -357,17 +377,17 @@ impl Client { }); self.teleport(self.position(), self.yaw(), self.pitch()); - } else if self.old_game_mode != self.new_game_mode { - self.old_game_mode = self.new_game_mode; + } else if self.0.old_game_mode != self.0.new_game_mode { + self.0.old_game_mode = self.0.new_game_mode; self.send_packet(ChangeGameState { reason: ChangeGameStateReason::ChangeGameMode, - value: self.new_game_mode as i32 as f32, + value: self.0.new_game_mode as i32 as f32, }); } // Update the players spawn position (compass position) - if self.modified_spawn_position { - self.modified_spawn_position = false; + if self.0.modified_spawn_position { + self.0.modified_spawn_position = false; self.send_packet(SpawnPosition { location: self.spawn_position, @@ -376,61 +396,42 @@ impl Client { } // Update view distance fog on the client if necessary. - if self.old_max_view_distance != self.new_max_view_distance { - self.old_max_view_distance = self.new_max_view_distance; - if self.created_tick != other.current_tick() { + 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() { self.send_packet(UpdateViewDistance { - view_distance: BoundedInt(VarInt(self.new_max_view_distance as i32)), + view_distance: BoundedInt(VarInt(self.0.new_max_view_distance as i32)), }) } } // Check if it's time to send another keepalive. - if other.current_tick() % (other.tick_rate() * 8) == 0 { - if self.got_keepalive { + if server.current_tick() % (server.tick_rate() * 8) == 0 { + if self.0.got_keepalive { let id = rand::random(); self.send_packet(KeepAliveClientbound { id }); - self.last_keepalive_id = id; - self.got_keepalive = false; + self.0.last_keepalive_id = id; + self.0.got_keepalive = false; } else { self.disconnect("Timed out (no keepalive response)"); } } - // Load, update, and unload chunks. - if self.old_world != self.new_world { - let old_dim = self - .old_world - .and_then(|w| worlds.get(w)) - .map_or(DimensionId::default(), |w| w.dimension()); - - let new_dim = dim_id; - - if old_dim != new_dim { - // Changing dimensions automatically unloads all chunks and - // entities. - self.loaded_chunks.clear(); - self.loaded_entities.clear(); - - todo!("need to send respawn packet for new dimension"); - } - - self.old_world = self.new_world; - } - + // The actual view distance. let view_dist = self + .0 .settings .as_ref() .map_or(2, |s| s.view_distance) .min(self.new_max_view_distance); - let center = ChunkPos::from_xz(self.new_position.xz()); + let center = ChunkPos::from_xz(self.0.new_position.xz()); // Send the update view position packet if the client changes the chunk section // they're in. { - let old_section = self.old_position.map(|n| (n / 16.0) as i32); - let new_section = self.new_position.map(|n| (n / 16.0) as i32); + let old_section = self.0.old_position.map(|n| (n / 16.0) as i32); + let new_section = self.0.new_position.map(|n| (n / 16.0) as i32); if old_section != new_section { self.send_packet(UpdateViewPosition { @@ -440,50 +441,41 @@ impl Client { } } - // Unload deleted chunks and those outside the view distance. Also update - // existing chunks. - self.loaded_chunks.retain(|&pos, &mut chunk_id| { - if let Some(chunk) = chunks.get(chunk_id) { - // The cache stops chunk data packets from needing to be sent when a player is - // jumping between adjacent chunks. - let cache = 2; - if is_chunk_in_view_distance(center, pos, view_dist + cache) { + // Update existing chunks and unload those outside the view distance. Chunks + // that have been overwritten also need to be unloaded. + self.0.loaded_chunks.retain(|&pos| { + // The cache stops chunk data packets from needing to be sent when a player + // moves to an adjacent chunk and back to the original. + let cache = 2; + + if let Some(chunk) = chunks.get(pos) { + if is_chunk_in_view_distance(center, pos, view_dist + cache) + && chunk.created_tick() != server.current_tick() + { if let Some(pkt) = chunk.block_change_packet(pos) { - send_packet(&mut self.send, pkt); + send_packet(&mut self.0.send, pkt); } - true - } else { - send_packet( - &mut self.send, - UnloadChunk { - chunk_x: pos.x, - chunk_z: pos.z, - }, - ); - false + return true; } - } else { - send_packet( - &mut self.send, - UnloadChunk { - chunk_x: pos.x, - chunk_z: pos.z, - }, - ); - false } + + send_packet( + &mut self.0.send, + UnloadChunk { + chunk_x: pos.x, + chunk_z: pos.z, + }, + ); + false }); // Load new chunks within the view distance for pos in chunks_in_view_distance(center, view_dist) { - if let Entry::Vacant(ve) = self.loaded_chunks.entry(pos) { - if let Some(&chunk_id) = world.and_then(|w| w.chunks().get(&pos)) { - if let Some(chunk) = chunks.get(chunk_id) { - ve.insert(chunk_id); - self.send_packet(chunk.chunk_data_packet(pos, (dim.height / 16) as usize)); - if let Some(pkt) = chunk.block_change_packet(pos) { - self.send_packet(pkt); - } + if let Some(chunk) = chunks.get(pos) { + if self.0.loaded_chunks.insert(pos) { + self.send_packet(chunk.chunk_data_packet(pos)); + if let Some(pkt) = chunk.block_change_packet(pos) { + self.send_packet(pkt); } } } @@ -491,25 +483,69 @@ impl Client { // This is done after the chunks are loaded so that the "downloading terrain" // screen is closed at the appropriate time. - if self.teleported_this_tick { - self.teleported_this_tick = false; + + // TODO: temporarily broken + // if self.0.teleported_this_tick { + // self.0.teleported_this_tick = false; - self.send_packet(PlayerPositionAndLook { - x: self.new_position.x, - y: self.new_position.y, - z: self.new_position.z, - yaw: self.yaw, - pitch: self.pitch, - flags: PlayerPositionAndLookFlags::new(false, false, false, false, false), - teleport_id: VarInt((self.teleport_id_counter - 1) as i32), - dismount_vehicle: false, + // self.send_packet(dbg!(PlayerPositionAndLook { + // position: self.new_position, + // yaw: self.yaw, + // pitch: self.pitch, + // flags: PlayerPositionAndLookFlags::new(false, false, false, false, false), + // teleport_id: VarInt((self.teleport_id_counter - 1) as i32), + // dismount_vehicle: false, + // })); + // } + + let mut entities_to_unload = Vec::new(); + + // Update all entities that are visible and unload entities that are no + // longer visible. + self.0.loaded_entities.retain(|&id| { + if let Some(entity) = entities.get(id) { + if glm::distance(&self.0.new_position, &entity.position()) + <= view_dist as f64 * 16.0 + { + todo!("update entity"); + return true; + } + } + + entities_to_unload.push(VarInt(id.to_network_id())); + false + }); + + if !entities_to_unload.is_empty() { + self.send_packet(DestroyEntities { + entities: entities_to_unload, }); } - self.old_position = self.new_position; + // Spawn new entities within the view distance. + for (id, entity) in entities.iter() { + if glm::distance(&self.position(), &entity.position()) <= view_dist as f64 * 16.0 + && entity.typ() != EntityType::Marker + && self.0.loaded_entities.insert(id) + { + self.send_packet( + entity + .spawn_packet(id) + .expect("should not be a marker entity"), + ); + + if let Some(meta) = entity.initial_metadata_packet(id) { + self.send_packet(meta); + } + } + } + + self.0.old_position = self.0.new_position; } fn handle_serverbound_packet(&mut self, pkt: ServerPlayPacket) { + let client = &mut self.0; + fn handle_movement_packet( client: &mut Client, new_position: DVec3, @@ -536,18 +572,18 @@ impl Client { match pkt { ServerPlayPacket::TeleportConfirm(p) => { - if self.pending_teleports == 0 { + if client.pending_teleports == 0 { self.disconnect("Unexpected teleport confirmation"); return; } let got = p.teleport_id.0 as u32; - let expected = self + let expected = client .teleport_id_counter - .wrapping_sub(self.pending_teleports); + .wrapping_sub(client.pending_teleports); if got == expected { - self.pending_teleports -= 1; + client.pending_teleports -= 1; } else { self.disconnect(format!( "Unexpected teleport ID (expected {expected}, got {got})" @@ -559,7 +595,7 @@ impl Client { ServerPlayPacket::ChatMessageServerbound(_) => {} ServerPlayPacket::ClientStatus(_) => {} ServerPlayPacket::ClientSettings(p) => { - let old = self.settings.replace(Settings { + let old = client.settings.replace(Settings { locale: p.locale.0, view_distance: p.view_distance.0, chat_mode: p.chat_mode, @@ -569,7 +605,7 @@ impl Client { allow_server_listings: p.allow_server_listings, }); - self.events.push(Event::SettingsChanged(old)); + client.events.push(Event::SettingsChanged(old)); } ServerPlayPacket::TabCompleteServerbound(_) => {} ServerPlayPacket::ClickWindowButton(_) => {} @@ -581,39 +617,36 @@ impl Client { ServerPlayPacket::InteractEntity(_) => {} ServerPlayPacket::GenerateStructure(_) => {} ServerPlayPacket::KeepAliveServerbound(p) => { - if self.got_keepalive { + let last_keepalive_id = client.last_keepalive_id; + if client.got_keepalive { self.disconnect("Unexpected keepalive"); - } else if p.id != self.last_keepalive_id { + } else if p.id != last_keepalive_id { self.disconnect(format!( "Keepalive ids don't match (expected {}, got {})", - self.last_keepalive_id, p.id + last_keepalive_id, p.id )); } else { - self.got_keepalive = true; + client.got_keepalive = true; } } ServerPlayPacket::LockDifficulty(_) => {} - ServerPlayPacket::PlayerPosition(p) => handle_movement_packet( - self, - glm::vec3(p.x, p.feet_y, p.z), - self.yaw, - self.pitch, - p.on_ground, - ), - ServerPlayPacket::PlayerPositionAndRotation(p) => handle_movement_packet( - self, - glm::vec3(p.x, p.feet_y, p.z), - p.yaw, - p.pitch, - p.on_ground, - ), + ServerPlayPacket::PlayerPosition(p) => { + handle_movement_packet(client, p.position, client.yaw, client.pitch, p.on_ground) + } + ServerPlayPacket::PlayerPositionAndRotation(p) => { + handle_movement_packet(client, p.position, p.yaw, p.pitch, p.on_ground) + } ServerPlayPacket::PlayerRotation(p) => { - handle_movement_packet(self, self.new_position, p.yaw, p.pitch, p.on_ground) + handle_movement_packet(client, client.new_position, p.yaw, p.pitch, p.on_ground) } - ServerPlayPacket::PlayerMovement(p) => { - handle_movement_packet(self, self.new_position, self.yaw, self.pitch, p.on_ground) - } + ServerPlayPacket::PlayerMovement(p) => handle_movement_packet( + client, + client.new_position, + client.yaw, + client.pitch, + p.on_ground, + ), ServerPlayPacket::VehicleMoveServerbound(_) => {} ServerPlayPacket::SteerBoat(_) => {} ServerPlayPacket::PickItem(_) => {} @@ -700,9 +733,9 @@ fn send_packet(send_opt: &mut Option>, pkt: impl Into DimensionCodec { +fn make_dimension_codec(server: &Server) -> DimensionCodec { let mut dims = Vec::new(); - for (id, dim) in other.dimensions() { + for (id, dim) in server.dimensions() { let id = id.0 as i32; dims.push(DimensionTypeRegistryEntry { name: ident!("{LIBRARY_NAMESPACE}:dimension_type_{id}"), @@ -712,7 +745,7 @@ fn make_dimension_codec(other: &Other) -> DimensionCodec { } let mut biomes = Vec::new(); - for (id, biome) in other.biomes() { + for (id, biome) in server.biomes() { biomes.push(to_biome_registry_item(biome, id.0 as i32)); } diff --git a/src/config.rs b/src/config.rs index 56fd7f4..c847f94 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,15 +5,17 @@ use std::panic::{RefUnwindSafe, UnwindSafe}; use async_trait::async_trait; use tokio::runtime::Handle as TokioHandle; -use crate::{ident, Id, Identifier, NewClientData, Server, SharedServer, Text, Ticks}; +use crate::client::ClientMut; +use crate::{ident, Identifier, NewClientData, Server, Text, Ticks, WorldId, WorldsMut}; /// A trait containing callbacks which are invoked by the running Minecraft /// 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. In other words, a mutex can always be aquired at the beginning -/// of a method and released at the end without risk of deadlocking. +/// 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 @@ -48,6 +50,9 @@ pub trait Config: Any + Send + Sync + UnwindSafe + RefUnwindSafe { /// /// The tick rate must be greater than zero. /// + /// Note that the official Minecraft client only processes packets at 20hz, + /// 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 { @@ -140,6 +145,45 @@ pub trait Config: Any + Send + Sync + UnwindSafe + RefUnwindSafe { vec![Biome::default()] } + /// Called when the server receives a Server List Ping query. + /// Data for the response can be provided or the query can be ignored. + /// + /// This method is called from within a tokio runtime. + /// + /// # Default Implementation + /// The query is ignored. + async fn server_list_ping(&self, server: &Server, remote_addr: SocketAddr) -> ServerListPing { + ServerListPing::Ignore + } + + /// 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. + /// + /// This method is the appropriate place to perform asynchronous + /// operations such as database queries which may take some time to + /// complete. If you need access to the worlds on the server and don't need + /// async, see [`Config::join`]. + /// + /// This method is called from within a tokio runtime. + /// + /// # Default Implementation + /// The client is allowed to join unconditionally. + async fn login(&self, server: &Server, ncd: &NewClientData) -> Result<(), Text> { + Ok(()) + } + + /// Called after a successful [`Config::login`] to determine what world the + /// new client should join. If this method returns with `Err(reason)`, then + /// the client is immediately disconnected with the given reason. + /// + /// If the returned [`WorldId`] is invalid, then the client is disconnected. + /// + /// This method is called from within a tokio runtime. + fn join(&self, server: &Server, client: ClientMut, worlds: WorldsMut) -> Result; + /// Called after the server is created, but prior to accepting connections /// and entering the update loop. /// @@ -147,10 +191,7 @@ pub trait Config: Any + Send + Sync + UnwindSafe + RefUnwindSafe { /// no connections to the server will be made until this function returns. /// /// This method is called from within a tokio runtime. - /// - /// # Default Implementation - /// The default implementation does nothing. - fn init(&self, server: &mut Server) {} + fn init(&self, server: &Server, worlds: WorldsMut) {} /// Called once at the beginning of every server update (also known as /// a "tick"). @@ -162,37 +203,7 @@ pub trait Config: Any + Send + Sync + UnwindSafe + RefUnwindSafe { /// /// # Default Implementation /// The default implementation does nothing. - fn update(&self, server: &mut Server) {} - - /// Called when the server receives a Server List Ping query. - /// Data for the response can be provided or the query can be ignored. - /// - /// This method is called from within a tokio runtime. - /// - /// # Default Implementation - /// The query is ignored. - async fn server_list_ping( - &self, - server: &SharedServer, - remote_addr: SocketAddr, - ) -> ServerListPing { - ServerListPing::Ignore - } - - /// Called asynchronously for each client after successful authentication - /// (if online mode is enabled) to determine if they are allowed to join the - /// server. On success, a client-backed entity is spawned. - /// - /// This function is the appropriate place to perform - /// player count checks, whitelist checks, database queries, etc. - /// - /// This method is called from within a tokio runtime. - /// - /// # Default Implementation - /// The client is allowed to join unconditionally. - async fn login(&self, server: &SharedServer, ncd: &NewClientData) -> Login { - Login::Join - } + fn update(&self, server: &Server, worlds: WorldsMut); } /// The result of the [`server_list_ping`](Handler::server_list_ping) callback. @@ -213,25 +224,14 @@ pub enum ServerListPing<'a> { Ignore, } -/// The result of the [`login`](Handler::login) callback. -#[derive(Debug)] -pub enum Login { - /// The client may join the server. - Join, - /// The client may not join the server and will be disconnected with the - /// provided reason. - Disconnect(Text), -} - /// A handle to a particular [`Dimension`] on the server. /// /// Dimension IDs must only be used on servers from which they originate. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct DimensionId(pub(crate) u16); -/// All dimension IDs are valid. -impl Id for DimensionId { - fn idx(self) -> usize { +impl DimensionId { + pub fn to_index(self) -> usize { self.0 as usize } } @@ -313,9 +313,8 @@ pub enum DimensionEffects { #[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct BiomeId(pub(crate) u16); -/// All Biome IDs are valid. -impl Id for BiomeId { - fn idx(self) -> usize { +impl BiomeId { + pub fn to_index(self) -> usize { self.0 as usize } } diff --git a/src/entity.rs b/src/entity.rs index a92ea1b..d558314 100644 --- a/src/entity.rs +++ b/src/entity.rs @@ -4,77 +4,44 @@ pub mod types; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::iter::FusedIterator; +use std::ops::Deref; +use bitfield_struct::bitfield; use rayon::iter::ParallelIterator; use uuid::Uuid; +use crate::byte_angle::ByteAngle; use crate::chunk::ChunkPos; -use crate::glm::DVec3; +use crate::glm::{DVec3, I16Vec3, Vec3}; +use crate::packets::play::{ + ClientPlayPacket, EntityMetadata, SpawnEntity, SpawnExperienceOrb, SpawnLivingEntity, + SpawnPainting, SpawnPlayer, +}; +use crate::protocol::ReadToEnd; use crate::slotmap::{Key, SlotMap}; -use crate::{Aabb, Id, WorldId}; +use crate::var_int::VarInt; +use crate::{Aabb, WorldId}; -pub struct EntityStore { +pub struct Entities { sm: SlotMap, uuid_to_entity: HashMap, - /// Maps chunk positions to the set of all entities with bounding volumes - /// intersecting that chunk. - partition: HashMap<(WorldId, ChunkPos), Vec>, } -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] -pub struct EntityId(Key); +pub struct EntitiesMut<'a>(&'a mut Entities); -impl Id for EntityId { - fn idx(self) -> usize { - self.0.index() as usize +impl<'a> Deref for EntitiesMut<'a> { + type Target = Entities; + + fn deref(&self) -> &Self::Target { + self.0 } } -impl EntityId { - pub(crate) fn to_network_id(self) -> i32 { - // TODO: is ID 0 reserved? - self.0.index() as i32 - } -} - -pub struct Entity { - data: EntityData, - old_type: EntityType, - new_position: DVec3, - old_position: DVec3, - new_world: Option, - old_world: Option, - uuid: Uuid, -} - -impl Entity { - pub fn data(&self) -> &EntityData { - &self.data - } - - pub fn typ(&self) -> EntityType { - self.data.typ() - } - - /// Changes the type of this entity. - pub fn change_type(&mut self, new_type: EntityType) { - todo!(); // TODO - } - - fn hitbox(&self) -> Aabb { - // TODO - Aabb::default() - } -} - -pub use types::{EntityData, EntityType}; - -impl EntityStore { +impl Entities { pub(crate) fn new() -> Self { Self { sm: SlotMap::new(), uuid_to_entity: HashMap::new(), - partition: HashMap::new(), } } @@ -83,11 +50,38 @@ impl EntityStore { self.sm.count() } - /// Spawns a new entity with the default data. The new entity'd [`EntityId`] + /// 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. + pub fn get_with_uuid(&self, uuid: Uuid) -> Option { + self.uuid_to_entity.get(&uuid).cloned() + } + + pub fn get(&self, entity: EntityId) -> Option<&Entity> { + self.sm.get(entity.0) + } + + pub fn iter(&self) -> impl FusedIterator + Clone + '_ { + self.sm.iter().map(|(k, v)| (EntityId(k), v)) + } + + pub fn par_iter(&self) -> impl ParallelIterator + Clone + '_ { + self.sm.par_iter().map(|(k, v)| (EntityId(k), v)) + } +} + +impl<'a> EntitiesMut<'a> { + pub(crate) fn new(entities: &'a mut Entities) -> Self { + Self(entities) + } + + /// Spawns a new entity with the default data. The new entity's [`EntityId`] /// is returned. /// /// To actually see the new entity, set its position to somewhere nearby and - /// [change its type](EntityData::change_type) to something visible. + /// [set its type](EntityData::set_type) to something visible. pub fn create(&mut self) -> EntityId { loop { let uuid = Uuid::from_bytes(rand::random()); @@ -100,19 +94,22 @@ impl EntityStore { /// Like [`create`](Entities::create), but requires specifying the new /// entity's UUID. This is useful for deserialization. /// - /// The provided UUID must not conflict with an existing entity UUID. If it - /// does, `None` is returned and the entity is not spawned. + /// 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. pub fn create_with_uuid(&mut self, uuid: Uuid) -> Option { - match self.uuid_to_entity.entry(uuid) { + match self.0.uuid_to_entity.entry(uuid) { Entry::Occupied(_) => None, Entry::Vacant(ve) => { - let entity = EntityId(self.sm.insert(Entity { - data: EntityData::Marker(types::Marker::new()), - old_type: EntityType::Marker, + let entity = EntityId(self.0.sm.insert(Entity { + flags: EntityFlags(0), + meta: EntityMeta::new(EntityType::Marker), new_position: DVec3::default(), old_position: DVec3::default(), - new_world: None, - old_world: None, + yaw: 0.0, + pitch: 0.0, + head_yaw: 0.0, + head_pitch: 0.0, + velocity: Vec3::default(), uuid, })); @@ -125,18 +122,10 @@ impl EntityStore { } } - /// 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. - pub fn get_with_uuid(&self, uuid: Uuid) -> Option { - self.uuid_to_entity.get(&uuid).cloned() - } - pub fn delete(&mut self, entity: EntityId) -> bool { - if let Some(e) = self.sm.remove(entity.0) { - self.uuid_to_entity + if let Some(e) = self.0.sm.remove(entity.0) { + self.0 + .uuid_to_entity .remove(&e.uuid) .expect("UUID should have been in UUID map"); @@ -147,197 +136,466 @@ impl EntityStore { } } - pub fn retain(&mut self, mut f: impl FnMut(EntityId, &mut Entity) -> bool) { - self.sm.retain(|k, v| f(EntityId(k), v)) + pub fn retain(&mut self, mut f: impl FnMut(EntityId, EntityMut) -> bool) { + // TODO + self.0.sm.retain(|k, v| f(EntityId(k), EntityMut(v))) } - pub fn get(&self, entity: EntityId) -> Option<&Entity> { - self.sm.get(entity.0) + pub fn get_mut(&mut self, entity: EntityId) -> Option { + self.0.sm.get_mut(entity.0).map(EntityMut) } - pub fn get_mut(&mut self, entity: EntityId) -> Option<&mut Entity> { - self.sm.get_mut(entity.0) + pub fn iter_mut(&mut self) -> impl FusedIterator + '_ { + self.0 + .sm + .iter_mut() + .map(|(k, v)| (EntityId(k), EntityMut(v))) } - pub fn iter(&self) -> impl FusedIterator + Clone + '_ { - self.sm.iter().map(|(k, v)| (EntityId(k), v)) - } - - pub fn iter_mut(&mut self) -> impl FusedIterator + '_ { - self.sm.iter_mut().map(|(k, v)| (EntityId(k), v)) - } - - pub fn par_iter(&self) -> impl ParallelIterator + Clone + '_ { - self.sm.par_iter().map(|(k, v)| (EntityId(k), v)) - } - - pub fn par_iter_mut(&mut self) -> impl ParallelIterator + '_ { - self.sm.par_iter_mut().map(|(k, v)| (EntityId(k), v)) - } - - pub(crate) fn from_network_id(&self, network_id: i32) -> Option { - self.sm.key_at_index(network_id as usize).map(EntityId) - } - - fn partition_insert(&mut self, entity: EntityId, world: WorldId, aabb: Aabb) { - let min_corner = ChunkPos::from_xz(aabb.min().xz()); - let max_corner = ChunkPos::from_xz(aabb.max().xz()); - - for z in min_corner.z..=max_corner.z { - for x in min_corner.x..=max_corner.x { - self.partition_insert_at(entity, world, ChunkPos { x, z }) - } - } - } - - fn partition_insert_at(&mut self, entity: EntityId, world: WorldId, pos: ChunkPos) { - match self.partition.entry((world, pos)) { - Entry::Occupied(mut oe) => { - debug_assert!( - !oe.get_mut().contains(&entity), - "spatial partition: entity already present" - ); - oe.get_mut().push(entity); - } - Entry::Vacant(ve) => { - ve.insert(vec![entity]); - } - } - } - - fn partition_remove(&mut self, entity: EntityId, world: WorldId, aabb: Aabb) { - let min_corner = ChunkPos::from_xz(aabb.min().xz()); - let max_corner = ChunkPos::from_xz(aabb.max().xz()); - - for z in min_corner.z..=max_corner.z { - for x in min_corner.x..=max_corner.x { - self.partition_remove_at(entity, world, ChunkPos::new(x, z)); - } - } - } - - fn partition_remove_at(&mut self, entity: EntityId, world: WorldId, pos: ChunkPos) { - let errmsg = "spatial partition: entity removal failed"; - match self.partition.entry((world, pos)) { - Entry::Occupied(mut oe) => { - let v = oe.get_mut(); - let idx = v.iter().position(|e| *e == entity).expect(errmsg); - v.swap_remove(idx); - - if v.is_empty() { - oe.remove(); - } - } - Entry::Vacant(_) => panic!("{errmsg}"), - } - } - - fn partition_modify( - &mut self, - entity: EntityId, - old_world: WorldId, - old_aabb: Aabb, - new_world: WorldId, - new_aabb: Aabb, - ) { - if old_world != new_world { - self.partition_remove(entity, old_world, old_aabb); - self.partition_insert(entity, new_world, new_aabb); - } else { - let old_min_corner = ChunkPos::from_xz(old_aabb.min().xz()); - let old_max_corner = ChunkPos::from_xz(old_aabb.max().xz()); - let new_min_corner = ChunkPos::from_xz(new_aabb.min().xz()); - let new_max_corner = ChunkPos::from_xz(new_aabb.max().xz()); - - for z in new_min_corner.z..=new_max_corner.z { - for x in new_min_corner.x..=new_max_corner.x { - if x < old_min_corner.x - || x > old_max_corner.x - || z < old_min_corner.z - || z > old_max_corner.z - { - self.partition_insert_at(entity, old_world, ChunkPos::new(x, z)); - } - } - } - - for z in old_min_corner.z..=old_max_corner.z { - for x in old_min_corner.x..=old_max_corner.x { - if x < new_min_corner.x - || x > new_max_corner.x - || z < new_min_corner.z - || z > new_max_corner.z - { - self.partition_remove_at(entity, old_world, ChunkPos::new(x, z)) - } - } - } - } - } - - /// Returns an iterator over all entities with bounding volumes intersecting - /// the given AABB in an arbitrary order. - pub fn intersecting_aabb( - &self, - world: WorldId, - aabb: Aabb, - ) -> impl FusedIterator + '_ { - let min_corner = ChunkPos::from_xz(aabb.min().xz()); - let max_corner = ChunkPos::from_xz(aabb.max().xz()); - (min_corner.z..=max_corner.z).flat_map(move |z| { - (min_corner.x..=max_corner.x).flat_map(move |x| { - self.partition - .get(&(world, ChunkPos::new(x, z))) - .into_iter() - .flat_map(move |v| { - v.iter().cloned().filter(move |&e| { - self.get(e) - .expect("spatial partition contains deleted entity") - .hitbox() - .collides_with_aabb(&aabb) - }) - }) - }) - }) + pub fn par_iter_mut(&mut self) -> impl ParallelIterator + '_ { + self.0 + .sm + .par_iter_mut() + .map(|(k, v)| (EntityId(k), EntityMut(v))) } pub(crate) fn update(&mut self) { for (_, e) in self.iter_mut() { - e.old_position = e.new_position; - e.old_world = e.new_world; - - // TODO: update entity old_type. - // TODO: clear changed bits in metadata. + e.0.old_position = e.new_position; + e.0.meta.clear_modifications(); + e.0.flags = EntityFlags(0); } } } -//#[cfg(test)] -//mod tests { -// use appearance::Player; -// -// use super::*; -// use crate::glm; -// -// // TODO: better test: spawn a bunch of random entities, spawn a random -// AABB, // assert collides_with_aabb consistency. -// -// #[test] -// fn space_partition() { -// let mut entities = EntityStore::new(); -// -// let ids = [(16.0, 16.0, 16.0), (8.0, 8.0, 8.0), (10.0, 50.0, 10.0)] -// .into_iter() -// .map(|(x, y, z)| entities.create(Player::new(glm::vec3(x, y, z), -// WorldId::NULL))) .collect::>(); -// -// let outside = *ids.last().unwrap(); -// -// assert!(entities -// .intersecting_aabb( -// WorldId::NULL, -// Aabb::new(glm::vec3(8.0, 8.0, 8.0), glm::vec3(16.0, 16.0, -// 16.0)), ) -// .all(|id| ids.contains(&id) && id != outside)); -// } -//} +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct EntityId(Key); + +impl EntityId { + pub(crate) fn to_network_id(self) -> i32 { + // TODO: is ID 0 reserved? + self.0.index() as i32 + } +} + +pub struct Entity { + flags: EntityFlags, + meta: EntityMeta, + new_position: DVec3, + old_position: DVec3, + yaw: f32, + pitch: f32, + head_yaw: f32, + head_pitch: f32, + velocity: Vec3, + uuid: Uuid, +} + +pub struct EntityMut<'a>(&'a mut Entity); + +impl<'a> Deref for EntityMut<'a> { + type Target = Entity; + + fn deref(&self) -> &Self::Target { + self.0 + } +} + +/// Contains a bit for certain fields in [`Entity`] to track if they have been +/// 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, + #[bits(3)] + _pad: u8, +} + +impl Entity { + pub(crate) fn flags(&self) -> EntityFlags { + self.flags + } + + /// Returns a reference to this entity's [`EntityMeta`]. + pub fn meta(&self) -> &EntityMeta { + &self.meta + } + + /// Returns the [`EntityType`] of this entity. + pub fn typ(&self) -> EntityType { + self.meta.typ() + } + + /// Returns the position of this entity in the world it inhabits. + pub fn position(&self) -> DVec3 { + self.new_position + } + + /// Returns the position of this entity as it existed at the end of the + /// previous tick. + pub fn old_position(&self) -> DVec3 { + self.old_position + } + + /// Gets the yaw of this entity (in degrees). + pub fn yaw(&self) -> f32 { + self.yaw + } + + /// Gets the pitch of this entity (in degrees). + pub fn pitch(&self) -> f32 { + self.pitch + } + + /// Gets the head yaw of this entity (in degrees). + pub fn head_yaw(&self) -> f32 { + 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 + } + + /// Gets the metadata packet to send to clients after this entity has been + /// spawned. + /// + /// Is `None` if there is no initial metadata. + pub(crate) fn initial_metadata_packet(&self, this_id: EntityId) -> Option { + self.meta.initial_metadata().map(|meta| EntityMetadata { + entity_id: VarInt(this_id.to_network_id()), + metadata: ReadToEnd(meta), + }) + } + + /// Gets the metadata packet to send to clients when the entity is modified. + /// + /// Is `None` if this entity's metadata has not been modified. + pub(crate) fn updated_metadata_packet(&self, this_id: EntityId) -> Option { + self.meta.updated_metadata().map(|meta| EntityMetadata { + entity_id: VarInt(this_id.to_network_id()), + metadata: ReadToEnd(meta), + }) + } + + pub(crate) fn spawn_packet(&self, this_id: EntityId) -> Option { + use EntityMeta::*; + match &self.meta { + Marker(_) => None, + ExperienceOrb(_) => Some(EntitySpawnPacket::SpawnExperienceOrb(SpawnExperienceOrb { + entity_id: VarInt(this_id.to_network_id()), + position: self.new_position, + count: 0, // TODO + })), + Painting(_) => todo!(), + Player(_) => todo!(), + AreaEffectCloud(_) + | Arrow(_) + | Boat(_) + | DragonFireball(_) + | EndCrystal(_) + | EvokerFangs(_) + | EyeOfEnder(_) + | FallingBlock(_) + | FireworkRocket(_) + | GlowItemFrame(_) + | Item(_) + | ItemFrame(_) + | Fireball(_) + | LeashKnot(_) + | LightningBolt(_) + | LlamaSpit(_) + | Minecart(_) + | ChestMinecart(_) + | CommandBlockMinecart(_) + | FurnaceMinecart(_) + | HopperMinecart(_) + | SpawnerMinecart(_) + | TntMinecart(_) + | Tnt(_) + | ShulkerBullet(_) + | SmallFireball(_) + | Snowball(_) + | SpectralArrow(_) + | Egg(_) + | EnderPearl(_) + | ExperienceBottle(_) + | Potion(_) + | Trident(_) + | WitherSkull(_) + | FishingBobber(_) => Some(EntitySpawnPacket::SpawnEntity(SpawnEntity { + entity_id: VarInt(this_id.to_network_id()), + object_uuid: self.uuid, + typ: VarInt(self.typ() as i32), + position: self.new_position, + pitch: ByteAngle::from_degrees(self.pitch), + yaw: ByteAngle::from_degrees(self.yaw), + data: 1, // TODO + velocity: velocity_to_packet_units(self.velocity), + })), + + ArmorStand(_) | Axolotl(_) | Bat(_) | Bee(_) | Blaze(_) | Cat(_) | CaveSpider(_) + | Chicken(_) | Cod(_) | Cow(_) | Creeper(_) | Dolphin(_) | Donkey(_) | Drowned(_) + | ElderGuardian(_) | EnderDragon(_) | Enderman(_) | Endermite(_) | Evoker(_) + | Fox(_) | Ghast(_) | Giant(_) | GlowSquid(_) | Goat(_) | Guardian(_) | Hoglin(_) + | Horse(_) | Husk(_) | Illusioner(_) | IronGolem(_) | Llama(_) | MagmaCube(_) + | Mule(_) | Mooshroom(_) | Ocelot(_) | Panda(_) | Parrot(_) | Phantom(_) | Pig(_) + | Piglin(_) | PiglinBrute(_) | Pillager(_) | PolarBear(_) | Pufferfish(_) + | Rabbit(_) | Ravager(_) | Salmon(_) | Sheep(_) | Shulker(_) | Silverfish(_) + | Skeleton(_) | SkeletonHorse(_) | Slime(_) | SnowGolem(_) | Spider(_) | Squid(_) + | Stray(_) | Strider(_) | TraderLlama(_) | TropicalFish(_) | Turtle(_) | Vex(_) + | Villager(_) | Vindicator(_) | WanderingTrader(_) | Witch(_) | Wither(_) + | WitherSkeleton(_) | Wolf(_) | Zoglin(_) | Zombie(_) | ZombieHorse(_) + | ZombieVillager(_) | ZombifiedPiglin(_) => { + Some(EntitySpawnPacket::SpawnLivingEntity(SpawnLivingEntity { + entity_id: VarInt(this_id.to_network_id()), + entity_uuid: self.uuid, + typ: VarInt(self.typ() as i32), + position: self.new_position, + yaw: ByteAngle::from_degrees(self.yaw), + pitch: ByteAngle::from_degrees(self.pitch), + head_pitch: ByteAngle::from_degrees(self.head_pitch), + velocity: velocity_to_packet_units(self.velocity), + })) + } + } + } + + pub fn hitbox(&self) -> Aabb { + let dims = match &self.meta { + EntityMeta::AreaEffectCloud(e) => [ + e.get_radius() as f64 * 2.0, + 0.5, + e.get_radius() as f64 * 2.0, + ], + EntityMeta::ArmorStand(e) => { + if e.get_marker() { + [0.0, 0.0, 0.0] + } else if e.get_small() { + [0.5, 0.9875, 0.5] + } else { + [0.5, 1.975, 0.5] + } + } + EntityMeta::Arrow(_) => [0.5, 0.5, 0.5], + EntityMeta::Axolotl(_) => [1.3, 0.6, 1.3], + EntityMeta::Bat(_) => [0.5, 0.9, 0.5], + EntityMeta::Bee(_) => [0.7, 0.6, 0.7], // TODO: baby size? + EntityMeta::Blaze(_) => [0.6, 1.8, 0.6], + EntityMeta::Boat(_) => [1.375, 0.5625, 1.375], + EntityMeta::Cat(_) => [0.6, 0.7, 0.6], + EntityMeta::CaveSpider(_) => [0.7, 0.5, 0.7], + EntityMeta::Chicken(_) => [0.4, 0.7, 0.4], // TODO: baby size? + EntityMeta::Cod(_) => [0.5, 0.3, 0.5], + EntityMeta::Cow(_) => [0.9, 1.4, 0.9], // TODO: baby size? + EntityMeta::Creeper(_) => [0.6, 1.7, 0.6], + EntityMeta::Dolphin(_) => [0.9, 0.6, 0.9], + EntityMeta::Donkey(_) => [1.5, 1.39648, 1.5], // TODO: baby size? + EntityMeta::DragonFireball(_) => [1.0, 1.0, 1.0], + EntityMeta::Drowned(_) => [0.6, 1.95, 0.6], // TODO: baby size? + EntityMeta::ElderGuardian(_) => [1.9975, 1.9975, 1.9975], + EntityMeta::EndCrystal(_) => [2.0, 2.0, 2.0], + EntityMeta::EnderDragon(_) => [16.0, 8.0, 16.0], + EntityMeta::Enderman(_) => [0.6, 2.9, 0.6], + EntityMeta::Endermite(_) => [0.4, 0.3, 0.4], + EntityMeta::Evoker(_) => [0.6, 1.95, 0.6], + EntityMeta::EvokerFangs(_) => [0.5, 0.8, 0.5], + EntityMeta::ExperienceOrb(_) => [0.5, 0.5, 0.5], + EntityMeta::EyeOfEnder(_) => [0.25, 0.25, 0.25], + EntityMeta::FallingBlock(_) => [0.98, 0.98, 0.98], + EntityMeta::FireworkRocket(_) => [0.25, 0.25, 0.25], + EntityMeta::Fox(_) => [0.6, 0.7, 0.6], // TODO: baby size? + EntityMeta::Ghast(_) => [4.0, 4.0, 4.0], + EntityMeta::Giant(_) => [3.6, 12.0, 3.6], + EntityMeta::GlowItemFrame(_) => todo!("account for rotation"), + EntityMeta::GlowSquid(_) => [0.8, 0.8, 0.8], + EntityMeta::Goat(e) => [1.3, 0.9, 1.3], // TODO: baby size? + EntityMeta::Guardian(_) => [0.85, 0.85, 0.85], + EntityMeta::Hoglin(_) => [1.39648, 1.4, 1.39648], // TODO: baby size? + EntityMeta::Horse(_) => [1.39648, 1.6, 1.39648], // TODO: baby size? + EntityMeta::Husk(_) => [0.6, 1.95, 0.6], // TODO: baby size? + EntityMeta::Illusioner(_) => [0.6, 1.95, 0.6], + EntityMeta::IronGolem(_) => [1.4, 2.7, 1.4], + EntityMeta::Item(_) => [0.25, 0.25, 0.25], + EntityMeta::ItemFrame(_) => todo!("account for rotation"), + EntityMeta::Fireball(_) => [1.0, 1.0, 1.0], + EntityMeta::LeashKnot(_) => [0.375, 0.5, 0.375], + EntityMeta::LightningBolt(_) => [0.0, 0.0, 0.0], + EntityMeta::Llama(_) => [0.9, 1.87, 0.9], // TODO: baby size? + EntityMeta::LlamaSpit(_) => [0.25, 0.25, 0.25], + EntityMeta::MagmaCube(e) => { + let s = e.get_size() as f64 * 0.51000005; + [s, s, s] + } + EntityMeta::Marker(_) => [0.0, 0.0, 0.0], + EntityMeta::Minecart(_) => [0.98, 0.7, 0.98], + EntityMeta::ChestMinecart(_) => [0.98, 0.7, 0.98], + EntityMeta::CommandBlockMinecart(_) => [0.98, 0.7, 0.98], + EntityMeta::FurnaceMinecart(_) => [0.98, 0.7, 0.98], + EntityMeta::HopperMinecart(_) => [0.98, 0.7, 0.98], + EntityMeta::SpawnerMinecart(_) => [0.98, 0.7, 0.98], + EntityMeta::TntMinecart(_) => [0.98, 0.7, 0.98], + EntityMeta::Mule(_) => [1.39648, 1.6, 1.39648], // TODO: baby size? + EntityMeta::Mooshroom(_) => [0.9, 1.4, 0.9], // TODO: baby size? + EntityMeta::Ocelot(_) => [0.6, 0.7, 0.6], // TODO: baby size? + EntityMeta::Painting(_) => todo!("account for rotation and type"), + EntityMeta::Panda(_) => [0.6, 0.7, 0.6], // TODO: baby size? + EntityMeta::Parrot(_) => [0.5, 0.9, 0.5], + EntityMeta::Phantom(_) => [0.9, 0.5, 0.9], + EntityMeta::Pig(_) => [0.9, 0.9, 0.9], // TODO: baby size? + EntityMeta::Piglin(_) => [0.6, 1.95, 0.6], // TODO: baby size? + EntityMeta::PiglinBrute(_) => [0.6, 1.95, 0.6], + EntityMeta::Pillager(_) => [0.6, 1.95, 0.6], + EntityMeta::PolarBear(_) => [1.4, 1.4, 1.4], // TODO: baby size? + EntityMeta::Tnt(_) => [0.98, 0.98, 0.98], + EntityMeta::Pufferfish(_) => [0.7, 0.7, 0.7], + EntityMeta::Rabbit(_) => [0.4, 0.5, 0.4], // TODO: baby size? + EntityMeta::Ravager(_) => [1.95, 2.2, 1.95], + EntityMeta::Salmon(_) => [0.7, 0.4, 0.7], + EntityMeta::Sheep(_) => [0.9, 1.3, 0.9], // TODO: baby size? + EntityMeta::Shulker(_) => [1.0, 1.0, 1.0], // TODO: how is height calculated? + EntityMeta::ShulkerBullet(_) => [0.3125, 0.3125, 0.3125], + EntityMeta::Silverfish(_) => [0.4, 0.3, 0.4], + EntityMeta::Skeleton(_) => [0.6, 1.99, 0.6], + EntityMeta::SkeletonHorse(_) => [1.39648, 1.6, 1.39648], // TODO: baby size? + EntityMeta::Slime(e) => { + let s = 0.51000005 * e.get_size() as f64; + [s, s, s] + } + EntityMeta::SmallFireball(_) => [0.3125, 0.3125, 0.3125], + EntityMeta::SnowGolem(_) => [0.7, 1.9, 0.7], + EntityMeta::Snowball(_) => [0.25, 0.25, 0.25], + EntityMeta::SpectralArrow(_) => [0.5, 0.5, 0.5], + EntityMeta::Spider(_) => [1.4, 0.9, 1.4], + EntityMeta::Squid(_) => [0.8, 0.8, 0.8], + EntityMeta::Stray(_) => [0.6, 1.99, 0.6], + EntityMeta::Strider(_) => [0.9, 1.7, 0.9], // TODO: baby size? + EntityMeta::Egg(_) => [0.25, 0.25, 0.25], + EntityMeta::EnderPearl(_) => [0.25, 0.25, 0.25], + EntityMeta::ExperienceBottle(_) => [0.25, 0.25, 0.25], + EntityMeta::Potion(_) => [0.25, 0.25, 0.25], + EntityMeta::Trident(_) => [0.5, 0.5, 0.5], + EntityMeta::TraderLlama(_) => [0.9, 1.87, 0.9], + EntityMeta::TropicalFish(_) => [0.5, 0.4, 0.5], + EntityMeta::Turtle(_) => [1.2, 0.4, 1.2], // TODO: baby size? + EntityMeta::Vex(_) => [0.4, 0.8, 0.4], + EntityMeta::Villager(_) => [0.6, 1.95, 0.6], // TODO: baby size? + EntityMeta::Vindicator(_) => [0.6, 1.95, 0.6], + EntityMeta::WanderingTrader(_) => [0.6, 1.95, 0.6], + EntityMeta::Witch(_) => [0.6, 1.95, 0.6], + EntityMeta::Wither(_) => [0.9, 3.5, 0.9], + EntityMeta::WitherSkeleton(_) => [0.7, 2.4, 0.7], + EntityMeta::WitherSkull(_) => [0.3125, 0.3125, 0.3125], + EntityMeta::Wolf(_) => [0.6, 0.85, 0.6], // TODO: baby size? + EntityMeta::Zoglin(_) => [1.39648, 1.4, 1.39648], // TODO: baby size? + EntityMeta::Zombie(_) => [0.6, 1.95, 0.6], // TODO: baby size? + EntityMeta::ZombieHorse(_) => [1.39648, 1.6, 1.39648], // TODO: baby size? + EntityMeta::ZombieVillager(_) => [0.6, 1.95, 0.6], // TODO: baby size? + EntityMeta::ZombifiedPiglin(_) => [0.6, 1.95, 0.6], // TODO: baby size? + EntityMeta::Player(_) => [0.6, 1.8, 0.6], // TODO: changes depending on the pose. + EntityMeta::FishingBobber(_) => [0.25, 0.25, 0.25], + }; + + Aabb::from_bottom_and_dimensions(self.new_position, dims) + } +} + +fn velocity_to_packet_units(vel: Vec3) -> I16Vec3 { + // The saturating cast to i16 is desirable. + vel.map(|v| (v * 400.0) as i16) +} + +impl<'a> EntityMut<'a> { + // TODO: exposing &mut EntityMeta is unsound? + /// Returns a mutable reference to this entity's [`EntityMeta`]. + /// + /// **NOTE:** Never call [`std::mem::swap`] on the returned reference or any + /// part of `EntityMeta` as this would break invariants within the + /// library. + pub fn meta_mut(&mut self) -> &mut EntityMeta { + &mut self.0.meta + } + + /// Changes the [`EntityType`] of this entity to the provided type. + /// + /// 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); + } + + /// Sets the position of this entity in the world it inhabits. + pub fn set_position(&mut self, pos: impl Into) { + self.0.new_position = pos.into(); + } + + /// 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) { + 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) { + 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) { + 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: Vec3) { + self.0.velocity = velocity; + if velocity_to_packet_units(self.velocity) != velocity_to_packet_units(velocity) { + self.0.flags.set_velocity_modified(true); + } + } +} + +pub(crate) enum EntitySpawnPacket { + SpawnEntity(SpawnEntity), + SpawnExperienceOrb(SpawnExperienceOrb), + SpawnLivingEntity(SpawnLivingEntity), + SpawnPainting(SpawnPainting), + SpawnPlayer(SpawnPlayer), +} + +impl From for ClientPlayPacket { + fn from(pkt: EntitySpawnPacket) -> Self { + match pkt { + EntitySpawnPacket::SpawnEntity(pkt) => pkt.into(), + EntitySpawnPacket::SpawnExperienceOrb(pkt) => pkt.into(), + EntitySpawnPacket::SpawnLivingEntity(pkt) => pkt.into(), + EntitySpawnPacket::SpawnPainting(pkt) => pkt.into(), + EntitySpawnPacket::SpawnPlayer(pkt) => pkt.into(), + } + } +} + +pub use types::{EntityMeta, EntityType}; diff --git a/src/entity/meta.rs b/src/entity/meta.rs index f5f6463..c8724a8 100644 --- a/src/entity/meta.rs +++ b/src/entity/meta.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use std::io::Write; use anyhow::Context; @@ -10,26 +12,25 @@ use crate::Text; #[derive(Clone, Copy, Default, PartialEq, PartialOrd, Debug)] pub struct ArmorStandRotations { - pub x_degrees: f32, - pub y_degrees: f32, - pub z_degrees: f32, + /// 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_degrees: f32, y_degrees: f32, z_degrees: f32) -> Self { - Self { - x_degrees, - y_degrees, - z_degrees, - } + 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_degrees.encode(w)?; - self.y_degrees.encode(w)?; - self.z_degrees.encode(w) + self.x.encode(w)?; + self.y.encode(w)?; + self.z.encode(w) } } @@ -126,22 +127,6 @@ impl Default for VillagerProfession { } } -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] -struct OptVarInt(Option); - -impl Encode for OptVarInt { - fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { - match self.0 { - Some(n) => VarInt( - n.checked_add(1) - .context("i32::MAX is unrepresentable as an optional VarInt")?, - ) - .encode(w), - None => VarInt(0).encode(w), - } - } -} - #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub enum Pose { Standing, @@ -166,6 +151,7 @@ impl Encode for Pose { } } +/// The main hand of a player. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub enum MainHand { Left, diff --git a/src/entity/types.rs b/src/entity/types.rs index f29989b..c04d9e3 100644 --- a/src/entity/types.rs +++ b/src/entity/types.rs @@ -1,5 +1,9 @@ +#![allow(clippy::all, missing_docs)] + use crate::block::BlockState; use crate::entity::meta::*; +use crate::protocol::Encode; +use crate::var_int::VarInt; use crate::{BlockPos, EntityId, Text, Uuid}; include!(concat!(env!("OUT_DIR"), "/entity.rs")); diff --git a/src/lib.rs b/src/lib.rs index 20f01ac..ec317b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ trivial_numeric_casts, unused_lifetimes, unused_import_braces, - missing_docs + // missing_docs )] mod aabb; @@ -30,15 +30,15 @@ pub mod world; pub use aabb::Aabb; pub use async_trait::async_trait; pub use block_pos::BlockPos; -pub use chunk::{Chunk, ChunkPos, ChunkStore}; -pub use client::{Client, ClientStore}; +pub use chunk::{Chunk, ChunkPos, Chunks, ChunksMut}; +pub use client::{Client, ClientMut, Clients, ClientsMut}; pub use config::{Biome, BiomeId, Config, Dimension, DimensionId}; -pub use entity::{Entity, EntityId, EntityStore}; +pub use entity::{Entities, EntitiesMut, Entity, EntityId}; pub use identifier::Identifier; -pub use server::{start_server, NewClientData, Server, SharedServer, ShutdownResult}; +pub use server::{start_server, NewClientData, Server, ShutdownResult}; pub use text::{Text, TextFormat}; pub use uuid::Uuid; -pub use world::{World, WorldId, WorldStore}; +pub use world::{WorldId, WorldMut, WorldRef, Worlds, WorldsMut}; pub use {nalgebra_glm as glm, nbt, uuid}; /// The Minecraft protocol version that this library targets. @@ -55,18 +55,3 @@ 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; - -/// Types such as [`EntityId`], [`WorldId`], and [`ChunkId`] which can be used -/// as indices into an array. -/// -/// Every ID is either valid or invalid. Valid IDs point to living values. For -/// instance, a valid [`EntityId`] points to a living entity on the server. When -/// that entity is deleted, the corresponding [`EntityId`] becomes invalid. -pub trait Id: Copy + Send + Sync + PartialEq + Eq { - /// Returns the index of this ID. - /// - /// For all IDs `a` and `b`, `a == b` implies `a.idx() == b.idx()`. If - /// both `a` and `b` are currently valid, then `a != b` implies `a.idx() != - /// b.idx()`. - fn idx(self) -> usize; -} diff --git a/src/packets.rs b/src/packets.rs index 6a27c89..d552ced 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -16,8 +16,9 @@ use uuid::Uuid; use crate::block_pos::BlockPos; use crate::byte_angle::ByteAngle; +use crate::glm::{DVec3, I16Vec3, Vec3}; use crate::identifier::Identifier; -use crate::protocol::{BoundedArray, BoundedString, Decode, Encode, Nbt, ReadToEnd}; +use crate::protocol::{BoundedArray, BoundedInt, BoundedString, Decode, Encode, Nbt, ReadToEnd}; use crate::var_int::VarInt; use crate::var_long::VarLong; use crate::Text; @@ -311,13 +312,13 @@ macro_rules! def_bitfield { } } - impl Encode for $name { + impl $crate::protocol::Encode for $name { fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { self.0.encode(w) } } - impl Decode for $name { + impl $crate::protocol::Decode for $name { fn decode(r: &mut impl Read) -> anyhow::Result { <$inner_ty>::decode(r).map(Self) } @@ -436,7 +437,6 @@ pub mod login { /// Packets and types used during the play state. pub mod play { use super::*; - use crate::protocol::BoundedInt; // ==== Clientbound ==== @@ -444,26 +444,19 @@ pub mod play { SpawnEntity 0x00 { entity_id: VarInt, object_uuid: Uuid, - typ: VarInt, // TODO: entity type enum - x: f64, - y: f64, - z: f64, + typ: VarInt, + position: DVec3, pitch: ByteAngle, yaw: ByteAngle, data: i32, - // TODO: entity velocity unit? - velocity_x: i16, - velocity_y: i16, - velocity_z: i16, + velocity: I16Vec3, } } def_struct! { SpawnExperienceOrb 0x01 { entity_id: VarInt, - x: f64, - y: f64, - z: f64, + position: DVec3, count: i16, } } @@ -472,17 +465,12 @@ pub mod play { SpawnLivingEntity 0x02 { entity_id: VarInt, entity_uuid: Uuid, - typ: VarInt, // TODO: entity type enum - x: f64, - y: f64, - z: f64, + typ: VarInt, + position: DVec3, yaw: ByteAngle, pitch: ByteAngle, head_pitch: ByteAngle, - // TODO: entity velocity unit? - velocity_x: i16, - velocity_y: i16, - velocity_z: i16, + velocity: I16Vec3, } } @@ -490,7 +478,7 @@ pub mod play { SpawnPainting 0x03 { entity_id: VarInt, entity_uuid: Uuid, - motive: VarInt, // TODO: painting ID enum + variant: VarInt, // TODO: painting ID enum location: BlockPos, direction: PaintingDirection, } @@ -509,9 +497,7 @@ pub mod play { SpawnPlayer 0x04 { entity_id: VarInt, player_uuid: Uuid, - x: f64, - y: f64, - z: f64, + position: DVec3, yaw: ByteAngle, pitch: ByteAngle, } @@ -980,9 +966,7 @@ pub mod play { def_struct! { PlayerPositionAndLook 0x38 { - x: f64, - y: f64, - z: f64, + position: DVec3, yaw: f32, pitch: f32, flags: PlayerPositionAndLookFlags, @@ -1001,6 +985,12 @@ pub mod play { } } + def_struct! { + DestroyEntities 0x3a { + entities: Vec, + } + } + def_struct! { MultiBlockChange 0x3f { chunk_section_position: u64, @@ -1035,6 +1025,13 @@ pub mod play { } } + def_struct! { + EntityMetadata 0x4d { + entity_id: VarInt, + metadata: ReadToEnd, + } + } + def_struct! { TimeUpdate 0x59 { /// The age of the world in 1/20ths of a second. @@ -1119,11 +1116,13 @@ pub mod play { ChunkDataAndUpdateLight, JoinGame, PlayerPositionAndLook, + DestroyEntities, MultiBlockChange, HeldItemChangeClientbound, UpdateViewPosition, UpdateViewDistance, SpawnPosition, + EntityMetadata, TimeUpdate, } @@ -1291,9 +1290,7 @@ pub mod play { def_struct! { InteractAtData { - target_x: f32, - target_y: f32, - target_z: f32, + target: Vec3, hand: Hand, } } @@ -1320,25 +1317,15 @@ pub mod play { def_struct! { PlayerPosition 0x11 { - /// Absolute position - x: f64, - /// Y position of the player's feet, normally 1.62 blocks below head. - feet_y: f64, - /// Absolute position - z: f64, - /// True if the client is on the ground, false otherwise. + position: DVec3, on_ground: bool, } } def_struct! { PlayerPositionAndRotation 0x12 { - /// Absolute position - x: f64, - /// Y position of the player's feet, normally 1.62 blocks below head. - feet_y: f64, - /// Absolute position - z: f64, + // Absolute position + position: DVec3, /// Absolute rotation on X axis in degrees. yaw: f32, /// Absolute rotation on Y axis in degrees. @@ -1366,11 +1353,7 @@ pub mod play { def_struct! { VehicleMoveServerbound 0x15 { /// Absolute position - x: f64, - /// Absolute position - y: f64, - /// Absolute position - z: f64, + position: DVec3, /// Degrees yaw: f32, /// Degrees @@ -1608,12 +1591,8 @@ pub mod play { action: StructureBlockAction, mode: StructureBlockMode, name: String, - offset_x: BoundedInt, - offset_y: BoundedInt, - offset_z: BoundedInt, - size_x: BoundedInt, - size_y: BoundedInt, - size_z: BoundedInt, + offset_xyz: [BoundedInt; 3], + size_xyz: [BoundedInt; 3], mirror: StructureBlockMirror, rotation: StructureBlockRotation, metadata: String, @@ -1669,10 +1648,7 @@ pub mod play { def_struct! { UpdateSign 0x2b { location: BlockPos, - line_1: BoundedString<0, 384>, - line_2: BoundedString<0, 384>, - line_3: BoundedString<0, 384>, - line_4: BoundedString<0, 384>, + lines: [BoundedString<0, 384>; 4], } } @@ -1693,9 +1669,7 @@ pub mod play { hand: Hand, location: BlockPos, face: BlockFace, - cursor_pos_x: f32, - cursor_pos_y: f32, - cursor_pos_z: f32, + cursor_pos: Vec3, head_inside_block: bool, } } diff --git a/src/protocol.rs b/src/protocol.rs index 5a1e71a..20d43ee 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -1,13 +1,16 @@ use std::io::{Read, Write}; use std::mem; -use anyhow::{anyhow, ensure}; +use anyhow::{anyhow, ensure, Context}; +use arrayvec::ArrayVec; use bitvec::prelude::*; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use nalgebra_glm::{Number, TVec}; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::var_int::VarInt; +use crate::EntityId; /// Trait for types that can be written to the Minecraft protocol. pub trait Encode { @@ -220,6 +223,30 @@ impl Decode for Option { } } +impl Encode for Box { + fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { + self.as_ref().encode(w) + } +} + +impl Decode for Box { + fn decode(r: &mut impl Read) -> anyhow::Result { + Ok(Box::new(T::decode(r)?)) + } +} + +impl Encode for Box { + fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { + encode_string_bounded(self, 0, 32767, w) + } +} + +impl Decode for Box { + fn decode(r: &mut impl Read) -> anyhow::Result { + Ok(String::decode(r)?.into_boxed_str()) + } +} + #[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct BoundedInt(pub T); @@ -335,11 +362,25 @@ impl Encode for [T; N] { impl Decode for [T; N] { fn decode(r: &mut impl Read) -> anyhow::Result { - let vec = decode_array_bounded(N, N, r)?; - match vec.try_into() { - Ok(arr) => Ok(arr), - Err(_) => unreachable!("array size does not match"), + let mut elems = ArrayVec::new(); + for _ in 0..N { + elems.push(T::decode(r)?); } + elems + .into_inner() + .map_err(|_| unreachable!("mismatched array size")) + } +} + +impl Encode for TVec { + fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { + encode_array_bounded(self.as_slice(), N, N, w) + } +} + +impl Decode for TVec { + fn decode(r: &mut impl Read) -> anyhow::Result { + Ok(<[T; N]>::decode(r)?.into()) } } @@ -459,6 +500,20 @@ impl Encode for ReadToEnd { } } +impl Encode for Option { + fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { + match self { + Some(id) => VarInt( + id.to_network_id() + .checked_add(1) + .context("i32::MAX is unrepresentable as an optional VarInt")?, + ), + None => VarInt(0), + } + .encode(w) + } +} + fn encode_array_bounded( s: &[T], min: usize, diff --git a/src/server.rs b/src/server.rs index b90b3fb..d92b6d1 100644 --- a/src/server.rs +++ b/src/server.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; use std::error::Error; use std::iter::FusedIterator; use std::net::SocketAddr; -use std::ops::{Deref, DerefMut}; +use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::Arc; use std::thread; use std::time::{Duration, Instant}; @@ -27,7 +27,7 @@ use tokio::sync::{oneshot, Semaphore}; use uuid::Uuid; use crate::codec::{Decoder, Encoder}; -use crate::config::{Biome, BiomeId, Config, Dimension, DimensionId, Login, ServerListPing}; +use crate::config::{Biome, BiomeId, Config, Dimension, DimensionId, ServerListPing}; use crate::packets::handshake::{Handshake, HandshakeNextState}; use crate::packets::login::{ self, EncryptionRequest, EncryptionResponse, LoginStart, LoginSuccess, SetCompression, @@ -37,45 +37,16 @@ use crate::packets::status::{Ping, Pong, Request, Response}; use crate::protocol::{BoundedArray, BoundedString}; use crate::util::valid_username; use crate::var_int::VarInt; -use crate::{ - ChunkStore, Client, ClientStore, EntityStore, Ticks, WorldStore, PROTOCOL_VERSION, VERSION_NAME, -}; +use crate::world::Worlds; +use crate::{Client, ClientMut, Ticks, WorldsMut, PROTOCOL_VERSION, VERSION_NAME}; -/// Holds the state of a running Minecraft server which is accessible inside the -/// update loop. To start a server, see [`ServerConfig`]. -/// -/// Fields of this struct are made public to enable disjoint borrows. For -/// instance, it is possible to mutate the list of entities while simultaneously -/// reading world data. -/// -/// Note the `Deref` and `DerefMut` impls on this struct are (ab)used to -/// allow convenient access to the `other` field. -#[non_exhaustive] -pub struct Server { - pub entities: EntityStore, - pub clients: ClientStore, - pub worlds: WorldStore, - pub chunks: ChunkStore, - pub other: Other, -} - -pub struct Other { - /// The shared portion of the server. - shared: SharedServer, - new_players_rx: Receiver, - /// Incremented on every game tick. - tick_counter: Ticks, - /// The instant the current game tick began. - tick_start: Instant, -} - -/// A server handle providing the subset of functionality which can be performed -/// outside the update loop. `SharedServer`s are interally refcounted and can be -/// freely cloned and shared between threads. +/// A handle to a running Minecraft server containing state which is accessible +/// outside the update loop. Servers are internally refcounted and can be shared +/// between threads. #[derive(Clone)] -pub struct SharedServer(Arc); +pub struct Server(Arc); -struct SharedServerInner { +struct ServerInner { cfg: Box, address: SocketAddr, tick_rate: Ticks, @@ -90,6 +61,11 @@ struct SharedServerInner { biomes: Vec, /// The instant the server was started. start_instant: Instant, + /// Receiver for new clients past the login stage. + new_clients_rx: Receiver, + new_clients_tx: Sender, + /// Incremented on every game tick. + tick_counter: AtomicI64, /// A semaphore used to limit the number of simultaneous connections to the /// server. Closing this semaphore stops new connections. connection_sema: Arc, @@ -102,7 +78,6 @@ struct SharedServerInner { public_key_der: Box<[u8]>, /// For session server requests. http_client: HttpClient, - new_clients_tx: Sender, } /// Contains information about a new client. @@ -114,7 +89,7 @@ pub struct NewClientData { struct NewClientMessage { ncd: NewClientData, - reply: oneshot::Sender>, + reply: oneshot::Sender, } /// The result type returned from [`ServerConfig::start`] after the server is @@ -125,24 +100,7 @@ pub type ShutdownError = Box; pub(crate) type ClientPacketChannels = (Sender, Receiver); pub(crate) type ServerPacketChannels = (Sender, Receiver); -impl Other { - /// Returns a reference to a [`SharedServer`]. - pub fn shared(&self) -> &SharedServer { - &self.shared - } - - /// Returns the number of ticks that have elapsed since the server began. - pub fn current_tick(&self) -> Ticks { - self.tick_counter - } - - /// Returns the instant the current tick began. - pub fn tick_start(&self) -> Instant { - self.tick_start - } -} - -impl SharedServer { +impl Server { pub fn config(&self) -> &(impl Config + ?Sized) { self.0.cfg.as_ref() } @@ -203,8 +161,11 @@ impl SharedServer { } /// Returns an iterator over all added biomes and their associated - /// [`BiomeId`]. - pub fn biomes(&self) -> impl FusedIterator + Clone { + /// [`BiomeId`] in ascending order. + pub fn biomes( + &self, + ) -> impl ExactSizeIterator + DoubleEndedIterator + FusedIterator + Clone + { self.0 .biomes .iter() @@ -217,6 +178,11 @@ impl SharedServer { self.0.start_instant } + /// Returns the number of ticks that have elapsed since the server began. + pub fn current_tick(&self) -> Ticks { + self.0.tick_counter.load(Ordering::SeqCst) + } + /// Immediately stops new connections to the server and initiates server /// shutdown. The given result is returned through [`ServerConfig::start`]. /// @@ -232,44 +198,23 @@ impl SharedServer { } } -impl Deref for Server { - type Target = Other; - - fn deref(&self) -> &Self::Target { - &self.other - } -} - -impl DerefMut for Server { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.other - } -} - -impl Deref for Other { - type Target = SharedServer; - - fn deref(&self) -> &Self::Target { - &self.shared - } -} - /// Consumes the configuration and starts the server. /// /// The function returns when the server has shut down, a runtime error /// occurs, or the configuration is invalid. pub fn start_server(config: impl Config) -> ShutdownResult { - let mut server = setup_server(config).map_err(ShutdownError::from)?; + let server = setup_server(config).map_err(ShutdownError::from)?; - let shared = server.shared().clone(); + let _guard = server.tokio_handle().enter(); - let _guard = shared.tokio_handle().enter(); + let mut worlds = Worlds::new(server.clone()); + let mut worlds_mut = WorldsMut::new(&mut worlds); - shared.config().init(&mut server); + server.config().init(&server, worlds_mut.reborrow()); - tokio::spawn(do_accept_loop(shared)); + tokio::spawn(do_accept_loop(server.clone())); - do_update_loop(&mut server) + do_update_loop(server, worlds_mut) } fn setup_server(cfg: impl Config) -> anyhow::Result { @@ -359,7 +304,7 @@ fn setup_server(cfg: impl Config) -> anyhow::Result { rsa_der::public_key_to_der(&rsa_key.n().to_bytes_be(), &rsa_key.e().to_bytes_be()) .into_boxed_slice(); - let (new_players_tx, new_players_rx) = flume::bounded(1); + let (new_clients_tx, new_clients_rx) = flume::bounded(1); let runtime = if tokio_handle.is_none() { Some(Runtime::new()?) @@ -372,123 +317,125 @@ fn setup_server(cfg: impl Config) -> anyhow::Result { None => tokio_handle.unwrap(), }; - let shared = SharedServer(Arc::new(SharedServerInner { + let server = ServerInner { cfg: Box::new(cfg), address, tick_rate, online_mode, max_connections, - outgoing_packet_capacity, incoming_packet_capacity, + outgoing_packet_capacity, tokio_handle, _tokio_runtime: runtime, dimensions, biomes, start_instant: Instant::now(), + new_clients_rx, + new_clients_tx, + tick_counter: AtomicI64::new(0), connection_sema: Arc::new(Semaphore::new(max_connections)), shutdown_result: Mutex::new(None), rsa_key, public_key_der, http_client: HttpClient::new(), - new_clients_tx: new_players_tx, - })); + }; - Ok(Server { - entities: EntityStore::new(), - clients: ClientStore::new(), - worlds: WorldStore::new(), - chunks: ChunkStore::new(), - other: Other { - shared: shared.clone(), - tick_counter: 0, - tick_start: Instant::now(), - new_players_rx, - }, - }) + Ok(Server(Arc::new(server))) } -fn do_update_loop(server: &mut Server) -> ShutdownResult { - server.tick_start = Instant::now(); - let shared = server.shared().clone(); +fn do_update_loop(server: Server, mut worlds: WorldsMut) -> ShutdownResult { + let mut tick_start = Instant::now(); loop { if let Some(res) = server.0.shutdown_result.lock().take() { return res; } - while let Ok(msg) = server.new_players_rx.try_recv() { - join_player(server, msg); + while let Ok(msg) = server.0.new_clients_rx.try_recv() { + join_player(&server, worlds.reborrow(), msg); } - server.clients.par_iter_mut().for_each(|(_, client)| { - client.update( - &server.entities, - &server.worlds, - &server.chunks, - &server.other, - ) - }); + server.config().update(&server, worlds.reborrow()); - server.entities.update(); + worlds.par_iter_mut().for_each(|(_, mut world)| { + world.chunks.par_iter_mut().for_each(|(_, mut chunk)| { + if chunk.created_tick() == server.current_tick() { + // Chunks created this tick can have their changes applied immediately because + // they have not been observed by clients yet. Clients will not have to be sent + // the block change packet in this case. + chunk.apply_modifications(); + } + }); - server - .chunks - .par_iter_mut() - .for_each(|(_, chunk)| chunk.apply_modifications()); + world.clients.par_iter_mut().for_each(|(_, mut client)| { + client.update(&server, &world.entities, &world.chunks, world.dimension); + }); - shared.config().update(server); + world.entities.update(); - // Chunks created this tick can have their changes applied immediately because - // they have not been observed by clients yet. - server.chunks.par_iter_mut().for_each(|(_, chunk)| { - if chunk.created_this_tick() { - chunk.clear_created_this_tick(); + world.chunks.par_iter_mut().for_each(|(_, mut chunk)| { chunk.apply_modifications(); - } + }); }); // Sleep for the remainder of the tick. let tick_duration = Duration::from_secs_f64((server.0.tick_rate as f64).recip()); - thread::sleep(tick_duration.saturating_sub(server.tick_start.elapsed())); + thread::sleep(tick_duration.saturating_sub(tick_start.elapsed())); - server.tick_start = Instant::now(); - server.tick_counter += 1; + tick_start = Instant::now(); + server.0.tick_counter.fetch_add(1, Ordering::SeqCst); } } -fn join_player(server: &mut Server, msg: NewClientMessage) { +fn join_player(server: &Server, mut worlds: WorldsMut, msg: NewClientMessage) { let (clientbound_tx, clientbound_rx) = flume::bounded(server.0.outgoing_packet_capacity); let (serverbound_tx, serverbound_rx) = flume::bounded(server.0.incoming_packet_capacity); let client_packet_channels: ClientPacketChannels = (serverbound_tx, clientbound_rx); let server_packet_channels: ServerPacketChannels = (clientbound_tx, serverbound_rx); - let _ = msg.reply.send(Ok(client_packet_channels)); + let _ = msg.reply.send(client_packet_channels); - let client_backed_entity = match server.entities.create_with_uuid(msg.ncd.uuid) { - Some(id) => id, - None => { - log::warn!( - "player '{}' cannot join the server because their UUID ({}) conflicts with an \ - existing entity", - msg.ncd.username, - msg.ncd.uuid - ); - return; - } - }; - - server.clients.create(Client::new( + let mut client = Client::new( server_packet_channels, - client_backed_entity, msg.ncd.username, + msg.ncd.uuid, server, - )); + ); + let mut client_mut = ClientMut::new(&mut client); + + match server + .0 + .cfg + .join(server, client_mut.reborrow(), worlds.reborrow()) + { + Ok(world_id) => { + if let Some(mut world) = worlds.get_mut(world_id) { + if world.entities.get_with_uuid(client.uuid()).is_none() { + world.clients.create(client); + } else { + log::warn!( + "client '{}' cannot join the server because their UUID ({}) conflicts \ + with an existing entity", + client.username(), + client.uuid() + ); + } + } else { + log::warn!( + "client '{}' cannot join the server because the WorldId returned by \ + Config::join is invalid.", + client.username() + ); + } + } + Err(errmsg) => client_mut.disconnect(errmsg), + } } type Codec = (Encoder, Decoder); -async fn do_accept_loop(server: SharedServer) { +async fn do_accept_loop(server: Server) { log::trace!("entering accept loop"); let listener = match TcpListener::bind(server.0.address).await { @@ -528,7 +475,7 @@ async fn do_accept_loop(server: SharedServer) { } async fn handle_connection( - server: SharedServer, + server: Server, stream: TcpStream, remote_addr: SocketAddr, ) -> anyhow::Result<()> { @@ -556,7 +503,7 @@ async fn handle_connection( } async fn handle_status( - server: SharedServer, + server: Server, c: &mut Codec, remote_addr: SocketAddr, ) -> anyhow::Result<()> { @@ -607,7 +554,7 @@ async fn handle_status( /// Handle the login process and return the new player's data if successful. async fn handle_login( - server: &SharedServer, + server: &Server, c: &mut Codec, remote_addr: SocketAddr, ) -> anyhow::Result> { @@ -720,7 +667,7 @@ async fn handle_login( remote_addr, }; - if let Login::Disconnect(reason) = server.0.cfg.login(server, &npd).await { + if let Err(reason) = server.0.cfg.login(server, &npd).await { log::info!("Disconnect at login: \"{reason}\""); c.0.write_packet(&login::Disconnect { reason }).await?; return Ok(None); @@ -735,7 +682,7 @@ async fn handle_login( Ok(Some(npd)) } -async fn handle_play(server: &SharedServer, c: Codec, ncd: NewClientData) -> anyhow::Result<()> { +async fn handle_play(server: &Server, c: Codec, ncd: NewClientData) -> anyhow::Result<()> { let (reply_tx, reply_rx) = oneshot::channel(); server @@ -748,7 +695,7 @@ async fn handle_play(server: &SharedServer, c: Codec, ncd: NewClientData) -> any .await?; let (packet_tx, packet_rx) = match reply_rx.await { - Ok(res) => res?, + Ok(res) => res, Err(_) => return Ok(()), // Server closed }; diff --git a/src/slotmap.rs b/src/slotmap.rs index d080e01..ad57987 100644 --- a/src/slotmap.rs +++ b/src/slotmap.rs @@ -159,7 +159,7 @@ impl SlotMap { } pub fn retain(&mut self, mut f: impl FnMut(Key, &mut T) -> bool) { - for (i, slot) in self.slots.iter_mut().enumerate() { + for (i, mut slot) in self.slots.iter_mut().enumerate() { if let Slot::Occupied { value, version } = &mut slot { let key = Key::new(i as u32, *version); diff --git a/src/world.rs b/src/world.rs index 55b2afe..a5a31a1 100644 --- a/src/world.rs +++ b/src/world.rs @@ -1,38 +1,72 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::iter::FusedIterator; +use std::ops::Deref; -use rayon::iter::ParallelIterator; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; -use crate::chunk::{ChunkId, ChunkPos}; +use crate::chunk::ChunkPos; use crate::config::DimensionId; use crate::slotmap::{Key, SlotMap}; -use crate::Id; +use crate::{ + Chunks, ChunksMut, Clients, ClientsMut, Entities, EntitiesMut, Entity, EntityId, Server, +}; -pub struct WorldStore { +pub struct Worlds { sm: SlotMap, + server: Server, +} + +pub struct WorldsMut<'a>(&'a mut Worlds); + +impl<'a> Deref for WorldsMut<'a> { + type Target = Worlds; + + fn deref(&self) -> &Self::Target { + &self.0 + } } #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct WorldId(Key); -impl Id for WorldId { - fn idx(self) -> usize { - self.0.index() as usize - } -} - -impl WorldStore { - pub(crate) fn new() -> Self { - Self { sm: SlotMap::new() } +impl Worlds { + pub(crate) fn new(server: Server) -> Self { + Self { + sm: SlotMap::new(), + server, + } } pub fn count(&self) -> usize { self.sm.count() } + pub fn get(&self, world: WorldId) -> Option { + self.sm.get(world.0).map(WorldRef::new) + } + + pub fn iter(&self) -> impl FusedIterator + Clone + '_ { + self.sm.iter().map(|(k, v)| (WorldId(k), WorldRef::new(v))) + } +} + +impl<'a> WorldsMut<'a> { + pub(crate) fn new(worlds: &'a mut Worlds) -> Self { + Self(worlds) + } + + pub fn reborrow(&mut self) -> WorldsMut { + WorldsMut(self.0) + } + pub fn create(&mut self, dim: DimensionId) -> WorldId { - WorldId(self.sm.insert(World { - chunks: HashMap::new(), + WorldId(self.0.sm.insert(World { + clients: Clients::new(), + entities: Entities::new(), + chunks: Chunks::new( + self.server.clone(), + (self.server.dimension(dim).height / 16) as u32, + ), dimension: dim, })) } @@ -43,53 +77,98 @@ impl WorldStore { /// Note that any entities with positions inside the deleted world will not /// be deleted themselves. pub fn delete(&mut self, world: WorldId) -> bool { - self.sm.remove(world.0).is_some() + self.0.sm.remove(world.0).is_some() } - pub fn retain(&mut self, mut f: impl FnMut(WorldId, &mut World) -> bool) { - self.sm.retain(|k, v| f(WorldId(k), v)) + pub fn retain(&mut self, mut f: impl FnMut(WorldId, WorldMut) -> bool) { + self.0.sm.retain(|k, v| f(WorldId(k), WorldMut::new(v))) } - pub fn get(&self, world: WorldId) -> Option<&World> { - self.sm.get(world.0) + pub fn get_mut(&mut self, world: WorldId) -> Option { + self.0.sm.get_mut(world.0).map(WorldMut::new) } - pub fn get_mut(&mut self, world: WorldId) -> Option<&mut World> { - self.sm.get_mut(world.0) + pub fn iter_mut(&mut self) -> impl FusedIterator + '_ { + self.0 + .sm + .iter_mut() + .map(|(k, v)| (WorldId(k), WorldMut::new(v))) } - pub fn iter(&self) -> impl FusedIterator + Clone + '_ { - self.sm.iter().map(|(k, v)| (WorldId(k), v)) + pub fn par_iter(&self) -> impl ParallelIterator + Clone + '_ { + self.0 + .sm + .par_iter() + .map(|(k, v)| (WorldId(k), WorldRef::new(v))) } - pub fn iter_mut(&mut self) -> impl FusedIterator + '_ { - self.sm.iter_mut().map(|(k, v)| (WorldId(k), v)) - } - - pub fn par_iter(&self) -> impl ParallelIterator + Clone + '_ { - self.sm.par_iter().map(|(k, v)| (WorldId(k), v)) - } - - pub fn par_iter_mut(&mut self) -> impl ParallelIterator + '_ { - self.sm.par_iter_mut().map(|(k, v)| (WorldId(k), v)) + pub fn par_iter_mut(&mut self) -> impl ParallelIterator + '_ { + self.0 + .sm + .par_iter_mut() + .map(|(k, v)| (WorldId(k), WorldMut::new(v))) } } -pub struct World { - chunks: HashMap, +/// A world on the server is a space for chunks, entities, and clients to +/// inhabit. +/// +/// Worlds maintain a collection of chunks and entities that are a part of it. +/// For a chunk or entity to appear, it must be inserted into the world. Chunks +/// and entities can be in multiple worlds at the same time. +/// +/// Deleted chunks and entities are automatically removed from worlds at the end +/// of each tick. +pub(crate) struct World { + clients: Clients, + entities: Entities, + chunks: Chunks, dimension: DimensionId, } -impl World { - pub fn dimension(&self) -> DimensionId { - self.dimension - } +/// A bag of immutable references to the components of a world. +pub struct WorldRef<'a> { + pub clients: &'a Clients, + pub entities: &'a Entities, + pub chunks: &'a Chunks, + pub dimension: DimensionId, +} - pub fn chunks(&self) -> &HashMap { - &self.chunks - } - - pub fn chunks_mut(&mut self) -> &mut HashMap { - &mut self.chunks +impl<'a> WorldRef<'a> { + pub(crate) fn new(w: &'a World) -> Self { + Self { + clients: &w.clients, + entities: &w.entities, + chunks: &w.chunks, + dimension: w.dimension, + } + } +} + +/// A bag of mutable references to the components of a world. +pub struct WorldMut<'a> { + pub clients: ClientsMut<'a>, + pub entities: EntitiesMut<'a>, + pub chunks: ChunksMut<'a>, + pub dimension: DimensionId, +} + +impl<'a> WorldMut<'a> { + pub(crate) fn new(w: &'a mut World) -> Self { + WorldMut { + clients: ClientsMut::new(&mut w.clients), + entities: EntitiesMut::new(&mut w.entities), + chunks: ChunksMut::new(&mut w.chunks), + dimension: w.dimension, + } + } + + pub fn immutable(&'a self) -> WorldRef<'a> { + WorldRef { + clients: &self.clients, + entities: &self.entities, + chunks: &self.chunks, + dimension: self.dimension, + } } }