1use std::sync::Arc;
4
5use simdnbt::owned::NbtTag;
6use steel_registry::{blocks::block_state_ext::BlockStateExt as _, vanilla_blocks};
7use steel_utils::{
8 BlockPos, BoundingBox, ChunkPos, SectionPos,
9 nbt::{NbtPath, compare_nbt_compounds},
10 translations,
11};
12use text_components::TextComponent;
13
14use super::super::super::{
15 brigadier::{CommandNodeBuilder, CommandRedirectTarget, CommandSyntaxError},
16 execution::{
17 CommandSource, SteelArgumentType, SteelCommandContext, SteelCommandRuntime, argument,
18 literal,
19 },
20};
21use super::{objective, source_command_storage, source_scoreboard};
22use crate::{block_entity::SharedBlockEntity, command::missing_argument, world::World};
23
24type Builder = CommandNodeBuilder<CommandSource, SteelCommandRuntime>;
25
26const EXECUTE_ROOT: CommandRedirectTarget = CommandRedirectTarget::CommandRoot;
27const MAX_BLOCKS_REGION: i64 = 32_768;
28
29pub(super) fn conditionals(name: &'static str, expected: bool) -> Builder {
30 literal(name)
35 .then(biome_condition(expected))
36 .then(block_condition(expected))
37 .then(blocks_condition(expected))
38 .then(data_condition(expected))
39 .then(dimension_condition(expected))
40 .then(entity_condition(expected))
41 .then(loaded_condition(expected))
42 .then(score_condition(expected))
43}
44
45fn data_condition(expected: bool) -> Builder {
46 literal("data")
47 .then(
48 literal("block").then(
49 argument("sourcePos", SteelArgumentType::block_pos())
50 .then(data_path(DataSource::Block, expected)),
51 ),
52 )
53 .then(
54 literal("entity").then(
55 argument("source", SteelArgumentType::entity())
56 .then(data_path(DataSource::Entity, expected)),
57 ),
58 )
59 .then(
60 literal("storage").then(
61 argument("source", SteelArgumentType::storage_key())
62 .then(data_path(DataSource::Storage, expected)),
63 ),
64 )
65}
66
67fn data_path(source: DataSource, expected: bool) -> Builder {
68 argument("path", SteelArgumentType::nbt_path())
69 .forks(EXECUTE_ROOT, move |context| {
70 let matches = data_match_count(context, source)? > 0;
71 Ok(conditional_sources(context.source(), expected, matches))
72 })
73 .executes(move |context| {
74 let count = data_match_count(context, source)?;
75 execute_numeric_condition(context, expected, count)
76 })
77}
78
79#[derive(Clone, Copy)]
80enum DataSource {
81 Block,
82 Entity,
83 Storage,
84}
85
86fn data_match_count(
87 context: &SteelCommandContext<CommandSource>,
88 source: DataSource,
89) -> Result<i32, CommandSyntaxError> {
90 let tag = match source {
91 DataSource::Block => {
92 let position = loaded_block_position(context, "sourcePos")?;
93 let block_entity = context
94 .source()
95 .world()
96 .get_block_entity(position)
97 .ok_or_else(invalid_block_data_source)?;
98 let data = block_entity.save_with_full_metadata();
99 NbtTag::Compound(data)
100 }
101 DataSource::Entity => {
102 let entity = context.entity("source")?;
103 NbtTag::Compound(entity.nbt_for_data_compare())
104 }
105 DataSource::Storage => {
106 let key = context.identifier("source")?;
107 NbtTag::Compound(source_command_storage(context)?.get(key))
108 }
109 };
110 let path = context.nbt_path("path")?;
111 matching_data_count(path, &tag)
112}
113
114fn matching_data_count(path: &NbtPath, tag: &NbtTag) -> Result<i32, CommandSyntaxError> {
115 i32::try_from(path.count_matching(tag))
116 .map_err(|_| CommandSyntaxError::dynamic("NBT match count exceeds the command range"))
117}
118
119pub(super) fn invalid_block_data_source() -> CommandSyntaxError {
120 CommandSyntaxError::dynamic(TextComponent::from(
121 &translations::COMMANDS_DATA_BLOCK_INVALID,
122 ))
123}
124
125fn dimension_condition(expected: bool) -> Builder {
126 literal("dimension").then(
127 argument("dimension", SteelArgumentType::world())
128 .forks(EXECUTE_ROOT, move |context| {
129 let matches = dimension_matches(context)?;
130 Ok(conditional_sources(context.source(), expected, matches))
131 })
132 .executes(move |context| {
133 execute_boolean_condition(context, expected, dimension_matches(context)?)
134 }),
135 )
136}
137
138fn dimension_matches(
139 context: &SteelCommandContext<CommandSource>,
140) -> Result<bool, CommandSyntaxError> {
141 let world = context
142 .world_argument("dimension")?
143 .resolve(context.source())?;
144 Ok(Arc::ptr_eq(context.source().world(), &world))
145}
146
147fn blocks_condition(expected: bool) -> Builder {
148 literal("blocks").then(
149 argument("start", SteelArgumentType::block_pos()).then(
150 argument("end", SteelArgumentType::block_pos()).then(
151 argument("destination", SteelArgumentType::block_pos())
152 .then(blocks_mode("all", expected, false))
153 .then(blocks_mode("masked", expected, true)),
154 ),
155 ),
156 )
157}
158
159fn blocks_mode(name: &'static str, expected: bool, skip_air: bool) -> Builder {
160 literal(name)
161 .forks(EXECUTE_ROOT, move |context| {
162 let matches = matching_block_region_count(context, skip_air)?.is_some();
163 Ok(conditional_sources(context.source(), expected, matches))
164 })
165 .executes(move |context| {
166 let count = matching_block_region_count(context, skip_air)?;
167 execute_blocks_condition(context, expected, count)
168 })
169}
170
171fn matching_block_region_count(
172 context: &SteelCommandContext<CommandSource>,
173 skip_air: bool,
174) -> Result<Option<i32>, CommandSyntaxError> {
175 let source_start = loaded_block_position(context, "start")?;
176 let source_end = loaded_block_position(context, "end")?;
177 let destination_start = loaded_block_position(context, "destination")?;
178 let source_region = BoundingBox::from_corners(source_start, source_end);
179 let destination_end = destination_start.offset(
180 source_region.max_x() - source_region.min_x(),
181 source_region.max_y() - source_region.min_y(),
182 source_region.max_z() - source_region.min_z(),
183 );
184 let destination_region = BoundingBox::from_corners(destination_start, destination_end);
185 let area = block_region_volume(&source_region);
186 if area > MAX_BLOCKS_REGION {
187 return Err(blocks_too_big(area));
188 }
189
190 let world = context.source().world();
191 ensure_region_chunks_loaded(world, &source_region)?;
192 ensure_region_chunks_loaded(world, &destination_region)?;
193
194 let offset_x = destination_region.min_x() - source_region.min_x();
195 let offset_y = destination_region.min_y() - source_region.min_y();
196 let offset_z = destination_region.min_z() - source_region.min_z();
197 let mut count = 0;
198 for z in source_region.min_z()..=source_region.max_z() {
199 for y in source_region.min_y()..=source_region.max_y() {
200 for x in source_region.min_x()..=source_region.max_x() {
201 let source_pos = BlockPos::new(x, y, z);
202 let source_state = world.get_block_state(source_pos);
203 if !should_compare_block(source_state, skip_air) {
204 continue;
205 }
206 let destination_pos = source_pos.offset(offset_x, offset_y, offset_z);
207 if source_state != world.get_block_state(destination_pos)
208 || !block_entities_match(world, source_pos, destination_pos)
209 {
210 return Ok(None);
211 }
212 count += 1;
213 }
214 }
215 }
216 Ok(Some(count))
217}
218
219fn block_region_volume(region: &BoundingBox) -> i64 {
220 let x_span = i64::from(region.max_x()) - i64::from(region.min_x()) + 1;
221 let y_span = i64::from(region.max_y()) - i64::from(region.min_y()) + 1;
222 let z_span = i64::from(region.max_z()) - i64::from(region.min_z()) + 1;
223 x_span.saturating_mul(y_span).saturating_mul(z_span)
224}
225
226pub(in crate::command::builtins) fn ensure_region_chunks_loaded(
228 world: &World,
229 region: &BoundingBox,
230) -> Result<(), CommandSyntaxError> {
231 ensure_region_chunks_available(world, region, RegionChunkAvailability::Full)
232}
233
234pub(in crate::command::builtins) fn ensure_region_chunks_block_ticking(
235 world: &World,
236 region: &BoundingBox,
237) -> Result<(), CommandSyntaxError> {
238 ensure_region_chunks_available(world, region, RegionChunkAvailability::BlockTicking)
239}
240
241#[derive(Clone, Copy)]
242enum RegionChunkAvailability {
243 Full,
244 BlockTicking,
245}
246
247fn ensure_region_chunks_available(
248 world: &World,
249 region: &BoundingBox,
250 availability: RegionChunkAvailability,
251) -> Result<(), CommandSyntaxError> {
252 if region.max_y() < world.get_min_y() || region.min_y() > world.get_max_y() {
253 return Ok(());
254 }
255 let min_chunk_x = SectionPos::block_to_section_coord(region.min_x());
256 let max_chunk_x = SectionPos::block_to_section_coord(region.max_x());
257 let min_chunk_z = SectionPos::block_to_section_coord(region.min_z());
258 let max_chunk_z = SectionPos::block_to_section_coord(region.max_z());
259 for chunk_z in min_chunk_z..=max_chunk_z {
260 for chunk_x in min_chunk_x..=max_chunk_x {
261 if !ChunkPos::is_valid(chunk_x, chunk_z) {
262 continue;
263 }
264 let pos = BlockPos::new(chunk_x * 16, world.get_min_y(), chunk_z * 16);
265 let available = match availability {
266 RegionChunkAvailability::Full => world.is_full_chunk_loaded_at(pos),
267 RegionChunkAvailability::BlockTicking => world.is_block_ticking_chunk_loaded(pos),
268 };
269 if !available {
270 return Err(unloaded_position());
271 }
272 }
273 }
274 Ok(())
275}
276
277fn should_compare_block(state: steel_utils::BlockStateId, skip_air: bool) -> bool {
278 !skip_air || state.get_block() != &vanilla_blocks::AIR
279}
280
281fn block_entities_match(world: &World, source: BlockPos, destination: BlockPos) -> bool {
282 let source_entity = world.get_block_entity(source);
283 let destination_entity = world.get_block_entity(destination);
284 block_entity_data_matches(source_entity.as_ref(), destination_entity.as_ref())
285}
286
287fn block_entity_data_matches(
288 source: Option<&SharedBlockEntity>,
289 destination: Option<&SharedBlockEntity>,
290) -> bool {
291 let Some(source) = source else {
292 return true;
293 };
294 let Some(destination) = destination else {
295 return false;
296 };
297 if Arc::ptr_eq(source, destination) {
298 return true;
299 }
300 if source.get_type() != destination.get_type() {
301 return false;
302 }
303 let source_data = source.save_custom_only();
304 let destination_data = destination.save_custom_only();
305 source_data.len() == destination_data.len()
306 && compare_nbt_compounds(&source_data, &destination_data, false)
307}
308
309fn execute_blocks_condition(
310 context: &SteelCommandContext<CommandSource>,
311 expected: bool,
312 count: Option<i32>,
313) -> Result<i32, CommandSyntaxError> {
314 match (expected, count) {
315 (true, Some(count)) => {
316 let message = translations::COMMANDS_EXECUTE_CONDITIONAL_PASS_COUNT
317 .message([TextComponent::from(count.to_string())])
318 .component();
319 context.source().send_success(&message, false);
320 Ok(count)
321 }
322 (true, None) => Err(conditional_failed()),
323 (false, Some(count)) => Err(conditional_failed_count(count)),
324 (false, None) => {
325 context.source().send_success(
326 &TextComponent::from(&translations::COMMANDS_EXECUTE_CONDITIONAL_PASS),
327 false,
328 );
329 Ok(1)
330 }
331 }
332}
333
334fn blocks_too_big(area: i64) -> CommandSyntaxError {
335 let message = translations::COMMANDS_EXECUTE_BLOCKS_TOOBIG
336 .message([
337 TextComponent::from(MAX_BLOCKS_REGION.to_string()),
338 TextComponent::from(area.to_string()),
339 ])
340 .component();
341 CommandSyntaxError::dynamic(message)
342}
343
344fn block_condition(expected: bool) -> Builder {
345 literal("block").then(
346 argument("pos", SteelArgumentType::block_pos()).then(
347 argument("block", SteelArgumentType::block_predicate())
348 .forks(EXECUTE_ROOT, move |context| {
349 let matches = block_matches(context)?;
350 Ok(conditional_sources(context.source(), expected, matches))
351 })
352 .executes(move |context| {
353 execute_boolean_condition(context, expected, block_matches(context)?)
354 }),
355 ),
356 )
357}
358
359fn block_matches(context: &SteelCommandContext<CommandSource>) -> Result<bool, CommandSyntaxError> {
360 let position = loaded_block_position(context, "pos")?;
361 let Ok(predicate) = context.block_predicate("block") else {
362 return Err(missing_argument("block"));
363 };
364 let world = context.source().world();
365 Ok(predicate.matches(world, position))
366}
367
368fn biome_condition(expected: bool) -> Builder {
369 literal("biome").then(
370 argument("pos", SteelArgumentType::block_pos()).then(
371 argument("biome", SteelArgumentType::biome_or_tag())
372 .forks(EXECUTE_ROOT, move |context| {
373 let matches = biome_matches(context)?;
374 Ok(conditional_sources(context.source(), expected, matches))
375 })
376 .executes(move |context| {
377 execute_boolean_condition(context, expected, biome_matches(context)?)
378 }),
379 ),
380 )
381}
382
383fn biome_matches(context: &SteelCommandContext<CommandSource>) -> Result<bool, CommandSyntaxError> {
384 let position = loaded_block_position(context, "pos")?;
385 let world = context.source().world();
386 let biome = world.biome_at(position).ok_or_else(|| {
387 CommandSyntaxError::dynamic(TextComponent::from(&translations::ARGUMENT_POS_UNLOADED))
388 })?;
389 let expected = context.biome_or_tag("biome")?;
390 Ok(expected.matches(biome))
391}
392
393pub fn loaded_block_position(
394 context: &SteelCommandContext<CommandSource>,
395 name: &str,
396) -> Result<steel_utils::BlockPos, CommandSyntaxError> {
397 let position = context.coordinates(name)?.block_pos(context.source());
398 let world = context.source().world();
399 if !world.is_full_chunk_loaded_at(position) {
400 return Err(unloaded_position());
401 }
402 if !world.is_in_valid_bounds(position) {
403 return Err(CommandSyntaxError::dynamic(TextComponent::from(
404 &translations::ARGUMENT_POS_OUTOFWORLD,
405 )));
406 }
407 Ok(position)
408}
409
410fn unloaded_position() -> CommandSyntaxError {
411 CommandSyntaxError::dynamic(TextComponent::from(&translations::ARGUMENT_POS_UNLOADED))
412}
413
414fn entity_condition(expected: bool) -> Builder {
415 literal("entity").then(
416 argument("entities", SteelArgumentType::entities())
417 .forks(EXECUTE_ROOT, move |context| {
418 let matches = !context.optional_entities("entities")?.is_empty();
419 Ok(conditional_sources(context.source(), expected, matches))
420 })
421 .executes(move |context| {
422 let count =
423 i32::try_from(context.optional_entities("entities")?.len()).map_err(|_| {
424 CommandSyntaxError::dynamic("Entity count exceeds the command result range")
425 })?;
426 execute_numeric_condition(context, expected, count)
427 }),
428 )
429}
430
431fn loaded_condition(expected: bool) -> Builder {
432 literal("loaded").then(
433 argument("pos", SteelArgumentType::block_pos())
434 .forks(EXECUTE_ROOT, move |context| {
435 let matches = loaded_matches(context)?;
436 Ok(conditional_sources(context.source(), expected, matches))
437 })
438 .executes(move |context| {
439 execute_boolean_condition(context, expected, loaded_matches(context)?)
440 }),
441 )
442}
443
444fn loaded_matches(
445 context: &SteelCommandContext<CommandSource>,
446) -> Result<bool, CommandSyntaxError> {
447 let position = context.coordinates("pos")?.block_pos(context.source());
448 Ok(context
449 .source()
450 .world()
451 .is_entity_ticking_chunk_loaded(position))
452}
453
454fn score_condition(expected: bool) -> Builder {
455 literal("score").then(
456 argument("target", SteelArgumentType::score_holder()).then(
457 argument("targetObjective", SteelArgumentType::objective())
458 .then(score_comparison("=", ScoreComparison::Equal, expected))
459 .then(score_comparison("<", ScoreComparison::Less, expected))
460 .then(score_comparison(
461 "<=",
462 ScoreComparison::LessOrEqual,
463 expected,
464 ))
465 .then(score_comparison(">", ScoreComparison::Greater, expected))
466 .then(score_comparison(
467 ">=",
468 ScoreComparison::GreaterOrEqual,
469 expected,
470 ))
471 .then(
472 literal("matches").then(
473 argument("range", SteelArgumentType::int_range())
474 .forks(EXECUTE_ROOT, move |context| {
475 let matches = score_range_matches(context)?;
476 Ok(conditional_sources(context.source(), expected, matches))
477 })
478 .executes(move |context| {
479 execute_boolean_condition(
480 context,
481 expected,
482 score_range_matches(context)?,
483 )
484 }),
485 ),
486 ),
487 ),
488 )
489}
490
491fn score_comparison(name: &'static str, comparison: ScoreComparison, expected: bool) -> Builder {
492 literal(name).then(
493 argument("source", SteelArgumentType::score_holder()).then(
494 argument("sourceObjective", SteelArgumentType::objective())
495 .forks(EXECUTE_ROOT, move |context| {
496 let matches = scores_match(context, comparison)?;
497 Ok(conditional_sources(context.source(), expected, matches))
498 })
499 .executes(move |context| {
500 execute_boolean_condition(context, expected, scores_match(context, comparison)?)
501 }),
502 ),
503 )
504}
505
506fn scores_match(
507 context: &SteelCommandContext<CommandSource>,
508 comparison: ScoreComparison,
509) -> Result<bool, CommandSyntaxError> {
510 let scoreboard = source_scoreboard(context)?;
511 let target = context.score_holder("target")?;
512 let target_objective = objective(context, scoreboard, "targetObjective")?;
513 let source = context.score_holder("source")?;
514 let source_objective = objective(context, scoreboard, "sourceObjective")?;
515 let Some(target_score) = scoreboard.score(&target, &target_objective) else {
516 return Ok(false);
517 };
518 let Some(source_score) = scoreboard.score(&source, &source_objective) else {
519 return Ok(false);
520 };
521 Ok(comparison.matches(target_score, source_score))
522}
523
524fn score_range_matches(
525 context: &SteelCommandContext<CommandSource>,
526) -> Result<bool, CommandSyntaxError> {
527 let scoreboard = source_scoreboard(context)?;
528 let target = context.score_holder("target")?;
529 let target_objective = objective(context, scoreboard, "targetObjective")?;
530 let range = context.int_range("range")?;
531 Ok(scoreboard
532 .score(&target, &target_objective)
533 .is_some_and(|score| range.matches(score)))
534}
535
536#[derive(Clone, Copy)]
537enum ScoreComparison {
538 Equal,
539 Less,
540 LessOrEqual,
541 Greater,
542 GreaterOrEqual,
543}
544
545impl ScoreComparison {
546 const fn matches(self, target: i32, source: i32) -> bool {
547 match self {
548 Self::Equal => target == source,
549 Self::Less => target < source,
550 Self::LessOrEqual => target <= source,
551 Self::Greater => target > source,
552 Self::GreaterOrEqual => target >= source,
553 }
554 }
555}
556
557fn conditional_sources(
558 source: &CommandSource,
559 expected: bool,
560 matches: bool,
561) -> Vec<CommandSource> {
562 if matches == expected {
563 vec![source.clone()]
564 } else {
565 Vec::new()
566 }
567}
568
569fn execute_boolean_condition(
570 context: &SteelCommandContext<CommandSource>,
571 expected: bool,
572 matches: bool,
573) -> Result<i32, CommandSyntaxError> {
574 if matches != expected {
575 return Err(conditional_failed());
576 }
577 context.source().send_success(
578 &TextComponent::from(&translations::COMMANDS_EXECUTE_CONDITIONAL_PASS),
579 false,
580 );
581 Ok(1)
582}
583
584fn execute_numeric_condition(
585 context: &SteelCommandContext<CommandSource>,
586 expected: bool,
587 count: i32,
588) -> Result<i32, CommandSyntaxError> {
589 if expected {
590 if count == 0 {
591 return Err(conditional_failed());
592 }
593 let message = translations::COMMANDS_EXECUTE_CONDITIONAL_PASS_COUNT
594 .message([TextComponent::from(count.to_string())])
595 .component();
596 context.source().send_success(&message, false);
597 return Ok(count);
598 }
599
600 if count != 0 {
601 return Err(conditional_failed_count(count));
602 }
603 context.source().send_success(
604 &TextComponent::from(&translations::COMMANDS_EXECUTE_CONDITIONAL_PASS),
605 false,
606 );
607 Ok(1)
608}
609
610fn conditional_failed() -> CommandSyntaxError {
611 CommandSyntaxError::dynamic(TextComponent::from(
612 &translations::COMMANDS_EXECUTE_CONDITIONAL_FAIL,
613 ))
614}
615
616fn conditional_failed_count(count: i32) -> CommandSyntaxError {
617 let message = translations::COMMANDS_EXECUTE_CONDITIONAL_FAIL_COUNT
618 .message([TextComponent::from(count.to_string())])
619 .component();
620 CommandSyntaxError::dynamic(message)
621}
622
623#[cfg(test)]
624mod tests {
625 use std::sync::Weak;
626
627 use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
628 use steel_registry::{init_vanilla_registry, vanilla_block_entity_types, vanilla_blocks};
629 use steel_utils::nbt::parse_nbt_path;
630
631 use super::*;
632 use crate::block_entity::entities::UnimplementedBlockEntity;
633
634 fn unimplemented_block_entity(
635 value: i32,
636 pos: BlockPos,
637 reverse_order: bool,
638 ) -> SharedBlockEntity {
639 let mut data = NbtCompound::new();
640 if reverse_order {
641 data.insert("other", 11_i32);
642 data.insert("value", value);
643 } else {
644 data.insert("value", value);
645 data.insert("other", 11_i32);
646 }
647 data.insert("x", pos.x());
648 Arc::new(UnimplementedBlockEntity::with_data(
649 &vanilla_block_entity_types::BARREL,
650 Weak::new(),
651 pos,
652 vanilla_blocks::BARREL.default_state(),
653 data,
654 ))
655 }
656
657 #[test]
658 fn block_region_volume_uses_inclusive_normalized_corners() {
659 let region = BoundingBox::from_corners(BlockPos::new(2, 5, -1), BlockPos::new(-1, 3, 2));
660
661 assert_eq!(block_region_volume(®ion), 48);
662 }
663
664 #[test]
665 fn data_match_count_returns_selected_tag_count() {
666 let path = parse_nbt_path("items[].value").expect("path should parse");
667 let mut first = NbtCompound::new();
668 first.insert("value", 1);
669 let mut second = NbtCompound::new();
670 second.insert("value", 2);
671 let mut root = NbtCompound::new();
672 root.insert("items", NbtList::Compound(vec![first, second]));
673 let tag = NbtTag::Compound(root);
674
675 assert_eq!(
676 matching_data_count(&path, &tag).expect("count should fit"),
677 2
678 );
679 }
680
681 #[test]
682 fn masked_regions_skip_only_vanilla_air() {
683 init_vanilla_registry();
684
685 assert!(!should_compare_block(
686 vanilla_blocks::AIR.default_state(),
687 true
688 ));
689 assert!(should_compare_block(
690 vanilla_blocks::CAVE_AIR.default_state(),
691 true
692 ));
693 assert!(should_compare_block(
694 vanilla_blocks::VOID_AIR.default_state(),
695 true
696 ));
697 assert!(should_compare_block(
698 vanilla_blocks::AIR.default_state(),
699 false
700 ));
701 }
702
703 #[test]
704 fn region_block_entities_compare_type_and_custom_data_only() {
705 init_vanilla_registry();
706 let source = unimplemented_block_entity(7, BlockPos::new(1, 64, 1), false);
707 let matching = unimplemented_block_entity(7, BlockPos::new(4, 70, 4), true);
708 let different = unimplemented_block_entity(8, BlockPos::new(4, 70, 4), false);
709
710 assert!(block_entity_data_matches(Some(&source), Some(&source)));
711 assert!(block_entity_data_matches(Some(&source), Some(&matching)));
712 assert!(!block_entity_data_matches(Some(&source), Some(&different)));
713 assert!(!block_entity_data_matches(Some(&source), None));
714 assert!(block_entity_data_matches(None, Some(&matching)));
715 }
716}