//! 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::*; use crate::networking::{ GossipBridge, NetworkedEntity, SessionId, VectorClock, blob_support::BlobStore, delta_generation::NodeVectorClock, entity_map::NetworkEntityMap, messages::{ EntityState, JoinType, SyncMessage, VersionedMessage, }, }; /// Build a JoinRequest message /// /// # Arguments /// * `node_id` - The UUID of the node requesting to join /// * `session_id` - The session to join /// * `session_secret` - Optional pre-shared secret for authentication /// * `last_known_clock` - Optional vector clock from previous session (for rejoin) /// * `join_type` - Whether this is a fresh join or rejoin /// /// # Example /// /// ``` /// use libmarathon::networking::{build_join_request, SessionId, JoinType}; /// use uuid::Uuid; /// /// let node_id = Uuid::new_v4(); /// let session_id = SessionId::new(); /// let request = build_join_request(node_id, session_id, None, None, JoinType::Fresh); /// ``` pub fn build_join_request( node_id: uuid::Uuid, session_id: SessionId, session_secret: Option>, last_known_clock: Option, join_type: JoinType, ) -> VersionedMessage { VersionedMessage::new(SyncMessage::JoinRequest { node_id, session_id, session_secret, last_known_clock, join_type, }) } /// 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`: Component 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: &crate::persistence::ComponentTypeRegistry, node_clock: &NodeVectorClock, blob_store: Option<&BlobStore>, ) -> VersionedMessage { use crate::{ networking::{ blob_support::create_component_data, messages::ComponentState, }, }; let mut entities = Vec::new(); for (entity, networked) in networked_entities.iter() { let mut components = Vec::new(); // Serialize all registered Synced components on this entity let serialized_components = type_registry.serialize_entity_components(world, entity); for (discriminant, _type_path, serialized) in serialized_components { // 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 { discriminant, 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`: Component 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, world: &mut World, type_registry: &crate::persistence::ComponentTypeRegistry, ) { use crate::networking::blob_support::get_component_data; info!("Applying FullState with {} entities", entities.len()); // Merge the remote vector clock { let mut node_clock = world.resource_mut::(); 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(mut registry) = world.get_resource_mut::() { registry.record_deletion( entity_state.entity_id, entity_state.owner_node_id, entity_state.vector_clock.clone(), ); } continue; } // Spawn entity with NetworkedEntity and Persisted components // This ensures entities received via FullState are persisted locally let entity = world .spawn(( NetworkedEntity::with_id(entity_state.entity_id, entity_state.owner_node_id), crate::persistence::Persisted::with_id(entity_state.entity_id), )) .id(); // Register in entity map { let mut entity_map = world.resource_mut::(); 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 { .. } => { let blob_store = world.get_resource::(); if let Some(store) = blob_store.as_deref() { match get_component_data(blob_ref, store) { | Ok(bytes) => bytes, | Err(e) => { error!( "Failed to retrieve blob for discriminant {}: {}", component_state.discriminant, e ); continue; }, } } else { error!( "Blob reference for discriminant {} but no blob store available", component_state.discriminant ); continue; } }, }; // Use the discriminant directly from ComponentState let discriminant = component_state.discriminant; // Deserialize the component let boxed_component = match type_registry.deserialize(discriminant, &data_bytes) { | Ok(component) => component, | Err(e) => { error!( "Failed to deserialize discriminant {}: {}", discriminant, e ); continue; }, }; // Get the insert function for this discriminant let Some(insert_fn) = type_registry.get_insert_fn(discriminant) else { error!("No insert function for discriminant {}", discriminant); continue; }; // Insert the component directly let type_name_for_log = type_registry.get_type_name(discriminant) .unwrap_or("unknown"); if let Ok(mut entity_mut) = world.get_entity_mut(entity) { insert_fn(&mut entity_mut, boxed_component); debug!("Applied component {} from FullState", type_name_for_log); } } 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 libmarathon::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.0; 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_id, session_secret, last_known_clock: _, join_type, } => { info!( "Received JoinRequest from node {} for session {} (type: {:?})", node_id, session_id, join_type ); // Validate session secret if configured if let Some(expected) = world.get_resource::() { match &session_secret { | Some(provided_secret) => { if let Err(e) = crate::networking::validate_session_secret( provided_secret, expected.as_bytes(), ) { error!("JoinRequest from {} rejected: {}", node_id, e); continue; // Skip this request, don't send FullState } info!("Session secret validated for node {}", node_id); }, | None => { warn!( "JoinRequest from {} missing required session secret, rejecting", node_id ); continue; // Reject requests without secret when one is configured }, } } else if session_secret.is_some() { // No session secret configured but peer provided one debug!("Session secret provided but none configured, accepting"); } // 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(world: &mut World) { // Check if bridge exists if world.get_resource::().is_none() { return; } let bridge = world.resource::().clone(); let type_registry = { let registry_resource = world.resource::(); registry_resource.0 }; // 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, world, type_registry, ); }, | _ => { // 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 session_id = SessionId::new(); let request = build_join_request(node_id, session_id.clone(), None, None, JoinType::Fresh); match request.message { | SyncMessage::JoinRequest { node_id: req_node_id, session_id: req_session_id, session_secret, last_known_clock, join_type, } => { assert_eq!(req_node_id, node_id); assert_eq!(req_session_id, session_id); assert!(session_secret.is_none()); assert!(last_known_clock.is_none()); assert!(matches!(join_type, JoinType::Fresh)); }, | _ => panic!("Expected JoinRequest"), } } #[test] fn test_build_join_request_with_secret() { let node_id = uuid::Uuid::new_v4(); let session_id = SessionId::new(); let secret = vec![1, 2, 3, 4]; let request = build_join_request( node_id, session_id.clone(), Some(secret.clone()), None, JoinType::Fresh, ); match request.message { | SyncMessage::JoinRequest { node_id: _, session_id: req_session_id, session_secret, last_known_clock, join_type, } => { assert_eq!(req_session_id, session_id); assert_eq!(session_secret, Some(secret)); assert!(last_known_clock.is_none()); assert!(matches!(join_type, JoinType::Fresh)); }, | _ => panic!("Expected JoinRequest"), } } #[test] fn test_build_join_request_rejoin() { let node_id = uuid::Uuid::new_v4(); let session_id = SessionId::new(); let clock = VectorClock::new(); let join_type = JoinType::Rejoin { last_active: 1234567890, entity_count: 42, }; let request = build_join_request( node_id, session_id.clone(), None, Some(clock.clone()), join_type.clone(), ); match request.message { | SyncMessage::JoinRequest { node_id: req_node_id, session_id: req_session_id, session_secret, last_known_clock, join_type: req_join_type, } => { assert_eq!(req_node_id, node_id); assert_eq!(req_session_id, session_id); assert!(session_secret.is_none()); assert_eq!(last_known_clock, Some(clock)); assert!(matches!(req_join_type, JoinType::Rejoin { .. })); }, | _ => 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 remote_clock = VectorClock::new(); let type_registry = crate::persistence::component_registry(); // Need a minimal Bevy app for testing let mut app = App::new(); // Insert required resources app.insert_resource(NetworkEntityMap::new()); app.insert_resource(NodeVectorClock::new(node_id)); apply_full_state( vec![], remote_clock.clone(), app.world_mut(), type_registry, ); // Should have merged clocks let node_clock = app.world().resource::(); assert_eq!(node_clock.clock, remote_clock); } }