Skip to main content

bevy_networker_multiplayer_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::{
4    parse::{Parse, ParseStream},
5    parse_macro_input,
6    punctuated::Punctuated,
7    token::Comma,
8    Data,
9    Expr,
10    Error,
11    Fields,
12    DeriveInput,
13    ItemStruct,
14    Path,
15    Token,
16};
17
18struct SyncArgs {
19    is_resource: bool,
20    prefab_components: Option<Vec<Expr>>,
21}
22
23enum SyncArg {
24    Resource,
25    Prefab(Vec<Expr>),
26}
27
28impl Parse for SyncArg {
29    fn parse(input: ParseStream) -> syn::Result<Self> {
30        let lookahead = input.lookahead1();
31        if lookahead.peek(syn::Ident) {
32            let ident: syn::Ident = input.parse()?;
33            if ident == "resource" {
34                return Ok(Self::Resource);
35            }
36
37            if ident == "prefab" {
38                let content;
39                syn::parenthesized!(content in input);
40                let components = Punctuated::<Expr, Comma>::parse_terminated(&content)?
41                    .into_iter()
42                    .collect();
43                return Ok(Self::Prefab(components));
44            }
45
46            return Err(Error::new_spanned(ident, "unsupported #[sync(...)] argument"));
47        }
48
49        Err(lookahead.error())
50    }
51}
52
53impl Parse for SyncArgs {
54    fn parse(input: ParseStream) -> syn::Result<Self> {
55        let args = Punctuated::<SyncArg, Comma>::parse_terminated(input)?;
56        let mut is_resource = false;
57        let mut prefab_components = None;
58
59        for arg in args {
60            match arg {
61                SyncArg::Resource => {
62                    is_resource = true;
63                }
64                SyncArg::Prefab(components) => {
65                    prefab_components = Some(components);
66                }
67            }
68        }
69
70        Ok(Self {
71            is_resource,
72            prefab_components,
73        })
74    }
75}
76
77#[proc_macro_attribute]
78pub fn sync(args: TokenStream, input: TokenStream) -> TokenStream {
79    let args = match parse_sync_args(args) {
80        Ok(args) => args,
81        Err(error) => return error,
82    };
83    let item = parse_macro_input!(input as ItemStruct);
84    expand_sync(item, args).into()
85}
86
87#[proc_macro_attribute]
88pub fn netmsg(args: TokenStream, input: TokenStream) -> TokenStream {
89    if !args.is_empty() {
90        let args_tokens: proc_macro2::TokenStream = args.into();
91        return Error::new_spanned(
92            args_tokens,
93            "#[netmsg] does not take any arguments",
94        )
95        .to_compile_error()
96        .into();
97    }
98
99    let item = parse_macro_input!(input as ItemStruct);
100    expand_netmsg(item).into()
101}
102
103#[proc_macro_derive(PredictLinearMotion)]
104pub fn derive_predict_linear_motion(input: TokenStream) -> TokenStream {
105    expand_prediction_derive(input, PredictionDeriveKind::PredictLinearMotion).into()
106}
107
108#[proc_macro_derive(Velocity2d)]
109pub fn derive_velocity_2d(input: TokenStream) -> TokenStream {
110    expand_prediction_derive(input, PredictionDeriveKind::Velocity2d).into()
111}
112
113fn parse_sync_args(args: TokenStream) -> Result<SyncArgs, TokenStream> {
114    if args.is_empty() {
115        return Ok(SyncArgs {
116            is_resource: false,
117            prefab_components: None,
118        });
119    }
120
121    syn::parse::<SyncArgs>(args)
122        .map_err(|error| -> TokenStream { error.to_compile_error().into() })
123}
124
125fn expand_sync(mut item: ItemStruct, args: SyncArgs) -> proc_macro2::TokenStream {
126    let ident = item.ident.clone();
127    let generics = item.generics.clone();
128    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
129    let prefab_components = args.prefab_components.clone();
130
131    let mut derive_paths: Vec<Path> = Vec::new();
132    let mut retained_attrs = Vec::new();
133
134    for attr in item.attrs.into_iter() {
135        if attr.path().is_ident("derive") {
136            let parsed: Punctuated<Path, Token![,]> = attr
137                .parse_args_with(Punctuated::<Path, Token![,]>::parse_terminated)
138                .expect("failed to parse derive attributes");
139            derive_paths.extend(parsed.into_iter());
140        } else {
141            retained_attrs.push(attr);
142        }
143    }
144
145    for required in [
146        syn::parse_str::<Path>("::bevy_networker_multiplayer::serde::Serialize").unwrap(),
147        syn::parse_str::<Path>("::bevy_networker_multiplayer::serde::Deserialize").unwrap(),
148        syn::parse_str::<Path>("Clone").unwrap(),
149    ] {
150        if !has_derive(&derive_paths, &required) {
151            derive_paths.push(required);
152        }
153    }
154
155    if args.is_resource {
156        let required = syn::parse_str::<Path>("::bevy_networker_multiplayer::bevy::prelude::Resource")
157            .unwrap();
158        if !has_derive(&derive_paths, &required) {
159            derive_paths.push(required);
160        }
161    }
162
163    item.attrs = retained_attrs;
164
165    let register_fn = format_ident!("__{}_register_sync", ident);
166    let prefab_register_fn = format_ident!("__{}_register_prefab", ident);
167    let apply_fn = format_ident!("__{}_apply_sync", ident);
168    let snapshot_fn = format_ident!("__{}_snapshot_sync", ident);
169    let prefab_apply_fn = format_ident!("__{}_apply_prefab", ident);
170    let prefab_matches_fn = format_ident!("__{}_matches_prefab", ident);
171    let follow_fn = format_ident!("__{}_follow_visual_transform", ident);
172
173    let sync_trait = if args.is_resource {
174        quote! { SyncResource }
175    } else {
176        quote! { SyncComponent }
177    };
178
179    let register_system = if args.is_resource {
180        quote! {
181            app.add_systems(
182                ::bevy_networker_multiplayer::bevy::prelude::Update,
183                ::bevy_networker_multiplayer::sync::sync_resource::<#ident #ty_generics>,
184            );
185        }
186    } else {
187        quote! {
188            app.add_systems(
189                ::bevy_networker_multiplayer::bevy::prelude::Update,
190                ::bevy_networker_multiplayer::sync::sync_component::<#ident #ty_generics>
191                    .after(::bevy_networker_multiplayer::sync::assign_network_ids),
192            );
193        }
194    };
195
196    let follow_system = if prefab_components.is_some() && is_vec2_tuple_struct(&item) {
197        quote! {
198            app.add_systems(
199                ::bevy_networker_multiplayer::bevy::prelude::PostUpdate,
200                #follow_fn,
201            );
202        }
203    } else {
204        quote! {}
205    };
206
207    let registration = if args.is_resource {
208        quote! {
209            ::bevy_networker_multiplayer::inventory::submit! {
210                ::bevy_networker_multiplayer::sync::ResourceRegistration {
211                    type_path: concat!(module_path!(), "::", stringify!(#ident)),
212                    wire_id: ::bevy_networker_multiplayer::sync::hash_type_path(concat!(module_path!(), "::", stringify!(#ident))),
213                    register: #register_fn,
214                    apply: #apply_fn,
215                    snapshot: #snapshot_fn,
216                }
217            }
218        }
219    } else if prefab_components.is_some() {
220        quote! {
221            ::bevy_networker_multiplayer::inventory::submit! {
222                ::bevy_networker_multiplayer::sync::ComponentRegistration {
223                    type_path: concat!(module_path!(), "::", stringify!(#ident)),
224                    wire_id: ::bevy_networker_multiplayer::sync::hash_type_path(concat!(module_path!(), "::", stringify!(#ident))),
225                    register: #register_fn,
226                    apply: #apply_fn,
227                    snapshot: #snapshot_fn,
228                }
229            }
230
231            ::bevy_networker_multiplayer::inventory::submit! {
232                ::bevy_networker_multiplayer::sync::PrefabRegistration {
233                    type_path: concat!(module_path!(), "::", stringify!(#ident)),
234                    wire_id: ::bevy_networker_multiplayer::sync::hash_type_path(concat!(module_path!(), "::", stringify!(#ident))),
235                    register: #prefab_register_fn,
236                    matches: #prefab_matches_fn,
237                    apply: #prefab_apply_fn,
238                }
239            }
240        }
241    } else {
242        quote! {
243            ::bevy_networker_multiplayer::inventory::submit! {
244                ::bevy_networker_multiplayer::sync::ComponentRegistration {
245                    type_path: concat!(module_path!(), "::", stringify!(#ident)),
246                    wire_id: ::bevy_networker_multiplayer::sync::hash_type_path(concat!(module_path!(), "::", stringify!(#ident))),
247                    register: #register_fn,
248                    apply: #apply_fn,
249                    snapshot: #snapshot_fn,
250                }
251            }
252        }
253    };
254
255    let snapshot_fn_def = if args.is_resource {
256        quote! {
257            #[allow(non_snake_case)]
258            fn #snapshot_fn(world: &mut ::bevy_networker_multiplayer::bevy::prelude::World) -> ::std::vec::Vec<::bevy_networker_multiplayer::netres::ReplicationPacket> {
259                let mut packets = ::std::vec::Vec::new();
260
261                if let Some(resource) = world.get_resource::<#ident #ty_generics>() {
262                    let bytes = ::bevy_networker_multiplayer::bincode::serde::encode_to_vec(
263                        resource,
264                        ::bevy_networker_multiplayer::bincode::config::standard(),
265                    ).expect("failed to serialize sync resource");
266
267                    packets.push(::bevy_networker_multiplayer::netres::ReplicationPacket::UpdateResource {
268                        resource_wire_id: ::bevy_networker_multiplayer::sync::hash_type_path(concat!(module_path!(), "::", stringify!(#ident))),
269                        bytes,
270                    });
271                }
272
273                packets
274            }
275        }
276    } else {
277        quote! {
278            #[allow(non_snake_case)]
279            fn #snapshot_fn(world: &mut ::bevy_networker_multiplayer::bevy::prelude::World) -> ::std::vec::Vec<::bevy_networker_multiplayer::netres::ReplicationPacket> {
280                let mut packets = ::std::vec::Vec::new();
281                let mut query = world.query_filtered::<(
282                    ::bevy_networker_multiplayer::bevy::prelude::Entity,
283                    &::bevy_networker_multiplayer::replicated::NetworkId,
284                    &#ident #ty_generics,
285                ), ::bevy_networker_multiplayer::bevy::prelude::With<::bevy_networker_multiplayer::replicated::Replicated>>();
286
287                for (_, network_id, component) in query.iter(world) {
288                    let bytes = ::bevy_networker_multiplayer::bincode::serde::encode_to_vec(
289                        component,
290                        ::bevy_networker_multiplayer::bincode::config::standard(),
291                    ).expect("failed to serialize sync component");
292
293                    packets.push(::bevy_networker_multiplayer::netres::ReplicationPacket::UpdateComponent {
294                        network_id: network_id.0,
295                        component_wire_id: ::bevy_networker_multiplayer::sync::hash_type_path(concat!(module_path!(), "::", stringify!(#ident))),
296                        bytes,
297                    });
298                }
299
300                packets
301            }
302        }
303    };
304
305    let prefab_apply_def = if let Some(prefab_components) = prefab_components.clone() {
306        quote! {
307            #[allow(non_snake_case)]
308            fn #prefab_apply_fn(world: &mut ::bevy_networker_multiplayer::bevy::prelude::World, entity: ::bevy_networker_multiplayer::bevy::prelude::Entity) {
309                world.entity_mut(entity).insert((#(#prefab_components),*));
310            }
311        }
312    } else {
313        quote! {}
314    };
315
316    let prefab_matches_def = if prefab_components.is_some() {
317        quote! {
318            #[allow(non_snake_case)]
319            fn #prefab_matches_fn(world: &::bevy_networker_multiplayer::bevy::prelude::World, entity: ::bevy_networker_multiplayer::bevy::prelude::Entity) -> bool {
320                world.entity(entity).contains::<#ident #ty_generics>()
321            }
322        }
323    } else {
324        quote! {}
325    };
326
327    let follow_fn_def = if prefab_components.is_some() && is_vec2_tuple_struct(&item) {
328        quote! {
329            #[allow(non_snake_case)]
330            fn #follow_fn(
331                mut query: ::bevy_networker_multiplayer::bevy::prelude::Query<
332                    (
333                        &#ident #ty_generics,
334                        &mut ::bevy_networker_multiplayer::bevy::prelude::Transform,
335                    ),
336                    (
337                        ::bevy_networker_multiplayer::bevy::prelude::With<
338                            ::bevy_networker_multiplayer::replicated::Replicated,
339                        >,
340                        ::bevy_networker_multiplayer::bevy::prelude::Or<(
341                            ::bevy_networker_multiplayer::bevy::prelude::Added<#ident #ty_generics>,
342                            ::bevy_networker_multiplayer::bevy::prelude::Changed<#ident #ty_generics>,
343                        )>,
344                    ),
345                >,
346            ) {
347                for (component, mut transform) in &mut query {
348                    transform.translation.x = component.0.x;
349                    transform.translation.y = component.0.y;
350                }
351            }
352        }
353    } else {
354        quote! {}
355    };
356
357    let apply_fn_def = if args.is_resource {
358        quote! {
359            #[allow(non_snake_case)]
360            fn #apply_fn(world: &mut ::bevy_networker_multiplayer::bevy::prelude::World, bytes: &[u8]) {
361                let (resource, _): (#ident #ty_generics, usize) = ::bevy_networker_multiplayer::bincode::serde::decode_from_slice(
362                    bytes,
363                    ::bevy_networker_multiplayer::bincode::config::standard(),
364                ).expect("failed to deserialize sync resource");
365
366                world.insert_resource(resource);
367            }
368        }
369    } else if prefab_components.is_some() {
370        let uses_transform = is_vec2_tuple_struct(&item);
371        let position_binding = if uses_transform {
372            quote! {
373                let position = component.0;
374            }
375        } else {
376            quote! {}
377        };
378        let visual_update = if uses_transform {
379            quote! {
380                if let Some(mut transform) = entity.get_mut::<::bevy_networker_multiplayer::bevy::prelude::Transform>() {
381                    transform.translation.x = position.x;
382                    transform.translation.y = position.y;
383                }
384            }
385        } else {
386            quote! {}
387        };
388
389        quote! {
390            #[allow(non_snake_case)]
391            fn #apply_fn(world: &mut ::bevy_networker_multiplayer::bevy::prelude::World, entity: ::bevy_networker_multiplayer::bevy::prelude::Entity, bytes: &[u8]) {
392                let (component, _): (#ident #ty_generics, usize) = ::bevy_networker_multiplayer::bincode::serde::decode_from_slice(
393                    bytes,
394                    ::bevy_networker_multiplayer::bincode::config::standard(),
395                ).expect("failed to deserialize sync component");
396
397                #position_binding
398                let mut entity = world.entity_mut(entity);
399                entity.insert(component);
400                #visual_update
401            }
402        }
403    } else {
404        quote! {
405            #[allow(non_snake_case)]
406            fn #apply_fn(world: &mut ::bevy_networker_multiplayer::bevy::prelude::World, entity: ::bevy_networker_multiplayer::bevy::prelude::Entity, bytes: &[u8]) {
407                let (component, _): (#ident #ty_generics, usize) = ::bevy_networker_multiplayer::bincode::serde::decode_from_slice(
408                    bytes,
409                    ::bevy_networker_multiplayer::bincode::config::standard(),
410                ).expect("failed to deserialize sync component");
411
412                world.entity_mut(entity).insert(component);
413            }
414        }
415    };
416
417    quote! {
418        #[derive(#(#derive_paths),*)]
419        #item
420
421        impl #impl_generics ::bevy_networker_multiplayer::sync::#sync_trait for #ident #ty_generics #where_clause {
422            const TYPE_PATH: &'static str = concat!(module_path!(), "::", stringify!(#ident));
423            const WIRE_ID: u64 = ::bevy_networker_multiplayer::sync::hash_type_path(Self::TYPE_PATH);
424        }
425
426        #apply_fn_def
427
428        #[allow(non_snake_case)]
429        fn #register_fn(app: &mut ::bevy_networker_multiplayer::bevy::prelude::App) {
430            #register_system
431            #follow_system
432        }
433
434        #[allow(non_snake_case)]
435        fn #prefab_register_fn(_app: &mut ::bevy_networker_multiplayer::bevy::prelude::App) {}
436
437        #registration
438        #snapshot_fn_def
439        #prefab_apply_def
440        #prefab_matches_def
441        #follow_fn_def
442    }
443}
444
445fn expand_netmsg(item: ItemStruct) -> proc_macro2::TokenStream {
446    let ident = item.ident.clone();
447    let generics = item.generics.clone();
448    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
449
450    quote! {
451        #item
452
453        impl #impl_generics ::bevy_networker_multiplayer::NetMessage for #ident #ty_generics #where_clause {
454            const TYPE_PATH: &'static str = concat!(module_path!(), "::", stringify!(#ident));
455            const WIRE_ID: u64 = ::bevy_networker_multiplayer::netmsg::hash_type_path(Self::TYPE_PATH);
456        }
457    }
458}
459
460enum PredictionDeriveKind {
461    PredictLinearMotion,
462    Velocity2d,
463}
464
465fn expand_prediction_derive(
466    input: TokenStream,
467    kind: PredictionDeriveKind,
468) -> proc_macro2::TokenStream {
469    let input = match syn::parse::<DeriveInput>(input) {
470        Ok(input) => input,
471        Err(error) => return error.to_compile_error(),
472    };
473    let ident = input.ident;
474    let generics = input.generics;
475    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
476
477    let field_error = match kind {
478        PredictionDeriveKind::PredictLinearMotion => {
479            "PredictLinearMotion can only be derived for tuple structs with one Vec2 field"
480        }
481        PredictionDeriveKind::Velocity2d => {
482            "Velocity2d can only be derived for tuple structs with one Vec2 field"
483        }
484    };
485
486    match input.data {
487        Data::Struct(data) => match data.fields {
488            Fields::Unnamed(fields)
489                if fields.unnamed.len() == 1 && is_vec2_type(&fields.unnamed[0].ty) => {}
490            _ => return Error::new_spanned(ident, field_error).to_compile_error(),
491        },
492        _ => return Error::new_spanned(ident, field_error).to_compile_error(),
493    }
494
495    match kind {
496        PredictionDeriveKind::PredictLinearMotion => quote! {
497            impl #impl_generics ::bevy_networker_multiplayer::prediction::PredictLinearMotion
498                for #ident #ty_generics #where_clause
499            {
500                fn predicted_position(&self) -> ::bevy_networker_multiplayer::bevy::prelude::Vec2 {
501                    self.0
502                }
503
504                fn set_predicted_position(
505                    &mut self,
506                    position: ::bevy_networker_multiplayer::bevy::prelude::Vec2,
507                ) {
508                    self.0 = position;
509                }
510            }
511        },
512        PredictionDeriveKind::Velocity2d => quote! {
513            impl #impl_generics ::bevy_networker_multiplayer::prediction::Velocity2d
514                for #ident #ty_generics #where_clause
515            {
516                fn velocity_2d(&self) -> ::bevy_networker_multiplayer::bevy::prelude::Vec2 {
517                    self.0
518                }
519            }
520        },
521    }
522}
523
524fn has_derive(existing: &[Path], required: &Path) -> bool {
525    let Some(required_ident) = required.segments.last().map(|segment| &segment.ident) else {
526        return false;
527    };
528
529    existing
530        .iter()
531        .any(|path| path.segments.last().map(|segment| &segment.ident) == Some(required_ident))
532}
533
534fn is_vec2_tuple_struct(item: &ItemStruct) -> bool {
535    matches!(
536        &item.fields,
537        Fields::Unnamed(fields) if fields.unnamed.len() == 1 && is_vec2_type(&fields.unnamed[0].ty)
538    )
539}
540
541fn is_vec2_type(ty: &syn::Type) -> bool {
542    match ty {
543        syn::Type::Path(type_path) => type_path
544            .path
545            .segments
546            .last()
547            .map(|segment| segment.ident == "Vec2")
548            .unwrap_or(false),
549        _ => false,
550    }
551}