diff --git a/framework_crates/bones_ecs/src/components.rs b/framework_crates/bones_ecs/src/components.rs index caf6b7ef97..bd8dc9a000 100644 --- a/framework_crates/bones_ecs/src/components.rs +++ b/framework_crates/bones_ecs/src/components.rs @@ -2,7 +2,7 @@ use fxhash::FxHasher; use once_map::OnceMap; -use std::{any::Any, sync::Arc}; +use std::sync::Arc; use crate::prelude::*; @@ -81,12 +81,16 @@ impl DesyncHash for ComponentStores { } } -impl BuildDesyncNode for ComponentStores { +impl BuildDesyncNode for ComponentStores { fn desync_tree_node( &self, include_unhashable: bool, ) -> DefaultDesyncTreeNode { let mut any_hashable = false; + + // We get the Name component store so we can lookup entity names and set those on component leaves. + let names = self.get::().borrow(); + let mut child_nodes = self .components .read_only_view() @@ -104,7 +108,24 @@ impl BuildDesyncNode for ComponentStores { } if include_unhashable || is_hashable { - let child_node = component_store.desync_tree_node::(include_unhashable); + let mut child_node = component_store.desync_tree_node::(include_unhashable); + + // Our child here is a component store, and its children are component leaves. + // Iterate through children, retrieve metadata storing entity_idx if set, and use this + // to update the node's name from Name component. + // + // This is fairly hacky, but should be good enough for now. + for component_node in child_node.children_mut().iter_mut() { + if let DesyncNodeMetadata::Component { entity_idx } = + component_node.metadata() + { + // Constructing Entity with fake generation is bit of a hack - but component store does not + // use generation, only the index. + if let Some(name) = names.get(Entity::new(*entity_idx, 0)) { + component_node.set_name(name.0.clone()); + } + } + } return Some(child_node); } @@ -126,7 +147,12 @@ impl BuildDesyncNode for ComponentStores { None }; - DefaultDesyncTreeNode::new(hash, Some("Components".into()), child_nodes) + DefaultDesyncTreeNode::new( + hash, + Some("Components".into()), + child_nodes, + DesyncNodeMetadata::None, + ) } } diff --git a/framework_crates/bones_ecs/src/components/untyped.rs b/framework_crates/bones_ecs/src/components/untyped.rs index ab3d58c917..efa1a7c13d 100644 --- a/framework_crates/bones_ecs/src/components/untyped.rs +++ b/framework_crates/bones_ecs/src/components/untyped.rs @@ -82,29 +82,42 @@ impl DesyncHash for UntypedComponentStore { } } -impl BuildDesyncNode for UntypedComponentStore { +impl BuildDesyncNode for UntypedComponentStore { fn desync_tree_node( &self, _include_unhashable: bool, ) -> DefaultDesyncTreeNode { let mut hasher = H::default(); - let child_nodes: Vec = self - .iter() - .map(|component| -> DefaultDesyncTreeNode { - let hash = if component - .schema() - .type_data - .get::() - .is_some() - { - // Update parent node hash from data - DesyncHash::hash(&component, &mut hasher); - Some(component.compute_hash::()) - } else { - None - }; - - DefaultDesyncTreeNode::new(hash, None, vec![]) + + // Iterate over components by index so we can save entity ID. + let iter = 0..self.bitset().bit_len(); + let child_nodes: Vec = iter + .filter_map(|entity_idx| -> Option { + if let Some(component) = self.get_idx(entity_idx) { + let hash = if component + .schema() + .type_data + .get::() + .is_some() + { + // Update parent node hash from data + DesyncHash::hash(&component, &mut hasher); + Some(component.compute_hash::()) + } else { + None + }; + + return Some(DefaultDesyncTreeNode::new( + hash, + None, + vec![], + DesyncNodeMetadata::Component { + entity_idx: entity_idx as u32, + }, + )); + } + + None }) .collect(); @@ -114,7 +127,12 @@ impl BuildDesyncNode for UntypedComponentStore { None }; - DefaultDesyncTreeNode::new(hash, Some(self.schema().full_name.to_string()), child_nodes) + DefaultDesyncTreeNode::new( + hash, + Some(self.schema().full_name.to_string()), + child_nodes, + DesyncNodeMetadata::None, + ) } } diff --git a/framework_crates/bones_ecs/src/entities.rs b/framework_crates/bones_ecs/src/entities.rs index 50f8f5ecdd..7ec80c453e 100644 --- a/framework_crates/bones_ecs/src/entities.rs +++ b/framework_crates/bones_ecs/src/entities.rs @@ -85,6 +85,22 @@ impl Default for Entities { } } +/// Utility component storing a name for entity +#[derive(HasSchema, Clone, Debug)] +pub struct Name(pub String); + +impl Default for Name { + fn default() -> Self { + Self("Unnamed".to_string()) + } +} + +impl From<&str> for Name { + fn from(value: &str) -> Self { + Self(value.into()) + } +} + /// A type representing a component-joining entity query. pub trait QueryItem { /// The type of iterator this query item creates diff --git a/framework_crates/bones_ecs/src/resources.rs b/framework_crates/bones_ecs/src/resources.rs index 607710bb46..06a72e7a21 100644 --- a/framework_crates/bones_ecs/src/resources.rs +++ b/framework_crates/bones_ecs/src/resources.rs @@ -34,7 +34,7 @@ impl DesyncHash for UntypedResource { } } -impl BuildDesyncNode for UntypedResource { +impl BuildDesyncNode for UntypedResource { fn desync_tree_node( &self, _include_unhashable: bool, @@ -49,10 +49,10 @@ impl BuildDesyncNode for UntypedResource { } else { None }; - return DefaultDesyncTreeNode::new(hash, name, vec![]); + return DefaultDesyncTreeNode::new(hash, name, vec![], DesyncNodeMetadata::None); } - DefaultDesyncTreeNode::new(None, name, vec![]) + DefaultDesyncTreeNode::new(None, name, vec![], DesyncNodeMetadata::None) } } @@ -197,7 +197,7 @@ impl DesyncHash for UntypedResources { } } -impl BuildDesyncNode for UntypedResources { +impl BuildDesyncNode for UntypedResources { fn desync_tree_node( &self, include_unhashable: bool, @@ -230,7 +230,12 @@ impl BuildDesyncNode for UntypedResources { } } - DefaultDesyncTreeNode::new(Some(hasher.finish()), Some("Resources".into()), child_nodes) + DefaultDesyncTreeNode::new( + Some(hasher.finish()), + Some("Resources".into()), + child_nodes, + DesyncNodeMetadata::None, + ) } } @@ -314,7 +319,7 @@ impl DesyncHash for Resources { } } -impl BuildDesyncNode for Resources { +impl BuildDesyncNode for Resources { fn desync_tree_node( &self, include_unhashable: bool, diff --git a/framework_crates/bones_ecs/src/world.rs b/framework_crates/bones_ecs/src/world.rs index eba09cb7ee..efc7de3d9a 100644 --- a/framework_crates/bones_ecs/src/world.rs +++ b/framework_crates/bones_ecs/src/world.rs @@ -43,7 +43,7 @@ impl DesyncHash for World { } } -impl BuildDesyncNode for World { +impl BuildDesyncNode for World { fn desync_tree_node( &self, include_unhashable: bool, @@ -64,7 +64,12 @@ impl BuildDesyncNode for World { } child_nodes.push(resources_node); - DefaultDesyncTreeNode::new(Some(hasher.finish()), Some("World".into()), child_nodes) + DefaultDesyncTreeNode::new( + Some(hasher.finish()), + Some("World".into()), + child_nodes, + DesyncNodeMetadata::None, + ) } } @@ -246,6 +251,22 @@ impl World { // Always maintain to clean up any killed entities self.maintain(); } + /// Build [`DefaultDesyncTree`] from [`World`]. + /// + /// `include_unhashable` sets whether components or resources be included as non-contributing nodes + /// in tree, to see what could be opted-in to desync hashing. + /// + /// # Panics + /// + /// This will immutably borrow all components and resources, if any are mutably borrowed, this will panic. + pub fn desync_hash_tree( + &self, + include_unhashable: bool, + ) -> DefaultDesyncTree { + let root = self.desync_tree_node::(include_unhashable); + + DefaultDesyncTree::from_root(root) + } } /// Creates an instance of the type this trait is implemented for diff --git a/framework_crates/bones_framework/src/networking/desync.rs b/framework_crates/bones_framework/src/networking/desync.rs index b5f8156a6b..131164eaf6 100644 --- a/framework_crates/bones_framework/src/networking/desync.rs +++ b/framework_crates/bones_framework/src/networking/desync.rs @@ -7,6 +7,7 @@ use bones_lib::{ecs::World, prelude::default}; /// but is private so cannot be used directly. const MAX_DESYNC_HISTORY_BUFFER: usize = 32; +/// Settings for desync detection #[derive(Clone)] pub struct DetectDesyncs { /// Interval in frames of how often to hash state and check for desync with other clients. diff --git a/framework_crates/bones_utils/Cargo.toml b/framework_crates/bones_utils/Cargo.toml index b240148649..8a76a35c73 100644 --- a/framework_crates/bones_utils/Cargo.toml +++ b/framework_crates/bones_utils/Cargo.toml @@ -12,22 +12,23 @@ keywords.workspace = true [features] default = ["ulid"] -glam = ["dep:glam"] -serde = ["dep:serde", "hashbrown/serde"] -ulid = ["dep:ulid", "instant", "turborand"] +glam = ["dep:glam"] +serde = ["dep:serde", "hashbrown/serde"] +ulid = ["dep:ulid", "instant", "turborand"] [dependencies] bones_utils_macros = { version = "0.4", path = "./macros" } fxhash = { workspace = true } hashbrown = { workspace = true } +tree_iterators_rs = { version = "1.2.1" } # Optional instant = { version = "0.1", features = ["wasm-bindgen"], optional = true } serde = { version = "1.0", optional = true } turborand = { version = "0.10", optional = true } ulid = { version = "1.0", optional = true } -glam = { version = "0.24", optional = true } -paste = { version = "1.0" } +glam = { version = "0.24", optional = true } +paste = { version = "1.0" } # Make sure that the getrandom package, used in `ulid` works on web # when compiling for WASM. diff --git a/framework_crates/bones_utils/src/desync_hash.rs b/framework_crates/bones_utils/src/desync_hash.rs index f3a8d6e14b..afb37739a3 100644 --- a/framework_crates/bones_utils/src/desync_hash.rs +++ b/framework_crates/bones_utils/src/desync_hash.rs @@ -2,14 +2,15 @@ //! //! In order to use [`DesyncHash`] with [`glam`] types, the "glam" feature flag must be used. -use std::time::Duration; - +use std::{slice::Iter, time::Duration}; use ustr::Ustr; +pub use tree_iterators_rs::prelude::BorrowedTreeNode; + /// [`DesyncHash`] is used to hash type and compare over network to detect desyncs. /// /// In order to opt in a `HasSchema` Component or Resource to be included in hash of World in networked session, -/// `#[net]` or `#[derive_type_data(SchemaDesyncHas)]` must also be included. +/// `#[net]` or `#[derive_type_data(SchemaDesyncHash)]` must also be included. /// /// Fields may be excluded from hash by using attribute: `#[desync_exclude]` pub trait DesyncHash { @@ -33,46 +34,127 @@ impl DesyncHashImpl for T { } /// Tree of desync hashes -pub trait DesyncTree: Clone { +pub trait DesyncTree: Clone { + /// Node type type Node; - fn get_hash(&self) -> Option; + /// Get root hash of tree + fn get_hash(&self) -> Option; - fn name(&self) -> &Option; + /// Get root node + fn root(&self) -> &Self::Node; + /// make tree from root node fn from_root(root: Self::Node) -> Self; } /// [`DesyncTree`] node trait, built from children and hash. A node is effectively a sub-tree, /// as we build the tree bottom-up. -pub trait DesyncTreeNode: Clone + PartialEq + Eq { - fn new(hash: Option, name: Option, children: Vec) -> Self; +pub trait DesyncTreeNode: Clone { + /// Get node hash + fn get_hash(&self) -> Option; + + /// Get children + fn children(&self) -> &Vec; - fn get_hash(&self) -> Option; + /// Get children mut + fn children_mut(&mut self) -> &mut Vec; } /// Implement to allow type to create a [`DesyncTreeNode`] containing hash built from children. -pub trait BuildDesyncNode -where - N: DesyncTreeNode, -{ +pub trait BuildDesyncNode { /// `include_unhashable` sets whether components or resources be included as non-contributing nodes /// in tree, to see what could be opted-in. - fn desync_tree_node(&self, include_unhashable: bool) -> N; + fn desync_tree_node( + &self, + include_unhashable: bool, + ) -> DefaultDesyncTreeNode; +} + +/// Metadata optionally included with ['DesyncTreeNode`]. +#[derive(Copy, Clone, Default)] +pub enum DesyncNodeMetadata { + /// No additional metadata + #[default] + None, + /// Node is a component + Component { + /// Entity idx of component + entity_idx: u32, + }, } /// Default impl for [`DesyncTreeNode`]. -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct DefaultDesyncTreeNode { name: Option, hash: Option, - children: Vec, + children: Vec, + + /// Some userdata that can be included in node. + #[cfg_attr(feature = "serde", serde(skip))] + metadata: DesyncNodeMetadata, } +impl DefaultDesyncTreeNode { + /// Create new node + pub fn new( + hash: Option, + name: Option, + children: Vec, + metadata: DesyncNodeMetadata, + ) -> Self { + Self { + name, + hash, + children, + metadata, + } + } + + /// Get node metadata + pub fn metadata(&self) -> &DesyncNodeMetadata { + &self.metadata + } + + /// Name of node + pub fn name(&self) -> &Option { + &self.name + } + + /// Set the name of node + pub fn set_name(&mut self, name: String) { + self.name = Some(name); + } + + /// Get node hash + pub fn get_hash(&self) -> Option { + self.hash + } + + /// Get children + pub fn children(&self) -> &Vec { + &self.children + } + + /// Get children mut + pub fn children_mut(&mut self) -> &mut Vec { + &mut self.children + } +} + +impl PartialEq for DefaultDesyncTreeNode { + fn eq(&self, other: &Self) -> bool { + self.hash == other.hash + } +} + +impl Eq for DefaultDesyncTreeNode {} + impl PartialOrd for DefaultDesyncTreeNode { fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) + Some(self.hash.cmp(&other.hash)) } } @@ -82,17 +164,16 @@ impl Ord for DefaultDesyncTreeNode { } } -impl DesyncTreeNode for DefaultDesyncTreeNode { - fn new(hash: Option, name: Option, children: Vec) -> Self { - Self { - name, - hash, - children, - } - } +/// Auto impl support for iterating over tree +impl<'a> BorrowedTreeNode<'a> for DefaultDesyncTreeNode { + type BorrowedValue = &'a Self; - fn get_hash(&self) -> Option { - self.hash + type BorrowedChildren = Iter<'a, DefaultDesyncTreeNode>; + + fn get_value_and_children_iter( + &'a self, + ) -> (Self::BorrowedValue, Option) { + (self, Some(self.children.iter())) } } @@ -109,15 +190,15 @@ impl From for DefaultDesyncTree { } } -impl DesyncTree for DefaultDesyncTree { +impl DesyncTree for DefaultDesyncTree { type Node = DefaultDesyncTreeNode; fn get_hash(&self) -> Option { self.root.get_hash() } - fn name(&self) -> &Option { - &self.root.name + fn root(&self) -> &Self::Node { + &self.root } fn from_root(root: Self::Node) -> Self {