Skip to main content

bedrock_world/
query.rs

1//! Professional map queries used by map viewers and offline tools.
2
3use 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/// Exact chunk record categories for batched queries.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct ChunkRecordQuery {
31    /// Include legacy inline entity records.
32    pub entities: bool,
33    /// Include block-entity records.
34    pub block_entities: bool,
35    /// Include pending tick records.
36    pub pending_ticks: bool,
37    /// Include hardcoded spawn-area records.
38    pub hardcoded_spawn_areas: bool,
39}
40
41impl ChunkRecordQuery {
42    #[must_use]
43    /// Returns the exact storage tags required by this query.
44    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/// Parsed records for one chunk returned by a batched record query.
61#[derive(Debug, Clone, PartialEq)]
62pub struct ChunkRecordQueryResult {
63    /// Queried chunk position.
64    pub pos: ChunkPos,
65    /// Parsed records limited to the requested categories.
66    pub records: Vec<crate::ParsedChunkRecord>,
67}
68
69/// XXH3-128 fingerprint of the exact raw records selected for one chunk.
70///
71/// The fingerprint includes selected missing records and, when entity records
72/// are requested, the chunk actor digest plus every referenced actor record.
73/// It can therefore validate cached query summaries without decoding NBT.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub struct ChunkRecordFingerprint {
76    /// Chunk whose selected records were hashed.
77    pub pos: ChunkPos,
78    /// XXH3-128 digest of the selected storage records.
79    pub value: u128,
80}
81
82/// Inclusive chunk bounds used by professional map queries.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
84pub struct SlimeChunkBounds {
85    /// Bedrock dimension queried by these bounds.
86    pub dimension: Dimension,
87    /// Inclusive minimum chunk X coordinate.
88    pub min_chunk_x: i32,
89    /// Inclusive maximum chunk X coordinate.
90    pub max_chunk_x: i32,
91    /// Inclusive minimum chunk Z coordinate.
92    pub min_chunk_z: i32,
93    /// Inclusive maximum chunk Z coordinate.
94    pub max_chunk_z: i32,
95}
96
97impl SlimeChunkBounds {
98    /// Validates this value and returns a typed error on failure.
99    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    /// Returns the number of chunks covered by these inclusive bounds.
111    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    /// Converts generic chunk bounds into slime-query bounds.
119    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    /// Returns the midpoint chunk X/Z coordinates for these inclusive bounds.
131    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/// Supported square windows for slime-farm candidate queries.
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
153pub struct SlimeWindowSize(u8);
154
155impl SlimeWindowSize {
156    /// Creates a new value.
157    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    /// Returns the value at the requested coordinates.
168    pub const fn get(self) -> u8 {
169        self.0
170    }
171}
172
173/// Ranked slime chunk window candidate.
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
175pub struct SlimeChunkWindow {
176    /// Center chunk for this candidate window.
177    pub center: ChunkPos,
178    /// Inclusive minimum chunk X coordinate.
179    pub min_chunk_x: i32,
180    /// Inclusive maximum chunk X coordinate.
181    pub max_chunk_x: i32,
182    /// Inclusive minimum chunk Z coordinate.
183    pub min_chunk_z: i32,
184    /// Inclusive maximum chunk Z coordinate.
185    pub max_chunk_z: i32,
186    /// Number of slime chunks inside the window.
187    pub slime_count: usize,
188    /// Total number of chunks inside the window.
189    pub total_count: usize,
190}
191
192/// One raw chunk record exposed through the query API.
193#[derive(Debug, Clone, PartialEq)]
194pub struct ChunkRecordDetail {
195    /// Bedrock chunk record tag for this value.
196    pub tag: ChunkRecordTag,
197    /// Length of the original storage value in bytes.
198    pub raw_value_len: usize,
199    /// Consecutive Bedrock NBT roots decoded from the value.
200    pub roots: Vec<NbtTag>,
201    /// Whether the record can be written back as NBT by this API.
202    pub writable_nbt: bool,
203}
204
205/// Detailed query result for one chunk.
206#[derive(Debug, Clone, PartialEq)]
207pub struct ChunkDetail {
208    /// Chunk position queried for this detail result.
209    pub pos: ChunkPos,
210    /// Records included in this result.
211    pub records: Vec<ChunkRecordDetail>,
212}
213
214/// Block/tip information for one map coordinate.
215#[derive(Debug, Clone, PartialEq)]
216pub struct BlockTip {
217    /// World block position for the queried map coordinate.
218    pub block: BlockPos,
219    /// Chunk containing the queried block.
220    pub chunk: ChunkPos,
221    /// Local X coordinate within the chunk, in the range 0..16.
222    pub local_x: u8,
223    /// Local Z coordinate within the chunk, in the range 0..16.
224    pub local_z: u8,
225    /// Surface-column sample for the queried block, when available.
226    pub surface: Option<SurfaceColumn>,
227    /// Biome id associated with the sampled column.
228    pub biome_id: Option<u32>,
229    /// Height in pixels or blocks, depending on the surrounding type.
230    pub height: Option<i16>,
231    /// Whether the chunk is a Bedrock slime chunk.
232    pub is_slime_chunk: bool,
233}
234
235/// Entity marker shown by map overlays.
236#[derive(Debug, Clone, PartialEq)]
237pub struct EntityOverlay {
238    /// Entity identifier decoded from NBT, when present.
239    pub identifier: Option<String>,
240    /// World position `[x, y, z]` decoded from the entity record.
241    pub position: [f64; 3],
242    /// Chunk containing the entity position.
243    pub chunk: ChunkPos,
244    /// Original or parsed Bedrock NBT payload.
245    pub nbt: NbtTag,
246}
247
248/// Block entity marker shown by map overlays.
249#[derive(Debug, Clone, PartialEq)]
250pub struct BlockEntityOverlay {
251    /// Identifier value decoded from storage or NBT.
252    pub id: Option<String>,
253    /// World block position `[x, y, z]` decoded from the block entity.
254    pub position: [i32; 3],
255    /// Chunk containing the block entity position.
256    pub chunk: ChunkPos,
257    /// Original or parsed Bedrock NBT payload.
258    pub nbt: NbtTag,
259}
260
261/// Pending tick marker shown by map overlays.
262#[derive(Debug, Clone, PartialEq)]
263pub struct PendingTickOverlay {
264    /// Chunk containing the pending tick record.
265    pub chunk: ChunkPos,
266    /// Original parsed Bedrock NBT payload.
267    pub tick: NbtTag,
268}
269
270/// Hardcoded spawn area overlay.
271#[derive(Debug, Clone, PartialEq, Eq)]
272pub struct HardcodedSpawnAreaOverlay {
273    /// Parsed hardcoded spawn area.
274    pub area: ParsedHardcodedSpawnArea,
275    /// Chunk containing the hardcoded spawn area anchor.
276    pub chunk: ChunkPos,
277}
278
279/// Village overlay. Bounds are best-effort because village NBT shapes vary by version.
280#[derive(Debug, Clone, PartialEq)]
281pub struct VillageOverlay {
282    /// Decoded storage key for this record.
283    pub key: crate::ParsedVillageKey,
284    /// Inclusive chunk bounds for this value.
285    pub bounds: Option<SlimeChunkBounds>,
286    /// Number of NBT roots decoded from the value.
287    pub root_count: usize,
288    /// Length of the original raw value in bytes.
289    pub raw_len: usize,
290}
291
292/// Reusable village overlay index for map viewers.
293#[derive(Debug, Clone, PartialEq)]
294pub struct VillageOverlayIndex {
295    /// Whether village records are included.
296    pub villages: Vec<VillageOverlay>,
297}
298
299impl VillageOverlayIndex {
300    /// Builds a reusable village overlay index on the calling thread.
301    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    /// Returns village overlays intersecting the requested bounds, capped by `max_items`.
319    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/// Overlay query result for a map region.
334#[derive(Debug, Clone, PartialEq)]
335pub struct RegionOverlayQuery {
336    /// Inclusive chunk bounds for this value.
337    pub bounds: SlimeChunkBounds,
338    /// Slime chunk positions in the queried region.
339    pub slime_chunks: Vec<ChunkPos>,
340    /// Hardcoded spawn area overlays in the queried region.
341    pub hardcoded_spawn_areas: Vec<HardcodedSpawnAreaOverlay>,
342    /// Parsed entity records included in this value.
343    pub entities: Vec<EntityOverlay>,
344    /// Whether block-entity records are loaded with render data.
345    pub block_entities: Vec<BlockEntityOverlay>,
346    /// Pending tick records in the queried region.
347    pub pending_ticks: Vec<PendingTickOverlay>,
348    /// Whether village records are included.
349    pub villages: Vec<VillageOverlay>,
350    /// Number of chunks scanned for this query.
351    pub scanned_chunks: usize,
352    /// Number of expected chunks missing from storage.
353    pub missing_chunks: usize,
354}
355
356/// Query options with hard limits for interactive map use.
357#[derive(Debug, Clone, Copy, PartialEq, Eq)]
358pub struct RegionOverlayQueryOptions {
359    /// Whether slime chunk overlays are included.
360    pub include_slime: bool,
361    /// Whether hardcoded spawn areas are included.
362    pub include_hardcoded_spawn_areas: bool,
363    /// Whether entity overlays are included.
364    pub include_entities: bool,
365    /// Whether block-entity overlays are included.
366    pub include_block_entities: bool,
367    /// Whether pending tick overlays are included.
368    pub include_pending_ticks: bool,
369    /// Whether village overlays are included.
370    pub include_villages: bool,
371    /// Maximum chunks accepted for this query.
372    pub max_chunks: usize,
373    /// Maximum overlay items returned for each item kind.
374    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/// Aggregate statistics for a selected chunk area.
393#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
394pub struct SelectionStats {
395    /// Inclusive chunk bounds for this value.
396    pub bounds: Option<SlimeChunkBounds>,
397    /// Number of chunks represented by these bounds.
398    pub chunk_count: usize,
399    /// Number of chunks with renderable data loaded.
400    pub loaded_chunks: usize,
401    /// Number of expected chunks missing from storage.
402    pub missing_chunks: usize,
403    /// Slime chunk positions in the queried region.
404    pub slime_chunks: usize,
405    /// Number of entity overlays found in the selection.
406    pub entity_count: usize,
407    /// Number of block entity overlays found in the selection.
408    pub block_entity_count: usize,
409    /// Number of pending tick overlays found in the selection.
410    pub pending_tick_count: usize,
411    /// Number of hardcoded spawn area overlays found in the selection.
412    pub hardcoded_spawn_area_count: usize,
413    /// Number of village overlays found in the selection.
414    pub village_count: usize,
415}
416
417/// Explicit write guard required by mutating query APIs.
418#[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    /// Creates a confirmed write guard for a specific world path and operation.
428    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    /// Validates that this guard authorizes writes to `world`.
437    ///
438    /// # Errors
439    ///
440    /// Returns validation errors for an unconfirmed operation or mismatched world path.
441    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]
462/// Is bedrock slime chunk.
463pub 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]
469/// Is slime chunk.
470pub fn is_slime_chunk(pos: ChunkPos) -> bool {
471    pos.dimension == Dimension::Overworld && is_bedrock_slime_chunk(pos.x, pos.z)
472}
473
474/// Query slime chunk windows.
475pub 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
548/// Query block tip blocking.
549pub 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
580/// Query chunk detail blocking.
581pub 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
599/// Reads selected record kinds for many chunks with exact-key batches.
600pub 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
611/// Reads selected record kinds for many chunks with cancellation support.
612pub 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
624/// Hashes selected chunk record kinds without decoding their NBT payloads.
625///
626/// This follows the same exact-key and actor-digest resolution path as
627/// [`query_chunk_records_many_blocking`], but only returns a compact
628/// XXH3-128 fingerprint per input chunk.
629pub 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
640/// Hashes selected chunk record kinds with cancellation support.
641pub 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
956/// Query region overlays blocking.
957pub 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
968/// Query region overlays blocking with control.
969pub 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
1133/// Query selection stats blocking.
1134pub 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
1157/// Delete chunks blocking.
1158pub 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
1184/// Deletes an arbitrary set of chunk positions in one atomic storage batch.
1185///
1186/// Duplicate positions are ignored.
1187///
1188/// # Errors
1189///
1190/// Returns validation, storage, or actor-digest parse errors.
1191pub 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
1210/// Write chunk record nbt blocking.
1211pub 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}