Files
marathon/crates/libmarathon/src/networking/delta_generation.rs
Sienna Meridian Satterwhite 8ca02fd492 Add spawn/delete commands, fix session state and entity broadcast
- marathonctl now supports spawn/delete entity commands
- Fixed session state bug (was transitioning to Left every 5s)
- Fixed entity broadcast to detect Added<NetworkedEntity>
- Added AppCommandQueue pattern for app-level control commands

References: #131, #132
2025-12-24 12:53:50 +00:00

260 lines
8.0 KiB
Rust

//! Delta generation system for broadcasting entity changes
//!
//! This module implements the core delta generation logic that detects changed
//! entities and broadcasts EntityDelta messages.
use bevy::prelude::*;
use crate::networking::{
NetworkedEntity,
change_detection::LastSyncVersions,
gossip_bridge::GossipBridge,
messages::{
EntityDelta,
SyncMessage,
VersionedMessage,
},
operation_builder::build_entity_operations,
vector_clock::{
NodeId,
VectorClock,
},
};
/// Resource wrapping our node's vector clock
///
/// This tracks the logical time for our local operations.
#[derive(Resource)]
pub struct NodeVectorClock {
pub node_id: NodeId,
pub clock: VectorClock,
}
impl NodeVectorClock {
pub fn new(node_id: NodeId) -> Self {
Self {
node_id,
clock: VectorClock::new(),
}
}
/// Increment our clock for a new operation
pub fn tick(&mut self) -> u64 {
self.clock.increment(self.node_id)
}
/// Get current sequence number for our node
pub fn sequence(&self) -> u64 {
self.clock.get(self.node_id)
}
}
/// System to generate and broadcast EntityDelta messages
///
/// This system:
/// 1. Queries for Added<NetworkedEntity> or Changed<NetworkedEntity>
/// 2. Serializes all components on those entities
/// 3. Builds EntityDelta messages
/// 4. Broadcasts via GossipBridge
///
/// Add this to your app to enable delta broadcasting:
///
/// ```no_run
/// use bevy::prelude::*;
/// use libmarathon::networking::generate_delta_system;
///
/// App::new().add_systems(Update, generate_delta_system);
/// ```
pub fn generate_delta_system(world: &mut World) {
// Check if bridge exists
if world.get_resource::<GossipBridge>().is_none() {
return;
}
let changed_entities: Vec<(Entity, uuid::Uuid, uuid::Uuid)> = {
let mut query =
world.query_filtered::<(Entity, &NetworkedEntity), Or<(Added<NetworkedEntity>, Changed<NetworkedEntity>)>>();
query
.iter(world)
.map(|(entity, networked)| (entity, networked.network_id, networked.owner_node_id))
.collect()
};
if changed_entities.is_empty() {
return;
}
debug!(
"generate_delta_system: Processing {} changed entities",
changed_entities.len()
);
// Process each entity separately to avoid borrow conflicts
for (entity, network_id, _owner_node_id) in changed_entities {
// Phase 1: Check and update clocks, collect data
let mut system_state: bevy::ecs::system::SystemState<(
Res<GossipBridge>,
Res<crate::persistence::ComponentTypeRegistryResource>,
ResMut<NodeVectorClock>,
ResMut<LastSyncVersions>,
Option<ResMut<crate::networking::OperationLog>>,
)> = bevy::ecs::system::SystemState::new(world);
let (node_id, vector_clock, current_seq) = {
let (_, _, mut node_clock, last_versions, _) = system_state.get_mut(world);
// Check if we should sync this entity
let current_seq = node_clock.sequence();
if !last_versions.should_sync(network_id, current_seq) {
drop(last_versions);
drop(node_clock);
system_state.apply(world);
continue;
}
// Increment our vector clock
node_clock.tick();
(node_clock.node_id, node_clock.clock.clone(), current_seq)
};
// Phase 2: Build operations (needs world access without holding other borrows)
let operations = {
let type_registry_res = world.resource::<crate::persistence::ComponentTypeRegistryResource>();
let type_registry = type_registry_res.0;
build_entity_operations(
entity,
world,
node_id,
vector_clock.clone(),
type_registry,
None, // blob_store - will be added in later phases
)
};
if operations.is_empty() {
system_state.apply(world);
continue;
}
// Phase 3: Record, broadcast, and update
let delta = {
let (bridge, _, _, mut last_versions, mut operation_log) = system_state.get_mut(world);
// Create EntityDelta
let delta = EntityDelta::new(network_id, node_id, vector_clock.clone(), operations);
// Record in operation log for anti-entropy
if let Some(ref mut log) = operation_log {
log.record_operation(delta.clone());
}
// Wrap in VersionedMessage
let message = VersionedMessage::new(SyncMessage::EntityDelta {
entity_id: delta.entity_id,
node_id: delta.node_id,
vector_clock: delta.vector_clock.clone(),
operations: delta.operations.clone(),
});
// Broadcast
if let Err(e) = bridge.send(message) {
error!("Failed to broadcast EntityDelta: {}", e);
} else {
debug!(
"Broadcast EntityDelta for entity {:?} with {} operations",
network_id,
delta.operations.len()
);
last_versions.update(network_id, current_seq);
}
delta
};
// Phase 4: Update component vector clocks for local modifications
{
// Get type registry first before mutable borrow
let type_registry = {
let type_registry_res = world.resource::<crate::persistence::ComponentTypeRegistryResource>();
type_registry_res.0
};
if let Some(mut component_clocks) =
world.get_resource_mut::<crate::networking::ComponentVectorClocks>()
{
for op in &delta.operations {
if let crate::networking::ComponentOp::Set {
discriminant,
vector_clock: op_clock,
..
} = op
{
let component_type_name = type_registry.get_type_name(*discriminant)
.unwrap_or("unknown");
component_clocks.set(
network_id,
component_type_name.to_string(),
op_clock.clone(),
node_id,
);
debug!(
"Updated local vector clock for {} on entity {:?} (node_id: {:?})",
component_type_name, network_id, node_id
);
}
}
}
}
system_state.apply(world);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_node_vector_clock_creation() {
let node_id = uuid::Uuid::new_v4();
let clock = NodeVectorClock::new(node_id);
assert_eq!(clock.node_id, node_id);
assert_eq!(clock.sequence(), 0);
}
#[test]
fn test_node_vector_clock_tick() {
let node_id = uuid::Uuid::new_v4();
let mut clock = NodeVectorClock::new(node_id);
assert_eq!(clock.tick(), 1);
assert_eq!(clock.sequence(), 1);
assert_eq!(clock.tick(), 2);
assert_eq!(clock.sequence(), 2);
}
#[test]
fn test_node_vector_clock_multiple_nodes() {
let node1 = uuid::Uuid::new_v4();
let node2 = uuid::Uuid::new_v4();
let mut clock1 = NodeVectorClock::new(node1);
let mut clock2 = NodeVectorClock::new(node2);
clock1.tick();
clock2.tick();
assert_eq!(clock1.sequence(), 1);
assert_eq!(clock2.sequence(), 1);
// Merge clocks
clock1.clock.merge(&clock2.clock);
assert_eq!(clock1.clock.get(node1), 1);
assert_eq!(clock1.clock.get(node2), 1);
}
}