From 89b733e00c9ef50aaaade5b86f44d840dc80d21c Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Fri, 23 Aug 2024 11:39:31 +0300 Subject: [PATCH] Make `PhysicsLayer` require a `#[default]` variant (#494) # Objective #476 changed collision layers to reserve the first bit for a default layer. However, for enums implementing `PhysicsLayer`, the default layer is rather implicit and potentially footgunny. The default layer should ideally be more explicit. ## Solution Change `PhysicsLayer` to require `Default` to be implemented. The proc macro orders the variants such that the layer set as the default is always the first bit. Note that `Default` *must* currently be derived instead of implemented manually, as the macro can only access the default variant if the `#[default]` attribute exists. An error is emitted if `#[default]` does not exist. If someone knows a way to make it work with the manual impl, let me know or consider opening a PR! I also added documentation for the `PhysicsLayer` macro, and improved error handling. --- ## Migration Guide Enums that derive `PhysicsLayer` must now also derive `Default` and specify a default layer using the `#[default]` attribute. Before: ```rust #[derive(PhysicsLayer)] enum GameLayer { Player, Enemy, Ground, } ``` After: ```rust #[derive(PhysicsLayer, Default)] enum GameLayer { #[default] Default, // The name doesn't matter, but Default is used here for clarity Player, Enemy, Ground, } ``` The default layer always has the bit value `0b0001` regardless of its order relative to the other variants. --------- Co-authored-by: Jan Hohenheim --- crates/avian2d/examples/collision_layers.rs | 4 +- crates/avian_derive/Cargo.toml | 1 + crates/avian_derive/src/lib.rs | 82 +++++++++++++++++++-- src/collision/layers.rs | 23 ++++-- 4 files changed, 96 insertions(+), 14 deletions(-) diff --git a/crates/avian2d/examples/collision_layers.rs b/crates/avian2d/examples/collision_layers.rs index 03cbe86f..e23fc824 100644 --- a/crates/avian2d/examples/collision_layers.rs +++ b/crates/avian2d/examples/collision_layers.rs @@ -20,8 +20,10 @@ fn main() { } // Define the collision layers -#[derive(PhysicsLayer)] +#[derive(PhysicsLayer, Default)] enum Layer { + #[default] + Default, Blue, Red, } diff --git a/crates/avian_derive/Cargo.toml b/crates/avian_derive/Cargo.toml index 9033379b..4b1a6100 100644 --- a/crates/avian_derive/Cargo.toml +++ b/crates/avian_derive/Cargo.toml @@ -14,5 +14,6 @@ bench = false [dependencies] proc-macro2 = "1.0.78" +proc-macro-error = "1.0" quote = "1.0" syn = "2.0" diff --git a/crates/avian_derive/src/lib.rs b/crates/avian_derive/src/lib.rs index 91385553..8d83f226 100644 --- a/crates/avian_derive/src/lib.rs +++ b/crates/avian_derive/src/lib.rs @@ -2,19 +2,51 @@ use proc_macro::TokenStream; -use quote::{quote, quote_spanned}; +use proc_macro_error::{abort, emit_error, proc_macro_error}; +use quote::quote; use syn::{parse_macro_input, spanned::Spanned, Data, DeriveInput}; -// Modified macro From the discontinued Heron, -// see https://github.com/jcornaz/heron/blob/main/macros/src/lib.rs +// Modified macro from the discontinued Heron +// https://github.com/jcornaz/heron/blob/main/macros/src/lib.rs +/// A derive macro for defining physics layers using an enum. +/// +/// Each variant of the enum represents a layer. Each layer has a unique bit determined by +/// the order of the variants. The bit value can be retrieved using the `to_bits` method. +/// +/// # Requirements +/// +/// - The enum must have at most 32 variants. +/// - The enum variants must not have any fields. +/// - The enum must have a default variant with the `#[default]` attribute. +/// - The first bit `1 << 0` will *always* be reserved for the default layer. +/// The bit values of the other layers are determined by their order in the enum, starting from `1 << 1`. +/// +/// # Example +/// +/// ```ignore +/// #[derive(PhysicsLayer, Clone, Copy, Debug, Default)] +/// enum GameLayer { +/// #[default] +/// Default, // Layer 0 - the default layer that objects are assigned to +/// Player, // Layer 1 +/// Enemy, // Layer 2 +/// Ground, // Layer 3 +/// } +/// +/// // The first bit is reserved for the default layer. +/// assert_eq!(GameLayer::default().to_bits(), 1 << 0); +/// +/// // The `GameLayer::Ground` layer is the fourth layer, so its bit value is `1 << 3`. +/// assert_eq!(GameLayer::Ground.to_bits(), 1 << 3); +/// ``` +#[proc_macro_error] #[proc_macro_derive(PhysicsLayer)] pub fn derive_physics_layer(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let enum_ident = input.ident; fn non_enum_item_error(span: proc_macro2::Span) -> TokenStream { - quote_spanned! { span => compile_error!("only enums can automatically derive PhysicsLayer"); } - .into() + abort!(span, "only enums can automatically derive `PhysicsLayer`"); } let variants = match &input.data { Data::Enum(data) => &data.variants, @@ -27,10 +59,39 @@ pub fn derive_physics_layer(input: TokenStream) -> TokenStream { }; if variants.len() > 32 { - return quote! { compile_error!("PhysicsLayer only supports a maximum of 32 layers"); } - .into(); + emit_error!( + enum_ident, + "`PhysicsLayer` only supports a maximum of 32 layers" + ); } + let mut default_variant_index = None; + + for (i, variant) in variants.iter().enumerate() { + for attr in variant.attrs.iter() { + if attr.path().is_ident("default") { + if default_variant_index.is_some() { + emit_error!(enum_ident, "multiple defaults"); + break; + } + default_variant_index = Some(i); + } + } + } + + let Some(default_variant_index) = default_variant_index else { + abort!( + enum_ident, + "`PhysicsLayer` enums must derive `Default` and have a variant annotated with the `#[default]` attribute."; + note = "Manually implementing `Default` using `impl Default for FooLayer` is not supported." + ); + }; + + // Move the default variant to the front (this probably isn't the best way to do this) + let mut variants = variants.iter().collect::>(); + let default_variant = variants.remove(default_variant_index); + variants.insert(0, default_variant); + let to_bits_result: Result, _> = variants .iter() .enumerate() @@ -47,7 +108,12 @@ pub fn derive_physics_layer(input: TokenStream) -> TokenStream { let to_bits = match to_bits_result { Ok(tokens) => tokens, - Err(span) => return quote_spanned! { span => compile_error!("can only derive PhysicsLayer for enums without fields"); }.into(), + Err(span) => { + abort!( + span, + "can only derive `PhysicsLayer` for enums without fields" + ); + } }; let all_bits: u32 = if variants.len() == 32 { diff --git a/src/collision/layers.rs b/src/collision/layers.rs index 594344f0..4e52cf2f 100644 --- a/src/collision/layers.rs +++ b/src/collision/layers.rs @@ -6,14 +6,17 @@ use bevy::prelude::*; /// Physics layers are used heavily by [`CollisionLayers`]. /// /// This trait can be derived for enums with `#[derive(PhysicsLayer)]`. -pub trait PhysicsLayer: Sized { +pub trait PhysicsLayer: Sized + Default { /// Converts the layer to a bitmask. fn to_bits(&self) -> u32; /// Creates a layer bitmask with all bits set to 1. fn all_bits() -> u32; } -impl PhysicsLayer for &L { +impl<'a, L: PhysicsLayer> PhysicsLayer for &'a L +where + &'a L: Default, +{ fn to_bits(&self) -> u32 { L::to_bits(self) } @@ -26,19 +29,27 @@ impl PhysicsLayer for &L { /// A bitmask for layers. /// /// A [`LayerMask`] can be constructed from bits directly, or from types implementing [`PhysicsLayer`]. +/// The first bit `0b0001` is reserved for the default layer, which all entities belong to by default. /// /// ``` #[cfg_attr(feature = "2d", doc = "# use avian2d::prelude::*;")] #[cfg_attr(feature = "3d", doc = "# use avian3d::prelude::*;")] /// # -/// #[derive(PhysicsLayer, Clone, Copy, Debug)] +/// #[derive(PhysicsLayer, Clone, Copy, Debug, Default)] /// enum GameLayer { +/// #[default] /// Default, // Layer 0 - the default layer that objects are assigned to /// Player, // Layer 1 /// Enemy, // Layer 2 /// Ground, // Layer 3 /// } /// +/// // The first bit is reserved for the default layer. +/// assert_eq!(GameLayer::default().to_bits(), 1 << 0); +/// +/// // The `GameLayer::Ground` layer is the fourth layer, so its bit value is `1 << 3`. +/// assert_eq!(GameLayer::Ground.to_bits(), 1 << 3); +/// /// // Here, `GameLayer::Enemy` is automatically converted to a `LayerMask` for the comparison. /// assert_eq!(LayerMask(0b00100), GameLayer::Enemy); /// ``` @@ -269,8 +280,9 @@ impl Not for LayerMask { #[cfg_attr(feature = "2d", doc = "# use avian2d::prelude::*;")] #[cfg_attr(feature = "3d", doc = "# use avian3d::prelude::*;")] /// # -/// #[derive(PhysicsLayer)] +/// #[derive(PhysicsLayer, Default)] /// enum GameLayer { +/// #[default] /// Default, // Layer 0 - the default layer that objects are assigned to /// Player, // Layer 1 /// Enemy, // Layer 2 @@ -430,8 +442,9 @@ mod tests { use crate::prelude::*; - #[derive(PhysicsLayer)] + #[derive(PhysicsLayer, Default)] enum GameLayer { + #[default] Default, Player, Enemy,