1use 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#[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 Activate,
39
40 RemoveBlock {
42 keep: bool,
44 },
45
46 Block(Block),
49
50 InfiniteBlocks(Block),
52
53 CopyFromSpace,
55
56 EditBlock,
60
61 PushPull,
63
64 Jetpack {
69 active: bool,
71 },
72
73 Custom {
78 op: Operation,
80 icon: Block,
82 },
83}
84
85impl Tool {
86 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 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 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 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 Ok(Some(_space_handle)) => {
181 Err(ToolError::Internal("EditBlock not implemented".to_string()))
182 }
183 Ok(None) => Err(ToolError::NotUsable),
184 Err(handle_err) => Err(ToolError::SpaceHandle(handle_err)),
186 }
187 }
188 Self::PushPull => {
189 let cursor = input.cursor()?;
193 let direction: Face6 = cursor
194 .face_selected()
195 .opposite()
196 .try_into()
197 .map_err(|_| ToolError::NotUsable)?;
198
199 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 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 return Err(ToolError::Internal(String::from("tool is immutable")));
240 }
241
242 Ok(transaction)
243 }
244
245 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 Self::RemoveBlock { keep: _ } => Cow::Borrowed(&predefined[Icons::Delete]),
259 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 Self::Custom { icon, op: _ } => {
273 Cow::Owned(icon.clone().with_modifier(block::Quote::default()))
274 }
275 }
276 }
277
278 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 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, }
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#[derive(Debug)]
336#[expect(clippy::exhaustive_structs, reason = "TODO: should be non_exhaustive")]
337pub struct ToolInput<'ticket> {
338 pub read_ticket: ReadTicket<'ticket>,
340
341 pub cursor: Option<Cursor>,
344
345 pub character: Option<Handle<Character>>,
349}
350
351#[allow(clippy::elidable_lifetime_names)]
352impl<'ticket> ToolInput<'ticket> {
353 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 fn place_block(
379 &self,
380 cursor: &Cursor,
381 old_block: Block,
382 mut new_block: Block,
383 ) -> Result<UniverseTransaction, ToolError> {
384 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 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 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 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 pub fn cursor(&self) -> Result<&Cursor, ToolError> {
440 self.cursor.as_ref().ok_or(ToolError::NothingSelected)
441 }
442
443 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 Ok(
452 CharacterTransaction::inventory(InventoryTransaction::insert(items))
453 .bind(character.clone()),
454 )
455 } else {
456 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 let cursor = self.cursor()?; 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#[derive(Clone, Debug, Eq, Hash, PartialEq, displaydoc::Display)]
503#[non_exhaustive]
504pub enum ToolError {
505 #[displaydoc("no tool")]
509 NoTool,
510 #[displaydoc("does not apply")]
512 NotUsable,
513 #[displaydoc("there's something in the way")]
515 Obstacle,
516 #[displaydoc("nothing is selected")]
518 NothingSelected,
519 #[displaydoc("error accessing space: {0}")]
521 SpaceHandle(HandleError),
522 #[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 #[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 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
571pub struct EphemeralOpaque<T: ?Sized>(pub(crate) Option<Arc<T>>);
579
580impl<T: ?Sized> EphemeralOpaque<T> {
581 pub fn new(contents: Arc<T>) -> Self {
583 Self(Some(contents))
584 }
585
586 pub fn defunct() -> Self {
589 Self(None)
590 }
591
592 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 }
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 }
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 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 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 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 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 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 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 }
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 }
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 #[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 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 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 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 #[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 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 #[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 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 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 assert_eq!(&tester.space()[[1, 0, 0]], &existing);
1141 }
1142
1143 #[test]
1144 fn use_custom_success() {
1145 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}