//! Networked entity components //! //! This module defines components that mark entities as networked and track //! their network identity across the distributed system. use bevy::prelude::*; use serde::{ Deserialize, Serialize, }; use crate::networking::vector_clock::NodeId; /// Marker component indicating an entity should be synchronized over the /// network /// /// Add this component to any entity that should have its state synchronized /// across peers. The networking system will automatically track changes and /// broadcast deltas. /// /// # Relationship with Persisted /// /// NetworkedEntity and Persisted are complementary: /// - `Persisted` - Entity state saved to local SQLite database /// - `NetworkedEntity` - Entity state synchronized across network peers /// /// Most entities will have both components for full durability and sync. /// /// # Network Identity /// /// Each networked entity has: /// - `network_id` - Globally unique UUID for this entity across all peers /// - `owner_node_id` - Node that originally created this entity /// /// # Example /// /// ``` /// use bevy::prelude::*; /// use lib::networking::NetworkedEntity; /// use uuid::Uuid; /// /// fn spawn_networked_entity(mut commands: Commands) { /// let node_id = Uuid::new_v4(); /// /// commands.spawn(( /// NetworkedEntity::new(node_id), /// Transform::default(), /// )); /// } /// ``` #[derive(Component, Reflect, Debug, Clone, Serialize, Deserialize)] #[reflect(Component)] pub struct NetworkedEntity { /// Globally unique network ID for this entity /// /// This ID is used to identify the entity across all peers in the network. /// When a peer receives an EntityDelta, it uses this ID to locate the /// corresponding local entity. pub network_id: uuid::Uuid, /// Node that created this entity /// /// Used for conflict resolution and ownership tracking. When two nodes /// concurrently create entities, the owner_node_id can be used as a /// tiebreaker. pub owner_node_id: NodeId, } impl NetworkedEntity { /// Create a new networked entity /// /// Generates a new random network_id and sets the owner to the specified /// node. /// /// # Example /// /// ``` /// use lib::networking::NetworkedEntity; /// use uuid::Uuid; /// /// let node_id = Uuid::new_v4(); /// let entity = NetworkedEntity::new(node_id); /// /// assert_eq!(entity.owner_node_id, node_id); /// ``` pub fn new(owner_node_id: NodeId) -> Self { Self { network_id: uuid::Uuid::new_v4(), owner_node_id, } } /// Create a networked entity with a specific network ID /// /// Used when receiving entities from remote peers - we need to use their /// network_id rather than generating a new one. /// /// # Example /// /// ``` /// use lib::networking::NetworkedEntity; /// use uuid::Uuid; /// /// let network_id = Uuid::new_v4(); /// let owner_id = Uuid::new_v4(); /// let entity = NetworkedEntity::with_id(network_id, owner_id); /// /// assert_eq!(entity.network_id, network_id); /// assert_eq!(entity.owner_node_id, owner_id); /// ``` pub fn with_id(network_id: uuid::Uuid, owner_node_id: NodeId) -> Self { Self { network_id, owner_node_id, } } /// Check if this node owns the entity pub fn is_owned_by(&self, node_id: NodeId) -> bool { self.owner_node_id == node_id } } impl Default for NetworkedEntity { fn default() -> Self { Self { network_id: uuid::Uuid::new_v4(), owner_node_id: uuid::Uuid::new_v4(), } } } /// Wrapper for Transform component that enables CRDT synchronization /// /// This is a marker component used alongside Transform to indicate that /// Transform changes should be synchronized using Last-Write-Wins semantics. /// /// # Example /// /// ``` /// use bevy::prelude::*; /// use lib::networking::{NetworkedEntity, NetworkedTransform}; /// use uuid::Uuid; /// /// fn spawn_synced_transform(mut commands: Commands) { /// let node_id = Uuid::new_v4(); /// /// commands.spawn(( /// NetworkedEntity::new(node_id), /// Transform::default(), /// NetworkedTransform, /// )); /// } /// ``` #[derive(Component, Reflect, Debug, Clone, Copy, Default)] #[reflect(Component)] pub struct NetworkedTransform; /// Wrapper for a selection component using OR-Set semantics /// /// Tracks a set of selected entity network IDs. Uses OR-Set (Observed-Remove) /// CRDT to handle concurrent add/remove operations correctly. /// /// # OR-Set Semantics /// /// - Concurrent adds and removes: add wins /// - Each add has a unique operation ID /// - Removes reference specific add operation IDs /// /// # Example /// /// ``` /// use bevy::prelude::*; /// use lib::networking::{NetworkedEntity, NetworkedSelection}; /// use uuid::Uuid; /// /// fn create_selection(mut commands: Commands) { /// let node_id = Uuid::new_v4(); /// let mut selection = NetworkedSelection::new(); /// /// // Add some entities to the selection /// selection.selected_ids.insert(Uuid::new_v4()); /// selection.selected_ids.insert(Uuid::new_v4()); /// /// commands.spawn(( /// NetworkedEntity::new(node_id), /// selection, /// )); /// } /// ``` #[derive(Component, Reflect, Debug, Clone, Default)] #[reflect(Component)] pub struct NetworkedSelection { /// Set of selected entity network IDs /// /// This will be synchronized using OR-Set CRDT semantics in later phases. /// For now, it's a simple HashSet. pub selected_ids: std::collections::HashSet, } impl NetworkedSelection { /// Create a new empty selection pub fn new() -> Self { Self { selected_ids: std::collections::HashSet::new(), } } /// Add an entity to the selection pub fn add(&mut self, entity_id: uuid::Uuid) { self.selected_ids.insert(entity_id); } /// Remove an entity from the selection pub fn remove(&mut self, entity_id: uuid::Uuid) { self.selected_ids.remove(&entity_id); } /// Check if an entity is selected pub fn contains(&self, entity_id: uuid::Uuid) -> bool { self.selected_ids.contains(&entity_id) } /// Clear all selections pub fn clear(&mut self) { self.selected_ids.clear(); } /// Get the number of selected entities pub fn len(&self) -> usize { self.selected_ids.len() } /// Check if the selection is empty pub fn is_empty(&self) -> bool { self.selected_ids.is_empty() } } /// Wrapper for a drawing path component using Sequence CRDT semantics /// /// Represents an ordered sequence of points that can be collaboratively edited. /// Uses RGA (Replicated Growable Array) CRDT to maintain consistent ordering /// across concurrent insertions. /// /// # RGA Semantics /// /// - Each point has a unique operation ID /// - Points reference the ID of the point they're inserted after /// - Concurrent insertions maintain consistent ordering /// /// # Example /// /// ``` /// use bevy::prelude::*; /// use lib::networking::{NetworkedEntity, NetworkedDrawingPath}; /// use uuid::Uuid; /// /// fn create_path(mut commands: Commands) { /// let node_id = Uuid::new_v4(); /// let mut path = NetworkedDrawingPath::new(); /// /// // Add some points to the path /// path.points.push(Vec2::new(0.0, 0.0)); /// path.points.push(Vec2::new(10.0, 10.0)); /// path.points.push(Vec2::new(20.0, 5.0)); /// /// commands.spawn(( /// NetworkedEntity::new(node_id), /// path, /// )); /// } /// ``` #[derive(Component, Reflect, Debug, Clone, Default)] #[reflect(Component)] pub struct NetworkedDrawingPath { /// Ordered sequence of points in the path /// /// This will be synchronized using RGA (Sequence CRDT) semantics in later /// phases. For now, it's a simple Vec. pub points: Vec, /// Drawing stroke color pub color: Color, /// Stroke width pub width: f32, } impl NetworkedDrawingPath { /// Create a new empty drawing path pub fn new() -> Self { Self { points: Vec::new(), color: Color::BLACK, width: 2.0, } } /// Create a path with a specific color and width pub fn with_style(color: Color, width: f32) -> Self { Self { points: Vec::new(), color, width, } } /// Add a point to the end of the path pub fn push(&mut self, point: Vec2) { self.points.push(point); } /// Get the number of points in the path pub fn len(&self) -> usize { self.points.len() } /// Check if the path is empty pub fn is_empty(&self) -> bool { self.points.is_empty() } /// Clear all points from the path pub fn clear(&mut self) { self.points.clear(); } } #[cfg(test)] mod tests { use super::*; #[test] fn test_networked_entity_new() { let node_id = uuid::Uuid::new_v4(); let entity = NetworkedEntity::new(node_id); assert_eq!(entity.owner_node_id, node_id); assert_ne!(entity.network_id, uuid::Uuid::nil()); } #[test] fn test_networked_entity_with_id() { let network_id = uuid::Uuid::new_v4(); let owner_id = uuid::Uuid::new_v4(); let entity = NetworkedEntity::with_id(network_id, owner_id); assert_eq!(entity.network_id, network_id); assert_eq!(entity.owner_node_id, owner_id); } #[test] fn test_networked_entity_is_owned_by() { let owner_id = uuid::Uuid::new_v4(); let other_id = uuid::Uuid::new_v4(); let entity = NetworkedEntity::new(owner_id); assert!(entity.is_owned_by(owner_id)); assert!(!entity.is_owned_by(other_id)); } #[test] fn test_networked_selection() { let mut selection = NetworkedSelection::new(); let id1 = uuid::Uuid::new_v4(); let id2 = uuid::Uuid::new_v4(); assert!(selection.is_empty()); selection.add(id1); assert_eq!(selection.len(), 1); assert!(selection.contains(id1)); selection.add(id2); assert_eq!(selection.len(), 2); assert!(selection.contains(id2)); selection.remove(id1); assert_eq!(selection.len(), 1); assert!(!selection.contains(id1)); selection.clear(); assert!(selection.is_empty()); } #[test] fn test_networked_drawing_path() { let mut path = NetworkedDrawingPath::new(); assert!(path.is_empty()); path.push(Vec2::new(0.0, 0.0)); assert_eq!(path.len(), 1); path.push(Vec2::new(10.0, 10.0)); assert_eq!(path.len(), 2); path.clear(); assert!(path.is_empty()); } #[test] fn test_drawing_path_with_style() { let path = NetworkedDrawingPath::with_style(Color::srgb(1.0, 0.0, 0.0), 5.0); assert_eq!(path.color, Color::srgb(1.0, 0.0, 0.0)); assert_eq!(path.width, 5.0); } }