//! Join protocol for new peer onboarding //! //! This module handles the protocol for new peers to join an existing session //! and receive the full world state. The join flow: //! //! 1. New peer sends JoinRequest with node ID and optional session secret //! 2. Existing peer validates request and responds with FullState //! 3. New peer applies FullState to initialize local world //! 4. New peer begins participating in delta synchronization //! //! **NOTE:** This is a simplified implementation for Phase 7. Full security //! and session management will be enhanced in Phase 13. use bevy::{ prelude::*, reflect::TypeRegistry, }; use crate::networking::{ blob_support::BlobStore, delta_generation::NodeVectorClock, entity_map::NetworkEntityMap, messages::{ EntityState, SyncMessage, VersionedMessage, }, GossipBridge, NetworkedEntity, }; /// Session secret for join authentication /// /// In Phase 7, this is optional. Phase 13 will add full authentication. pub type SessionSecret = Vec; /// Build a JoinRequest message /// /// # Example /// /// ``` /// use lib::networking::build_join_request; /// use uuid::Uuid; /// /// let node_id = Uuid::new_v4(); /// let request = build_join_request(node_id, None); /// ``` pub fn build_join_request(node_id: uuid::Uuid, session_secret: Option) -> VersionedMessage { VersionedMessage::new(SyncMessage::JoinRequest { node_id, session_secret, }) } /// Build a FullState message containing all networked entities /// /// This serializes the entire world state for a new peer. Large worlds may /// take significant bandwidth - Phase 14 will add compression. /// /// # Parameters /// /// - `world`: Bevy world containing entities /// - `query`: Query for all NetworkedEntity components /// - `type_registry`: Type registry for serialization /// - `node_clock`: Current node vector clock /// - `blob_store`: Optional blob store for large components /// /// # Returns /// /// A FullState message ready to send to the joining peer pub fn build_full_state( world: &World, networked_entities: &Query<(Entity, &NetworkedEntity)>, type_registry: &TypeRegistry, node_clock: &NodeVectorClock, blob_store: Option<&BlobStore>, ) -> VersionedMessage { use crate::{ networking::{ blob_support::create_component_data, messages::ComponentState, }, persistence::reflection::serialize_component, }; let mut entities = Vec::new(); for (entity, networked) in networked_entities.iter() { let entity_ref = world.entity(entity); let mut components = Vec::new(); // Iterate over all type registrations to find components for registration in type_registry.iter() { // Skip if no ReflectComponent data let Some(reflect_component) = registration.data::() else { continue; }; let type_path = registration.type_info().type_path(); // Skip networked wrapper components if type_path.ends_with("::NetworkedEntity") || type_path.ends_with("::NetworkedTransform") || type_path.ends_with("::NetworkedSelection") || type_path.ends_with("::NetworkedDrawingPath") { continue; } // Try to reflect this component from the entity if let Some(reflected) = reflect_component.reflect(entity_ref) { // Serialize the component if let Ok(serialized) = serialize_component(reflected, type_registry) { // Create component data (inline or blob) let data = if let Some(store) = blob_store { match create_component_data(serialized, store) { Ok(d) => d, Err(_) => continue, } } else { crate::networking::ComponentData::Inline(serialized) }; components.push(ComponentState { component_type: type_path.to_string(), data, }); } } } entities.push(EntityState { entity_id: networked.network_id, owner_node_id: networked.owner_node_id, vector_clock: node_clock.clock.clone(), components, is_deleted: false, }); } info!( "Built FullState with {} entities for new peer", entities.len() ); VersionedMessage::new(SyncMessage::FullState { entities, vector_clock: node_clock.clock.clone(), }) } /// Apply a FullState message to the local world /// /// This initializes the world for a newly joined peer by spawning all entities /// and applying their component state. /// /// # Parameters /// /// - `entities`: List of entity states from FullState message /// - `vector_clock`: Vector clock from FullState /// - `commands`: Bevy commands for spawning entities /// - `entity_map`: Entity map to populate /// - `type_registry`: Type registry for deserialization /// - `node_clock`: Our node's vector clock to update /// - `blob_store`: Optional blob store for resolving blob references /// - `tombstone_registry`: Optional tombstone registry for deletion tracking pub fn apply_full_state( entities: Vec, remote_clock: crate::networking::VectorClock, commands: &mut Commands, entity_map: &mut NetworkEntityMap, type_registry: &TypeRegistry, node_clock: &mut NodeVectorClock, blob_store: Option<&BlobStore>, mut tombstone_registry: Option<&mut crate::networking::TombstoneRegistry>, ) { use crate::{ networking::blob_support::get_component_data, persistence::reflection::deserialize_component, }; info!("Applying FullState with {} entities", entities.len()); // Merge the remote vector clock node_clock.clock.merge(&remote_clock); // Spawn all entities and apply their state for entity_state in entities { // Handle deleted entities (tombstones) if entity_state.is_deleted { // Record tombstone if let Some(ref mut registry) = tombstone_registry { registry.record_deletion( entity_state.entity_id, entity_state.owner_node_id, entity_state.vector_clock.clone(), ); } continue; } // Spawn entity with NetworkedEntity component let entity = commands .spawn(NetworkedEntity::with_id( entity_state.entity_id, entity_state.owner_node_id, )) .id(); // Register in entity map entity_map.insert(entity_state.entity_id, entity); let num_components = entity_state.components.len(); // Apply all components for component_state in &entity_state.components { // Get the actual data (resolve blob if needed) let data_bytes = match &component_state.data { | crate::networking::ComponentData::Inline(bytes) => bytes.clone(), | blob_ref @ crate::networking::ComponentData::BlobRef { .. } => { if let Some(store) = blob_store { match get_component_data(blob_ref, store) { Ok(bytes) => bytes, Err(e) => { error!( "Failed to retrieve blob for {}: {}", component_state.component_type, e ); continue; } } } else { error!( "Blob reference for {} but no blob store available", component_state.component_type ); continue; } } }; // Deserialize the component let reflected = match deserialize_component(&data_bytes, type_registry) { Ok(r) => r, Err(e) => { error!( "Failed to deserialize {}: {}", component_state.component_type, e ); continue; } }; // Get the type registration let registration = match type_registry.get_with_type_path(&component_state.component_type) { Some(reg) => reg, None => { error!( "Component type {} not registered", component_state.component_type ); continue; } }; // Get ReflectComponent data let reflect_component = match registration.data::() { Some(rc) => rc.clone(), None => { error!( "Component type {} does not have ReflectComponent data", component_state.component_type ); continue; } }; // Insert the component let component_type_owned = component_state.component_type.clone(); commands.queue(move |world: &mut World| { let type_registry_arc = { let Some(type_registry_res) = world.get_resource::() else { error!("AppTypeRegistry not found in world"); return; }; type_registry_res.clone() }; let type_registry = type_registry_arc.read(); if let Ok(mut entity_mut) = world.get_entity_mut(entity) { reflect_component.insert(&mut entity_mut, &*reflected, &type_registry); debug!("Applied component {} from FullState", component_type_owned); } }); } debug!( "Spawned entity {:?} from FullState with {} components", entity_state.entity_id, num_components ); } info!("FullState applied successfully"); } /// System to handle JoinRequest messages /// /// When we receive a JoinRequest, build and send a FullState response. /// /// Add this to your app: /// /// ```no_run /// use bevy::prelude::*; /// use lib::networking::handle_join_requests_system; /// /// App::new() /// .add_systems(Update, handle_join_requests_system); /// ``` pub fn handle_join_requests_system( world: &World, bridge: Option>, networked_entities: Query<(Entity, &NetworkedEntity)>, type_registry: Res, node_clock: Res, blob_store: Option>, ) { let Some(bridge) = bridge else { return; }; let registry = type_registry.read(); let blob_store_ref = blob_store.as_deref(); // Poll for incoming JoinRequest messages while let Some(message) = bridge.try_recv() { match message.message { | SyncMessage::JoinRequest { node_id, session_secret, } => { info!("Received JoinRequest from node {}", node_id); // TODO: Validate session_secret in Phase 13 if let Some(_secret) = session_secret { debug!("Session secret validation not yet implemented"); } // Build full state let full_state = build_full_state( world, &networked_entities, ®istry, &node_clock, blob_store_ref, ); // Send full state to joining peer if let Err(e) = bridge.send(full_state) { error!("Failed to send FullState: {}", e); } else { info!("Sent FullState to node {}", node_id); } } | _ => { // Not a JoinRequest, ignore (other systems handle other messages) } } } } /// System to handle FullState messages /// /// When we receive a FullState (after sending JoinRequest), apply it to our world. /// /// This system should run BEFORE receive_and_apply_deltas_system to ensure /// we're fully initialized before processing deltas. pub fn handle_full_state_system( mut commands: Commands, bridge: Option>, mut entity_map: ResMut, type_registry: Res, mut node_clock: ResMut, blob_store: Option>, mut tombstone_registry: Option>, ) { let Some(bridge) = bridge else { return; }; let registry = type_registry.read(); let blob_store_ref = blob_store.as_deref(); // Poll for FullState messages while let Some(message) = bridge.try_recv() { match message.message { | SyncMessage::FullState { entities, vector_clock, } => { info!("Received FullState with {} entities", entities.len()); apply_full_state( entities, vector_clock, &mut commands, &mut entity_map, ®istry, &mut node_clock, blob_store_ref, tombstone_registry.as_deref_mut(), ); } | _ => { // Not a FullState, ignore } } } } #[cfg(test)] mod tests { use super::*; use crate::networking::VectorClock; #[test] fn test_build_join_request() { let node_id = uuid::Uuid::new_v4(); let request = build_join_request(node_id, None); match request.message { | SyncMessage::JoinRequest { node_id: req_node_id, session_secret, } => { assert_eq!(req_node_id, node_id); assert!(session_secret.is_none()); } | _ => panic!("Expected JoinRequest"), } } #[test] fn test_build_join_request_with_secret() { let node_id = uuid::Uuid::new_v4(); let secret = vec![1, 2, 3, 4]; let request = build_join_request(node_id, Some(secret.clone())); match request.message { | SyncMessage::JoinRequest { node_id: _, session_secret, } => { assert_eq!(session_secret, Some(secret)); } | _ => panic!("Expected JoinRequest"), } } #[test] fn test_entity_state_structure() { let entity_id = uuid::Uuid::new_v4(); let owner_node_id = uuid::Uuid::new_v4(); let state = EntityState { entity_id, owner_node_id, vector_clock: VectorClock::new(), components: vec![], is_deleted: false, }; assert_eq!(state.entity_id, entity_id); assert_eq!(state.owner_node_id, owner_node_id); assert_eq!(state.components.len(), 0); assert!(!state.is_deleted); } #[test] fn test_apply_full_state_empty() { let node_id = uuid::Uuid::new_v4(); let mut node_clock = NodeVectorClock::new(node_id); let remote_clock = VectorClock::new(); // Create minimal setup for testing let mut entity_map = NetworkEntityMap::new(); let type_registry = TypeRegistry::new(); // Need a minimal Bevy app for Commands let mut app = App::new(); let mut commands = app.world_mut().commands(); apply_full_state( vec![], remote_clock.clone(), &mut commands, &mut entity_map, &type_registry, &mut node_clock, None, None, // tombstone_registry ); // Should have merged clocks assert_eq!(node_clock.clock, remote_clock); } }