1use alloc::collections::BTreeMap;
2use alloc::collections::btree_map::Entry::*;
3use alloc::string::ToString;
4use alloc::sync::Arc;
5use alloc::vec::Vec;
6use core::{fmt, mem};
7
8use bevy_ecs::prelude as ecs;
9use hashbrown::HashMap as HbHashMap;
10
11use crate::behavior::{self, BehaviorSetTransaction};
12use crate::block::Block;
13use crate::drawing::DrawingPlane;
14use crate::fluff::Fluff;
15use crate::math::{Cube, GridCoordinate, GridPoint, Gridgid, Vol};
16use crate::space::{
17 self, ActivatableRegion, GridAab, Mutation, PendingEvaluation, SetCubeError, Space,
18};
19use crate::transaction::{
20 CommitError, Equal, ExecuteError, Merge, NoOutput, Transaction, Transactional, no_outputs,
21};
22use crate::universe::{self, ReadTicket};
23use crate::util::{ConciseDebug, Refmt as _};
24
25#[cfg(doc)]
26use crate::behavior::BehaviorSet;
27
28impl Transactional for Space {
29 type Transaction = SpaceTransaction;
30}
31
32#[derive(Clone, Default, Eq, PartialEq)]
34#[must_use]
35pub struct SpaceTransaction {
36 cubes: BTreeMap<[GridCoordinate; 3], CubeTransaction>,
37 behaviors: BehaviorSetTransaction<Space>,
38}
39
40impl SpaceTransaction {
41 pub fn at(&mut self, cube: Cube) -> &mut CubeTransaction {
51 let cube: GridPoint = cube.into();
52 self.cubes.entry(cube.into()).or_default()
53 }
54
55 pub fn filling<F>(region: GridAab, mut function: F) -> Self
58 where
59 F: FnMut(Cube) -> CubeTransaction,
60 {
61 let mut txn = SpaceTransaction::default();
64 for cube in region.interior_iter() {
65 *txn.at(cube) = function(cube);
66 }
67 txn
68 }
69
70 pub fn set_cube(cube: impl Into<Cube>, old: Option<Block>, new: Option<Block>) -> Self {
78 CubeTransaction::replacing(old, new).at(cube.into())
79 }
80
81 pub fn draw_target<C>(&mut self, transform: Gridgid) -> DrawingPlane<'_, Self, C> {
87 DrawingPlane::new(self, transform)
88 }
89
90 pub fn nonconserved(mut self) -> Self {
102 for (_, cube_txn) in self.cubes.iter_mut() {
103 cube_txn.conserved = false;
104 }
105 self
106 }
107
108 pub fn behaviors(t: BehaviorSetTransaction<Space>) -> Self {
110 Self {
111 behaviors: t,
112 ..Default::default()
113 }
114 }
115
116 pub fn add_behavior<B>(bounds: GridAab, behavior: B) -> Self
119 where
120 B: behavior::Behavior<Space> + 'static,
121 {
122 Self::behaviors(BehaviorSetTransaction::insert(
123 super::SpaceBehaviorAttachment::new(bounds),
124 Arc::new(behavior),
125 ))
126 }
127
128 pub fn bounds_only_cubes(&self) -> Option<GridAab> {
136 let Self {
138 cubes,
139 behaviors: _,
140 } = self;
141 let mut bounds: Option<GridAab> = None;
142
143 for &cube_array in cubes.keys() {
144 let cube = Cube::from(cube_array);
145 if let Some(bounds) = &mut bounds {
146 *bounds = (*bounds).union_cube(cube);
147 } else {
148 bounds = Some(GridAab::single_cube(cube));
149 }
150 }
151
152 bounds
153 }
154
155 pub fn bounds(&self) -> Option<GridAab> {
159 let Self {
161 cubes: _,
162 behaviors,
163 } = self;
164 let mut bounds: Option<GridAab> = self.bounds_only_cubes();
165
166 for attachment in behaviors.attachments_affected() {
167 if let Some(bounds) = &mut bounds {
168 *bounds = (*bounds).union_box(attachment.bounds);
169 } else {
170 bounds = Some(attachment.bounds);
171 }
172 }
173
174 bounds
175 }
176
177 fn check_common(
179 &self,
180 read_ticket: ReadTicket<'_>,
181 palette: &space::Palette,
182 contents: Vol<&[space::BlockIndex]>,
183 behaviors: &behavior::BehaviorSet<Space>,
184 ) -> Result<CommitCheck, SpaceTransactionMismatch> {
185 let mut new_block_evaluations: HbHashMap<Block, PendingEvaluation> = HbHashMap::new();
186
187 for (
188 &cube,
189 CubeTransaction {
190 old,
191 new,
192 conserved,
193 activate_behavior: _,
194 fluff: _,
195 },
196 ) in &self.cubes
197 {
198 if let Equal(Some(new_block)) = new
201 && let hashbrown::hash_map::Entry::Vacant(ve) =
202 new_block_evaluations.entry(new_block.clone())
203 {
204 ve.insert(PendingEvaluation::new(
205 read_ticket,
206 palette,
207 new_block.clone(),
208 ));
209 }
210
211 let cube = Cube::from(cube);
212 if let Some(cube_index) = contents.index(cube) {
213 if let Equal(Some(old)) = old {
214 if palette.entry(contents.as_linear()[cube_index]).block() != old {
217 return Err(SpaceTransactionMismatch::Cube(cube));
218 }
219 }
220 } else {
221 if *conserved || old.0.is_some() {
223 return Err(SpaceTransactionMismatch::OutOfBounds {
229 transaction: cube.grid_aab(),
230 space: contents.bounds(),
231 });
232 }
233 }
234 }
235 Ok(CommitCheck {
236 behaviors: self
237 .behaviors
238 .check(behaviors, ())
239 .map_err(SpaceTransactionMismatch::Behaviors)?,
240 new_block_evaluations,
241 })
242 }
243
244 fn commit_common(
245 self,
246 m: &mut Mutation<'_, '_>,
247 mut check: CommitCheck,
248 ) -> Result<(), CommitError> {
249 let mut to_activate = Vec::new();
250
251 for (
252 cube,
253 CubeTransaction {
254 old: _,
255 new,
256 conserved,
257 activate_behavior: activate,
258 fluff,
259 },
260 ) in self.cubes
261 {
262 let cube = Cube::from(cube);
263
264 if let Equal(Some(new)) = new {
265 match Space::set_impl(m, cube, &new, check.new_block_evaluations.remove(&new)) {
266 Ok(_) => Ok(()),
267 Err(SetCubeError::OutOfBounds { .. }) if !conserved => {
268 Ok(())
270 }
271 Err(other) => Err(CommitError::catch::<Self, _>(other)),
272 }?;
273 }
274
275 if activate {
276 to_activate.push(cube);
278 }
279
280 #[expect(
281 clippy::shadow_unrelated,
282 reason = "https://github.com/rust-lang/rust-clippy/issues/11827"
283 )]
284 for fluff in fluff.iter().cloned() {
285 m.fluff_buffer.push(super::SpaceFluff {
286 position: cube,
287 fluff,
288 });
289 }
290 }
291
292 self.behaviors
293 .commit(m.behaviors, check.behaviors, &mut no_outputs)
294 .map_err(|e| e.context("behaviors".into()))?;
295
296 if !to_activate.is_empty() {
297 'b: for query_item in m.behaviors.query::<ActivatableRegion>() {
298 for cube in to_activate.iter().copied() {
300 if query_item.attachment.bounds.contains_cube(cube) {
302 query_item.behavior.activate();
303 continue 'b;
304 }
305 }
306 }
307 }
308
309 Ok(())
310 }
311
312 pub fn execute_m(self, target: &mut Mutation<'_, '_>) -> Result<(), ExecuteError<Self>> {
315 let check = self
316 .check_common(
317 target.read_ticket,
318 target.palette,
319 target.contents.as_ref(),
320 target.behaviors,
321 )
322 .map_err(ExecuteError::Check)?;
323 self.commit_common(target, check).map_err(ExecuteError::Commit)
324 }
325}
326
327#[doc(hidden)] #[derive(Debug)]
329pub struct CommitCheck {
330 behaviors: <BehaviorSetTransaction<Space> as Transaction>::CommitCheck,
331 new_block_evaluations: hashbrown::HashMap<Block, PendingEvaluation>,
332}
333
334impl Transaction for SpaceTransaction {
335 type Target = Space;
336 type Context<'a> = ReadTicket<'a>;
337 type CommitCheck = CommitCheck;
338 type Output = NoOutput;
339 type Mismatch = SpaceTransactionMismatch;
340
341 fn check(
342 &self,
343 space: &Space,
344 read_ticket: Self::Context<'_>,
345 ) -> Result<Self::CommitCheck, Self::Mismatch> {
346 self.check_common(
347 read_ticket,
348 &space.palette,
349 space.contents.as_ref(),
350 &space.behaviors,
351 )
352 }
353
354 fn commit(
355 self,
356 space: &mut Space,
357 check: Self::CommitCheck,
358 _outputs: &mut dyn FnMut(Self::Output),
359 ) -> Result<(), CommitError> {
360 space.mutate(ReadTicket::stub(), |m| self.commit_common(m, check))
363 }
364}
365
366impl universe::TransactionOnEcs for SpaceTransaction {
367 type WriteQueryData = (
368 &'static mut space::Palette,
369 &'static mut space::Contents,
370 &'static mut space::LightStorage,
371 &'static mut space::BehaviorSet<Space>,
372 &'static mut space::DefaultSpawn,
373 &'static space::Notifiers,
374 &'static mut space::Ticks,
375 );
376
377 fn check(
378 &self,
379 target: space::Read<'_>,
380 read_ticket: ReadTicket<'_>,
381 ) -> Result<Self::CommitCheck, Self::Mismatch> {
382 self.check_common(
383 read_ticket,
384 target.palette,
385 target.contents,
386 target.behaviors,
387 )
388 }
389
390 fn commit(
391 self,
392 target: (
393 ecs::Mut<'_, space::Palette>,
394 ecs::Mut<'_, space::Contents>,
395 ecs::Mut<'_, space::LightStorage>,
396 ecs::Mut<'_, space::BehaviorSet<Space>>,
397 ecs::Mut<'_, space::DefaultSpawn>,
398 &space::Notifiers,
399 ecs::Mut<'_, space::Ticks>,
400 ),
401 check: Self::CommitCheck,
402 ) -> Result<(), CommitError> {
403 Mutation::with_write_query(ReadTicket::stub(), target, |m| self.commit_common(m, check))
406 }
407}
408impl Merge for SpaceTransaction {
409 type MergeCheck = <BehaviorSetTransaction<Space> as Merge>::MergeCheck;
410 type Conflict = SpaceTransactionConflict;
411
412 fn check_merge(&self, other: &Self) -> Result<Self::MergeCheck, Self::Conflict> {
413 let mut cubes1 = &self.cubes;
414 let mut cubes2 = &other.cubes;
415 if cubes1.len() > cubes2.len() {
416 mem::swap(&mut cubes1, &mut cubes2);
423 }
424 for (&cube, t1) in cubes1 {
425 if let Some(t2) = cubes2.get(&cube) {
426 let CubeMergeCheck {} =
427 t1.check_merge(t2).map_err(|conflict| SpaceTransactionConflict::Cube {
428 cube: cube.into(),
429 conflict,
430 })?;
431 }
432 }
433 self.behaviors
434 .check_merge(&other.behaviors)
435 .map_err(SpaceTransactionConflict::Behaviors)
436 }
437
438 fn commit_merge(&mut self, mut other: Self, check: Self::MergeCheck) {
439 let Self { cubes, behaviors } = self;
440
441 if other.cubes.len() > cubes.len() {
442 mem::swap(cubes, &mut other.cubes);
444 }
445 for (cube, t2) in other.cubes {
446 match cubes.entry(cube) {
447 Occupied(mut entry) => {
448 entry.get_mut().commit_merge(t2, CubeMergeCheck {});
449 }
450 Vacant(entry) => {
451 entry.insert(t2);
452 }
453 }
454 }
455
456 behaviors.commit_merge(other.behaviors, check);
457 }
458}
459
460impl fmt::Debug for SpaceTransaction {
461 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
462 let Self { cubes, behaviors } = self;
463 let mut ds = fmt.debug_struct("SpaceTransaction");
464 for (cube, txn) in cubes {
465 ds.field(&Cube::from(*cube).refmt(&ConciseDebug).to_string(), txn);
466 }
467 if !behaviors.is_empty() {
468 ds.field("behaviors", &behaviors);
469 }
470 ds.finish()
471 }
472}
473
474impl universe::VisitHandles for SpaceTransaction {
475 fn visit_handles(&self, visitor: &mut dyn universe::HandleVisitor) {
476 let Self { cubes, behaviors } = self;
477 for cube_txn in cubes.values() {
478 cube_txn.visit_handles(visitor);
479 }
480 behaviors.visit_handles(visitor);
481 }
482}
483
484#[derive(Clone, Debug, Eq, PartialEq)]
486#[non_exhaustive]
487pub enum SpaceTransactionMismatch {
488 #[allow(missing_docs)]
489 Cube(Cube),
490
491 OutOfBounds {
493 transaction: GridAab,
496
497 space: GridAab,
499 },
500
501 #[allow(missing_docs)]
502 Behaviors(behavior::BehaviorTransactionMismatch),
503}
504
505#[derive(Clone, Debug, Eq, PartialEq)]
507#[non_exhaustive]
508pub enum SpaceTransactionConflict {
509 #[allow(missing_docs)]
510 Cube {
511 cube: Cube, conflict: CubeConflict,
513 },
514 #[allow(missing_docs)]
515 Behaviors(behavior::BehaviorTransactionConflict),
516}
517
518impl core::error::Error for SpaceTransactionMismatch {
519 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
520 match self {
521 SpaceTransactionMismatch::Cube(_) => None,
522 SpaceTransactionMismatch::OutOfBounds { .. } => None,
523 SpaceTransactionMismatch::Behaviors(mismatch) => Some(mismatch),
524 }
525 }
526}
527impl core::error::Error for SpaceTransactionConflict {
528 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
529 match self {
530 SpaceTransactionConflict::Cube { conflict, .. } => Some(conflict),
531 SpaceTransactionConflict::Behaviors(conflict) => Some(conflict),
532 }
533 }
534}
535
536impl fmt::Display for SpaceTransactionMismatch {
537 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
538 match self {
539 SpaceTransactionMismatch::Cube(cube) => {
540 write!(f, "mismatch at cube {c}", c = cube.refmt(&ConciseDebug))
541 }
542
543 SpaceTransactionMismatch::OutOfBounds { transaction, space } => {
544 write!(
546 f,
547 "transaction bounds {transaction:?} exceed space bounds {space:?}"
548 )
549 }
550 SpaceTransactionMismatch::Behaviors(_) => write!(f, "in behaviors"),
551 }
552 }
553}
554impl fmt::Display for SpaceTransactionConflict {
555 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
556 match self {
557 SpaceTransactionConflict::Cube { cube, conflict: _ } => {
558 write!(f, "conflict at cube {c}", c = cube.refmt(&ConciseDebug))
559 }
560 SpaceTransactionConflict::Behaviors(_) => write!(f, "conflict in behaviors"),
561 }
562 }
563}
564
565#[derive(Clone, Default, Eq, PartialEq)]
571pub struct CubeTransaction {
572 old: Equal<Block>,
575
576 new: Equal<Block>,
579
580 conserved: bool,
582
583 activate_behavior: bool,
586
587 fluff: Vec<Fluff>,
595}
596
597impl fmt::Debug for CubeTransaction {
598 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
599 let Self {
600 old: Equal(old),
601 new: Equal(new),
602 conserved,
603 activate_behavior,
604 fluff,
605 } = self;
606 let mut ds = f.debug_struct("CubeTransaction");
607 if old.is_some() || new.is_some() {
608 ds.field("old", &old);
609 ds.field("new", &new);
610 ds.field("conserved", &conserved);
611 }
612 if *activate_behavior {
613 ds.field("activate_behavior", &activate_behavior);
614 }
615 if !fluff.is_empty() {
616 ds.field("fluff", &fluff);
617 }
618 ds.finish()
619 }
620}
621
622impl CubeTransaction {
623 pub fn at(self, cube: Cube) -> SpaceTransaction {
625 SpaceTransaction {
626 cubes: BTreeMap::from([(<[i32; 3]>::from(cube), self)]),
627 ..Default::default()
628 }
629 }
630
631 pub(crate) const ACTIVATE_BEHAVIOR: Self = Self {
632 old: Equal(None),
633 new: Equal(None),
634 conserved: false,
635 activate_behavior: true,
636 fluff: Vec::new(),
637 };
638
639 pub fn replacing(old: Option<Block>, new: Option<Block>) -> Self {
645 CubeTransaction {
646 old: Equal(old),
647 new: Equal(new),
648 conserved: true,
649 ..Default::default()
650 }
651 }
652
653 pub fn overwrite(&mut self, block: Block) {
661 self.new = Equal(Some(block));
662 }
663
664 #[doc(hidden)] pub fn new_mut(&mut self) -> Option<&mut Block> {
666 self.new.0.as_mut()
667 }
668
669 pub fn fluff(fluff: Fluff) -> Self {
671 let mut this = Self::default();
672 this.add_fluff(fluff);
673 this
674 }
675
676 pub fn add_fluff(&mut self, fluff: Fluff) {
679 self.fluff.push(fluff)
680 }
681}
682
683impl Merge for CubeTransaction {
684 type MergeCheck = CubeMergeCheck;
685 type Conflict = CubeConflict;
686
687 fn check_merge(&self, other: &Self) -> Result<Self::MergeCheck, Self::Conflict> {
688 let conflict = CubeConflict {
689 old: self.old.check_merge(&other.old).is_err(),
691 new: if self.conserved {
692 self.new.0.is_some() && other.new.0.is_some()
695 } else {
696 self.new.check_merge(&other.new).is_err()
698 },
699 };
700
701 if (conflict
702 != CubeConflict {
703 old: false,
704 new: false,
705 })
706 {
707 Err(conflict)
708 } else {
709 Ok(CubeMergeCheck {})
710 }
711 }
712
713 fn commit_merge(&mut self, other: Self, CubeMergeCheck {}: Self::MergeCheck) {
714 let Self {
715 old,
716 new,
717 conserved,
718 activate_behavior,
719 fluff,
720 } = self;
721
722 *conserved = (*conserved && new.0.is_some()) || (other.conserved && other.new.0.is_some());
724
725 old.commit_merge(other.old, ());
726 new.commit_merge(other.new, ());
727
728 *activate_behavior |= other.activate_behavior;
729
730 fluff.extend(other.fluff);
731 }
732}
733
734impl universe::VisitHandles for CubeTransaction {
735 fn visit_handles(&self, visitor: &mut dyn universe::HandleVisitor) {
736 let Self {
737 old,
738 new,
739 conserved: _,
740 activate_behavior: _,
741 fluff,
742 } = self;
743
744 old.visit_handles(visitor);
745 new.visit_handles(visitor);
746 fluff.visit_handles(visitor);
747 }
748}
749
750#[doc(hidden)]
751#[derive(Debug)]
752#[non_exhaustive]
753pub struct CubeMergeCheck {
754 }
758
759#[derive(Clone, Copy, Debug, Eq, PartialEq)]
762#[non_exhaustive]
763pub struct CubeConflict {
764 pub(crate) old: bool,
766 pub(crate) new: bool,
768}
769
770impl core::error::Error for CubeConflict {}
771
772impl fmt::Display for CubeConflict {
773 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
774 match *self {
775 CubeConflict {
776 old: true,
777 new: false,
778 } => write!(f, "different preconditions"),
779 CubeConflict {
780 old: false,
781 new: true,
782 } => write!(f, "cannot write the same cube twice"),
783 CubeConflict {
784 old: true,
785 new: true,
786 } => write!(f, "different preconditions (with write)"),
787 CubeConflict {
788 old: false,
789 new: false,
790 } => unreachable!(),
791 }
792 }
793}
794
795#[cfg(test)]
796mod tests {
797 use super::*;
798 use crate::behavior::NoopBehavior;
799 use crate::block::{self, AIR};
800 use crate::content::make_some_blocks;
801 use crate::inv::EphemeralOpaque;
802 use crate::transaction::TransactionTester;
803 use core::sync::atomic::{AtomicU32, Ordering};
804 use pretty_assertions::assert_eq;
805
806 #[test]
807 fn set_out_of_bounds_conserved_fails() {
808 let [block] = make_some_blocks();
809 SpaceTransaction::set_cube([1, 0, 0], None, Some(block))
811 .check(&Space::empty_positive(1, 1, 1), ReadTicket::stub())
812 .unwrap_err();
813 }
814
815 #[test]
816 fn set_out_of_bounds_nonconserved_succeeds() {
817 let [block] = make_some_blocks();
818 SpaceTransaction::set_cube([1, 0, 0], None, Some(block))
819 .nonconserved()
820 .execute(
821 &mut Space::empty_positive(1, 1, 1),
822 ReadTicket::stub(),
823 &mut no_outputs,
824 )
825 .unwrap();
826 }
827
828 #[test]
829 fn compare_out_of_bounds_conserved_fails() {
830 let [block] = make_some_blocks();
831 SpaceTransaction::set_cube([1, 0, 0], Some(block), None)
832 .check(&Space::empty_positive(1, 1, 1), ReadTicket::stub())
833 .unwrap_err();
834 }
835
836 #[test]
837 fn compare_out_of_bounds_nonconserved_fails() {
838 let [block] = make_some_blocks();
839 SpaceTransaction::set_cube([1, 0, 0], Some(block), None)
840 .nonconserved()
841 .check(&Space::empty_positive(1, 1, 1), ReadTicket::stub())
842 .unwrap_err();
843 }
844
845 #[rstest::rstest]
852 fn block_changed_between_check_and_commit(#[values(false, true)] actually_change: bool) {
853 let mut u = universe::Universe::new();
854 let [initial_block_value, final_block_value] = make_some_blocks();
855 let block_handle = u.insert_anonymous(block::BlockDef::new(
856 u.read_ticket(),
857 initial_block_value.clone(),
858 ));
859 let indirect_block = Block::from(block_handle.clone());
860 let mut space = Space::builder(GridAab::ORIGIN_CUBE).build();
861
862 let space_txn =
863 SpaceTransaction::set_cube([0, 0, 0], Some(AIR), Some(indirect_block.clone()));
864 let check = space_txn.check(&space, u.read_ticket()).unwrap();
865 if actually_change {
866 u.execute_1(
867 &block_handle,
868 block::BlockDefTransaction::overwrite(final_block_value.clone()),
869 )
870 .unwrap();
871 }
872 space_txn.commit(&mut space, check, &mut no_outputs).unwrap();
873
874 assert_eq!(
875 space.get_evaluated([0, 0, 0]).color(),
876 initial_block_value.color(),
877 "evaluation should always be from check time, regardless of change"
878 );
879 assert_eq!(
880 space.palette.block_is_to_be_reevaluated(&indirect_block),
881 actually_change,
882 "space todo ≠ actual change"
883 );
884 }
885
886 #[test]
887 fn merge_allows_independent() {
888 let [b1, b2, b3] = make_some_blocks();
889 let t1 = SpaceTransaction::set_cube([0, 0, 0], Some(b1.clone()), Some(b2.clone()));
890 let t2 = SpaceTransaction::set_cube([1, 0, 0], Some(b1.clone()), Some(b3.clone()));
891 let t3 = t1.clone().merge(t2.clone()).unwrap();
892 assert_eq!(
893 t3.cubes.into_iter().collect::<Vec<_>>(),
894 vec![
895 (
896 [0, 0, 0],
897 CubeTransaction {
898 old: Equal(Some(b1.clone())),
899 new: Equal(Some(b2.clone())),
900 conserved: true,
901 activate_behavior: false,
902 fluff: vec![],
903 }
904 ),
905 (
906 [1, 0, 0],
907 CubeTransaction {
908 old: Equal(Some(b1.clone())),
909 new: Equal(Some(b3.clone())),
910 conserved: true,
911 activate_behavior: false,
912 fluff: vec![],
913 }
914 ),
915 ]
916 );
917 }
918
919 #[test]
920 fn merge_rejects_same_new_conserved() {
921 let [block] = make_some_blocks();
922 let t1 = SpaceTransaction::set_cube([0, 0, 0], None, Some(block.clone()));
923 let t2 = SpaceTransaction::set_cube([0, 0, 0], None, Some(block.clone()));
924 t1.merge(t2).unwrap_err();
925 }
926
927 #[test]
928 fn merge_allows_same_new_nonconserved() {
929 let [old, new] = make_some_blocks();
930 let t1 = SpaceTransaction::set_cube([0, 0, 0], Some(old), Some(new.clone())).nonconserved();
931 let t2 = SpaceTransaction::set_cube([0, 0, 0], None, Some(new.clone())).nonconserved();
932 assert_eq!(t1.clone().merge(t2).unwrap(), t1);
933 }
934
935 #[test]
936 fn merge_rejects_different_new_conserved() {
937 let [b1, b2] = make_some_blocks();
938 let t1 = SpaceTransaction::set_cube([0, 0, 0], None, Some(b1.clone()));
939 let t2 = SpaceTransaction::set_cube([0, 0, 0], None, Some(b2.clone()));
940 t1.merge(t2).unwrap_err();
941 }
942
943 #[test]
944 fn merge_rejects_different_new_nonconserved() {
945 let [b1, b2] = make_some_blocks();
946 let t1 = SpaceTransaction::set_cube([0, 0, 0], None, Some(b1.clone())).nonconserved();
947 let t2 = SpaceTransaction::set_cube([0, 0, 0], None, Some(b2.clone())).nonconserved();
948 t1.merge(t2).unwrap_err();
949 }
950
951 #[test]
952 fn merge_rejects_different_old() {
953 let [b1, b2] = make_some_blocks();
954 let t1 = SpaceTransaction::set_cube([0, 0, 0], Some(b1.clone()), None);
955 let t2 = SpaceTransaction::set_cube([0, 0, 0], Some(b2.clone()), None);
956 t1.merge(t2).unwrap_err();
957 }
958
959 #[test]
960 fn merge_allows_same_old() {
961 let [b1, b2] = make_some_blocks();
962 let t1 = SpaceTransaction::set_cube([0, 0, 0], Some(b1.clone()), Some(b2.clone()));
963 let t2 = SpaceTransaction::set_cube([0, 0, 0], Some(b1.clone()), None);
964 assert_eq!(t1.clone(), t1.clone().merge(t2).unwrap());
965 }
966
967 #[test]
968 fn activate() {
969 let mut space = Space::empty_positive(1, 1, 1);
970 let cube = Cube::new(0, 0, 0);
971
972 let signal = Arc::new(AtomicU32::new(0));
973 SpaceTransaction::add_behavior(
974 GridAab::single_cube(cube),
975 ActivatableRegion {
976 effect: EphemeralOpaque::new(Arc::new({
978 let signal = signal.clone();
979 move || {
980 signal.fetch_add(1, Ordering::Relaxed);
981 }
982 })),
983 },
984 )
985 .execute(&mut space, ReadTicket::stub(), &mut no_outputs)
986 .unwrap();
987
988 CubeTransaction::ACTIVATE_BEHAVIOR
989 .at(cube)
990 .execute(&mut space, ReadTicket::stub(), &mut drop)
991 .unwrap();
992 assert_eq!(signal.load(Ordering::Relaxed), 1);
993 }
994
995 #[test]
996 fn systematic() {
997 let [b1, b2, b3] = make_some_blocks();
998 TransactionTester::new()
999 .transaction(SpaceTransaction::default(), |_, _| Ok(()))
1000 .transaction(
1001 SpaceTransaction::set_cube([0, 0, 0], Some(b1.clone()), Some(b2.clone())),
1002 |_, after| {
1003 if after[[0, 0, 0]] != b2 {
1004 return Err("did not set b2".into());
1005 }
1006 Ok(())
1007 },
1008 )
1009 .transaction(
1010 SpaceTransaction::set_cube([0, 0, 0], Some(b1.clone()), Some(b3.clone())),
1011 |_, after| {
1012 if after[[0, 0, 0]] != b3 {
1013 return Err("did not set b3".into());
1014 }
1015 Ok(())
1016 },
1017 )
1018 .transaction(
1019 SpaceTransaction::set_cube([0, 0, 0], None, Some(b2.clone())),
1020 |_, after| {
1021 if after[[0, 0, 0]] != b2 {
1022 return Err("did not set b2".into());
1023 }
1024 Ok(())
1025 },
1026 )
1027 .transaction(
1028 SpaceTransaction::set_cube([0, 0, 0], Some(b2.clone()), None),
1029 |_, _| Ok(()),
1030 )
1031 .transaction(
1032 SpaceTransaction::set_cube([0, 0, 0], Some(b1.clone()), None),
1033 |_, _| Ok(()),
1034 )
1035 .transaction(
1036 CubeTransaction::ACTIVATE_BEHAVIOR.at(Cube::new(0, 0, 0)),
1037 |_, _| Ok(()),
1039 )
1040 .transaction(
1041 CubeTransaction::ACTIVATE_BEHAVIOR.at(Cube::new(1, 0, 0)),
1042 |_, _| Ok(()),
1044 )
1045 .target(|| Space::empty_positive(2, 1, 1))
1046 .target(|| {
1047 Space::builder(GridAab::from_lower_size([0, 0, 0], [2, 1, 1]))
1048 .build_and_mutate(|m| {
1049 m.set([0, 0, 0], &b1)?;
1050 Ok(())
1051 })
1052 .unwrap()
1053 })
1054 .target(|| {
1055 Space::builder(GridAab::from_lower_size([0, 0, 0], [2, 1, 1]))
1056 .build_and_mutate(|m| {
1057 m.set([0, 0, 0], &b2)?;
1058 Ok(())
1059 })
1060 .unwrap()
1061 })
1062 .target(|| {
1063 Space::empty(GridAab::from_lower_size([1, 0, 0], [1, 1, 1]))
1065 })
1066 .test(ReadTicket::stub());
1068 }
1069
1070 #[test]
1071 fn bounds_empty() {
1072 assert_eq!(SpaceTransaction::default().bounds(), None);
1073 }
1074
1075 #[test]
1076 fn bounds_single_cube() {
1077 assert_eq!(
1078 SpaceTransaction::set_cube([-7, 3, 5], None, Some(AIR)).bounds(),
1079 Some(GridAab::single_cube(Cube::new(-7, 3, 5)))
1080 );
1081 }
1082
1083 #[test]
1084 fn bounds_multi_cube() {
1085 let t1 = SpaceTransaction::set_cube([-7, 3, 5], None, Some(AIR));
1086 let t2 = SpaceTransaction::set_cube([10, 3, 5], None, Some(AIR));
1087 assert_eq!(
1088 t1.merge(t2).unwrap().bounds(),
1089 Some(GridAab::from_lower_upper([-7, 3, 5], [11, 4, 6]))
1090 );
1091 }
1092
1093 #[test]
1094 fn bounds_behavior() {
1095 let bounds = GridAab::from_lower_size([1, 2, 3], [4, 5, 6]);
1096 let txn = SpaceTransaction::add_behavior(bounds, NoopBehavior(1));
1097 assert_eq!(txn.bounds(), Some(bounds));
1098 }
1099}