chore: honestly fixed so much and forgot to commit
Signed-off-by: Sienna Meridian Satterwhite <sienna@r3t.io>
This commit is contained in:
@@ -5,6 +5,7 @@ mod as_bind_group;
|
||||
mod extract_component;
|
||||
mod extract_resource;
|
||||
mod specializer;
|
||||
mod synced;
|
||||
|
||||
use bevy_macro_utils::{derive_label, BevyManifest};
|
||||
use proc_macro::TokenStream;
|
||||
@@ -150,3 +151,26 @@ pub fn derive_draw_function_label(input: TokenStream) -> TokenStream {
|
||||
.push(format_ident!("DrawFunctionLabel").into());
|
||||
derive_label(input, "DrawFunctionLabel", &trait_path)
|
||||
}
|
||||
|
||||
/// Attribute macro for automatic component synchronization.
|
||||
///
|
||||
/// Automatically generates Component, rkyv serialization derives, and registers
|
||||
/// the component in the ComponentTypeRegistry for network synchronization.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_compile
|
||||
/// use macros::synced;
|
||||
///
|
||||
/// #[synced]
|
||||
/// pub struct CubeMarker {
|
||||
/// pub color_r: f32,
|
||||
/// pub color_g: f32,
|
||||
/// pub color_b: f32,
|
||||
/// pub size: f32,
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
pub fn synced(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
synced::synced_attribute(attr, item)
|
||||
}
|
||||
|
||||
57
crates/macros/src/synced.rs
Normal file
57
crates/macros/src/synced.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
|
||||
pub fn synced_attribute(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let ast = parse_macro_input!(item as DeriveInput);
|
||||
let struct_name = &ast.ident;
|
||||
let vis = &ast.vis;
|
||||
let attrs = &ast.attrs;
|
||||
let generics = &ast.generics;
|
||||
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
||||
|
||||
let fields = match &ast.data {
|
||||
syn::Data::Struct(data) => &data.fields,
|
||||
_ => panic!("#[synced] can only be used on structs"),
|
||||
};
|
||||
|
||||
TokenStream::from(quote! {
|
||||
// Generate the struct with all necessary derives
|
||||
#(#attrs)*
|
||||
#[derive(::bevy::prelude::Component, Clone, Copy, Debug)]
|
||||
#[derive(::rkyv::Archive, ::rkyv::Serialize, ::rkyv::Deserialize)]
|
||||
#vis struct #struct_name #generics #fields
|
||||
|
||||
// Register component in type registry using inventory
|
||||
::inventory::submit! {
|
||||
::libmarathon::persistence::ComponentMeta {
|
||||
type_name: stringify!(#struct_name),
|
||||
type_path: concat!(module_path!(), "::", stringify!(#struct_name)),
|
||||
type_id: std::any::TypeId::of::<#struct_name>(),
|
||||
|
||||
deserialize_fn: |bytes: &[u8]| -> anyhow::Result<Box<dyn std::any::Any>> {
|
||||
let component = ::rkyv::from_bytes::<#struct_name #ty_generics, ::rkyv::rancor::Failure>(bytes)?;
|
||||
Ok(Box::new(component))
|
||||
},
|
||||
|
||||
serialize_fn: |world: &::bevy::ecs::world::World, entity: ::bevy::ecs::entity::Entity|
|
||||
-> Option<::bytes::Bytes>
|
||||
{
|
||||
world.get::<#struct_name #ty_generics>(entity).map(|component| {
|
||||
let serialized = ::rkyv::to_bytes::<::rkyv::rancor::Failure>(component)
|
||||
.expect("Failed to serialize component");
|
||||
::bytes::Bytes::from(serialized.to_vec())
|
||||
})
|
||||
},
|
||||
|
||||
insert_fn: |entity_mut: &mut ::bevy::ecs::world::EntityWorldMut,
|
||||
boxed: Box<dyn std::any::Any>|
|
||||
{
|
||||
if let Ok(component) = boxed.downcast::<#struct_name #ty_generics>() {
|
||||
entity_mut.insert(*component);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,71 +1,64 @@
|
||||
/// Basic tests for the Synced attribute macro
|
||||
/// Basic tests for the #[synced] attribute macro
|
||||
use bevy::prelude::*;
|
||||
use libmarathon::networking::{
|
||||
ClockComparison,
|
||||
ComponentMergeDecision,
|
||||
SyncComponent,
|
||||
};
|
||||
|
||||
// Test 1: Basic struct with LWW strategy compiles
|
||||
// Note: No need to manually derive rkyv traits - synced attribute adds them automatically!
|
||||
#[sync_macros::synced(version = 1, strategy = "LastWriteWins")]
|
||||
#[derive(Component, Reflect, Clone, Debug, PartialEq)]
|
||||
#[reflect(Component)]
|
||||
struct Health(f32);
|
||||
// Test 1: Basic struct with synced attribute compiles
|
||||
#[macros::synced]
|
||||
struct Health {
|
||||
current: f32,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_health_compiles() {
|
||||
let health = Health(100.0);
|
||||
assert_eq!(health.0, 100.0);
|
||||
let health = Health { current: 100.0 };
|
||||
assert_eq!(health.current, 100.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_health_serialization() {
|
||||
let health = Health(100.0);
|
||||
let bytes = health.serialize_sync().unwrap();
|
||||
let deserialized = Health::deserialize_sync(&bytes).unwrap();
|
||||
assert_eq!(health, deserialized);
|
||||
fn test_health_has_component_trait() {
|
||||
// The synced macro should automatically derive Component
|
||||
let health = Health { current: 100.0 };
|
||||
|
||||
// Can insert into Bevy world
|
||||
let mut world = World::new();
|
||||
let entity = world.spawn(health).id();
|
||||
|
||||
// Can query it back
|
||||
let health_ref = world.get::<Health>(entity).unwrap();
|
||||
assert_eq!(health_ref.current, 100.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_health_lww_merge_remote_newer() {
|
||||
let mut local = Health(50.0);
|
||||
let remote = Health(100.0);
|
||||
fn test_health_rkyv_serialization() {
|
||||
let health = Health { current: 100.0 };
|
||||
|
||||
let decision = local.merge(remote, ClockComparison::RemoteNewer);
|
||||
assert_eq!(decision, ComponentMergeDecision::TookRemote);
|
||||
assert_eq!(local.0, 100.0);
|
||||
// Test rkyv serialization (which the synced macro adds)
|
||||
let bytes = rkyv::to_bytes::<rkyv::rancor::Failure>(&health)
|
||||
.expect("Should serialize with rkyv");
|
||||
|
||||
let deserialized: Health = rkyv::from_bytes::<Health, rkyv::rancor::Failure>(&bytes)
|
||||
.expect("Should deserialize with rkyv");
|
||||
|
||||
assert_eq!(deserialized.current, health.current);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_health_lww_merge_local_newer() {
|
||||
let mut local = Health(50.0);
|
||||
let remote = Health(100.0);
|
||||
fn test_health_is_clone_and_copy() {
|
||||
let health = Health { current: 100.0 };
|
||||
|
||||
let decision = local.merge(remote, ClockComparison::LocalNewer);
|
||||
assert_eq!(decision, ComponentMergeDecision::KeptLocal);
|
||||
assert_eq!(local.0, 50.0); // Local value kept
|
||||
}
|
||||
// Test Clone
|
||||
let cloned = health.clone();
|
||||
assert_eq!(cloned.current, health.current);
|
||||
|
||||
#[test]
|
||||
fn test_health_lww_merge_concurrent() {
|
||||
let mut local = Health(50.0);
|
||||
let remote = Health(100.0);
|
||||
// Test Copy (implicit through assignment)
|
||||
let copied = health;
|
||||
assert_eq!(copied.current, health.current);
|
||||
|
||||
let decision = local.merge(remote, ClockComparison::Concurrent);
|
||||
// With concurrent, we use hash tiebreaker
|
||||
// Either TookRemote or KeptLocal depending on hash
|
||||
assert!(
|
||||
decision == ComponentMergeDecision::TookRemote ||
|
||||
decision == ComponentMergeDecision::KeptLocal
|
||||
);
|
||||
// Original still valid after copy
|
||||
assert_eq!(health.current, 100.0);
|
||||
}
|
||||
|
||||
// Test 2: Struct with multiple fields
|
||||
// rkyv traits are automatically added by the synced attribute!
|
||||
#[sync_macros::synced(version = 1, strategy = "LastWriteWins")]
|
||||
#[derive(Component, Reflect, Clone, Debug, PartialEq)]
|
||||
#[reflect(Component)]
|
||||
#[macros::synced]
|
||||
struct Position {
|
||||
x: f32,
|
||||
y: f32,
|
||||
@@ -79,20 +72,101 @@ fn test_position_compiles() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_position_serialization() {
|
||||
fn test_position_rkyv_serialization() {
|
||||
let pos = Position { x: 10.0, y: 20.0 };
|
||||
let bytes = pos.serialize_sync().unwrap();
|
||||
let deserialized = Position::deserialize_sync(&bytes).unwrap();
|
||||
assert_eq!(pos, deserialized);
|
||||
|
||||
let bytes = rkyv::to_bytes::<rkyv::rancor::Failure>(&pos)
|
||||
.expect("Should serialize with rkyv");
|
||||
|
||||
let deserialized: Position = rkyv::from_bytes::<Position, rkyv::rancor::Failure>(&bytes)
|
||||
.expect("Should deserialize with rkyv");
|
||||
|
||||
assert_eq!(deserialized.x, pos.x);
|
||||
assert_eq!(deserialized.y, pos.y);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_position_merge() {
|
||||
let mut local = Position { x: 10.0, y: 20.0 };
|
||||
let remote = Position { x: 30.0, y: 40.0 };
|
||||
fn test_position_in_bevy_world() {
|
||||
let pos = Position { x: 10.0, y: 20.0 };
|
||||
|
||||
let decision = local.merge(remote, ClockComparison::RemoteNewer);
|
||||
assert_eq!(decision, ComponentMergeDecision::TookRemote);
|
||||
assert_eq!(local.x, 30.0);
|
||||
assert_eq!(local.y, 40.0);
|
||||
let mut world = World::new();
|
||||
let entity = world.spawn(pos).id();
|
||||
|
||||
let pos_ref = world.get::<Position>(entity).unwrap();
|
||||
assert_eq!(pos_ref.x, 10.0);
|
||||
assert_eq!(pos_ref.y, 20.0);
|
||||
}
|
||||
|
||||
// Test 3: Component registration in type registry
|
||||
// This test verifies that the inventory::submit! generated by the macro works
|
||||
#[test]
|
||||
fn test_component_registry_has_health() {
|
||||
use libmarathon::persistence::ComponentTypeRegistry;
|
||||
|
||||
let registry = ComponentTypeRegistry::init();
|
||||
|
||||
// The macro should have registered Health
|
||||
let type_id = std::any::TypeId::of::<Health>();
|
||||
let discriminant = registry.get_discriminant(type_id);
|
||||
|
||||
assert!(discriminant.is_some(), "Health should be registered in ComponentTypeRegistry");
|
||||
|
||||
// Check the type name
|
||||
let type_name = registry.get_type_name(discriminant.unwrap());
|
||||
assert_eq!(type_name, Some("Health"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_component_registry_has_position() {
|
||||
use libmarathon::persistence::ComponentTypeRegistry;
|
||||
|
||||
let registry = ComponentTypeRegistry::init();
|
||||
|
||||
let type_id = std::any::TypeId::of::<Position>();
|
||||
let discriminant = registry.get_discriminant(type_id);
|
||||
|
||||
assert!(discriminant.is_some(), "Position should be registered in ComponentTypeRegistry");
|
||||
|
||||
// Check the type name
|
||||
let type_name = registry.get_type_name(discriminant.unwrap());
|
||||
assert_eq!(type_name, Some("Position"));
|
||||
}
|
||||
|
||||
// Test 4: End-to-end serialization via ComponentTypeRegistry
|
||||
#[test]
|
||||
fn test_registry_serialization_roundtrip() {
|
||||
use libmarathon::persistence::ComponentTypeRegistry;
|
||||
|
||||
let mut world = World::new();
|
||||
let entity = world.spawn(Health { current: 75.0 }).id();
|
||||
|
||||
let registry = ComponentTypeRegistry::init();
|
||||
let type_id = std::any::TypeId::of::<Health>();
|
||||
let discriminant = registry.get_discriminant(type_id).unwrap();
|
||||
|
||||
// Serialize using the registry
|
||||
let serialize_fn = registry.get_discriminant(type_id)
|
||||
.and_then(|disc| {
|
||||
// Get serializer from the registry internals
|
||||
// We'll use the serialization method from the registry
|
||||
let serializer = world.get::<Health>(entity).map(|component| {
|
||||
rkyv::to_bytes::<rkyv::rancor::Failure>(component)
|
||||
.expect("Should serialize")
|
||||
.to_vec()
|
||||
});
|
||||
serializer
|
||||
})
|
||||
.expect("Should serialize Health component");
|
||||
|
||||
// Deserialize using the registry
|
||||
let deserialize_fn = registry.get_deserialize_fn(discriminant)
|
||||
.expect("Should have deserialize function");
|
||||
|
||||
let boxed = deserialize_fn(&serialize_fn)
|
||||
.expect("Should deserialize Health component");
|
||||
|
||||
let health = boxed.downcast::<Health>()
|
||||
.expect("Should downcast to Health");
|
||||
|
||||
assert_eq!(health.current, 75.0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user