Files
marathon/crates/lib/src/networking/entity_map.rs
Sienna Meridian Satterwhite 1504807afe code review results
Signed-off-by: Sienna Meridian Satterwhite <sienna@r3t.io>
2025-12-11 18:39:57 +00:00

440 lines
13 KiB
Rust

//! Bidirectional mapping between network IDs and Bevy entities
//!
//! This module provides efficient lookup in both directions:
//! - network_id → Entity (when receiving remote operations)
//! - Entity → network_id (when broadcasting local changes)
use std::collections::HashMap;
use bevy::prelude::*;
/// Bidirectional mapping between network IDs and Bevy entities
///
/// This resource maintains two HashMaps for O(1) lookup in both directions.
/// It's updated automatically by the networking systems when entities are
/// spawned or despawned.
///
/// # Thread Safety
///
/// This is a Bevy Resource, so it's automatically synchronized across systems.
///
/// # Example
///
/// ```
/// use bevy::prelude::*;
/// use lib::networking::{
/// NetworkEntityMap,
/// NetworkedEntity,
/// };
/// use uuid::Uuid;
///
/// fn example_system(mut map: ResMut<NetworkEntityMap>, query: Query<(Entity, &NetworkedEntity)>) {
/// // Register networked entities
/// for (entity, networked) in query.iter() {
/// map.insert(networked.network_id, entity);
/// }
///
/// // Later, look up by network ID
/// let network_id = Uuid::new_v4();
/// if let Some(entity) = map.get_entity(network_id) {
/// println!("Found entity: {:?}", entity);
/// }
/// }
/// ```
#[derive(Resource, Default, Debug)]
pub struct NetworkEntityMap {
/// Map from network ID to Bevy Entity
network_id_to_entity: HashMap<uuid::Uuid, Entity>,
/// Map from Bevy Entity to network ID
entity_to_network_id: HashMap<Entity, uuid::Uuid>,
}
impl NetworkEntityMap {
/// Create a new empty entity map
pub fn new() -> Self {
Self {
network_id_to_entity: HashMap::new(),
entity_to_network_id: HashMap::new(),
}
}
/// Insert a bidirectional mapping
///
/// If the network_id or entity already exists in the map, the old mapping
/// is removed first to maintain consistency.
///
/// # Example
///
/// ```
/// use bevy::prelude::*;
/// use lib::networking::NetworkEntityMap;
/// use uuid::Uuid;
///
/// # let mut world = World::new();
/// # let entity = world.spawn_empty().id();
/// let mut map = NetworkEntityMap::new();
/// let network_id = Uuid::new_v4();
///
/// map.insert(network_id, entity);
/// assert_eq!(map.get_entity(network_id), Some(entity));
/// assert_eq!(map.get_network_id(entity), Some(network_id));
/// ```
pub fn insert(&mut self, network_id: uuid::Uuid, entity: Entity) {
// Remove old mappings if they exist
if let Some(old_entity) = self.network_id_to_entity.get(&network_id) {
self.entity_to_network_id.remove(old_entity);
}
if let Some(old_network_id) = self.entity_to_network_id.get(&entity) {
self.network_id_to_entity.remove(old_network_id);
}
// Insert new mappings
self.network_id_to_entity.insert(network_id, entity);
self.entity_to_network_id.insert(entity, network_id);
}
/// Get the Bevy Entity for a network ID
///
/// Returns None if the network ID is not in the map.
///
/// # Example
///
/// ```
/// use bevy::prelude::*;
/// use lib::networking::NetworkEntityMap;
/// use uuid::Uuid;
///
/// # let mut world = World::new();
/// # let entity = world.spawn_empty().id();
/// let mut map = NetworkEntityMap::new();
/// let network_id = Uuid::new_v4();
///
/// map.insert(network_id, entity);
/// assert_eq!(map.get_entity(network_id), Some(entity));
///
/// let unknown_id = Uuid::new_v4();
/// assert_eq!(map.get_entity(unknown_id), None);
/// ```
pub fn get_entity(&self, network_id: uuid::Uuid) -> Option<Entity> {
self.network_id_to_entity.get(&network_id).copied()
}
/// Get the network ID for a Bevy Entity
///
/// Returns None if the entity is not in the map.
///
/// # Example
///
/// ```
/// use bevy::prelude::*;
/// use lib::networking::NetworkEntityMap;
/// use uuid::Uuid;
///
/// # let mut world = World::new();
/// # let entity = world.spawn_empty().id();
/// let mut map = NetworkEntityMap::new();
/// let network_id = Uuid::new_v4();
///
/// map.insert(network_id, entity);
/// assert_eq!(map.get_network_id(entity), Some(network_id));
///
/// # let unknown_entity = world.spawn_empty().id();
/// assert_eq!(map.get_network_id(unknown_entity), None);
/// ```
pub fn get_network_id(&self, entity: Entity) -> Option<uuid::Uuid> {
self.entity_to_network_id.get(&entity).copied()
}
/// Remove a mapping by network ID
///
/// Returns the Entity that was mapped to this network ID, if any.
///
/// # Example
///
/// ```
/// use bevy::prelude::*;
/// use lib::networking::NetworkEntityMap;
/// use uuid::Uuid;
///
/// # let mut world = World::new();
/// # let entity = world.spawn_empty().id();
/// let mut map = NetworkEntityMap::new();
/// let network_id = Uuid::new_v4();
///
/// map.insert(network_id, entity);
/// assert_eq!(map.remove_by_network_id(network_id), Some(entity));
/// assert_eq!(map.get_entity(network_id), None);
/// ```
pub fn remove_by_network_id(&mut self, network_id: uuid::Uuid) -> Option<Entity> {
if let Some(entity) = self.network_id_to_entity.remove(&network_id) {
self.entity_to_network_id.remove(&entity);
Some(entity)
} else {
None
}
}
/// Remove a mapping by Entity
///
/// Returns the network ID that was mapped to this entity, if any.
///
/// # Example
///
/// ```
/// use bevy::prelude::*;
/// use lib::networking::NetworkEntityMap;
/// use uuid::Uuid;
///
/// # let mut world = World::new();
/// # let entity = world.spawn_empty().id();
/// let mut map = NetworkEntityMap::new();
/// let network_id = Uuid::new_v4();
///
/// map.insert(network_id, entity);
/// assert_eq!(map.remove_by_entity(entity), Some(network_id));
/// assert_eq!(map.get_network_id(entity), None);
/// ```
pub fn remove_by_entity(&mut self, entity: Entity) -> Option<uuid::Uuid> {
if let Some(network_id) = self.entity_to_network_id.remove(&entity) {
self.network_id_to_entity.remove(&network_id);
Some(network_id)
} else {
None
}
}
/// Check if a network ID exists in the map
pub fn contains_network_id(&self, network_id: uuid::Uuid) -> bool {
self.network_id_to_entity.contains_key(&network_id)
}
/// Check if an entity exists in the map
pub fn contains_entity(&self, entity: Entity) -> bool {
self.entity_to_network_id.contains_key(&entity)
}
/// Get the number of mapped entities
pub fn len(&self) -> usize {
self.network_id_to_entity.len()
}
/// Check if the map is empty
pub fn is_empty(&self) -> bool {
self.network_id_to_entity.is_empty()
}
/// Clear all mappings
pub fn clear(&mut self) {
self.network_id_to_entity.clear();
self.entity_to_network_id.clear();
}
/// Get an iterator over all (network_id, entity) pairs
pub fn iter(&self) -> impl Iterator<Item = (&uuid::Uuid, &Entity)> {
self.network_id_to_entity.iter()
}
/// Get all network IDs
pub fn network_ids(&self) -> impl Iterator<Item = &uuid::Uuid> {
self.network_id_to_entity.keys()
}
/// Get all entities
pub fn entities(&self) -> impl Iterator<Item = &Entity> {
self.entity_to_network_id.keys()
}
}
/// System to automatically register NetworkedEntity components in the map
///
/// This system runs in PostUpdate to catch newly spawned networked entities
/// and add them to the NetworkEntityMap.
///
/// Add this to your app:
/// ```no_run
/// use bevy::prelude::*;
/// use lib::networking::register_networked_entities_system;
///
/// App::new().add_systems(PostUpdate, register_networked_entities_system);
/// ```
pub fn register_networked_entities_system(
mut map: ResMut<NetworkEntityMap>,
query: Query<
(Entity, &crate::networking::NetworkedEntity),
Added<crate::networking::NetworkedEntity>,
>,
) {
for (entity, networked) in query.iter() {
map.insert(networked.network_id, entity);
}
}
/// System to automatically unregister despawned entities from the map
///
/// This system cleans up the NetworkEntityMap when networked entities are
/// despawned.
///
/// Add this to your app:
/// ```no_run
/// use bevy::prelude::*;
/// use lib::networking::cleanup_despawned_entities_system;
///
/// App::new().add_systems(PostUpdate, cleanup_despawned_entities_system);
/// ```
pub fn cleanup_despawned_entities_system(
mut map: ResMut<NetworkEntityMap>,
mut removed: RemovedComponents<crate::networking::NetworkedEntity>,
) {
for entity in removed.read() {
map.remove_by_entity(entity);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_insert_and_get() {
let mut map = NetworkEntityMap::new();
let mut world = World::new();
let entity = world.spawn_empty().id();
let network_id = uuid::Uuid::new_v4();
map.insert(network_id, entity);
assert_eq!(map.get_entity(network_id), Some(entity));
assert_eq!(map.get_network_id(entity), Some(network_id));
}
#[test]
fn test_get_nonexistent() {
let map = NetworkEntityMap::new();
let mut world = World::new();
let entity = world.spawn_empty().id();
let network_id = uuid::Uuid::new_v4();
assert_eq!(map.get_entity(network_id), None);
assert_eq!(map.get_network_id(entity), None);
}
#[test]
fn test_remove_by_network_id() {
let mut map = NetworkEntityMap::new();
let mut world = World::new();
let entity = world.spawn_empty().id();
let network_id = uuid::Uuid::new_v4();
map.insert(network_id, entity);
assert_eq!(map.remove_by_network_id(network_id), Some(entity));
assert_eq!(map.get_entity(network_id), None);
assert_eq!(map.get_network_id(entity), None);
}
#[test]
fn test_remove_by_entity() {
let mut map = NetworkEntityMap::new();
let mut world = World::new();
let entity = world.spawn_empty().id();
let network_id = uuid::Uuid::new_v4();
map.insert(network_id, entity);
assert_eq!(map.remove_by_entity(entity), Some(network_id));
assert_eq!(map.get_entity(network_id), None);
assert_eq!(map.get_network_id(entity), None);
}
#[test]
fn test_contains() {
let mut map = NetworkEntityMap::new();
let mut world = World::new();
let entity = world.spawn_empty().id();
let network_id = uuid::Uuid::new_v4();
assert!(!map.contains_network_id(network_id));
assert!(!map.contains_entity(entity));
map.insert(network_id, entity);
assert!(map.contains_network_id(network_id));
assert!(map.contains_entity(entity));
}
#[test]
fn test_len_and_is_empty() {
let mut map = NetworkEntityMap::new();
let mut world = World::new();
assert!(map.is_empty());
assert_eq!(map.len(), 0);
let entity1 = world.spawn_empty().id();
let id1 = uuid::Uuid::new_v4();
map.insert(id1, entity1);
assert!(!map.is_empty());
assert_eq!(map.len(), 1);
let entity2 = world.spawn_empty().id();
let id2 = uuid::Uuid::new_v4();
map.insert(id2, entity2);
assert_eq!(map.len(), 2);
}
#[test]
fn test_clear() {
let mut map = NetworkEntityMap::new();
let mut world = World::new();
let entity = world.spawn_empty().id();
let network_id = uuid::Uuid::new_v4();
map.insert(network_id, entity);
assert_eq!(map.len(), 1);
map.clear();
assert!(map.is_empty());
}
#[test]
fn test_insert_overwrites_old_mapping() {
let mut map = NetworkEntityMap::new();
let mut world = World::new();
let entity1 = world.spawn_empty().id();
let entity2 = world.spawn_empty().id();
let network_id = uuid::Uuid::new_v4();
// Insert first mapping
map.insert(network_id, entity1);
assert_eq!(map.get_entity(network_id), Some(entity1));
// Insert same network_id with different entity
map.insert(network_id, entity2);
assert_eq!(map.get_entity(network_id), Some(entity2));
assert_eq!(map.get_network_id(entity1), None); // Old mapping removed
assert_eq!(map.len(), 1); // Still only one mapping
}
#[test]
fn test_iter() {
let mut map = NetworkEntityMap::new();
let mut world = World::new();
let entity1 = world.spawn_empty().id();
let entity2 = world.spawn_empty().id();
let id1 = uuid::Uuid::new_v4();
let id2 = uuid::Uuid::new_v4();
map.insert(id1, entity1);
map.insert(id2, entity2);
let mut count = 0;
for (network_id, entity) in map.iter() {
assert!(network_id == &id1 || network_id == &id2);
assert!(entity == &entity1 || entity == &entity2);
count += 1;
}
assert_eq!(count, 2);
}
}