
# Objective The objective of this PR is to enable Components to use their `MapEntities` implementation for `Component::map_entities`. With the improvements to the entity mapping system, there is definitely a huge reduction in boilerplate. However, especially since `(Entity)HashMap<..>` doesn't implement `MapEntities` (I presume because the lack of specialization in rust makes `HashMap<Entity|X, Entity|X>` complicated), when somebody has types that contain these hashmaps they can't use this approach. More so, we can't even depend on the previous implementation, since `Component::map_entities` is used instead of `MapEntities::map_entities`. Outside of implementing `Component `and `Component::map_entities` on these types directly, the only path forward is to create a custom type to wrap the hashmaps and implement map entities on that, or split these components into a wrapper type that implement `Component`, and an inner type that implements `MapEntities`. ## Current Solution The solution was to allow adding `#[component(map_entities)]` on the component. By default this will defer to the `MapEntities` implementation. ```rust #[derive(Component)] #[component(map_entities)] struct Inventory { items: HashMap<Entity, usize> } impl MapEntities for Inventory { fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M) { self.items = self.items .drain() .map(|(id, count)|(entity_mapper.get_mapped(id), count)) .collect(); } } ``` You can use `#[component(map_entities = <function path>)]` instead to substitute other code in for components. This function can also include generics, but sso far I haven't been able to find a case where they are needed. ```rust #[derive(Component)] #[component(map_entities = map_the_map)] // Also works #[component(map_entities = map_the_map::<T,_>)] struct Inventory<T> { items: HashMap<Entity, T> } fn map_the_map<T, M: EntityMapper>(inv: &mut Inventory<T>, entity_mapper: &mut M) { inv.items = inv.items .drain() .map(|(id, count)|(entity_mapper.get_mapped(id), count)) .collect(); } ``` The idea is that with the previous changes to MapEntities, MapEntities is implemented more for entity collections than for Components. If you have a component that makes sense as both, `#[component(map_entities)]` would work great, while otherwise a component can use `#[component(map_entities = <function>)]` to change the behavior of `Component::map_entities` without opening up the component type to be included in other components. ## (Original Solution if you want to follow the PR) The solution was to allow adding `#[component(entities)]` on the component itself to defer to the `MapEntities` implementation ```rust #[derive(Component)] #[component(entities)] struct Inventory { items: HashMap<Entity, usize> } impl MapEntities for Inventory { fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M) { self.items = self.items .drain() .map(|(id, count)|(entity_mapper.get_mapped(id), count)) .collect(); } } ``` ## Testing I tested this by patching my local changes into my own bevy project. I had a system that loads a scene file and executes some logic with a Component that contains a `HashMap<Entity, UVec2>`, and it panics when Entity is not found from another query. Since the 0.16 update this system has reliably panicked upon attempting to the load the scene. After patching my code in, I added `#[component(entities)]` to this component, and I was able to successfully load the scene. Additionally, I wrote a doc test. ## Call-outs ### Relationships This overrules the default mapping of relationship fields. Anything else seemed more problematic, as you'd have inconsistent behavior between `MapEntities` and `Component`.
968 lines
34 KiB
Rust
968 lines
34 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();
|
|
|
|
ast.generics
|
|
.make_where_clause()
|
|
.predicates
|
|
.push(parse_quote! { Self: Send + Sync + 'static });
|
|
|
|
if let Some(attr) = ast.attrs.iter().find(|attr| attr.path().is_ident(EVENT)) {
|
|
if let Err(e) = attr.parse_nested_meta(|meta| match meta.path.get_ident() {
|
|
Some(ident) if ident == AUTO_PROPAGATE => {
|
|
auto_propagate = true;
|
|
Ok(())
|
|
}
|
|
Some(ident) if ident == TRAVERSAL => {
|
|
traversal = meta.value()?.parse()?;
|
|
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 {
|
|
}
|
|
})
|
|
}
|
|
|
|
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());
|
|
let mut register_recursive_requires = Vec::with_capacity(attrs.requires.iter().len());
|
|
if let Some(requires) = requires {
|
|
for require in requires {
|
|
let ident = &require.path;
|
|
register_recursive_requires.push(quote! {
|
|
<#ident as #bevy_ecs_path::component::Component>::register_required_components(
|
|
requiree,
|
|
components,
|
|
required_components,
|
|
inheritance_depth + 1,
|
|
recursion_check_stack
|
|
);
|
|
});
|
|
match &require.func {
|
|
Some(func) => {
|
|
register_required.push(quote! {
|
|
components.register_required_components_manual::<Self, #ident>(
|
|
required_components,
|
|
|| { let x: #ident = (#func)().into(); x },
|
|
inheritance_depth,
|
|
recursion_check_stack
|
|
);
|
|
});
|
|
}
|
|
None => {
|
|
register_required.push(quote! {
|
|
components.register_required_components_manual::<Self, #ident>(
|
|
required_components,
|
|
<#ident as Default>::default,
|
|
inheritance_depth,
|
|
recursion_check_stack
|
|
);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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,
|
|
components: &mut #bevy_ecs_path::component::ComponentsRegistrator,
|
|
required_components: &mut #bevy_ecs_path::component::RequiredComponents,
|
|
inheritance_depth: u16,
|
|
recursion_check_stack: &mut #bevy_ecs_path::__macro_exports::Vec<#bevy_ecs_path::component::ComponentId>
|
|
) {
|
|
#bevy_ecs_path::component::enforce_no_required_components_recursion(components, recursion_check_stack);
|
|
let self_id = components.register_component::<Self>();
|
|
recursion_check_stack.push(self_id);
|
|
#(#register_required)*
|
|
#(#register_recursive_requires)*
|
|
recursion_check_stack.pop();
|
|
}
|
|
|
|
#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
|
|
#[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."),
|
|
)),
|
|
}
|
|
}
|