cleaned up code

Signed-off-by: Sienna Meridian Satterwhite <sienna@r3t.io>
This commit is contained in:
2025-12-17 20:11:31 +00:00
parent 99e31b1157
commit b421aaf037
15 changed files with 220 additions and 90 deletions

View File

@@ -181,7 +181,7 @@ impl WindowToEguiContextMap {
// NOTE: We don't use bevy_winit since we own the event loop // NOTE: We don't use bevy_winit since we own the event loop
// event_loop_proxy: Res<bevy_winit::EventLoopProxyWrapper<bevy_winit::WakeUp>>, // event_loop_proxy: Res<bevy_winit::EventLoopProxyWrapper<bevy_winit::WakeUp>>,
) { ) {
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 if let bevy::camera::RenderTarget::Window(window_ref) = camera.target
&& let Some(window_ref) = window_ref.normalize(primary_window.single().ok()) && let Some(window_ref) = window_ref.normalize(primary_window.single().ok())
{ {

View File

@@ -35,7 +35,7 @@ pub fn process_output_system(
egui_global_settings: Res<EguiGlobalSettings>, egui_global_settings: Res<EguiGlobalSettings>,
window_to_egui_context_map: Res<WindowToEguiContextMap>, window_to_egui_context_map: Res<WindowToEguiContextMap>,
) { ) {
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 for (entity, mut context, mut full_output, mut render_output, mut egui_output, settings) in
context_query.iter_mut() context_query.iter_mut()
@@ -115,7 +115,7 @@ pub fn process_output_system(
} }
let needs_repaint = !render_output.is_empty(); 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) // NOTE: RequestRedraw not needed - we own winit and run unbounded (continuous redraws)

View File

@@ -42,10 +42,7 @@ use std::{
}; };
use bevy::prelude::*; use bevy::prelude::*;
use serde::{
Deserialize,
Serialize,
};
use uuid::Uuid; use uuid::Uuid;
use crate::networking::{ use crate::networking::{

View File

@@ -5,7 +5,6 @@
//! dispatcher system polls once and routes messages to appropriate handlers. //! dispatcher system polls once and routes messages to appropriate handlers.
use bevy::{ use bevy::{
ecs::system::SystemState,
prelude::*, prelude::*,
}; };
@@ -13,14 +12,12 @@ use crate::networking::{
GossipBridge, GossipBridge,
JoinType, JoinType,
NetworkedEntity, NetworkedEntity,
TombstoneRegistry,
VersionedMessage, VersionedMessage,
apply_entity_delta, apply_entity_delta,
apply_full_state, apply_full_state,
blob_support::BlobStore, blob_support::BlobStore,
build_missing_deltas, build_missing_deltas,
delta_generation::NodeVectorClock, delta_generation::NodeVectorClock,
entity_map::NetworkEntityMap,
messages::SyncMessage, messages::SyncMessage,
operation_log::OperationLog, operation_log::OperationLog,
plugin::SessionSecret, plugin::SessionSecret,

View File

@@ -3,10 +3,7 @@
//! This module defines the protocol messages used for distributed //! This module defines the protocol messages used for distributed
//! synchronization according to RFC 0001. //! synchronization according to RFC 0001.
use serde::{
Deserialize,
Serialize,
};
use crate::networking::{ use crate::networking::{
locks::LockMessage, locks::LockMessage,

View File

@@ -4,10 +4,7 @@
//! on components in the distributed system. Each operation type corresponds to //! on components in the distributed system. Each operation type corresponds to
//! a specific CRDT merge strategy. //! a specific CRDT merge strategy.
use serde::{
Deserialize,
Serialize,
};
use crate::networking::{ use crate::networking::{
messages::ComponentData, messages::ComponentData,

View File

@@ -41,10 +41,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use bevy::prelude::*; use bevy::prelude::*;
use serde::{
Deserialize,
Serialize,
};
use crate::networking::vector_clock::{ use crate::networking::vector_clock::{
NodeId, NodeId,

View File

@@ -6,10 +6,7 @@ use std::fmt;
/// human-readable ! session codes, ALPN-based network isolation, and persistent /// human-readable ! session codes, ALPN-based network isolation, and persistent
/// session tracking. /// session tracking.
use bevy::prelude::*; use bevy::prelude::*;
use serde::{
Deserialize,
Serialize,
};
use uuid::Uuid; use uuid::Uuid;
use crate::networking::VectorClock; use crate::networking::VectorClock;

View File

@@ -5,10 +5,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use serde::{
Deserialize,
Serialize,
};
use crate::networking::error::{ use crate::networking::error::{
NetworkingError, NetworkingError,

View File

@@ -77,7 +77,7 @@ impl ApplicationHandler for DesktopApp {
/// ///
/// This takes ownership of the main thread and runs the winit event loop. /// This takes ownership of the main thread and runs the winit event loop.
/// The update_fn is called each frame to update game logic. /// The update_fn is called each frame to update game logic.
pub fn run(mut update_fn: impl FnMut() + 'static) -> Result<(), Box<dyn std::error::Error>> { pub fn run(_update_fn: impl FnMut() + 'static) -> Result<(), Box<dyn std::error::Error>> {
let event_loop = EventLoop::new()?; let event_loop = EventLoop::new()?;
event_loop.set_control_flow(ControlFlow::Poll); // Run as fast as possible event_loop.set_control_flow(ControlFlow::Poll); // Run as fast as possible

View File

@@ -51,31 +51,24 @@
//! Note: Battery-aware adaptive frame limiting is planned for production use. //! Note: Battery-aware adaptive frame limiting is planned for production use.
use bevy::prelude::*; use bevy::prelude::*;
use bevy::app::AppExit;
use bevy::input::{ use bevy::input::{
ButtonInput, ButtonInput,
mouse::MouseButton as BevyMouseButton, mouse::MouseButton as BevyMouseButton,
keyboard::KeyCode as BevyKeyCode, keyboard::KeyCode as BevyKeyCode,
touch::{Touches, TouchInput}, touch::{Touches, TouchInput},
gestures::*,
keyboard::KeyboardInput,
mouse::{MouseButtonInput, MouseMotion, MouseWheel},
}; };
use bevy::window::{ use bevy::window::{
PrimaryWindow, WindowCreated, WindowResized, WindowScaleFactorChanged, WindowClosing, PrimaryWindow, WindowCreated, WindowResized, WindowScaleFactorChanged, WindowClosing,
WindowResolution, WindowMode, WindowPosition, WindowEvent as BevyWindowEvent, WindowResolution, WindowMode, WindowPosition, WindowEvent as BevyWindowEvent,
RawHandleWrapper, WindowWrapper, RawHandleWrapper, WindowWrapper,
CursorMoved, CursorEntered, CursorLeft,
WindowFocused, WindowOccluded, WindowMoved, WindowThemeChanged, WindowDestroyed,
FileDragAndDrop, Ime, WindowCloseRequested,
}; };
use bevy::ecs::message::Messages; 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 super::{push_window_event, push_device_event, drain_as_input_events, set_scale_factor};
use std::sync::Arc; use std::sync::Arc;
use winit::application::ApplicationHandler; use winit::application::ApplicationHandler;
use winit::event::{Event as WinitEvent, WindowEvent as WinitWindowEvent}; use winit::event::WindowEvent as WinitWindowEvent;
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy}; use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::window::{Window as WinitWindow, WindowId, WindowAttributes}; use winit::window::{Window as WinitWindow, WindowId, WindowAttributes};
/// Application handler state machine /// Application handler state machine

View File

@@ -51,11 +51,11 @@ use glam::Vec2;
use std::sync::{Mutex, OnceLock}; use std::sync::{Mutex, OnceLock};
use std::path::PathBuf; use std::path::PathBuf;
use winit::event::{ use winit::event::{
DeviceEvent, ElementState, MouseButton as WinitMouseButton, MouseScrollDelta, WindowEvent, ElementState, MouseButton as WinitMouseButton, MouseScrollDelta, WindowEvent,
Touch as WinitTouch, Force as WinitForce, TouchPhase as WinitTouchPhase, Force as WinitForce, TouchPhase as WinitTouchPhase,
Ime as WinitIme, Ime as WinitIme,
}; };
use winit::keyboard::{PhysicalKey, Key as LogicalKey, NamedKey}; use winit::keyboard::{PhysicalKey, Key as LogicalKey};
use winit::window::Theme as WinitTheme; use winit::window::Theme as WinitTheme;
/// Raw winit input events before conversion /// Raw winit input events before conversion

View File

@@ -60,7 +60,6 @@ use libmarathon::{
}, },
}; };
// Note: Test components use rkyv instead of serde // Note: Test components use rkyv instead of serde
use sync_macros::Synced as SyncedDerive;
use tempfile::TempDir; use tempfile::TempDir;
use uuid::Uuid; use uuid::Uuid;
@@ -69,20 +68,18 @@ use uuid::Uuid;
// ============================================================================ // ============================================================================
/// Simple position component for testing sync /// 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)] #[reflect(Component)]
#[derive(SyncedDerive)]
#[sync(version = 1, strategy = "LastWriteWins")]
struct TestPosition { struct TestPosition {
x: f32, x: f32,
y: f32, y: f32,
} }
/// Simple health component for testing sync /// 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)] #[reflect(Component)]
#[derive(SyncedDerive)]
#[sync(version = 1, strategy = "LastWriteWins")]
struct TestHealth { struct TestHealth {
current: f32, current: f32,
max: f32, max: f32,
@@ -93,10 +90,7 @@ struct TestHealth {
// ============================================================================ // ============================================================================
mod test_utils { mod test_utils {
use rusqlite::{ use rusqlite::Connection;
Connection,
OptionalExtension,
};
use super::*; use super::*;

View File

@@ -2,6 +2,7 @@ use proc_macro::TokenStream;
use quote::quote; use quote::quote;
use syn::{ use syn::{
DeriveInput, DeriveInput,
ItemStruct,
parse_macro_input, parse_macro_input,
}; };
@@ -44,16 +45,12 @@ impl SyncStrategy {
struct SyncAttributes { struct SyncAttributes {
version: u32, version: u32,
strategy: SyncStrategy, strategy: SyncStrategy,
persist: bool,
lazy: bool,
} }
impl SyncAttributes { impl SyncAttributes {
fn parse(input: &DeriveInput) -> Result<Self, syn::Error> { fn parse(input: &DeriveInput) -> Result<Self, syn::Error> {
let mut version: Option<u32> = None; let mut version: Option<u32> = None;
let mut strategy: Option<SyncStrategy> = None; let mut strategy: Option<SyncStrategy> = None;
let mut persist = true; // default
let mut lazy = false; // default
// Find the #[sync(...)] attribute // Find the #[sync(...)] attribute
for attr in &input.attrs { for attr in &input.attrs {
@@ -74,14 +71,6 @@ impl SyncAttributes {
.map_err(|e| syn::Error::new_spanned(&value, e))?, .map_err(|e| syn::Error::new_spanned(&value, e))?,
); );
Ok(()) 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 { } else {
Err(meta.error("unrecognized sync attribute")) Err(meta.error("unrecognized sync attribute"))
} }
@@ -92,29 +81,20 @@ impl SyncAttributes {
let version = version.ok_or_else(|| { let version = version.ok_or_else(|| {
syn::Error::new( syn::Error::new(
proc_macro2::Span::call_site(), proc_macro2::Span::call_site(),
"Missing required attribute `version`\n\ "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",
\n\
= help: Add #[sync(version = 1, strategy = \"...\")] to your struct\n\
= note: See documentation: https://docs.rs/lonni/sync/strategies.html",
) )
})?; })?;
let strategy = strategy.ok_or_else(|| { let strategy = strategy.ok_or_else(|| {
syn::Error::new( syn::Error::new(
proc_macro2::Span::call_site(), proc_macro2::Span::call_site(),
"Missing required attribute `strategy`\n\ "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",
\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",
) )
})?; })?;
Ok(SyncAttributes { Ok(SyncAttributes {
version, version,
strategy, strategy,
persist,
lazy,
}) })
} }
} }
@@ -159,9 +139,35 @@ pub fn derive_synced(input: TokenStream) -> TokenStream {
// Generate merge method based on strategy // Generate merge method based on strategy
let merge_impl = generate_merge(&input, &attrs.strategy); let merge_impl = generate_merge(&input, &attrs.strategy);
// Note: Users must add #[derive(rkyv::Archive, rkyv::Serialize, // Extract struct attributes and visibility for re-emission
// rkyv::Deserialize)] to their struct 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! { let expanded = quote! {
// Re-emit struct with rkyv derives
#rkyv_struct
// Register component with inventory for type registry // Register component with inventory for type registry
// Build type path at compile time using concat! and module_path! // Build type path at compile time using concat! and module_path!
// since std::any::type_name() is not yet const // 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 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::<ItemStruct>(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<Box<dyn std::any::Any>> {
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<Vec<u8>> {
world.get::<#name>(entity).and_then(|component| {
rkyv::to_bytes::<rkyv::rancor::Failure>(component)
.map(|bytes| bytes.to_vec())
.ok()
})
},
insert_fn: |entity_mut: &mut bevy::ecs::world::EntityWorldMut, boxed: Box<dyn std::any::Any>| {
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<Vec<u8>> {
#serialize_impl
}
#[inline]
fn deserialize_sync(data: &[u8]) -> anyhow::Result<Self> {
#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)
}

View File

@@ -1,20 +1,16 @@
/// Basic tests for the Synced derive macro /// Basic tests for the Synced attribute macro
use bevy::prelude::*; use bevy::prelude::*;
use libmarathon::networking::{ use libmarathon::networking::{
ClockComparison, ClockComparison,
ComponentMergeDecision, ComponentMergeDecision,
SyncComponent, SyncComponent,
SyncStrategy,
Synced,
}; };
use sync_macros::Synced as SyncedDerive;
// Test 1: Basic struct with LWW strategy compiles // 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(Component, Reflect, Clone, Debug, PartialEq)]
#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
#[reflect(Component)] #[reflect(Component)]
#[derive(SyncedDerive)]
#[sync(version = 1, strategy = "LastWriteWins")]
struct Health(f32); struct Health(f32);
#[test] #[test]
@@ -66,11 +62,10 @@ fn test_health_lww_merge_concurrent() {
} }
// Test 2: Struct with multiple fields // 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(Component, Reflect, Clone, Debug, PartialEq)]
#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
#[reflect(Component)] #[reflect(Component)]
#[derive(SyncedDerive)]
#[sync(version = 1, strategy = "LastWriteWins")]
struct Position { struct Position {
x: f32, x: f32,
y: f32, y: f32,