From 20df126374aab71acfaff9e22e18cd735f6b3c1f Mon Sep 17 00:00:00 2001 From: Patrick Owen Date: Sun, 30 Jun 2024 17:06:33 -0400 Subject: [PATCH] Load character entities from the save file --- client/src/net.rs | 10 ++- common/src/math.rs | 6 ++ common/src/proto.rs | 11 ++- save/src/lib.rs | 10 +++ server/src/lib.rs | 33 ++++++-- server/src/sim.rs | 187 ++++++++++++++++++++++++++++++++++++++++---- 6 files changed, 232 insertions(+), 25 deletions(-) diff --git a/client/src/net.rs b/client/src/net.rs index bbbae577..fb79bf20 100644 --- a/client/src/net.rs +++ b/client/src/net.rs @@ -4,7 +4,10 @@ use anyhow::{anyhow, Error, Result}; use quinn::rustls; use tokio::sync::mpsc; -use common::{codec, proto}; +use common::{ + codec, + proto::{self, connection_error_codes}, +}; use crate::Config; @@ -126,7 +129,10 @@ async fn handle_unordered(incoming: mpsc::UnboundedSender, connection: match codec::recv_whole::(2usize.pow(16), stream).await { Err(e) => { tracing::error!("Error when parsing unordered stream from server: {e}"); - connection.close(1u32.into(), b"could not process stream"); + connection.close( + connection_error_codes::STREAM_ERROR, + b"could not process stream", + ); } Ok(msg) => { let _ = incoming.send(Message::StateDelta(msg)); diff --git a/common/src/math.rs b/common/src/math.rs index 69cc3bd3..3814f32e 100644 --- a/common/src/math.rs +++ b/common/src/math.rs @@ -117,6 +117,12 @@ impl MIsometry { pub fn from_columns_unchecked(columns: &[MVector; 4]) -> Self { Self(na::Matrix4::from_columns(&(*columns).map(|x| x.0))) } + /// Creates an `MIsometry` with its elements filled with the components provided by a slice in column-major order. + /// It is the caller's responsibility to ensure that the resulting matrix is a valid isometry. + #[inline] + pub fn from_column_slice_unchecked(data: &[N]) -> Self { + Self(na::Matrix4::from_column_slice(data)) + } /// Minkowski transpose. Inverse for hyperbolic isometries #[rustfmt::skip] pub fn mtranspose(self) -> Self { diff --git a/common/src/proto.rs b/common/src/proto.rs index 5895f55c..e2b355c6 100644 --- a/common/src/proto.rs +++ b/common/src/proto.rs @@ -66,7 +66,7 @@ pub struct Command { pub orientation: na::UnitQuaternion, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct CharacterInput { /// Relative to the character's current position, excluding orientation pub movement: na::Vector3, @@ -114,3 +114,12 @@ pub struct Character { pub struct Inventory { pub contents: Vec, } + +pub mod connection_error_codes { + use quinn::VarInt; + + pub const CONNECTION_LOST: VarInt = VarInt::from_u32(0); + pub const BAD_CLIENT_COMMAND: VarInt = VarInt::from_u32(2); + pub const STREAM_ERROR: VarInt = VarInt::from_u32(1); + pub const NAME_CONFLICT: VarInt = VarInt::from_u32(3); +} diff --git a/save/src/lib.rs b/save/src/lib.rs index 65f519f3..3dae31e9 100644 --- a/save/src/lib.rs +++ b/save/src/lib.rs @@ -138,6 +138,16 @@ impl Reader { .map(|n| Ok(n.map_err(GetError::from)?.0.value())) .collect() } + + /// Temporary function to load all entity-related save data at once. + /// TODO: Replace this implementation with a streaming implementation + /// that does not require loading everything at once + pub fn get_all_entity_node_ids(&self) -> Result, GetError> { + self.entity_nodes + .iter()? + .map(|n| Ok(n.map_err(GetError::from)?.0.value())) + .collect() + } } fn decompress( diff --git a/server/src/lib.rs b/server/src/lib.rs index 99083887..37b1f2f9 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -16,7 +16,11 @@ use tokio::sync::mpsc; use tokio_stream::wrappers::{IntervalStream, ReceiverStream}; use tracing::{debug, error, error_span, info, trace}; -use common::{codec, proto, SimConfig}; +use common::{ + codec, + proto::{self, connection_error_codes}, + SimConfig, +}; use input_queue::InputQueue; use save::Save; use sim::Sim; @@ -138,9 +142,10 @@ impl Server { } for client_id in overran { error!("dropping slow client {:?}", client_id.0); - self.clients[client_id] - .conn - .close(1u32.into(), b"client reading too slowly"); + self.clients[client_id].conn.close( + connection_error_codes::STREAM_ERROR, + b"client reading too slowly", + ); self.cleanup_client(client_id); } @@ -161,7 +166,14 @@ impl Server { ClientEvent::Hello(hello) => { assert!(client.handles.is_none()); let snapshot = Arc::new(self.sim.snapshot()); - let (id, entity) = self.sim.spawn_character(hello); + let Some((id, entity)) = self.sim.activate_or_spawn_character(&hello) else { + error!("could not spawn {} due to name conflict", hello.name); + client + .conn + .close(connection_error_codes::NAME_CONFLICT, b"name conflict"); + self.cleanup_client(client_id); + return; + }; let (ordered_send, ordered_recv) = mpsc::channel(32); ordered_send.try_send(snapshot).unwrap(); let (unordered_send, unordered_recv) = mpsc::channel(32); @@ -183,7 +195,9 @@ impl Server { } ClientEvent::Lost(e) => { error!("lost: {:#}", e); - client.conn.close(0u32.into(), b""); + client + .conn + .close(connection_error_codes::CONNECTION_LOST, b""); self.cleanup_client(client_id); } ClientEvent::Command(cmd) => { @@ -199,7 +213,7 @@ impl Server { fn cleanup_client(&mut self, client: ClientId) { if let Some(ref x) = self.clients[client].handles { - self.sim.destroy(x.character); + self.sim.deactivate_character(x.character); } self.clients.remove(client); } @@ -249,7 +263,10 @@ async fn drive_recv( // we want to drop the client. We close the connection, which will cause `drive_recv` to // return eventually. tracing::error!("Error when parsing unordered stream from client: {e}"); - connection.close(2u32.into(), b"could not process stream"); + connection.close( + connection_error_codes::BAD_CLIENT_COMMAND, + b"could not process stream", + ); } Ok(msg) => { let _ = send.send((id, ClientEvent::Command(msg))).await; diff --git a/server/src/sim.rs b/server/src/sim.rs index e5b761ad..225f311f 100644 --- a/server/src/sim.rs +++ b/server/src/sim.rs @@ -1,7 +1,8 @@ use std::sync::Arc; use anyhow::Context; -use common::dodeca::Vertex; +use common::dodeca::{Side, Vertex}; +use common::math::MIsometry; use common::node::VoxelData; use common::proto::{BlockUpdate, Inventory, SerializedVoxelData}; use common::world::Material; @@ -11,6 +12,7 @@ use hecs::{DynamicBundle, Entity, EntityBuilder}; use rand::rngs::SmallRng; use rand::{Rng, SeedableRng}; use save::ComponentType; +use serde::{Deserialize, Serialize}; use tracing::{error, error_span, info, trace}; use common::{ @@ -69,6 +71,9 @@ impl Sim { .load_all_voxels(save) .expect("save file must be of a valid format"); result + .load_all_entities(save) + .expect("save file must be of a valid format"); + result } pub fn save(&mut self, save: &mut save::Save) -> Result<(), save::DbError> { @@ -109,6 +114,99 @@ impl Sim { Ok(()) } + fn load_all_entities(&mut self, save: &save::Save) -> anyhow::Result<()> { + let mut read = save.read()?; + for node_hash in read.get_all_entity_node_ids()? { + let Some(entity_node) = read.get_entity_node(node_hash)? else { + continue; + }; + let node_id = self.graph.from_hash(node_hash); + for entity_bytes in entity_node.entities { + let save_entity: SaveEntity = postcard::from_bytes(&entity_bytes)?; + self.load_entity(&mut read, node_id, save_entity)?; + } + } + Ok(()) + } + + fn load_entity( + &mut self, + read: &mut save::Reader, + node: NodeId, + save_entity: SaveEntity, + ) -> anyhow::Result<()> { + let entity_id = EntityId::from_bits(u64::from_le_bytes(save_entity.entity)); + let mut entity_builder = EntityBuilder::new(); + entity_builder.add(entity_id); + for (component_type, component_bytes) in save_entity.components { + self.load_component( + read, + &mut entity_builder, + node, + ComponentType::try_from(component_type as i32).unwrap(), + component_bytes, + )?; + } + let entity = self.world.spawn(entity_builder.build()); + self.graph_entities.insert(node, entity); + self.entity_ids.insert(entity_id, entity); + Ok(()) + } + + fn load_component( + &mut self, + read: &mut save::Reader, + entity_builder: &mut EntityBuilder, + node: NodeId, + component_type: ComponentType, + component_bytes: Vec, + ) -> anyhow::Result<()> { + match component_type { + ComponentType::Position => { + let column_slice: [f32; 16] = postcard::from_bytes(&component_bytes)?; + entity_builder.add(Position { + node, + local: MIsometry::from_column_slice_unchecked(&column_slice), + }); + } + ComponentType::Name => { + let name = String::from_utf8(component_bytes)?; + // Ensure that every node occupied by a character is generated. + let Some(character) = read.get_character(&name)? else { + // Skip loading named entities that lack path information. + error!("Entity {} will not be loaded because their node path information is missing.", name); + return Ok(()); + }; + let mut current_node = NodeId::ROOT; + for side in character + .path + .into_iter() + .map(|side| Side::from_index(side as usize)) + { + current_node = self.graph.ensure_neighbor(current_node, side); + } + if current_node != node { + // Skip loading named entities that are in the wrong place. This can happen + // when there are multiple entities with the same name, which has been possible + // in previous versions of Hypermine. + error!("Entity {} will not be loaded because their node path information is incorrect.", name); + return Ok(()); + } + // Prepare all relevant components that are needed to support ComponentType::Name + entity_builder.add(InactiveCharacter(Character { + name, + state: CharacterState { + velocity: na::Vector3::zeros(), + on_ground: false, + orientation: na::UnitQuaternion::identity(), + }, + })); + entity_builder.add(Inventory { contents: vec![] }); + } + } + Ok(()) + } + fn load_all_voxels(&mut self, save: &save::Save) -> anyhow::Result<()> { let mut read = save.read()?; for node_hash in read.get_all_voxel_node_ids()? { @@ -149,7 +247,11 @@ impl Sim { postcard::to_stdvec(&pos.local.as_ref()).unwrap(), )); } - if let Some(ch) = entity.get::<&Character>() { + if let Some(ch) = entity.get::<&Character>().or_else(|| { + entity + .get::<&InactiveCharacter>() + .map(|ich| hecs::Ref::map(ich, |ich| &ich.0)) // Extract Ref from Ref + }) { components.push((ComponentType::Name as u64, ch.name.as_bytes().into())); } let mut repr = Vec::new(); @@ -185,13 +287,46 @@ impl Sim { save::VoxelNode { chunks } } - pub fn spawn_character(&mut self, hello: ClientHello) -> (EntityId, Entity) { + /// Activates or spawns a character with a given name, or returns None if there is already an active + /// character with that name + pub fn activate_or_spawn_character( + &mut self, + hello: &ClientHello, + ) -> Option<(EntityId, Entity)> { + // Check for conflicting characters + if self + .world + .query::<&Character>() + .iter() + .any(|(_, character)| character.name == hello.name) + { + return None; + } + + // Check for matching characters + let matching_character = self + .world + .query::<(&EntityId, &InactiveCharacter)>() + .iter() + .find(|(_, (_, inactive_character))| inactive_character.0.name == hello.name) + .map(|(entity, (entity_id, _))| (*entity_id, entity)); + if let Some((entity_id, entity)) = matching_character { + info!(id = %entity_id, name = %hello.name, "activating character"); + let inactive_character = self.world.remove_one::(entity).unwrap(); + self.world + .insert(entity, (inactive_character.0, CharacterInput::default())) + .unwrap(); + self.accumulated_changes.spawns.push(entity); + return Some((entity_id, entity)); + } + + // Spawn entirely new character let position = Position { node: NodeId::ROOT, local: math::translate_along(&(na::Vector3::y() * 1.4)), }; let character = Character { - name: hello.name, + name: hello.name.clone(), state: CharacterState { orientation: na::one(), velocity: na::Vector3::zeros(), @@ -199,13 +334,20 @@ impl Sim { }, }; let inventory = Inventory { contents: vec![] }; - let initial_input = CharacterInput { - movement: na::Vector3::zeros(), - jump: false, - no_clip: true, - block_update: None, - }; - self.spawn((position, character, inventory, initial_input)) + let initial_input = CharacterInput::default(); + Some(self.spawn((position, character, inventory, initial_input))) + } + + pub fn deactivate_character(&mut self, entity: Entity) { + let entity_id = *self.world.get::<&EntityId>(entity).unwrap(); + let (character, _) = self + .world + .remove::<(Character, CharacterInput)>(entity) + .unwrap(); + self.world + .insert_one(entity, InactiveCharacter(character)) + .unwrap(); + self.accumulated_changes.despawns.push(entity_id); } fn spawn(&mut self, bundle: impl DynamicBundle) -> (EntityId, Entity) { @@ -225,7 +367,10 @@ impl Sim { } self.entity_ids.insert(id, entity); - self.accumulated_changes.spawns.push(entity); + + if !self.world.satisfies::<&InactiveCharacter>(entity).unwrap() { + self.accumulated_changes.spawns.push(entity); + } (id, entity) } @@ -249,7 +394,10 @@ impl Sim { self.graph_entities.remove(position.node, entity); } self.world.despawn(entity).unwrap(); - self.accumulated_changes.despawns.push(id); + + if !self.world.satisfies::<&InactiveCharacter>(entity).unwrap() { + self.accumulated_changes.despawns.push(id); + } } /// Collect information about all entities, for transmission to new clients @@ -268,7 +416,10 @@ impl Sim { inventory_additions: Vec::new(), inventory_removals: Vec::new(), }; - for (entity, &id) in &mut self.world.query::<&EntityId>() { + for (entity, &id) in &mut self + .world + .query::>() + { spawns.spawns.push((id, dump_entity(&self.world, entity))); } for &chunk_id in self.modified_chunks.iter() { @@ -487,7 +638,12 @@ impl Sim { } } +/// Collect all information about a particular entity for transmission to clients. fn dump_entity(world: &hecs::World, entity: Entity) -> Vec { + assert!( + !world.satisfies::<&InactiveCharacter>(entity).unwrap(), + "Inactive characters should not be sent to clients" + ); let mut components = Vec::new(); if let Ok(x) = world.get::<&Position>(entity) { components.push(Component::Position(*x)); @@ -504,6 +660,9 @@ fn dump_entity(world: &hecs::World, entity: Entity) -> Vec { components } +#[derive(Debug, Serialize, Deserialize, Clone)] +struct InactiveCharacter(pub Character); + /// Stores changes that the server has canonically done but hasn't yet broadcast to clients #[derive(Default)] struct AccumulatedChanges {