all_is_cubes/block/
attributes.rs

1//! [`BlockAttributes`] and closely related types.
2
3use core::{fmt, ops};
4
5use arcstr::ArcStr;
6
7use crate::inv::InvInBlock;
8use crate::math::{Face6, GridRotation};
9use crate::op::Operation;
10use crate::universe::{HandleVisitor, VisitHandles};
11
12use crate::block::Modifier;
13use crate::time;
14#[cfg(doc)]
15use crate::{
16    block::{Block, BlockDef, Primitive},
17    space::Space,
18    time::TickSchedule,
19};
20
21/// This single-use macro takes the [`BlockAttributes`] struct declaration and derives various
22/// items so that fewer other things need to be updated when an attribute is added or changed.
23macro_rules! derive_attribute_helpers {
24    ($(#[$_:meta])* pub struct BlockAttributes {
25        $(
26            $(#[doc = $field_doc:literal])*
27            #[custom(
28                arbitrary_type = $arbitrary_type:ty,
29                builder_param_style = $builder_param_style:tt,
30            )]
31            pub $field_name:ident: $field_type:ty,
32        )*
33    }) => {
34        impl fmt::Debug for BlockAttributes {
35            /// Only attributes which differ from the default are shown.
36            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37                if self == Self::DEFAULT_REF {
38                    // Avoid the braceless formatting that `debug_struct` uses
39                    // if no fields are given.
40                    write!(f, "BlockAttributes {{}}")
41                } else {
42                    let Self {
43                        $($field_name,)*
44                    } = self;
45
46                    let mut debug_struct = f.debug_struct("BlockAttributes");
47                    $(
48                        if *$field_name != Self::DEFAULT_REF.$field_name {
49                            debug_struct.field(stringify!($field_name), $field_name);
50                        }
51                    )*
52                    debug_struct.finish()
53                }
54            }
55        }
56
57        impl VisitHandles for BlockAttributes {
58            fn visit_handles(&self, visitor: &mut dyn HandleVisitor) {
59                let Self { $( $field_name, )* } = self;
60                $(
61                    <$field_type as VisitHandles>::visit_handles($field_name, visitor);
62                )*
63            }
64        }
65
66        #[cfg(feature = "arbitrary")]
67        #[mutants::skip]
68        impl<'a> arbitrary::Arbitrary<'a> for BlockAttributes {
69            fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
70                Ok(BlockAttributes {
71                    $(
72                        $field_name:
73                            <$arbitrary_type as arbitrary::Arbitrary>::arbitrary(u)?.into(),
74                    )*
75                })
76            }
77
78            fn size_hint(depth: usize) -> (usize, Option<usize>) {
79                Self::try_size_hint(depth).unwrap_or_default()
80            }
81            fn try_size_hint(
82                depth: usize,
83            ) -> Result<(usize, Option<usize>), arbitrary::MaxRecursionReached> {
84                arbitrary::size_hint::try_recursion_guard(depth, |depth| {
85                    Ok(arbitrary::size_hint::and_all(&[
86                        $(
87                            <$arbitrary_type as arbitrary::Arbitrary>::try_size_hint(depth)?,
88                        )*
89                    ]))
90                })
91            }
92        }
93
94        /// Methods for setting [attributes](BlockAttributes) of the block.
95        impl<P, Txn> crate::block::Builder<'_, P, Txn> {
96            $(
97                attribute_builder_method!($(#[doc = $field_doc] )* $builder_param_style $field_name: $field_type);
98            )*
99        }
100    };
101}
102
103#[rustfmt::skip] // avoid bug <https://github.com/rust-lang/rustfmt/issues/5489>
104macro_rules! attribute_builder_method {
105    // Custom name for the `inventory` field.
106    ($(#[doc = $field_doc:literal] )* exact inventory: $field_type:ty) => {
107        #[doc = concat!(
108            "Sets the value for [`BlockAttributes::inventory`], which is:",
109        )]
110        #[doc = ""]
111        $(#[doc = concat!("> ", $field_doc)] )*
112        pub fn inventory_config(mut self, value: crate::inv::InvInBlock) -> Self {
113            self.attributes.inventory = value.into();
114            self
115        }
116    };
117
118    // These two rules are identical except for the type of the method’s value parameter.
119    ($(#[doc = $field_doc:literal] )* exact $field_name:ident: $field_type:ty) => {
120        #[doc = concat!(
121            "Sets the value for [`BlockAttributes::",
122            stringify!($field_name),
123            "`], which is:",
124        )]
125        #[doc = ""]
126        $(#[doc = concat!("> ", $field_doc)] )*
127        pub fn $field_name(mut self, value: $field_type) -> Self {
128            self.attributes.$field_name = value;
129            self
130        }
131    };
132    ($(#[doc = $field_doc:literal] )* into $field_name:ident: $field_type:ty) => {
133        #[doc = concat!(
134            "Sets the value for [`BlockAttributes::",
135            stringify!($field_name),
136            "`], which is:",
137        )]
138        #[doc = ""]
139        $(#[doc = concat!("> ", $field_doc)] )*
140        pub fn $field_name(mut self, value: impl Into<$field_type>) -> Self {
141            self.attributes.$field_name = value.into();
142            self
143        }
144    };
145}
146
147/// Miscellaneous properties of blocks that are not the block’s voxels.
148///
149/// `BlockAttributes::default()` will produce a reasonable set of defaults for “ordinary”
150/// blocks.
151#[derive(Clone, Eq, Hash, PartialEq)]
152#[non_exhaustive]
153#[macro_rules_attribute::derive(derive_attribute_helpers!)]
154pub struct BlockAttributes {
155    /// The name that should be displayed to players.
156    ///
157    /// The default value is the empty string. The empty string should be considered a
158    /// reasonable choice for solid-color blocks with no special features.
159    //---
160    // Design note: The use of `ArcStr` allows cloning a `BlockAttributes` to be O(1)
161    // allocate no additional memory.
162    #[custom(
163        arbitrary_type = alloc::string::String,
164        builder_param_style = into,
165    )]
166    pub display_name: ArcStr,
167
168    /// Whether players' [cursors](crate::character::Cursor) target it or pass through it.
169    ///
170    /// The default value is `true`.
171    #[custom(
172        arbitrary_type = bool,
173        builder_param_style = exact,
174    )]
175    pub selectable: bool,
176
177    /// Definition of, if this block has an attached [`Modifier::Inventory`],
178    /// what size and rendering it has.
179    #[custom(
180        arbitrary_type = InvInBlock,
181        builder_param_style = exact,
182    )]
183    pub inventory: InvInBlock,
184
185    /// Continuous sound emission and absorption by this block.
186    #[custom(
187        arbitrary_type = crate::sound::Ambient,
188        builder_param_style = exact,
189    )]
190    pub ambient_sound: crate::sound::Ambient,
191
192    /// Rule about how this block should be rotated, or not, when placed in a [`Space`] by
193    /// some agent not otherwise specifying rotation.
194    ///
195    /// The default value is [`RotationPlacementRule::Never`].
196    //---
197    // TODO: Replace this with `placement_action` features?
198    #[custom(
199        arbitrary_type = RotationPlacementRule,
200        builder_param_style = exact,
201    )]
202    pub rotation_rule: RotationPlacementRule,
203
204    /// Something to do instead of placing this block in a [`Space`].
205    //---
206    // TODO: Should this not be optional, instead having a value that expresses the default
207    // block placement behavior?
208    #[custom(
209        arbitrary_type = Option<PlacementAction>,
210        builder_param_style = into,
211    )]
212    pub placement_action: Option<PlacementAction>,
213
214    /// Something this block does when time passes.
215    #[custom(
216        arbitrary_type =  Option<TickAction>,
217        builder_param_style = into,
218    )]
219    pub tick_action: Option<TickAction>,
220
221    /// Something this block does when activated with [`Activate`](crate::inv::Tool::Activate).
222    #[custom(
223        arbitrary_type = Option<Operation>,
224        builder_param_style = into,
225    )]
226    pub activation_action: Option<Operation>,
227
228    /// Advice to the renderer about how to expect this block to change, and hence
229    /// what rendering strategy to use.
230    ///
231    /// Note: This is automatically augmented for [`Primitive::Recur`] blocks if they
232    /// contain voxels with animation hints themselves.
233    #[custom(
234        arbitrary_type = AnimationHint,
235        builder_param_style = exact,
236    )]
237    pub animation_hint: AnimationHint,
238}
239
240impl BlockAttributes {
241    const DEFAULT: Self = BlockAttributes {
242        display_name: arcstr::literal!(""),
243        selectable: true,
244        inventory: InvInBlock::EMPTY,
245        ambient_sound: crate::sound::Ambient::SILENT,
246        rotation_rule: RotationPlacementRule::Never,
247        placement_action: None,
248        tick_action: None,
249        activation_action: None,
250        animation_hint: AnimationHint::UNCHANGING,
251    };
252    /// Declaring a &'static reference allows accessing the default's fields in constant
253    /// without constructing and dropping an instance which would trigger a
254    /// `feature(const_precise_live_drops)` requirement.
255    pub(crate) const DEFAULT_REF: &'static Self = &Self::DEFAULT;
256
257    /// Block attributes suitable as default values for in-game use.
258    ///
259    /// This function differs from the [`Default::default`] trait implementation only
260    /// in that it is a `const fn`.
261    pub const fn default() -> BlockAttributes {
262        Self::DEFAULT
263    }
264
265    #[mutants::skip] // currently used only as an optimization, and hard to test usefully
266    pub(crate) fn rotationally_symmetric(&self) -> bool {
267        let Self {
268            display_name: _,
269            selectable: _,
270            inventory,
271            ambient_sound: _,
272            rotation_rule,
273            placement_action,
274            tick_action,
275            activation_action,
276            animation_hint: _,
277        } = self;
278
279        inventory.rotationally_symmetric()
280            && rotation_rule.rotationally_symmetric()
281            && placement_action.as_ref().is_none_or(|a| a.rotationally_symmetric())
282            && tick_action.as_ref().is_none_or(|a| a.rotationally_symmetric())
283            && activation_action.as_ref().is_none_or(|a| a.rotationally_symmetric())
284    }
285
286    pub(crate) fn rotate(self, rotation: GridRotation) -> BlockAttributes {
287        let Self {
288            display_name,
289            selectable,
290            inventory,
291            ambient_sound,
292            rotation_rule,
293            placement_action,
294            tick_action,
295            activation_action,
296            animation_hint,
297        } = self;
298
299        Self {
300            display_name,
301            selectable,
302            inventory: inventory.rotate(rotation),
303            ambient_sound,
304            rotation_rule: rotation_rule.rotate(rotation),
305            placement_action: placement_action.map(|a| a.rotate(rotation)),
306            tick_action: tick_action.map(|a| a.rotate(rotation)),
307            activation_action: activation_action.map(|a| a.rotate(rotation)),
308            animation_hint,
309        }
310    }
311}
312
313impl Default for BlockAttributes {
314    /// Block attributes suitable as default values for in-game use.
315    fn default() -> BlockAttributes {
316        // Delegate to the inherent impl `const fn`.
317        BlockAttributes::default()
318    }
319}
320
321impl From<BlockAttributes> for Modifier {
322    /// Converts [`BlockAttributes`] to a modifier that applies them to a block.
323    fn from(value: BlockAttributes) -> Self {
324        Modifier::Attributes(alloc::sync::Arc::new(value))
325    }
326}
327
328/// Specifies the effect on a [`Body`](crate::physics::Body) of colliding with a
329/// [`Primitive::Atom`] block or voxel having this property.
330//
331// TODO: This is no longer a part of attributes. Move? Rename?
332#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
333#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
334#[non_exhaustive]
335pub enum BlockCollision {
336    /// The atom can be passed through; it is not an obstacle (though intersecting it
337    /// might cause other effects not directly part of collision response).
338    None,
339    /// The atom is a perfectly solid obstacle occupying its entire bounding cube.
340    ///
341    /// This is the default value used for most blocks.
342    Hard,
343    // Future values might include bouncy solid, water-like resistance, force fields, etc.
344}
345
346impl BlockCollision {
347    /// Value used when we are constructing a block from a color with default other
348    /// characteristics. This is not just a [`Default`] impl so that if we later decide
349    /// that e.g. transparent atoms automatically become non-colliding, we can replace
350    /// uses of this constant with that.
351    pub(crate) const DEFAULT_FOR_FROM_COLOR: Self = Self::Hard;
352}
353
354/// Rule about how this block should be rotated, or not, when placed in a [`Space`] by
355/// some agent not otherwise specifying rotation.
356///
357/// TODO: We may want to replace this with a struct that also carries declared symmetries
358/// ("this is a vertical pillar so never make it upside down") and/or prohibited rotations
359/// rather than requiring each individual rule variant to be sufficiently expressive.
360#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
361#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
362#[non_exhaustive]
363pub enum RotationPlacementRule {
364    /// Never rotate the block.
365    Never,
366    /// Rotate the block so that the specified face meets the face it was placed against.
367    Attach {
368        /// This face of the placed block will meet the face it was placed against.
369        ///
370        /// If the block was somehow placed without such an adjacent block, it will not be
371        /// rotated.
372        by: Face6,
373        // TODO: control rotation about additional axis
374    },
375}
376
377impl RotationPlacementRule {
378    pub(crate) fn rotationally_symmetric(self) -> bool {
379        match self {
380            RotationPlacementRule::Never => true,
381            RotationPlacementRule::Attach { by: _ } => false,
382        }
383    }
384
385    fn rotate(mut self, rotation: GridRotation) -> RotationPlacementRule {
386        match &mut self {
387            RotationPlacementRule::Attach { by } => *by = rotation.transform(*by),
388            RotationPlacementRule::Never => {}
389        }
390        self
391    }
392}
393
394impl VisitHandles for RotationPlacementRule {
395    fn visit_handles(&self, _: &mut dyn HandleVisitor) {}
396}
397
398/// Specifies how a [`Block`] might change in the very near future, for the benefit
399/// of rendering algorithms. Does not currently describe non-visual aspects of a block.
400///
401/// This should be configured for blocks which either are continuously animated in some
402/// fashion, or for which it is especially important that the specified changes are handled
403/// efficiently, at the possible cost of spending more resources on those blocks. Blocks
404/// which merely might change in response to user action should not set this hint.
405///
406/// The `|` operator may be used to combine multiple hints into “both of these will happen”.
407#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
408#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
409#[non_exhaustive]
410pub struct AnimationHint {
411    /// Ways in which the block's definition might change (via modification to a
412    /// [`BlockDef`] or recursive [`Space`]) such that many instances of this block
413    /// will become another simultaneously.
414    pub redefinition: AnimationChange,
415
416    /// If this block is likely to be replaced in a [`Space`] by another, this field
417    /// specifies the replacement's relation to this.
418    pub replacement: AnimationChange,
419}
420
421impl AnimationHint {
422    // TODO: get rid of these constants or replace them with a clearer new system
423
424    /// There are no expectations that the block is soon going to change.
425    ///
426    /// This is the default value of this type and within [`BlockAttributes`].
427    pub const UNCHANGING: Self = Self {
428        redefinition: AnimationChange::None,
429        replacement: AnimationChange::None,
430    };
431
432    /// Creates a hint that the block definition might be redefined,
433    /// in the ways specified by `change`.
434    pub const fn redefinition(change: AnimationChange) -> Self {
435        Self {
436            redefinition: change,
437            replacement: AnimationChange::None,
438        }
439    }
440
441    /// Creates a hint that the block will be replaced with another block, which differs in the
442    /// ways specified by `change`.
443    pub const fn replacement(change: AnimationChange) -> Self {
444        Self {
445            redefinition: AnimationChange::None,
446            replacement: change,
447        }
448    }
449
450    /// Returns whether this block's value for [`EvaluatedBlock::visible`] is likely to
451    /// change from `false` to `true` for animation reasons.
452    pub(crate) fn might_become_visible(self) -> bool {
453        self.redefinition.might_become_visible() || self.replacement.might_become_visible()
454    }
455}
456
457impl Default for AnimationHint {
458    fn default() -> Self {
459        Self::UNCHANGING
460    }
461}
462
463impl ops::BitOr for AnimationHint {
464    type Output = Self;
465    #[inline]
466    fn bitor(self, rhs: Self) -> Self::Output {
467        Self {
468            redefinition: self.redefinition | rhs.redefinition,
469            replacement: self.replacement | rhs.replacement,
470        }
471    }
472}
473impl ops::BitOrAssign for AnimationHint {
474    #[inline]
475    fn bitor_assign(&mut self, rhs: Self) {
476        *self = *self | rhs;
477    }
478}
479
480impl VisitHandles for AnimationHint {
481    fn visit_handles(&self, _: &mut dyn HandleVisitor) {}
482}
483
484/// Component of [`AnimationHint`], describing the type of change predicted.
485#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
486#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
487#[non_exhaustive]
488pub enum AnimationChange {
489    /// Expect no changes.
490    None,
491    /// Expect that the block’s voxels’ colors will change, while remaining within the
492    /// same [`OpacityCategory`](crate::math::OpacityCategory); that is, the alpha will
493    /// remain 0, remain 1, or remain neither 0 nor 1.
494    ///
495    /// Suggestion to renderers: prepare to update texturing without recomputing an
496    /// otherwise identical mesh.
497    ColorSameCategory,
498    /// Expect that the block’s colors and shape will change; that is, at least some
499    /// voxels’ alpha will move from one [`OpacityCategory`](crate::math::OpacityCategory)
500    /// to another.
501    ///
502    /// Suggestion to renderers: use a rendering strategy which is shape-independent, or
503    /// prepare to efficiently recompute the mesh (don't merge with neighbors).
504    Shape,
505}
506
507impl AnimationChange {
508    /// Helper for [`AnimationHint::might_become_visible`].
509    fn might_become_visible(self) -> bool {
510        match self {
511            AnimationChange::None => false,
512            // same category implies not becoming visible if invisible
513            AnimationChange::ColorSameCategory => false,
514            AnimationChange::Shape => true,
515        }
516    }
517}
518
519impl ops::BitOr for AnimationChange {
520    type Output = Self;
521    #[inline]
522    fn bitor(self, rhs: Self) -> Self::Output {
523        use AnimationChange::*;
524        match (self, rhs) {
525            (Shape, _) | (_, Shape) => Shape,
526            (ColorSameCategory, _) | (_, ColorSameCategory) => ColorSameCategory,
527            (None, None) => None,
528        }
529    }
530}
531
532/// Something a block does when time passes.
533///
534/// Stored in [`BlockAttributes`].
535#[derive(Clone, Debug, Eq, Hash, PartialEq)]
536#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
537#[expect(clippy::exhaustive_structs, reason = "will deliberately break")]
538pub struct TickAction {
539    /// Operation to perform on the schedule.
540    pub operation: Operation,
541    /// Period, relative to the universe's [`TickSchedule`]'s tick length, with which
542    /// to perform this action.
543    ///
544    /// For example, if this is `1`, then it will be executed on every tick.
545    //---
546    // TODO: This should probably be its own data type
547    pub schedule: time::Schedule,
548}
549
550impl TickAction {
551    fn rotationally_symmetric(&self) -> bool {
552        let Self {
553            operation,
554            schedule: _,
555        } = self;
556        operation.rotationally_symmetric()
557    }
558
559    fn rotate(self, rotation: GridRotation) -> TickAction {
560        let Self {
561            operation,
562            schedule,
563        } = self;
564        let operation = operation.rotate(rotation);
565        Self {
566            operation,
567            schedule,
568        }
569    }
570}
571
572impl From<Operation> for TickAction {
573    /// TODO: remove uses of this
574    fn from(operation: Operation) -> Self {
575        Self {
576            operation,
577            schedule: time::Schedule::EVERY_TICK,
578        }
579    }
580}
581
582impl VisitHandles for TickAction {
583    fn visit_handles(&self, visitor: &mut dyn HandleVisitor) {
584        let Self {
585            operation,
586            schedule: _,
587        } = self;
588        operation.visit_handles(visitor);
589    }
590}
591
592/// [Attribute](BlockAttributes) of a [`Block`] that specifies what happens instead of the default
593/// when a player attempts to place it in some [`Space`].
594#[derive(Clone, Debug, Eq, Hash, PartialEq)]
595#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
596#[expect(clippy::exhaustive_structs, reason = "no better plan yet")]
597pub struct PlacementAction {
598    /// The operation to perform instead of placing this block.
599    pub operation: Operation,
600
601    /// Whether to affect the cube in front of (nearer to the character than) the targeted cube,
602    /// instead of the targeted block itself.
603    /// `true` is the normal behavior for placed blocks;
604    /// `false` is for modifying a block already present in some way.
605    pub in_front: bool,
606}
607
608impl PlacementAction {
609    fn rotationally_symmetric(&self) -> bool {
610        let Self {
611            operation,
612            in_front: _,
613        } = self;
614        operation.rotationally_symmetric()
615    }
616
617    fn rotate(self, rotation: GridRotation) -> PlacementAction {
618        Self {
619            operation: self.operation.rotate(rotation),
620            in_front: self.in_front,
621        }
622    }
623}
624
625impl VisitHandles for PlacementAction {
626    fn visit_handles(&self, visitor: &mut dyn HandleVisitor) {
627        let Self {
628            operation,
629            in_front: _,
630        } = self;
631        operation.visit_handles(visitor);
632    }
633}
634
635#[cfg(test)]
636mod tests {
637    use super::*;
638    use alloc::string::String;
639
640    #[test]
641    fn size() {
642        let size = size_of::<BlockAttributes>();
643        assert!(
644            size <= 360,
645            "size_of::<BlockAttributes>() = {size} unexpectedly large"
646        );
647    }
648
649    /// [`BlockAttributes`] has an inherent `default()` function, which should be
650    /// equivalent to the [`Default`] trait function.
651    #[test]
652    fn default_equivalent() {
653        assert_eq!(
654            BlockAttributes::default(),
655            <BlockAttributes as Default>::default()
656        );
657    }
658
659    #[test]
660    fn debug() {
661        let default = BlockAttributes::default;
662        #[expect(clippy::needless_pass_by_value)]
663        fn debug(a: BlockAttributes) -> String {
664            format!("{a:?}")
665        }
666        assert_eq!(&*debug(BlockAttributes::default()), "BlockAttributes {}",);
667        assert_eq!(
668            &*debug(BlockAttributes {
669                display_name: "x".into(),
670                ..default()
671            }),
672            "BlockAttributes { display_name: \"x\" }",
673        );
674        assert_eq!(
675            &*debug(BlockAttributes {
676                selectable: false,
677                ..default()
678            }),
679            "BlockAttributes { selectable: false }",
680        );
681        assert_eq!(
682            &*debug(BlockAttributes {
683                animation_hint: AnimationHint::replacement(AnimationChange::Shape),
684                ..default()
685            }),
686            "BlockAttributes { animation_hint: \
687            AnimationHint { redefinition: None, replacement: Shape } }",
688        );
689
690        // Test a case of multiple attributes
691        assert_eq!(
692            &*debug(BlockAttributes {
693                display_name: "y".into(),
694                selectable: false,
695                ..default()
696            }),
697            "BlockAttributes { display_name: \"y\", selectable: false }",
698        );
699    }
700}