all_is_cubes/block/modifier/
mod.rs

1use core::fmt;
2
3use alloc::sync::Arc;
4use alloc::vec::Vec;
5
6use crate::block::{self, Block, BlockAttributes, Evoxels, MinEval};
7use crate::inv;
8use crate::math::{GridRotation, Vol};
9use crate::tag;
10use crate::universe::{HandleVisitor, VisitHandles};
11
12mod composite;
13pub use composite::*;
14#[cfg(test)]
15mod inventory_tests;
16mod r#move;
17pub use r#move::*;
18mod quote;
19pub use quote::*;
20#[cfg(test)]
21mod rotate_tests;
22mod zoom;
23pub use zoom::*;
24
25/// Modifiers can be applied to a [`Block`] to change the result of
26/// [`evaluate()`](Block::evaluate)ing it, and thus create variations, such as rotations
27/// or combinations of multiple blocks.
28///
29/// # Usage
30///
31/// Most modifiers have their own dedicated structs, such as [`Composite`]; these may
32/// be converted to [`Modifier`] using their [`From`] implementations, or by constructing
33/// the enum variant ([`Modifier::Composite`]) explicitly. Some modifiers have specific
34/// functions for constructing their typical usages, such as [`Block::rotate()`].
35///
36/// [`Block::with_modifier()`] is provided to conveniently add a single modifier to a block;
37/// [`Block::modifiers()`] and [`Block::modifiers_mut()`] provide general direct access.
38/// Note that [`Block`] is a clone-on-write type for when modifiers are changed.
39///
40/// # Arranging modifiers
41///
42/// Operations which add or remove modifiers, such as [`Block::rotate()`],
43/// follow some general principles and special cases:
44///
45/// * There should not be consecutive [`Rotate`] modifiers, but a single
46///   one with the combined rotation. [`Block::rotate()`] maintains this property.
47/// * It is preferable to have [`Rotate`] appear last, since rotation and
48///   [unrotation](Block::unspecialize) is part of player interaction, and the identity
49///   of block modifiers, not just their final result, determines whether blocks are
50///   equal for purposes of inventory management.
51///     * [`Composite::compose_or_replace()`] avoids applying [`Composite`] after
52///       [`Rotate`], so that rotated versions of the same combination are represented
53///       identically.
54///
55/// There is not yet any general “algebra” defining all cases where combinations of
56/// modifiers should be canonicalized to other forms. Future versions of All is Cubes may
57/// do so; that will be a breaking change (particularly since [`Block::modifiers_mut()`]
58/// exists, so no rules are currently enforceable).
59///
60/// [`Rotate`]: Self::Rotate
61#[derive(Clone, Eq, Hash, PartialEq)]
62#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
63#[non_exhaustive]
64pub enum Modifier {
65    /// Sets or overrides the [attributes](BlockAttributes) of the block.
66    //---
67    // Design note: Indirection is used here to keep `Modifier` small.
68    // `Arc` specifically is used so cloning does not allocate.
69    Attributes(Arc<BlockAttributes>),
70
71    /// Applies the given tag to this block.
72    Tag(tag::Be),
73
74    /// Suppresses all behaviors of the [`Block`] that might affect the space around it,
75    /// (or itself).
76    Quote(Quote),
77
78    /// Rotate the block about its cube center by the given rotation.
79    ///
80    /// This modifier should normally be used by means of [`Block::rotate()`].
81    Rotate(GridRotation),
82
83    /// Combine the voxels of multiple blocks using some per-voxel rule.
84    Composite(Composite),
85
86    /// Zoom in on a portion of the block; become part of a multi-block structure whose
87    /// parts are parts of the original block.
88    Zoom(Zoom),
89
90    /// Displace the block out of the grid, cropping it.
91    Move(Move),
92
93    /// The block has an inventory (e.g. a chest, a dropped item, a machine).
94    ///
95    /// The effects this inventory has on the evaluation of this block are determined by
96    /// the [`InvInBlock`](inv::InvInBlock) stored in [`BlockAttributes::inventory`].
97    Inventory(inv::Inventory),
98}
99
100impl fmt::Debug for Modifier {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        // Print most modifiers’ data without the enum variant, because their struct names
103        // are identifying enough.
104        match self {
105            Self::Attributes(a) => a.fmt(f),
106            Self::Tag(t) => t.fmt(f),
107            Self::Quote(q) => q.fmt(f),
108            Self::Rotate(r) => write!(f, "Rotate({r:?})"),
109            Self::Composite(c) => c.fmt(f),
110            Self::Zoom(z) => z.fmt(f),
111            Self::Move(m) => m.fmt(f),
112            Self::Inventory(i) => i.fmt(f),
113        }
114    }
115}
116
117impl Modifier {
118    /// Compute the effect of this modifier.
119    ///
120    /// * `block` is the original block value (modifiers do not alter it).
121    /// * `this_modifier_index` is the index in `block.modifiers()` of `self`.
122    ///   In cases where the modifier needs to produce another block value,
123    ///   such as generating a [`Modifier::Become`], all modifiers following
124    ///   `this_modifier_index` should be omitted.
125    ///   TODO: Make it easier to comply.
126    /// * `value` is the output of the preceding modifier or primitive, which is what the
127    ///   current modifier should be applied to.
128    /// * `filter` controls evaluation options and listening, and its budget is decremented by
129    ///   1 component (the modifier itself) plus as many voxels and additional components as the
130    ///   particular modifier needs to calculate.
131    pub(in crate::block) fn evaluate(
132        &self,
133        block: &Block,
134        this_modifier_index: usize,
135        mut value: MinEval,
136        filter: &block::EvalFilter<'_>,
137    ) -> Result<MinEval, block::InEvalError> {
138        block::Budget::decrement_components(&filter.budget)?;
139
140        Ok(match *self {
141            // TODO: Eventually, we want to be able to override individual attributes.
142            // We will need a new schema (possibly a set of individual modifiers) for that.
143            Modifier::Attributes(ref attributes) => {
144                value.set_attributes(BlockAttributes::clone(attributes));
145                value
146            }
147
148            Modifier::Tag(_) => {
149                // For now, the tag modifier does nothing — it is only examined directly to match
150                // `Block` values.
151                value
152            }
153
154            Modifier::Quote(ref quote) => quote.evaluate(value, filter)?,
155
156            Modifier::Rotate(rotation) => evaluate_rotate(value, filter, rotation)?,
157
158            Modifier::Composite(ref c) => c.evaluate(block, this_modifier_index, value, filter)?,
159
160            Modifier::Zoom(ref z) => z.evaluate(value, filter)?,
161
162            Modifier::Move(ref m) => m.evaluate(block, this_modifier_index, value, filter)?,
163
164            // Inventories are rendered by compositing their icon blocks in.
165            Modifier::Inventory(ref i) => render_inventory(value, i, filter)?,
166        })
167    }
168
169    /// Given a [`Block`] whose last modifier is `self`, returns the block that
170    /// [`Block::unspecialize`] should produce instead of the modified block.
171    pub(crate) fn unspecialize(&self, block: &Block) -> ModifierUnspecialize {
172        // When modifying this match, update the public documentation of `Block::unspecialize` too.
173        match self {
174            Modifier::Attributes(_) => ModifierUnspecialize::Keep,
175
176            Modifier::Tag(_) => ModifierUnspecialize::Keep,
177
178            Modifier::Quote(_) => ModifierUnspecialize::Keep,
179
180            Modifier::Rotate(_) => ModifierUnspecialize::Pop,
181
182            Modifier::Composite(c) => c.unspecialize(block),
183
184            // TODO: Implement removal of multiblock structures.
185            // This will require awareness of neighboring blocks (so that the whole set
186            // becomes one block) and probably a total replacement of the unspecialize() design.
187            Modifier::Zoom(_) => ModifierUnspecialize::Keep,
188
189            // TODO: Implement deletion of moving blocks.
190            // This is essentially a 2-block multiblock situation.
191            Modifier::Move(_) => ModifierUnspecialize::Keep,
192
193            Modifier::Inventory(i) => {
194                // TODO(inventory): Should be possible for the contents of the inventory to be
195                // split off, depending on the block definition's desires and possibly on exactly
196                // what role this unspecialize operation is playing.
197                if i.is_empty() {
198                    ModifierUnspecialize::Pop
199                } else {
200                    ModifierUnspecialize::Keep
201                }
202            }
203        }
204    }
205
206    /// This is like `rotationally_symmetric()` on many other data types, except that it doesn't
207    /// describe whether it _affects_ rotations — in particular, it does not mean “this modifier
208    /// commutes with `Rotate` modifiers”, but rather “will it introduce asymmetry to a block that
209    /// had none”.
210    pub(crate) fn does_not_introduce_asymmetry(&self) -> bool {
211        match self {
212            Modifier::Attributes(attr) => attr.rotationally_symmetric(),
213            // TODO: I am pretty sure I want tags to be able to identify faces/rotations
214            // but they don't currently
215            Modifier::Tag(_) => true,
216            // Quote has no asymmetry
217            Modifier::Quote(_) => true,
218            // Rotate may change existing asymmetry but does not introduce it
219            Modifier::Rotate(_) => true,
220            Modifier::Composite(c) => c.rotationally_symmetric(),
221            // Technically, Zoom on a resolution-1 block is symmetric but that case is
222            // hard to calculate without knowing something about the primitive.
223            Modifier::Zoom(_) => false,
224
225            Modifier::Move(_) => false,
226            // This requires knowing the `InvInBlock` to answer true, and hence requires evaluation.
227            // So don't try.
228            Modifier::Inventory(_) => false,
229        }
230    }
231
232    /// Rotates this modifier, so that whatever asymmetric effects it has, it has them in
233    /// different directions.
234    ///
235    /// If [`Self::does_not_introduce_asymmetry()`] returns `true`, then this has no
236    /// effect.
237    pub(crate) fn rotate(self, rotation: GridRotation) -> Self {
238        match self {
239            Modifier::Attributes(a) => {
240                Modifier::Attributes(Arc::new((*a).clone().rotate(rotation)))
241            }
242            // TODO: I am pretty sure I want tags to be able to identify faces/rotations
243            // but they don't currently
244            m @ Modifier::Tag(_) => m,
245            Modifier::Rotate(r) => Modifier::Rotate(rotation * r * rotation.inverse()), // TODO: no test this is correct yet
246            Modifier::Composite(c) => Modifier::Composite(c.rotate(rotation)),
247            Modifier::Zoom(z) => Modifier::Zoom(z.rotate(rotation)),
248            Modifier::Move(m) => Modifier::Move(m.rotate(rotation)),
249            m @ (Modifier::Inventory(_) | Modifier::Quote(_)) => m,
250        }
251    }
252}
253
254impl VisitHandles for Modifier {
255    fn visit_handles(&self, visitor: &mut dyn HandleVisitor) {
256        match self {
257            Modifier::Attributes(a) => a.visit_handles(visitor),
258            Modifier::Tag(t) => t.visit_handles(visitor),
259            Modifier::Quote(m) => m.visit_handles(visitor),
260            Modifier::Rotate(_) => {}
261            Modifier::Composite(m) => m.visit_handles(visitor),
262            Modifier::Zoom(m) => m.visit_handles(visitor),
263            Modifier::Move(m) => m.visit_handles(visitor),
264            Modifier::Inventory(i) => i.visit_handles(visitor),
265        }
266    }
267}
268
269/// Result of [`Modifier::unspecialize()`] returned to [`Block::unspecialize()`].
270#[derive(Debug)]
271pub(crate) enum ModifierUnspecialize {
272    /// Produce the block unchanged.
273    Keep,
274    /// Pop the modifier.
275    Pop,
276    /// Replace with a different set of blocks.
277    /// `unspecialize()` will be called on each of those automatically.
278    Replace(Vec<Block>),
279}
280
281/// Implementation of [`Modifier::Rotate`].
282#[inline(never)] // this function exists largely to have a named function for profiling
283fn evaluate_rotate(
284    value: MinEval,
285    filter: &block::EvalFilter<'_>,
286    rotation: GridRotation,
287) -> Result<MinEval, block::InEvalError> {
288    Ok(
289        if filter.skip_eval || rotation == GridRotation::IDENTITY || value.rotationally_symmetric()
290        {
291            // Skip computation of transforms
292            value
293        } else {
294            let (attributes, voxels) = value.into_parts();
295            block::Budget::decrement_voxels(&filter.budget, voxels.count())?;
296
297            // It'd be nice if this rotation operation were in-place, but I've read that
298            // it's actually quite difficult to implement a 3D array rotation in-place.
299            // (But another possible improvement would be to have a spare buffer to reuse
300            // across multiple evaluations/steps.)
301            // TODO: But check if we can make the arithmetic simpler by using incrementing
302            // instead of running a general transform on every Cube.
303
304            let resolution = voxels.resolution();
305            let inner_to_outer = rotation.to_positive_octant_transform(resolution.into());
306            let outer_to_inner = rotation.inverse().to_positive_octant_transform(resolution.into());
307
308            MinEval::new(
309                attributes.rotate(rotation),
310                Evoxels::from_many(
311                    resolution,
312                    Vol::from_fn(voxels.bounds().transform(inner_to_outer).unwrap(), |cube| {
313                        voxels.get(outer_to_inner.transform_cube(cube)).unwrap()
314                    }),
315                ),
316            )
317        },
318    )
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324    use crate::block::BlockAttributes;
325    use pretty_assertions::assert_eq;
326
327    /// Track the size of the `Modifier` enum to make sure we don't accidentally make it bigger
328    /// by giving one variant more data.
329    #[test]
330    fn size_of_modifier() {
331        // The largest modifier, currently, is `Composite`, which contains a `Block` plus
332        // additional data, and a `Block` is a pointer plus additional data;
333        // in each case the additional data does not exceed 4 bytes, so on both 32 and
334        // 64-bit systems, the size will be rounded up to three pointers
335        // (unless the alignment of pointers is less than their size).
336        assert_eq!(size_of::<Modifier>(), 3 * size_of::<*const ()>());
337    }
338
339    #[test]
340    fn modifier_debug() {
341        let modifiers: Vec<Modifier> = vec![
342            BlockAttributes {
343                display_name: arcstr::literal!("hello"),
344                ..Default::default()
345            }
346            .into(),
347            Modifier::Quote(Quote::new()),
348            Modifier::Rotate(GridRotation::RXyZ),
349            Modifier::Composite(Composite::new(block::AIR, CompositeOperator::Over)),
350            Modifier::Inventory(inv::Inventory::from_slots([])),
351        ];
352        assert_eq!(
353            format!("{modifiers:#?}"),
354            indoc::indoc! {
355                r#"[
356                    BlockAttributes {
357                        display_name: "hello",
358                    },
359                    Quote {
360                        suppress_ambient: false,
361                    },
362                    Rotate(RXyZ),
363                    Composite {
364                        source: Block {
365                            primitive: Air,
366                        },
367                        operator: Over,
368                        reverse: false,
369                        disassemblable: false,
370                    },
371                    Inventory {
372                        slots: [],
373                    },
374                ]"#
375            }
376        );
377    }
378}