1use crate::error::{BedrockWorldError, Result};
4use crate::nbt::{NbtTag, serialize_root_nbt};
5use crate::parsed::{
6 ParsedBlockEntity, ParsedChunkRecordValue, ParsedEntity, ParsedHardcodedSpawnArea,
7 ParsedVillageData, WorldParseOptions, parse_actor_digest_ids,
8};
9use crate::world::{BedrockWorld, ChunkBounds, SurfaceColumnOptions, WorldStorageHandle};
10use crate::{
11 ActorDigestKey, BlockPos, CancelFlag, Chunk, ChunkKey, ChunkPos, ChunkRecord, ChunkRecordTag,
12 Dimension, StorageReadOptions, SurfaceColumn, WorldChunkQueryRegion,
13};
14use bytes::Bytes;
15use serde::{Deserialize, Serialize};
16use std::cmp::Reverse;
17use std::collections::BTreeSet;
18use std::path::PathBuf;
19use xxhash_rust::xxh3::Xxh3;
20
21const MT_N: usize = 624;
22const MT_M: usize = 397;
23const MT_MATRIX_A: u32 = 0x9908_b0df;
24const MT_UPPER_MASK: u32 = 0x8000_0000;
25const MT_LOWER_MASK: u32 = 0x7fff_ffff;
26const WRITE_CONFIRM_TOKEN: &str = "CONFIRMED";
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct ChunkRecordQuery {
31 pub entities: bool,
33 pub block_entities: bool,
35 pub pending_ticks: bool,
37 pub hardcoded_spawn_areas: bool,
39}
40
41impl ChunkRecordQuery {
42 #[must_use]
43 pub fn tags(self) -> Vec<ChunkRecordTag> {
45 [
46 (self.entities, ChunkRecordTag::Entity),
47 (self.block_entities, ChunkRecordTag::BlockEntity),
48 (self.pending_ticks, ChunkRecordTag::PendingTicks),
49 (
50 self.hardcoded_spawn_areas,
51 ChunkRecordTag::HardcodedSpawners,
52 ),
53 ]
54 .into_iter()
55 .filter_map(|(enabled, tag)| enabled.then_some(tag))
56 .collect()
57 }
58}
59
60#[derive(Debug, Clone, PartialEq)]
62pub struct ChunkRecordQueryResult {
63 pub pos: ChunkPos,
65 pub records: Vec<crate::ParsedChunkRecord>,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub struct ChunkRecordFingerprint {
76 pub pos: ChunkPos,
78 pub value: u128,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
84pub struct SlimeChunkBounds {
85 pub dimension: Dimension,
87 pub min_chunk_x: i32,
89 pub max_chunk_x: i32,
91 pub min_chunk_z: i32,
93 pub max_chunk_z: i32,
95}
96
97impl SlimeChunkBounds {
98 pub fn validate(self) -> Result<()> {
100 if self.min_chunk_x > self.max_chunk_x || self.min_chunk_z > self.max_chunk_z {
101 return Err(BedrockWorldError::Validation(format!(
102 "invalid chunk bounds: min=({}, {}) max=({}, {})",
103 self.min_chunk_x, self.min_chunk_z, self.max_chunk_x, self.max_chunk_z
104 )));
105 }
106 Ok(())
107 }
108
109 #[must_use]
110 pub const fn chunk_count(self) -> usize {
112 let width = self.max_chunk_x.saturating_sub(self.min_chunk_x) as usize + 1;
113 let height = self.max_chunk_z.saturating_sub(self.min_chunk_z) as usize + 1;
114 width.saturating_mul(height)
115 }
116
117 #[must_use]
118 pub fn from_chunk_bounds(bounds: ChunkBounds) -> Self {
120 Self {
121 dimension: bounds.dimension,
122 min_chunk_x: bounds.min_chunk_x,
123 max_chunk_x: bounds.max_chunk_x,
124 min_chunk_z: bounds.min_chunk_z,
125 max_chunk_z: bounds.max_chunk_z,
126 }
127 }
128
129 #[must_use]
130 pub const fn center(self) -> (i32, i32) {
132 (
133 i32::midpoint(self.min_chunk_x, self.max_chunk_x),
134 i32::midpoint(self.min_chunk_z, self.max_chunk_z),
135 )
136 }
137}
138
139impl From<WorldChunkQueryRegion> for SlimeChunkBounds {
140 fn from(region: WorldChunkQueryRegion) -> Self {
141 Self {
142 dimension: region.dimension,
143 min_chunk_x: region.min_chunk_x,
144 max_chunk_x: region.max_chunk_x,
145 min_chunk_z: region.min_chunk_z,
146 max_chunk_z: region.max_chunk_z,
147 }
148 }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
153pub struct SlimeWindowSize(u8);
154
155impl SlimeWindowSize {
156 pub fn new(size: u8) -> Result<Self> {
158 if size == 0 || size.is_multiple_of(2) {
159 return Err(BedrockWorldError::Validation(format!(
160 "slime query window must be a positive odd size, got {size}"
161 )));
162 }
163 Ok(Self(size))
164 }
165
166 #[must_use]
167 pub const fn get(self) -> u8 {
169 self.0
170 }
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
175pub struct SlimeChunkWindow {
176 pub center: ChunkPos,
178 pub min_chunk_x: i32,
180 pub max_chunk_x: i32,
182 pub min_chunk_z: i32,
184 pub max_chunk_z: i32,
186 pub slime_count: usize,
188 pub total_count: usize,
190}
191
192#[derive(Debug, Clone, PartialEq)]
194pub struct ChunkRecordDetail {
195 pub tag: ChunkRecordTag,
197 pub raw_value_len: usize,
199 pub roots: Vec<NbtTag>,
201 pub writable_nbt: bool,
203}
204
205#[derive(Debug, Clone, PartialEq)]
207pub struct ChunkDetail {
208 pub pos: ChunkPos,
210 pub records: Vec<ChunkRecordDetail>,
212}
213
214#[derive(Debug, Clone, PartialEq)]
216pub struct BlockTip {
217 pub block: BlockPos,
219 pub chunk: ChunkPos,
221 pub local_x: u8,
223 pub local_z: u8,
225 pub surface: Option<SurfaceColumn>,
227 pub biome_id: Option<u32>,
229 pub height: Option<i16>,
231 pub is_slime_chunk: bool,
233}
234
235#[derive(Debug, Clone, PartialEq)]
237pub struct EntityOverlay {
238 pub identifier: Option<String>,
240 pub position: [f64; 3],
242 pub chunk: ChunkPos,
244 pub nbt: NbtTag,
246}
247
248#[derive(Debug, Clone, PartialEq)]
250pub struct BlockEntityOverlay {
251 pub id: Option<String>,
253 pub position: [i32; 3],
255 pub chunk: ChunkPos,
257 pub nbt: NbtTag,
259}
260
261#[derive(Debug, Clone, PartialEq)]
263pub struct PendingTickOverlay {
264 pub chunk: ChunkPos,
266 pub tick: NbtTag,
268}
269
270#[derive(Debug, Clone, PartialEq, Eq)]
272pub struct HardcodedSpawnAreaOverlay {
273 pub area: ParsedHardcodedSpawnArea,
275 pub chunk: ChunkPos,
277}
278
279#[derive(Debug, Clone, PartialEq)]
281pub struct VillageOverlay {
282 pub key: crate::ParsedVillageKey,
284 pub bounds: Option<SlimeChunkBounds>,
286 pub root_count: usize,
288 pub raw_len: usize,
290}
291
292#[derive(Debug, Clone, PartialEq)]
294pub struct VillageOverlayIndex {
295 pub villages: Vec<VillageOverlay>,
297}
298
299impl VillageOverlayIndex {
300 pub fn build_blocking_with_control<S>(
302 world: &BedrockWorld<S>,
303 cancel: &CancelFlag,
304 ) -> Result<Self>
305 where
306 S: WorldStorageHandle,
307 {
308 check_query_cancelled(Some(cancel))?;
309 let mut villages = Vec::new();
310 for village in world.scan_villages_lightweight_blocking(cancel)? {
311 check_query_cancelled(Some(cancel))?;
312 villages.push(village_overlay(village));
313 }
314 Ok(Self { villages })
315 }
316
317 #[must_use]
318 pub fn query(&self, bounds: SlimeChunkBounds, max_items: usize) -> Vec<VillageOverlay> {
320 self.villages
321 .iter()
322 .filter(|overlay| {
323 overlay
324 .bounds
325 .is_none_or(|village_bounds| bounds_intersect(bounds, village_bounds))
326 })
327 .take(max_items)
328 .cloned()
329 .collect()
330 }
331}
332
333#[derive(Debug, Clone, PartialEq)]
335pub struct RegionOverlayQuery {
336 pub bounds: SlimeChunkBounds,
338 pub slime_chunks: Vec<ChunkPos>,
340 pub hardcoded_spawn_areas: Vec<HardcodedSpawnAreaOverlay>,
342 pub entities: Vec<EntityOverlay>,
344 pub block_entities: Vec<BlockEntityOverlay>,
346 pub pending_ticks: Vec<PendingTickOverlay>,
348 pub villages: Vec<VillageOverlay>,
350 pub scanned_chunks: usize,
352 pub missing_chunks: usize,
354}
355
356#[derive(Debug, Clone, Copy, PartialEq, Eq)]
358pub struct RegionOverlayQueryOptions {
359 pub include_slime: bool,
361 pub include_hardcoded_spawn_areas: bool,
363 pub include_entities: bool,
365 pub include_block_entities: bool,
367 pub include_pending_ticks: bool,
369 pub include_villages: bool,
371 pub max_chunks: usize,
373 pub max_items_per_kind: usize,
375}
376
377impl Default for RegionOverlayQueryOptions {
378 fn default() -> Self {
379 Self {
380 include_slime: true,
381 include_hardcoded_spawn_areas: true,
382 include_entities: true,
383 include_block_entities: true,
384 include_pending_ticks: true,
385 include_villages: true,
386 max_chunks: 65_536,
387 max_items_per_kind: 10_000,
388 }
389 }
390}
391
392#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
394pub struct SelectionStats {
395 pub bounds: Option<SlimeChunkBounds>,
397 pub chunk_count: usize,
399 pub loaded_chunks: usize,
401 pub missing_chunks: usize,
403 pub slime_chunks: usize,
405 pub entity_count: usize,
407 pub block_entity_count: usize,
409 pub pending_tick_count: usize,
411 pub hardcoded_spawn_area_count: usize,
413 pub village_count: usize,
415}
416
417#[derive(Debug, Clone, PartialEq, Eq)]
419pub struct WriteGuard {
420 world_path: PathBuf,
421 confirmation_token: String,
422 operation: String,
423}
424
425impl WriteGuard {
426 #[must_use]
427 pub fn confirmed(world_path: impl Into<PathBuf>, operation: impl Into<String>) -> Self {
429 Self {
430 world_path: world_path.into(),
431 confirmation_token: WRITE_CONFIRM_TOKEN.to_string(),
432 operation: operation.into(),
433 }
434 }
435
436 pub fn validate<S>(&self, world: &BedrockWorld<S>) -> Result<()>
442 where
443 S: WorldStorageHandle,
444 {
445 if self.confirmation_token != WRITE_CONFIRM_TOKEN || self.operation.trim().is_empty() {
446 return Err(BedrockWorldError::Validation(
447 "write guard is not confirmed".to_string(),
448 ));
449 }
450 if self.world_path != world.path() {
451 return Err(BedrockWorldError::Validation(format!(
452 "write guard world path does not match: guard={} world={}",
453 self.world_path.display(),
454 world.path().display()
455 )));
456 }
457 Ok(())
458 }
459}
460
461#[must_use]
462pub fn is_bedrock_slime_chunk(chunk_x: i32, chunk_z: i32) -> bool {
464 let seed = (chunk_x as u32).wrapping_mul(0x1f1f_1f1f) ^ (chunk_z as u32);
465 mt19937_first_u32(seed).is_multiple_of(10)
466}
467
468#[must_use]
469pub fn is_slime_chunk(pos: ChunkPos) -> bool {
471 pos.dimension == Dimension::Overworld && is_bedrock_slime_chunk(pos.x, pos.z)
472}
473
474pub fn query_slime_chunk_windows(
476 bounds: SlimeChunkBounds,
477 window_size: SlimeWindowSize,
478 max_results: usize,
479) -> Result<Vec<SlimeChunkWindow>> {
480 bounds.validate()?;
481 if bounds.dimension != Dimension::Overworld || max_results == 0 {
482 return Ok(Vec::new());
483 }
484 let width = bounds.max_chunk_x.saturating_sub(bounds.min_chunk_x) as usize + 1;
485 let height = bounds.max_chunk_z.saturating_sub(bounds.min_chunk_z) as usize + 1;
486 let size = usize::from(window_size.get());
487 if width < size || height < size {
488 return Ok(Vec::new());
489 }
490 let mut prefix = vec![0usize; (width + 1) * (height + 1)];
491 for z in 0..height {
492 let chunk_z = bounds
493 .min_chunk_z
494 .saturating_add(i32::try_from(z).unwrap_or(i32::MAX));
495 for x in 0..width {
496 let chunk_x = bounds
497 .min_chunk_x
498 .saturating_add(i32::try_from(x).unwrap_or(i32::MAX));
499 let value = usize::from(is_bedrock_slime_chunk(chunk_x, chunk_z));
500 let index = (z + 1) * (width + 1) + (x + 1);
501 prefix[index] =
502 value + prefix[z * (width + 1) + (x + 1)] + prefix[(z + 1) * (width + 1) + x]
503 - prefix[z * (width + 1) + x];
504 }
505 }
506 let (center_x, center_z) = bounds.center();
507 let mut windows = Vec::with_capacity((width - size + 1).saturating_mul(height - size + 1));
508 for z in 0..=(height - size) {
509 for x in 0..=(width - size) {
510 let x2 = x + size;
511 let z2 = z + size;
512 let count = prefix[z2 * (width + 1) + x2] + prefix[z * (width + 1) + x]
513 - prefix[z * (width + 1) + x2]
514 - prefix[z2 * (width + 1) + x];
515 let min_chunk_x = bounds.min_chunk_x + i32::try_from(x).unwrap_or(i32::MAX);
516 let min_chunk_z = bounds.min_chunk_z + i32::try_from(z).unwrap_or(i32::MAX);
517 let max_chunk_x = min_chunk_x + i32::from(window_size.get()) - 1;
518 let max_chunk_z = min_chunk_z + i32::from(window_size.get()) - 1;
519 windows.push(SlimeChunkWindow {
520 center: ChunkPos {
521 x: i32::midpoint(min_chunk_x, max_chunk_x),
522 z: i32::midpoint(min_chunk_z, max_chunk_z),
523 dimension: bounds.dimension,
524 },
525 min_chunk_x,
526 max_chunk_x,
527 min_chunk_z,
528 max_chunk_z,
529 slime_count: count,
530 total_count: size * size,
531 });
532 }
533 }
534 windows.sort_by_key(|window| {
535 let dx = i64::from(window.center.x) - i64::from(center_x);
536 let dz = i64::from(window.center.z) - i64::from(center_z);
537 (
538 Reverse(window.slime_count),
539 dx.saturating_mul(dx).saturating_add(dz.saturating_mul(dz)),
540 window.center.z,
541 window.center.x,
542 )
543 });
544 windows.truncate(max_results);
545 Ok(windows)
546}
547
548pub fn query_block_tip_blocking<S>(
550 world: &BedrockWorld<S>,
551 block: BlockPos,
552 dimension: Dimension,
553) -> Result<BlockTip>
554where
555 S: WorldStorageHandle,
556{
557 let chunk = block.to_chunk_pos(dimension);
558 let (local_x, _, local_z) = block.in_chunk_offset();
559 let surface = world.get_surface_column_blocking(
560 chunk,
561 local_x,
562 local_z,
563 SurfaceColumnOptions::default(),
564 )?;
565 let height = world.get_height_at_blocking(chunk, local_x, local_z)?;
566 let biome_y = surface.as_ref().map_or(block.y, |surface| surface.y);
567 let biome_id = world.get_biome_id_blocking(chunk, local_x, local_z, biome_y)?;
568 Ok(BlockTip {
569 block,
570 chunk,
571 local_x,
572 local_z,
573 surface,
574 biome_id,
575 height,
576 is_slime_chunk: is_slime_chunk(chunk),
577 })
578}
579
580pub fn query_chunk_detail_blocking<S>(world: &BedrockWorld<S>, pos: ChunkPos) -> Result<ChunkDetail>
582where
583 S: WorldStorageHandle,
584{
585 let chunk = world.get_chunk_blocking(pos)?;
586 let mut records = Vec::with_capacity(chunk.records.len());
587 for record in chunk.records {
588 let roots = parse_record_roots(record.key.tag, &record.value);
589 records.push(ChunkRecordDetail {
590 tag: record.key.tag,
591 raw_value_len: record.value.len(),
592 writable_nbt: record_tag_accepts_nbt_write(record.key.tag),
593 roots,
594 });
595 }
596 Ok(ChunkDetail { pos, records })
597}
598
599pub fn query_chunk_records_many_blocking<S>(
601 world: &BedrockWorld<S>,
602 positions: impl IntoIterator<Item = ChunkPos>,
603 query: ChunkRecordQuery,
604) -> Result<Vec<ChunkRecordQueryResult>>
605where
606 S: WorldStorageHandle,
607{
608 query_chunk_records_many_blocking_inner(world, positions, query, None, None)
609}
610
611pub fn query_chunk_records_many_blocking_with_control<S>(
613 world: &BedrockWorld<S>,
614 positions: impl IntoIterator<Item = ChunkPos>,
615 query: ChunkRecordQuery,
616 cancel: &CancelFlag,
617) -> Result<Vec<ChunkRecordQueryResult>>
618where
619 S: WorldStorageHandle,
620{
621 query_chunk_records_many_blocking_inner(world, positions, query, Some(cancel), None)
622}
623
624pub fn fingerprint_chunk_records_many_blocking<S>(
630 world: &BedrockWorld<S>,
631 positions: impl IntoIterator<Item = ChunkPos>,
632 query: ChunkRecordQuery,
633) -> Result<Vec<ChunkRecordFingerprint>>
634where
635 S: WorldStorageHandle,
636{
637 fingerprint_chunk_records_many_blocking_inner(world, positions, query, None)
638}
639
640pub fn fingerprint_chunk_records_many_blocking_with_control<S>(
642 world: &BedrockWorld<S>,
643 positions: impl IntoIterator<Item = ChunkPos>,
644 query: ChunkRecordQuery,
645 cancel: &CancelFlag,
646) -> Result<Vec<ChunkRecordFingerprint>>
647where
648 S: WorldStorageHandle,
649{
650 fingerprint_chunk_records_many_blocking_inner(world, positions, query, Some(cancel))
651}
652
653fn fingerprint_chunk_records_many_blocking_inner<S>(
654 world: &BedrockWorld<S>,
655 positions: impl IntoIterator<Item = ChunkPos>,
656 query: ChunkRecordQuery,
657 cancel: Option<&CancelFlag>,
658) -> Result<Vec<ChunkRecordFingerprint>>
659where
660 S: WorldStorageHandle,
661{
662 check_query_cancelled(cancel)?;
663 let positions = positions.into_iter().collect::<Vec<_>>();
664 let tags = query.tags();
665 if positions.is_empty() {
666 return Ok(Vec::new());
667 }
668 let mut hashers = positions
669 .iter()
670 .map(|pos| {
671 let mut hasher = Xxh3::new();
672 hasher.update(&pos.x.to_le_bytes());
673 hasher.update(&pos.z.to_le_bytes());
674 hasher.update(&pos.dimension.id().to_le_bytes());
675 hasher
676 })
677 .collect::<Vec<_>>();
678 if tags.is_empty() {
679 return Ok(positions
680 .into_iter()
681 .zip(hashers)
682 .map(|(pos, hasher)| ChunkRecordFingerprint {
683 pos,
684 value: hasher.digest128(),
685 })
686 .collect());
687 }
688 let (keys, owners) = fingerprint_record_requests(&positions, &tags, query.entities);
689 let values = world
690 .storage()
691 .get_many_ordered_with_control(&keys, record_query_read_options(cancel))?;
692 let mut actor_ids_by_chunk = vec![Vec::new(); positions.len()];
693 for ((owner, key), value) in owners.into_iter().zip(keys).zip(values) {
694 match owner {
695 BatchedRecordOwner::ChunkRecord { chunk_index, tag } => {
696 if let Some(hasher) = hashers.get_mut(chunk_index) {
697 hash_record(hasher, &key, value.as_deref(), Some(tag));
698 }
699 }
700 BatchedRecordOwner::ActorDigest { chunk_index } => {
701 if let Some(hasher) = hashers.get_mut(chunk_index) {
702 hash_record(hasher, &key, value.as_deref(), None);
703 }
704 if let Some(value) = value {
705 if let Some(actor_ids) = actor_ids_by_chunk.get_mut(chunk_index) {
706 actor_ids.extend(parse_actor_digest_ids(&value)?);
707 }
708 }
709 }
710 }
711 }
712 let mut actor_ids = actor_ids_by_chunk
713 .iter()
714 .flatten()
715 .copied()
716 .collect::<Vec<_>>();
717 actor_ids.sort_unstable();
718 actor_ids.dedup();
719 if !actor_ids.is_empty() {
720 check_query_cancelled(cancel)?;
721 let actor_keys = actor_ids
722 .iter()
723 .map(|actor_id| actor_id.storage_key())
724 .collect::<Vec<_>>();
725 let actor_values = world
726 .storage()
727 .get_many_ordered_with_control(&actor_keys, record_query_read_options(cancel))?;
728 for (hasher, chunk_actor_ids) in hashers.iter_mut().zip(actor_ids_by_chunk) {
729 for actor_id in chunk_actor_ids {
730 check_query_cancelled(cancel)?;
731 let actor_index = actor_ids.binary_search(&actor_id).map_err(|_| {
732 BedrockWorldError::Validation("actor digest id was not indexed".to_string())
733 })?;
734 let actor_key = actor_keys.get(actor_index).ok_or_else(|| {
735 BedrockWorldError::Validation("actor key index was missing".to_string())
736 })?;
737 let actor_value = actor_values.get(actor_index).and_then(Option::as_deref);
738 hash_record(hasher, actor_key, actor_value, None);
739 }
740 }
741 }
742 Ok(positions
743 .into_iter()
744 .zip(hashers)
745 .map(|(pos, hasher)| ChunkRecordFingerprint {
746 pos,
747 value: hasher.digest128(),
748 })
749 .collect())
750}
751
752fn fingerprint_record_requests(
753 positions: &[ChunkPos],
754 tags: &[ChunkRecordTag],
755 include_entities: bool,
756) -> (Vec<Bytes>, Vec<BatchedRecordOwner>) {
757 let capacity = positions
758 .len()
759 .saturating_mul(tags.len().saturating_add(usize::from(include_entities)));
760 let mut keys = Vec::with_capacity(capacity);
761 let mut owners = Vec::with_capacity(capacity);
762 for (chunk_index, pos) in positions.iter().copied().enumerate() {
763 for tag in tags {
764 keys.push(ChunkKey::new(pos, *tag).encode());
765 owners.push(BatchedRecordOwner::ChunkRecord {
766 chunk_index,
767 tag: *tag,
768 });
769 }
770 if include_entities {
771 keys.push(ActorDigestKey::new(pos).storage_key());
772 owners.push(BatchedRecordOwner::ActorDigest { chunk_index });
773 }
774 }
775 (keys, owners)
776}
777
778fn hash_record(hasher: &mut Xxh3, key: &[u8], value: Option<&[u8]>, tag: Option<ChunkRecordTag>) {
779 hasher.update(&[tag.map_or(0xff, ChunkRecordTag::byte)]);
780 hasher.update(&(key.len() as u32).to_le_bytes());
781 hasher.update(key);
782 match value {
783 Some(value) => {
784 hasher.update(&[1]);
785 hasher.update(&(value.len() as u64).to_le_bytes());
786 hasher.update(value);
787 }
788 None => hasher.update(&[0]),
789 }
790}
791
792fn query_chunk_records_many_blocking_inner<S>(
793 world: &BedrockWorld<S>,
794 positions: impl IntoIterator<Item = ChunkPos>,
795 query: ChunkRecordQuery,
796 cancel: Option<&CancelFlag>,
797 max_actor_records: Option<usize>,
798) -> Result<Vec<ChunkRecordQueryResult>>
799where
800 S: WorldStorageHandle,
801{
802 check_query_cancelled(cancel)?;
803 let positions = positions.into_iter().collect::<Vec<_>>();
804 let tags = query.tags();
805 if positions.is_empty() || tags.is_empty() {
806 return Ok(positions
807 .into_iter()
808 .map(|pos| ChunkRecordQueryResult {
809 pos,
810 records: Vec::new(),
811 })
812 .collect());
813 }
814 let mut keys = Vec::with_capacity(
815 positions
816 .len()
817 .saturating_mul(tags.len().saturating_add(usize::from(query.entities))),
818 );
819 let mut owners = Vec::with_capacity(keys.capacity());
820 for (chunk_index, pos) in positions.iter().copied().enumerate() {
821 for tag in &tags {
822 keys.push(ChunkKey::new(pos, *tag).encode());
823 owners.push(BatchedRecordOwner::ChunkRecord {
824 chunk_index,
825 tag: *tag,
826 });
827 }
828 if query.entities {
829 keys.push(ActorDigestKey::new(pos).storage_key());
830 owners.push(BatchedRecordOwner::ActorDigest { chunk_index });
831 }
832 }
833 let values = world
834 .storage()
835 .get_many_ordered_with_control(&keys, record_query_read_options(cancel))?;
836 let mut chunks = positions
837 .iter()
838 .copied()
839 .map(|pos| Chunk {
840 pos,
841 version: None,
842 records: Vec::new(),
843 })
844 .collect::<Vec<_>>();
845 let mut actor_ids_by_chunk = vec![Vec::new(); chunks.len()];
846 for (owner, value) in owners.into_iter().zip(values) {
847 let Some(value) = value else {
848 continue;
849 };
850 match owner {
851 BatchedRecordOwner::ChunkRecord { chunk_index, tag } => {
852 if let Some(chunk) = chunks.get_mut(chunk_index) {
853 chunk.records.push(ChunkRecord {
854 key: ChunkKey::new(chunk.pos, tag),
855 value,
856 });
857 }
858 }
859 BatchedRecordOwner::ActorDigest { chunk_index } => {
860 if let Some(actor_ids) = actor_ids_by_chunk.get_mut(chunk_index) {
861 actor_ids.extend(parse_actor_digest_ids(&value)?);
862 }
863 }
864 }
865 }
866 append_referenced_actor_records(
867 world,
868 &mut chunks,
869 &actor_ids_by_chunk,
870 cancel,
871 max_actor_records,
872 )?;
873 Ok(chunks
874 .into_iter()
875 .map(|chunk| {
876 let parsed = crate::parsed::parse_chunk_records_with_options(
877 chunk.pos,
878 chunk.records,
879 WorldParseOptions::structured(),
880 );
881 ChunkRecordQueryResult {
882 pos: parsed.pos,
883 records: parsed.records,
884 }
885 })
886 .collect())
887}
888
889#[derive(Clone, Copy)]
890enum BatchedRecordOwner {
891 ChunkRecord {
892 chunk_index: usize,
893 tag: ChunkRecordTag,
894 },
895 ActorDigest {
896 chunk_index: usize,
897 },
898}
899
900fn record_query_read_options(cancel: Option<&CancelFlag>) -> StorageReadOptions {
901 StorageReadOptions {
902 cache_policy: crate::StorageCachePolicy::Use,
903 cancel: cancel.map(CancelFlag::to_storage_cancel),
904 ..StorageReadOptions::default()
905 }
906}
907
908fn append_referenced_actor_records<S>(
909 world: &BedrockWorld<S>,
910 chunks: &mut [Chunk],
911 actor_ids_by_chunk: &[Vec<crate::ActorUid>],
912 cancel: Option<&CancelFlag>,
913 max_actor_records: Option<usize>,
914) -> Result<()>
915where
916 S: WorldStorageHandle,
917{
918 let mut unique_actor_ids = actor_ids_by_chunk
919 .iter()
920 .flatten()
921 .copied()
922 .collect::<Vec<_>>();
923 unique_actor_ids.sort_unstable();
924 unique_actor_ids.dedup();
925 if let Some(max_actor_records) = max_actor_records {
926 unique_actor_ids.truncate(max_actor_records);
927 }
928 if unique_actor_ids.is_empty() {
929 return Ok(());
930 }
931 check_query_cancelled(cancel)?;
932 let actor_keys = unique_actor_ids
933 .iter()
934 .map(|actor_id| actor_id.storage_key())
935 .collect::<Vec<_>>();
936 let actor_values = world
937 .storage()
938 .get_many_ordered_with_control(&actor_keys, record_query_read_options(cancel))?;
939 for (chunk, chunk_actor_ids) in chunks.iter_mut().zip(actor_ids_by_chunk) {
940 for actor_id in chunk_actor_ids {
941 let Ok(actor_index) = unique_actor_ids.binary_search(actor_id) else {
942 continue;
943 };
944 let Some(Some(value)) = actor_values.get(actor_index) else {
945 continue;
946 };
947 chunk.records.push(ChunkRecord {
948 key: ChunkKey::new(chunk.pos, ChunkRecordTag::Entity),
949 value: value.clone(),
950 });
951 }
952 }
953 Ok(())
954}
955
956pub fn query_region_overlays_blocking<S>(
958 world: &BedrockWorld<S>,
959 bounds: SlimeChunkBounds,
960 options: RegionOverlayQueryOptions,
961) -> Result<RegionOverlayQuery>
962where
963 S: WorldStorageHandle,
964{
965 query_region_overlays_blocking_inner(world, bounds, options, None)
966}
967
968pub fn query_region_overlays_blocking_with_control<S>(
970 world: &BedrockWorld<S>,
971 bounds: SlimeChunkBounds,
972 options: RegionOverlayQueryOptions,
973 cancel: &CancelFlag,
974) -> Result<RegionOverlayQuery>
975where
976 S: WorldStorageHandle,
977{
978 query_region_overlays_blocking_inner(world, bounds, options, Some(cancel))
979}
980
981fn query_region_overlays_blocking_inner<S>(
982 world: &BedrockWorld<S>,
983 bounds: SlimeChunkBounds,
984 options: RegionOverlayQueryOptions,
985 cancel: Option<&CancelFlag>,
986) -> Result<RegionOverlayQuery>
987where
988 S: WorldStorageHandle,
989{
990 bounds.validate()?;
991 if bounds.chunk_count() > options.max_chunks {
992 return Err(BedrockWorldError::Validation(format!(
993 "query covers {} chunks, limit is {}",
994 bounds.chunk_count(),
995 options.max_chunks
996 )));
997 }
998 let mut result = RegionOverlayQuery {
999 bounds,
1000 slime_chunks: Vec::new(),
1001 hardcoded_spawn_areas: Vec::new(),
1002 entities: Vec::new(),
1003 block_entities: Vec::new(),
1004 pending_ticks: Vec::new(),
1005 villages: Vec::new(),
1006 scanned_chunks: 0,
1007 missing_chunks: 0,
1008 };
1009 let needs_chunk_records = overlay_options_need_chunk_records(options);
1010 let mut positions = Vec::with_capacity(bounds.chunk_count());
1011 for chunk_z in bounds.min_chunk_z..=bounds.max_chunk_z {
1012 check_query_cancelled(cancel)?;
1013 for chunk_x in bounds.min_chunk_x..=bounds.max_chunk_x {
1014 check_query_cancelled(cancel)?;
1015 let pos = ChunkPos {
1016 x: chunk_x,
1017 z: chunk_z,
1018 dimension: bounds.dimension,
1019 };
1020 if options.include_slime && is_slime_chunk(pos) {
1021 result.slime_chunks.push(pos);
1022 }
1023 if needs_chunk_records {
1024 positions.push(pos);
1025 }
1026 }
1027 }
1028 if needs_chunk_records {
1029 let records = query_chunk_records_many_blocking_inner(
1030 world,
1031 positions,
1032 overlay_record_query(options),
1033 cancel,
1034 Some(options.max_items_per_kind),
1035 )?;
1036 for parsed in records {
1037 check_query_cancelled(cancel)?;
1038 if parsed.records.is_empty() {
1039 result.missing_chunks = result.missing_chunks.saturating_add(1);
1040 continue;
1041 }
1042 result.scanned_chunks = result.scanned_chunks.saturating_add(1);
1043 append_overlay_records(&mut result, parsed.pos, parsed.records, options);
1044 }
1045 }
1046 if options.include_villages {
1047 check_query_cancelled(cancel)?;
1048 let village_cancel = cancel.cloned().unwrap_or_default();
1049 let index = VillageOverlayIndex::build_blocking_with_control(world, &village_cancel)?;
1050 result.villages = index.query(bounds, options.max_items_per_kind);
1051 }
1052 Ok(result)
1053}
1054
1055fn overlay_options_need_chunk_records(options: RegionOverlayQueryOptions) -> bool {
1056 options.include_hardcoded_spawn_areas
1057 || options.include_entities
1058 || options.include_block_entities
1059 || options.include_pending_ticks
1060}
1061
1062fn overlay_record_query(options: RegionOverlayQueryOptions) -> ChunkRecordQuery {
1063 ChunkRecordQuery {
1064 entities: options.include_entities,
1065 block_entities: options.include_block_entities,
1066 pending_ticks: options.include_pending_ticks,
1067 hardcoded_spawn_areas: options.include_hardcoded_spawn_areas,
1068 }
1069}
1070
1071fn append_overlay_records(
1072 result: &mut RegionOverlayQuery,
1073 pos: ChunkPos,
1074 records: Vec<crate::ParsedChunkRecord>,
1075 options: RegionOverlayQueryOptions,
1076) {
1077 for record in records {
1078 match record.value {
1079 ParsedChunkRecordValue::HardcodedSpawnAreas(areas)
1080 if options.include_hardcoded_spawn_areas =>
1081 {
1082 for area in areas {
1083 if result.hardcoded_spawn_areas.len() >= options.max_items_per_kind {
1084 break;
1085 }
1086 result
1087 .hardcoded_spawn_areas
1088 .push(HardcodedSpawnAreaOverlay { area, chunk: pos });
1089 }
1090 }
1091 ParsedChunkRecordValue::Entities(entities) if options.include_entities => {
1092 push_entities(
1093 &mut result.entities,
1094 entities,
1095 pos,
1096 options.max_items_per_kind,
1097 );
1098 }
1099 ParsedChunkRecordValue::BlockEntities(block_entities)
1100 if options.include_block_entities =>
1101 {
1102 push_block_entities(
1103 &mut result.block_entities,
1104 block_entities,
1105 pos,
1106 options.max_items_per_kind,
1107 );
1108 }
1109 ParsedChunkRecordValue::PendingTicks(ticks) if options.include_pending_ticks => {
1110 for tick in ticks {
1111 if result.pending_ticks.len() >= options.max_items_per_kind {
1112 break;
1113 }
1114 result
1115 .pending_ticks
1116 .push(PendingTickOverlay { chunk: pos, tick });
1117 }
1118 }
1119 _ => {}
1120 }
1121 }
1122}
1123
1124fn check_query_cancelled(cancel: Option<&CancelFlag>) -> Result<()> {
1125 if cancel.is_some_and(CancelFlag::is_cancelled) {
1126 return Err(BedrockWorldError::Cancelled {
1127 operation: "region overlay query",
1128 });
1129 }
1130 Ok(())
1131}
1132
1133pub fn query_selection_stats_blocking<S>(
1135 world: &BedrockWorld<S>,
1136 bounds: SlimeChunkBounds,
1137 options: RegionOverlayQueryOptions,
1138) -> Result<SelectionStats>
1139where
1140 S: WorldStorageHandle,
1141{
1142 let overlays = query_region_overlays_blocking(world, bounds, options)?;
1143 Ok(SelectionStats {
1144 bounds: Some(bounds),
1145 chunk_count: bounds.chunk_count(),
1146 loaded_chunks: overlays.scanned_chunks,
1147 missing_chunks: overlays.missing_chunks,
1148 slime_chunks: overlays.slime_chunks.len(),
1149 entity_count: overlays.entities.len(),
1150 block_entity_count: overlays.block_entities.len(),
1151 pending_tick_count: overlays.pending_ticks.len(),
1152 hardcoded_spawn_area_count: overlays.hardcoded_spawn_areas.len(),
1153 village_count: overlays.villages.len(),
1154 })
1155}
1156
1157pub fn delete_chunks_blocking<S>(
1159 world: &BedrockWorld<S>,
1160 bounds: SlimeChunkBounds,
1161 guard: &WriteGuard,
1162) -> Result<usize>
1163where
1164 S: WorldStorageHandle,
1165{
1166 bounds.validate()?;
1167 guard.validate(world)?;
1168 let mut deleted = 0usize;
1169 let mut transaction = world.transaction();
1170 for chunk_z in bounds.min_chunk_z..=bounds.max_chunk_z {
1171 for chunk_x in bounds.min_chunk_x..=bounds.max_chunk_x {
1172 let pos = ChunkPos {
1173 x: chunk_x,
1174 z: chunk_z,
1175 dimension: bounds.dimension,
1176 };
1177 deleted = deleted.saturating_add(transaction.delete_chunk(pos)?);
1178 }
1179 }
1180 transaction.commit()?;
1181 Ok(deleted)
1182}
1183
1184pub fn delete_chunk_positions_blocking<S, I>(
1192 world: &BedrockWorld<S>,
1193 positions: I,
1194 guard: &WriteGuard,
1195) -> Result<usize>
1196where
1197 S: WorldStorageHandle,
1198 I: IntoIterator<Item = ChunkPos>,
1199{
1200 guard.validate(world)?;
1201 let mut deleted = 0usize;
1202 let mut transaction = world.transaction();
1203 for pos in positions.into_iter().collect::<BTreeSet<_>>() {
1204 deleted = deleted.saturating_add(transaction.delete_chunk(pos)?);
1205 }
1206 transaction.commit()?;
1207 Ok(deleted)
1208}
1209
1210pub fn write_chunk_record_nbt_blocking<S>(
1212 world: &BedrockWorld<S>,
1213 pos: ChunkPos,
1214 record_kind: ChunkRecordTag,
1215 tag: &NbtTag,
1216 guard: &WriteGuard,
1217) -> Result<()>
1218where
1219 S: WorldStorageHandle,
1220{
1221 guard.validate(world)?;
1222 if !record_tag_accepts_nbt_write(record_kind) {
1223 return Err(BedrockWorldError::Validation(format!(
1224 "chunk record {record_kind:?} does not support NBT writes"
1225 )));
1226 }
1227 let bytes = serialize_record_nbt(tag)?;
1228 world.put_raw_record_blocking(&crate::ChunkKey::new(pos, record_kind), &bytes)
1229}
1230
1231fn push_entities(
1232 target: &mut Vec<EntityOverlay>,
1233 entities: Vec<ParsedEntity>,
1234 fallback_chunk: ChunkPos,
1235 limit: usize,
1236) {
1237 for entity in entities {
1238 if target.len() >= limit {
1239 break;
1240 }
1241 let Some(position) = entity.position else {
1242 continue;
1243 };
1244 target.push(EntityOverlay {
1245 identifier: entity.identifier,
1246 chunk: BlockPos {
1247 x: position[0].floor() as i32,
1248 y: position[1].floor() as i32,
1249 z: position[2].floor() as i32,
1250 }
1251 .to_chunk_pos(fallback_chunk.dimension),
1252 position,
1253 nbt: entity.nbt,
1254 });
1255 }
1256}
1257
1258fn push_block_entities(
1259 target: &mut Vec<BlockEntityOverlay>,
1260 block_entities: Vec<ParsedBlockEntity>,
1261 fallback_chunk: ChunkPos,
1262 limit: usize,
1263) {
1264 for block_entity in block_entities {
1265 if target.len() >= limit {
1266 break;
1267 }
1268 let Some(position) = block_entity.position else {
1269 continue;
1270 };
1271 target.push(BlockEntityOverlay {
1272 id: block_entity.id,
1273 chunk: BlockPos {
1274 x: position[0],
1275 y: position[1],
1276 z: position[2],
1277 }
1278 .to_chunk_pos(fallback_chunk.dimension),
1279 position,
1280 nbt: block_entity.nbt,
1281 });
1282 }
1283}
1284
1285fn parse_record_roots(tag: ChunkRecordTag, value: &[u8]) -> Vec<NbtTag> {
1286 match tag {
1287 ChunkRecordTag::BlockEntity | ChunkRecordTag::Entity | ChunkRecordTag::PendingTicks => {
1288 crate::nbt::parse_consecutive_root_nbt(value).unwrap_or_default()
1289 }
1290 _ => Vec::new(),
1291 }
1292}
1293
1294fn record_tag_accepts_nbt_write(tag: ChunkRecordTag) -> bool {
1295 matches!(
1296 tag,
1297 ChunkRecordTag::BlockEntity | ChunkRecordTag::Entity | ChunkRecordTag::PendingTicks
1298 )
1299}
1300
1301fn serialize_record_nbt(tag: &NbtTag) -> Result<Vec<u8>> {
1302 match tag {
1303 NbtTag::List(values) => {
1304 let mut bytes = Vec::new();
1305 for value in values {
1306 bytes.extend(serialize_root_nbt(value)?);
1307 }
1308 Ok(bytes)
1309 }
1310 _ => serialize_root_nbt(tag),
1311 }
1312}
1313
1314fn village_overlay(village: ParsedVillageData) -> VillageOverlay {
1315 let bounds = infer_village_bounds(&village.roots);
1316 VillageOverlay {
1317 key: village.key,
1318 bounds,
1319 root_count: village.roots.len(),
1320 raw_len: village.raw.len(),
1321 }
1322}
1323
1324fn infer_village_bounds(roots: &[NbtTag]) -> Option<SlimeChunkBounds> {
1325 for root in roots {
1326 if let Some(bounds) = infer_bounds_from_tag(root) {
1327 return Some(bounds);
1328 }
1329 }
1330 None
1331}
1332
1333fn infer_bounds_from_tag(tag: &NbtTag) -> Option<SlimeChunkBounds> {
1334 let NbtTag::Compound(map) = tag else {
1335 return None;
1336 };
1337 let min_x = nbt_i32_named(map, &["min_x", "MinX", "x0", "X0", "minBlockX"])?;
1338 let min_z = nbt_i32_named(map, &["min_z", "MinZ", "z0", "Z0", "minBlockZ"])?;
1339 let max_x = nbt_i32_named(map, &["max_x", "MaxX", "x1", "X1", "maxBlockX"])?;
1340 let max_z = nbt_i32_named(map, &["max_z", "MaxZ", "z1", "Z1", "maxBlockZ"])?;
1341 Some(SlimeChunkBounds {
1342 dimension: Dimension::Overworld,
1343 min_chunk_x: min_x.div_euclid(16),
1344 max_chunk_x: max_x.div_euclid(16),
1345 min_chunk_z: min_z.div_euclid(16),
1346 max_chunk_z: max_z.div_euclid(16),
1347 })
1348}
1349
1350fn nbt_i32_named(map: &indexmap::IndexMap<String, NbtTag>, names: &[&str]) -> Option<i32> {
1351 for name in names {
1352 if let Some(value) = map.get(*name).and_then(nbt_i32) {
1353 return Some(value);
1354 }
1355 }
1356 None
1357}
1358
1359fn nbt_i32(tag: &NbtTag) -> Option<i32> {
1360 match tag {
1361 NbtTag::Byte(value) => Some(i32::from(*value)),
1362 NbtTag::Short(value) => Some(i32::from(*value)),
1363 NbtTag::Int(value) => Some(*value),
1364 NbtTag::Long(value) => i32::try_from(*value).ok(),
1365 _ => None,
1366 }
1367}
1368
1369fn bounds_intersect(left: SlimeChunkBounds, right: SlimeChunkBounds) -> bool {
1370 left.dimension == right.dimension
1371 && left.min_chunk_x <= right.max_chunk_x
1372 && left.max_chunk_x >= right.min_chunk_x
1373 && left.min_chunk_z <= right.max_chunk_z
1374 && left.max_chunk_z >= right.min_chunk_z
1375}
1376
1377fn mt19937_first_u32(seed: u32) -> u32 {
1378 let mut mt = [0_u32; MT_N];
1379 mt[0] = seed;
1380 for i in 1..MT_N {
1381 mt[i] = 1_812_433_253_u32
1382 .wrapping_mul(mt[i - 1] ^ (mt[i - 1] >> 30))
1383 .wrapping_add(i as u32);
1384 }
1385 for i in 0..MT_N {
1386 let y = (mt[i] & MT_UPPER_MASK) | (mt[(i + 1) % MT_N] & MT_LOWER_MASK);
1387 mt[i] = mt[(i + MT_M) % MT_N] ^ (y >> 1) ^ if y & 1 == 0 { 0 } else { MT_MATRIX_A };
1388 }
1389 temper(mt[0])
1390}
1391
1392const fn temper(mut value: u32) -> u32 {
1393 value ^= value >> 11;
1394 value ^= (value << 7) & 0x9d2c_5680;
1395 value ^= (value << 15) & 0xefc6_0000;
1396 value ^= value >> 18;
1397 value
1398}
1399
1400#[cfg(test)]
1401mod tests {
1402 use super::*;
1403 use crate::{
1404 ActorDigestKey, ActorUid, ChunkKey, MemoryStorage, OpenOptions, ParsedEntity, WorldStorage,
1405 };
1406 use indexmap::IndexMap;
1407 use std::sync::Arc;
1408
1409 #[test]
1410 fn bedrock_slime_vectors_match_known_pe_results() {
1411 assert!(is_bedrock_slime_chunk(-1, 0));
1412 assert!(is_bedrock_slime_chunk(109, 3));
1413 assert!(!is_bedrock_slime_chunk(0, 0));
1414 assert!(!is_bedrock_slime_chunk(110, 3));
1415 }
1416
1417 #[test]
1418 fn slime_window_query_matches_naive_count() {
1419 let bounds = SlimeChunkBounds {
1420 dimension: Dimension::Overworld,
1421 min_chunk_x: -4,
1422 max_chunk_x: 4,
1423 min_chunk_z: -4,
1424 max_chunk_z: 4,
1425 };
1426 let windows =
1427 query_slime_chunk_windows(bounds, SlimeWindowSize::new(3).unwrap(), 100).unwrap();
1428 for window in windows {
1429 let mut count = 0;
1430 for z in window.min_chunk_z..=window.max_chunk_z {
1431 for x in window.min_chunk_x..=window.max_chunk_x {
1432 count += usize::from(is_bedrock_slime_chunk(x, z));
1433 }
1434 }
1435 assert_eq!(window.slime_count, count);
1436 }
1437 }
1438
1439 #[test]
1440 fn slime_window_query_prefix_sum_keeps_stable_sorting_for_negative_bounds() {
1441 let bounds = SlimeChunkBounds {
1442 dimension: Dimension::Overworld,
1443 min_chunk_x: -12,
1444 max_chunk_x: 7,
1445 min_chunk_z: -9,
1446 max_chunk_z: 8,
1447 };
1448 let windows =
1449 query_slime_chunk_windows(bounds, SlimeWindowSize::new(5).unwrap(), 24).unwrap();
1450 let mut expected = Vec::new();
1451 let center = bounds.center();
1452 for min_z in bounds.min_chunk_z..=bounds.max_chunk_z - 4 {
1453 for min_x in bounds.min_chunk_x..=bounds.max_chunk_x - 4 {
1454 let max_x = min_x + 4;
1455 let max_z = min_z + 4;
1456 let mut count = 0usize;
1457 for z in min_z..=max_z {
1458 for x in min_x..=max_x {
1459 count += usize::from(is_bedrock_slime_chunk(x, z));
1460 }
1461 }
1462 expected.push(SlimeChunkWindow {
1463 center: ChunkPos {
1464 x: i32::midpoint(min_x, max_x),
1465 z: i32::midpoint(min_z, max_z),
1466 dimension: Dimension::Overworld,
1467 },
1468 min_chunk_x: min_x,
1469 max_chunk_x: max_x,
1470 min_chunk_z: min_z,
1471 max_chunk_z: max_z,
1472 slime_count: count,
1473 total_count: 25,
1474 });
1475 }
1476 }
1477 expected.sort_by_key(|window| {
1478 let dx = i64::from(window.center.x) - i64::from(center.0);
1479 let dz = i64::from(window.center.z) - i64::from(center.1);
1480 (
1481 Reverse(window.slime_count),
1482 dx.saturating_mul(dx).saturating_add(dz.saturating_mul(dz)),
1483 window.center.z,
1484 window.center.x,
1485 )
1486 });
1487 expected.truncate(24);
1488
1489 assert_eq!(windows, expected);
1490 }
1491
1492 #[test]
1493 fn overlay_query_respects_cancel_before_scanning_chunks() {
1494 let storage = Arc::new(MemoryStorage::default()) as Arc<dyn crate::WorldStorage>;
1495 let world = BedrockWorld::from_storage(
1496 std::path::PathBuf::from("cancelled"),
1497 storage,
1498 OpenOptions::default(),
1499 );
1500 let cancel = CancelFlag::new();
1501 cancel.cancel();
1502 let bounds = SlimeChunkBounds {
1503 dimension: Dimension::Overworld,
1504 min_chunk_x: -128,
1505 max_chunk_x: 128,
1506 min_chunk_z: -128,
1507 max_chunk_z: 128,
1508 };
1509 let error = query_region_overlays_blocking_with_control(
1510 &world,
1511 bounds,
1512 RegionOverlayQueryOptions {
1513 max_chunks: 100_000,
1514 ..RegionOverlayQueryOptions::default()
1515 },
1516 &cancel,
1517 )
1518 .expect_err("cancelled query should fail");
1519
1520 assert_eq!(error.kind(), crate::BedrockWorldErrorKind::Cancelled);
1521 }
1522
1523 #[test]
1524 fn chunk_record_query_reads_modern_actors_and_pending_ticks_in_order() {
1525 let storage = Arc::new(MemoryStorage::new());
1526 let world = BedrockWorld::from_storage(
1527 "memory",
1528 storage,
1529 OpenOptions {
1530 read_only: false,
1531 ..OpenOptions::default()
1532 },
1533 );
1534 let first = ChunkPos {
1535 x: 2,
1536 z: 3,
1537 dimension: Dimension::Overworld,
1538 };
1539 let second = ChunkPos {
1540 x: 4,
1541 z: 5,
1542 dimension: Dimension::Overworld,
1543 };
1544 let actor = ParsedEntity {
1545 identifier: Some("minecraft:pig".to_string()),
1546 definitions: Vec::new(),
1547 unique_id: Some(77),
1548 position: Some([32.0, 64.0, 48.0]),
1549 rotation: None,
1550 motion: None,
1551 items: Vec::new(),
1552 nbt: NbtTag::Compound(IndexMap::from([
1553 (
1554 "identifier".to_string(),
1555 NbtTag::String("minecraft:pig".to_string()),
1556 ),
1557 ("UniqueID".to_string(), NbtTag::Long(77)),
1558 (
1559 "Pos".to_string(),
1560 NbtTag::List(vec![
1561 NbtTag::Float(32.0),
1562 NbtTag::Float(64.0),
1563 NbtTag::Float(48.0),
1564 ]),
1565 ),
1566 ])),
1567 };
1568 world
1569 .put_actor_blocking(first, &actor)
1570 .expect("write actor");
1571 let pending_tick = NbtTag::Compound(IndexMap::from([("x".to_string(), NbtTag::Int(65))]));
1572 world
1573 .put_raw_record_blocking(
1574 &ChunkKey::new(second, ChunkRecordTag::PendingTicks),
1575 &serialize_root_nbt(&pending_tick).expect("serialize pending tick"),
1576 )
1577 .expect("write pending tick");
1578
1579 let results = query_chunk_records_many_blocking(
1580 &world,
1581 [second, first],
1582 ChunkRecordQuery {
1583 entities: true,
1584 block_entities: false,
1585 pending_ticks: true,
1586 hardcoded_spawn_areas: false,
1587 },
1588 )
1589 .expect("query records");
1590
1591 assert_eq!(
1592 results.iter().map(|result| result.pos).collect::<Vec<_>>(),
1593 [second, first]
1594 );
1595 assert!(matches!(
1596 results[0].records.first().map(|record| &record.value),
1597 Some(ParsedChunkRecordValue::PendingTicks(ticks)) if ticks.len() == 1
1598 ));
1599 assert!(matches!(
1600 results[1].records.first().map(|record| &record.value),
1601 Some(ParsedChunkRecordValue::Entities(entities)) if entities.len() == 1
1602 ));
1603 }
1604
1605 #[test]
1606 fn region_overlay_query_includes_batched_modern_actors_and_pending_ticks() {
1607 let storage = Arc::new(MemoryStorage::new());
1608 let world = BedrockWorld::from_storage(
1609 "memory",
1610 storage,
1611 OpenOptions {
1612 read_only: false,
1613 ..OpenOptions::default()
1614 },
1615 );
1616 let pos = ChunkPos {
1617 x: 2,
1618 z: 3,
1619 dimension: Dimension::Overworld,
1620 };
1621 let actor = ParsedEntity {
1622 identifier: Some("minecraft:pig".to_string()),
1623 definitions: Vec::new(),
1624 unique_id: Some(77),
1625 position: Some([32.0, 64.0, 48.0]),
1626 rotation: None,
1627 motion: None,
1628 items: Vec::new(),
1629 nbt: NbtTag::Compound(IndexMap::from([
1630 (
1631 "identifier".to_string(),
1632 NbtTag::String("minecraft:pig".to_string()),
1633 ),
1634 ("UniqueID".to_string(), NbtTag::Long(77)),
1635 (
1636 "Pos".to_string(),
1637 NbtTag::List(vec![
1638 NbtTag::Float(32.0),
1639 NbtTag::Float(64.0),
1640 NbtTag::Float(48.0),
1641 ]),
1642 ),
1643 ])),
1644 };
1645 world.put_actor_blocking(pos, &actor).expect("write actor");
1646 let pending_tick = NbtTag::Compound(IndexMap::from([("x".to_string(), NbtTag::Int(33))]));
1647 world
1648 .put_raw_record_blocking(
1649 &ChunkKey::new(pos, ChunkRecordTag::PendingTicks),
1650 &serialize_root_nbt(&pending_tick).expect("serialize pending tick"),
1651 )
1652 .expect("write pending tick");
1653
1654 let overlays = query_region_overlays_blocking(
1655 &world,
1656 SlimeChunkBounds {
1657 dimension: Dimension::Overworld,
1658 min_chunk_x: pos.x,
1659 max_chunk_x: pos.x,
1660 min_chunk_z: pos.z,
1661 max_chunk_z: pos.z,
1662 },
1663 RegionOverlayQueryOptions {
1664 include_slime: false,
1665 include_hardcoded_spawn_areas: false,
1666 include_entities: true,
1667 include_block_entities: false,
1668 include_pending_ticks: true,
1669 include_villages: false,
1670 max_chunks: 1,
1671 max_items_per_kind: 10,
1672 },
1673 )
1674 .expect("query overlays");
1675
1676 assert_eq!(overlays.entities.len(), 1);
1677 assert_eq!(overlays.pending_ticks.len(), 1);
1678 assert_eq!(overlays.pending_ticks[0].chunk, pos);
1679 }
1680
1681 #[test]
1682 fn chunk_record_fingerprint_changes_when_selected_raw_record_changes() {
1683 let storage = Arc::new(MemoryStorage::new());
1684 let world = BedrockWorld::from_storage(
1685 "memory",
1686 storage,
1687 OpenOptions {
1688 read_only: false,
1689 ..OpenOptions::default()
1690 },
1691 );
1692 let pos = ChunkPos {
1693 x: -2,
1694 z: 7,
1695 dimension: Dimension::Overworld,
1696 };
1697 let key = ChunkKey::new(pos, ChunkRecordTag::BlockEntity);
1698 world
1699 .put_raw_record_blocking(&key, b"first")
1700 .expect("write first value");
1701 let query = ChunkRecordQuery {
1702 entities: false,
1703 block_entities: true,
1704 pending_ticks: false,
1705 hardcoded_spawn_areas: false,
1706 };
1707 let first = fingerprint_chunk_records_many_blocking(&world, [pos], query)
1708 .expect("fingerprint first value");
1709 world
1710 .put_raw_record_blocking(&key, b"second")
1711 .expect("write second value");
1712 let second = fingerprint_chunk_records_many_blocking(&world, [pos], query)
1713 .expect("fingerprint second value");
1714
1715 assert_ne!(first[0].value, second[0].value);
1716 }
1717
1718 #[test]
1719 fn invalid_slime_window_size_is_rejected() {
1720 assert!(SlimeWindowSize::new(0).is_err());
1721 assert!(SlimeWindowSize::new(4).is_err());
1722 assert!(SlimeWindowSize::new(5).is_ok());
1723 }
1724
1725 #[test]
1726 fn read_only_write_guard_still_rejects_mutation() {
1727 let world = BedrockWorld::from_storage(
1728 "memory",
1729 Arc::new(MemoryStorage::new()),
1730 OpenOptions::default(),
1731 );
1732 let guard = WriteGuard::confirmed("memory", "test write");
1733 let error = write_chunk_record_nbt_blocking(
1734 &world,
1735 ChunkPos {
1736 x: 0,
1737 z: 0,
1738 dimension: Dimension::Overworld,
1739 },
1740 ChunkRecordTag::BlockEntity,
1741 &NbtTag::Compound(indexmap::IndexMap::new()),
1742 &guard,
1743 )
1744 .expect_err("read-only world rejects writes");
1745 assert_eq!(error.kind(), crate::BedrockWorldErrorKind::ReadOnly);
1746 }
1747
1748 #[test]
1749 fn delete_chunks_removes_modern_actor_digest_and_prefix_records() {
1750 let storage = Arc::new(MemoryStorage::new());
1751 let world = BedrockWorld::from_storage(
1752 "memory",
1753 storage.clone(),
1754 OpenOptions {
1755 read_only: false,
1756 ..OpenOptions::default()
1757 },
1758 );
1759 let pos = ChunkPos {
1760 x: 2,
1761 z: 3,
1762 dimension: Dimension::Overworld,
1763 };
1764 world
1765 .put_raw_record_blocking(&ChunkKey::new(pos, ChunkRecordTag::Version), &[1])
1766 .expect("write chunk record");
1767 let actor = ParsedEntity {
1768 identifier: Some("minecraft:pig".to_string()),
1769 definitions: Vec::new(),
1770 unique_id: Some(77),
1771 position: Some([32.0, 64.0, 48.0]),
1772 rotation: None,
1773 motion: None,
1774 items: Vec::new(),
1775 nbt: NbtTag::Compound(IndexMap::from([
1776 (
1777 "identifier".to_string(),
1778 NbtTag::String("minecraft:pig".to_string()),
1779 ),
1780 ("UniqueID".to_string(), NbtTag::Long(77)),
1781 (
1782 "Pos".to_string(),
1783 NbtTag::List(vec![
1784 NbtTag::Float(32.0),
1785 NbtTag::Float(64.0),
1786 NbtTag::Float(48.0),
1787 ]),
1788 ),
1789 ])),
1790 };
1791 world.put_actor_blocking(pos, &actor).expect("write actor");
1792
1793 let guard = WriteGuard::confirmed("memory", "delete chunk actor data");
1794 let deleted = delete_chunks_blocking(
1795 &world,
1796 SlimeChunkBounds {
1797 dimension: pos.dimension,
1798 min_chunk_x: pos.x,
1799 max_chunk_x: pos.x,
1800 min_chunk_z: pos.z,
1801 max_chunk_z: pos.z,
1802 },
1803 &guard,
1804 )
1805 .expect("delete chunk");
1806
1807 assert_eq!(deleted, 2);
1808 assert!(
1809 storage
1810 .get(&ChunkKey::new(pos, ChunkRecordTag::Version).encode())
1811 .expect("read chunk record")
1812 .is_none()
1813 );
1814 assert!(
1815 storage
1816 .get(&ActorDigestKey::new(pos).storage_key())
1817 .expect("read actor digest")
1818 .is_none()
1819 );
1820 assert!(
1821 storage
1822 .get(&ActorUid(77).storage_key())
1823 .expect("read actor prefix")
1824 .is_none()
1825 );
1826 }
1827}