all_is_cubes/inv/
tool.rs

1//! [`Tool`] and related.
2
3use alloc::borrow::Cow;
4use alloc::string::{String, ToString};
5use alloc::sync::Arc;
6use core::{fmt, hash};
7
8use crate::block::{self, AIR, Block, Primitive, RotationPlacementRule};
9use crate::character::{self, Character, CharacterTransaction, Cursor};
10use crate::fluff::Fluff;
11use crate::inv::{self, Icons, InventoryTransaction, StackLimit};
12use crate::linking::BlockProvider;
13use crate::math::{Cube, Face6, GridRotation, Gridgid};
14use crate::op::{self, Operation};
15use crate::space::{CubeTransaction, Space, SpaceTransaction};
16use crate::transaction::{Merge, Transaction};
17use crate::universe::{
18    Handle, HandleError, HandleVisitor, ReadTicket, UniverseTransaction, VisitHandles,
19};
20
21/// A `Tool` is an object which a character can use to have some effect in the game,
22/// such as placing or removing a block. In particular, a tool use usually corresponds
23/// to a click.
24///
25/// Currently, `Tool`s also play the role of “inventory items”. This may change in the
26/// future.
27///
28#[doc = include_str!("../save/serde-warning.md")]
29#[derive(Clone, Debug, Eq, Hash, PartialEq)]
30#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
31#[non_exhaustive]
32pub enum Tool {
33    /// “Click”, or “push button”, or generally “activate the function of this”
34    /// as opposed to editing it.
35    ///
36    /// This can activate an [`ActivatableRegion`](crate::space::ActivatableRegion).
37    /// It may have more functions in the future.
38    Activate,
39
40    /// Delete any targeted block from the space.
41    RemoveBlock {
42        /// If true, move it to inventory. If false, discard it entirely.
43        keep: bool,
44    },
45
46    /// Move the given block out of inventory (consuming this tool) into the targeted
47    /// empty space.
48    Block(Block),
49
50    /// Places copies of the given block in targeted empty space. Infinite uses.
51    InfiniteBlocks(Block),
52
53    /// Copy block from space to inventory.
54    CopyFromSpace,
55
56    /// Teleport into a block's space for editing.
57    ///
58    /// TODO: This is not yet actually implemented.
59    EditBlock,
60
61    /// Push targeted block into adjacent cube.
62    PushPull,
63
64    /// Allows flight.
65    ///
66    /// TODO: This should probably be a feature a tool can have rather than a
67    /// single-purpose item, but we don't yet have a plan for programmable items.
68    Jetpack {
69        /// Actually currently flying?
70        active: bool,
71    },
72
73    /// A tool which performs an arbitrary [`Operation`].
74    // ---
75    // TODO: Custom tools like this should be able to have their definitions stored in the
76    // Universe. Probably `Operation` should have that and tools become a case of it?
77    Custom {
78        /// Operation to perform when the tool is used.
79        op: Operation,
80        /// Icon for the tool.
81        icon: Block,
82    },
83}
84
85impl Tool {
86    /// Computes the effect of using the tool.
87    ///
88    /// The effect consists of both mutations to `self` and a [`UniverseTransaction`].
89    /// If the result is `None` then the tool is deleted.
90    /// If the transaction does not succeed, the original `Tool` value should be kept.
91    ///
92    /// This function has no side effects; for example, it may be safely used to determine
93    /// *whether* a tool applies or not.
94    ///
95    /// TODO: Return type is inelegant
96    pub fn use_tool(
97        self,
98        input: &ToolInput<'_>,
99    ) -> Result<(Option<Self>, UniverseTransaction), ToolError> {
100        match self {
101            Self::Activate => {
102                let cursor = input.cursor()?;
103                if let Some(activation_action) =
104                    &cursor.hit().evaluated.attributes().activation_action
105                {
106                    Ok((
107                        Some(self),
108                        input.apply_operation(activation_action, GridRotation::IDENTITY, false)?,
109                    ))
110                } else {
111                    // TODO: we should probably replace the activate transaction with some other
112                    // mechanism, that can communicate "nothing found".
113                    Ok((
114                        Some(self),
115                        CubeTransaction::ACTIVATE_BEHAVIOR
116                            .at(cursor.cube())
117                            .bind(cursor.space().clone()),
118                    ))
119                }
120            }
121            Self::RemoveBlock { keep } => {
122                let cursor = input.cursor()?;
123                let mut deletion =
124                    input.set_cube(cursor.cube(), cursor.hit().block.clone(), AIR)?;
125                if keep {
126                    deletion
127                        .merge_from(input.produce_items(
128                            cursor.hit().block.unspecialize().into_iter().map(Tool::Block),
129                        )?)
130                        .unwrap();
131                }
132                Ok((Some(self), deletion))
133            }
134            Self::Block(ref block) => {
135                let cursor = input.cursor()?;
136                let block = block.clone();
137                Ok((None, input.place_block(cursor, AIR, block)?))
138            }
139            Self::InfiniteBlocks(ref block) => {
140                let cursor = input.cursor()?;
141                let block = block.clone();
142                Ok((Some(self), input.place_block(cursor, AIR, block)?))
143            }
144            Self::CopyFromSpace => {
145                let cursor = input.cursor()?;
146                // TODO: if inventory already contains tool then don't add it, just select
147                // it
148                Ok((
149                    Some(self),
150                    input.produce_items(
151                        cursor
152                            .hit()
153                            .block
154                            .clone()
155                            .unspecialize()
156                            .into_iter()
157                            .map(Tool::InfiniteBlocks),
158                    )?,
159                ))
160            }
161            Self::EditBlock => {
162                // TODO: this should probably be a utility on Block itself
163                fn find_space(
164                    read_ticket: ReadTicket<'_>,
165                    block: &Block,
166                ) -> Result<Option<Handle<Space>>, HandleError> {
167                    match block.primitive() {
168                        Primitive::Indirect(handle) => {
169                            find_space(read_ticket, handle.read(read_ticket)?.block())
170                        }
171                        Primitive::Recur { space, .. } => Ok(Some(space.clone())),
172                        Primitive::Atom(_)
173                        | Primitive::Air
174                        | Primitive::Text { .. }
175                        | Primitive::Raw { .. } => Ok(None),
176                    }
177                }
178                match find_space(input.read_ticket, &input.cursor()?.hit().block) {
179                    // TODO: Actually implement the tool.
180                    Ok(Some(_space_handle)) => {
181                        Err(ToolError::Internal("EditBlock not implemented".to_string()))
182                    }
183                    Ok(None) => Err(ToolError::NotUsable),
184                    // TODO: slightly wrong meaning of error variant
185                    Err(handle_err) => Err(ToolError::SpaceHandle(handle_err)),
186                }
187            }
188            Self::PushPull => {
189                // TODO: this tool is just a demonstration piece, and so we should
190                // make it possible to express as just a `Tool::Custom`.
191
192                let cursor = input.cursor()?;
193                let direction: Face6 = cursor
194                    .face_selected()
195                    .opposite()
196                    .try_into()
197                    .map_err(|_| ToolError::NotUsable)?;
198
199                // TODO: Tool should have user-controllable modes for push vs. pull when the
200                // choice is free
201
202                let velocity = 8;
203                let op = Operation::Alt(
204                    [
205                        Operation::StartMove(block::Move::new(direction, 0, velocity)),
206                        Operation::StartMove(block::Move::new(direction.opposite(), 0, velocity)),
207                    ]
208                    .into(),
209                );
210
211                Ok((
212                    Some(self),
213                    input.apply_operation(&op, GridRotation::IDENTITY, false)?,
214                ))
215            }
216            Self::Jetpack { active } => Ok((
217                Some(Self::Jetpack { active: !active }),
218                UniverseTransaction::default(),
219            )),
220            Self::Custom { ref op, icon: _ } => Ok((
221                Some(self.clone()),
222                input.apply_operation(op, GridRotation::IDENTITY, false)?,
223            )),
224        }
225    }
226
227    /// As [`Self::use_tool`], except that it does not allow the tool to modify itself.
228    ///
229    /// This operation is used for special cases where an action is expressed by a tool
230    /// but the tool is not a “game item”.
231    pub fn use_immutable_tool(
232        &self,
233        input: &ToolInput<'_>,
234    ) -> Result<UniverseTransaction, ToolError> {
235        let (new_tool, transaction) = self.clone().use_tool(input)?;
236
237        if new_tool.as_ref() != Some(self) {
238            // TODO: Define a separate error for this to report.
239            return Err(ToolError::Internal(String::from("tool is immutable")));
240        }
241
242        Ok(transaction)
243    }
244
245    /// Return a block to use as an icon for this tool. For tools that place blocks, has the
246    /// same appearance as the block to be placed. The display name of the block should be
247    /// the display name of the tool.
248    ///
249    /// If the returned block is not from `predefined`, then it will be
250    /// [quoted][crate::block::Modifier::Quote] to ensure it has no unwanted side effects.
251    ///
252    /// TODO (API instability): Eventually we will probably want additional decorations
253    /// that probably should not need to be painted into the block itself.
254    pub fn icon<'a>(&'a self, predefined: &'a BlockProvider<Icons>) -> Cow<'a, Block> {
255        match self {
256            Self::Activate => Cow::Borrowed(&predefined[Icons::Activate]),
257            // TODO: Give Remove different icons
258            Self::RemoveBlock { keep: _ } => Cow::Borrowed(&predefined[Icons::Delete]),
259            // TODO: InfiniteBlocks should have a different name and appearance
260            // (or maybe that distinction should appear in the quantity-text field)
261            Self::Block(block) | Self::InfiniteBlocks(block) => {
262                Cow::Owned(block.clone().with_modifier(block::Quote::default()))
263            }
264            Self::CopyFromSpace => Cow::Borrowed(&predefined[Icons::CopyFromSpace]),
265            Self::EditBlock => Cow::Borrowed(&predefined[Icons::EditBlock]),
266            Self::PushPull => Cow::Borrowed(&predefined[Icons::PushPull]),
267            Self::Jetpack { active } => {
268                Cow::Borrowed(&predefined[Icons::Jetpack { active: *active }])
269            }
270            // We don’t trust the provided icon not to cause trouble, so it is quoted even
271            // though it is meant to be an icon.
272            Self::Custom { icon, op: _ } => {
273                Cow::Owned(icon.clone().with_modifier(block::Quote::default()))
274            }
275        }
276    }
277
278    /// Kludge restricted version of `icon()` to get inventory-in-a-block rendering working at all.
279    /// TODO(inventory): <https://github.com/kpreid/all-is-cubes/issues/480>
280    pub(crate) fn icon_only_if_intrinsic(&self) -> Option<&Block> {
281        match self {
282            Tool::Activate => None,
283            Tool::RemoveBlock { .. } => None,
284            Tool::Block(block) => Some(block),
285            Tool::InfiniteBlocks(block) => Some(block),
286            Tool::CopyFromSpace => None,
287            Tool::EditBlock => None,
288            Tool::PushPull => None,
289            Tool::Jetpack { .. } => None,
290            Tool::Custom { op: _, icon } => Some(icon),
291        }
292    }
293
294    /// Specifies a limit on the number of this item that should be combined in a single
295    /// [`Slot`].
296    pub(crate) fn stack_limit(&self) -> StackLimit {
297        use StackLimit::{One, Standard};
298        match self {
299            Tool::Activate => One,
300            Tool::RemoveBlock { .. } => One,
301            Tool::Block(_) => Standard,
302            Tool::InfiniteBlocks(_) => One,
303            Tool::CopyFromSpace => One,
304            Tool::EditBlock => One,
305            Tool::PushPull => One,
306            Tool::Jetpack { .. } => One,
307            Tool::Custom { .. } => One, // TODO: let tool specify
308        }
309    }
310}
311
312impl VisitHandles for Tool {
313    fn visit_handles(&self, visitor: &mut dyn HandleVisitor) {
314        match self {
315            Tool::Activate => {}
316            Tool::RemoveBlock { .. } => {}
317            Tool::Block(block) => block.visit_handles(visitor),
318            Tool::InfiniteBlocks(block) => block.visit_handles(visitor),
319            Tool::CopyFromSpace => {}
320            Tool::EditBlock => {}
321            Tool::PushPull => {}
322            Tool::Jetpack { active: _ } => {}
323            Tool::Custom { op, icon } => {
324                op.visit_handles(visitor);
325                icon.visit_handles(visitor);
326            }
327        }
328    }
329}
330
331/// Resources available to a `Tool` to perform its function.
332///
333/// This is intended to provide future extensibility compared to having a complex
334/// parameter list for `Tool::use_tool`.
335#[derive(Debug)]
336#[expect(clippy::exhaustive_structs, reason = "TODO: should be non_exhaustive")]
337pub struct ToolInput<'ticket> {
338    /// Access to the universe being operated on.
339    pub read_ticket: ReadTicket<'ticket>,
340
341    /// Cursor identifying block(s) to act on. If [`None`] then the tool was used while
342    /// pointing at nothing or by an agent without an ability to aim.
343    pub cursor: Option<Cursor>,
344
345    /// Character that is using the tool.
346    ///
347    /// TODO: We want to be able to express “inventory host”, not just specifically Character (but there aren't any other examples).
348    pub character: Option<Handle<Character>>,
349}
350
351#[allow(clippy::elidable_lifetime_names)]
352impl<'ticket> ToolInput<'ticket> {
353    /// Generic handler for a tool that replaces one cube.
354    ///
355    /// TODO: This should probably be replaced with a `Transaction` whose failure
356    /// is translated into the `ToolError`, since this code is basically doing
357    /// `SpaceTransaction::check` anyway.
358    fn set_cube(
359        &self,
360        cube: Cube,
361        old_block: Block,
362        new_block: Block,
363    ) -> Result<UniverseTransaction, ToolError> {
364        let space_handle = self.cursor()?.space();
365        let space = space_handle.read(self.read_ticket).map_err(ToolError::SpaceHandle)?;
366        if space[cube] != old_block {
367            return Err(ToolError::Obstacle);
368        }
369
370        Ok(
371            SpaceTransaction::set_cube(cube, Some(old_block), Some(new_block))
372                .bind(space_handle.clone()),
373        )
374    }
375
376    /// As [`Self::set_cube`] but also applying rotation (or other transformations
377    /// in the future) specified by the block's attributes
378    fn place_block(
379        &self,
380        cursor: &Cursor,
381        old_block: Block,
382        mut new_block: Block,
383    ) -> Result<UniverseTransaction, ToolError> {
384        // TODO: better error typing here
385        let new_ev = new_block
386            .evaluate(self.read_ticket)
387            .map_err(|e| ToolError::Internal(e.to_string()))?;
388
389        let rotation = match new_ev.attributes().rotation_rule {
390            RotationPlacementRule::Never => GridRotation::IDENTITY,
391            RotationPlacementRule::Attach { by: attached_face } => {
392                let world_cube_face: Face6 =
393                    cursor.face_selected().opposite().try_into().unwrap_or(Face6::NZ);
394                // TODO: RotationPlacementRule should control the "up" axis choices
395                GridRotation::from_to(attached_face, world_cube_face, Face6::PY)
396                    .or_else(|| GridRotation::from_to(attached_face, world_cube_face, Face6::PX))
397                    .or_else(|| GridRotation::from_to(attached_face, world_cube_face, Face6::PZ))
398                    .unwrap_or(GridRotation::IDENTITY)
399            }
400        };
401
402        if let Some(ref action) = new_ev.attributes().placement_action {
403            let &block::PlacementAction {
404                ref operation,
405                in_front,
406            } = action;
407            self.apply_operation(operation, rotation, in_front)
408        } else {
409            new_block = new_block.rotate(rotation);
410
411            // TODO: there should probably be one canonical implementation of "add the inventory
412            // modifier" between this and `EvaluatedBlock::with_inventory()`.
413            let inventory_size = new_ev.attributes().inventory.size;
414            if inventory_size > 0 {
415                new_block = new_block.with_modifier(inv::Inventory::new(inventory_size));
416            }
417
418            let affected_cube = cursor.cube() + cursor.face_selected().normal_vector();
419
420            let mut txn = self.set_cube(affected_cube, old_block, new_block)?;
421
422            // Add fluff. TODO: This should probably be part of set_cube()?
423            txn.merge_from(
424                CubeTransaction::fluff(Fluff::PlaceBlockGeneric)
425                    .at(affected_cube)
426                    .bind(self.cursor()?.space().clone()),
427            )
428            .expect("fluff never fails to merge");
429
430            Ok(txn)
431        }
432    }
433
434    /// Returns a [`Cursor`] indicating what blocks the tool should act on, if it is
435    /// a sort of tool that acts on blocks. If there is no [`Cursor`], because of aim
436    /// or because of being used in a context where there cannot be any aiming, returns
437    /// [`Err(ToolError::NothingSelected)`](ToolError::NothingSelected) for convenient
438    /// propagation.
439    pub fn cursor(&self) -> Result<&Cursor, ToolError> {
440        self.cursor.as_ref().ok_or(ToolError::NothingSelected)
441    }
442
443    /// Add the provided items to the inventory from which the tool was used.
444    pub fn produce_items<S: Into<inv::Slot>, I: IntoIterator<Item = S>>(
445        &self,
446        items: I,
447    ) -> Result<UniverseTransaction, ToolError> {
448        if let Some(ref character) = self.character {
449            // TODO: pre-check whether there's enough inventory space to allow alternative
450            // handling
451            Ok(
452                CharacterTransaction::inventory(InventoryTransaction::insert(items))
453                    .bind(character.clone()),
454            )
455        } else {
456            // TODO: Specific error
457            Err(ToolError::NotUsable)
458        }
459    }
460
461    pub(crate) fn apply_operation(
462        &self,
463        op: &Operation,
464        rotation: GridRotation,
465        in_front: bool,
466    ) -> Result<UniverseTransaction, ToolError> {
467        // TODO: This is a mess; figure out how much impedance-mismatch we want to fix here.
468
469        let cursor = self.cursor()?; // TODO: allow op to not be spatial, i.e. not always fail if this returns None?
470        let character: Option<character::Read<'_>> =
471            self.character.as_ref().map(|c| c.read(self.read_ticket)).transpose()?;
472
473        let cube = if in_front {
474            cursor.preceding_cube()
475        } else {
476            cursor.cube()
477        };
478
479        let (space_txn, inventory_txn) = op.apply(
480            &cursor.space().read(self.read_ticket)?,
481            character.map(|c| c.inventory()),
482            Gridgid::from_translation(cube.lower_bounds().to_vector())
483                * rotation.to_positive_octant_transform(1),
484        )?;
485        let mut txn = space_txn.bind(cursor.space().clone());
486        if inventory_txn != InventoryTransaction::default() {
487            txn.merge_from(CharacterTransaction::inventory(inventory_txn).bind(
488                self.character.clone().ok_or_else(|| {
489                    ToolError::Internal(format!(
490                        "operation produced inventory transaction \
491                                    without being given an inventory: {op:?}"
492                    ))
493                })?,
494            ))
495            .unwrap();
496        }
497        Ok(txn)
498    }
499}
500
501/// Ways that a tool can fail.
502#[derive(Clone, Debug, Eq, Hash, PartialEq, displaydoc::Display)]
503#[non_exhaustive]
504pub enum ToolError {
505    // TODO: Add tests for these error messages and make them make good sense in contexts
506    // they might appear ... or possibly we have a separate trait for them
507    /// There was no tool to use (empty inventory slot, nonexistent slot, nonexistent inventory…).
508    #[displaydoc("no tool")]
509    NoTool,
510    /// The tool cannot currently be used or does not apply to the target.
511    #[displaydoc("does not apply")]
512    NotUsable,
513    /// Cannot place a block or similar because there's a block occupying the space.
514    #[displaydoc("there's something in the way")]
515    Obstacle,
516    /// The tool requires a target cube and none was present.
517    #[displaydoc("nothing is selected")]
518    NothingSelected,
519    /// The space to be operated on could not be accessed.
520    #[displaydoc("error accessing space: {0}")]
521    SpaceHandle(HandleError),
522    /// An error occurred while executing the effects of the tool.
523    /// TODO: Improve this along with [`Transaction`] error types.
524    #[displaydoc("unexpected error: {0}")]
525    Internal(String),
526}
527
528impl core::error::Error for ToolError {
529    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
530        match self {
531            ToolError::NoTool => None,
532            ToolError::NotUsable => None,
533            ToolError::Obstacle => None,
534            ToolError::NothingSelected => None,
535            ToolError::SpaceHandle(e) => Some(e),
536            ToolError::Internal(_) => None,
537        }
538    }
539}
540
541impl ToolError {
542    /// Return [`Fluff`] to accompany this error.
543    ///
544    /// TODO: This should have spatial information (located at the cursor target or the
545    /// character's "hand" or other).
546    #[allow(clippy::unused_self, reason = "will be used in the future")]
547    pub fn fluff(&self) -> impl Iterator<Item = Fluff> + use<> {
548        core::iter::once(Fluff::Beep)
549    }
550}
551
552impl From<op::OperationError> for ToolError {
553    fn from(value: op::OperationError) -> Self {
554        match value {
555            // TODO: should not forget source()s
556            op::OperationError::InternalConflict(c) => ToolError::Internal(c.to_string()),
557            op::OperationError::Unmatching
558            | op::OperationError::BlockInventoryFull { .. }
559            | op::OperationError::CharacterInventoryFull => ToolError::NotUsable,
560            op::OperationError::OutOfBounds { .. } => ToolError::Obstacle,
561        }
562    }
563}
564
565impl From<HandleError> for ToolError {
566    fn from(value: HandleError) -> Self {
567        ToolError::SpaceHandle(value)
568    }
569}
570
571/// A wrapper around a value which cannot be printed or serialized,
572/// used primarily to allow external functions to be called from objects
573/// within a [`Universe`](crate::universe::Universe).
574///
575/// TODO: relocate this type once we figure out where it belongs.
576/// TODO: Probably they should be their own kind of `UniverseMember`, so that they can
577/// be reattached in the future.
578pub struct EphemeralOpaque<T: ?Sized>(pub(crate) Option<Arc<T>>);
579
580impl<T: ?Sized> EphemeralOpaque<T> {
581    /// Constructs an [`EphemeralOpaque`] that holds the given value.
582    pub fn new(contents: Arc<T>) -> Self {
583        Self(Some(contents))
584    }
585
586    /// Constructs an [`EphemeralOpaque`] that is already defunct
587    /// (holds no value).
588    pub fn defunct() -> Self {
589        Self(None)
590    }
591
592    /// Get a reference to the value if it still exists.
593    pub fn try_ref(&self) -> Option<&T> {
594        self.0.as_deref()
595    }
596}
597
598impl<T: ?Sized> From<Arc<T>> for EphemeralOpaque<T> {
599    fn from(contents: Arc<T>) -> Self {
600        Self(Some(contents))
601    }
602}
603
604impl<T: ?Sized> fmt::Debug for EphemeralOpaque<T> {
605    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
606        write!(f, "EphemeralOpaque(..)")
607    }
608}
609impl<T: ?Sized> PartialEq for EphemeralOpaque<T> {
610    fn eq(&self, other: &Self) -> bool {
611        self.0.as_ref().map(Arc::as_ptr) == other.0.as_ref().map(Arc::as_ptr)
612        //self.0.as_ref().zip(other.0.as_ref())
613        //Arc::ptr_eq(&self.0, &other.0)
614    }
615}
616impl<T: ?Sized> Eq for EphemeralOpaque<T> {}
617impl<T: ?Sized> Clone for EphemeralOpaque<T> {
618    fn clone(&self) -> Self {
619        Self(self.0.clone())
620    }
621}
622impl<T: ?Sized> hash::Hash for EphemeralOpaque<T> {
623    fn hash<H: hash::Hasher>(&self, state: &mut H) {
624        self.0.as_ref().map(Arc::as_ptr).hash(state)
625    }
626}
627
628impl<T: ?Sized> VisitHandles for EphemeralOpaque<T> {
629    fn visit_handles(&self, _: &mut dyn HandleVisitor) {
630        // Being opaque, an `EphemeralOpaque` doesn’t count as containing any handles.
631        // In the future, we might replace it with something that *does* constitute a handle
632        // to a special “external connection” entity, and if we do that, this will change.
633    }
634}
635
636#[cfg(feature = "arbitrary")]
637#[mutants::skip]
638impl<'a, T: arbitrary::Arbitrary<'a>> arbitrary::Arbitrary<'a> for EphemeralOpaque<T> {
639    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
640        Ok(EphemeralOpaque(if u.arbitrary()? {
641            Some(Arc::new(u.arbitrary()?))
642        } else {
643            None
644        }))
645    }
646
647    fn size_hint(depth: usize) -> (usize, Option<usize>) {
648        Self::try_size_hint(depth).unwrap_or_default()
649    }
650    fn try_size_hint(
651        depth: usize,
652    ) -> Result<(usize, Option<usize>), arbitrary::MaxRecursionReached> {
653        use arbitrary::{Arbitrary, size_hint};
654        size_hint::try_recursion_guard(depth, |depth| {
655            Ok(size_hint::and(
656                <bool as Arbitrary>::size_hint(depth),
657                <T as Arbitrary>::try_size_hint(depth)?,
658            ))
659        })
660    }
661}
662
663#[cfg(test)]
664mod tests {
665    use super::*;
666    use crate::block::Resolution::*;
667    use crate::character::cursor_raycast;
668    use crate::content::{make_some_blocks, make_some_voxel_blocks};
669    use crate::inv::Slot;
670    use crate::math::{FreeCoordinate, rgba_const};
671    use crate::raycast::Ray;
672    use crate::raytracer::print_space;
673    use crate::universe::Universe;
674    use crate::util::yield_progress_for_testing;
675    use crate::{space, transaction};
676    use all_is_cubes_base::math::Rgba;
677    use alloc::boxed::Box;
678    use arcstr::literal;
679    use pretty_assertions::assert_eq;
680    use rstest::rstest;
681
682    #[derive(Debug)]
683    struct ToolTester {
684        universe: Box<Universe>,
685        character_handle: Handle<Character>,
686        space_handle: Handle<Space>,
687    }
688    impl ToolTester {
689        /// The provided function should modify the space to contain the blocks to operate on,
690        /// given a cursor ray along the line of cubes from the origin in the +X direction.
691        fn new<F: FnOnce(&mut space::Mutation<'_, '_>)>(f: F) -> Self {
692            let mut universe = Universe::new();
693            let mut space = Space::empty_positive(6, 4, 4);
694            space.mutate(universe.read_ticket(), f);
695            let space_handle = universe.insert("ToolTester/space".into(), space).unwrap();
696            let read_ticket = universe.read_ticket();
697
698            Self {
699                character_handle: universe
700                    .insert(
701                        "ToolTester/character".into(),
702                        Character::spawn_default(read_ticket, space_handle.clone()).unwrap(),
703                    )
704                    .unwrap(),
705                space_handle,
706                universe,
707            }
708        }
709
710        fn input(&self) -> ToolInput<'_> {
711            ToolInput {
712                // TODO: define ToolInput::new
713                read_ticket: self.universe.read_ticket(),
714                cursor: cursor_raycast(
715                    self.universe.read_ticket(),
716                    Ray::new([0., 0.5, 0.5], [1., 0., 0.]),
717                    &self.space_handle,
718                    FreeCoordinate::INFINITY,
719                )
720                .unwrap(),
721                character: Some(self.character_handle.clone()),
722            }
723        }
724
725        fn equip_and_use_tool(
726            &mut self,
727            stack: impl Into<Slot>,
728        ) -> Result<UniverseTransaction, ToolError> {
729            // Put the tool in inventory.
730            let index = 0;
731            let insert_txn = CharacterTransaction::inventory(InventoryTransaction::replace(
732                index,
733                self.character().inventory().slots[usize::from(index)].clone(),
734                stack.into(),
735            ));
736            self.universe.execute_1(&self.character_handle, insert_txn).unwrap();
737
738            // Invoke Inventory::use_tool, which knows how to assemble the answer into a
739            // single transaction.
740            let input = self.input();
741            self.character().inventory().use_tool(
742                self.universe.read_ticket(),
743                input.cursor().ok(),
744                self.character_handle.clone(),
745                index,
746            )
747        }
748
749        /// As `equip_and_use_tool`, but also commit the transaction.
750        fn equip_use_commit(&mut self, stack: impl Into<Slot>) -> Result<(), EucError> {
751            let transaction = self.equip_and_use_tool(stack).map_err(EucError::Use)?;
752            transaction
753                .execute(&mut self.universe, (), &mut transaction::no_outputs)
754                .map_err(EucError::Commit)?;
755            Ok(())
756        }
757
758        fn space(&self) -> space::Read<'_> {
759            self.space_handle.read(self.universe.read_ticket()).unwrap()
760        }
761        fn space_handle(&self) -> &Handle<Space> {
762            &self.space_handle
763        }
764        fn character(&self) -> character::Read<'_> {
765            self.character_handle.read(self.universe.read_ticket()).unwrap()
766        }
767    }
768
769    #[derive(Clone, Debug)]
770    #[expect(dead_code, reason = "fields only used in Debug if an expect() fails")]
771    enum EucError {
772        Use(ToolError),
773        Commit(transaction::ExecuteError<UniverseTransaction>),
774    }
775
776    async fn dummy_icons() -> BlockProvider<Icons> {
777        // TODO: Might be good to generate differently labeled blocks... maybe BlockProvider should have a way to do that for any enum.
778        let [block] = make_some_blocks();
779        BlockProvider::new(yield_progress_for_testing(), |_| Ok(block.clone()))
780            .await
781            .unwrap()
782    }
783
784    #[macro_rules_attribute::apply(smol_macros::test)]
785    async fn icon_activate() {
786        let dummy_icons = dummy_icons().await;
787        assert_eq!(
788            &*Tool::Activate.icon(&dummy_icons),
789            &dummy_icons[Icons::Activate]
790        );
791    }
792
793    #[test]
794    fn use_activate_on_behavior() {
795        let [existing] = make_some_blocks();
796        let mut tester = ToolTester::new(|m| {
797            m.set([1, 0, 0], &existing).unwrap();
798        });
799        assert_eq!(
800            tester.equip_and_use_tool(Tool::Activate),
801            Ok(CubeTransaction::ACTIVATE_BEHAVIOR
802                .at(Cube::new(1, 0, 0))
803                .bind(tester.space_handle.clone()))
804        );
805
806        // Tool::Activate on a behavior currently has no cases where it fails
807        // (unless the transaction fails), so there are no tests for that.
808    }
809
810    #[test]
811    fn use_activate_on_block_action() {
812        let after = Block::builder().color(Rgba::WHITE).display_name("after").build();
813        let before = Block::builder()
814            .color(Rgba::WHITE)
815            .display_name("before")
816            .activation_action(Operation::Become(after.clone()))
817            .build();
818        let mut tester = ToolTester::new(|m| {
819            m.set([1, 0, 0], &before).unwrap();
820        });
821
822        assert_eq!(
823            tester.equip_and_use_tool(Tool::Activate),
824            Ok(CubeTransaction::replacing(Some(before), Some(after))
825                .at(Cube::new(1, 0, 0))
826                .bind(tester.space_handle.clone()))
827        );
828
829        // TODO: Should have another test with a failing `Operation`, but we can't set that up yet.
830    }
831
832    #[macro_rules_attribute::apply(smol_macros::test)]
833    async fn icon_remove_block() {
834        let dummy_icons = dummy_icons().await;
835        assert_eq!(
836            &*Tool::RemoveBlock { keep: true }.icon(&dummy_icons),
837            &dummy_icons[Icons::Delete]
838        );
839    }
840
841    #[rstest]
842    fn use_remove_block(#[values(false, true)] keep: bool) {
843        let [existing] = make_some_blocks();
844        let mut tester = ToolTester::new(|m| {
845            m.set([1, 0, 0], &existing).unwrap();
846        });
847        let actual_transaction = tester.equip_and_use_tool(Tool::RemoveBlock { keep }).unwrap();
848
849        let mut expected_delete =
850            SpaceTransaction::set_cube([1, 0, 0], Some(existing.clone()), Some(AIR))
851                .bind(tester.space_handle.clone());
852        if keep {
853            expected_delete
854                .merge_from(
855                    CharacterTransaction::inventory(InventoryTransaction::insert([Tool::Block(
856                        existing,
857                    )]))
858                    .bind(tester.character_handle.clone()),
859                )
860                .unwrap();
861        }
862        assert_eq!(actual_transaction, expected_delete);
863
864        actual_transaction.execute(&mut tester.universe, (), &mut drop).unwrap();
865        print_space(&tester.space(), [-1., 1., 1.]);
866        assert_eq!(&tester.space()[[1, 0, 0]], &AIR);
867    }
868
869    #[test]
870    fn use_remove_block_without_target() {
871        let mut tester = ToolTester::new(|_| {});
872        assert_eq!(
873            tester.equip_and_use_tool(Tool::RemoveBlock { keep: true }),
874            Err(ToolError::NothingSelected)
875        );
876    }
877
878    #[macro_rules_attribute::apply(smol_macros::test)]
879    async fn icon_place_block() {
880        let dummy_icons = dummy_icons().await;
881        let [block] = make_some_blocks();
882        assert_eq!(
883            *Tool::InfiniteBlocks(block.clone()).icon(&dummy_icons),
884            block.with_modifier(block::Quote {
885                suppress_ambient: false
886            }),
887        );
888    }
889
890    #[rstest]
891    fn use_block(#[values(Tool::Block, Tool::InfiniteBlocks)] tool_ctor: fn(Block) -> Tool) {
892        let [existing, tool_block] = make_some_blocks();
893        let tool = tool_ctor(tool_block.clone());
894        let expect_consume = matches!(tool, Tool::Block(_));
895
896        let mut tester = ToolTester::new(|m| {
897            m.set([1, 0, 0], &existing).unwrap();
898        });
899        let transaction = tester.equip_and_use_tool(tool.clone()).unwrap();
900
901        let mut expected_cube_transaction =
902            SpaceTransaction::set_cube(Cube::ORIGIN, Some(AIR), Some(tool_block.clone()));
903        expected_cube_transaction.at(Cube::ORIGIN).add_fluff(Fluff::PlaceBlockGeneric);
904        let mut expected_cube_transaction =
905            expected_cube_transaction.bind(tester.space_handle.clone());
906        if expect_consume {
907            expected_cube_transaction
908                .merge_from(
909                    CharacterTransaction::inventory(InventoryTransaction::replace(
910                        0,
911                        Slot::from(tool.clone()),
912                        Slot::Empty,
913                    ))
914                    .bind(tester.character_handle.clone()),
915                )
916                .unwrap();
917        }
918        assert_eq!(transaction, expected_cube_transaction);
919
920        transaction.execute(&mut tester.universe, (), &mut drop).unwrap();
921        print_space(&tester.space(), [-1., 1., 1.]);
922        assert_eq!(&tester.space()[[1, 0, 0]], &existing);
923        assert_eq!(&tester.space()[[0, 0, 0]], &tool_block);
924    }
925
926    /// TODO: Expand this test to exhaustively test all rotation placement rules?
927    #[test]
928    fn use_block_automatic_rotation() {
929        let [existing] = make_some_blocks();
930        let mut tester = ToolTester::new(|m| {
931            m.set([1, 0, 0], &existing).unwrap();
932        });
933
934        // Make a block with a rotation rule
935        let [mut tool_block] = make_some_voxel_blocks(&mut tester.universe);
936        tool_block.modifiers_mut().push(block::Modifier::from(block::BlockAttributes {
937            rotation_rule: RotationPlacementRule::Attach { by: Face6::NZ },
938            ..block::BlockAttributes::default()
939        }));
940
941        // TODO: For more thorough testing, we need to be able to control ToolTester's choice of ray
942        let transaction =
943            tester.equip_and_use_tool(Tool::InfiniteBlocks(tool_block.clone())).unwrap();
944        assert_eq!(
945            transaction,
946            {
947                let mut t = SpaceTransaction::set_cube(
948                    Cube::ORIGIN,
949                    Some(AIR),
950                    Some(tool_block.clone().rotate(Face6::PY.clockwise())),
951                );
952                t.at(Cube::ORIGIN).add_fluff(Fluff::PlaceBlockGeneric);
953                t
954            }
955            .bind(tester.space_handle.clone())
956        );
957    }
958
959    #[test]
960    fn use_block_with_inventory_config() {
961        let [existing] = make_some_blocks();
962        let mut tester = ToolTester::new(|m| {
963            m.set([1, 0, 0], &existing).unwrap();
964        });
965
966        // Make a block with an inventory config
967        let tool_block = Block::builder()
968            .color(Rgba::WHITE)
969            .inventory_config(inv::InvInBlock::new(10, R4, R16, []))
970            .build();
971
972        let transaction =
973            tester.equip_and_use_tool(Tool::InfiniteBlocks(tool_block.clone())).unwrap();
974        assert_eq!(
975            transaction,
976            {
977                let mut t = SpaceTransaction::set_cube(
978                    Cube::ORIGIN,
979                    Some(AIR),
980                    Some(tool_block.with_modifier(inv::Inventory::new(10))),
981                );
982                t.at(Cube::ORIGIN).add_fluff(Fluff::PlaceBlockGeneric);
983                t
984            }
985            .bind(tester.space_handle.clone())
986        );
987    }
988
989    /// If a block has a `placement_action`, then that action is performed instead of the
990    /// normal placement. TODO: how this interacts with consumption is not yet worked out.
991    #[rstest]
992    fn use_block_which_has_placement_action(
993        #[values(Tool::Block, Tool::InfiniteBlocks)] tool_ctor: fn(Block) -> Tool,
994        #[values(false, true)] in_front: bool,
995    ) {
996        let [existing_target] = make_some_blocks();
997        let modifier_to_add: block::Modifier = block::BlockAttributes {
998            display_name: literal!("modifier_to_add"),
999            ..Default::default()
1000        }
1001        .into();
1002        let existing_affected_block = if in_front {
1003            AIR
1004        } else {
1005            existing_target.clone()
1006        };
1007        let tool_block = Block::builder()
1008            .color(rgba_const!(1.0, 0.0, 0.0, 0.0))
1009            .display_name("tool_block")
1010            .placement_action(block::PlacementAction {
1011                operation: Operation::AddModifiers([modifier_to_add.clone()].into()),
1012                in_front,
1013            })
1014            .build();
1015        let tool = tool_ctor(tool_block.clone());
1016        let expect_consume = matches!(tool, Tool::Block(_));
1017        let expected_result_block =
1018            existing_affected_block.clone().with_modifier(modifier_to_add.clone());
1019
1020        dbg!(&tool);
1021        let mut tester = ToolTester::new(|m| {
1022            m.set([1, 0, 0], &existing_target).unwrap();
1023        });
1024        let transaction = tester.equip_and_use_tool(tool.clone()).unwrap();
1025
1026        let expected_cube_transaction = SpaceTransaction::set_cube(
1027            if in_front {
1028                Cube::ORIGIN
1029            } else {
1030                Cube::new(1, 0, 0)
1031            },
1032            Some(existing_affected_block.clone()),
1033            Some(expected_result_block.clone()),
1034        );
1035        // note there is *not* a place-block fluff -- the operation should do that if
1036        // applicable
1037        let mut expected_cube_transaction =
1038            expected_cube_transaction.bind(tester.space_handle.clone());
1039        if expect_consume {
1040            expected_cube_transaction
1041                .merge_from(
1042                    CharacterTransaction::inventory(InventoryTransaction::replace(
1043                        0,
1044                        Slot::from(tool.clone()),
1045                        Slot::Empty,
1046                    ))
1047                    .bind(tester.character_handle.clone()),
1048                )
1049                .unwrap();
1050        }
1051        assert_eq!(
1052            transaction, expected_cube_transaction,
1053            "actual transaction ≠ expected transaction"
1054        );
1055
1056        transaction.execute(&mut tester.universe, (), &mut drop).unwrap();
1057        print_space(&tester.space(), [-1., 1., 1.]);
1058        assert_eq!(
1059            (&tester.space()[[1, 0, 0]], &tester.space()[[0, 0, 0]]),
1060            if in_front {
1061                (&existing_target, &expected_result_block)
1062            } else {
1063                (&expected_result_block, &AIR)
1064            },
1065            "actual space state ≠ expected space state"
1066        );
1067    }
1068
1069    /// Note: This is more of a test of [`Inventory`] and [`Slot`] stack management
1070    /// than the tool.
1071    #[test]
1072    fn use_block_stack_decrements() {
1073        let [existing, tool_block] = make_some_blocks();
1074        let stack_2 = Slot::stack(2, Tool::Block(tool_block.clone()));
1075        let stack_1 = Slot::stack(1, Tool::Block(tool_block));
1076
1077        let mut tester = ToolTester::new(|m| {
1078            // This must be far enough along +X for the blocks we're placing to not run out of space.
1079            m.set([4, 0, 0], &existing).unwrap();
1080        });
1081        tester.equip_use_commit(stack_2).expect("tool failure 1");
1082        assert_eq!(tester.character().inventory().slots[0], stack_1);
1083        tester.equip_use_commit(stack_1).expect("tool failure 2");
1084        assert_eq!(tester.character().inventory().slots[0], Slot::Empty);
1085    }
1086
1087    #[rstest]
1088    fn use_block_with_obstacle(
1089        #[values(Tool::Block, Tool::InfiniteBlocks)] tool_ctor: fn(Block) -> Tool,
1090    ) {
1091        let [existing, tool_block, obstacle] = make_some_blocks();
1092        let tool = tool_ctor(tool_block);
1093        let mut tester = ToolTester::new(|m| {
1094            m.set([1, 0, 0], &existing).unwrap();
1095        });
1096        // Place the obstacle after the raycast
1097        let space_handle = tester.space_handle().clone();
1098        tester
1099            .universe
1100            .execute_1(
1101                &space_handle,
1102                SpaceTransaction::set_cube([0, 0, 0], None, Some(obstacle.clone())),
1103            )
1104            .unwrap();
1105        assert_eq!(tester.equip_and_use_tool(tool), Err(ToolError::Obstacle));
1106        print_space(&tester.space(), [-1., 1., 1.]);
1107        assert_eq!(&tester.space()[[1, 0, 0]], &existing);
1108        assert_eq!(&tester.space()[[0, 0, 0]], &obstacle);
1109    }
1110
1111    #[rstest]
1112    fn use_block_without_target(
1113        #[values(Tool::Block, Tool::InfiniteBlocks)] tool_ctor: fn(Block) -> Tool,
1114    ) {
1115        let [tool_block] = make_some_blocks();
1116        let tool = tool_ctor(tool_block);
1117        let mut tester = ToolTester::new(|_| {});
1118        assert_eq!(
1119            tester.equip_and_use_tool(tool),
1120            Err(ToolError::NothingSelected)
1121        );
1122    }
1123
1124    #[test]
1125    fn use_copy_from_space() {
1126        let [existing] = make_some_blocks();
1127        let mut tester = ToolTester::new(|m| {
1128            m.set([1, 0, 0], &existing).unwrap();
1129        });
1130        let transaction = tester.equip_and_use_tool(Tool::CopyFromSpace).unwrap();
1131        assert_eq!(
1132            transaction,
1133            CharacterTransaction::inventory(InventoryTransaction::insert([Tool::InfiniteBlocks(
1134                existing.clone()
1135            )]))
1136            .bind(tester.character_handle.clone())
1137        );
1138        transaction.execute(&mut tester.universe, (), &mut drop).unwrap();
1139        // Space is unmodified
1140        assert_eq!(&tester.space()[[1, 0, 0]], &existing);
1141    }
1142
1143    #[test]
1144    fn use_custom_success() {
1145        // TODO: also test an operation that cares about the existing block
1146
1147        let [existing, icon, placed] = make_some_blocks();
1148        let tool = Tool::Custom {
1149            op: Operation::Become(placed.clone()),
1150            icon,
1151        };
1152        let mut tester = ToolTester::new(|m| {
1153            m.set([0, 0, 0], &existing).unwrap();
1154        });
1155
1156        let transaction = tester.equip_and_use_tool(tool).unwrap();
1157
1158        assert_eq!(
1159            transaction,
1160            SpaceTransaction::set_cube(
1161                [0, 0, 0],
1162                Some(existing),
1163                Some(placed.rotate(Face6::PY.clockwise())),
1164            )
1165            .bind(tester.space_handle)
1166        );
1167    }
1168}