411 lines
11 KiB
Rust
411 lines
11 KiB
Rust
|
|
//! 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<uuid::Uuid>,
|
||
|
|
}
|
||
|
|
|
||
|
|
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<Vec2>,
|
||
|
|
|
||
|
|
/// 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);
|
||
|
|
}
|
||
|
|
}
|