diff --git a/Cargo.toml b/Cargo.toml index 92c1396..60b3c5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,10 @@ path = "examples/basic.rs" name = "bundles" path = "examples/bundles.rs" +[[example]] +name = "attributes" +path = "examples/attributes.rs" + [[example]] name = "templates" path = "examples/templates.rs" diff --git a/README.md b/README.md index dea62ec..8c79b7e 100644 --- a/README.md +++ b/README.md @@ -131,21 +131,19 @@ For simple components, `ProtoComponent` may be derived: ```rust use bevy_proto::ProtoComponent; -#[derive(Serialize, Deserialize, ProtoComponent)] +#[derive(Clone, Serialize, Deserialize, ProtoComponent, Component)] struct Movement { - #[proto_comp(Copy)] speed: u16 } // Also works on tuple structs: -#[derive(Serialize, Deserialize, ProtoComponent)] -struct Inventory ( - // Optional: #[proto_comp(Clone)] +#[derive(Clone, Serialize, Deserialize, ProtoComponent, Component)] +struct Inventory ( Option> -) +); ``` -> By default, the fields of a `ProtoComponent` are cloned into spawned entities. This can be somewhat controlled via the `proto_comp` attribute, which can tell the compiler to use the `Copy` trait instead. +> By default, the `ProtoComponent` is cloned into spawned entities. Otherwise, you can define them manually (the two attributes are required with this method): @@ -170,6 +168,8 @@ impl ProtoComponent for Inventory { ``` > A `ProtoComponent` does *not* need to be a component itself. It can be used purely as a [DTO](https://en.wikipedia.org/wiki/Data_transfer_object) to generate other components or bundles. You have full access to the `EntityCommands` so you can insert bundles or even multiple components at once. +> +> Other ways of generating components from non-component `ProtoComponent` structs can be found in the [attributes](https://github.com/MrGVSV/bevy_proto/blob/main/examples/attributes.rs) example. ### Defining the Prototype diff --git a/assets/prototypes/emoji_angry.yaml b/assets/prototypes/emoji_angry.yaml new file mode 100644 index 0000000..df5604f --- /dev/null +++ b/assets/prototypes/emoji_angry.yaml @@ -0,0 +1,5 @@ +--- +name: "Angry" +components: + - type: Face + value: Frowning \ No newline at end of file diff --git a/assets/prototypes/emoji_happy.yaml b/assets/prototypes/emoji_happy.yaml new file mode 100644 index 0000000..cfb2305 --- /dev/null +++ b/assets/prototypes/emoji_happy.yaml @@ -0,0 +1,6 @@ +--- +name: "Happy" +components: + - type: EmojiDef + value: + emoji: 😁 diff --git a/assets/prototypes/emoji_sad.yaml b/assets/prototypes/emoji_sad.yaml new file mode 100644 index 0000000..adb30a3 --- /dev/null +++ b/assets/prototypes/emoji_sad.yaml @@ -0,0 +1,6 @@ +--- +name: "Sad" +components: + - type: EmojiDef + value: + emoji: ðŸ˜Ē diff --git a/assets/prototypes/emoji_silly.yaml b/assets/prototypes/emoji_silly.yaml new file mode 100644 index 0000000..7054578 --- /dev/null +++ b/assets/prototypes/emoji_silly.yaml @@ -0,0 +1,5 @@ +--- +name: "Silly" +components: + - type: Mood + value: Silly diff --git a/bevy_proto_derive/src/attributes.rs b/bevy_proto_derive/src/attributes.rs new file mode 100644 index 0000000..ed69a69 --- /dev/null +++ b/bevy_proto_derive/src/attributes.rs @@ -0,0 +1,71 @@ +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{format_ident, quote, ToTokens}; +use syn::parse::{Parse, ParseStream}; +use syn::{Error, LitStr, Path, Result, Token}; + +use crate::constants::{INTO_IDENT, WITH_IDENT}; + +/// ProtoComponent attributes applied on structs +pub(crate) enum ProtoCompAttr { + /// Captures the `#[proto_comp(into = "ActualComponent")]` attribute + /// + /// This is used to specify a separate Component that this marked struct will be cloned into. + /// + /// Generates the following code: + /// ```rust + /// let component: ActualComponent = self.clone().into(); + /// commands.insert(component); + /// ``` + Into(Ident), + /// Captures the `#[proto_comp(with = "my_function")]` attribute + /// + /// This is used to specify a custom function with which custom Components will be creatde and/or inserted. + /// This is essentially identical to just simply implementing `ProtoComponent` yourself. + /// + /// Generates the following code: + /// ```rust + /// my_function(self, commands, asset_server); + /// ``` + With(Ident), +} + +impl Parse for ProtoCompAttr { + fn parse(input: ParseStream) -> Result { + let path: Path = input.parse()?; + let _: Token![=] = input.parse()?; + let item: LitStr = input.parse()?; + let ident = format_ident!("{}", item.value()); + + if path == WITH_IDENT { + Ok(Self::With(ident)) + } else if path == INTO_IDENT { + Ok(Self::Into(ident)) + } else { + Err(Error::new( + Span::call_site(), + format!("Unexpected path '{:?}'", path), + )) + } + } +} + +impl ToTokens for ProtoCompAttr { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Self::Into(ident) => { + let into_ident = quote! { + let cloned = self.clone(); + let component: #ident = cloned.into(); + commands.insert(component); + }; + into_ident.to_tokens(tokens); + } + Self::With(ident) => { + let with_ident = quote! { + #ident(self, commands, asset_server); + }; + with_ident.to_tokens(tokens); + } + } + } +} diff --git a/bevy_proto_derive/src/constants.rs b/bevy_proto_derive/src/constants.rs index 12b7915..9915de2 100644 --- a/bevy_proto_derive/src/constants.rs +++ b/bevy_proto_derive/src/constants.rs @@ -14,6 +14,5 @@ impl PartialEq for syn::Path { } } -pub(crate) const ATTR_IDENT: Symbol = Symbol("proto_comp"); -pub(crate) const COPY_IDENT: Symbol = Symbol("Copy"); -pub(crate) const CLONE_IDENT: Symbol = Symbol("Clone"); +pub(crate) const WITH_IDENT: Symbol = Symbol("with"); +pub(crate) const INTO_IDENT: Symbol = Symbol("into"); diff --git a/bevy_proto_derive/src/fields.rs b/bevy_proto_derive/src/fields.rs deleted file mode 100644 index 83f9cdd..0000000 --- a/bevy_proto_derive/src/fields.rs +++ /dev/null @@ -1,104 +0,0 @@ -//! This module provides some helper functions for processing field data, -//! namely their attributes - -use std::iter::Peekable; -use std::slice::Iter; - -use syn::*; - -use crate::constants::{ATTR_IDENT, CLONE_IDENT, COPY_IDENT}; - -/// Enum used to specify how an item's data should be duplicated -#[derive(Debug)] -pub(crate) enum ProtoCompDupeAttr { - /// Specifies the tagged item should be duplicated via Copy - AttrCopy, - /// Specifies the tagged item should be duplicated via Clone - AttrClone, - // TODO: Allow custom duplication method: - // AttrCustom(Duplicator), -} - -impl ProtoCompDupeAttr { - /// Attempts to parse some metadata into a [`ProtoCompDupeAttr`] - /// - /// # Arguments - /// - /// * `meta`: The meta to parse - /// - /// returns: Option - fn from_meta(meta: &Meta) -> Option { - let path = meta.path(); - if path == COPY_IDENT { - Some(ProtoCompDupeAttr::AttrCopy) - } else if path == CLONE_IDENT { - Some(ProtoCompDupeAttr::AttrClone) - } else { - None - } - } -} - -impl Default for ProtoCompDupeAttr { - fn default() -> Self { - Self::AttrClone - } -} - -/// For the given field, attempt to find an attribute determining how its -/// data should be duplicated (e.g. Copy, Clone, etc.). -/// -/// Only the first valid attribute will be used -/// -/// # Arguments -/// -/// * `field`: The field to inspect -/// -/// returns: Option -pub(crate) fn get_dupe_attr(field: &Field) -> Option { - let mut dupe: Option = None; - let mut iter: Peekable> = field.attrs.iter().peekable(); - - // Try to find a valid attribute specifying the duplication method - // This loop checks each attribute and finds the first one to meet - // that conditiion. - while dupe.is_none() && iter.peek().is_some() { - let meta = find_attr_meta(&mut iter)?; - - dupe = match meta { - Meta::List(ref list) => { - let nested = list.nested.first().unwrap(); - - match nested { - NestedMeta::Meta(meta) => ProtoCompDupeAttr::from_meta(meta), - _ => None, - } - } - _ => None, - }; - } - - dupe -} - -/// Tries to find an attribute with path: [`ATTR_NAME`] -/// -/// # Arguments -/// -/// * `attrs`: The list of attributes for this item -/// -/// returns: Option<&Attribute> -pub(crate) fn find_attr<'a>(attrs: &'a mut Peekable>) -> Option<&'a Attribute> { - attrs.find(|attr| attr.path == ATTR_IDENT) -} - -/// Tries to find an attribute with path: [`ATTR_NAME`], and returns its [`Meta`] if found -/// -/// # Arguments -/// -/// * `attrs`: The list of attributes for this item -/// -/// returns: Option -fn find_attr_meta(attrs: &mut Peekable>) -> Option { - find_attr(attrs).and_then(|attr| attr.parse_meta().ok()) -} diff --git a/bevy_proto_derive/src/lib.rs b/bevy_proto_derive/src/lib.rs index 5778777..ebd0c65 100644 --- a/bevy_proto_derive/src/lib.rs +++ b/bevy_proto_derive/src/lib.rs @@ -1,30 +1,27 @@ use proc_macro::TokenStream; -use fields::ProtoCompDupeAttr; -use proc_macro2::{Span, TokenStream as TokenStream2}; +use proc_macro2::Span; use quote::quote; use syn::*; +use crate::attributes::ProtoCompAttr; + +mod attributes; mod constants; -mod fields; /// Automatically implements [`ProtoComponent`] for the given -/// struct. This works on all structs, including tuple and unit structs. Enums are not -/// currently supported. +/// struct or enum. This works on all structs and enums, including tuple and unit structs. /// -/// **NOTE: [`serde::Serialize`] and [`serde::Deserialize`] must also be implemented/derived** +/// **NOTE: `Clone`, `serde::Serialize`, and `serde::Deserialize` must also be implemented/derived** /// /// # Examples /// /// ``` -/// use serde::{Deserialize, Serialize}; +/// # use serde::{Deserialize, Serialize}; /// -/// #[derive(Serialize, Deserialize, ProtoComponent)] +/// #[derive(Clone, Serialize, Deserialize, ProtoComponent)] /// struct SomeComponent { -/// // Optional: #[proto_comp(Clone)] /// some_string: String, -/// -/// #[proto_comp(Copy)] /// some_int: i32, /// } /// @@ -33,125 +30,60 @@ mod fields; /// // #[typetag::serde] /// // impl bevy_proto::ProtoComponent for #ident { /// /// // fn insert_self( -/// // &self, -/// // commands: &mut bevy_proto::ProtoCommands, -/// // asset_server: &bevy::prelude::Res, -/// // ) { -/// // let component = Self { -/// // some_string: ::std::clone::Clone::clone(&self.some_string), -/// // some_int: self.some_int -/// // }; -/// // commands.insert(component); -/// // } +/// // &self, +/// // commands: &mut bevy_proto::ProtoCommands, +/// // asset_server: &bevy::prelude::Res, +/// // ) { +/// // let component = self.clone(); +/// // commands.insert(component); +/// // } /// // } /// ``` #[proc_macro_derive(ProtoComponent, attributes(proto_comp))] pub fn proto_comp_derive(input: TokenStream) -> TokenStream { - let DeriveInput { ident, data, .. } = parse_macro_input!(input); + let DeriveInput { + ident, data, attrs, .. + } = parse_macro_input!(input); + + let mut generator = None; + for attr in attrs { + let struct_attr: Result = attr.parse_args(); + if let Ok(struct_attr) = struct_attr { + generator = Some(quote! { #struct_attr }); + break; + } + } - let generator = match data { - Data::Struct(data_struct) => proc_fields(data_struct.fields), - _ => syn::Error::new( - Span::call_site(), - "ProtoComponent can only be applied on struct types", - ) - .to_compile_error(), + let generator = if let Some(generator) = generator { + generator + } else { + match data { + Data::Struct(..) | Data::Enum(..) => { + quote! { + let component = self.clone(); + commands.insert(component); + } + } + _ => syn::Error::new( + Span::call_site(), + "ProtoComponent can only be applied on struct types", + ) + .to_compile_error(), + } }; let output = quote! { #[typetag::serde] - impl bevy_proto::ProtoComponent for #ident { + impl bevy_proto::prelude::ProtoComponent for #ident { fn insert_self( &self, - commands: &mut bevy_proto::ProtoCommands, + commands: &mut bevy_proto::prelude::ProtoCommands, asset_server: &bevy::prelude::Res, ) { - let component = #generator; - commands.insert(component); + #generator; } } }; output.into() } - -/// Process all fields -/// -/// # Arguments -/// -/// * `fields`: The fields to process -/// -/// returns: TokenStream -fn proc_fields(fields: Fields) -> TokenStream2 { - match fields { - Fields::Named(named) => { - let inner = proc_named_fields(named); - quote! { - Self { - #inner - } - } - } - Fields::Unnamed(unnamed) => { - let inner = proc_unnnamed_fields(unnamed); - quote! { - Self (#inner); - } - } - Fields::Unit => quote! {Self{}}, - } -} - -/// Process all named fields -/// -/// # Arguments -/// -/// * `fields`: The fields to process -/// -/// returns: TokenStream -fn proc_named_fields(fields: FieldsNamed) -> TokenStream2 { - let field_stream = fields.named.iter().map(|field| { - let dupe_type = fields::get_dupe_attr(&field); - let Field { ident, .. } = field.clone(); - - match dupe_type { - Some(ProtoCompDupeAttr::AttrCopy) => quote! { - #ident: self.#ident - }, - Some(ProtoCompDupeAttr::AttrClone) | None => quote! { - #ident: ::std::clone::Clone::clone(&self.#ident) - }, - } - }); - - quote! { - #(#field_stream),* - } -} - -/// Process all unnamed fields -/// -/// # Arguments -/// -/// * `fields`: The fields to process -/// -/// returns: TokenStream -fn proc_unnnamed_fields(fields: FieldsUnnamed) -> TokenStream2 { - let field_stream = fields.unnamed.iter().enumerate().map(|(index, field)| { - let idx = Index::from(index); - let dupe_type = fields::get_dupe_attr(&field); - - match dupe_type { - Some(ProtoCompDupeAttr::AttrCopy) => quote! { - self.#idx - }, - _ => quote! { - ::std::clone::Clone::clone(&self.#idx) - }, - } - }); - - quote! { - #(#field_stream),* - } -} diff --git a/examples/attributes.rs b/examples/attributes.rs new file mode 100644 index 0000000..556afcd --- /dev/null +++ b/examples/attributes.rs @@ -0,0 +1,112 @@ +use bevy::prelude::*; +use bevy_proto::prelude::*; +use serde::{Deserialize, Serialize}; + +/// Not every `ProtoComponent` needs to itself be a `Component` (or vice-versa) +/// +/// Let's pretend we don't have access to this struct. Maybe it's from another library or +/// its a Bevy internal component. Maybe it's has a generic type that can't be deserialized. +/// +/// Whatever, the case, how can we include this component in our templates? We _could_ manually +/// implement `ProtoComponent` like in the `bundles` example. However, there are other means of +/// achieving this that might be a little easier or utilize other code you might already have. +#[derive(Component)] +struct Emoji(String); + +/// The simplest method to spawn a different component is to utilize the `From` trait. +/// +/// If our `ProtoComponent` implements `From` for the "ActualComponent", we can use the +/// `#[proto_comp(into = "ActualComponent")]` attribute. This attribute essentially just clones +/// our `ProtoComponent` had turns it into our "ActualComponent". +/// +/// Note: We still do need to derive/impl `Clone`, `Serialize`, and `Deserialize` traits. +#[derive(Clone, Serialize, Deserialize, ProtoComponent)] +#[proto_comp(into = "Emoji")] +struct EmojiDef { + emoji: String, +} + +/// Make sure you impl `From`! +impl From for Emoji { + fn from(def: EmojiDef) -> Self { + Self(def.emoji) + } +} + +/// Alternatively, you might want or have a function that performs the spawn logic that should +/// be shared so that it may be used in other places or for other components. +/// +/// Say we have a trait that allows its implementors to return an `Emoji` struct. +trait AsEmoji { + fn as_emoji(&self) -> Emoji; +} + +/// We can create a function that takes any `ProtoComponent` that implements `AsEmoji` and inserts +/// an `Emoji` component. +fn create_emoji( + component: &T, + commands: &mut ProtoCommands, + _asset_server: &Res, +) { + commands.insert(component.as_emoji()); +} + +/// Then we can use the `#[proto_comp(with = "my_function")]` attribute. This works exactly +/// like [`ProtoComponent::insert_self`], but allows you to use an extracted version of that function. +#[derive(Clone, Serialize, Deserialize, ProtoComponent)] +#[proto_comp(with = "create_emoji")] +enum Mood { + Normal, + Silly, +} +impl AsEmoji for Mood { + fn as_emoji(&self) -> Emoji { + match self { + Self::Normal => Emoji(String::from("ðŸ˜ķ")), + Self::Silly => Emoji(String::from("ðŸĪŠ")), + } + } +} + +/// Notice that we only had to define the function once even though we're using it across multiple +/// `ProtoComponent` structs. +#[derive(Clone, Serialize, Deserialize, ProtoComponent)] +#[proto_comp(with = "create_emoji")] +enum Face { + Normal, + Frowning, +} +impl AsEmoji for Face { + fn as_emoji(&self) -> Emoji { + match self { + Self::Normal => Emoji(String::from("ðŸ˜ķ")), + Self::Frowning => Emoji(String::from("😠")), + } + } +} + +fn spawn_emojis(mut commands: Commands, data: Res, asset_server: Res) { + let proto = data.get_prototype("Happy").expect("Should exist!"); + proto.spawn(&mut commands, &data, &asset_server); + let proto = data.get_prototype("Sad").expect("Should exist!"); + proto.spawn(&mut commands, &data, &asset_server); + let proto = data.get_prototype("Silly").expect("Should exist!"); + proto.spawn(&mut commands, &data, &asset_server); + let proto = data.get_prototype("Angry").expect("Should exist!"); + proto.spawn(&mut commands, &data, &asset_server); +} + +fn print_emojies(query: Query<&Emoji, Added>) { + for emoji in query.iter() { + println!("{}", emoji.0); + } +} + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugin(ProtoPlugin::default()) + .add_startup_system(spawn_emojis) + .add_system(print_emojies) + .run(); +} diff --git a/examples/basic.rs b/examples/basic.rs index 1e408b9..2eb2c7e 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -3,11 +3,11 @@ use bevy::prelude::*; use serde::{Deserialize, Serialize}; -use bevy_proto::{ProtoCommands, ProtoComponent, ProtoData, ProtoPlugin}; +use bevy_proto::prelude::*; /// This is the component we will use with our prototype -/// It must derive both Serialize and Deserialize from serde in order to compile -#[derive(Serialize, Deserialize, Component)] +/// It must impl/derive Serialize, Clone, and Deserialize from serde in order to compile +#[derive(Clone, Serialize, Deserialize, Component)] struct Person { pub name: String, } @@ -35,18 +35,13 @@ impl ProtoComponent for Person { /// /// The [`Person`] component defined above could have simply been written as: /// ``` -/// #[derive(Serialize, Deserialize, ProtoComponent)] +/// #[derive(Clone, Serialize, Deserialize, Component, ProtoComponent)] /// struct Person { -/// // Optional: #[proto_comp(Clone)] /// pub name: String, /// } /// ``` -/// -/// Here, we call the attribute with the "Copy" argument as this struct can -/// readily derive Copy and should be marginally faster than Clone -#[derive(Serialize, Deserialize, ProtoComponent, Component)] +#[derive(Copy, Clone, Serialize, Deserialize, ProtoComponent, Component)] struct Ordered { - #[proto_comp(Copy)] pub order: i32, } diff --git a/examples/bundles.rs b/examples/bundles.rs index d51ff3a..1279c91 100644 --- a/examples/bundles.rs +++ b/examples/bundles.rs @@ -3,7 +3,7 @@ use bevy::prelude::*; use serde::{Deserialize, Serialize}; -use bevy_proto::{HandlePath, ProtoCommands, ProtoComponent, ProtoData, ProtoPlugin, Prototypical}; +use bevy_proto::prelude::*; #[derive(Serialize, Deserialize, Component)] struct SpriteBundleDef { diff --git a/src/components.rs b/src/components.rs index 622743f..f4fa19e 100644 --- a/src/components.rs +++ b/src/components.rs @@ -1,6 +1,7 @@ use bevy::prelude::{AssetServer, Res, World}; -use crate::{ProtoCommands, ProtoData, Prototypical}; +use crate::data::{ProtoCommands, ProtoData}; +use crate::prototype::Prototypical; /// A trait that allows components to be used within [`Prototypical`] structs #[typetag::serde(tag = "type", content = "value")] diff --git a/src/data.rs b/src/data.rs index de2d534..5aca53c 100644 --- a/src/data.rs +++ b/src/data.rs @@ -12,7 +12,7 @@ use dyn_clone::DynClone; use indexmap::IndexSet; use serde::{Deserialize, Serialize}; -use crate::{ProtoComponent, Prototypical}; +use crate::{components::ProtoComponent, prototype::Prototypical, utils::handle_cycle}; /// A String newtype for a handle's asset path #[derive(Serialize, Deserialize, Clone, Hash, Eq, PartialEq, Debug)] diff --git a/src/lib.rs b/src/lib.rs index 7120a19..e166c56 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,13 +1,17 @@ extern crate bevy_proto_derive; + +pub mod components; +pub mod data; +pub mod plugin; +pub mod prototype; #[macro_use] mod utils; -mod components; -mod data; -mod plugin; -mod prototype; -pub use bevy_proto_derive::*; -pub use components::*; -pub use data::*; -pub use plugin::*; -pub use prototype::*; +pub mod prelude { + pub use bevy_proto_derive::*; + + pub use super::components::*; + pub use super::data::*; + pub use super::plugin::*; + pub use super::prototype::{Prototype, Prototypical}; +} diff --git a/src/plugin.rs b/src/plugin.rs index 8e8ded9..1f607e9 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -1,6 +1,10 @@ -use crate::{ProtoData, ProtoDataOptions, ProtoDeserializer, Prototype, Prototypical}; use bevy::app::{App, Plugin}; +use crate::{ + data::{ProtoData, ProtoDataOptions, ProtoDeserializer}, + prototype::{Prototype, Prototypical}, +}; + pub struct ProtoPlugin { pub options: Option, } diff --git a/src/prototype.rs b/src/prototype.rs index b9b4fee..1466e88 100644 --- a/src/prototype.rs +++ b/src/prototype.rs @@ -11,7 +11,9 @@ use serde::{ Deserialize, Deserializer, Serialize, }; -use crate::{ProtoCommands, ProtoComponent, ProtoData}; +use crate::{ + components::ProtoComponent, data::ProtoCommands, data::ProtoData, utils::handle_cycle, +}; /// Allows access to a prototype's name and components so that it can be spawned in pub trait Prototypical: 'static + Send + Sync { diff --git a/src/utils.rs b/src/utils.rs index 61de144..2f4a130 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -79,3 +79,5 @@ macro_rules! handle_cycle { ); }}; } + +pub(crate) use handle_cycle;