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    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/// Inclusive chunk bounds used by professional map queries.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28pub struct SlimeChunkBounds {
29    /// Bedrock dimension queried by these bounds.
30    pub dimension: Dimension,
31    /// Inclusive minimum chunk X coordinate.
32    pub min_chunk_x: i32,
33    /// Inclusive maximum chunk X coordinate.
34    pub max_chunk_x: i32,
35    /// Inclusive minimum chunk Z coordinate.
36    pub min_chunk_z: i32,
37    /// Inclusive maximum chunk Z coordinate.
38    pub max_chunk_z: i32,
39}
40
41impl SlimeChunkBounds {
42    /// Validates this value and returns a typed error on failure.
43    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    /// Returns the number of chunks covered by these inclusive bounds.
55    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    /// Converts generic chunk bounds into slime-query bounds.
63    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    /// Returns the midpoint chunk X/Z coordinates for these inclusive bounds.
75    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/// Supported square windows for slime-farm candidate queries.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
97pub struct SlimeWindowSize(u8);
98
99impl SlimeWindowSize {
100    /// Creates a new value.
101    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    /// Returns the value at the requested coordinates.
112    pub const fn get(self) -> u8 {
113        self.0
114    }
115}
116
117/// Ranked slime chunk window candidate.
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119pub struct SlimeChunkWindow {
120    /// Center chunk for this candidate window.
121    pub center: ChunkPos,
122    /// Inclusive minimum chunk X coordinate.
123    pub min_chunk_x: i32,
124    /// Inclusive maximum chunk X coordinate.
125    pub max_chunk_x: i32,
126    /// Inclusive minimum chunk Z coordinate.
127    pub min_chunk_z: i32,
128    /// Inclusive maximum chunk Z coordinate.
129    pub max_chunk_z: i32,
130    /// Number of slime chunks inside the window.
131    pub slime_count: usize,
132    /// Total number of chunks inside the window.
133    pub total_count: usize,
134}
135
136/// One raw chunk record exposed through the query API.
137#[derive(Debug, Clone, PartialEq)]
138pub struct ChunkRecordDetail {
139    /// Bedrock chunk record tag for this value.
140    pub tag: ChunkRecordTag,
141    /// Length of the original storage value in bytes.
142    pub raw_value_len: usize,
143    /// Consecutive Bedrock NBT roots decoded from the value.
144    pub roots: Vec<NbtTag>,
145    /// Whether the record can be written back as NBT by this API.
146    pub writable_nbt: bool,
147}
148
149/// Detailed query result for one chunk.
150#[derive(Debug, Clone, PartialEq)]
151pub struct ChunkDetail {
152    /// Chunk position queried for this detail result.
153    pub pos: ChunkPos,
154    /// Records included in this result.
155    pub records: Vec<ChunkRecordDetail>,
156}
157
158/// Block/tip information for one map coordinate.
159#[derive(Debug, Clone, PartialEq)]
160pub struct BlockTip {
161    /// World block position for the queried map coordinate.
162    pub block: BlockPos,
163    /// Chunk containing the queried block.
164    pub chunk: ChunkPos,
165    /// Local X coordinate within the chunk, in the range 0..16.
166    pub local_x: u8,
167    /// Local Z coordinate within the chunk, in the range 0..16.
168    pub local_z: u8,
169    /// Surface-column sample for the queried block, when available.
170    pub surface: Option<SurfaceColumn>,
171    /// Biome id associated with the sampled column.
172    pub biome_id: Option<u32>,
173    /// Height in pixels or blocks, depending on the surrounding type.
174    pub height: Option<i16>,
175    /// Whether the chunk is a Bedrock slime chunk.
176    pub is_slime_chunk: bool,
177}
178
179/// Entity marker shown by map overlays.
180#[derive(Debug, Clone, PartialEq)]
181pub struct EntityOverlay {
182    /// Entity identifier decoded from NBT, when present.
183    pub identifier: Option<String>,
184    /// World position `[x, y, z]` decoded from the entity record.
185    pub position: [f64; 3],
186    /// Chunk containing the entity position.
187    pub chunk: ChunkPos,
188    /// Original or parsed Bedrock NBT payload.
189    pub nbt: NbtTag,
190}
191
192/// Block entity marker shown by map overlays.
193#[derive(Debug, Clone, PartialEq)]
194pub struct BlockEntityOverlay {
195    /// Identifier value decoded from storage or NBT.
196    pub id: Option<String>,
197    /// World block position `[x, y, z]` decoded from the block entity.
198    pub position: [i32; 3],
199    /// Chunk containing the block entity position.
200    pub chunk: ChunkPos,
201    /// Original or parsed Bedrock NBT payload.
202    pub nbt: NbtTag,
203}
204
205/// Hardcoded spawn area overlay.
206#[derive(Debug, Clone, PartialEq, Eq)]
207pub struct HardcodedSpawnAreaOverlay {
208    /// Parsed hardcoded spawn area.
209    pub area: ParsedHardcodedSpawnArea,
210    /// Chunk containing the hardcoded spawn area anchor.
211    pub chunk: ChunkPos,
212}
213
214/// Village overlay. Bounds are best-effort because village NBT shapes vary by version.
215#[derive(Debug, Clone, PartialEq)]
216pub struct VillageOverlay {
217    /// Decoded storage key for this record.
218    pub key: crate::ParsedVillageKey,
219    /// Inclusive chunk bounds for this value.
220    pub bounds: Option<SlimeChunkBounds>,
221    /// Number of NBT roots decoded from the value.
222    pub root_count: usize,
223    /// Length of the original raw value in bytes.
224    pub raw_len: usize,
225}
226
227/// Reusable village overlay index for map viewers.
228#[derive(Debug, Clone, PartialEq)]
229pub struct VillageOverlayIndex {
230    /// Whether village records are included.
231    pub villages: Vec<VillageOverlay>,
232}
233
234impl VillageOverlayIndex {
235    /// Builds a reusable village overlay index on the calling thread.
236    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    /// Returns village overlays intersecting the requested bounds, capped by `max_items`.
254    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/// Overlay query result for a map region.
269#[derive(Debug, Clone, PartialEq)]
270pub struct RegionOverlayQuery {
271    /// Inclusive chunk bounds for this value.
272    pub bounds: SlimeChunkBounds,
273    /// Slime chunk positions in the queried region.
274    pub slime_chunks: Vec<ChunkPos>,
275    /// Hardcoded spawn area overlays in the queried region.
276    pub hardcoded_spawn_areas: Vec<HardcodedSpawnAreaOverlay>,
277    /// Parsed entity records included in this value.
278    pub entities: Vec<EntityOverlay>,
279    /// Whether block-entity records are loaded with render data.
280    pub block_entities: Vec<BlockEntityOverlay>,
281    /// Whether village records are included.
282    pub villages: Vec<VillageOverlay>,
283    /// Number of chunks scanned for this query.
284    pub scanned_chunks: usize,
285    /// Number of expected chunks missing from storage.
286    pub missing_chunks: usize,
287}
288
289/// Query options with hard limits for interactive map use.
290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
291pub struct RegionOverlayQueryOptions {
292    /// Whether slime chunk overlays are included.
293    pub include_slime: bool,
294    /// Whether hardcoded spawn areas are included.
295    pub include_hardcoded_spawn_areas: bool,
296    /// Whether entity overlays are included.
297    pub include_entities: bool,
298    /// Whether block-entity overlays are included.
299    pub include_block_entities: bool,
300    /// Whether village overlays are included.
301    pub include_villages: bool,
302    /// Maximum chunks accepted for this query.
303    pub max_chunks: usize,
304    /// Maximum overlay items returned for each item kind.
305    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/// Aggregate statistics for a selected chunk area.
323#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
324pub struct SelectionStats {
325    /// Inclusive chunk bounds for this value.
326    pub bounds: Option<SlimeChunkBounds>,
327    /// Number of chunks represented by these bounds.
328    pub chunk_count: usize,
329    /// Number of chunks with renderable data loaded.
330    pub loaded_chunks: usize,
331    /// Number of expected chunks missing from storage.
332    pub missing_chunks: usize,
333    /// Slime chunk positions in the queried region.
334    pub slime_chunks: usize,
335    /// Number of entity overlays found in the selection.
336    pub entity_count: usize,
337    /// Number of block entity overlays found in the selection.
338    pub block_entity_count: usize,
339    /// Number of hardcoded spawn area overlays found in the selection.
340    pub hardcoded_spawn_area_count: usize,
341    /// Number of village overlays found in the selection.
342    pub village_count: usize,
343}
344
345/// Explicit write guard required by mutating query APIs.
346#[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    /// Creates a confirmed write guard for a specific world path and operation.
356    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]
385/// Is bedrock slime chunk.
386pub 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]
392/// Is slime chunk.
393pub fn is_slime_chunk(pos: ChunkPos) -> bool {
394    pos.dimension == Dimension::Overworld && is_bedrock_slime_chunk(pos.x, pos.z)
395}
396
397/// Query slime chunk windows.
398pub 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
471/// Query block tip blocking.
472pub 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
503/// Query chunk detail blocking.
504pub 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
522/// Query region overlays blocking.
523pub 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
534/// Query region overlays blocking with control.
535pub 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
678/// Query selection stats blocking.
679pub 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
701/// Delete chunks blocking.
702pub 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
731/// Write chunk record nbt blocking.
732pub 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}