aboutsummaryrefslogtreecommitdiff
path: root/azalea-ecs
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2023-02-04 19:32:27 -0600
committerGitHub <noreply@github.com>2023-02-04 19:32:27 -0600
commita5672815ccef520b433363ac622dbb6d6af60c91 (patch)
treef9bb1b41876d81423ac3f188f4d368e6d362eed1 /azalea-ecs
parent7c7446ab1e467c29f86e9bfba260741fc469389a (diff)
downloadazalea-drasl-a5672815ccef520b433363ac622dbb6d6af60c91.tar.xz
Use an ECS (#52)
* add EntityData::kind * start making metadata use hecs * make entity codegen generate ecs stuff * fix registry codegen * get rid of worldhaver it's not even used * add bevy_ecs to deps * rename Component to FormattedText also start making the metadata use bevy_ecs but bevy_ecs doesn't let you query on Bundles so it's annoying * generate metadata.rs correctly for bevy_ecs * start switching more entity stuff to use ecs * more ecs stuff for entity storage * ok well it compiles but it definitely doesn't work * random fixes * change a bunch of entity things to use the components * some ecs stuff in az-client * packet handler uses the ecs now and other fun changes i still need to make ticking use the ecs but that's tricker, i'm considering using bevy_ecs systems for those bevy_ecs systems can't be async but the only async things in ticking is just sending packets which can just be done as a tokio task so that's not a big deal * start converting some functions in az-client into systems committing because i'm about to try something that might go horribly wrong * start splitting client i'm probably gonna change it so azalea entity ids are separate from minecraft entity ids next (so stuff like player ids can be consistent and we don't have to wait for the login packet) * separate minecraft entity ids from azalea entity ids + more ecs stuff i guess i'm using bevy_app now too huh it's necessary for plugins and it lets us control the tick rate anyways so it's fine i think i'm still not 100% sure how packet handling that interacts with the world will work, but i think if i can sneak the ecs world into there it'll be fine. Can't put packet handling in the schedule because that'd make it tick-bound, which it's not (technically it'd still work but it'd be wrong and anticheats might realize). * packet handling now it runs the schedule only when we get a tick or packet :smile: also i systemified some more functions and did other random fixes so az-world and az-physics compile making azalea-client use the ecs is almost done! all the hard parts are done now i hope, i just have to finish writing all the code so it actually works * start figuring out how functions in Client will work generally just lifetimes being annoying but i think i can get it all to work * make writing packets work synchronously* * huh az-client compiles * start fixing stuff * start fixing some packets * make packet handler work i still haven't actually tested any of this yet lol but in theory it should all work i'll probably either actually test az-client and fix all the remaining issues or update the azalea crate next ok also one thing that i'm not particularly happy with is how the packet handlers are doing ugly queries like ```rs let local_player = ecs .query::<&LocalPlayer>() .get_mut(ecs, player_entity) .unwrap(); ``` i think the right way to solve it would be by putting every packet handler in its own system but i haven't come up with a way to make that not be really annoying yet * fix warnings * ok what if i just have a bunch of queries and a single packet handler system * simple example for azalea-client * :bug: * maybe fix deadlock idk can't test it rn lmao * make physicsstate its own component * use the default plugins * azalea compiles lol * use systemstate for packet handler * fix entities basically moved some stuff from being in the world to just being components * physics (ticking) works * try to add a .entity_by function still doesn't work because i want to make the predicate magic * try to make entity_by work well it does work but i couldn't figure out how to make it look not terrible. Will hopefully change in the future * everything compiles * start converting swarm to use builder * continue switching swarm to builder and fix stuff * make swarm use builder still have to fix some stuff and make client use builder * fix death event * client builder * fix some warnings * document plugins a bit * start trying to fix tests * azalea-ecs * azalea-ecs stuff compiles * az-physics tests pass :tada: * fix all the tests * clippy on azalea-ecs-macros * remove now-unnecessary trait_upcasting feature * fix some clippy::pedantic warnings lol * why did cargo fmt not remove the trailing spaces * FIX ALL THE THINGS * when i said 'all' i meant non-swarm bugs * start adding task pool * fix entity deduplication * fix pathfinder not stopping * fix some more random bugs * fix panic that sometimes happens in swarms * make pathfinder run in task * fix some tests * fix doctests and clippy * deadlock * fix systems running in wrong order * fix non-swarm bots
Diffstat (limited to 'azalea-ecs')
-rw-r--r--azalea-ecs/Cargo.toml13
-rwxr-xr-xazalea-ecs/azalea-ecs-macros/Cargo.toml15
-rw-r--r--azalea-ecs/azalea-ecs-macros/src/component.rs125
-rw-r--r--azalea-ecs/azalea-ecs-macros/src/fetch.rs466
-rwxr-xr-xazalea-ecs/azalea-ecs-macros/src/lib.rs523
-rw-r--r--azalea-ecs/azalea-ecs-macros/src/utils/attrs.rs45
-rw-r--r--azalea-ecs/azalea-ecs-macros/src/utils/mod.rs224
-rw-r--r--azalea-ecs/azalea-ecs-macros/src/utils/shape.rs21
-rw-r--r--azalea-ecs/azalea-ecs-macros/src/utils/symbol.rs35
-rw-r--r--azalea-ecs/src/lib.rs144
10 files changed, 1611 insertions, 0 deletions
diff --git a/azalea-ecs/Cargo.toml b/azalea-ecs/Cargo.toml
new file mode 100644
index 00000000..30c9676b
--- /dev/null
+++ b/azalea-ecs/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+edition = "2021"
+name = "azalea-ecs"
+version = "0.5.0"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+azalea-ecs-macros = {path = "./azalea-ecs-macros", version = "^0.5.0"}
+bevy_app = "0.9.1"
+bevy_ecs = {version = "0.9.1", default-features = false}
+iyes_loopless = "0.9.1"
+tokio = {version = "1.25.0", features = ["time"]}
diff --git a/azalea-ecs/azalea-ecs-macros/Cargo.toml b/azalea-ecs/azalea-ecs-macros/Cargo.toml
new file mode 100755
index 00000000..2301f2f1
--- /dev/null
+++ b/azalea-ecs/azalea-ecs-macros/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+description = "Azalea ECS Macros"
+edition = "2021"
+license = "MIT OR Apache-2.0"
+name = "azalea-ecs-macros"
+version = "0.5.0"
+
+[lib]
+proc-macro = true
+
+[dependencies]
+proc-macro2 = "1.0"
+quote = "1.0"
+syn = "1.0"
+toml = "0.7.0"
diff --git a/azalea-ecs/azalea-ecs-macros/src/component.rs b/azalea-ecs/azalea-ecs-macros/src/component.rs
new file mode 100644
index 00000000..306b64de
--- /dev/null
+++ b/azalea-ecs/azalea-ecs-macros/src/component.rs
@@ -0,0 +1,125 @@
+use crate::utils::{get_lit_str, Symbol};
+use proc_macro::TokenStream;
+use proc_macro2::{Span, TokenStream as TokenStream2};
+use quote::{quote, ToTokens};
+use syn::{parse_macro_input, parse_quote, DeriveInput, Error, Ident, Path, Result};
+
+use crate::utils;
+
+pub fn derive_resource(input: TokenStream) -> TokenStream {
+ let mut ast = parse_macro_input!(input as DeriveInput);
+ let azalea_ecs_path: Path = crate::azalea_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 #azalea_ecs_path::system::BevyResource for #struct_name #type_generics #where_clause {
+ }
+ })
+}
+
+pub fn derive_component(input: TokenStream) -> TokenStream {
+ let mut ast = parse_macro_input!(input as DeriveInput);
+ let azalea_ecs_path: Path = crate::azalea_ecs_path();
+
+ let attrs = match parse_component_attr(&ast) {
+ Ok(attrs) => attrs,
+ Err(e) => return e.into_compile_error().into(),
+ };
+
+ let storage = storage_path(&azalea_ecs_path, attrs.storage);
+
+ 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 #azalea_ecs_path::component::BevyComponent for #struct_name #type_generics #where_clause {
+ type Storage = #storage;
+ }
+ })
+}
+
+pub const COMPONENT: Symbol = Symbol("component");
+pub const STORAGE: Symbol = Symbol("storage");
+
+struct Attrs {
+ storage: StorageTy,
+}
+
+#[derive(Clone, Copy)]
+enum StorageTy {
+ Table,
+ SparseSet,
+}
+
+// values for `storage` attribute
+const TABLE: &str = "Table";
+const SPARSE_SET: &str = "SparseSet";
+
+fn parse_component_attr(ast: &DeriveInput) -> Result<Attrs> {
+ let meta_items = utils::parse_attrs(ast, COMPONENT)?;
+
+ let mut attrs = Attrs {
+ storage: StorageTy::Table,
+ };
+
+ for meta in meta_items {
+ use syn::{
+ Meta::NameValue,
+ NestedMeta::{Lit, Meta},
+ };
+ match meta {
+ Meta(NameValue(m)) if m.path == STORAGE => {
+ attrs.storage = match get_lit_str(STORAGE, &m.lit)?.value().as_str() {
+ TABLE => StorageTy::Table,
+ SPARSE_SET => StorageTy::SparseSet,
+ s => {
+ return Err(Error::new_spanned(
+ m.lit,
+ format!(
+ "Invalid storage type `{s}`, expected '{TABLE}' or '{SPARSE_SET}'."
+ ),
+ ))
+ }
+ };
+ }
+ Meta(meta_item) => {
+ return Err(Error::new_spanned(
+ meta_item.path(),
+ format!(
+ "unknown component attribute `{}`",
+ meta_item.path().into_token_stream()
+ ),
+ ));
+ }
+ Lit(lit) => {
+ return Err(Error::new_spanned(
+ lit,
+ "unexpected literal in component attribute",
+ ))
+ }
+ }
+ }
+
+ Ok(attrs)
+}
+
+fn storage_path(azalea_ecs_path: &Path, ty: StorageTy) -> TokenStream2 {
+ let typename = match ty {
+ StorageTy::Table => Ident::new("TableStorage", Span::call_site()),
+ StorageTy::SparseSet => Ident::new("SparseStorage", Span::call_site()),
+ };
+
+ quote! { #azalea_ecs_path::component::#typename }
+}
diff --git a/azalea-ecs/azalea-ecs-macros/src/fetch.rs b/azalea-ecs/azalea-ecs-macros/src/fetch.rs
new file mode 100644
index 00000000..8a6b93ba
--- /dev/null
+++ b/azalea-ecs/azalea-ecs-macros/src/fetch.rs
@@ -0,0 +1,466 @@
+use proc_macro::TokenStream;
+use proc_macro2::{Ident, Span};
+use quote::{quote, ToTokens};
+use syn::{
+ parse::{Parse, ParseStream},
+ parse_quote,
+ punctuated::Punctuated,
+ Attribute, Data, DataStruct, DeriveInput, Field, Fields,
+};
+
+use crate::azalea_ecs_path;
+
+#[derive(Default)]
+struct FetchStructAttributes {
+ pub is_mutable: bool,
+ pub derive_args: Punctuated<syn::NestedMeta, syn::token::Comma>,
+}
+
+static MUTABLE_ATTRIBUTE_NAME: &str = "mutable";
+static DERIVE_ATTRIBUTE_NAME: &str = "derive";
+
+mod field_attr_keywords {
+ syn::custom_keyword!(ignore);
+}
+
+pub static WORLD_QUERY_ATTRIBUTE_NAME: &str = "world_query";
+
+pub fn derive_world_query_impl(ast: DeriveInput) -> TokenStream {
+ let visibility = ast.vis;
+
+ let mut fetch_struct_attributes = FetchStructAttributes::default();
+ for attr in &ast.attrs {
+ if !attr
+ .path
+ .get_ident()
+ .map_or(false, |ident| ident == WORLD_QUERY_ATTRIBUTE_NAME)
+ {
+ continue;
+ }
+
+ attr.parse_args_with(|input: ParseStream| {
+ let meta = input.parse_terminated::<syn::Meta, syn::token::Comma>(syn::Meta::parse)?;
+ for meta in meta {
+ let ident = meta.path().get_ident().unwrap_or_else(|| {
+ panic!(
+ "Unrecognized attribute: `{}`",
+ meta.path().to_token_stream()
+ )
+ });
+ if ident == MUTABLE_ATTRIBUTE_NAME {
+ if let syn::Meta::Path(_) = meta {
+ fetch_struct_attributes.is_mutable = true;
+ } else {
+ panic!(
+ "The `{MUTABLE_ATTRIBUTE_NAME}` attribute is expected to have no value or arguments"
+ );
+ }
+ } else if ident == DERIVE_ATTRIBUTE_NAME {
+ if let syn::Meta::List(meta_list) = meta {
+ fetch_struct_attributes
+ .derive_args
+ .extend(meta_list.nested.iter().cloned());
+ } else {
+ panic!(
+ "Expected a structured list within the `{DERIVE_ATTRIBUTE_NAME}` attribute"
+ );
+ }
+ } else {
+ panic!(
+ "Unrecognized attribute: `{}`",
+ meta.path().to_token_stream()
+ );
+ }
+ }
+ Ok(())
+ })
+ .unwrap_or_else(|_| panic!("Invalid `{WORLD_QUERY_ATTRIBUTE_NAME}` attribute format"));
+ }
+
+ let path = azalea_ecs_path();
+
+ let user_generics = ast.generics.clone();
+ let (user_impl_generics, user_ty_generics, user_where_clauses) = user_generics.split_for_impl();
+ let user_generics_with_world = {
+ let mut generics = ast.generics.clone();
+ generics.params.insert(0, parse_quote!('__w));
+ generics
+ };
+ let (user_impl_generics_with_world, user_ty_generics_with_world, user_where_clauses_with_world) =
+ user_generics_with_world.split_for_impl();
+
+ let struct_name = ast.ident.clone();
+ let read_only_struct_name = if fetch_struct_attributes.is_mutable {
+ Ident::new(&format!("{struct_name}ReadOnly"), Span::call_site())
+ } else {
+ struct_name.clone()
+ };
+
+ let item_struct_name = Ident::new(&format!("{struct_name}Item"), Span::call_site());
+ let read_only_item_struct_name = if fetch_struct_attributes.is_mutable {
+ Ident::new(&format!("{struct_name}ReadOnlyItem"), Span::call_site())
+ } else {
+ item_struct_name.clone()
+ };
+
+ let fetch_struct_name = Ident::new(&format!("{struct_name}Fetch"), Span::call_site());
+ let read_only_fetch_struct_name = if fetch_struct_attributes.is_mutable {
+ Ident::new(&format!("{struct_name}ReadOnlyFetch"), Span::call_site())
+ } else {
+ fetch_struct_name.clone()
+ };
+
+ let state_struct_name = Ident::new(&format!("{struct_name}State"), Span::call_site());
+
+ let fields = match &ast.data {
+ Data::Struct(DataStruct {
+ fields: Fields::Named(fields),
+ ..
+ }) => &fields.named,
+ _ => panic!("Expected a struct with named fields"),
+ };
+
+ let mut ignored_field_attrs = Vec::new();
+ let mut ignored_field_visibilities = Vec::new();
+ let mut ignored_field_idents = Vec::new();
+ let mut ignored_field_types = Vec::new();
+ let mut field_attrs = Vec::new();
+ let mut field_visibilities = Vec::new();
+ let mut field_idents = Vec::new();
+ let mut field_types = Vec::new();
+ let mut read_only_field_types = Vec::new();
+
+ for field in fields {
+ let WorldQueryFieldInfo { is_ignored, attrs } = read_world_query_field_info(field);
+
+ let field_ident = field.ident.as_ref().unwrap().clone();
+ if is_ignored {
+ ignored_field_attrs.push(attrs);
+ ignored_field_visibilities.push(field.vis.clone());
+ ignored_field_idents.push(field_ident.clone());
+ ignored_field_types.push(field.ty.clone());
+ } else {
+ field_attrs.push(attrs);
+ field_visibilities.push(field.vis.clone());
+ field_idents.push(field_ident.clone());
+ let field_ty = field.ty.clone();
+ field_types.push(quote!(#field_ty));
+ read_only_field_types.push(quote!(<#field_ty as #path::query::WorldQuery>::ReadOnly));
+ }
+ }
+
+ let derive_args = &fetch_struct_attributes.derive_args;
+ // `#[derive()]` is valid syntax
+ let derive_macro_call = quote! { #[derive(#derive_args)] };
+
+ let impl_fetch = |is_readonly: bool| {
+ let struct_name = if is_readonly {
+ &read_only_struct_name
+ } else {
+ &struct_name
+ };
+ let item_struct_name = if is_readonly {
+ &read_only_item_struct_name
+ } else {
+ &item_struct_name
+ };
+ let fetch_struct_name = if is_readonly {
+ &read_only_fetch_struct_name
+ } else {
+ &fetch_struct_name
+ };
+
+ let field_types = if is_readonly {
+ &read_only_field_types
+ } else {
+ &field_types
+ };
+
+ quote! {
+ #derive_macro_call
+ #[doc = "Automatically generated [`WorldQuery`] item type for [`"]
+ #[doc = stringify!(#struct_name)]
+ #[doc = "`], returned when iterating over query results."]
+ #[automatically_derived]
+ #visibility struct #item_struct_name #user_impl_generics_with_world #user_where_clauses_with_world {
+ #(#(#field_attrs)* #field_visibilities #field_idents: <#field_types as #path::query::WorldQuery>::Item<'__w>,)*
+ #(#(#ignored_field_attrs)* #ignored_field_visibilities #ignored_field_idents: #ignored_field_types,)*
+ }
+
+ #[doc(hidden)]
+ #[doc = "Automatically generated internal [`WorldQuery`] fetch type for [`"]
+ #[doc = stringify!(#struct_name)]
+ #[doc = "`], used to define the world data accessed by this query."]
+ #[automatically_derived]
+ #visibility struct #fetch_struct_name #user_impl_generics_with_world #user_where_clauses_with_world {
+ #(#field_idents: <#field_types as #path::query::WorldQuery>::Fetch<'__w>,)*
+ #(#ignored_field_idents: #ignored_field_types,)*
+ }
+
+ // SAFETY: `update_component_access` and `update_archetype_component_access` are called on every field
+ unsafe impl #user_impl_generics #path::query::WorldQuery
+ for #struct_name #user_ty_generics #user_where_clauses {
+
+ type Item<'__w> = #item_struct_name #user_ty_generics_with_world;
+ type Fetch<'__w> = #fetch_struct_name #user_ty_generics_with_world;
+ type ReadOnly = #read_only_struct_name #user_ty_generics;
+ type State = #state_struct_name #user_ty_generics;
+
+ fn shrink<'__wlong: '__wshort, '__wshort>(
+ item: <#struct_name #user_ty_generics as #path::query::WorldQuery>::Item<'__wlong>
+ ) -> <#struct_name #user_ty_generics as #path::query::WorldQuery>::Item<'__wshort> {
+ #item_struct_name {
+ #(
+ #field_idents: <#field_types>::shrink(item.#field_idents),
+ )*
+ #(
+ #ignored_field_idents: item.#ignored_field_idents,
+ )*
+ }
+ }
+
+ unsafe fn init_fetch<'__w>(
+ _world: &'__w #path::world::World,
+ state: &Self::State,
+ _last_change_tick: u32,
+ _change_tick: u32
+ ) -> <Self as #path::query::WorldQuery>::Fetch<'__w> {
+ #fetch_struct_name {
+ #(#field_idents:
+ <#field_types>::init_fetch(
+ _world,
+ &state.#field_idents,
+ _last_change_tick,
+ _change_tick
+ ),
+ )*
+ #(#ignored_field_idents: Default::default(),)*
+ }
+ }
+
+ unsafe fn clone_fetch<'__w>(
+ _fetch: &<Self as #path::query::WorldQuery>::Fetch<'__w>
+ ) -> <Self as #path::query::WorldQuery>::Fetch<'__w> {
+ #fetch_struct_name {
+ #(
+ #field_idents: <#field_types>::clone_fetch(& _fetch. #field_idents),
+ )*
+ #(
+ #ignored_field_idents: Default::default(),
+ )*
+ }
+ }
+
+ const IS_DENSE: bool = true #(&& <#field_types>::IS_DENSE)*;
+
+ const IS_ARCHETYPAL: bool = true #(&& <#field_types>::IS_ARCHETYPAL)*;
+
+ /// SAFETY: we call `set_archetype` for each member that implements `Fetch`
+ #[inline]
+ unsafe fn set_archetype<'__w>(
+ _fetch: &mut <Self as #path::query::WorldQuery>::Fetch<'__w>,
+ _state: &Self::State,
+ _archetype: &'__w #path::archetype::Archetype,
+ _table: &'__w #path::storage::Table
+ ) {
+ #(<#field_types>::set_archetype(&mut _fetch.#field_idents, &_state.#field_idents, _archetype, _table);)*
+ }
+
+ /// SAFETY: we call `set_table` for each member that implements `Fetch`
+ #[inline]
+ unsafe fn set_table<'__w>(
+ _fetch: &mut <Self as #path::query::WorldQuery>::Fetch<'__w>,
+ _state: &Self::State,
+ _table: &'__w #path::storage::Table
+ ) {
+ #(<#field_types>::set_table(&mut _fetch.#field_idents, &_state.#field_idents, _table);)*
+ }
+
+ /// SAFETY: we call `fetch` for each member that implements `Fetch`.
+ #[inline(always)]
+ unsafe fn fetch<'__w>(
+ _fetch: &mut <Self as #path::query::WorldQuery>::Fetch<'__w>,
+ _entity: Entity,
+ _table_row: usize
+ ) -> <Self as #path::query::WorldQuery>::Item<'__w> {
+ Self::Item {
+ #(#field_idents: <#field_types>::fetch(&mut _fetch.#field_idents, _entity, _table_row),)*
+ #(#ignored_field_idents: Default::default(),)*
+ }
+ }
+
+ #[allow(unused_variables)]
+ #[inline(always)]
+ unsafe fn filter_fetch<'__w>(
+ _fetch: &mut <Self as #path::query::WorldQuery>::Fetch<'__w>,
+ _entity: Entity,
+ _table_row: usize
+ ) -> bool {
+ true #(&& <#field_types>::filter_fetch(&mut _fetch.#field_idents, _entity, _table_row))*
+ }
+
+ fn update_component_access(state: &Self::State, _access: &mut #path::query::FilteredAccess<#path::component::ComponentId>) {
+ #( <#field_types>::update_component_access(&state.#field_idents, _access); )*
+ }
+
+ fn update_archetype_component_access(
+ state: &Self::State,
+ _archetype: &#path::archetype::Archetype,
+ _access: &mut #path::query::Access<#path::archetype::ArchetypeComponentId>
+ ) {
+ #(
+ <#field_types>::update_archetype_component_access(&state.#field_idents, _archetype, _access);
+ )*
+ }
+
+ fn init_state(world: &mut #path::world::World) -> #state_struct_name #user_ty_generics {
+ #state_struct_name {
+ #(#field_idents: <#field_types>::init_state(world),)*
+ #(#ignored_field_idents: Default::default(),)*
+ }
+ }
+
+ fn matches_component_set(state: &Self::State, _set_contains_id: &impl Fn(#path::component::ComponentId) -> bool) -> bool {
+ true #(&& <#field_types>::matches_component_set(&state.#field_idents, _set_contains_id))*
+ }
+ }
+ }
+ };
+
+ let mutable_impl = impl_fetch(false);
+ let readonly_impl = if fetch_struct_attributes.is_mutable {
+ let world_query_impl = impl_fetch(true);
+ quote! {
+ #[doc(hidden)]
+ #[doc = "Automatically generated internal [`WorldQuery`] type for [`"]
+ #[doc = stringify!(#struct_name)]
+ #[doc = "`], used for read-only access."]
+ #[automatically_derived]
+ #visibility struct #read_only_struct_name #user_impl_generics #user_where_clauses {
+ #( #field_idents: #read_only_field_types, )*
+ #(#(#ignored_field_attrs)* #ignored_field_visibilities #ignored_field_idents: #ignored_field_types,)*
+ }
+
+ #world_query_impl
+ }
+ } else {
+ quote! {}
+ };
+
+ let read_only_asserts = if fetch_struct_attributes.is_mutable {
+ quote! {
+ // Double-check that the data fetched by `<_ as WorldQuery>::ReadOnly` is read-only.
+ // This is technically unnecessary as `<_ as WorldQuery>::ReadOnly: ReadOnlyWorldQuery`
+ // but to protect against future mistakes we assert the assoc type implements `ReadOnlyWorldQuery` anyway
+ #( assert_readonly::<#read_only_field_types>(); )*
+ }
+ } else {
+ quote! {
+ // Statically checks that the safety guarantee of `ReadOnlyWorldQuery` for `$fetch_struct_name` actually holds true.
+ // We need this to make sure that we don't compile `ReadOnlyWorldQuery` if our struct contains nested `WorldQuery`
+ // members that don't implement it. I.e.:
+ // ```
+ // #[derive(WorldQuery)]
+ // pub struct Foo { a: &'static mut MyComponent }
+ // ```
+ #( assert_readonly::<#field_types>(); )*
+ }
+ };
+
+ TokenStream::from(quote! {
+ #mutable_impl
+
+ #readonly_impl
+
+ #[doc(hidden)]
+ #[doc = "Automatically generated internal [`WorldQuery`] state type for [`"]
+ #[doc = stringify!(#struct_name)]
+ #[doc = "`], used for caching."]
+ #[automatically_derived]
+ #visibility struct #state_struct_name #user_impl_generics #user_where_clauses {
+ #(#field_idents: <#field_types as #path::query::WorldQuery>::State,)*
+ #(#ignored_field_idents: #ignored_field_types,)*
+ }
+
+ /// SAFETY: we assert fields are readonly below
+ unsafe impl #user_impl_generics #path::query::ReadOnlyWorldQuery
+ for #read_only_struct_name #user_ty_generics #user_where_clauses {}
+
+ #[allow(dead_code)]
+ const _: () = {
+ fn assert_readonly<T>()
+ where
+ T: #path::query::ReadOnlyWorldQuery,
+ {
+ }
+
+ // We generate a readonly assertion for every struct member.
+ fn assert_all #user_impl_generics_with_world () #user_where_clauses_with_world {
+ #read_only_asserts
+ }
+ };
+
+ // The original struct will most likely be left unused. As we don't want our users having
+ // to specify `#[allow(dead_code)]` for their custom queries, we are using this cursed
+ // workaround.
+ #[allow(dead_code)]
+ const _: () = {
+ fn dead_code_workaround #user_impl_generics (
+ q: #struct_name #user_ty_generics,
+ q2: #read_only_struct_name #user_ty_generics
+ ) #user_where_clauses {
+ #(q.#field_idents;)*
+ #(q.#ignored_field_idents;)*
+ #(q2.#field_idents;)*
+ #(q2.#ignored_field_idents;)*
+
+ }
+ };
+ })
+}
+
+struct WorldQueryFieldInfo {
+ /// Has `#[fetch(ignore)]` or `#[filter_fetch(ignore)]` attribute.
+ is_ignored: bool,
+ /// All field attributes except for `world_query` ones.
+ attrs: Vec<Attribute>,
+}
+
+fn read_world_query_field_info(field: &Field) -> WorldQueryFieldInfo {
+ let is_ignored = field
+ .attrs
+ .iter()
+ .find(|attr| {
+ attr.path
+ .get_ident()
+ .map_or(false, |ident| ident == WORLD_QUERY_ATTRIBUTE_NAME)
+ })
+ .map_or(false, |attr| {
+ let mut is_ignored = false;
+ attr.parse_args_with(|input: ParseStream| {
+ if input
+ .parse::<Option<field_attr_keywords::ignore>>()?
+ .is_some()
+ {
+ is_ignored = true;
+ }
+ Ok(())
+ })
+ .unwrap_or_else(|_| panic!("Invalid `{WORLD_QUERY_ATTRIBUTE_NAME}` attribute format"));
+
+ is_ignored
+ });
+
+ let attrs = field
+ .attrs
+ .iter()
+ .filter(|attr| {
+ attr.path
+ .get_ident()
+ .map_or(true, |ident| ident != WORLD_QUERY_ATTRIBUTE_NAME)
+ })
+ .cloned()
+ .collect();
+
+ WorldQueryFieldInfo { is_ignored, attrs }
+}
diff --git a/azalea-ecs/azalea-ecs-macros/src/lib.rs b/azalea-ecs/azalea-ecs-macros/src/lib.rs
new file mode 100755
index 00000000..09ccb094
--- /dev/null
+++ b/azalea-ecs/azalea-ecs-macros/src/lib.rs
@@ -0,0 +1,523 @@
+//! A fork of bevy_ecs_macros that uses azalea_ecs instead of bevy_ecs.
+
+extern crate proc_macro;
+
+mod component;
+mod fetch;
+pub(crate) mod utils;
+
+use crate::fetch::derive_world_query_impl;
+use proc_macro::TokenStream;
+use proc_macro2::Span;
+use quote::{format_ident, quote};
+use syn::{
+ parse::{Parse, ParseStream},
+ parse_macro_input,
+ punctuated::Punctuated,
+ spanned::Spanned,
+ token::Comma,
+ DeriveInput, Field, GenericParam, Ident, Index, LitInt, Meta, MetaList, NestedMeta, Result,
+ Token, TypeParam,
+};
+use utils::{derive_label, get_named_struct_fields, BevyManifest};
+
+struct AllTuples {
+ macro_ident: Ident,
+ start: usize,
+ end: usize,
+ idents: Vec<Ident>,
+}
+
+impl Parse for AllTuples {
+ fn parse(input: ParseStream) -> Result<Self> {
+ let macro_ident = input.parse::<Ident>()?;
+ input.parse::<Comma>()?;
+ let start = input.parse::<LitInt>()?.base10_parse()?;
+ input.parse::<Comma>()?;
+ let end = input.parse::<LitInt>()?.base10_parse()?;
+ input.parse::<Comma>()?;
+ let mut idents = vec![input.parse::<Ident>()?];
+ while input.parse::<Comma>().is_ok() {
+ idents.push(input.parse::<Ident>()?);
+ }
+
+ Ok(AllTuples {
+ macro_ident,
+ start,
+ end,
+ idents,
+ })
+ }
+}
+
+#[proc_macro]
+pub fn all_tuples(input: TokenStream) -> TokenStream {
+ let input = parse_macro_input!(input as AllTuples);
+ let len = input.end - input.start;
+ let mut ident_tuples = Vec::with_capacity(len);
+ for i in input.start..=input.end {
+ let idents = input
+ .idents
+ .iter()
+ .map(|ident| format_ident!("{}{}", ident, i));
+ if input.idents.len() < 2 {
+ ident_tuples.push(quote! {
+ #(#idents)*
+ });
+ } else {
+ ident_tuples.push(quote! {
+ (#(#idents),*)
+ });
+ }
+ }
+
+ let macro_ident = &input.macro_ident;
+ let invocations = (input.start..=input.end).map(|i| {
+ let ident_tuples = &ident_tuples[..i];
+ quote! {
+ #macro_ident!(#(#ident_tuples),*);
+ }
+ });
+ TokenStream::from(quote! {
+ #(
+ #invocations
+ )*
+ })
+}
+
+enum BundleFieldKind {
+ Component,
+ Ignore,
+}
+
+const BUNDLE_ATTRIBUTE_NAME: &str = "bundle";
+const BUNDLE_ATTRIBUTE_IGNORE_NAME: &str = "ignore";
+
+#[proc_macro_derive(Bundle, attributes(bundle))]
+pub fn derive_bundle(input: TokenStream) -> TokenStream {
+ let ast = parse_macro_input!(input as DeriveInput);
+ let ecs_path = azalea_ecs_path();
+
+ let named_fields = match get_named_struct_fields(&ast.data) {
+ Ok(fields) => &fields.named,
+ Err(e) => return e.into_compile_error().into(),
+ };
+
+ let mut field_kind = Vec::with_capacity(named_fields.len());
+
+ 'field_loop: for field in named_fields.iter() {
+ for attr in &field.attrs {
+ if attr.path.is_ident(BUNDLE_ATTRIBUTE_NAME) {
+ if let Ok(Meta::List(MetaList { nested, .. })) = attr.parse_meta() {
+ if let Some(&NestedMeta::Meta(Meta::Path(ref path))) = nested.first() {
+ if path.is_ident(BUNDLE_ATTRIBUTE_IGNORE_NAME) {
+ field_kind.push(BundleFieldKind::Ignore);
+ continue 'field_loop;
+ }
+
+ return syn::Error::new(
+ path.span(),
+ format!(
+ "Invalid bundle attribute. Use `{BUNDLE_ATTRIBUTE_IGNORE_NAME}`"
+ ),
+ )
+ .into_compile_error()
+ .into();
+ }
+
+ return syn::Error::new(attr.span(), format!("Invalid bundle attribute. Use `#[{BUNDLE_ATTRIBUTE_NAME}({BUNDLE_ATTRIBUTE_IGNORE_NAME})]`")).into_compile_error().into();
+ }
+ }
+ }
+
+ field_kind.push(BundleFieldKind::Component);
+ }
+
+ let field = named_fields
+ .iter()
+ .map(|field| field.ident.as_ref().unwrap())
+ .collect::<Vec<_>>();
+ let field_type = named_fields
+ .iter()
+ .map(|field| &field.ty)
+ .collect::<Vec<_>>();
+
+ let mut field_component_ids = Vec::new();
+ let mut field_get_components = Vec::new();
+ let mut field_from_components = Vec::new();
+ for ((field_type, field_kind), field) in
+ field_type.iter().zip(field_kind.iter()).zip(field.iter())
+ {
+ match field_kind {
+ BundleFieldKind::Component => {
+ field_component_ids.push(quote! {
+ <#field_type as #ecs_path::bundle::BevyBundle>::component_ids(components, storages, &mut *ids);
+ });
+ field_get_components.push(quote! {
+ self.#field.get_components(&mut *func);
+ });
+ field_from_components.push(quote! {
+ #field: <#field_type as #ecs_path::bundle::BevyBundle>::from_components(ctx, &mut *func),
+ });
+ }
+
+ BundleFieldKind::Ignore => {
+ field_from_components.push(quote! {
+ #field: ::std::default::Default::default(),
+ });
+ }
+ }
+ }
+ let generics = ast.generics;
+ let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
+ let struct_name = &ast.ident;
+
+ TokenStream::from(quote! {
+ /// SAFETY: ComponentId is returned in field-definition-order. [from_components] and [get_components] use field-definition-order
+ unsafe impl #impl_generics #ecs_path::bundle::BevyBundle for #struct_name #ty_generics #where_clause {
+ fn component_ids(
+ components: &mut #ecs_path::component::Components,
+ storages: &mut #ecs_path::storage::Storages,
+ ids: &mut impl FnMut(#ecs_path::component::ComponentId)
+ ){
+ #(#field_component_ids)*
+ }
+
+ #[allow(unused_variables, non_snake_case)]
+ unsafe fn from_components<__T, __F>(ctx: &mut __T, func: &mut __F) -> Self
+ where
+ __F: FnMut(&mut __T) -> #ecs_path::ptr::OwningPtr<'_>
+ {
+ Self {
+ #(#field_from_components)*
+ }
+ }
+
+ #[allow(unused_variables)]
+ fn get_components(self, func: &mut impl FnMut(#ecs_path::ptr::OwningPtr<'_>)) {
+ #(#field_get_components)*
+ }
+ }
+ })
+}
+
+fn get_idents(fmt_string: fn(usize) -> String, count: usize) -> Vec<Ident> {
+ (0..count)
+ .map(|i| Ident::new(&fmt_string(i), Span::call_site()))
+ .collect::<Vec<Ident>>()
+}
+
+#[proc_macro]
+pub fn impl_param_set(_input: TokenStream) -> TokenStream {
+ let mut tokens = TokenStream::new();
+ let max_params = 8;
+ let params = get_idents(|i| format!("P{i}"), max_params);
+ let params_fetch = get_idents(|i| format!("PF{i}"), max_params);
+ let metas = get_idents(|i| format!("m{i}"), max_params);
+ let mut param_fn_muts = Vec::new();
+ for (i, param) in params.iter().enumerate() {
+ let fn_name = Ident::new(&format!("p{i}"), Span::call_site());
+ let index = Index::from(i);
+ param_fn_muts.push(quote! {
+ pub fn #fn_name<'a>(&'a mut self) -> <#param::Fetch as SystemParamFetch<'a, 'a>>::Item {
+ // SAFETY: systems run without conflicts with other systems.
+ // Conflicting params in ParamSet are not accessible at the same time
+ // ParamSets are guaranteed to not conflict with other SystemParams
+ unsafe {
+ <#param::Fetch as SystemParamFetch<'a, 'a>>::get_param(&mut self.param_states.#index, &self.system_meta, self.world, self.change_tick)
+ }
+ }
+ });
+ }
+
+ for param_count in 1..=max_params {
+ let param = &params[0..param_count];
+ let param_fetch = &params_fetch[0..param_count];
+ let meta = &metas[0..param_count];
+ let param_fn_mut = &param_fn_muts[0..param_count];
+ tokens.extend(TokenStream::from(quote! {
+ impl<'w, 's, #(#param: SystemParam,)*> SystemParam for ParamSet<'w, 's, (#(#param,)*)>
+ {
+ type Fetch = ParamSetState<(#(#param::Fetch,)*)>;
+ }
+
+ // SAFETY: All parameters are constrained to ReadOnlyFetch, so World is only read
+
+ unsafe impl<#(#param_fetch: for<'w1, 's1> SystemParamFetch<'w1, 's1>,)*> ReadOnlySystemParamFetch for ParamSetState<(#(#param_fetch,)*)>
+ where #(#param_fetch: ReadOnlySystemParamFetch,)*
+ { }
+
+ // SAFETY: Relevant parameter ComponentId and ArchetypeComponentId access is applied to SystemMeta. If any ParamState conflicts
+ // with any prior access, a panic will occur.
+
+ unsafe impl<#(#param_fetch: for<'w1, 's1> SystemParamFetch<'w1, 's1>,)*> SystemParamState for ParamSetState<(#(#param_fetch,)*)>
+ {
+ fn init(world: &mut World, system_meta: &mut SystemMeta) -> Self {
+ #(
+ // Pretend to add each param to the system alone, see if it conflicts
+ let mut #meta = system_meta.clone();
+ #meta.component_access_set.clear();
+ #meta.archetype_component_access.clear();
+ #param_fetch::init(world, &mut #meta);
+ let #param = #param_fetch::init(world, &mut system_meta.clone());
+ )*
+ #(
+ system_meta
+ .component_access_set
+ .extend(#meta.component_access_set);
+ system_meta
+ .archetype_component_access
+ .extend(&#meta.archetype_component_access);
+ )*
+ ParamSetState((#(#param,)*))
+ }
+
+ fn new_archetype(&mut self, archetype: &Archetype, system_meta: &mut SystemMeta) {
+ let (#(#param,)*) = &mut self.0;
+ #(
+ #param.new_archetype(archetype, system_meta);
+ )*
+ }
+
+ fn apply(&mut self, world: &mut World) {
+ self.0.apply(world)
+ }
+ }
+
+
+
+ impl<'w, 's, #(#param_fetch: for<'w1, 's1> SystemParamFetch<'w1, 's1>,)*> SystemParamFetch<'w, 's> for ParamSetState<(#(#param_fetch,)*)>
+ {
+ type Item = ParamSet<'w, 's, (#(<#param_fetch as SystemParamFetch<'w, 's>>::Item,)*)>;
+
+ #[inline]
+ unsafe fn get_param(
+ state: &'s mut Self,
+ system_meta: &SystemMeta,
+ world: &'w World,
+ change_tick: u32,
+ ) -> Self::Item {
+ ParamSet {
+ param_states: &mut state.0,
+ system_meta: system_meta.clone(),
+ world,
+ change_tick,
+ }
+ }
+ }
+
+ impl<'w, 's, #(#param: SystemParam,)*> ParamSet<'w, 's, (#(#param,)*)>
+ {
+
+ #(#param_fn_mut)*
+ }
+ }));
+ }
+
+ tokens
+}
+
+#[derive(Default)]
+struct SystemParamFieldAttributes {
+ pub ignore: bool,
+}
+
+static SYSTEM_PARAM_ATTRIBUTE_NAME: &str = "system_param";
+
+/// Implement `SystemParam` to use a struct as a parameter in a system
+#[proc_macro_derive(SystemParam, attributes(system_param))]
+pub fn derive_system_param(input: TokenStream) -> TokenStream {
+ let ast = parse_macro_input!(input as DeriveInput);
+ let fields = match get_named_struct_fields(&ast.data) {
+ Ok(fields) => &fields.named,
+ Err(e) => return e.into_compile_error().into(),
+ };
+ let path = azalea_ecs_path();
+
+ let field_attributes = fields
+ .iter()
+ .map(|field| {
+ (
+ field,
+ field
+ .attrs
+ .iter()
+ .find(|a| *a.path.get_ident().as_ref().unwrap() == SYSTEM_PARAM_ATTRIBUTE_NAME)
+ .map_or_else(SystemParamFieldAttributes::default, |a| {
+ syn::custom_keyword!(ignore);
+ let mut attributes = SystemParamFieldAttributes::default();
+ a.parse_args_with(|input: ParseStream| {
+ if input.parse::<Option<ignore>>()?.is_some() {
+ attributes.ignore = true;
+ }
+ Ok(())
+ })
+ .expect("Invalid 'system_param' attribute format.");
+
+ attributes
+ }),
+ )
+ })
+ .collect::<Vec<(&Field, SystemParamFieldAttributes)>>();
+ let mut fields = Vec::new();
+ let mut field_indices = Vec::new();
+ let mut field_types = Vec::new();
+ let mut ignored_fields = Vec::new();
+ let mut ignored_field_types = Vec::new();
+ for (i, (field, attrs)) in field_attributes.iter().enumerate() {
+ if attrs.ignore {
+ ignored_fields.push(field.ident.as_ref().unwrap());
+ ignored_field_types.push(&field.ty);
+ } else {
+ fields.push(field.ident.as_ref().unwrap());
+ field_types.push(&field.ty);
+ field_indices.push(Index::from(i));
+ }
+ }
+
+ let generics = ast.generics;
+ let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
+
+ let lifetimeless_generics: Vec<_> = generics
+ .params
+ .iter()
+ .filter(|g| matches!(g, GenericParam::Type(_)))
+ .collect();
+
+ let mut punctuated_generics = Punctuated::<_, Token![,]>::new();
+ punctuated_generics.extend(lifetimeless_generics.iter().map(|g| match g {
+ GenericParam::Type(g) => GenericParam::Type(TypeParam {
+ default: None,
+ ..g.clone()
+ }),
+ _ => unreachable!(),
+ }));
+
+ let mut punctuated_generic_idents = Punctuated::<_, Token![,]>::new();
+ punctuated_generic_idents.extend(lifetimeless_generics.iter().map(|g| match g {
+ GenericParam::Type(g) => &g.ident,
+ _ => unreachable!(),
+ }));
+
+ let struct_name = &ast.ident;
+ let fetch_struct_visibility = &ast.vis;
+
+ TokenStream::from(quote! {
+ // We define the FetchState struct in an anonymous scope to avoid polluting the user namespace.
+ // The struct can still be accessed via SystemParam::Fetch, e.g. EventReaderState can be accessed via
+ // <EventReader<'static, 'static, T> as SystemParam>::Fetch
+ const _: () = {
+ impl #impl_generics #path::system::SystemParam for #struct_name #ty_generics #where_clause {
+ type Fetch = FetchState <(#(<#field_types as #path::system::SystemParam>::Fetch,)*), #punctuated_generic_idents>;
+ }
+
+ #[doc(hidden)]
+ #fetch_struct_visibility struct FetchState <TSystemParamState, #punctuated_generic_idents> {
+ state: TSystemParamState,
+ marker: std::marker::PhantomData<fn()->(#punctuated_generic_idents)>
+ }
+
+ unsafe impl<TSystemParamState: #path::system::SystemParamState, #punctuated_generics> #path::system::SystemParamState for FetchState <TSystemParamState, #punctuated_generic_idents> #where_clause {
+ fn init(world: &mut #path::world::World, system_meta: &mut #path::system::SystemMeta) -> Self {
+ Self {
+ state: TSystemParamState::init(world, system_meta),
+ marker: std::marker::PhantomData,
+ }
+ }
+
+ fn new_archetype(&mut self, archetype: &#path::archetype::Archetype, system_meta: &mut #path::system::SystemMeta) {
+ self.state.new_archetype(archetype, system_meta)
+ }
+
+ fn apply(&mut self, world: &mut #path::world::World) {
+ self.state.apply(world)
+ }
+ }
+
+ impl #impl_generics #path::system::SystemParamFetch<'w, 's> for FetchState <(#(<#field_types as #path::system::SystemParam>::Fetch,)*), #punctuated_generic_idents> #where_clause {
+ type Item = #struct_name #ty_generics;
+ unsafe fn get_param(
+ state: &'s mut Self,
+ system_meta: &#path::system::SystemMeta,
+ world: &'w #path::world::World,
+ change_tick: u32,
+ ) -> Self::Item {
+ #struct_name {
+ #(#fields: <<#field_types as #path::system::SystemParam>::Fetch as #path::system::SystemParamFetch>::get_param(&mut state.state.#field_indices, system_meta, world, change_tick),)*
+ #(#ignored_fields: <#ignored_field_types>::default(),)*
+ }
+ }
+ }
+
+ // Safety: The `ParamState` is `ReadOnlySystemParamFetch`, so this can only read from the `World`
+ unsafe impl<TSystemParamState: #path::system::SystemParamState + #path::system::ReadOnlySystemParamFetch, #punctuated_generics> #path::system::ReadOnlySystemParamFetch for FetchState <TSystemParamState, #punctuated_generic_idents> #where_clause {}
+ };
+ })
+}
+
+/// Implement `WorldQuery` to use a struct as a parameter in a query
+#[proc_macro_derive(WorldQuery, attributes(world_query))]
+pub fn derive_world_query(input: TokenStream) -> TokenStream {
+ let ast = parse_macro_input!(input as DeriveInput);
+ derive_world_query_impl(ast)
+}
+
+/// Generates an impl of the `SystemLabel` trait.
+///
+/// This works only for unit structs, or enums with only unit variants.
+/// You may force a struct or variant to behave as if it were fieldless with
+/// `#[system_label(ignore_fields)]`.
+#[proc_macro_derive(SystemLabel, attributes(system_label))]
+pub fn derive_system_label(input: TokenStream) -> TokenStream {
+ let input = parse_macro_input!(input as DeriveInput);
+ let mut trait_path = azalea_ecs_path();
+ trait_path.segments.push(format_ident!("schedule").into());
+ trait_path
+ .segments
+ .push(format_ident!("SystemLabel").into());
+ derive_label(input, &trait_path, "system_label")
+}
+
+/// Generates an impl of the `StageLabel` trait.
+///
+/// This works only for unit structs, or enums with only unit variants.
+/// You may force a struct or variant to behave as if it were fieldless with
+/// `#[stage_label(ignore_fields)]`.
+#[proc_macro_derive(StageLabel, attributes(stage_label))]
+pub fn derive_stage_label(input: TokenStream) -> TokenStream {
+ let input = parse_macro_input!(input as DeriveInput);
+ let mut trait_path = azalea_ecs_path();
+ trait_path.segments.push(format_ident!("schedule").into());
+ trait_path.segments.push(format_ident!("StageLabel").into());
+ derive_label(input, &trait_path, "stage_label")
+}
+
+/// Generates an impl of the `RunCriteriaLabel` trait.
+///
+/// This works only for unit structs, or enums with only unit variants.
+/// You may force a struct or variant to behave as if it were fieldless with
+/// `#[run_criteria_label(ignore_fields)]`.
+#[proc_macro_derive(RunCriteriaLabel, attributes(run_criteria_label))]
+pub fn derive_run_criteria_label(input: TokenStream) -> TokenStream {
+ let input = parse_macro_input!(input as DeriveInput);
+ let mut trait_path = azalea_ecs_path();
+ trait_path.segments.push(format_ident!("schedule").into());
+ trait_path
+ .segments
+ .push(format_ident!("RunCriteriaLabel").into());
+ derive_label(input, &trait_path, "run_criteria_label")
+}
+
+pub(crate) fn azalea_ecs_path() -> syn::Path {
+ BevyManifest::default().get_path("azalea_ecs")
+}
+
+#[proc_macro_derive(Resource)]
+pub fn derive_resource(input: TokenStream) -> TokenStream {
+ component::derive_resource(input)
+}
+
+#[proc_macro_derive(Component, attributes(component))]
+pub fn derive_component(input: TokenStream) -> TokenStream {
+ component::derive_component(input)
+}
diff --git a/azalea-ecs/azalea-ecs-macros/src/utils/attrs.rs b/azalea-ecs/azalea-ecs-macros/src/utils/attrs.rs
new file mode 100644
index 00000000..05f0712a
--- /dev/null
+++ b/azalea-ecs/azalea-ecs-macros/src/utils/attrs.rs
@@ -0,0 +1,45 @@
+#![allow(dead_code)]
+
+use syn::DeriveInput;
+
+use super::symbol::Symbol;
+
+pub fn parse_attrs(ast: &DeriveInput, attr_name: Symbol) -> syn::Result<Vec<syn::NestedMeta>> {
+ let mut list = Vec::new();
+ for attr in ast.attrs.iter().filter(|a| a.path == attr_name) {
+ match attr.parse_meta()? {
+ syn::Meta::List(meta) => list.extend(meta.nested.into_iter()),
+ other => {
+ return Err(syn::Error::new_spanned(
+ other,
+ format!("expected #[{attr_name}(...)]"),
+ ))
+ }
+ }
+ }
+ Ok(list)
+}
+
+pub fn get_lit_str(attr_name: Symbol, lit: &syn::Lit) -> syn::Result<&syn::LitStr> {
+ if let syn::Lit::Str(lit) = lit {
+ Ok(lit)
+ } else {
+ Err(syn::Error::new_spanned(
+ lit,
+ format!("expected {attr_name} attribute to be a string: `{attr_name} = \"...\"`"),
+ ))
+ }
+}
+
+pub fn get_lit_bool(attr_name: Symbol, lit: &syn::Lit) -> syn::Result<bool> {
+ if let syn::Lit::Bool(lit) = lit {
+ Ok(lit.value())
+ } else {
+ Err(syn::Error::new_spanned(
+ lit,
+ format!(
+ "expected {attr_name} attribute to be a bool value, `true` or `false`: `{attr_name} = ...`"
+ ),
+ ))
+ }
+}
diff --git a/azalea-ecs/azalea-ecs-macros/src/utils/mod.rs b/azalea-ecs/azalea-ecs-macros/src/utils/mod.rs
new file mode 100644
index 00000000..5fbba0e9
--- /dev/null
+++ b/azalea-ecs/azalea-ecs-macros/src/utils/mod.rs
@@ -0,0 +1,224 @@
+#![allow(dead_code)]
+
+extern crate proc_macro;
+
+mod attrs;
+mod shape;
+mod symbol;
+
+pub use attrs::*;
+pub use shape::*;
+pub use symbol::*;
+
+use proc_macro::TokenStream;
+use quote::{quote, quote_spanned};
+use std::{env, path::PathBuf};
+use syn::spanned::Spanned;
+use toml::{map::Map, Value};
+
+pub struct BevyManifest {
+ manifest: Map<String, Value>,
+}
+
+impl Default for BevyManifest {
+ fn default() -> Self {
+ Self {
+ manifest: env::var_os("CARGO_MANIFEST_DIR")
+ .map(PathBuf::from)
+ .map(|mut path| {
+ path.push("Cargo.toml");
+ let manifest = std::fs::read_to_string(path).unwrap();
+ toml::from_str(&manifest).unwrap()
+ })
+ .unwrap(),
+ }
+ }
+}
+
+impl BevyManifest {
+ pub fn maybe_get_path(&self, name: &str) -> Option<syn::Path> {
+ const AZALEA: &str = "azalea";
+ const BEVY_ECS: &str = "bevy_ecs";
+ const BEVY: &str = "bevy";
+
+ fn dep_package(dep: &Value) -> Option<&str> {
+ if dep.as_str().is_some() {
+ None
+ } else {
+ dep.as_table()
+ .unwrap()
+ .get("package")
+ .map(|name| name.as_str().unwrap())
+ }
+ }
+
+ let find_in_deps = |deps: &Map<String, Value>| -> Option<syn::Path> {
+ let package = if let Some(dep) = deps.get(name) {
+ return Some(Self::parse_str(dep_package(dep).unwrap_or(name)));
+ } else if let Some(dep) = deps.get(AZALEA) {
+ dep_package(dep).unwrap_or(AZALEA)
+ } else if let Some(dep) = deps.get(BEVY_ECS) {
+ dep_package(dep).unwrap_or(BEVY_ECS)
+ } else if let Some(dep) = deps.get(BEVY) {
+ dep_package(dep).unwrap_or(BEVY)
+ } else {
+ return None;
+ };
+
+ let mut path = Self::parse_str::<syn::Path>(package);
+ if let Some(module) = name.strip_prefix("azalea_") {
+ path.segments.push(Self::parse_str(module));
+ }
+ Some(path)
+ };
+
+ let deps = self
+ .manifest
+ .get("dependencies")
+ .map(|deps| deps.as_table().unwrap());
+ let deps_dev = self
+ .manifest
+ .get("dev-dependencies")
+ .map(|deps| deps.as_table().unwrap());
+
+ deps.and_then(find_in_deps)
+ .or_else(|| deps_dev.and_then(find_in_deps))
+ }
+
+ /// Returns the path for the crate with the given name.
+ ///
+ /// This is a convenience method for constructing a [manifest] and
+ /// calling the [`get_path`] method.
+ ///
+ /// This method should only be used where you just need the path and can't
+ /// cache the [manifest]. If caching is possible, it's recommended to create
+ /// the [manifest] yourself and use the [`get_path`] method.
+ ///
+ /// [`get_path`]: Self::get_path
+ /// [manifest]: Self
+ pub fn get_path_direct(name: &str) -> syn::Path {
+ Self::default().get_path(name)
+ }
+
+ pub fn get_path(&self, name: &str) -> syn::Path {
+ self.maybe_get_path(name)
+ .unwrap_or_else(|| Self::parse_str(name))
+ }
+
+ pub fn parse_str<T: syn::parse::Parse>(path: &str) -> T {
+ syn::parse(path.parse::<TokenStream>().unwrap()).unwrap()
+ }
+}
+
+/// Derive a label trait
+///
+/// # Args
+///
+/// - `input`: The [`syn::DeriveInput`] for struct that is deriving the label
+/// trait
+/// - `trait_path`: The path [`syn::Path`] to the label trait
+pub fn derive_label(
+ input: syn::DeriveInput,
+ trait_path: &syn::Path,
+ attr_name: &str,
+) -> TokenStream {
+ // return true if the variant specified is an `ignore_fields` attribute
+ fn is_ignore(attr: &syn::Attribute, attr_name: &str) -> bool {
+ if attr.path.get_ident().as_ref().unwrap() != &attr_name {
+ return false;
+ }
+
+ syn::custom_keyword!(ignore_fields);
+ attr.parse_args_with(|input: syn::parse::ParseStream| {
+ let ignore = input.parse::<Option<ignore_fields>>()?.is_some();
+ Ok(ignore)
+ })
+ .unwrap()
+ }
+
+ let ident = input.ident.clone();
+
+ let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
+ let mut where_clause = where_clause.cloned().unwrap_or_else(|| syn::WhereClause {
+ where_token: Default::default(),
+ predicates: Default::default(),
+ });
+ where_clause
+ .predicates
+ .push(syn::parse2(quote! { Self: 'static }).unwrap());
+
+ let as_str = match input.data {
+ syn::Data::Struct(d) => {
+ // see if the user tried to ignore fields incorrectly
+ if let Some(attr) = d
+ .fields
+ .iter()
+ .flat_map(|f| &f.attrs)
+ .find(|a| is_ignore(a, attr_name))
+ {
+ let err_msg = format!("`#[{attr_name}(ignore_fields)]` cannot be applied to fields individually: add it to the struct declaration");
+ return quote_spanned! {
+ attr.span() => compile_error!(#err_msg);
+ }
+ .into();
+ }
+ // Structs must either be fieldless, or explicitly ignore the fields.
+ let ignore_fields = input.attrs.iter().any(|a| is_ignore(a, attr_name));
+ if matches!(d.fields, syn::Fields::Unit) || ignore_fields {
+ let lit = ident.to_string();
+ quote! { #lit }
+ } else {
+ let err_msg = format!("Labels cannot contain data, unless explicitly ignored with `#[{attr_name}(ignore_fields)]`");
+ return quote_spanned! {
+ d.fields.span() => compile_error!(#err_msg);
+ }
+ .into();
+ }
+ }
+ syn::Data::Enum(d) => {
+ // check if the user put #[label(ignore_fields)] in the wrong place
+ if let Some(attr) = input.attrs.iter().find(|a| is_ignore(a, attr_name)) {
+ let err_msg = format!("`#[{attr_name}(ignore_fields)]` can only be applied to enum variants or struct declarations");
+ return quote_spanned! {
+ attr.span() => compile_error!(#err_msg);
+ }
+ .into();
+ }
+ let arms = d.variants.iter().map(|v| {
+ // Variants must either be fieldless, or explicitly ignore the fields.
+ let ignore_fields = v.attrs.iter().any(|a| is_ignore(a, attr_name));
+ if matches!(v.fields, syn::Fields::Unit) | ignore_fields {
+ let mut path = syn::Path::from(ident.clone());
+ path.segments.push(v.ident.clone().into());
+ let lit = format!("{ident}::{}", v.ident.clone());
+ quote! { #path { .. } => #lit }
+ } else {
+ let err_msg = format!("Label variants cannot contain data, unless explicitly ignored with `#[{attr_name}(ignore_fields)]`");
+ quote_spanned! {
+ v.fields.span() => _ => { compile_error!(#err_msg); }
+ }
+ }
+ });
+ quote! {
+ match self {
+ #(#arms),*
+ }
+ }
+ }
+ syn::Data::Union(_) => {
+ return quote_spanned! {
+ input.span() => compile_error!("Unions cannot be used as labels.");
+ }
+ .into();
+ }
+ };
+
+ (quote! {
+ impl #impl_generics #trait_path for #ident #ty_generics #where_clause {
+ fn as_str(&self) -> &'static str {
+ #as_str
+ }
+ }
+ })
+ .into()
+}
diff --git a/azalea-ecs/azalea-ecs-macros/src/utils/shape.rs b/azalea-ecs/azalea-ecs-macros/src/utils/shape.rs
new file mode 100644
index 00000000..98230749
--- /dev/null
+++ b/azalea-ecs/azalea-ecs-macros/src/utils/shape.rs
@@ -0,0 +1,21 @@
+use proc_macro::Span;
+use syn::{Data, DataStruct, Error, Fields, FieldsNamed};
+
+/// Get the fields of a data structure if that structure is a struct with named
+/// fields; otherwise, return a compile error that points to the site of the
+/// macro invocation.
+pub fn get_named_struct_fields(data: &syn::Data) -> syn::Result<&FieldsNamed> {
+ match data {
+ Data::Struct(DataStruct {
+ fields: Fields::Named(fields),
+ ..
+ }) => Ok(fields),
+ _ => Err(Error::new(
+ // This deliberately points to the call site rather than the structure
+ // body; marking the entire body as the source of the error makes it
+ // impossible to figure out which `derive` has a problem.
+ Span::call_site().into(),
+ "Only structs with named fields are supported",
+ )),
+ }
+}
diff --git a/azalea-ecs/azalea-ecs-macros/src/utils/symbol.rs b/azalea-ecs/azalea-ecs-macros/src/utils/symbol.rs
new file mode 100644
index 00000000..dc639f4d
--- /dev/null
+++ b/azalea-ecs/azalea-ecs-macros/src/utils/symbol.rs
@@ -0,0 +1,35 @@
+use std::fmt::{self, Display};
+use syn::{Ident, Path};
+
+#[derive(Copy, Clone)]
+pub struct Symbol(pub &'static str);
+
+impl PartialEq<Symbol> for Ident {
+ fn eq(&self, word: &Symbol) -> bool {
+ self == word.0
+ }
+}
+
+impl<'a> PartialEq<Symbol> for &'a Ident {
+ fn eq(&self, word: &Symbol) -> bool {
+ *self == word.0
+ }
+}
+
+impl PartialEq<Symbol> for Path {
+ fn eq(&self, word: &Symbol) -> bool {
+ self.is_ident(word.0)
+ }
+}
+
+impl<'a> PartialEq<Symbol> for &'a Path {
+ fn eq(&self, word: &Symbol) -> bool {
+ self.is_ident(word.0)
+ }
+}
+
+impl Display for Symbol {
+ fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str(self.0)
+ }
+}
diff --git a/azalea-ecs/src/lib.rs b/azalea-ecs/src/lib.rs
new file mode 100644
index 00000000..44579c5d
--- /dev/null
+++ b/azalea-ecs/src/lib.rs
@@ -0,0 +1,144 @@
+#![feature(trait_alias)]
+
+//! Re-export important parts of `bevy_ecs` and `bevy_app` and make them more
+//! compatible with Azalea.
+//!
+//! This is completely compatible with `bevy_ecs`, so it won't cause issues if
+//! you use plugins meant for Bevy.
+//!
+//! Changes:
+//! - Add [`TickPlugin`], [`TickStage`] and [`AppTickExt`]
+//! - Change the macros to use azalea/azalea_ecs instead of bevy/bevy_ecs
+//! - Rename bevy_ecs::world::World to azalea_ecs::ecs::Ecs
+//! - Re-export `bevy_app` in the `app` module.
+
+use std::time::{Duration, Instant};
+
+pub mod ecs {
+ pub use bevy_ecs::world::World as Ecs;
+ pub use bevy_ecs::world::{EntityMut, EntityRef, Mut};
+}
+pub mod component {
+ pub use azalea_ecs_macros::Component;
+ pub use bevy_ecs::component::{ComponentId, ComponentStorage, Components, TableStorage};
+
+ // we do this because re-exporting Component would re-export the macro as well,
+ // which is bad (since we have our own Component macro)
+ // instead, we have to do this so Component is a trait alias and the original
+ // impl-able trait is still available as BevyComponent
+ pub trait Component = bevy_ecs::component::Component;
+ pub use bevy_ecs::component::Component as BevyComponent;
+}
+pub mod bundle {
+ pub use azalea_ecs_macros::Bundle;
+ pub trait Bundle = bevy_ecs::bundle::Bundle;
+ pub use bevy_ecs::bundle::Bundle as BevyBundle;
+}
+pub mod system {
+ pub use azalea_ecs_macros::Resource;
+ pub use bevy_ecs::system::{
+ Command, Commands, EntityCommands, Query, Res, ResMut, SystemState,
+ };
+ pub trait Resource = bevy_ecs::system::Resource;
+ pub use bevy_ecs::system::Resource as BevyResource;
+}
+pub use bevy_app as app;
+pub use bevy_ecs::{entity, event, ptr, query, schedule, storage};
+
+use app::{App, CoreStage, Plugin};
+use bevy_ecs::schedule::*;
+use ecs::Ecs;
+
+pub struct TickPlugin {
+ /// How often a tick should happen. 50 milliseconds by default. Set to 0 to
+ /// tick every update.
+ pub tick_interval: Duration,
+}
+impl Plugin for TickPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_stage_before(
+ CoreStage::Update,
+ TickLabel,
+ TickStage {
+ interval: self.tick_interval,
+ next_tick: Instant::now(),
+ stage: Box::new(SystemStage::parallel()),
+ },
+ );
+ }
+}
+impl Default for TickPlugin {
+ fn default() -> Self {
+ Self {
+ tick_interval: Duration::from_millis(50),
+ }
+ }
+}
+
+#[derive(StageLabel)]
+struct TickLabel;
+
+/// A [`Stage`] that runs every 50 milliseconds.
+pub struct TickStage {
+ pub interval: Duration,
+ pub next_tick: Instant,
+ stage: Box<dyn Stage>,
+}
+
+impl Stage for TickStage {
+ fn run(&mut self, ecs: &mut Ecs) {
+ // if the interval is 0, that means it runs every tick
+ if self.interval.is_zero() {
+ self.stage.run(ecs);
+ return;
+ }
+ // keep calling run until it's caught up
+ // TODO: Minecraft bursts up to 10 ticks and then skips, we should too (but
+ // check the source so we do it right)
+ while Instant::now() > self.next_tick {
+ self.next_tick += self.interval;
+ self.stage.run(ecs);
+ }
+ }
+}
+
+pub trait AppTickExt {
+ fn add_tick_system_set(&mut self, system_set: SystemSet) -> &mut App;
+ fn add_tick_system<Params>(&mut self, system: impl IntoSystemDescriptor<Params>) -> &mut App;
+}
+
+impl AppTickExt for App {
+ /// Adds a set of ECS systems that will run every 50 milliseconds.
+ ///
+ /// Note that you should NOT have `EventReader`s in tick systems, as this
+ /// will make them sometimes be missed.
+ fn add_tick_system_set(&mut self, system_set: SystemSet) -> &mut App {
+ let tick_stage = self
+ .schedule
+ .get_stage_mut::<TickStage>(TickLabel)
+ .expect("Tick Stage not found");
+ let stage = tick_stage
+ .stage
+ .downcast_mut::<SystemStage>()
+ .expect("Fixed Timestep sub-stage is not a SystemStage");
+ stage.add_system_set(system_set);
+ self
+ }
+
+ /// Adds a new ECS system that will run every 50 milliseconds.
+ ///
+ /// Note that you should NOT have `EventReader`s in tick systems, as this
+ /// will make them sometimes be missed.
+ fn add_tick_system<Params>(&mut self, system: impl IntoSystemDescriptor<Params>) -> &mut App {
+ let tick_stage = self
+ .schedule
+ .get_stage_mut::<TickStage>(TickLabel)
+ .expect("Tick Stage not found");
+ let stage = tick_stage
+ .stage
+ .downcast_mut::<SystemStage>()
+ .expect("Fixed Timestep sub-stage is not a SystemStage");
+ stage.add_system(system);
+ self
+ }
+}