1use crate::error::{BedrockWorldError, Result};
4use crate::nbt::{NbtTag, serialize_root_nbt};
5use crate::parsed::{
6 ActorResolution, ParsedBlockEntity, ParsedChunkRecordValue, ParsedEntity,
7 ParsedHardcodedSpawnArea, ParsedVillageData, RetentionMode, WorldParseCategories,
8 WorldParseOptions,
9};
10use crate::world::{BedrockWorld, ChunkBounds, SurfaceColumnOptions, WorldStorageHandle};
11use crate::{
12 BlockPos, CancelFlag, ChunkPos, ChunkRecordTag, Dimension, RenderChunkRegion,
13 SubChunkDecodeMode, SurfaceColumn,
14};
15use serde::{Deserialize, Serialize};
16use std::cmp::Reverse;
17use std::path::PathBuf;
18
19const MT_N: usize = 624;
20const MT_M: usize = 397;
21const MT_MATRIX_A: u32 = 0x9908_b0df;
22const MT_UPPER_MASK: u32 = 0x8000_0000;
23const MT_LOWER_MASK: u32 = 0x7fff_ffff;
24const WRITE_CONFIRM_TOKEN: &str = "CONFIRMED";
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28pub struct SlimeChunkBounds {
29 pub dimension: Dimension,
31 pub min_chunk_x: i32,
33 pub max_chunk_x: i32,
35 pub min_chunk_z: i32,
37 pub max_chunk_z: i32,
39}
40
41impl SlimeChunkBounds {
42 pub fn validate(self) -> Result<()> {
44 if self.min_chunk_x > self.max_chunk_x || self.min_chunk_z > self.max_chunk_z {
45 return Err(BedrockWorldError::Validation(format!(
46 "invalid chunk bounds: min=({}, {}) max=({}, {})",
47 self.min_chunk_x, self.min_chunk_z, self.max_chunk_x, self.max_chunk_z
48 )));
49 }
50 Ok(())
51 }
52
53 #[must_use]
54 pub const fn chunk_count(self) -> usize {
56 let width = self.max_chunk_x.saturating_sub(self.min_chunk_x) as usize + 1;
57 let height = self.max_chunk_z.saturating_sub(self.min_chunk_z) as usize + 1;
58 width.saturating_mul(height)
59 }
60
61 #[must_use]
62 pub fn from_chunk_bounds(bounds: ChunkBounds) -> Self {
64 Self {
65 dimension: bounds.dimension,
66 min_chunk_x: bounds.min_chunk_x,
67 max_chunk_x: bounds.max_chunk_x,
68 min_chunk_z: bounds.min_chunk_z,
69 max_chunk_z: bounds.max_chunk_z,
70 }
71 }
72
73 #[must_use]
74 pub const fn center(self) -> (i32, i32) {
76 (
77 i32::midpoint(self.min_chunk_x, self.max_chunk_x),
78 i32::midpoint(self.min_chunk_z, self.max_chunk_z),
79 )
80 }
81}
82
83impl From<RenderChunkRegion> for SlimeChunkBounds {
84 fn from(region: RenderChunkRegion) -> Self {
85 Self {
86 dimension: region.dimension,
87 min_chunk_x: region.min_chunk_x,
88 max_chunk_x: region.max_chunk_x,
89 min_chunk_z: region.min_chunk_z,
90 max_chunk_z: region.max_chunk_z,
91 }
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
97pub struct SlimeWindowSize(u8);
98
99impl SlimeWindowSize {
100 pub fn new(size: u8) -> Result<Self> {
102 if size == 0 || size.is_multiple_of(2) {
103 return Err(BedrockWorldError::Validation(format!(
104 "slime query window must be a positive odd size, got {size}"
105 )));
106 }
107 Ok(Self(size))
108 }
109
110 #[must_use]
111 pub const fn get(self) -> u8 {
113 self.0
114 }
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119pub struct SlimeChunkWindow {
120 pub center: ChunkPos,
122 pub min_chunk_x: i32,
124 pub max_chunk_x: i32,
126 pub min_chunk_z: i32,
128 pub max_chunk_z: i32,
130 pub slime_count: usize,
132 pub total_count: usize,
134}
135
136#[derive(Debug, Clone, PartialEq)]
138pub struct ChunkRecordDetail {
139 pub tag: ChunkRecordTag,
141 pub raw_value_len: usize,
143 pub roots: Vec<NbtTag>,
145 pub writable_nbt: bool,
147}
148
149#[derive(Debug, Clone, PartialEq)]
151pub struct ChunkDetail {
152 pub pos: ChunkPos,
154 pub records: Vec<ChunkRecordDetail>,
156}
157
158#[derive(Debug, Clone, PartialEq)]
160pub struct BlockTip {
161 pub block: BlockPos,
163 pub chunk: ChunkPos,
165 pub local_x: u8,
167 pub local_z: u8,
169 pub surface: Option<SurfaceColumn>,
171 pub biome_id: Option<u32>,
173 pub height: Option<i16>,
175 pub is_slime_chunk: bool,
177}
178
179#[derive(Debug, Clone, PartialEq)]
181pub struct EntityOverlay {
182 pub identifier: Option<String>,
184 pub position: [f64; 3],
186 pub chunk: ChunkPos,
188 pub nbt: NbtTag,
190}
191
192#[derive(Debug, Clone, PartialEq)]
194pub struct BlockEntityOverlay {
195 pub id: Option<String>,
197 pub position: [i32; 3],
199 pub chunk: ChunkPos,
201 pub nbt: NbtTag,
203}
204
205#[derive(Debug, Clone, PartialEq, Eq)]
207pub struct HardcodedSpawnAreaOverlay {
208 pub area: ParsedHardcodedSpawnArea,
210 pub chunk: ChunkPos,
212}
213
214#[derive(Debug, Clone, PartialEq)]
216pub struct VillageOverlay {
217 pub key: crate::ParsedVillageKey,
219 pub bounds: Option<SlimeChunkBounds>,
221 pub root_count: usize,
223 pub raw_len: usize,
225}
226
227#[derive(Debug, Clone, PartialEq)]
229pub struct VillageOverlayIndex {
230 pub villages: Vec<VillageOverlay>,
232}
233
234impl VillageOverlayIndex {
235 pub fn build_blocking_with_control<S>(
237 world: &BedrockWorld<S>,
238 cancel: &CancelFlag,
239 ) -> Result<Self>
240 where
241 S: WorldStorageHandle,
242 {
243 check_query_cancelled(Some(cancel))?;
244 let mut villages = Vec::new();
245 for village in world.scan_villages_lightweight_blocking(cancel)? {
246 check_query_cancelled(Some(cancel))?;
247 villages.push(village_overlay(village));
248 }
249 Ok(Self { villages })
250 }
251
252 #[must_use]
253 pub fn query(&self, bounds: SlimeChunkBounds, max_items: usize) -> Vec<VillageOverlay> {
255 self.villages
256 .iter()
257 .filter(|overlay| {
258 overlay
259 .bounds
260 .is_none_or(|village_bounds| bounds_intersect(bounds, village_bounds))
261 })
262 .take(max_items)
263 .cloned()
264 .collect()
265 }
266}
267
268#[derive(Debug, Clone, PartialEq)]
270pub struct RegionOverlayQuery {
271 pub bounds: SlimeChunkBounds,
273 pub slime_chunks: Vec<ChunkPos>,
275 pub hardcoded_spawn_areas: Vec<HardcodedSpawnAreaOverlay>,
277 pub entities: Vec<EntityOverlay>,
279 pub block_entities: Vec<BlockEntityOverlay>,
281 pub villages: Vec<VillageOverlay>,
283 pub scanned_chunks: usize,
285 pub missing_chunks: usize,
287}
288
289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
291pub struct RegionOverlayQueryOptions {
292 pub include_slime: bool,
294 pub include_hardcoded_spawn_areas: bool,
296 pub include_entities: bool,
298 pub include_block_entities: bool,
300 pub include_villages: bool,
302 pub max_chunks: usize,
304 pub max_items_per_kind: usize,
306}
307
308impl Default for RegionOverlayQueryOptions {
309 fn default() -> Self {
310 Self {
311 include_slime: true,
312 include_hardcoded_spawn_areas: true,
313 include_entities: true,
314 include_block_entities: true,
315 include_villages: true,
316 max_chunks: 65_536,
317 max_items_per_kind: 10_000,
318 }
319 }
320}
321
322#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
324pub struct SelectionStats {
325 pub bounds: Option<SlimeChunkBounds>,
327 pub chunk_count: usize,
329 pub loaded_chunks: usize,
331 pub missing_chunks: usize,
333 pub slime_chunks: usize,
335 pub entity_count: usize,
337 pub block_entity_count: usize,
339 pub hardcoded_spawn_area_count: usize,
341 pub village_count: usize,
343}
344
345#[derive(Debug, Clone, PartialEq, Eq)]
347pub struct WriteGuard {
348 world_path: PathBuf,
349 confirmation_token: String,
350 operation: String,
351}
352
353impl WriteGuard {
354 #[must_use]
355 pub fn confirmed(world_path: impl Into<PathBuf>, operation: impl Into<String>) -> Self {
357 Self {
358 world_path: world_path.into(),
359 confirmation_token: WRITE_CONFIRM_TOKEN.to_string(),
360 operation: operation.into(),
361 }
362 }
363
364 fn validate<S>(&self, world: &BedrockWorld<S>) -> Result<()>
365 where
366 S: WorldStorageHandle,
367 {
368 if self.confirmation_token != WRITE_CONFIRM_TOKEN || self.operation.trim().is_empty() {
369 return Err(BedrockWorldError::Validation(
370 "write guard is not confirmed".to_string(),
371 ));
372 }
373 if self.world_path != world.path() {
374 return Err(BedrockWorldError::Validation(format!(
375 "write guard world path does not match: guard={} world={}",
376 self.world_path.display(),
377 world.path().display()
378 )));
379 }
380 Ok(())
381 }
382}
383
384#[must_use]
385pub fn is_bedrock_slime_chunk(chunk_x: i32, chunk_z: i32) -> bool {
387 let seed = (chunk_x as u32).wrapping_mul(0x1f1f_1f1f) ^ (chunk_z as u32);
388 mt19937_first_u32(seed).is_multiple_of(10)
389}
390
391#[must_use]
392pub fn is_slime_chunk(pos: ChunkPos) -> bool {
394 pos.dimension == Dimension::Overworld && is_bedrock_slime_chunk(pos.x, pos.z)
395}
396
397pub fn query_slime_chunk_windows(
399 bounds: SlimeChunkBounds,
400 window_size: SlimeWindowSize,
401 max_results: usize,
402) -> Result<Vec<SlimeChunkWindow>> {
403 bounds.validate()?;
404 if bounds.dimension != Dimension::Overworld || max_results == 0 {
405 return Ok(Vec::new());
406 }
407 let width = bounds.max_chunk_x.saturating_sub(bounds.min_chunk_x) as usize + 1;
408 let height = bounds.max_chunk_z.saturating_sub(bounds.min_chunk_z) as usize + 1;
409 let size = usize::from(window_size.get());
410 if width < size || height < size {
411 return Ok(Vec::new());
412 }
413 let mut prefix = vec![0usize; (width + 1) * (height + 1)];
414 for z in 0..height {
415 let chunk_z = bounds
416 .min_chunk_z
417 .saturating_add(i32::try_from(z).unwrap_or(i32::MAX));
418 for x in 0..width {
419 let chunk_x = bounds
420 .min_chunk_x
421 .saturating_add(i32::try_from(x).unwrap_or(i32::MAX));
422 let value = usize::from(is_bedrock_slime_chunk(chunk_x, chunk_z));
423 let index = (z + 1) * (width + 1) + (x + 1);
424 prefix[index] =
425 value + prefix[z * (width + 1) + (x + 1)] + prefix[(z + 1) * (width + 1) + x]
426 - prefix[z * (width + 1) + x];
427 }
428 }
429 let (center_x, center_z) = bounds.center();
430 let mut windows = Vec::with_capacity((width - size + 1).saturating_mul(height - size + 1));
431 for z in 0..=(height - size) {
432 for x in 0..=(width - size) {
433 let x2 = x + size;
434 let z2 = z + size;
435 let count = prefix[z2 * (width + 1) + x2] + prefix[z * (width + 1) + x]
436 - prefix[z * (width + 1) + x2]
437 - prefix[z2 * (width + 1) + x];
438 let min_chunk_x = bounds.min_chunk_x + i32::try_from(x).unwrap_or(i32::MAX);
439 let min_chunk_z = bounds.min_chunk_z + i32::try_from(z).unwrap_or(i32::MAX);
440 let max_chunk_x = min_chunk_x + i32::from(window_size.get()) - 1;
441 let max_chunk_z = min_chunk_z + i32::from(window_size.get()) - 1;
442 windows.push(SlimeChunkWindow {
443 center: ChunkPos {
444 x: i32::midpoint(min_chunk_x, max_chunk_x),
445 z: i32::midpoint(min_chunk_z, max_chunk_z),
446 dimension: bounds.dimension,
447 },
448 min_chunk_x,
449 max_chunk_x,
450 min_chunk_z,
451 max_chunk_z,
452 slime_count: count,
453 total_count: size * size,
454 });
455 }
456 }
457 windows.sort_by_key(|window| {
458 let dx = i64::from(window.center.x) - i64::from(center_x);
459 let dz = i64::from(window.center.z) - i64::from(center_z);
460 (
461 Reverse(window.slime_count),
462 dx.saturating_mul(dx).saturating_add(dz.saturating_mul(dz)),
463 window.center.z,
464 window.center.x,
465 )
466 });
467 windows.truncate(max_results);
468 Ok(windows)
469}
470
471pub fn query_block_tip_blocking<S>(
473 world: &BedrockWorld<S>,
474 block: BlockPos,
475 dimension: Dimension,
476) -> Result<BlockTip>
477where
478 S: WorldStorageHandle,
479{
480 let chunk = block.to_chunk_pos(dimension);
481 let (local_x, _, local_z) = block.in_chunk_offset();
482 let surface = world.get_surface_column_blocking(
483 chunk,
484 local_x,
485 local_z,
486 SurfaceColumnOptions::default(),
487 )?;
488 let height = world.get_height_at_blocking(chunk, local_x, local_z)?;
489 let biome_y = surface.as_ref().map_or(block.y, |surface| surface.y);
490 let biome_id = world.get_biome_id_blocking(chunk, local_x, local_z, biome_y)?;
491 Ok(BlockTip {
492 block,
493 chunk,
494 local_x,
495 local_z,
496 surface,
497 biome_id,
498 height,
499 is_slime_chunk: is_slime_chunk(chunk),
500 })
501}
502
503pub fn query_chunk_detail_blocking<S>(world: &BedrockWorld<S>, pos: ChunkPos) -> Result<ChunkDetail>
505where
506 S: WorldStorageHandle,
507{
508 let chunk = world.get_chunk_blocking(pos)?;
509 let mut records = Vec::with_capacity(chunk.records.len());
510 for record in chunk.records {
511 let roots = parse_record_roots(record.key.tag, &record.value);
512 records.push(ChunkRecordDetail {
513 tag: record.key.tag,
514 raw_value_len: record.value.len(),
515 writable_nbt: record_tag_accepts_nbt_write(record.key.tag),
516 roots,
517 });
518 }
519 Ok(ChunkDetail { pos, records })
520}
521
522pub fn query_region_overlays_blocking<S>(
524 world: &BedrockWorld<S>,
525 bounds: SlimeChunkBounds,
526 options: RegionOverlayQueryOptions,
527) -> Result<RegionOverlayQuery>
528where
529 S: WorldStorageHandle,
530{
531 query_region_overlays_blocking_inner(world, bounds, options, None)
532}
533
534pub fn query_region_overlays_blocking_with_control<S>(
536 world: &BedrockWorld<S>,
537 bounds: SlimeChunkBounds,
538 options: RegionOverlayQueryOptions,
539 cancel: &CancelFlag,
540) -> Result<RegionOverlayQuery>
541where
542 S: WorldStorageHandle,
543{
544 query_region_overlays_blocking_inner(world, bounds, options, Some(cancel))
545}
546
547fn query_region_overlays_blocking_inner<S>(
548 world: &BedrockWorld<S>,
549 bounds: SlimeChunkBounds,
550 options: RegionOverlayQueryOptions,
551 cancel: Option<&CancelFlag>,
552) -> Result<RegionOverlayQuery>
553where
554 S: WorldStorageHandle,
555{
556 bounds.validate()?;
557 if bounds.chunk_count() > options.max_chunks {
558 return Err(BedrockWorldError::Validation(format!(
559 "query covers {} chunks, limit is {}",
560 bounds.chunk_count(),
561 options.max_chunks
562 )));
563 }
564 let mut result = RegionOverlayQuery {
565 bounds,
566 slime_chunks: Vec::new(),
567 hardcoded_spawn_areas: Vec::new(),
568 entities: Vec::new(),
569 block_entities: Vec::new(),
570 villages: Vec::new(),
571 scanned_chunks: 0,
572 missing_chunks: 0,
573 };
574 let chunk_parse_options = overlay_chunk_parse_options(options);
575 let needs_chunk_records = overlay_options_need_chunk_records(options);
576 for chunk_z in bounds.min_chunk_z..=bounds.max_chunk_z {
577 check_query_cancelled(cancel)?;
578 for chunk_x in bounds.min_chunk_x..=bounds.max_chunk_x {
579 check_query_cancelled(cancel)?;
580 let pos = ChunkPos {
581 x: chunk_x,
582 z: chunk_z,
583 dimension: bounds.dimension,
584 };
585 if options.include_slime && is_slime_chunk(pos) {
586 result.slime_chunks.push(pos);
587 }
588 if !needs_chunk_records {
589 continue;
590 }
591 let parsed = world.parse_chunk_with_options_blocking(pos, chunk_parse_options)?;
592 if parsed.records.is_empty() {
593 result.missing_chunks = result.missing_chunks.saturating_add(1);
594 continue;
595 }
596 result.scanned_chunks = result.scanned_chunks.saturating_add(1);
597 for record in parsed.records {
598 match record.value {
599 ParsedChunkRecordValue::HardcodedSpawnAreas(areas)
600 if options.include_hardcoded_spawn_areas =>
601 {
602 for area in areas {
603 if result.hardcoded_spawn_areas.len() >= options.max_items_per_kind {
604 break;
605 }
606 result
607 .hardcoded_spawn_areas
608 .push(HardcodedSpawnAreaOverlay { area, chunk: pos });
609 }
610 }
611 ParsedChunkRecordValue::Entities(entities) if options.include_entities => {
612 push_entities(
613 &mut result.entities,
614 entities,
615 pos,
616 options.max_items_per_kind,
617 );
618 }
619 ParsedChunkRecordValue::BlockEntities(block_entities)
620 if options.include_block_entities =>
621 {
622 push_block_entities(
623 &mut result.block_entities,
624 block_entities,
625 pos,
626 options.max_items_per_kind,
627 );
628 }
629 _ => {}
630 }
631 }
632 }
633 }
634 if options.include_villages {
635 check_query_cancelled(cancel)?;
636 let village_cancel = cancel.cloned().unwrap_or_default();
637 let index = VillageOverlayIndex::build_blocking_with_control(world, &village_cancel)?;
638 result.villages = index.query(bounds, options.max_items_per_kind);
639 }
640 Ok(result)
641}
642
643fn overlay_options_need_chunk_records(options: RegionOverlayQueryOptions) -> bool {
644 options.include_hardcoded_spawn_areas
645 || options.include_entities
646 || options.include_block_entities
647}
648
649fn overlay_chunk_parse_options(options: RegionOverlayQueryOptions) -> WorldParseOptions {
650 WorldParseOptions {
651 categories: WorldParseCategories {
652 chunks: true,
653 players: false,
654 actors: options.include_entities,
655 maps: false,
656 villages: false,
657 globals: false,
658 },
659 retention: RetentionMode::Structured,
660 subchunk_decode_mode: SubChunkDecodeMode::CountsOnly,
661 actor_resolution: if options.include_entities {
662 ActorResolution::ResolveReferenced
663 } else {
664 ActorResolution::None
665 },
666 }
667}
668
669fn check_query_cancelled(cancel: Option<&CancelFlag>) -> Result<()> {
670 if cancel.is_some_and(CancelFlag::is_cancelled) {
671 return Err(BedrockWorldError::Cancelled {
672 operation: "region overlay query",
673 });
674 }
675 Ok(())
676}
677
678pub fn query_selection_stats_blocking<S>(
680 world: &BedrockWorld<S>,
681 bounds: SlimeChunkBounds,
682 options: RegionOverlayQueryOptions,
683) -> Result<SelectionStats>
684where
685 S: WorldStorageHandle,
686{
687 let overlays = query_region_overlays_blocking(world, bounds, options)?;
688 Ok(SelectionStats {
689 bounds: Some(bounds),
690 chunk_count: bounds.chunk_count(),
691 loaded_chunks: overlays.scanned_chunks,
692 missing_chunks: overlays.missing_chunks,
693 slime_chunks: overlays.slime_chunks.len(),
694 entity_count: overlays.entities.len(),
695 block_entity_count: overlays.block_entities.len(),
696 hardcoded_spawn_area_count: overlays.hardcoded_spawn_areas.len(),
697 village_count: overlays.villages.len(),
698 })
699}
700
701pub fn delete_chunks_blocking<S>(
703 world: &BedrockWorld<S>,
704 bounds: SlimeChunkBounds,
705 guard: &WriteGuard,
706) -> Result<usize>
707where
708 S: WorldStorageHandle,
709{
710 bounds.validate()?;
711 guard.validate(world)?;
712 let mut deleted = 0usize;
713 let mut transaction = world.transaction();
714 for chunk_z in bounds.min_chunk_z..=bounds.max_chunk_z {
715 for chunk_x in bounds.min_chunk_x..=bounds.max_chunk_x {
716 let pos = ChunkPos {
717 x: chunk_x,
718 z: chunk_z,
719 dimension: bounds.dimension,
720 };
721 for record in world.get_chunk_blocking(pos)?.records {
722 transaction.delete_raw_record(&record.key);
723 deleted = deleted.saturating_add(1);
724 }
725 }
726 }
727 transaction.commit()?;
728 Ok(deleted)
729}
730
731pub fn write_chunk_record_nbt_blocking<S>(
733 world: &BedrockWorld<S>,
734 pos: ChunkPos,
735 record_kind: ChunkRecordTag,
736 tag: &NbtTag,
737 guard: &WriteGuard,
738) -> Result<()>
739where
740 S: WorldStorageHandle,
741{
742 guard.validate(world)?;
743 if !record_tag_accepts_nbt_write(record_kind) {
744 return Err(BedrockWorldError::Validation(format!(
745 "chunk record {record_kind:?} does not support NBT writes"
746 )));
747 }
748 let bytes = serialize_record_nbt(tag)?;
749 world.put_raw_record_blocking(&crate::ChunkKey::new(pos, record_kind), &bytes)
750}
751
752fn push_entities(
753 target: &mut Vec<EntityOverlay>,
754 entities: Vec<ParsedEntity>,
755 fallback_chunk: ChunkPos,
756 limit: usize,
757) {
758 for entity in entities {
759 if target.len() >= limit {
760 break;
761 }
762 let Some(position) = entity.position else {
763 continue;
764 };
765 target.push(EntityOverlay {
766 identifier: entity.identifier,
767 chunk: BlockPos {
768 x: position[0].floor() as i32,
769 y: position[1].floor() as i32,
770 z: position[2].floor() as i32,
771 }
772 .to_chunk_pos(fallback_chunk.dimension),
773 position,
774 nbt: entity.nbt,
775 });
776 }
777}
778
779fn push_block_entities(
780 target: &mut Vec<BlockEntityOverlay>,
781 block_entities: Vec<ParsedBlockEntity>,
782 fallback_chunk: ChunkPos,
783 limit: usize,
784) {
785 for block_entity in block_entities {
786 if target.len() >= limit {
787 break;
788 }
789 let Some(position) = block_entity.position else {
790 continue;
791 };
792 target.push(BlockEntityOverlay {
793 id: block_entity.id,
794 chunk: BlockPos {
795 x: position[0],
796 y: position[1],
797 z: position[2],
798 }
799 .to_chunk_pos(fallback_chunk.dimension),
800 position,
801 nbt: block_entity.nbt,
802 });
803 }
804}
805
806fn parse_record_roots(tag: ChunkRecordTag, value: &[u8]) -> Vec<NbtTag> {
807 match tag {
808 ChunkRecordTag::BlockEntity | ChunkRecordTag::Entity | ChunkRecordTag::PendingTicks => {
809 crate::nbt::parse_consecutive_root_nbt(value).unwrap_or_default()
810 }
811 _ => Vec::new(),
812 }
813}
814
815fn record_tag_accepts_nbt_write(tag: ChunkRecordTag) -> bool {
816 matches!(
817 tag,
818 ChunkRecordTag::BlockEntity | ChunkRecordTag::Entity | ChunkRecordTag::PendingTicks
819 )
820}
821
822fn serialize_record_nbt(tag: &NbtTag) -> Result<Vec<u8>> {
823 match tag {
824 NbtTag::List(values) => {
825 let mut bytes = Vec::new();
826 for value in values {
827 bytes.extend(serialize_root_nbt(value)?);
828 }
829 Ok(bytes)
830 }
831 _ => serialize_root_nbt(tag),
832 }
833}
834
835fn village_overlay(village: ParsedVillageData) -> VillageOverlay {
836 let bounds = infer_village_bounds(&village.roots);
837 VillageOverlay {
838 key: village.key,
839 bounds,
840 root_count: village.roots.len(),
841 raw_len: village.raw.len(),
842 }
843}
844
845fn infer_village_bounds(roots: &[NbtTag]) -> Option<SlimeChunkBounds> {
846 for root in roots {
847 if let Some(bounds) = infer_bounds_from_tag(root) {
848 return Some(bounds);
849 }
850 }
851 None
852}
853
854fn infer_bounds_from_tag(tag: &NbtTag) -> Option<SlimeChunkBounds> {
855 let NbtTag::Compound(map) = tag else {
856 return None;
857 };
858 let min_x = nbt_i32_named(map, &["min_x", "MinX", "x0", "X0", "minBlockX"])?;
859 let min_z = nbt_i32_named(map, &["min_z", "MinZ", "z0", "Z0", "minBlockZ"])?;
860 let max_x = nbt_i32_named(map, &["max_x", "MaxX", "x1", "X1", "maxBlockX"])?;
861 let max_z = nbt_i32_named(map, &["max_z", "MaxZ", "z1", "Z1", "maxBlockZ"])?;
862 Some(SlimeChunkBounds {
863 dimension: Dimension::Overworld,
864 min_chunk_x: min_x.div_euclid(16),
865 max_chunk_x: max_x.div_euclid(16),
866 min_chunk_z: min_z.div_euclid(16),
867 max_chunk_z: max_z.div_euclid(16),
868 })
869}
870
871fn nbt_i32_named(map: &indexmap::IndexMap<String, NbtTag>, names: &[&str]) -> Option<i32> {
872 for name in names {
873 if let Some(value) = map.get(*name).and_then(nbt_i32) {
874 return Some(value);
875 }
876 }
877 None
878}
879
880fn nbt_i32(tag: &NbtTag) -> Option<i32> {
881 match tag {
882 NbtTag::Byte(value) => Some(i32::from(*value)),
883 NbtTag::Short(value) => Some(i32::from(*value)),
884 NbtTag::Int(value) => Some(*value),
885 NbtTag::Long(value) => i32::try_from(*value).ok(),
886 _ => None,
887 }
888}
889
890fn bounds_intersect(left: SlimeChunkBounds, right: SlimeChunkBounds) -> bool {
891 left.dimension == right.dimension
892 && left.min_chunk_x <= right.max_chunk_x
893 && left.max_chunk_x >= right.min_chunk_x
894 && left.min_chunk_z <= right.max_chunk_z
895 && left.max_chunk_z >= right.min_chunk_z
896}
897
898fn mt19937_first_u32(seed: u32) -> u32 {
899 let mut mt = [0_u32; MT_N];
900 mt[0] = seed;
901 for i in 1..MT_N {
902 mt[i] = 1_812_433_253_u32
903 .wrapping_mul(mt[i - 1] ^ (mt[i - 1] >> 30))
904 .wrapping_add(i as u32);
905 }
906 for i in 0..MT_N {
907 let y = (mt[i] & MT_UPPER_MASK) | (mt[(i + 1) % MT_N] & MT_LOWER_MASK);
908 mt[i] = mt[(i + MT_M) % MT_N] ^ (y >> 1) ^ if y & 1 == 0 { 0 } else { MT_MATRIX_A };
909 }
910 temper(mt[0])
911}
912
913const fn temper(mut value: u32) -> u32 {
914 value ^= value >> 11;
915 value ^= (value << 7) & 0x9d2c_5680;
916 value ^= (value << 15) & 0xefc6_0000;
917 value ^= value >> 18;
918 value
919}
920
921#[cfg(test)]
922mod tests {
923 use super::*;
924 use crate::{MemoryStorage, OpenOptions};
925 use std::sync::Arc;
926
927 #[test]
928 fn bedrock_slime_vectors_match_known_pe_results() {
929 assert!(is_bedrock_slime_chunk(-1, 0));
930 assert!(is_bedrock_slime_chunk(109, 3));
931 assert!(!is_bedrock_slime_chunk(0, 0));
932 assert!(!is_bedrock_slime_chunk(110, 3));
933 }
934
935 #[test]
936 fn slime_window_query_matches_naive_count() {
937 let bounds = SlimeChunkBounds {
938 dimension: Dimension::Overworld,
939 min_chunk_x: -4,
940 max_chunk_x: 4,
941 min_chunk_z: -4,
942 max_chunk_z: 4,
943 };
944 let windows =
945 query_slime_chunk_windows(bounds, SlimeWindowSize::new(3).unwrap(), 100).unwrap();
946 for window in windows {
947 let mut count = 0;
948 for z in window.min_chunk_z..=window.max_chunk_z {
949 for x in window.min_chunk_x..=window.max_chunk_x {
950 count += usize::from(is_bedrock_slime_chunk(x, z));
951 }
952 }
953 assert_eq!(window.slime_count, count);
954 }
955 }
956
957 #[test]
958 fn slime_window_query_prefix_sum_keeps_stable_sorting_for_negative_bounds() {
959 let bounds = SlimeChunkBounds {
960 dimension: Dimension::Overworld,
961 min_chunk_x: -12,
962 max_chunk_x: 7,
963 min_chunk_z: -9,
964 max_chunk_z: 8,
965 };
966 let windows =
967 query_slime_chunk_windows(bounds, SlimeWindowSize::new(5).unwrap(), 24).unwrap();
968 let mut expected = Vec::new();
969 let center = bounds.center();
970 for min_z in bounds.min_chunk_z..=bounds.max_chunk_z - 4 {
971 for min_x in bounds.min_chunk_x..=bounds.max_chunk_x - 4 {
972 let max_x = min_x + 4;
973 let max_z = min_z + 4;
974 let mut count = 0usize;
975 for z in min_z..=max_z {
976 for x in min_x..=max_x {
977 count += usize::from(is_bedrock_slime_chunk(x, z));
978 }
979 }
980 expected.push(SlimeChunkWindow {
981 center: ChunkPos {
982 x: i32::midpoint(min_x, max_x),
983 z: i32::midpoint(min_z, max_z),
984 dimension: Dimension::Overworld,
985 },
986 min_chunk_x: min_x,
987 max_chunk_x: max_x,
988 min_chunk_z: min_z,
989 max_chunk_z: max_z,
990 slime_count: count,
991 total_count: 25,
992 });
993 }
994 }
995 expected.sort_by_key(|window| {
996 let dx = i64::from(window.center.x) - i64::from(center.0);
997 let dz = i64::from(window.center.z) - i64::from(center.1);
998 (
999 Reverse(window.slime_count),
1000 dx.saturating_mul(dx).saturating_add(dz.saturating_mul(dz)),
1001 window.center.z,
1002 window.center.x,
1003 )
1004 });
1005 expected.truncate(24);
1006
1007 assert_eq!(windows, expected);
1008 }
1009
1010 #[test]
1011 fn overlay_query_respects_cancel_before_scanning_chunks() {
1012 let storage = Arc::new(MemoryStorage::default()) as Arc<dyn crate::WorldStorage>;
1013 let world = BedrockWorld::from_storage(
1014 std::path::PathBuf::from("cancelled"),
1015 storage,
1016 OpenOptions::default(),
1017 );
1018 let cancel = CancelFlag::new();
1019 cancel.cancel();
1020 let bounds = SlimeChunkBounds {
1021 dimension: Dimension::Overworld,
1022 min_chunk_x: -128,
1023 max_chunk_x: 128,
1024 min_chunk_z: -128,
1025 max_chunk_z: 128,
1026 };
1027 let error = query_region_overlays_blocking_with_control(
1028 &world,
1029 bounds,
1030 RegionOverlayQueryOptions {
1031 max_chunks: 100_000,
1032 ..RegionOverlayQueryOptions::default()
1033 },
1034 &cancel,
1035 )
1036 .expect_err("cancelled query should fail");
1037
1038 assert_eq!(error.kind(), crate::BedrockWorldErrorKind::Cancelled);
1039 }
1040
1041 #[test]
1042 fn invalid_slime_window_size_is_rejected() {
1043 assert!(SlimeWindowSize::new(0).is_err());
1044 assert!(SlimeWindowSize::new(4).is_err());
1045 assert!(SlimeWindowSize::new(5).is_ok());
1046 }
1047
1048 #[test]
1049 fn read_only_write_guard_still_rejects_mutation() {
1050 let world = BedrockWorld::from_storage(
1051 "memory",
1052 Arc::new(MemoryStorage::new()),
1053 OpenOptions::default(),
1054 );
1055 let guard = WriteGuard::confirmed("memory", "test write");
1056 let error = write_chunk_record_nbt_blocking(
1057 &world,
1058 ChunkPos {
1059 x: 0,
1060 z: 0,
1061 dimension: Dimension::Overworld,
1062 },
1063 ChunkRecordTag::BlockEntity,
1064 &NbtTag::Compound(indexmap::IndexMap::new()),
1065 &guard,
1066 )
1067 .expect_err("read-only world rejects writes");
1068 assert_eq!(error.kind(), crate::BedrockWorldErrorKind::ReadOnly);
1069 }
1070}