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