947 lines
33 KiB
Rust
947 lines
33 KiB
Rust
use proc_macro::TokenStream;
|
|
use proc_macro2::{Span, TokenStream as TokenStream2};
|
|
use quote::{format_ident, quote, ToTokens};
|
|
use std::collections::HashSet;
|
|
use syn::{
|
|
braced, parenthesized,
|
|
parse::Parse,
|
|
parse_macro_input, parse_quote,
|
|
punctuated::Punctuated,
|
|
spanned::Spanned,
|
|
token::{Brace, Comma, Paren},
|
|
Data, DataEnum, DataStruct, DeriveInput, Expr, ExprCall, ExprPath, Field, Fields, Ident,
|
|
LitStr, Member, Path, Result, Token, Type, Visibility,
|
|
};
|
|
|
|
pub const EVENT: &str = "entity_event";
|
|
pub const AUTO_PROPAGATE: &str = "auto_propagate";
|
|
pub const TRAVERSAL: &str = "traversal";
|
|
|
|
pub fn derive_event(input: TokenStream) -> TokenStream {
|
|
let mut ast = parse_macro_input!(input as DeriveInput);
|
|
let bevy_ecs_path: Path = crate::bevy_ecs_path();
|
|
|
|
ast.generics
|
|
.make_where_clause()
|
|
.predicates
|
|
.push(parse_quote! { Self: Send + Sync + 'static });
|
|
|
|
let struct_name = &ast.ident;
|
|
let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();
|
|
|
|
TokenStream::from(quote! {
|
|
impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause {}
|
|
})
|
|
}
|
|
|
|
pub fn derive_entity_event(input: TokenStream) -> TokenStream {
|
|
let mut ast = parse_macro_input!(input as DeriveInput);
|
|
let mut auto_propagate = false;
|
|
let mut traversal: Type = parse_quote!(());
|
|
let bevy_ecs_path: Path = crate::bevy_ecs_path();
|
|
|
|
let mut processed_attrs = Vec::new();
|
|
|
|
ast.generics
|
|
.make_where_clause()
|
|
.predicates
|
|
.push(parse_quote! { Self: Send + Sync + 'static });
|
|
|
|
for attr in ast.attrs.iter().filter(|attr| attr.path().is_ident(EVENT)) {
|
|
if let Err(e) = attr.parse_nested_meta(|meta| match meta.path.get_ident() {
|
|
Some(ident) if processed_attrs.iter().any(|i| ident == i) => {
|
|
Err(meta.error(format!("duplicate attribute: {ident}")))
|
|
}
|
|
Some(ident) if ident == AUTO_PROPAGATE => {
|
|
auto_propagate = true;
|
|
processed_attrs.push(AUTO_PROPAGATE);
|
|
Ok(())
|
|
}
|
|
Some(ident) if ident == TRAVERSAL => {
|
|
traversal = meta.value()?.parse()?;
|
|
processed_attrs.push(TRAVERSAL);
|
|
Ok(())
|
|
}
|
|
Some(ident) => Err(meta.error(format!("unsupported attribute: {ident}"))),
|
|
None => Err(meta.error("expected identifier")),
|
|
}) {
|
|
return e.to_compile_error().into();
|
|
}
|
|
}
|
|
|
|
let struct_name = &ast.ident;
|
|
let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();
|
|
|
|
TokenStream::from(quote! {
|
|
impl #impl_generics #bevy_ecs_path::event::EntityEvent for #struct_name #type_generics #where_clause {
|
|
type Traversal = #traversal;
|
|
const AUTO_PROPAGATE: bool = #auto_propagate;
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn derive_buffered_event(input: TokenStream) -> TokenStream {
|
|
let mut ast = parse_macro_input!(input as DeriveInput);
|
|
let bevy_ecs_path: Path = crate::bevy_ecs_path();
|
|
|
|
ast.generics
|
|
.make_where_clause()
|
|
.predicates
|
|
.push(parse_quote! { Self: Send + Sync + 'static });
|
|
|
|
let struct_name = &ast.ident;
|
|
let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();
|
|
|
|
TokenStream::from(quote! {
|
|
impl #impl_generics #bevy_ecs_path::event::BufferedEvent for #struct_name #type_generics #where_clause {}
|
|
})
|
|
}
|
|
|
|
pub fn derive_resource(input: TokenStream) -> TokenStream {
|
|
let mut ast = parse_macro_input!(input as DeriveInput);
|
|
let bevy_ecs_path: Path = crate::bevy_ecs_path();
|
|
|
|
ast.generics
|
|
.make_where_clause()
|
|
.predicates
|
|
.push(parse_quote! { Self: Send + Sync + 'static });
|
|
|
|
let struct_name = &ast.ident;
|
|
let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();
|
|
|
|
TokenStream::from(quote! {
|
|
impl #impl_generics #bevy_ecs_path::resource::Resource for #struct_name #type_generics #where_clause {
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Component derive syntax is documented on both the macro and the trait.
|
|
pub fn derive_component(input: TokenStream) -> TokenStream {
|
|
let mut ast = parse_macro_input!(input as DeriveInput);
|
|
let bevy_ecs_path: Path = crate::bevy_ecs_path();
|
|
|
|
let attrs = match parse_component_attr(&ast) {
|
|
Ok(attrs) => attrs,
|
|
Err(e) => return e.into_compile_error().into(),
|
|
};
|
|
|
|
let relationship = match derive_relationship(&ast, &attrs, &bevy_ecs_path) {
|
|
Ok(value) => value,
|
|
Err(err) => err.into_compile_error().into(),
|
|
};
|
|
let relationship_target = match derive_relationship_target(&ast, &attrs, &bevy_ecs_path) {
|
|
Ok(value) => value,
|
|
Err(err) => err.into_compile_error().into(),
|
|
};
|
|
|
|
let map_entities = map_entities(
|
|
&ast.data,
|
|
&bevy_ecs_path,
|
|
Ident::new("this", Span::call_site()),
|
|
relationship.is_some(),
|
|
relationship_target.is_some(),
|
|
attrs.map_entities
|
|
).map(|map_entities_impl| quote! {
|
|
fn map_entities<M: #bevy_ecs_path::entity::EntityMapper>(this: &mut Self, mapper: &mut M) {
|
|
use #bevy_ecs_path::entity::MapEntities;
|
|
#map_entities_impl
|
|
}
|
|
});
|
|
|
|
let storage = storage_path(&bevy_ecs_path, attrs.storage);
|
|
|
|
let on_add_path = attrs
|
|
.on_add
|
|
.map(|path| path.to_token_stream(&bevy_ecs_path));
|
|
let on_remove_path = attrs
|
|
.on_remove
|
|
.map(|path| path.to_token_stream(&bevy_ecs_path));
|
|
|
|
let on_insert_path = if relationship.is_some() {
|
|
if attrs.on_insert.is_some() {
|
|
return syn::Error::new(
|
|
ast.span(),
|
|
"Custom on_insert hooks are not supported as relationships already define an on_insert hook",
|
|
)
|
|
.into_compile_error()
|
|
.into();
|
|
}
|
|
|
|
Some(quote!(<Self as #bevy_ecs_path::relationship::Relationship>::on_insert))
|
|
} else {
|
|
attrs
|
|
.on_insert
|
|
.map(|path| path.to_token_stream(&bevy_ecs_path))
|
|
};
|
|
|
|
let on_replace_path = if relationship.is_some() {
|
|
if attrs.on_replace.is_some() {
|
|
return syn::Error::new(
|
|
ast.span(),
|
|
"Custom on_replace hooks are not supported as Relationships already define an on_replace hook",
|
|
)
|
|
.into_compile_error()
|
|
.into();
|
|
}
|
|
|
|
Some(quote!(<Self as #bevy_ecs_path::relationship::Relationship>::on_replace))
|
|
} else if attrs.relationship_target.is_some() {
|
|
if attrs.on_replace.is_some() {
|
|
return syn::Error::new(
|
|
ast.span(),
|
|
"Custom on_replace hooks are not supported as RelationshipTarget already defines an on_replace hook",
|
|
)
|
|
.into_compile_error()
|
|
.into();
|
|
}
|
|
|
|
Some(quote!(<Self as #bevy_ecs_path::relationship::RelationshipTarget>::on_replace))
|
|
} else {
|
|
attrs
|
|
.on_replace
|
|
.map(|path| path.to_token_stream(&bevy_ecs_path))
|
|
};
|
|
|
|
let on_despawn_path = if attrs
|
|
.relationship_target
|
|
.is_some_and(|target| target.linked_spawn)
|
|
{
|
|
if attrs.on_despawn.is_some() {
|
|
return syn::Error::new(
|
|
ast.span(),
|
|
"Custom on_despawn hooks are not supported as this RelationshipTarget already defines an on_despawn hook, via the 'linked_spawn' attribute",
|
|
)
|
|
.into_compile_error()
|
|
.into();
|
|
}
|
|
|
|
Some(quote!(<Self as #bevy_ecs_path::relationship::RelationshipTarget>::on_despawn))
|
|
} else {
|
|
attrs
|
|
.on_despawn
|
|
.map(|path| path.to_token_stream(&bevy_ecs_path))
|
|
};
|
|
|
|
let on_add = hook_register_function_call(&bevy_ecs_path, quote! {on_add}, on_add_path);
|
|
let on_insert = hook_register_function_call(&bevy_ecs_path, quote! {on_insert}, on_insert_path);
|
|
let on_replace =
|
|
hook_register_function_call(&bevy_ecs_path, quote! {on_replace}, on_replace_path);
|
|
let on_remove = hook_register_function_call(&bevy_ecs_path, quote! {on_remove}, on_remove_path);
|
|
let on_despawn =
|
|
hook_register_function_call(&bevy_ecs_path, quote! {on_despawn}, on_despawn_path);
|
|
|
|
ast.generics
|
|
.make_where_clause()
|
|
.predicates
|
|
.push(parse_quote! { Self: Send + Sync + 'static });
|
|
|
|
let requires = &attrs.requires;
|
|
let mut register_required = Vec::with_capacity(attrs.requires.iter().len());
|
|
if let Some(requires) = requires {
|
|
for require in requires {
|
|
let ident = &require.path;
|
|
let constructor = match &require.func {
|
|
Some(func) => quote! { || { let x: #ident = (#func)().into(); x } },
|
|
None => quote! { <#ident as Default>::default },
|
|
};
|
|
register_required.push(quote! {
|
|
required_components.register_required::<#ident>(#constructor);
|
|
});
|
|
}
|
|
}
|
|
let struct_name = &ast.ident;
|
|
let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();
|
|
|
|
let required_component_docs = attrs.requires.map(|r| {
|
|
let paths = r
|
|
.iter()
|
|
.map(|r| format!("[`{}`]", r.path.to_token_stream()))
|
|
.collect::<Vec<_>>()
|
|
.join(", ");
|
|
let doc = format!("**Required Components**: {paths}. \n\n A component's Required Components are inserted whenever it is inserted. Note that this will also insert the required components _of_ the required components, recursively, in depth-first order.");
|
|
quote! {
|
|
#[doc = #doc]
|
|
}
|
|
});
|
|
|
|
let mutable_type = (attrs.immutable || relationship.is_some())
|
|
.then_some(quote! { #bevy_ecs_path::component::Immutable })
|
|
.unwrap_or(quote! { #bevy_ecs_path::component::Mutable });
|
|
|
|
let clone_behavior = if relationship_target.is_some() {
|
|
quote!(#bevy_ecs_path::component::ComponentCloneBehavior::Custom(#bevy_ecs_path::relationship::clone_relationship_target::<Self>))
|
|
} else if let Some(behavior) = attrs.clone_behavior {
|
|
quote!(#bevy_ecs_path::component::ComponentCloneBehavior::#behavior)
|
|
} else {
|
|
quote!(
|
|
use #bevy_ecs_path::component::{DefaultCloneBehaviorBase, DefaultCloneBehaviorViaClone};
|
|
(&&&#bevy_ecs_path::component::DefaultCloneBehaviorSpecialization::<Self>::default()).default_clone_behavior()
|
|
)
|
|
};
|
|
|
|
// This puts `register_required` before `register_recursive_requires` to ensure that the constructors of _all_ top
|
|
// level components are initialized first, giving them precedence over recursively defined constructors for the same component type
|
|
TokenStream::from(quote! {
|
|
#required_component_docs
|
|
impl #impl_generics #bevy_ecs_path::component::Component for #struct_name #type_generics #where_clause {
|
|
const STORAGE_TYPE: #bevy_ecs_path::component::StorageType = #storage;
|
|
type Mutability = #mutable_type;
|
|
fn register_required_components(
|
|
_requiree: #bevy_ecs_path::component::ComponentId,
|
|
required_components: &mut #bevy_ecs_path::component::RequiredComponentsRegistrator,
|
|
) {
|
|
#(#register_required)*
|
|
}
|
|
|
|
#on_add
|
|
#on_insert
|
|
#on_replace
|
|
#on_remove
|
|
#on_despawn
|
|
|
|
fn clone_behavior() -> #bevy_ecs_path::component::ComponentCloneBehavior {
|
|
#clone_behavior
|
|
}
|
|
|
|
#map_entities
|
|
}
|
|
|
|
#relationship
|
|
|
|
#relationship_target
|
|
})
|
|
}
|
|
|
|
const ENTITIES: &str = "entities";
|
|
|
|
pub(crate) fn map_entities(
|
|
data: &Data,
|
|
bevy_ecs_path: &Path,
|
|
self_ident: Ident,
|
|
is_relationship: bool,
|
|
is_relationship_target: bool,
|
|
map_entities_attr: Option<MapEntitiesAttributeKind>,
|
|
) -> Option<TokenStream2> {
|
|
if let Some(map_entities_override) = map_entities_attr {
|
|
let map_entities_tokens = map_entities_override.to_token_stream(bevy_ecs_path);
|
|
return Some(quote!(
|
|
#map_entities_tokens(#self_ident, mapper)
|
|
));
|
|
}
|
|
|
|
match data {
|
|
Data::Struct(DataStruct { fields, .. }) => {
|
|
let mut map = Vec::with_capacity(fields.len());
|
|
|
|
let relationship = if is_relationship || is_relationship_target {
|
|
relationship_field(fields, "MapEntities", fields.span()).ok()
|
|
} else {
|
|
None
|
|
};
|
|
fields
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(_, field)| {
|
|
field.attrs.iter().any(|a| a.path().is_ident(ENTITIES))
|
|
|| relationship.is_some_and(|relationship| relationship == *field)
|
|
})
|
|
.for_each(|(index, field)| {
|
|
let field_member = field
|
|
.ident
|
|
.clone()
|
|
.map_or(Member::from(index), Member::Named);
|
|
|
|
map.push(quote!(#self_ident.#field_member.map_entities(mapper);));
|
|
});
|
|
if map.is_empty() {
|
|
return None;
|
|
};
|
|
Some(quote!(
|
|
#(#map)*
|
|
))
|
|
}
|
|
Data::Enum(DataEnum { variants, .. }) => {
|
|
let mut map = Vec::with_capacity(variants.len());
|
|
|
|
for variant in variants.iter() {
|
|
let field_members = variant
|
|
.fields
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(_, field)| field.attrs.iter().any(|a| a.path().is_ident(ENTITIES)))
|
|
.map(|(index, field)| {
|
|
field
|
|
.ident
|
|
.clone()
|
|
.map_or(Member::from(index), Member::Named)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let ident = &variant.ident;
|
|
let field_idents = field_members
|
|
.iter()
|
|
.map(|member| format_ident!("__self_{}", member))
|
|
.collect::<Vec<_>>();
|
|
|
|
map.push(
|
|
quote!(Self::#ident {#(#field_members: #field_idents,)* ..} => {
|
|
#(#field_idents.map_entities(mapper);)*
|
|
}),
|
|
);
|
|
}
|
|
|
|
if map.is_empty() {
|
|
return None;
|
|
};
|
|
|
|
Some(quote!(
|
|
match #self_ident {
|
|
#(#map,)*
|
|
_ => {}
|
|
}
|
|
))
|
|
}
|
|
Data::Union(_) => None,
|
|
}
|
|
}
|
|
|
|
pub const COMPONENT: &str = "component";
|
|
pub const STORAGE: &str = "storage";
|
|
pub const REQUIRE: &str = "require";
|
|
pub const RELATIONSHIP: &str = "relationship";
|
|
pub const RELATIONSHIP_TARGET: &str = "relationship_target";
|
|
|
|
pub const ON_ADD: &str = "on_add";
|
|
pub const ON_INSERT: &str = "on_insert";
|
|
pub const ON_REPLACE: &str = "on_replace";
|
|
pub const ON_REMOVE: &str = "on_remove";
|
|
pub const ON_DESPAWN: &str = "on_despawn";
|
|
pub const MAP_ENTITIES: &str = "map_entities";
|
|
|
|
pub const IMMUTABLE: &str = "immutable";
|
|
pub const CLONE_BEHAVIOR: &str = "clone_behavior";
|
|
|
|
/// All allowed attribute value expression kinds for component hooks.
|
|
/// This doesn't simply use general expressions because of conflicting needs:
|
|
/// - we want to be able to use `Self` & generic parameters in paths
|
|
/// - call expressions producing a closure need to be wrapped in a function
|
|
/// to turn them into function pointers, which prevents access to the outer generic params
|
|
#[derive(Debug)]
|
|
enum HookAttributeKind {
|
|
/// expressions like function or struct names
|
|
///
|
|
/// structs will throw compile errors on the code generation so this is safe
|
|
Path(ExprPath),
|
|
/// function call like expressions
|
|
Call(ExprCall),
|
|
}
|
|
|
|
impl HookAttributeKind {
|
|
fn from_expr(value: Expr) -> Result<Self> {
|
|
match value {
|
|
Expr::Path(path) => Ok(HookAttributeKind::Path(path)),
|
|
Expr::Call(call) => Ok(HookAttributeKind::Call(call)),
|
|
// throw meaningful error on all other expressions
|
|
_ => Err(syn::Error::new(
|
|
value.span(),
|
|
[
|
|
"Not supported in this position, please use one of the following:",
|
|
"- path to function",
|
|
"- call to function yielding closure",
|
|
]
|
|
.join("\n"),
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn to_token_stream(&self, bevy_ecs_path: &Path) -> TokenStream2 {
|
|
match self {
|
|
HookAttributeKind::Path(path) => path.to_token_stream(),
|
|
HookAttributeKind::Call(call) => {
|
|
quote!({
|
|
fn _internal_hook(world: #bevy_ecs_path::world::DeferredWorld, ctx: #bevy_ecs_path::lifecycle::HookContext) {
|
|
(#call)(world, ctx)
|
|
}
|
|
_internal_hook
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for HookAttributeKind {
|
|
fn parse(input: syn::parse::ParseStream) -> Result<Self> {
|
|
input.parse::<Expr>().and_then(Self::from_expr)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(super) enum MapEntitiesAttributeKind {
|
|
/// expressions like function or struct names
|
|
///
|
|
/// structs will throw compile errors on the code generation so this is safe
|
|
Path(ExprPath),
|
|
/// When no value is specified
|
|
Default,
|
|
}
|
|
|
|
impl MapEntitiesAttributeKind {
|
|
fn from_expr(value: Expr) -> Result<Self> {
|
|
match value {
|
|
Expr::Path(path) => Ok(Self::Path(path)),
|
|
// throw meaningful error on all other expressions
|
|
_ => Err(syn::Error::new(
|
|
value.span(),
|
|
[
|
|
"Not supported in this position, please use one of the following:",
|
|
"- path to function",
|
|
"- nothing to default to MapEntities implementation",
|
|
]
|
|
.join("\n"),
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn to_token_stream(&self, bevy_ecs_path: &Path) -> TokenStream2 {
|
|
match self {
|
|
MapEntitiesAttributeKind::Path(path) => path.to_token_stream(),
|
|
MapEntitiesAttributeKind::Default => {
|
|
quote!(
|
|
<Self as #bevy_ecs_path::entity::MapEntities>::map_entities
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for MapEntitiesAttributeKind {
|
|
fn parse(input: syn::parse::ParseStream) -> Result<Self> {
|
|
if input.peek(Token![=]) {
|
|
input.parse::<Token![=]>()?;
|
|
input.parse::<Expr>().and_then(Self::from_expr)
|
|
} else {
|
|
Ok(Self::Default)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Attrs {
|
|
storage: StorageTy,
|
|
requires: Option<Punctuated<Require, Comma>>,
|
|
on_add: Option<HookAttributeKind>,
|
|
on_insert: Option<HookAttributeKind>,
|
|
on_replace: Option<HookAttributeKind>,
|
|
on_remove: Option<HookAttributeKind>,
|
|
on_despawn: Option<HookAttributeKind>,
|
|
relationship: Option<Relationship>,
|
|
relationship_target: Option<RelationshipTarget>,
|
|
immutable: bool,
|
|
clone_behavior: Option<Expr>,
|
|
map_entities: Option<MapEntitiesAttributeKind>,
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
enum StorageTy {
|
|
Table,
|
|
SparseSet,
|
|
}
|
|
|
|
struct Require {
|
|
path: Path,
|
|
func: Option<TokenStream2>,
|
|
}
|
|
|
|
struct Relationship {
|
|
relationship_target: Type,
|
|
}
|
|
|
|
struct RelationshipTarget {
|
|
relationship: Type,
|
|
linked_spawn: bool,
|
|
}
|
|
|
|
// values for `storage` attribute
|
|
const TABLE: &str = "Table";
|
|
const SPARSE_SET: &str = "SparseSet";
|
|
|
|
fn parse_component_attr(ast: &DeriveInput) -> Result<Attrs> {
|
|
let mut attrs = Attrs {
|
|
storage: StorageTy::Table,
|
|
on_add: None,
|
|
on_insert: None,
|
|
on_replace: None,
|
|
on_remove: None,
|
|
on_despawn: None,
|
|
requires: None,
|
|
relationship: None,
|
|
relationship_target: None,
|
|
immutable: false,
|
|
clone_behavior: None,
|
|
map_entities: None,
|
|
};
|
|
|
|
let mut require_paths = HashSet::new();
|
|
for attr in ast.attrs.iter() {
|
|
if attr.path().is_ident(COMPONENT) {
|
|
attr.parse_nested_meta(|nested| {
|
|
if nested.path.is_ident(STORAGE) {
|
|
attrs.storage = match nested.value()?.parse::<LitStr>()?.value() {
|
|
s if s == TABLE => StorageTy::Table,
|
|
s if s == SPARSE_SET => StorageTy::SparseSet,
|
|
s => {
|
|
return Err(nested.error(format!(
|
|
"Invalid storage type `{s}`, expected '{TABLE}' or '{SPARSE_SET}'.",
|
|
)));
|
|
}
|
|
};
|
|
Ok(())
|
|
} else if nested.path.is_ident(ON_ADD) {
|
|
attrs.on_add = Some(nested.value()?.parse::<HookAttributeKind>()?);
|
|
Ok(())
|
|
} else if nested.path.is_ident(ON_INSERT) {
|
|
attrs.on_insert = Some(nested.value()?.parse::<HookAttributeKind>()?);
|
|
Ok(())
|
|
} else if nested.path.is_ident(ON_REPLACE) {
|
|
attrs.on_replace = Some(nested.value()?.parse::<HookAttributeKind>()?);
|
|
Ok(())
|
|
} else if nested.path.is_ident(ON_REMOVE) {
|
|
attrs.on_remove = Some(nested.value()?.parse::<HookAttributeKind>()?);
|
|
Ok(())
|
|
} else if nested.path.is_ident(ON_DESPAWN) {
|
|
attrs.on_despawn = Some(nested.value()?.parse::<HookAttributeKind>()?);
|
|
Ok(())
|
|
} else if nested.path.is_ident(IMMUTABLE) {
|
|
attrs.immutable = true;
|
|
Ok(())
|
|
} else if nested.path.is_ident(CLONE_BEHAVIOR) {
|
|
attrs.clone_behavior = Some(nested.value()?.parse()?);
|
|
Ok(())
|
|
} else if nested.path.is_ident(MAP_ENTITIES) {
|
|
attrs.map_entities = Some(nested.input.parse::<MapEntitiesAttributeKind>()?);
|
|
Ok(())
|
|
} else {
|
|
Err(nested.error("Unsupported attribute"))
|
|
}
|
|
})?;
|
|
} else if attr.path().is_ident(REQUIRE) {
|
|
let punctuated =
|
|
attr.parse_args_with(Punctuated::<Require, Comma>::parse_terminated)?;
|
|
for require in punctuated.iter() {
|
|
if !require_paths.insert(require.path.to_token_stream().to_string()) {
|
|
return Err(syn::Error::new(
|
|
require.path.span(),
|
|
"Duplicate required components are not allowed.",
|
|
));
|
|
}
|
|
}
|
|
if let Some(current) = &mut attrs.requires {
|
|
current.extend(punctuated);
|
|
} else {
|
|
attrs.requires = Some(punctuated);
|
|
}
|
|
} else if attr.path().is_ident(RELATIONSHIP) {
|
|
let relationship = attr.parse_args::<Relationship>()?;
|
|
attrs.relationship = Some(relationship);
|
|
} else if attr.path().is_ident(RELATIONSHIP_TARGET) {
|
|
let relationship_target = attr.parse_args::<RelationshipTarget>()?;
|
|
attrs.relationship_target = Some(relationship_target);
|
|
}
|
|
}
|
|
|
|
if attrs.relationship_target.is_some() && attrs.clone_behavior.is_some() {
|
|
return Err(syn::Error::new(
|
|
attrs.clone_behavior.span(),
|
|
"A Relationship Target already has its own clone behavior, please remove `clone_behavior = ...`",
|
|
));
|
|
}
|
|
|
|
Ok(attrs)
|
|
}
|
|
|
|
impl Parse for Require {
|
|
fn parse(input: syn::parse::ParseStream) -> Result<Self> {
|
|
let mut path = input.parse::<Path>()?;
|
|
let mut last_segment_is_lower = false;
|
|
let mut is_constructor_call = false;
|
|
|
|
// Use the case of the type name to check if it's an enum
|
|
// This doesn't match everything that can be an enum according to the rust spec
|
|
// but it matches what clippy is OK with
|
|
let is_enum = {
|
|
let mut first_chars = path
|
|
.segments
|
|
.iter()
|
|
.rev()
|
|
.filter_map(|s| s.ident.to_string().chars().next());
|
|
if let Some(last) = first_chars.next() {
|
|
if last.is_uppercase() {
|
|
if let Some(last) = first_chars.next() {
|
|
last.is_uppercase()
|
|
} else {
|
|
false
|
|
}
|
|
} else {
|
|
last_segment_is_lower = true;
|
|
false
|
|
}
|
|
} else {
|
|
false
|
|
}
|
|
};
|
|
|
|
let func = if input.peek(Token![=]) {
|
|
// If there is an '=', then this is a "function style" require
|
|
input.parse::<Token![=]>()?;
|
|
let expr: Expr = input.parse()?;
|
|
Some(quote!(|| #expr ))
|
|
} else if input.peek(Brace) {
|
|
// This is a "value style" named-struct-like require
|
|
let content;
|
|
braced!(content in input);
|
|
let content = content.parse::<TokenStream2>()?;
|
|
Some(quote!(|| #path { #content }))
|
|
} else if input.peek(Paren) {
|
|
// This is a "value style" tuple-struct-like require
|
|
let content;
|
|
parenthesized!(content in input);
|
|
let content = content.parse::<TokenStream2>()?;
|
|
is_constructor_call = last_segment_is_lower;
|
|
Some(quote!(|| #path (#content)))
|
|
} else if is_enum {
|
|
// if this is an enum, then it is an inline enum component declaration
|
|
Some(quote!(|| #path))
|
|
} else {
|
|
// if this isn't any of the above, then it is a component ident, which will use Default
|
|
None
|
|
};
|
|
if is_enum || is_constructor_call {
|
|
path.segments.pop();
|
|
path.segments.pop_punct();
|
|
}
|
|
Ok(Require { path, func })
|
|
}
|
|
}
|
|
|
|
fn storage_path(bevy_ecs_path: &Path, ty: StorageTy) -> TokenStream2 {
|
|
let storage_type = match ty {
|
|
StorageTy::Table => Ident::new("Table", Span::call_site()),
|
|
StorageTy::SparseSet => Ident::new("SparseSet", Span::call_site()),
|
|
};
|
|
|
|
quote! { #bevy_ecs_path::component::StorageType::#storage_type }
|
|
}
|
|
|
|
fn hook_register_function_call(
|
|
bevy_ecs_path: &Path,
|
|
hook: TokenStream2,
|
|
function: Option<TokenStream2>,
|
|
) -> Option<TokenStream2> {
|
|
function.map(|meta| {
|
|
quote! {
|
|
fn #hook() -> ::core::option::Option<#bevy_ecs_path::lifecycle::ComponentHook> {
|
|
::core::option::Option::Some(#meta)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
mod kw {
|
|
syn::custom_keyword!(relationship_target);
|
|
syn::custom_keyword!(relationship);
|
|
syn::custom_keyword!(linked_spawn);
|
|
}
|
|
|
|
impl Parse for Relationship {
|
|
fn parse(input: syn::parse::ParseStream) -> Result<Self> {
|
|
input.parse::<kw::relationship_target>()?;
|
|
input.parse::<Token![=]>()?;
|
|
Ok(Relationship {
|
|
relationship_target: input.parse::<Type>()?,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Parse for RelationshipTarget {
|
|
fn parse(input: syn::parse::ParseStream) -> Result<Self> {
|
|
let mut relationship: Option<Type> = None;
|
|
let mut linked_spawn: bool = false;
|
|
|
|
while !input.is_empty() {
|
|
let lookahead = input.lookahead1();
|
|
if lookahead.peek(kw::linked_spawn) {
|
|
input.parse::<kw::linked_spawn>()?;
|
|
linked_spawn = true;
|
|
} else if lookahead.peek(kw::relationship) {
|
|
input.parse::<kw::relationship>()?;
|
|
input.parse::<Token![=]>()?;
|
|
relationship = Some(input.parse()?);
|
|
} else {
|
|
return Err(lookahead.error());
|
|
}
|
|
if !input.is_empty() {
|
|
input.parse::<Token![,]>()?;
|
|
}
|
|
}
|
|
Ok(RelationshipTarget {
|
|
relationship: relationship.ok_or_else(|| {
|
|
syn::Error::new(input.span(), "Missing `relationship = X` attribute")
|
|
})?,
|
|
linked_spawn,
|
|
})
|
|
}
|
|
}
|
|
|
|
fn derive_relationship(
|
|
ast: &DeriveInput,
|
|
attrs: &Attrs,
|
|
bevy_ecs_path: &Path,
|
|
) -> Result<Option<TokenStream2>> {
|
|
let Some(relationship) = &attrs.relationship else {
|
|
return Ok(None);
|
|
};
|
|
let Data::Struct(DataStruct {
|
|
fields,
|
|
struct_token,
|
|
..
|
|
}) = &ast.data
|
|
else {
|
|
return Err(syn::Error::new(
|
|
ast.span(),
|
|
"Relationship can only be derived for structs.",
|
|
));
|
|
};
|
|
let field = relationship_field(fields, "Relationship", struct_token.span())?;
|
|
|
|
let relationship_member = field.ident.clone().map_or(Member::from(0), Member::Named);
|
|
let members = fields
|
|
.members()
|
|
.filter(|member| member != &relationship_member);
|
|
|
|
let struct_name = &ast.ident;
|
|
let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();
|
|
|
|
let relationship_target = &relationship.relationship_target;
|
|
|
|
Ok(Some(quote! {
|
|
impl #impl_generics #bevy_ecs_path::relationship::Relationship for #struct_name #type_generics #where_clause {
|
|
type RelationshipTarget = #relationship_target;
|
|
|
|
#[inline(always)]
|
|
fn get(&self) -> #bevy_ecs_path::entity::Entity {
|
|
self.#relationship_member
|
|
}
|
|
|
|
#[inline]
|
|
fn from(entity: #bevy_ecs_path::entity::Entity) -> Self {
|
|
Self {
|
|
#(#members: core::default::Default::default(),)*
|
|
#relationship_member: entity
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn set_risky(&mut self, entity: Entity) {
|
|
self.#relationship_member = entity;
|
|
}
|
|
}
|
|
}))
|
|
}
|
|
|
|
fn derive_relationship_target(
|
|
ast: &DeriveInput,
|
|
attrs: &Attrs,
|
|
bevy_ecs_path: &Path,
|
|
) -> Result<Option<TokenStream2>> {
|
|
let Some(relationship_target) = &attrs.relationship_target else {
|
|
return Ok(None);
|
|
};
|
|
|
|
let Data::Struct(DataStruct {
|
|
fields,
|
|
struct_token,
|
|
..
|
|
}) = &ast.data
|
|
else {
|
|
return Err(syn::Error::new(
|
|
ast.span(),
|
|
"RelationshipTarget can only be derived for structs.",
|
|
));
|
|
};
|
|
let field = relationship_field(fields, "RelationshipTarget", struct_token.span())?;
|
|
|
|
if field.vis != Visibility::Inherited {
|
|
return Err(syn::Error::new(field.span(), "The collection in RelationshipTarget must be private to prevent users from directly mutating it, which could invalidate the correctness of relationships."));
|
|
}
|
|
let collection = &field.ty;
|
|
let relationship_member = field.ident.clone().map_or(Member::from(0), Member::Named);
|
|
|
|
let members = fields
|
|
.members()
|
|
.filter(|member| member != &relationship_member);
|
|
|
|
let relationship = &relationship_target.relationship;
|
|
let struct_name = &ast.ident;
|
|
let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();
|
|
let linked_spawn = relationship_target.linked_spawn;
|
|
Ok(Some(quote! {
|
|
impl #impl_generics #bevy_ecs_path::relationship::RelationshipTarget for #struct_name #type_generics #where_clause {
|
|
const LINKED_SPAWN: bool = #linked_spawn;
|
|
type Relationship = #relationship;
|
|
type Collection = #collection;
|
|
|
|
#[inline]
|
|
fn collection(&self) -> &Self::Collection {
|
|
&self.#relationship_member
|
|
}
|
|
|
|
#[inline]
|
|
fn collection_mut_risky(&mut self) -> &mut Self::Collection {
|
|
&mut self.#relationship_member
|
|
}
|
|
|
|
#[inline]
|
|
fn from_collection_risky(collection: Self::Collection) -> Self {
|
|
Self {
|
|
#(#members: core::default::Default::default(),)*
|
|
#relationship_member: collection
|
|
}
|
|
}
|
|
}
|
|
}))
|
|
}
|
|
|
|
/// Returns the field with the `#[relationship]` attribute, the only field if unnamed,
|
|
/// or the only field in a [`Fields::Named`] with one field, otherwise `Err`.
|
|
fn relationship_field<'a>(
|
|
fields: &'a Fields,
|
|
derive: &'static str,
|
|
span: Span,
|
|
) -> Result<&'a Field> {
|
|
match fields {
|
|
Fields::Named(fields) if fields.named.len() == 1 => Ok(fields.named.first().unwrap()),
|
|
Fields::Named(fields) => fields.named.iter().find(|field| {
|
|
field
|
|
.attrs
|
|
.iter()
|
|
.any(|attr| attr.path().is_ident(RELATIONSHIP))
|
|
}).ok_or(syn::Error::new(
|
|
span,
|
|
format!("{derive} derive expected named structs with a single field or with a field annotated with #[relationship].")
|
|
)),
|
|
Fields::Unnamed(fields) => fields
|
|
.unnamed
|
|
.len()
|
|
.eq(&1)
|
|
.then(|| fields.unnamed.first())
|
|
.flatten()
|
|
.ok_or(syn::Error::new(
|
|
span,
|
|
format!("{derive} derive expected unnamed structs with one field."),
|
|
)),
|
|
Fields::Unit => Err(syn::Error::new(
|
|
span,
|
|
format!("{derive} derive expected named or unnamed struct, found unit struct."),
|
|
)),
|
|
}
|
|
}
|