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}