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