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}