From b421aaf037287fb8aebeea818c265613f2102342 Mon Sep 17 00:00:00 2001 From: Sienna Meridian Satterwhite Date: Wed, 17 Dec 2025 20:11:31 +0000 Subject: [PATCH] cleaned up code Signed-off-by: Sienna Meridian Satterwhite --- crates/libmarathon/src/debug_ui/input.rs | 2 +- crates/libmarathon/src/debug_ui/output.rs | 4 +- crates/libmarathon/src/networking/locks.rs | 5 +- .../src/networking/message_dispatcher.rs | 3 - crates/libmarathon/src/networking/messages.rs | 5 +- .../libmarathon/src/networking/operations.rs | 5 +- crates/libmarathon/src/networking/rga.rs | 5 +- crates/libmarathon/src/networking/session.rs | 5 +- .../src/networking/vector_clock.rs | 5 +- .../src/platform/desktop/event_loop.rs | 2 +- .../src/platform/desktop/executor.rs | 13 +- .../libmarathon/src/platform/desktop/input.rs | 6 +- .../tests/sync_integration_headless.rs | 16 +- crates/sync-macros/src/lib.rs | 219 ++++++++++++++++-- crates/sync-macros/tests/basic_macro_test.rs | 15 +- 15 files changed, 220 insertions(+), 90 deletions(-) diff --git a/crates/libmarathon/src/debug_ui/input.rs b/crates/libmarathon/src/debug_ui/input.rs index e91bea5..5c2b860 100644 --- a/crates/libmarathon/src/debug_ui/input.rs +++ b/crates/libmarathon/src/debug_ui/input.rs @@ -181,7 +181,7 @@ impl WindowToEguiContextMap { // NOTE: We don't use bevy_winit since we own the event loop // event_loop_proxy: Res>, ) { - for (egui_context_entity, camera, egui_context) in added_contexts { + for (egui_context_entity, camera, _egui_context) in added_contexts { if let bevy::camera::RenderTarget::Window(window_ref) = camera.target && let Some(window_ref) = window_ref.normalize(primary_window.single().ok()) { diff --git a/crates/libmarathon/src/debug_ui/output.rs b/crates/libmarathon/src/debug_ui/output.rs index f2e27c4..bca62ed 100644 --- a/crates/libmarathon/src/debug_ui/output.rs +++ b/crates/libmarathon/src/debug_ui/output.rs @@ -35,7 +35,7 @@ pub fn process_output_system( egui_global_settings: Res, window_to_egui_context_map: Res, ) { - let mut should_request_redraw = false; + let mut _should_request_redraw = false; for (entity, mut context, mut full_output, mut render_output, mut egui_output, settings) in context_query.iter_mut() @@ -115,7 +115,7 @@ pub fn process_output_system( } let needs_repaint = !render_output.is_empty(); - should_request_redraw |= ctx.has_requested_repaint() && needs_repaint; + _should_request_redraw |= ctx.has_requested_repaint() && needs_repaint; } // NOTE: RequestRedraw not needed - we own winit and run unbounded (continuous redraws) diff --git a/crates/libmarathon/src/networking/locks.rs b/crates/libmarathon/src/networking/locks.rs index ed7978c..dd26adf 100644 --- a/crates/libmarathon/src/networking/locks.rs +++ b/crates/libmarathon/src/networking/locks.rs @@ -42,10 +42,7 @@ use std::{ }; use bevy::prelude::*; -use serde::{ - Deserialize, - Serialize, -}; + use uuid::Uuid; use crate::networking::{ diff --git a/crates/libmarathon/src/networking/message_dispatcher.rs b/crates/libmarathon/src/networking/message_dispatcher.rs index d64e28e..9c73360 100644 --- a/crates/libmarathon/src/networking/message_dispatcher.rs +++ b/crates/libmarathon/src/networking/message_dispatcher.rs @@ -5,7 +5,6 @@ //! dispatcher system polls once and routes messages to appropriate handlers. use bevy::{ - ecs::system::SystemState, prelude::*, }; @@ -13,14 +12,12 @@ use crate::networking::{ GossipBridge, JoinType, NetworkedEntity, - TombstoneRegistry, VersionedMessage, apply_entity_delta, apply_full_state, blob_support::BlobStore, build_missing_deltas, delta_generation::NodeVectorClock, - entity_map::NetworkEntityMap, messages::SyncMessage, operation_log::OperationLog, plugin::SessionSecret, diff --git a/crates/libmarathon/src/networking/messages.rs b/crates/libmarathon/src/networking/messages.rs index 892097a..20ba6cb 100644 --- a/crates/libmarathon/src/networking/messages.rs +++ b/crates/libmarathon/src/networking/messages.rs @@ -3,10 +3,7 @@ //! This module defines the protocol messages used for distributed //! synchronization according to RFC 0001. -use serde::{ - Deserialize, - Serialize, -}; + use crate::networking::{ locks::LockMessage, diff --git a/crates/libmarathon/src/networking/operations.rs b/crates/libmarathon/src/networking/operations.rs index e318882..620e32f 100644 --- a/crates/libmarathon/src/networking/operations.rs +++ b/crates/libmarathon/src/networking/operations.rs @@ -4,10 +4,7 @@ //! on components in the distributed system. Each operation type corresponds to //! a specific CRDT merge strategy. -use serde::{ - Deserialize, - Serialize, -}; + use crate::networking::{ messages::ComponentData, diff --git a/crates/libmarathon/src/networking/rga.rs b/crates/libmarathon/src/networking/rga.rs index 28b2a95..f15796a 100644 --- a/crates/libmarathon/src/networking/rga.rs +++ b/crates/libmarathon/src/networking/rga.rs @@ -41,10 +41,7 @@ use std::collections::HashMap; use bevy::prelude::*; -use serde::{ - Deserialize, - Serialize, -}; + use crate::networking::vector_clock::{ NodeId, diff --git a/crates/libmarathon/src/networking/session.rs b/crates/libmarathon/src/networking/session.rs index 240bd35..51c424b 100644 --- a/crates/libmarathon/src/networking/session.rs +++ b/crates/libmarathon/src/networking/session.rs @@ -6,10 +6,7 @@ use std::fmt; /// human-readable ! session codes, ALPN-based network isolation, and persistent /// session tracking. use bevy::prelude::*; -use serde::{ - Deserialize, - Serialize, -}; + use uuid::Uuid; use crate::networking::VectorClock; diff --git a/crates/libmarathon/src/networking/vector_clock.rs b/crates/libmarathon/src/networking/vector_clock.rs index 224d961..85bee33 100644 --- a/crates/libmarathon/src/networking/vector_clock.rs +++ b/crates/libmarathon/src/networking/vector_clock.rs @@ -5,10 +5,7 @@ use std::collections::HashMap; -use serde::{ - Deserialize, - Serialize, -}; + use crate::networking::error::{ NetworkingError, diff --git a/crates/libmarathon/src/platform/desktop/event_loop.rs b/crates/libmarathon/src/platform/desktop/event_loop.rs index 22016d0..dddb6fb 100644 --- a/crates/libmarathon/src/platform/desktop/event_loop.rs +++ b/crates/libmarathon/src/platform/desktop/event_loop.rs @@ -77,7 +77,7 @@ impl ApplicationHandler for DesktopApp { /// /// This takes ownership of the main thread and runs the winit event loop. /// The update_fn is called each frame to update game logic. -pub fn run(mut update_fn: impl FnMut() + 'static) -> Result<(), Box> { +pub fn run(_update_fn: impl FnMut() + 'static) -> Result<(), Box> { let event_loop = EventLoop::new()?; event_loop.set_control_flow(ControlFlow::Poll); // Run as fast as possible diff --git a/crates/libmarathon/src/platform/desktop/executor.rs b/crates/libmarathon/src/platform/desktop/executor.rs index 54d7fae..1c8323a 100644 --- a/crates/libmarathon/src/platform/desktop/executor.rs +++ b/crates/libmarathon/src/platform/desktop/executor.rs @@ -51,31 +51,24 @@ //! Note: Battery-aware adaptive frame limiting is planned for production use. use bevy::prelude::*; -use bevy::app::AppExit; use bevy::input::{ ButtonInput, mouse::MouseButton as BevyMouseButton, keyboard::KeyCode as BevyKeyCode, touch::{Touches, TouchInput}, - gestures::*, - keyboard::KeyboardInput, - mouse::{MouseButtonInput, MouseMotion, MouseWheel}, }; use bevy::window::{ PrimaryWindow, WindowCreated, WindowResized, WindowScaleFactorChanged, WindowClosing, WindowResolution, WindowMode, WindowPosition, WindowEvent as BevyWindowEvent, RawHandleWrapper, WindowWrapper, - CursorMoved, CursorEntered, CursorLeft, - WindowFocused, WindowOccluded, WindowMoved, WindowThemeChanged, WindowDestroyed, - FileDragAndDrop, Ime, WindowCloseRequested, }; use bevy::ecs::message::Messages; -use crate::platform::input::{InputEvent, InputEventBuffer}; +use crate::platform::input::InputEventBuffer; use super::{push_window_event, push_device_event, drain_as_input_events, set_scale_factor}; use std::sync::Arc; use winit::application::ApplicationHandler; -use winit::event::{Event as WinitEvent, WindowEvent as WinitWindowEvent}; -use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy}; +use winit::event::WindowEvent as WinitWindowEvent; +use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; use winit::window::{Window as WinitWindow, WindowId, WindowAttributes}; /// Application handler state machine diff --git a/crates/libmarathon/src/platform/desktop/input.rs b/crates/libmarathon/src/platform/desktop/input.rs index 5b7ad0f..1233aa1 100644 --- a/crates/libmarathon/src/platform/desktop/input.rs +++ b/crates/libmarathon/src/platform/desktop/input.rs @@ -51,11 +51,11 @@ use glam::Vec2; use std::sync::{Mutex, OnceLock}; use std::path::PathBuf; use winit::event::{ - DeviceEvent, ElementState, MouseButton as WinitMouseButton, MouseScrollDelta, WindowEvent, - Touch as WinitTouch, Force as WinitForce, TouchPhase as WinitTouchPhase, + ElementState, MouseButton as WinitMouseButton, MouseScrollDelta, WindowEvent, + Force as WinitForce, TouchPhase as WinitTouchPhase, Ime as WinitIme, }; -use winit::keyboard::{PhysicalKey, Key as LogicalKey, NamedKey}; +use winit::keyboard::{PhysicalKey, Key as LogicalKey}; use winit::window::Theme as WinitTheme; /// Raw winit input events before conversion diff --git a/crates/libmarathon/tests/sync_integration_headless.rs b/crates/libmarathon/tests/sync_integration_headless.rs index f5b0fa5..81f73fc 100644 --- a/crates/libmarathon/tests/sync_integration_headless.rs +++ b/crates/libmarathon/tests/sync_integration_headless.rs @@ -60,7 +60,6 @@ use libmarathon::{ }, }; // Note: Test components use rkyv instead of serde -use sync_macros::Synced as SyncedDerive; use tempfile::TempDir; use uuid::Uuid; @@ -69,20 +68,18 @@ use uuid::Uuid; // ============================================================================ /// Simple position component for testing sync -#[derive(Component, Reflect, Clone, Debug, PartialEq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] +#[sync_macros::synced(version = 1, strategy = "LastWriteWins")] +#[derive(Component, Reflect, Clone, Debug, PartialEq)] #[reflect(Component)] -#[derive(SyncedDerive)] -#[sync(version = 1, strategy = "LastWriteWins")] struct TestPosition { x: f32, y: f32, } /// Simple health component for testing sync -#[derive(Component, Reflect, Clone, Debug, PartialEq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] +#[sync_macros::synced(version = 1, strategy = "LastWriteWins")] +#[derive(Component, Reflect, Clone, Debug, PartialEq)] #[reflect(Component)] -#[derive(SyncedDerive)] -#[sync(version = 1, strategy = "LastWriteWins")] struct TestHealth { current: f32, max: f32, @@ -93,10 +90,7 @@ struct TestHealth { // ============================================================================ mod test_utils { - use rusqlite::{ - Connection, - OptionalExtension, - }; + use rusqlite::Connection; use super::*; diff --git a/crates/sync-macros/src/lib.rs b/crates/sync-macros/src/lib.rs index 2b63392..6904e17 100644 --- a/crates/sync-macros/src/lib.rs +++ b/crates/sync-macros/src/lib.rs @@ -2,6 +2,7 @@ use proc_macro::TokenStream; use quote::quote; use syn::{ DeriveInput, + ItemStruct, parse_macro_input, }; @@ -44,16 +45,12 @@ impl SyncStrategy { struct SyncAttributes { version: u32, strategy: SyncStrategy, - persist: bool, - lazy: bool, } impl SyncAttributes { fn parse(input: &DeriveInput) -> Result { let mut version: Option = None; let mut strategy: Option = None; - let mut persist = true; // default - let mut lazy = false; // default // Find the #[sync(...)] attribute for attr in &input.attrs { @@ -74,14 +71,6 @@ impl SyncAttributes { .map_err(|e| syn::Error::new_spanned(&value, e))?, ); Ok(()) - } else if meta.path.is_ident("persist") { - let value: syn::LitBool = meta.value()?.parse()?; - persist = value.value; - Ok(()) - } else if meta.path.is_ident("lazy") { - let value: syn::LitBool = meta.value()?.parse()?; - lazy = value.value; - Ok(()) } else { Err(meta.error("unrecognized sync attribute")) } @@ -92,29 +81,20 @@ impl SyncAttributes { let version = version.ok_or_else(|| { syn::Error::new( proc_macro2::Span::call_site(), - "Missing required attribute `version`\n\ - \n\ - = help: Add #[sync(version = 1, strategy = \"...\")] to your struct\n\ - = note: See documentation: https://docs.rs/lonni/sync/strategies.html", + "Missing required attribute `version`\n\n \n\n = help: Add #[sync(version = 1, strategy = \"...\")] to your struct\n\n = note: See documentation: https://docs.rs/lonni/sync/strategies.html", ) })?; let strategy = strategy.ok_or_else(|| { syn::Error::new( proc_macro2::Span::call_site(), - "Missing required attribute `strategy`\n\ - \n\ - = help: Choose one of: \"LastWriteWins\", \"Set\", \"Sequence\", \"Custom\"\n\ - = help: Add #[sync(version = 1, strategy = \"LastWriteWins\")] to your struct\n\ - = note: See documentation: https://docs.rs/lonni/sync/strategies.html", + "Missing required attribute `strategy`\n\n \n\n = help: Choose one of: \"LastWriteWins\", \"Set\", \"Sequence\", \"Custom\"\n\n = help: Add #[sync(version = 1, strategy = \"LastWriteWins\")] to your struct\n\n = note: See documentation: https://docs.rs/lonni/sync/strategies.html", ) })?; Ok(SyncAttributes { version, strategy, - persist, - lazy, }) } } @@ -159,9 +139,35 @@ pub fn derive_synced(input: TokenStream) -> TokenStream { // Generate merge method based on strategy let merge_impl = generate_merge(&input, &attrs.strategy); - // Note: Users must add #[derive(rkyv::Archive, rkyv::Serialize, - // rkyv::Deserialize)] to their struct + // Extract struct attributes and visibility for re-emission + let vis = &input.vis; + let attrs_without_sync: Vec<_> = input + .attrs + .iter() + .filter(|attr| !attr.path().is_ident("sync")) + .collect(); + let struct_token = match &input.data { + | syn::Data::Struct(_) => quote! { struct }, + | _ => quote! {}, + }; + + // Re-emit the struct with rkyv derives added + let rkyv_struct = match &input.data { + | syn::Data::Struct(data_struct) => { + let fields = &data_struct.fields; + quote! { + #[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] + #(#attrs_without_sync)* + #vis #struct_token #name #fields + } + }, + | _ => quote! {}, + }; + let expanded = quote! { + // Re-emit struct with rkyv derives + #rkyv_struct + // Register component with inventory for type registry // Build type path at compile time using concat! and module_path! // since std::any::type_name() is not yet const @@ -407,3 +413,166 @@ fn generate_custom_merge(input: &DeriveInput) -> proc_macro2::TokenStream { libmarathon::networking::ComponentMergeDecision::KeptLocal } } + + +/// Attribute macro for synced components +/// +/// This is an alternative to the derive macro that automatically adds rkyv derives. +/// +/// # Example +/// ```ignore +/// #[synced(version = 1, strategy = "LastWriteWins")] +/// struct Health(f32); +/// ``` +#[proc_macro_attribute] +pub fn synced(attr: TokenStream, item: TokenStream) -> TokenStream { + let input_struct = match syn::parse::(item.clone()) { + Ok(s) => s, + Err(e) => { + return syn::Error::new_spanned( + proc_macro2::TokenStream::from(item), + format!("synced attribute can only be applied to structs: {}", e), + ) + .to_compile_error() + .into(); + } + }; + + // Parse the attribute arguments manually + let attr_str = attr.to_string(); + let (version, strategy) = parse_attr_string(&attr_str); + + // Generate the same implementations as the derive macro + let name = &input_struct.ident; + let name_str = name.to_string(); + let strategy_tokens = strategy.to_tokens(); + let vis = &input_struct.vis; + let attrs = &input_struct.attrs; + let generics = &input_struct.generics; + let fields = &input_struct.fields; + + // Convert ItemStruct to DeriveInput for compatibility with existing functions + // Build it manually to avoid parse_quote issues with tuple structs + let derive_input = DeriveInput { + attrs: attrs.clone(), + vis: vis.clone(), + ident: name.clone(), + generics: generics.clone(), + data: syn::Data::Struct(syn::DataStruct { + struct_token: syn::token::Struct::default(), + fields: fields.clone(), + semi_token: if matches!(fields, syn::Fields::Unit) { + Some(syn::token::Semi::default()) + } else { + None + }, + }), + }; + + let serialize_impl = generate_serialize(&derive_input); + let deserialize_impl = generate_deserialize(&derive_input, name); + let merge_impl = generate_merge(&derive_input, &strategy); + + // Add semicolon for tuple/unit structs + let semi = if matches!(fields, syn::Fields::Named(_)) { + quote! {} + } else { + quote! { ; } + }; + + let expanded = quote! { + // Output the struct with rkyv derives added + #[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] + #(#attrs)* + #vis struct #name #generics #fields #semi + + // Register component with inventory for type registry + const _: () = { + const TYPE_PATH: &str = concat!(module_path!(), "::", stringify!(#name)); + + inventory::submit! { + libmarathon::persistence::ComponentMeta { + type_name: #name_str, + type_path: TYPE_PATH, + type_id: std::any::TypeId::of::<#name>(), + deserialize_fn: |bytes: &[u8]| -> anyhow::Result> { + let component: #name = rkyv::from_bytes::<#name, rkyv::rancor::Failure>(bytes)?; + Ok(Box::new(component)) + }, + serialize_fn: |world: &bevy::ecs::world::World, entity: bevy::ecs::entity::Entity| -> Option> { + world.get::<#name>(entity).and_then(|component| { + rkyv::to_bytes::(component) + .map(|bytes| bytes.to_vec()) + .ok() + }) + }, + insert_fn: |entity_mut: &mut bevy::ecs::world::EntityWorldMut, boxed: Box| { + if let Ok(component) = boxed.downcast::<#name>() { + entity_mut.insert(*component); + } + }, + } + }; + }; + + impl libmarathon::networking::SyncComponent for #name { + const VERSION: u32 = #version; + const STRATEGY: libmarathon::networking::SyncStrategy = #strategy_tokens; + + #[inline] + fn serialize_sync(&self) -> anyhow::Result> { + #serialize_impl + } + + #[inline] + fn deserialize_sync(data: &[u8]) -> anyhow::Result { + #deserialize_impl + } + + #[inline] + fn merge(&mut self, remote: Self, clock_cmp: libmarathon::networking::ClockComparison) -> libmarathon::networking::ComponentMergeDecision { + #merge_impl + } + } + }; + + TokenStream::from(expanded) +} + +/// Parse attribute string (simple parser for version and strategy) +fn parse_attr_string(attr: &str) -> (u32, SyncStrategy) { + let mut version = 1; + let mut strategy = SyncStrategy::LastWriteWins; + + // Simple parsing - look for version = N and strategy = "..." + if let Some(v_pos) = attr.find("version") { + if let Some(eq_pos) = attr[v_pos..].find('=') { + let start = v_pos + eq_pos + 1; + let rest = &attr[start..].trim(); + if let Some(comma_pos) = rest.find(',') { + if let Ok(v) = rest[..comma_pos].trim().parse() { + version = v; + } + } else if let Ok(v) = rest.trim().parse() { + version = v; + } + } + } + + if let Some(s_pos) = attr.find("strategy") { + if let Some(eq_pos) = attr[s_pos..].find('=') { + let start = s_pos + eq_pos + 1; + let rest = &attr[start..].trim(); + if let Some(quote_start) = rest.find('"') { + if let Some(quote_end) = rest[quote_start + 1..].find('"') { + let strategy_str = &rest[quote_start + 1..quote_start + 1 + quote_end]; + if let Ok(s) = SyncStrategy::from_str(strategy_str) { + strategy = s; + } + } + } + } + } + + (version, strategy) +} diff --git a/crates/sync-macros/tests/basic_macro_test.rs b/crates/sync-macros/tests/basic_macro_test.rs index be6020e..ecd2fc6 100644 --- a/crates/sync-macros/tests/basic_macro_test.rs +++ b/crates/sync-macros/tests/basic_macro_test.rs @@ -1,20 +1,16 @@ -/// Basic tests for the Synced derive macro +/// Basic tests for the Synced attribute macro use bevy::prelude::*; use libmarathon::networking::{ ClockComparison, ComponentMergeDecision, SyncComponent, - SyncStrategy, - Synced, }; -use sync_macros::Synced as SyncedDerive; // 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)] -#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] #[reflect(Component)] -#[derive(SyncedDerive)] -#[sync(version = 1, strategy = "LastWriteWins")] struct Health(f32); #[test] @@ -66,11 +62,10 @@ fn test_health_lww_merge_concurrent() { } // 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)] -#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] #[reflect(Component)] -#[derive(SyncedDerive)] -#[sync(version = 1, strategy = "LastWriteWins")] struct Position { x: f32, y: f32,