Files
marathon/crates/lib/src/networking/operation_builder.rs
2025-12-09 22:21:58 +00:00

261 lines
8.0 KiB
Rust

//! Build CRDT operations from ECS component changes
//!
//! This module provides utilities to convert Bevy component changes into
//! ComponentOp operations that can be synchronized across the network.
use bevy::{
prelude::*,
reflect::TypeRegistry,
};
use crate::{
networking::{
blob_support::{
create_component_data,
BlobStore,
},
error::Result,
messages::ComponentData,
operations::{
ComponentOp,
ComponentOpBuilder,
},
vector_clock::{
NodeId,
VectorClock,
},
},
persistence::reflection::serialize_component_typed,
};
/// Build a Set operation (LWW) from a component
///
/// Serializes the component using Bevy's reflection system and creates a
/// ComponentOp::Set for Last-Write-Wins synchronization. Automatically uses
/// blob storage for components >64KB.
///
/// # Parameters
///
/// - `component`: The component to serialize
/// - `component_type`: Type path string
/// - `node_id`: Our node ID
/// - `vector_clock`: Current vector clock
/// - `type_registry`: Bevy's type registry
/// - `blob_store`: Optional blob store for large components
///
/// # Returns
///
/// A ComponentOp::Set ready to be broadcast
pub fn build_set_operation(
component: &dyn Reflect,
component_type: String,
node_id: NodeId,
vector_clock: VectorClock,
type_registry: &TypeRegistry,
blob_store: Option<&BlobStore>,
) -> Result<ComponentOp> {
// Serialize the component
let serialized = serialize_component_typed(component, type_registry)?;
// Create component data (inline or blob)
let data = if let Some(store) = blob_store {
create_component_data(serialized, store)?
} else {
ComponentData::Inline(serialized)
};
// Build the operation
let builder = ComponentOpBuilder::new(node_id, vector_clock);
Ok(builder.set(component_type, data))
}
/// Build Set operations for all components on an entity
///
/// This iterates over all components with reflection data and creates Set
/// operations for each one. Automatically uses blob storage for large components.
///
/// # Parameters
///
/// - `entity`: The entity to serialize
/// - `world`: Bevy world
/// - `node_id`: Our node ID
/// - `vector_clock`: Current vector clock
/// - `type_registry`: Bevy's type registry
/// - `blob_store`: Optional blob store for large components
///
/// # Returns
///
/// Vector of ComponentOp::Set operations, one per component
pub fn build_entity_operations(
entity: Entity,
world: &World,
node_id: NodeId,
vector_clock: VectorClock,
type_registry: &TypeRegistry,
blob_store: Option<&BlobStore>,
) -> Vec<ComponentOp> {
let mut operations = Vec::new();
let entity_ref = world.entity(entity);
debug!("build_entity_operations: Building operations for entity {:?}", entity);
// Iterate over all type registrations
for registration in type_registry.iter() {
// Skip if no ReflectComponent data
let Some(reflect_component) = registration.data::<ReflectComponent>() else {
continue;
};
// Get the type path
let type_path = registration.type_info().type_path();
// Skip certain 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_typed(reflected, type_registry) {
// Create component data (inline or blob)
let data = if let Some(store) = blob_store {
if let Ok(component_data) = create_component_data(serialized, store) {
component_data
} else {
continue; // Skip this component if blob storage fails
}
} else {
ComponentData::Inline(serialized)
};
// Build the operation
let mut clock = vector_clock.clone();
clock.increment(node_id);
operations.push(ComponentOp::Set {
component_type: type_path.to_string(),
data,
vector_clock: clock.clone(),
});
debug!(" ✓ Added Set operation for {}", type_path);
}
}
}
debug!(
"build_entity_operations: Built {} operations for entity {:?}",
operations.len(),
entity
);
operations
}
/// Build a Set operation for Transform component specifically
///
/// This is a helper for the common case of synchronizing Transform changes.
///
/// # Example
///
/// ```
/// use bevy::prelude::*;
/// use lib::networking::{build_transform_operation, VectorClock};
/// use uuid::Uuid;
///
/// # fn example(transform: &Transform, type_registry: &bevy::reflect::TypeRegistry) {
/// let node_id = Uuid::new_v4();
/// let clock = VectorClock::new();
///
/// let op = build_transform_operation(transform, node_id, clock, type_registry, None).unwrap();
/// # }
/// ```
pub fn build_transform_operation(
transform: &Transform,
node_id: NodeId,
vector_clock: VectorClock,
type_registry: &TypeRegistry,
blob_store: Option<&BlobStore>,
) -> Result<ComponentOp> {
// Use reflection to serialize Transform
let serialized = serialize_component_typed(transform.as_reflect(), type_registry)?;
// Create component data (inline or blob)
let data = if let Some(store) = blob_store {
create_component_data(serialized, store)?
} else {
ComponentData::Inline(serialized)
};
let builder = ComponentOpBuilder::new(node_id, vector_clock);
Ok(builder.set("bevy_transform::components::transform::Transform".to_string(), data))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_transform_operation() {
let mut type_registry = TypeRegistry::new();
type_registry.register::<Transform>();
let transform = Transform::default();
let node_id = uuid::Uuid::new_v4();
let clock = VectorClock::new();
let op = build_transform_operation(&transform, node_id, clock, &type_registry, None).unwrap();
assert!(op.is_set());
assert_eq!(
op.component_type(),
Some("bevy_transform::components::transform::Transform")
);
assert_eq!(op.vector_clock().get(node_id), 1);
}
#[test]
fn test_build_entity_operations() {
let mut world = World::new();
let mut type_registry = TypeRegistry::new();
// Register Transform
type_registry.register::<Transform>();
// Spawn entity with Transform
let entity = world
.spawn(Transform::from_xyz(1.0, 2.0, 3.0))
.id();
let node_id = uuid::Uuid::new_v4();
let clock = VectorClock::new();
let ops = build_entity_operations(entity, &world, node_id, clock, &type_registry, None);
// Should have at least Transform operation
assert!(!ops.is_empty());
assert!(ops.iter().all(|op| op.is_set()));
}
#[test]
fn test_vector_clock_increment() {
let mut type_registry = TypeRegistry::new();
type_registry.register::<Transform>();
let transform = Transform::default();
let node_id = uuid::Uuid::new_v4();
let mut clock = VectorClock::new();
let op1 = build_transform_operation(&transform, node_id, clock.clone(), &type_registry, None).unwrap();
assert_eq!(op1.vector_clock().get(node_id), 1);
clock.increment(node_id);
let op2 = build_transform_operation(&transform, node_id, clock.clone(), &type_registry, None).unwrap();
assert_eq!(op2.vector_clock().get(node_id), 2);
}
}